一、文件描述符就绪
epoll用来同时监听多个文件描述符是否就绪,那么哪些情况下文件描述符可以被认为是可读、可写或者异常呢?
1、文件描述符可读的情况
- socket内核接收缓冲区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地读,并且读操作返回的字节数大于0.
- socket通信的对方关闭连接。此时对该socket的读操作将返回0.
- 监听socket上有新的连接请求。
- socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
2、文件描述符可写的情况
- socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写,并且写操作返回的字节数大于0.
- socket的写操作被关闭。对写操作关闭的socket执行写操作将触发一个SIGPIPE信号。
- socket使用非阻塞connect连接成功或者失败(超时)之后。
- socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。
二、系统调用简介
epoll将用户关心的文件描述符上的事件放在内核的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件表述符集或事件集。
- int epoll_create(int size)这个函数用来返回一个标识内核事件表的文件描述符。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)这个函数可以向内核事件表中注册、修改和删除fd上的事件。
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)这个函数用来等待一组文件描述符上的事件,如果其检测到事件,就将所有就绪的事件从内核事件表中复制到第二个参数events指向的数组。这个数组只包含检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。
三、LT和ET模式
LT(level-triger)和ET(edge-triger)是epoll对文件描述符的操作的两种模式,二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。换句话说,如果epoll_wait通知有事件发生后,应用程序没有立即处理该事件(readable或writable状态依然存在),应用程序下一次调用epoll_wait函数会有不同的表现:
- LT模式下epoll_wait会再次向应用程序通告此事件(因为socket依然处于readable/writable状态);
- ET模式下epoll_wait将不再通知这一事件(因为socket的readable/writable状态没有变化)。epoll默认工作在LT模式。
- 读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
- 写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
在一个非阻塞的socket上调用read/write函数返回-1且errno等于EAGAIN或者EWOULDBLOCK时,表示资源暂时不够,read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种情况,如果是阻塞socket,read/write就要阻塞掉。
所以,对于阻塞socket,read/write返回-1代表网络出错了。但对于非阻塞socket,read/write返回-1不一定网络真的出错了。
综上,对于non-blocking的socket,正确的读写操作为:
- 读:忽略掉errno = EAGAIN的错误,下次继续读
- 写:忽略掉errno = EAGAIN的错误,下次继续写
考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。
解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。
2、ET模式下accept存在的问题
考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。
解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。