《Linux高性能服务器编程》学习总结(六)——高级I/O函数

第六章      高级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: [email protected]
 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: [email protected]
 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: [email protected]
 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: [email protected]
 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: [email protected]
 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来查看。

原文地址:https://www.cnblogs.com/Torrance/p/8407253.html

时间: 2024-10-30 00:14:49

《Linux高性能服务器编程》学习总结(六)——高级I/O函数的相关文章

Linux 高性能服务器编程——高级I/O函数

重定向dup和dup2函数 [cpp] view plaincopyprint? #include <unistd.h> int dup(int file_descriptor); int dup2(int file_descriptor_one, int file_descriptor_two); dup创建一个新的文件描述符, 此描述符和原有的file_descriptor指向相同的文件.管道或者网络连接. dup返回的文件描述符总是取系统当前可用的最小整数值. dup2函数通过使用参数f

Linux高性能服务器编程——高级I/O函数

 高级I/O函数 pipe函数 pipe函数用于创建一个管道,实现进程间的通信. #include <unistd.h> int pipe(int pipefd[2]); 通过pipe函数创建的文件描述符fd[0]和fd[1]分别构成管道的两端,往fd[1]写入的数据可以从fd[0]读出,不能反过来.管道内部传输的数据时字节流,和TCP字节流概念相同,但有区别,管道本身拥有一个容量限制,它规定如果应用程序不将数据从管道读走的话,该管道最多能被写入多少字节的数据.管道容量阿东小默认是6553

linux高性能服务器编程

<Linux高性能服务器编程>:当当网.亚马逊 目录: 第一章:tcp/ip协议族 第二章:ip协议族 第三章:tcp协议详解 第四章:tcp/ip通信案例:访问Internet 第五章:linux网络编程基础API 第六章:高级IO函数 第七章:linux服务器程序规范 第八章:高性能服务器框架 第九章:IO复用 第十章:信号 第十一章:定时器 第十二章:高性能IO框架库libevent 第十三章:多进程编程 第十四章:多线程编程 第十五章:进程池和线程池 第十六章:服务器调制.调试和测试

Linux高性能服务器编程——定时器

 定时器 服务器程序通常管理着众多定时事件,因此有效组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响.位置我们要将每个定时事件封装成定时器,并使用某种容器类型的数据结构,比如链表.排序链表和时间轮将所有定时器串联起来,以实现对定时事件的统一管理. Linux提供三种定时方法: 1.socket选项SO_RECVTIMEO和SO_SNDTIMEO. 2.SIGALRM信号 3.I/O复用系统调用的超时参数 socket选项SO_RCVTI

Linux高性能服务器编程——信号及应用

 信号 信号是由用户.系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常.Linux信号可由如下条件产生: 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号.比如输入Ctrl+C通常会给进程发送一个终端信号. 2.系统异常 系统状态变化 运行kill命令或调用kill函数 Linux信号概述 发送信号 Linux下,一个进程给其他进程发送信号的API是kill函数.其定义如下: #include <sys/types.h> #include <sign

Linux高性能服务器编程——多进程编程

多进程编程 多进程编程包括如下内容: 复制进程影映像的fork系统调用和替换进程映像的exec系列系统调用. 僵尸进程以及如何避免僵尸进程 进程间通信(Inter-Process Communication,IPC)最简单的方式:管道 3种进程间通信方式:信号量,消息队列和共享内存 fork系统调用 #include<unistd.h> pid_tfork(void); 该函数的每次都用都返回两次,在父进程中返回的是子进程的PID,在子进程中返回的是0.该返回值是后续代码判断当前进程是父进程还

Linux高性能服务器编程——系统检测工具

系统检测工具 tcpdump tcpdump是一款经典的转包工具,tcpdump给使用者提供了大量的选项,泳衣过滤数据报或者定制输出格式. lsof lsof是一个列出当前系统打开的文件描述符的工具.通过它我们可以了解感兴趣的进程打开了哪些文件描述符,或者我们感兴趣的文件描述符被哪些进程打卡了. nc nc命令主要被用来快速构建网络连接.我们可以让它以服务器方式运行,监听某个端口并接收客户连接,因此它可用来调试客户端程序.我们也可以使之以客户端方式运行,向服务器发起连接并收发数据,因此它可以用来

Linux高性能服务器编程——进程池和线程池

进程池和线程池 池的概念 由于服务器的硬件资源"充裕",那么提高服务器性能的一个很直接的方法就是以空间换时间,即"浪费"服务器的硬件资源,以换取其运行效率.这就是池的概念.池是一组资源的集合,这组资源在服务器启动之初就完全被创建并初始化,这称为静态资源分配.当服务器进入正是运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配.很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的.当

Linux高性能服务器编程——I/O复用

 IO复用 I/O复用使得程序能同时监听多个文件描述符,通常网络程序在下列情况下需要使用I/O复用技术: 客户端程序要同时处理多个socket 客户端程序要同时处理用户输入和网络连接 TCP服务器要同时处理监听socket和连接socket,这是I/O复用使用最多的场合 服务器要同时处理TCP请求和UDP请求.比如本章将要讨论的会社服务器 服务器要同时监听多个端口,或者处理多种服务. I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的.并且当多个文件描述符同时就绪时,如果不采用额外措施

Linux高性能服务器编程——多线程编程(上)

多线程编程 Linux线程概述 线程模型 线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体.根据运行环境和调度者的身份,线程可分为内核线程和用户线程.内核线程,在有的系统上也称为LWP(Light Weigth Process,轻量级进程),运行在内核空间,由内核来调度:用户线程运行在用户空间,由线程库来调度.当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程.可见,内核线程相当于用于线程运行的容器.一个进程可以拥有M个内核线程和N个用户线程,其中M≤N.并且在一