poll,epoll函数都是实现IO多路转接的,今天我们看下poll ,epoll的具体实现过程。
1. poll函数接口
poll函数和select函数一样,就是实现IO的第一步——等。
函数原型:
函数功能: 同时等待多个文件描述符就绪。
函数参数:
fds是一个结构体指针,包含三部分:文件描述符,监听的事件集合,返回的事件集合。
nfds表示fds结构体指针的大小,即能存储文件描述符的大小。
timeout的取值表示poll等的方式,和select一样。
函数返回值:
返回值小于0,说明poll出错;
返回值等于0,说明超时返回;
返回值大于0,表示poll由于监听的文件描述符就绪而返回。
关于struct pollfd 的结构
注:在poll中,对输入事件和输出事件进行了区分,解决了select在调用之前必须重新设置的问题。
能存储文件描述符的多少由自己决定,可以认为文件描述符无上限。
2. poll的优缺点
优点:poll的优点是相对于select来说的:
(1)poll使用一个结构体指针来表示要关心的文件描述符的什么事件或者操作系统告诉你关心的哪些文件描述符的哪些事件就绪。
(2)poll结构包含了要关心的event和发生的revent,不用再调用之前设置文件描述符了。
(3)poll的文件描述符不受限制。
缺点:
(1)和select一样,在返回后,要轮询获取就绪的文件描述符。
(2)每次调用poll都需要将大量的pollfd结构从用户态拷贝到内核态。
(3)同时连接大量用户,性能会降低。
3. epoll 函数
epoll函数和select,poll函数类似,都是实现了IO多路转接,但是epoll函数细分为三个函数,每个函数实现它特定的功能。
(1)epoll_create函数
函数原型:
函数功能:
创建epoll模型
函数参数:
自从Linux2.6.8之后,size常数是被忽略的。
用完之后,必须用close()关闭。
函数返回值:
成功返回文件描述符,失败返回-1,并且置错误码。
(2)epoll_ctl函数
函数功能:
将所要关心的文件描述符的某些事件注册到红黑树中。
函数参数:
epfd表示上一步创建的epoll模型。
op表示要做的动作,包括以下三个,分别是EPOLL_CTL_ADD(注册新的fd到epfd中),EPOLL_CTL_MOD(修改已经注册的fd的监听事件),EPOLL_CTL_DEL(从epfd中删除一个fd)。
event表示要监控的事件,有以下取值
EPOLLIN: 表示对应的文件描述符可以读;
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急数据可读;
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设置为边缘触发模式;
EPOLLONESHORT:表示只监听一次事件,当监听完之后就不再监听了。
注:这些事件的表示都是宏,宏是一个二进制的序列,并且全都不相同,若要关心好几个事件集,则将要关心的事件进行“I(或)”运算。
关于struct epoll_event的结构:
函数返回值:如果成功返回0,如果有错误,返回-1。
(3)epoll_wait函数
函数原型:
函数功能:
检查是否有事件发生,即检查就绪队列是否为空。
函数参数:
events指向缓冲区首地址,是分配好的epoll_event 结构体数组。event不可以是空指针;
maxevents表示缓冲区的大小,但是不能大于创建epoll_create()时的size;
timeout和select中的timeout作用和取值一样。
函数返回值:
函数成功,返回对应IO上已经准备好的文件描述符的大小;
返回0,表示超时;
返回-1,出错;
注:在这个函数成功后,内核只负责把数据复制到events数组中,不会帮助我们在用户态分配内存。
4. 关于epoll模型
(1)当调用系统函数epoll_create时,操作系统会给我们创建一个epoll模型,这个epoll模型包括三个部分,一棵空红黑树,回调机制,就绪队列。
空红黑树用来保存要监控的哪些文件描述符的哪些事件;
回调机制确保有事件就绪时驱动会告诉操作系统,有事件就绪了;
在事件就绪后,操作系统将对应的文件描述符的事件的结点放置到就绪队列中,由用户检查就绪队列,判断是否有事件就绪。
(2)调用epoll_ctl,完成事件注册。即告诉操作系统我们要关心哪些文件描述符的哪些事件,然后操作系统将我们要关心的对象添加到红黑树中,帮助我们管理所要关心的对象。
(3)调用epoll_wait,检查是否有事件发生。先检查就绪队列是否为空,如果不为空,就绪队列中保存的就是已经就绪的事件;然后操作系统利用用户提供的缓冲区,将数据按顺序放置在数组中(一个挨着一个存放),由用户自行拿出。
5. epoll的优点
相对于poll ,select来说的。
(1)文件描述符无上限:通过epoll_ctl来注册一个文件描述符,内核使用红黑树来管理要关心的事件。
(2)基于事件的通知方式:有回调机制,一旦关心的某个文件描述符就绪,驱动就会通知操作系统,操作系统迅速激活这个文件描述符。
(3)维护就绪队列:当文件描述符就绪,就会被添加到内核的就绪队列中,这样获取就绪文件描述符的时候,时间复杂度就是O(1)。
(4)不存在内存映射机制,有两个原因,第一:操作系统是不相信任何人的,不会将任何的数据或数据结构暴露给用户。
第二:epoll_wait自己提供了缓冲区,是将数据拷贝到缓冲区,不存在映射。
6. epoll 的工作模式
(1)水平触发(Lever Triggered)
epoll的默认工作模式。
只要有数据,操作系统就会通知用户,epoll会一直处于触发状态。
此时,epoll可以对数据不立刻进行处理,或者只处理一部分。
直到缓冲区上所有的数据都被处理完,否则epoll_wait不会立刻返回。
支持阻塞读写和非阻塞读写。
(2)边缘触发(Eage Triggered)
只要有新的数据来,操作系统才会通知用户,才会触发边缘模式。
当epoll检测到有事件就绪时,必须立即处理。
ET模式下,文件描述符上的事件就绪后只有一次处理机会。
ET的性能比LT性能高,但是对用户的要求更高。(Nginx默认下采用ET模式)
只支持非阻塞的读写。
7. epoll网络服务器
我们这次编写的epoll服务器是基于LT的工作模式,并且即关心读事件,又关心写事件,对于服务器来说,我们要先关心它的读事件,再关心写事件,并且只写一次,就关闭连接。(类似于HTTP的短链接)。
(1)创建listen_socket
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <netinet/in.h>
5 #include <sys/types.h>
6 #include <sys/socket.h>
7 #include <arpa/inet.h>
8 #include <unistd.h>
9 #include <sys/epoll.h>
10
11 int startup(int port)
12 {
13 int sock=socket(AF_INET,SOCK_STREAM,0);
14 if(sock<0)
15 {
16 perror("socket");
17 exit(2);
18 }
19 struct sockaddr_in local;
20 local.sin_family=AF_INET;
21 local.sin_addr.s_addr=htonl(INADDR_ANY);
22 local.sin_port=htons(port);
23
24 if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
25 {
26 perror("bind");
27 exit(3);
28 }
29 if(listen(sock,5)<0)
30 {
31 perror("listen");
32 exit(4);
33 }
34 return sock;
35
36 }
(2)IO服务
38 void serviceIO(int efd,struct epoll_event* buf,int num,int listen_sock)
39 {
40 int i=0;
41
42 struct epoll_event eve;
43 for(i=0;i<num;i++)
44 {
45
46 int fd=buf[i].data.fd;
47 if((buf[i].events)&EPOLLIN)
48 {
49 if(buf[i].data.fd==listen_sock)
50 {
51 //listen_sock
52 struct sockaddr_in client;
53 socklen_t len=sizeof(client);
54 int newsock=accept(listen_sock,(struct sockaddr*)&client,&le n);
55 if(newsock<0)
56 {
57 perror("accept\n");
58 continue;
59 }
60 printf("get a connect:%s:%d\n",inet_ntoa(client.sin_addr),\
61 ntohs(client.sin_port));
62 eve.events=EPOLLIN;
63 eve.data.fd=newsock;
64 int ret=epoll_ctl(efd,EPOLL_CTL_ADD,newsock,&eve);
65 if(ret<0)
66 {
67 perror("epoll_ctl\n");
68 exit(7);
69 }
70 }
71 else
72 {
//nomal sock
74 eve.events=EPOLLOUT;
75 eve.data.fd=fd;
76 char buffer[1024];
77 ssize_t s=read(fd,buffer,sizeof(buffer)-1);
78 if(s>0)
79 {
80 buffer[s]=0;
81 printf("%s\n",buffer);
82 epoll_ctl(efd,EPOLL_CTL_MOD,fd,&eve);
83 }
84 else if(s==0)
85 {
86 close(fd);
87 epoll_ctl(efd,EPOLL_CTL_DEL,fd,NULL);
88 printf("client quit!\n");
89
90 }
91 else
92 {
93
94 perror("read");
95 close(fd);
96 epoll_ctl(efd,EPOLL_CTL_DEL,fd,NULL);
97 }
98 }
99 }
100 if(buf[i].events&EPOLLOUT) //判断是否有写事件发生
101 {
102 const char*str="HTTP/1.0 200 OK\r\n\r\n<html><h1>hello my epoll!</h1></html>";
103 write(fd,str,strlen(str));
104 close(fd);
105 epoll_ctl(efd,EPOLL_CTL_DEL,fd,NULL);
106
107 }
108 }
109 }
110 int main(int argc,char* argv[])
111 {
112
113 if(argc!=2)
114 {
115 printf("Usage:%s[port]\n",argv[0]);
116 return 1;
117 }
118
119 int listen_sock=startup(atoi(argv[1]));
120 int efd=epoll_create(256);
121 if(efd<0)
122 {
123 perror("epoll_create\n");
124 return 5;
125 }
126
127 struct epoll_event eve;
128 eve.events=EPOLLIN;
129 eve.data.fd=listen_sock;
130
131 int ret= epoll_ctl(efd,EPOLL_CTL_ADD,listen_sock,&eve);
132 if(ret<0)
133 {
134 perror("epoll_ctl\n");
135 return 6;
136 }
137 for(;;)
138 {
139 struct epoll_event buf[200];
140
141 int num=epoll_wait(efd,buf,sizeof(buf),-1);
142 switch(num)
143 {
144 case -1:
145 perror("epoll_wait\n");
146 break;
147 case 2:
148 printf("timeout...\n");
149 break;
150 default:
151 serviceIO(efd,buf,num,listen_sock);
152 break;
153 }
154 }
155
156 }
(3)我们利用telnet(远程登录)来检测我们的服务器
如果你的bash告诉你找不到telnet,这时你就需要安装了。安装的过程很简单,就是两条命令,如下:
yum install telnet-server
yum install telnet
然后我们就可以使用telnet了。
用法:telnet 目的IP地址 目的端口号