第六章 高级I/O函数
网络I/O一直是Linux网络编程中极其重要的一部分,除了前面讲到的send、recv等,socket编程接口还给出了很多高级了I/O函数,这些函数大致分为三类:用于创建文件描述符的函数、用于读写控制的函数和用于控制I/O行为和属性的函数。
pipe函数是用来创建一个管道,管道是较为原始的进程间通信手段,分为无名管道和有名管道,而无名管道只能用于有亲缘关系的进程之间传递消息。pipe建立的管道是单工的,其参数是一个包含两个元素的整形数组fd[2],创建成功后fd[0]代表管道可读的一端,fd[1]代表可写的一端,这两个的本质都是文件描述符,当进程间有数据要传输时,数据发送的一端需要关闭fd[0],接收端要关闭fd[1],才能正常传送数据。需要注意的是无名管道只能用低级文件编程库中的读写函数进行操作,如read和write,当我们向一个空管道执行read时,函数会阻塞,直到有数据写入才继续执行,同理对满的管道执行write也会进入阻塞状态。但是如果对于这两个文件描述符设置为非阻塞模式,则他们会有不同的行为。如果fd[1]的引用计数减少至0,即没有写端进程向管道中写,则fd[0]上的read操作将会读取到EOF标志,返回0;反之如果fd[0]上的引用计数减少至0,即没有读端程序调用read,则此时fd[1]上的write操作将失败并引发SIGPIPE信号。为了便于使用,API中还有一个函数用来创建双向管道,是socketpair函数,使用这个函数创建的双向管道只能使用AF_UNIX协议,即UNIX本地域协议族,它创建的两个文件描述符是既可读又可写的。
dup函数和dup2函数用于复制文件描述符,区别在于dup函数是将一个文件描述符复制到当前系统可用的最小整数值,而dup2则是不小于其参数的最小整数值,注意,通过这两个函数复制的文件描述符不继承其原来的属性。我们来看一个CGI服务器的例子:
1 /************************************************************************* 2 > File Name: 6-1.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Thu 01 Feb 2018 11:29:09 PM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char **argv) { 12 if(argc <= 2) { 13 printf("usage: %s ip_address port_number\n", basename(argv[0])); 14 return 1; 15 } 16 const char* ip = argv[1]; 17 int port = atoi(argv[2]); 18 struct sockaddr_in address; 19 bzero(&address, sizeof(address)); 20 address.sin_family = AF_INET; 21 inet_pton(AF_INET, ip, &address.sin_addr); 22 address.sin_port = htons(port); 23 24 int sock = socket(AF_INET, SOCK_STREAM, 0); 25 assert(sock >= 0); 26 27 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 28 assert(ret != -1); 29 30 ret = listen(sock, 5); 31 assert(ret != -1); 32 33 struct sockaddr_in client; 34 socklen_t client_addrlength = sizeof(client); 35 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 36 if(connfd < 0) { 37 printf("errno is: %d\n", errno); 38 } 39 else { 40 close(STDOUT_FILENO); 41 int newfd = dup(connfd); 42 printf("abcd\n"); 43 close(connfd); 44 } 45 close(sock); 46 return 0; 47 }
使用telnet客户端连接服务器发现有abcd的回显,通过这个例子我们可以看到,我们关闭了标准输出文件描述符后再调用dup,会将要复制的connfd复制到当前未使用的最小的文件描述符也就是标准输出文件描述符上,实现了输出的重定向。
readv和writev函数和前面提过的readmsg和writemsg函数类似,也是用来对数据的集中写和分散读,相当于前面两个函数的简化版。举一个例子来说明,在Web服务器解析完HTTP请求后如果客户端请求的文件存在并且有权限时,就需要返回一个HTTP首部状态码和状态信息,然后再返回该文件,但是我们考虑效率问题,如果每次我们都需要将两个不相关的存储空间合并到一起再发送势必会很影响效率,所以我们可以事先将HTTP不同的头部存储好,找到文件后使用sendv函数直接发送即可。我们建立一个test.txt文件模拟一下,服务器代码如下:
1 /************************************************************************* 2 > File Name: 6-2.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 01:30:46 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 #define BUFFER_SIZE 1024 12 //用于定义HTTP的成功和失败的状态码和状态信息 13 static const char* status_line[2] = {"200 OK", "500 Internal server error"}; 14 15 int main(int argc, char **argv) { 16 if(argc <= 3) { 17 printf("usage: %s ip_address port_number filename\n", basename(argv[0])); 18 return 1; 19 } 20 const char* ip = argv[1]; 21 int port = atoi(argv[2]); 22 const char* file_name = argv[3]; 23 24 struct sockaddr_in address; 25 bzero(&address, sizeof(address)); 26 address.sin_family = AF_INET; 27 address.sin_port = htons(port); 28 inet_pton(AF_INET, ip, &address.sin_addr); 29 30 int sock = socket(AF_INET, SOCK_STREAM, 0); 31 assert(sock >= 0); 32 33 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 34 assert(ret != -1); 35 36 ret = listen(sock, 5); 37 assert(ret != -1); 38 39 struct sockaddr_in client; 40 socklen_t client_addrlength = sizeof(client); 41 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 42 if(connfd < 0) { 43 printf("errno is: %d\n", errno); 44 } 45 else { 46 char header_buf[BUFFER_SIZE]; 47 memset(header_buf, 0, sizeof(header_buf)); 48 char *file_buf; 49 struct stat file_stat; //用于获取文件属性的结构体 50 bool file_is_valid = true; 51 int len = 0; 52 if(stat(file_name, &file_stat) < 0) { //获取文件信息 53 file_is_valid = false; 54 } 55 else { 56 if(S_ISDIR(file_stat.st_mode)) { //如果是目录 57 file_is_valid = false; 58 } 59 else if(file_stat.st_mode & S_IROTH) { //如果当前用户对文件有读的权限 60 int fd = open(file_name, O_RDONLY); 61 file_buf = new char[file_stat.st_size + 1]; 62 memset(file_buf, 0, sizeof(file_buf)); 63 if(read(fd, file_buf, file_stat.st_size + 1) < 0) { 64 file_is_valid = false; 65 } 66 } 67 else file_is_valid = false; 68 //如果文件合法则返回正确的状态信息以及文件内容,否则返回错误 69 if(file_is_valid) { 70 ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[0]); 71 len += ret; 72 ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "Content-Length: %d\r\n", (int)file_stat.st_size); 73 len += ret; 74 ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n"); 75 //将头部信息和文件信息装入不同的iovec中调用writev集中写 76 struct iovec iv[2]; 77 iv[0].iov_base = header_buf; 78 iv[0].iov_len = strlen(header_buf); 79 iv[1].iov_base = file_buf; 80 iv[1].iov_len = strlen(file_buf); 81 ret = writev(connfd, iv, 2); 82 } 83 else { 84 ret = snprintf(header_buf, BUFFER_SIZE - 1, "%s %s\r\n", "HTTP/1.1", status_line[1]); 85 len += ret; 86 ret = snprintf(header_buf + len, BUFFER_SIZE - 1 - len, "%s", "\r\n"); 87 send(connfd, header_buf, strlen(header_buf), 0); 88 } 89 } 90 close(connfd); 91 delete []file_buf; 92 } 93 close(sock); 94 return 0; 95 }
使用telnet连接连接服务器端,发现正常回显了HTTP首部和数据。
接下来还有一个较为常用的函数sendfile,它的作用是在两个文件描述符之间直接传递数据,是一个零拷贝函数。所谓零拷贝函数,首先要知道内核空间和用户空间的概念和区别。Linux操作系统的内核使用了内存中的低地址区域,这里是我们在编程时不能访问的,很多缓冲区都是在这里定义,而用户空间就是其余的内存空间,我们在编写程序时可以进行操作。平时我们调用recv函数会将网络I/O数据拷贝到定义的用户缓冲区内,这样就会在内核空间和用户空间之间进行数据拷贝,这样就会导致进程再内核态和用户态之间进行频繁转换,降低效率。而零拷贝函数可以直接在内核态完成数据的传递,效率较高。其函数原型如下:
1 #include<sys/sendfile.h> 2 ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);
值得注意的是,out_fd是待写入的文件描述符,必须是一个socket,而in_fd是待读出的文件描述符,但它必须指向真实的文件,不能使管道或者socket。我们用一个例子来看一下:
1 /************************************************************************* 2 > File Name: 6-3.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 03:25:03 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char** argv) { 12 if(argc <= 3) { 13 printf("usage: %s ip_address port_number file_name", basename(argv[0])); 14 return 1; 15 } 16 const char* ip = argv[1]; 17 int port = atoi(argv[2]); 18 const char* file_name = argv[3]; 19 20 int filefd = open(file_name, O_RDONLY); 21 assert(filefd > 0); 22 struct stat stat_buf; 23 fstat(filefd, &stat_buf); 24 25 struct sockaddr_in address; 26 bzero(&address, sizeof(address)); 27 address.sin_family = AF_INET; 28 inet_pton(AF_INET, ip, &address.sin_addr); 29 address.sin_port = htons(port); 30 31 int sock = socket(AF_INET, SOCK_STREAM, 0); 32 assert(sock >= 0); 33 34 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 35 assert(ret != -1); 36 37 ret = listen(sock, 5); 38 assert(ret != -1); 39 40 struct sockaddr_in client; 41 socklen_t client_addrlength = sizeof(client); 42 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 43 if(connfd < 0) { 44 printf("errno is: %d\n", errno); 45 } 46 else { 47 sendfile(connfd, filefd, NULL, stat_buf.st_size); 48 close(connfd); 49 } 50 close(sock); 51 return 0; 52 }
在上例中未在用户空间内申请任何缓冲区即完成了文件的传送,效率要比原始做法高得多。
splice函数用来在两个文件描述符间移动数据,也是零拷贝操作,但是其in_fd和out_fd中必须至少有一个管道文件描述符,调用成功时返回一共转移的字节数,以一个splice实现的简单回射服务器为例:
1 /************************************************************************* 2 > File Name: 6-4.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 03:45:40 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char** argv) { 12 if(argc <= 2) { 13 printf("usage: %s ip_address port_number\n", basename(argv[0])); 14 return 1; 15 } 16 const char* ip = argv[1]; 17 int port = atoi(argv[2]); 18 19 struct sockaddr_in address; 20 bzero(&address, sizeof(address)); 21 address.sin_family = AF_INET; 22 inet_pton(AF_INET, ip, &address.sin_addr); 23 address.sin_port = htons(port); 24 25 int sock = socket(AF_INET, SOCK_STREAM, 0); 26 assert(sock >= 0); 27 28 int ret = bind(sock, (struct sockaddr*)&address, sizeof(address)); 29 assert(ret != -1); 30 31 ret = listen(sock, 5); 32 assert(ret != -1); 33 34 struct sockaddr_in client; 35 socklen_t client_addrlength = sizeof(client); 36 int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength); 37 if(connfd < 0) { 38 printf("errno is: %d\n", errno); 39 } 40 else { 41 int pipefd[2]; 42 assert(ret != -1); 43 ret = pipe(pipefd); 44 ret = splice(connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MOVE); 45 assert(ret != -1); 46 ret = splice(pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MOVE); 47 assert(ret != -1); 48 close(connfd); 49 } 50 close(sock); 51 return 0; 52 }
由于我们不能将数据从connfd的输入直接变成connfd的输出,所以我们借助了一个管道,将connfd的输入与管道的输入连接,将管道的输出与connfd的回射连接,这样就做成了一个高效率的回射服务器。
tee函数是在两个管道文件描述符之间复制数据,也是零拷贝操作,而它不消耗数据,原始数据仍可以用于后续操作,函数原型与返回值与splice类似,我们以一个可以同时输出数据到终端和文件的程序为例:
1 /************************************************************************* 2 > File Name: 6-5.cpp 3 > Author: Torrance_ZHANG 4 > Mail: 597156711@qq.com 5 > Created Time: Fri 02 Feb 2018 04:28:26 AM PST 6 ************************************************************************/ 7 8 #include"head.h" 9 using namespace std; 10 11 int main(int argc, char** argv) { 12 if(argc != 2) { 13 printf("usage: %s <file>\n", basename(argv[0])); 14 return 1; 15 } 16 int filefd = open(argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666); 17 assert(filefd > 0); 18 19 int pipefd_stdout[2]; 20 int ret = pipe(pipefd_stdout); 21 assert(ret != -1); 22 23 int pipefd_file[2]; 24 ret = pipe(pipefd_file); 25 assert(ret != -1); 26 27 //将标准输入重定向到管道pipefd_stdout中 28 ret = splice(STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE); 29 assert(ret != -1); 30 31 //将pipefd_stdout中的数据拷贝一份到pipefd_file中 32 ret = tee(pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK); 33 assert(ret != -1); 34 35 //分别将两个管道的输出端和标准输出文件与创建的文件相连接 36 ret = splice(pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE); 37 assert(ret != -1); 38 39 ret = splice(pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MOVE); 40 assert(ret != -1); 41 42 close(filefd); 43 close(pipefd_file[0]); 44 close(pipefd_file[1]); 45 close(pipefd_stdout[0]); 46 close(pipefd_stdout[1]); 47 return 0; 48 }
此程序有一个问题,使用splice将pipefd_stdout[0]连接到STDOUT_FILENO时出错,errno的值为EINVAL,在网上查了好久资料都没有收获,简单说一下我的看法:返回EINVAL的原因主要有四种,目标文件系统不支持splice,目标文件以追加方式打开,两个文件描述符都不是管道文件和某个offset参数被用于不支持随机访问的设备,而我们可以轻易排除1、3、4,标准输出文件默认应该是以追加方式打开的,这样在输出时才不会覆盖之前的数据,所以splice出错。
Linux提供了tee命令用于完成上述程序的操作,在tee函数的帮助文档里也有一个例子来完成上述操作,可用man 2 tee来查看。