一、为什么要用epoll?
在高并发的情况下,我们可以在服务器端用多路复用的方法来处理,具体的多路复用方法有select(),poll()和epoll()。但与epoll()相比,select()和poll()都有一些比较明显的缺点,其具体的缺点如下:
1.select()中单个进程所能监视的文件描述符存在限制,通常是1024个;并且select()采用轮询的方式扫描文件描述符,文件描述符越多,其性能越差。
2.内核/用户文件拷贝问题,select()需要复制大量的句柄数据结构。
3.select()采用水平触发方式。
4.select()的返回值是含有整个句柄的数组,需要遍历整个数组才能才能知道是哪些文件句柄发生了变化。
poll()与select()相比,其采用链表的方式来保存文件描述符,所以就没有了文件描述符数量的限制,但其余三个缺点还存在。
epoll是Linux系统下select()和poll()的加强版本,它主要有以下优点:
1.显著提高程序在大量并发的情况下只有少量活跃的系统CPU利用率。
2.无需遍历整个被监听的描述符集,只需遍历被内核IO事件异步唤醒而加入READY队列的描述符集,显著提高了性能。
epoll不仅提供了水平触发,还提供了边缘触发。
LT(level triggered):是默认的工作发式,内核会告诉你当前有哪些文件描述符已经处于就绪状态,然后你就可以对这些文件描述符进行相应的操作,如果不对这些已经就绪的文件描述符做出操作,内核还是会继续通知你,所以在这种工作方式下,出错的概率就会小一些。
ET(edge triggered):边缘触发方式,当一个文件描述符处于就绪状态时,内核就会通过epoll来告诉你,并假设你已经知道这个文件描述符处于就绪态,接下来也就不会再通知你关于这个文件描述符的信息,知道你做出了一些操作改变了这个文件描述符的状态将其变为未就绪态,但是,如果你并没有将这个文件描述符变为未就绪态,内核也就不会再通知你关于这个文件描述符你还未做出操作的信息了。
所以,我们可以将LT模式理解为,在LT模式下,事件不会丢弃,只要读buffer里有数据就会不断的让用户读。而ET模式只会在事件开始发生的时候通知你,所以你可能会错过这个通知。
二.epoll的原理及实现
epoll的设计及实现与select不同,epoll通过在内核建立一个简易的文件系统。其将select()/poll()的调用分成了三个部分。
1.int epoll_create(int size);
调用epoll_create()函数创建了一个epoll对象,在epoll文件系统中为这个epoll对象分配资源。此时epoll对象的兴趣列表为空。
2.epoll_ctl(epoll_fd,op,fd,&events)
向epoll对象中添加连接的套接字,修改(一般是添加,删除)epoll对象的兴趣列表。
3.epoll_wait()
收集已经发生事件的连接。
所以,我们先可以创建一个epoll对象,然后再修改其兴趣列表,最后通过epoll_wait()来收集已经发生事件的连接,注意,epoll_wait()并没有遍历所有的连接。
创建一个epoll实例:
int epoll_create(int size)
epoll_create()是系统调用,用户态会切换到内核态,该系统调用会新建一个epoll对象,此时该epoll()对象的兴趣列表为空,如果成功,返回一个文件描述符指向该epoll对象,否则,返回-1。这个文件描述符在下面几个epoll调用中代表了此epoll实例,当我们要关闭该文件描述符,应调用close(),当所有与该epoll实例相关的文件描述符被关闭时,该epoll实例也将被销毁。其参数size是我们所想要通过epoll实例来检查的文件描述符个数,其并不是一个上限,而是当我们指定了这个参数后,会告诉内核该怎样为内部数据结构划分大小。从linux2.6.8以来,size参数被忽略。
修该epoll实例的兴趣列表int epoll_ctl(int epoll_fd,int op,int fd,struct epoll_event *ev)
该系统调用能够修改由文件描述符epoll_fd所指向的epoll实例的兴趣列表,如果成功返回0,否则返回-1。
第一个参数:epoll_fd,由epoll_create()返回的指向epoll对象的文件描述符。
第二个参数:op,指定对第三个参数fd所要进行的操作,可以是以下几种值:
EPOLL_CTL_ADD:将fd加入到epoll实例的兴趣列表中,对于该fd上我们所感兴趣的事件,都指定在参数ev所指定的结构体之中。如果将一个已在兴趣列表中的fd再次加入该兴趣列表,就会出现EEXIST错误。
EPOLL_CTL_MOD:修改该fd上我们所感兴趣的事件,需要用到ev结构体中的信息,如果该fd不在兴趣列表中,会出现ENOENT错误。
EPOLL_CTL_DEL:将该fd从兴趣列表中删除,忽略参数ev。如果试图将一个不在兴趣列表中的文件描述符删除,会出现ENOENT错误。当我们关闭一个文件描述符时,会自动将其从所有的epoll实例的兴趣列表中移除。
第三个参数:fd就是我们所要在兴趣列表中进行修改时所需要的文件描述符,注意,其不能是代表普通文件或目录的文件描述符。
第四个参数是一个指向结构体epoll_event的指针,该结构体的定义如下:
typedef union epoll_data
{
void *ptr /*pointer to user_defined data*/
int fd /*文件描述符*/
uint_t u32 /*32位的整形数据*/
uint_t u64 /*64位的整形数据*/
}epoll_data_t;
struct epoll_event
{
uint32_t events /*epoll events(位掩码)*/
epoll_data_t data /*user data*/
};
参数ev位文件描述符fd所做的设置:
events成员是指定我们为待检查的文件描述符fd上所感兴趣的事件集合;
data成员是一个联合体,当fd为就绪态时,联合的成员可用来指定传回给调用进程的信。
收集有事件发生的连接:
int epoll_wait(int epoll_fd,struct epoll_event *evlist,int maxevents int timeout)
epoll_wait返回就绪态文件描述符的信息,返回的就绪态文件描述符的信息存放在evlist数组中。若调用成功,返回evlist数组中的元素个数,如果在timeout时间段内无就绪态文件描述符,则其返回值为0,如果出错,则其返回值为-1,错误信息存放在errno中。
第一个参数是epoll_create()的返回值,表示指向由epoll_create()创建的epoll实例。
第二个参数是一个epoll_event类型的结构体数组,该数组中存放的是有关处于就绪态文件描述符的信息。
第三个参数指定evlist数组的元素个数。
第四个参数timeout用来指定epoll_wait()函数的阻塞方式,有以下几种:
timeout = -1,一直阻塞,直到兴趣列表中的文件描述符上有事件发生或者捕获到一个信号才返回。
timeout = 0,执行一次非阻塞式的检查,看兴趣列表的哪些文件描述符发生了事件。
调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。
数组evlist中,每个元素中都包含了单个就绪态文件描述符的信息,其中events字段返回在该描述符上已经发生的事件掩码,data字段返回的是我们在调用epoll_ctl在epoll实例中注册感兴趣的事件时所指定的ev.data的值。因为data字段是唯一可获知的同这个我们所感兴趣的事件的文件描述符的途径,所以,当我们调用epoll_ctl()将文件描述符注册到兴趣列表中时,也应将ev.data.fd设置为文件描述符号,或者将ev.data.ptr指向包含文件描述符号的结构体。
当调用epoll_ctl()时ev.events指定的位掩码以及epoll_wait()返回的evlist[].events中的值如下所示:
默认情况下,我们通过epoll_ctl()的EPOLL_CTL_ADD操作将文件描述符加入到兴趣列表中后,该文件描述符就在兴趣列表中处于**状态,等到调用epoll_wait()时,如果该文件描述符处于就绪态,则就会通过存放就绪态文件描述符的数组来通知我们,直到当我们调用epoll_ctl()的EPOLL_CTL_DEL操作将该文件描述符从兴趣列表中移除。如果我们希望某个文件描述符只通知一次,那么可以在调用epoll_ctl()是将ev.events字段设置为EPOLLONESHOT标志,当我们设置了这个标志之后,在下一个epoll_wait()通知我们对应的文件描述符处于就绪态后,该文件描述符就会在兴趣列表中被设置为非**状态,后面几次epoll_wait()都不会再通知关于该文件描述符的状态了。如果我们想要让该文件描述符重新处于**状态,则可以通过epoll_ctl()的EPOLL_CTL_MOD操作修改该文件描述符为**状态。
下面通过一个实例再深入认识一下:
132 listen_fd = create_socket(port);
133 epoll_fd = epoll_create(MAX_EVENTS);
134 printf("epoll_fd = %d\n",epoll_fd);
135 if(epoll_fd < 0)
136 {
137 printf("epoll_create() get failure:%s\n",strerror(errno));
138 return -1;
139 }
140
141 event.events = EPOLLIN;
142 event.data.fd = listen_fd;
143 if(( rv = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,&event)) < 0)
144 {
145 printf("epoll_ctr() error:%s\n",strerror(errno));
146 return -2;
147 }
148 while(!g_stop)
149 {
150 int i ;
151 /*program will blocked here*/
152
153 if((events = epoll_wait(epoll_fd,event_array,MAX_EVENTS,-1)) < 0)
154 {
155 printf("epoll_wait() error:%s\n",strerror(errno));
156 break;
157 }
158 if(events == 0)
159 {
160 printf("epoll_wait() get timeout\n");
161 continue;
162 }
163 /*epoll_wait 的返回值如果大于0,其返回值是有事件发生的客户端的个数,即处于就绪态的文件描述符的个数*/
164 for(i = 0;i < events;i++)
165 {
166 if((event_array[i].events == EPOLLERR) || (event_array[i].events == EPOLLHUP))
167 {
168 printf("epoll_wait get error on fd:%d and error is:%s\n",event_array[i].data.fd,strerror(errno));
169 epoll_ctl(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
170 }
171 /*listen socket get event means new client connect*/
174 if((event_array[i].data.fd == listen_fd))
175 {
176
177 if((connect_fd = accept(listen_fd,(struct sockaddr *)NULL,NULL)) < 0)
178 {
179 printf("accept new client failure:%s\n",strerror(errno));
180 continue;
181 }
182
183 event.data.fd = connect_fd;
184 event.events = EPOLLIN;
185
186 if((epoll_ctl(epoll_fd,EPOLL_CTL_ADD,connect_fd,&event)) < 0)
187 {
188 printf("epoll_ctl add new client fd failure:%s\n",strerror(errno));
189 close(event_array[i].data.fd);
190 continue;
191 }
192 printf("add new client fd[%d] successful\n",connect_fd);
193 }
194 else
195 {
196 if((rv = read(event_array[i].data.fd,buf,sizeof(buf))) <= 0)
197 {
198 printf("socket fd [%d] read failure or get disconnect\n",event_array[i].data.fd);
199 epoll_ctl(epoll_fd,EPOLL_CTL_DEL,event_array[i].data.fd,NULL);
200 close(event_array[i].data.fd);
201 }
202 else
203 {
204 printf("read %d bytes from client:%s\n",strlen(buf),buf);
205 Record_temperature(buf,db);
206 }
207
208 }
209
210 }
211 }
212
213 close(listen_fd);
214 return 0;
215}
在服务器端创建socket套接字的流程是:socket,setsockopt,bind,listen,socket会创建一个文件描述符listen_fd,setsockopt解决"address in use"的问题,bind函数给创建的listen_fd绑定一个端口,申明该端口已经被使用,别的socket程序不能再占用该端口。listen函数使socket处于被动的监听模式,并为该socket建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理他们。
现在,来分析一下上面的程序逻辑:
代码第132行系统调用epoll_create()创建了一个epoll对象实例,并用epoll_fd指向该实例;
代码143行系统调用epoll_ctl修改epoll_fd所指向的epoll实例的兴趣列表,因为需要监听客户端的连接,所以我们需要加listen_fd加入到epoll实例的兴趣列表,并设置ev.enents = EPOLLIN,表明对该listen_fd关注其是否可读。
代码第153行epoll_wait收集处于就绪态的文件描述符。返回值-1表明出错,返回值为0表明无处于就绪态的文件描述符。
代码第164行将进入for循环,遍历处于就绪态的文件描述符,接着先判断处于就绪态的文件描述符是否出错或者对端关闭,并作相应处理,注意,如果出现错误,需要将该文件描述符从epoll实例的兴趣列表中删除。
代码174行判断是否是新来的请求连接的文件描述符,如果是,则accept,并将accept放回的代表该客户端的connect_fd 加入epoll实例的兴趣列表,并设置ev的events字段。
如果不是新来的请求连接的客户端,则说明是已连接的客户端有数据到来,于是就应该read,read出错做相应处理,成功也做相应处理。