首先我们要明确的是在任何IO操作中,均包含两个步骤,等待和拷贝,而在实际的业务中,等待所消耗的时间往往大于拷贝的时间,因此,让IO操作更高效,核心的方法就是将等待的时间缩短,而这两个过程是在内核当中的过程。
低阶IO:是指类似于将用户输入的内容读取到某个变量中,将变量中的值打印在屏幕上等等,简单来说就是对C库自己所维护的缓冲区进行I/O操作。
高阶IO:通常应用于网络Socket编程,对UDP(TCP)所维护的发送缓冲区和接收缓冲区进行I/O操作。并且高阶IO分为同步IO和异步IO,同步IO又分为阻塞IO、非阻塞IO、信号驱动IO和多路转接IO。
信号驱动IO:当内核将数据准备好之后,或者说告诉应用进程何时才可以开始拷贝数据,会给应用进程发送一个SIGIO的信号,通知其进行IO操作。当应用程序接收到该信号之后,证明数据已经准备好了,接下来就会调用系统调用函数对其进行相应的IO操作。
由内核在数据拷贝完成时, 通知应用程序进行相关操作(而信号驱动是当内核中数据准备好了就通知应用程序)。
注:为了性能和效率的优先,C++默认采用的是异步IO的方式。
内核帮我们监控了多个文件描述符,当某一个或者若干个文件描述符就绪的时候,就会通知调用者,调用者调用系统调用函数针对就绪的文件描述符进行操作。
首先我们要知道多路复用函数的作用是什么,其本质上就是让内核帮助程序员监控多个文件描述符的IO事件,一旦监控的某个文件描述符对应的事件产生(IO就绪),就会通知调用者,也就是说可以并行的处理多条客户端的请求,换句话说就是实现了高并发。
作用:监控多个文件描述符,就绪之后,通知调用者。
select的函数原型如下: #include
int select(int nfds, fd_set * readfds, fd_set * writefds,fd_set * exceptfds, struct timeval timeout);
使用vim打开/usr/include/sys/select.h路径下查看源码:
内核在使用该数组的时候采用的是位图的方式,一共有16 * 8 * 8=1024个比特位
fd_set事件集合占用比特位的个数和宏_FD_SETSIZE强相关,即,_FDSETSIZE多大,fd_set事件集合就有多少个比特位。
其实fd_set结构就是一个整数数组,更严格的是,是一个“位图”,使用位图中对应的位来表示要监控的文件描述符,如下图所示:
提供了一组操作fd_set的接口, 来比较方便的操作位图:
从事件集合当中删除一个文件描述符
void FD_CLR(int fd, fd_set *set);
判断文件描述符是否在某一个事件集合当中
int FD_ISSET(int fd, fd_set *set);
设置文件描述符到事件集合当中
void FD_SET(int fd, fd_set *set);
清空事件集合,将所有的比特位全部置为0
我们使用select对0号文件描述符(读缓冲区)进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来。
1 #include<iostream>
2 #include<sys/select.h>
3 #include<unistd.h>
4 #include<stdio.h>
5 using namespace std;
6 int main()
7 {
8 fd_set readfds;
9
10 FD_ZERO(&readfds);
11 FD_SET(0,&readfds);
12
13 while(1)
14 {
15 int ret=select(1,&readfds,NULL,NULL,NULL);
16
17 if(ret<0)
18 {
19 perror("select");
20 return 0;
21 }
22
23 char buf[1024]={0};
24 read(0,buf,sizeof(buf)-1);
25
26 cout<<buf<<endl;
27 }
28 return 0;
29 }
前提:均是监控多个文件描述符,就绪之后,然后通知调用者。与select相比并不支持跨平台,与epoll相比,没有epoll的效率高。
- 想让poll监控多个文件描述符,只需要在定义事件结构数组的时候,多传递几个元素
- eg:struct pollfd arr[10];
arr[0].fd = 0;
arr[0].events = POLLIN;- nfds:事件结构数组中有效元素的个数
- timeout:
>0:带有超时时间,单位:秒
==0:非阻塞
<0:阻塞- 返回值:就绪文件描述符的个数
利用poll函数对系统的0号文件描述符(读缓冲区)进行监控,一旦监控到读的事件,则将其读入的内容打印到屏幕上。
1 #include<iostream>
2 #include<stdio.h>
3 #include<poll.h>
4 #include<unistd.h>
5
6 using namespace std;
7 int main()
8 {
9 struct pollfd pf;
10
11 pf.fd=0;
12 pf.events=POLLIN;
13
14 while(1)
15 {
16 int ret=poll(&pf,1,-1);
17 if(ret<0)
18 {
19 perror("poll");
20 }
21 else if(ret==0)
22 {
23 cout<<"TimeOut!"<<endl;
24 sleep(1);
25 continue;
26 }
27 char buf[1024]={0};
28 read(0,buf,sizeof(buf)-1);
29 cout<<buf<<endl;
30
31 }
32 return 0;
33 }
epoll函数是目前世界上公认在Linux下,多路转接监控效率最高的模型。
- size:自从Linux2.6.8之后,size参数是被忽略的;但不要传递小于0的数字
- 返回值:返回epoll的操作句柄
- epfd:epoll的操作句柄。
- events:时间结构数组(集合),从epoll当中获取就绪的事件结构。
- maxevents:最多一次获取多少个事件结构。
- timeout:
0:带有超时事件
==0:非阻塞
<0:阻塞- 返回值:就绪的文件描述符个数。
当某一个进程调用epoll_create函数时,LInux内核会创建一个eventpoll的结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
当调用epoll_create函数时,会在内核创建一个eventpoll结构体,在该结构体中有一个rdlist成员和rbr成员,它两分别是一个双向链表和红黑树,而调用epoll_ctl函数添加、修改、删除文件描述符对应的事件集合其实是对红黑树中的节点进行相应的添加、修改、删除操作,而所有添加到epoll的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当文件描述符准备就绪后,内核会回调ep_poll_callback函数,将准备就绪的事件集合添加到rdlist双向链表中,而当调用epoll_wait进行监控的时候,如果双向链表为空,则表明当前没有就绪的事件发生,如果不为空,则将双向链表中的内容复制到用户态,并返回将事件数量返回给用户。
【注意】:这里的双向链表其实实现的是一个队列,虽然是一个双向链表,但是他只支持先进先出(FIFO),是队列的特性。每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。
我们使用epoll对0号文件描述符进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来。
1 #include<iostream>
2 #include<stdio.h>
3 #include<unistd.h>
4 #include<sys/epoll.h>
5
6 using namespace std;
7 int main()
8 {
9 int epfd=epoll_create(3);
10 if(epfd<0)
11 {
12 perror("epoll_create");
13 return 0;
14 }
15
16 struct epoll_event ee;
17 ee.events=EPOLLIN;
18 ee.data.fd=0;
19 epoll_ctl(epfd,EPOLL_CTL_ADD,0,&ee);
20
21 while(1)
22 {
23 struct epoll_event arr[2];
24 int ret=epoll_wait(epfd,arr,sizeof(arr)/sizeof(arr[0]),-1);
25 if(ret<0)
26 {
27 perror("epoll_wait");
28 continue;
29 }
30
31 for(int i=0;i<ret;i++)
32 {
33 if(arr[i].events==EPOLLIN)
34 {
35 char buf[1024]={0};
36
37 read(arr[i].data.fd,buf,sizeof(buf)-1);
38 cout<<buf<<endl;
39 }
40 }
41 }
42 return 0;
43 }
① LT(Level Triggered) 水平触发工作模式
在LT模式下,当epoll检测到事件就绪的时候,可以不处理或处理一部分,但是可以连续多次调用epoll_wait对事件进行处理,简单点来说的话就是如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你
② ET(Edge Triggered) 边缘触发工作模式
在ET模式下,当epoll检测到事件就绪的时候,会立即进行处理,并且只会处理一次,换句话说就是文件描述符上的事件就绪之后,只有一次处理机会。 简单来说就是如果事件来了,不管来了几个,你若不处理或者没有处理完,除非下一个事件到来,否则epoll将不会再通知你。ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll。只支持非阻塞的读写。
1 /*================================================================
2 ================================================================*/
3
4 #include <stdio.h>
5 #include <unistd.h>
6 #include <sys/epoll.h>
7 #include <fcntl.h>
8 #include <errno.h>
9 #include <string>
10 /*
11 * 1.监控0号文件描述符对应的可读事件
12 */
13
14 using namespace std;
15
16 int main()
17 {
18 //1.创建epoll操作句柄
19 int epfd = epoll_create(5);
20 if(epfd < 0)
21 {
22 perror("epoll_create:");
23 return 0;
24 }
25
26 //2.添加0号文件描述符对应的事件结构到epoll当中
27
28 struct epoll_event ee;
29 ee.events = EPOLLIN ;
30 //ee.data.fd = 0 : 当监控成功之后, 从双向链表当中将事件结构拷贝到用户空间的事件结构数组当中之后
31 //程序员可以通过该结构当中的fd, 知道该事件结构属于哪一个文件描述符
32 ee.data.fd = 0;
33
34 epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
35
36 //epoll等待
37 int count = 0;
38 while(1)
39 {
40 struct epoll_event arr[10];
41 //int ret = epoll_wait(epfd, arr, 10, -1);
42 //int ret = epoll_wait(epfd, arr, 10, 1000);
43 int ret = epoll_wait(epfd, arr, 10, 0);
44 if(ret < 0)
45 {
46 perror("epoll_wait:");
47 return 0;
48 }
49 else if(ret == 0)
50 {
51 //printf("timeout: %d\n", count++);
52 continue;
53 }
54
55
56 char buf[2] = {0};
57 read(arr[0].data.fd, buf, sizeof(buf) - 1);
58 //printf("ret : %d\n", ret);
59 //a b c
60 //
61 printf("%s ", buf);
62 printf("count: %d\n", count++);
63
64 }
65 return 0;
66 }
11 #include <stdio.h>
12 #include <unistd.h>
13 #include <sys/epoll.h>
14 #include <fcntl.h>
15 #include <errno.h>
16 #include <string>
17 /*
18 * 1.监控0号文件描述符对应的可读事件
19 */
20
21 using namespace std;
22
23 int main()
24 {
25 int flag = fcntl(0, F_GETFL);
26 fcntl(0, F_SETFL, flag | O_NONBLOCK);
27
28
29 //1.创建epoll操作句柄
30 int epfd = epoll_create(5);
31 if(epfd < 0)
32 {
33 perror("epoll_create:");
34 return 0;
35 }
36
37 //2.添加0号文件描述符对应的事件结构到epoll当中
38
39 struct epoll_event ee;
40 ee.events = EPOLLIN | EPOLLET;
41 //ee.data.fd = 0 : 当监控成功之后, 从双向链表当中将事件结构拷贝到用户空间的事件结构数组当中之后
42 //程序员可以通过该结构当中的fd, 知道该事件结构属于哪一个文件描述符
43 ee.data.fd = 0;
44
45 epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
46
47 //epoll等待
48 int count = 0;
49 while(1)
50 {
51 struct epoll_event arr[10];
52 //int ret = epoll_wait(epfd, arr, 10, -1);
53 //int ret = epoll_wait(epfd, arr, 10, 1000);
54 int ret = epoll_wait(epfd, arr, 10, 0);
55 if(ret < 0)
56 {
57 perror("epoll_wait:");
58 return 0;
59 }
60 else if(ret == 0)
61 {
62 //printf("timeout: %d\n", count++);
63 continue;
64 }
65
66
67 string str;
68 while(1)
69 {
70 char buf[2] = {0};
71 int ret = read(arr[0].data.fd, buf, sizeof(buf) - 1);
72 //printf("ret : %d\n", ret);
73 if(ret < 0)
74 {
75 if(errno == EAGAIN || errno == EWOULDBLOCK)
76 {
77 break;
78 }
79 }
80 //a b c
81 //
82 printf("%s ", buf);
83 str.append(buf);
84 //printf("count: %d\n", count++);
85 }
86
87 printf("str : %s\n", str.c_str());
88 }
89 return 0;
90 }
1 /*================================================================
2 *
3 ================================================================*/
4
5 #include <stdio.h>
6 #include <unistd.h>
7 #include <string.h>
8 #include <sys/epoll.h>
9 #include <sys/socket.h>
10 #include <arpa/inet.h>
11 #include <netinet/in.h>
12
13 /*
14 * 1.实现单线程的tcp服务端
15 *
16 *
17 * 2.在单线程tcp服务端基础上, 添加epoll代码(让epoll监控两种文件描述符 侦听套接字 & 新连接套接字)
18 * epoll监控的文件描述符 = 1个侦听套接字 + 若干个新连接的套接字
19 * 侦听套接字(读事件): 一旦读事件就绪, 表示当前有新连接三次握手建立连接了, 调用accept函数处理读事件
20 * 新连接的套接字(读事件) : 一旦读事件就绪了,表示当前客户端给服务端发送消息了, 调用recv函数处理读事件
21 * */
22
23 int main()
24 {
25 int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
26 if(listen_sockfd < 0){
27 perror("socket:");
28 return 0;
29 }
30
31 struct sockaddr_in addr;
32 addr.sin_family = AF_INET;
33 addr.sin_port = htons(45678);
34 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
35
36 int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
37 if(ret < 0){
38 perror("bind:");
39 return 0;
40 }
41
42 ret = listen(listen_sockfd, 5);
43 if(ret < 0){
44 perror("listen: ");
45 return 0;
46 }
47
48 /*
49 * 1.创建epoll操作句柄
50 * 2.添加侦听套接字, 让epoll进行监控
51 * 3.监控到侦听套接字的读事件之后, 调用accept函数处理读事件(接收新连接)
52 * 区分到底是新连接套接字还是侦听套接字, 分别处理
53 * 3.1 将新连接的套接字添加到epoll当种进行监控
54 *
55 *
56 * 如果说是新连接套接字的读事件产生, 则接收数据
57 *
58 * */
59
60
61 int epoll_fd = epoll_create(5);
62 if(epoll_fd < 0){
63 perror("epoll_create");
64 return 0;
65 }
66
67 struct epoll_event ee;
68 ee.events = EPOLLIN;
69 ee.data.fd = listen_sockfd;
70 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sockfd, &ee);
71
72 while(1){
73 struct epoll_event arr[10];
74 int ret = epoll_wait(epoll_fd, arr, 10, -1);
75 if(ret < 0){
76 perror("epoll_wait:");
77 continue;
78 }else if(ret == 0){
79 continue;
80 }
81 //一定是由epoll监控的文件描述符, 有对应的就绪事件产生了
82 for(int i = 0; i < ret; i++){
83 if(arr[i].data.fd == listen_sockfd){
84 //accept
85 int new_sockfd = accept(listen_sockfd, NULL, NULL);
86 if(new_sockfd < 0){
87 perror("accept:");
88 continue;
89 }
90
91 struct epoll_event new_ee;
92 new_ee.events = EPOLLIN;
93 new_ee.data.fd = new_sockfd;
94 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_sockfd, &new_ee);
95 }else{
96 int new_sockfd = arr[i].data.fd;
97 char buf[1024] = {0};
98 ssize_t recv_size = recv(new_sockfd, buf, sizeof(buf), 0);
99 if(recv_size < 0){
100 perror("recv:");
101 continue;
102 }else if(recv_size == 0){
103 printf("peershutdown connect\n");
104 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, new_sockfd, NULL);
105 close(new_sockfd);
106 continue;
107 }
108 printf("buf: %s\n", buf);
109 memset(buf, '\0', sizeof(buf));
110 strcpy(buf, "hello, i am server~~");
111 send(new_sockfd, buf, strlen(buf), 0);
112 }
113 }
114 }
115
116 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, listen_sockfd, NULL);
117 close(listen_sockfd);
118 return 0;
119 }
1 /*================================================================
2 ================================================================*/
3
4 #include <stdio.h>
5 #include <unistd.h>
6 #include <string.h>
7 #include <fcntl.h>
8 #include <sys/epoll.h>
9 #include <sys/socket.h>
10 #include <arpa/inet.h>
11 #include <netinet/in.h>
12 #include <string>
13
14 using namespace std;
15
16 /*
17 * 1.实现单线程的tcp服务端
18 *
19 *
20 * 2.在单线程tcp服务端基础上, 添加epoll代码(让epoll监控两种文件描述符 侦听套接字 & 新连接套接字)
21 * epoll监控的文件描述符 = 1个侦听套接字 + 若干个新连接的套接字
22 * 侦听套接字(读事件): 一旦读事件就绪, 表示当前有新连接三次握手建立连接了, 调用accept函数处理读事件
23 * 新连接的套接字(读事件) : 一旦读事件就绪了,表示当前客户端给服务端发送消息了, 调用recv函数处理读事件
24 * */
25
26 int main()
27 {
28 int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
29 if(listen_sockfd < 0){
30 perror("socket:");
31 return 0;
32 }
33
34 struct sockaddr_in addr;
35 addr.sin_family = AF_INET;
36 addr.sin_port = htons(45678);
37 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
38
39 int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
40 if(ret < 0){
41 perror("bind:");
42 return 0;
43 }
44
45 ret = listen(listen_sockfd, 5);
46 if(ret < 0){
47 perror("listen: ");
48 return 0;
49 }
50
51 /*
52 * 1.创建epoll操作句柄
53 * 2.添加侦听套接字, 让epoll进行监控
54 * 3.监控到侦听套接字的读事件之后, 调用accept函数处理读事件(接收新连接)
55 * 区分到底是新连接套接字还是侦听套接字, 分别处理
56 * 3.1 将新连接的套接字添加到epoll当种进行监控
57 *
58 *
59 * 如果说是新连接套接字的读事件产生, 则接收数据
60 *
61 * */
62
63
64 int epoll_fd = epoll_create(5);
65 if(epoll_fd < 0){
66 perror("epoll_create");
67 return 0;
68 }
69
70 struct epoll_event ee;
71 ee.events = EPOLLIN;
72 ee.data.fd = listen_sockfd;
73 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sockfd, &ee);
74
75 while(1){
76 struct epoll_event arr[10];
77 int ret = epoll_wait(epoll_fd, arr, 10, -1);
78 if(ret < 0){
79 perror("epoll_wait:");
80 continue;
81 }else if(ret == 0){
82 continue;
83 }
84 //一定是由epoll监控的文件描述符, 有对应的就绪事件产生了
85 for(int i = 0; i < ret; i++){
86 if(arr[i].data.fd == listen_sockfd){
87 //accept
88 int new_sockfd = accept(listen_sockfd, NULL, NULL);
89 if(new_sockfd < 0){
90 perror("accept:");
91 continue;
92 }
93
94 int flag = fcntl(new_sockfd, F_GETFL);
95 fcntl(new_sockfd, F_SETFL, flag | O_NONBLOCK);
96
97 struct epoll_event new_ee;
98 new_ee.events = EPOLLIN | EPOLLET;
99 new_ee.data.fd = new_sockfd;
100 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_sockfd, &new_ee);
101 }else{
102 int new_sockfd = arr[i].data.fd;
103 string recv_buf;
104 int flag = true;
105
106 while(1){
107 char buf[2] = {0};
108 ssize_t recv_size = recv(new_sockfd, buf, sizeof(buf) - 1, 0);
109 if(recv_size < 0){
110 if(errno == EAGAIN || errno == EWOULDBLOCK){
111 flag = true;
112 break;
113 }
114 perror("recv:");
115 flag = false;
116 break;
117 }else if(recv_size == 0){
118 printf("peershutdown connect\n");
119 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, new_sockfd, NULL);
120 close(new_sockfd);
121 flag = false;
122 break;
123 }
124 //printf("%s\n", buf);
125 recv_buf.append(buf);
126 }
127
128 if(flag == true){
129 printf("recv_buf: %s\n", recv_buf.c_str());
130 char buf[1024] = {0};
131 memset(buf, '\0', sizeof(buf));
132 strncpy(buf, "hello, i am server~~", sizeof(buf));
133 send(new_sockfd, buf, strlen(buf), 0);
134 }
135 }
136 }
137 }
138
139 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, listen_sockfd, NULL);
140 close(listen_sockfd);
141 return 0;
142 }
以上就是今天要讲的内容,本文详细介绍了Linux高级IO中的5中ION模型的原理及用法,高级IO提供了大量的方法供我们使用,非常的便捷,我们务必掌握。希望大家多多支持!另外如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。加油啊!