前言
前面讲了IO多路复用的API,select和poll的缺点是性能不够,客户端连接越多性能下降越明显,epoll的出现解决了这个问题,引用The Linux Programming Interface的一个统计对比如下:
fd数量 poll CPU时间(秒) select CPU时间(秒) epoll CPU时间(秒)
---------------------------------------------------------------------
10 0.61 0.73 0.41
100 2.9 3.0 0.42
1000 35 35 0.53
10000 990 930 0.66
---------------------------------------------------------------------
可以看出fd达到100个以后,select/poll就非常慢了,而epoll即使达到10000个也表现得非常好,因为:每次调用select/poll,内核必须检查所有传进来的描述符;而对于epoll,每次调用epoll_ctl,内核会把相关信息与底层的文件描述关联起来,当IO事件就绪时,内核把信息加到epoll的就绪列表里。随后调用epoll_wait,内核只需把就绪列表中的信息提取出来返回即可。
每次调用select/poll,都要把待监控的所有文件描述符传给内核,函数返回时,内核要把描述符返回并标识哪些就绪,得到结果后还要逐个判断所有描述符,才能确定哪些有事件;epoll在调用epoll_ctl时就已经维护着监控的列表,epoll_wait不需要传入任何信息,并且返回的结果只包含就绪的描述符,这样就不用去判断所有描述符。
从概念上理解epoll是这样的,把要监控的fd的IO事件注册给epoll(调用epoll_ctl),然后调用epoll的API等待事件到达(调用epoll_wait),内核可能对每个fd维护着一个读和写的缓冲区,那么:如果我监控读事件,并且读缓冲区有数据了,epoll_wait就会返回,此时我可以调用read读数据。
如果我监控写事件,并且写缓存区未满,epoll_wait也会返回,此时我可以调用write写数据。
如果fd发生了一些错误,epoll_wait也会返回,此时我根据返回的标志位,就可以知道。
如果我监控读事件 并且有客户端连接进来,epoll_wait就会返回,此时我可以调用accept接受客户端。
epoll的API介绍int epoll_create(int size);
创建一个epoll实例,返回代表实例的文件描述符(fd),size自Linux 2.6.8以后忽略,但必须大于0.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll控制接口,epfd就是epoll的文件描述符,fd是要操作的文件描述符,op有如下几种:EPOLL_CTL_ADD 注册fd的事件,事件类型在event指定。
EPOLL_CTL_MOD 修改已注册的fd事件。
EPOLL_CTL_DEL 删除fd的事件。
epoll_event有一个events成员,指定要注册的事件类型,比较重要的几个:EPOLLIN fd可读事件
EPOLLOUT fd可写事件
EPOLLERR fd发生错误,这个事件总是会被监控,不必手动增加
EPOLLHUP fd被挂起时,这个事件总是会被监控,不必手动增加,这通常发生在socket异常关闭时,此时read返回0,然后正常的清理socket资源。
epoll_event还有一个epoll_data_t成员,由外部设置自定义数据,以方便后续处理。int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件发生,如果没有事件发生,线程会被挂起,maxevents指定最大事件数,events外部传入的事件数组,长度应当等于maxevents,当事件发生时,epoll会把事件信息填到这里, timeout指定等待的最大时间, 0表示马上返回,-1表示无限等待。
epoll_wait返回等待到的事件数,返回时,遍历events对fd进行处理。 当epoll不再使用时,应该调用close关闭epollfd。
水平触发和边