任何IO操作都需要经过 等待 和 拷贝数据 这两个过程,而这两个过程是在内核当中的过程
在内核将数据准备好之前, 系统调用接口会一直等待,不会返回,所有的套接字, 默认都是阻塞方式.
应用进程发起系统调用,在内核中判断数据没准备好,直接返回,并且返回EWOULDBLOCK错误码
非阻塞IO一般需要搭配循环使用,反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用
内核将数据准备好的时候, 会向应用进程发送SIGIO信号告知应用进程可以进行IO操作
内核帮我们监控了多个文件描述符,当某一个或者若干个文件描述符就绪的时候,就会通知调用者,调用者调用系统调用函数针对就绪的文件描述符进行操作
由内核在数据拷贝完成时, 通知应用程序进行相关操作
(而信号驱动是当内核中数据准备好了就通知应用程序).
多路转接本质上是让内核帮助程序员监控多个文件描述符的IO事件,一旦监控的某个文件描述符对应的事件产生(IO就绪),就会通知调用者
作用:监控多个文件描述符,就绪之后通知调用者
使用vim打开/usr/include/sys/select.h路径下查看源码
内核在使用该数组的时候采用的是位图的方式,一共有16*8*8=1024个比特位
fd_set事件集合占用比特位的个数和宏_FD_SETSIZE强相关,即,_FDSETSIZE多大,fd_set事件集合就有多少个比特位
其实fd_set结构就是一个整数数组,更严格的是,是一个“位图”,使用位图中对应的位来表示要监控的文件描述符,如下图所示:
如上图所示
- 如果关心某个文件描述符对应的某个事件,则将文件描述符添加到对应的事件集合当中。eg:关心3号文件描述符的可读事件,则将3号文件描述符添加到read_fds当中
- 添加文件描述符到事件集合的时候,是将文件描述符对应的比特位设置为1
- 如果一个文件描述符关心多种事件(可读,可写,异常),则将文件描述符添加到不同的事件集合当中
- select的监控效率会随着监控文件描述符的增多而下降,本质原因是由于监控轮询的范围变大了
为了方便操的作位图,可使用如下接口来操作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
void FD_ZERO(fd_set *set);
【注意】:select返回之后,需要重新添加文件描述符
优点:
- select遵循的是POSIX标准,说明select函数是一个跨平台的函数,既可以在Windows当中运行,也可以在Linux当中运行
- select在带有超时时间的监控的时候,超时时间的单位可以为微妙
缺点:
- 监控文件描述符个数的上限为1024
- 随着文件描述符的增多,select监控效率在下降(本质是select在轮询进行监控)
- 可读、可写、异常这些事件需要单独的添加到不同的事件集合当中
- 当select监控成功之后,会从事件集合当中去除掉未就绪的文件描述符,这使程序下一次调用select时,还需要重新添加文件描述符
- 在每次select进行监控的时候,都会将准备好的事件集合拷贝到内核空间,select返回的时候都会将内核空间拷贝给用户空间
我们使用select对0号文件描述符进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来
代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/select.h>
4
5 int main()
6 {
7 fd_set readfds;
8 FD_ZERO(&readfds);
9 FD_SET(0,&readfds);
10
11 while(1)
12 {
13 int ret = select(1,&readfds,NULL,NULL,NULL);
14 if(ret < 0)
15 {
16 perror("select");
17 return 0;
18 }
19
20 char buf[1024] = {0};
21 read(0,buf,sizeof(buf)-1);
22
23 printf("%s\n",buf);
24 }
25
26 return 0;
27 }
前提:与select相比并不支持跨平台,与epoll相比,没有epoll的效率高
优点:
- 提出了事件结构的方式,在给poll函数传递参数的时候,不需要分别添加到“事件集合”中
- 事件结构数组的大小可以根据程序员自己进行定义,并没有上限要求
- 不用在监控到就绪文件描述符之后,重新添加文件描述符
缺点:
- 不支持跨平台
- 内核对事件结构数组的监控也是采用轮询遍历的方式,即随着监控文件描述符的增多,监控效率会下降
- 每次调用poll都需要把大量的pollf结构从用户态拷贝到内核态,poll返回的时候,会将内核空间拷贝给用户空间(从内核态到用户态会调用do_signal会有开销)
1 #include<stdio.h>
2 #include<poll.h>
3 #include<unistd.h>
4
5 int main()
6 {
7 struct pollfd arr[10];
8
9 arr[0].fd = 0;
10 arr[0].events = POLLIN;
11
12 while(1)
13 {
14 int ret = poll(arr,1,-1);
15 if(ret < 0)
16 {
17 perror("poll");
18 return 0;
19 }
20 for(int i = 0;i < 10;++i)
21 {
22 if(arr[i].revents == POLLIN)
23 {
24 char buf[1024] = {0};
25 read(arr[i].fd,buf,sizeof(buf)-1);
26 printf("%s\n",buf);
27 }
28 }
29 }
30 return 0;
31 }
epoll是目前世界公认的在Linux下,多路转接监控效率最高的模型
- size:自从Linux2.6.8之后,size参数是被忽略的;但不要传递小于0的数字
- 返回值:返回epoll的操作句柄
- epfd:epoll的操作句柄
- events:时间结构数组(集合),从epoll当中获取就绪的事件结构
- maxevents:最多一次获取多少个事件结构
timeout:
0:带有超时事件
==0:非阻塞
<0:阻塞- 返回值:就绪的文件描述符个数
当调用epoll_create函数时,会在内核创建一个eventpoll结构体,在该结构体中有一个rdlist成员和rbr成员,它两分别是一个双向链表和红黑树,而调用epoll_ctl函数添加、修改、删除文件描述符对应的事件集合其实是对红黑树中的节点进行相应的添加、修改、删除操作,而所有添加到epoll的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当文件描述符准备就绪后,内核会回调ep_poll_callback函数,将准备就绪的事件集合添加到rdlist双向链表中,而当调用epoll_wait进行监控的时候,如果双向链表为空,则表明当前没有就绪的事件发生,如果不为空,则将双向链表中的内容复制到用户态,并返回将事件数量返回给用户。
【注意】每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
- 事件回调机制,当文件描述符就绪之后,会调用回调函数将事件结构复制到双向链表中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 没有数量限制: 文件描述符数目无上限.
我们使用epoll对0号文件描述符进行监控,如果监控到了我们从0号文件描述符中去读取内容并将读取到的内容打印出来
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/epoll.h>
4
5 int main()
6 {
7 int epfd = epoll_create(3);
8 if(epfd < 0)
9 {
10 perror("epoll_create");
11 return 0;
12 }
13
14 struct epoll_event ee;
15 ee.events = EPOLLIN;
16 ee.data.fd = 0;
17 epoll_ctl(epfd,EPOLL_CTL_ADD,0,&ee);
18
19 while(1)
20 {
21 struct epoll_event arr[2];
22 int ret = epoll_wait(epfd,arr,sizeof(arr)/sizeof(arr[0]),-1);
23 if(ret < 0)
24 {
25 perror("epoll_wait");
26 continue;
27 }
28
29 for(int i = 0;i < ret;++i)
30 {
31 if(arr[i].events == EPOLLIN)
32 {
33 char buf[1024] = {0};
34 read(arr[i].data.fd,buf,sizeof(buf) - 1);
35
36 printf("%s\n",buf);
37 }
38 }
39 }
40 return 0;
41 }
在TCP单进程中存在一个问题:
- 将accept放在while循环外部,只能接收一个客户端的连接(客户端recv函数阻塞)
- 将accept放到while循环内部,和每个客户端只能通信一次(服务端accept阻塞)
具体分析可查看【TCP单进程版本问题】
而多路转接正好可以解决这个问题,下面是使用epoll解决的代码
客户端1
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6
7 int main()
8 {
9 int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
10 if(sockfd < 0)
11 {
12 perror("socket");
13 return 0;
14 }
15
16 struct sockaddr_in addr;
17 addr.sin_family = AF_INET;
18 addr.sin_port = htons(18989);
19 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
20 int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
21 if(ret < 0)
22 {
23 perror("connect");
24 return 0;
25 }
26
27
28 while(1)
29 {
30
31 sleep(1);
32
33 char buf[1024] = {0};
34 sprintf(buf,"hello server,i am client xxxxx");
35 ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
36 if(send_ret < 0)
37 {
38 perror("send");
39 continue;
40 }
41
42
43 memset(buf,'\0',sizeof(buf));
44 ssize_t recv_ret = recv(sockfd,buf,sizeof(buf)-1,0);
45 if(recv_ret < 0)
46 {
47 perror("recv");
48 continue;
49 }
50 else if(recv_ret == 0)
51 {
52 printf("server close");
53
54 close(sockfd);
55
56 return 0;
57 }
58
59 printf("server say: %s\n",buf);
60 }
61
62 close(sockfd);
63 return 0;
64 }
客户端2
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/socket.h>
5 #include<arpa/inet.h>
6
7 int main()
8 {
9 int sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
10 if(sockfd < 0)
11 {
12 perror("socket");
13 return 0;
14 }
15
16 struct sockaddr_in addr;
17 addr.sin_family = AF_INET;
18 addr.sin_port = htons(18989);
19 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
20 int ret = connect(sockfd,(struct sockaddr*)&addr,sizeof(addr));
21 if(ret < 0)
22 {
23 perror("connect");
24 return 0;
25 }
26
27
28 while(1)
29 {
30
31 sleep(1);
32
33 char buf[1024] = {0};
34 sprintf(buf,"hello server,i am client PPPPPPPPP");
35 ssize_t send_ret = send(sockfd,buf,strlen(buf),0);
36 if(send_ret < 0)
37 {
38 perror("send");
39 continue;
40 }
41
42
43 memset(buf,'\0',sizeof(buf));
44 ssize_t recv_ret = recv(sockfd,buf,sizeof(buf)-1,0);
45 if(recv_ret < 0)
46 {
47 perror("recv");
48 continue;
49 }
50 else if(recv_ret == 0)
51 {
52 printf("server close");
53
54 close(sockfd);
55
56 return 0;
57 }
58
59 printf("server say: %s\n",buf);
60 }
61
62 close(sockfd);
63 return 0;
64 }
服务端:
1 #include<stdio.h>
2 #include<string.h>
3 #include<unistd.h>
4 #include<sys/epoll.h>
5 #include<sys/socket.h>
6 #include<arpa/inet.h>
7
8 int main()
9 {
10 int listen_sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
11 if(listen_sockfd < 0)
12 {
13 perror("socket");
14 return 0;
15 }
16
17 struct sockaddr_in addr;
18 addr.sin_family = AF_INET;
19 addr.sin_port = htons(18989);
20 addr.sin_addr.s_addr = inet_addr("0.0.0.0");
21 int ret = bind(listen_sockfd,(struct sockaddr*)&addr,sizeof(addr));
22 if(ret < 0)
23 {
24 perror("bind");
25 return 0;
26 }
27
28 ret = listen(listen_sockfd,1);
29 if(ret < 0)
30 {
31 perror("listen");
32 return 0;
33 }
34
35 //初始化epoll
36 int epfd = epoll_create(4);
37 if(epfd < 0)
38 {
39 perror("epfd");
40 return 0;
41 }
42
43 struct epoll_event ee;
44 ee.events = EPOLLIN;
45 ee.data.fd = listen_sockfd;
46 epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sockfd,&ee);
47
48
49 while(1)
50 {
51 struct epoll_event arr[10];
52 int ret = epoll_wait(epfd,arr,sizeof(arr)/sizeof(arr[0]),-1);
53 if(ret < 0)
54 {
55 continue;
56 }
57 for(int i = 0;i < ret;++i)
58 {
59 if(arr[i].data.fd == listen_sockfd)
60 {
61 struct sockaddr_in peer_addr;
62 socklen_t socklen = sizeof(peer_addr);
63 int new_sockfd = accept(listen_sockfd,(struct sockaddr*)&peer_addr,&socklen);
64 if(new_sockfd < 0)
65 {
66 perror("accept");
67 return 0;
68 }
69
70 struct epoll_event newee;
71 newee.data.fd = new_sockfd;
72 newee.events = EPOLLIN;
73 epoll_ctl(epfd,EPOLL_CTL_ADD,new_sockfd,&newee);
74
75 }
76 else
77 {
78 char buf[1024] = {0};
79 ssize_t recv_ret = recv(arr[i].data.fd,buf,sizeof(buf)-1,0);
80 if(recv_ret < 0)
81 {
82 perror("recv");
83 continue;
84 }
85 else if(recv_ret == 0)
86 {
87 printf("client close");
88 close(arr[i].data.fd);
89 close(listen_sockfd);
90 return 0;
91 }
92
93 printf("client%d say:%s%d\n",arr[i].data.fd,buf,arr[i].data.fd);
94
95 memset(buf,'\0',sizeof(buf));
96 sprintf(buf,"hello,i am server,i recv client%d",arr[i].data.fd);
97 ssize_t send_ret = send(arr[i].data.fd,buf,strlen(buf),0);
98 if(send_ret < 0)
99 {
100 perror("send");
101 continue;
102 }
103
104 }
105 }
106
107 }
108 close(listen_sockfd);
109 close(epfd);
110 return 0;
111 }
让程序跑起来可以看到一个服务端可以同时和两个客户端进行正常通信