Linux的哲学是一切皆文件,而操作文件是通过文件描述符来进行。本文梳理一下dup / dup2 / sendfile / splice/ tee函数对文件描述符的操作。
目录
1.dup
2.dup2
3.sendfile
4.splice
5.tee
1.dup
#include <unistd.h>
int dup(int fd);
复制一个现有的文件描述符,dup会返回一个新的描述符,这个描述一定是当前可用文件描述符中的最小值。我们知道,一般的0,1,2描述符分别被标准输入、输出、错误占用,所以在程序中如果close掉标准输出1后,调用dup函数,此时返回的描述符就是1。函数返回后fd和dup返回值指向同一个文件,只是文件描述符不同。
以下下代码演示将标准输出复制成普通文件:
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("usage: %s <file>\n", argv[0]);
return 1;
}
int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
assert(filefd > 0);
close(STDOUT_FILENO);
printf("=== %d\n", dup(filefd));
write(filefd, "hello world\n", 12);
close(filefd);
printf("dup函数,你好啊");
return 0;
}
运行结果:
2.dup2
#include <unistd.h>
int dup2(int fd, int fd2);
复制一个现有的文件描述符,用fd2指定新描述符的值,如果fd2本身已经打开了,则会先将其关闭。如果fd等于fd2,则返回fd2,并不关闭它。函数返回后fd和fd2指向同一个文件,只是文件描述符不同。
一下代码演示将标准输出复制成普通文件:
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("usage: %s <file>\n", argv[0]);
return 1;
}
int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
assert(filefd > 0);
write(filefd, "hello liudehua\n", 15);
printf("=== %d\n", dup2(filefd, STDOUT_FILENO));
write(filefd, "hello world\n", 12);
close(filefd);
return 0;
}
文件内容如下:
使用strace跟踪:
3.sendfile
#include<sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd , off_t* offset ,size_t count);
功能:在两个文件描述符之间传递数据(完全在内核中操作),从而避免内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这就是传说中的零拷贝。
参数:
in_fd:参数是待读出内容的文件描述符
out_fd:参数是待写入内容的文件描述符
offset:参数执行从读入文件流的哪个位置开始读,如果为空,则使用读入文件流的默认起始位置
count:参数指定在文件描述符in_fd和out_fd之间传输的字节数
返回值:成功时返回传输的字节数,失败则返回-1并设置errno
重点说明:该函数的man手册明确指出,in_fd必须是一个支持mmap函数的文件描述符,即它必须指向真实的文件,而不能是socket和管道,
而out_fd则必须是一个socket。所以sendfile几乎是专门为在网络上传输文件而设计的
以下测试代码编写一个简单的tcp服务器。通过sendfile向客户端发送文件的例子。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
int main( int argc, char* argv[] ) {
if( argc <= 3 ) {
printf("usage: %s ip port filename\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
const char* file_name = argv[3];
int filefd = open(file_name, O_RDONLY);
assert(filefd > 0);
struct stat stat_buf;
fstat(filefd, &stat_buf);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if (connfd < 0) {
printf( "errno is: %d\n", errno);
}
else {
off_t len = 0;
while (len < stat_buf.st_size) {
int ret = sendfile(connfd, filefd, &len, stat_buf.st_size - len);
//printf("ret value %d \n", ret);
if (-1 == ret) {
if (EAGAIN == errno) {
//printf("no data\n");
perror("sendfile");
}
else {
printf("client quit \n");
break;
}
}
}
close(connfd);
}
close(sock);
return 0;
}
使用nc测试,结果如下:
可以看到文件内容被nc程序正确接收。
4.splice
#include<fcntl.h>
ssize_t splice(int fd_in,loff_t* off_in,int fd_out,loff_t* off_out,size_t len ,unsigned int flags);
功能:splice函数用于在两个文件描述符之间移动数据,也是零拷贝
参数:
fd_in/off_in:fd_in为待输入数据的文件描述符, 如果fd_in是一个管道文件描述符,那么off_in参数必须设置为NULL。
如果fd_in不是一个管道文件(比如是一个socket),那么off_in表示从输入数据流的何处开始读取数据。此时,如果off_in被设置为NULL,则表示从输入数据流的当前偏移位置读入;
若off_in不为NULL,则它将指出具体的偏移位置。
fd_out/off_out参数的含义与fd_in/off_in相同,不过用于输出数据流。
len:移动数据的长度
flag:控制数据如何移动,它可以设置为下表中的某些值的按位或。
返回值:
使用splice函数时,fd_in 和fd_out必须至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量,它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据,而该管道没有被写入任何数据时。spice函数失败时返回-1并设置errno.常见的errno如下图
以下测试代码编写一个简单的tcp服务器,使用匿名管道再结合splice向客户端回显信息。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char* argv[]) {
if (argc <= 2) {
printf("usage: %s ip port\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock >= 0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if (connfd < 0) {
printf("errno is: %d\n", errno);
}
else {
//创建匿名队列,pipefd[0]读,pipefd[1]写
int pipefd[2];
assert(ret != -1);
ret = pipe(pipefd);
// connfd -> pipefd[1]
ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
// pipefd[0] -> connfd
ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
close(pipefd[0]);
close(pipefd[1]);
close(connfd);
}
close(sock);
return 0;
}
运行结果如下:
可以看到客户端输入的内容被正确回显。
5.tee
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);
功能:在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。
参数:
fd_in:待输入数据的文件描述符,必须是管道文件。
fd_out:待输出数据的文件描述符,必须是管道文件。
len:赋值的数据长度(字节数)
flags 修饰标志,跟splice(2)/vmsplice(2) 共享命名空间:
1)SPLICE_F_MOVE 当前对tee没有效果。
2)SPLICE_F_NONBLOCK 非阻塞的I/O操作,实际效果还会受文件描述符本身的阻塞状态的影响。
3)SPLICE_F_MORE当前对tee没有效果。
4)SPLICE_F_GIFT 对tee没有效果。
返回值:
成功时,返回两个文件描述符之间复制的数据量(字节数)。返回0表示没有复制任何数据,可能碰到EOF。失败时,返回-1,并设置errno。
如下代码显示从标准输入接收数据,通过管道将数据写入文件,可以看到tee(pipefd_stdout[0]调用两次,pipefd_stdout[0]中数据一直存在。
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("usage: %s <file>\n", argv[0]);
return 1;
}
int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666);
assert(filefd > 0);
int pipefd_stdout[2];
int ret = pipe(pipefd_stdout);
assert(ret != -1);
int pipefd_file[2];
ret = pipe(pipefd_file);
assert(ret != -1);
// 标准输入 STDIN_FILENO -> pipefd_stdout[1]
ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
// pipefd_stdout[0] -> pipefd_file[1]
ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK);
assert(ret != -1);
ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK);
assert(ret != -1);
// pipefd_file[0] -> filefd
ret = splice(pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
assert(ret != -1);
close(filefd);
close(pipefd_stdout[0]);
close(pipefd_stdout[1]);
close(pipefd_file[0]);
close(pipefd_file[1]);
return 0;
}
运行结果如下:
tee和splice的区别
tee类似于splice,都用于两个fd之间数据拷贝。区别在于:
1)对参数fd的要求
splice要求2个fd中至少必须有一个fd是管道文件;
tee要求两个fd都是管道文件。
2)对fd数据的消耗
splice是两个fd之间数据移动,splice会消耗fd数据;
tee是两个fd之间数据复制,tee不会消耗fd数据。
3)flags参数
Linux2.6.21以前,SPLICE_F_MOVE 对splice有效果,之后没效果。SPLICE_F_NONBLOCK 和SPLICE_F_MORE都对splice有效果;
只有SPLICE_F_NONBLOCK 才对tee有效果;