前言
EPOLL是LINUX独有的I/O复用函数。其将用户关心的文件描述符(fd)上的事件放在内核里的一个事件表中。Select和Poll会在每次调用的时候线性扫描全部的Socket集合,而Epoll是通过向内核去注册回调事件的方式,只有活跃的socket才会主动的调用callback函数,如果不是所有socket都活跃的情况下,效率相较于select/poll有很大的提升。
epoll
epoll主要为事件驱动,有三个主要的系统调用:
- epoll_create: 创建一个epoll句柄,占用一个fd
- epoll_ctl:向句柄注册epoll事件,参数包括,epoll的句柄值,执行的动作(注册,修改,删除),需要监听的fd,需要监听的事件(struct epoll_event)
- epoll_wait:收集在epoll监控的事件中已经发生的事件,返回就绪的事件数目,同时将就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中,使用的时候直接遍历数组即可,不需要遍历所有已注册的文件描述符
epoll对fd的操作有LT(水平触发,默认)和ET(边缘触发,高效)两种方式

区别在于:使用LT的fd,当epoll_wait检测到fd上有事件发生通知应用程序时,应用程序可以不立即处理。当程序下一次调用epoll_wait时,仍然会向应用程序通知这件事,直到事件被处理。而使用ET的fd,当有事件发生,epoll_wait通知应用程序时,应用程序必须立即处理该事件,后续的epoll_wait调用将不再向应用程序通知这件事情
服务端循环调用epoll_wait,监听客户端fd是否有可读事件
1 | while (1) |
使用LT
lt的处理代码,当lt监测到可读事件时从缓冲区读取数据
1 | void lt(epoll_event *events, int number, int epollfd, int listenfd) |
测试:使用客户端连接服务端,发送“123456789”(9字节)的数据,服务端设置了一次从缓冲区读取4字节

可以看到第一次epoll_wait检测到事件发生,应用程序从缓冲区中读取了4字节,接着下一次epoll_wait仍然会通知应用程序有可读事件
使用ET
et模式下由于事件只会通知一次,所以我们需要在et模式的处理函数中,对缓冲区进行循环读取,一次性将数据读取完毕
1 | void et(epoll_event *events, int number, int epollfd, int listenfd) |
测试:步骤同上

可以看到在一个Et的工作流程中将缓冲区的数据全部读取
注意
使用Et模式的文件描述符,一定要设置成非阻塞,设置方法:
1 | //使用ET模式的文件描述符都应该是非阻塞的。如果文件描述符是阻塞的,读和写操作都会因为没有后续事件而处于阻塞状态 |
原因在于,上面的ET处理代码里在while循环中不断的用recv()函数读取缓冲区,如果设置为阻塞的话,当文件描述符上没有内容可读,或者没有空间可写,那么读和写事件会因为没有后续事件而一直阻塞。而当一个非阻塞IO当你去读写时,无论可不可以读写,都会立即返回。返回成功的话就说明读写完成,返回失败的话就会设置相应的errno状态码,这时就可以根据errno状态码来进行进一步处理。如ET代码中:
1 | while (1) |
EAGAIN:应用程序现在没有数据可读请稍后再试。或者当系统调用(比如fork)没有足够资源的时候,返回EAGAIN提示你再试一次
EWOULDBLOCK:同EAGAIN,Windows情况下
