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之间传输的字节数。sendfile成功时返回传输的字节数,失败则返回-1并设置errno。该函数的man手册明确指出,in_fd必须是一个支持mmap函数的文件描述符,即它必须指向真实的文件,而不能是socket和管道;则out_fd则必须是一个socket。由此可见,sendfile几乎是专门为在网络上传输文件而设计的。
下面的例子利用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_address port_number 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 { sendfile( connfd, filefd, NULL, stat_buf.st_size ); close( connfd ); } close( sock ); return 0; }
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)
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如下图
下面的例子是利用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_address port_number\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 { int pipefd[2]; assert( ret != -1 ); ret = pipe( pipefd ); ret = splice( connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); assert( ret != -1 ); ret = splice( pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); assert( ret != -1 ); close( connfd ); } close( sock ); return 0; }
我们通过splice函数将客户端的内容读取到pipefd[1]中,然后在使用splice函数从pipefd[0]中读出该内容到客户端,从而实现了简单高效的回射服务。整个过程未执行recv/send操作,因此也未涉及用户空间和内核空间之间的拷贝。
tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。函数原型如下:
#include<fcntl.h> ssize_t tee(int fd_in ,int fd_out,size_t len ,unsigned int flags);
该函数的参数的含义以splice相同(但fd_in 和fd_out都必须是管道文件描述符)。tee函数成功时返回在两个文件描述符之间复制的数据数量(字节数)。返回0表示没有复制任何数据,tee失败时返回-1并设置errno。
如下代码利用tee函数和splice函数,实现了linux下的tee程序(同时输出数据到终端和文件的程序)
#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 ); //close( STDIN_FILENO ); // dup2( pipefd_stdout[1], STDIN_FILENO ); //write( pipefd_stdout[1], "abc\n", 4 ); ret = splice( STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); assert( ret != -1 ); ret = tee( pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK ); assert( ret != -1 ); ret = splice( pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE ); assert( ret != -1 ); ret = splice( pipefd_stdout[0], NULL, STDOUT_FILENO, 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; }