ShadowBurn 2017-01-31
一、epoll简介
epoll是Linux内核为处理大批量文件描述符而作了改进的poll, 是Linux下多路复用IO接口select/poll的增强版本, 它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候, 它无须遍历整个被侦听的描述符集, 只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
相关文件到安科网资源站下载:
------------------------------------------分割线------------------------------------------
具体下载目录在 /2017年资料/1月/31日/Linux IO多路复用 epoll 心得/
------------------------------------------分割线------------------------------------------
二、epoll的API函数
1. 句柄创建函数
int epoll_create(int size);
创建一个epoll的句柄, size用来告诉内核这个监听的数目一共有多大。
int epoll_create1(int flag);
这个函数是在linux 2.6.27中加入的, 其实它和epoll_create差不多, 不同的是epoll_create1函数的参数是flag。
当flag是0时, 表示和epoll_create函数完全一样, 不需要size的提示了。
当flag = EPOLL_CLOEXEC, 创建的epfd会设置FD_CLOEXEC, 它是fd的一个标识说明, 用来设置文件close-on-exec状态的。
当flag = EPOLL_NONBLOCK, 创建的epfd会设置为非阻塞。
2. 事件操作函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
第一个参数epfd, 为epoll_create返回的的epoll文件描述符。
第二个参数op表示操作值。有三个操作类型:
EPOLL_CTL_ADD //注册目标fd到epfd中, 同时关联内部event到fd上
EPOLL_CTL_MOD //修改已经注册到fd的监听事件
EPOLL_CTL_DEL //从epfd中删除/移除已注册的fd, event可以被忽略, 也可以为NULL
第三个参数fd表示需要监听的fd。
第四个参数event表示需要监听的事件。
event参数是一个枚举的集合, 可以用“|”来增加事件类型, 枚举如下:
// EPOLLIN: 表示关联的fd可以进行读操作了。
// EPOLLOUT: 表示关联的fd可以进行写操作了。
// EPOLLRDHUP(since Linux 2.6.17): 表示套接字关闭了连接, 或者关闭了正写一半的连接。
// EPOLLPRI: 表示关联的fd有紧急优先事件可以进行读操作了。
// EPOLLERR: 表示关联的fd发生了错误, epoll_wait会一直等待这个事件, 所以一般没必要设置这个属性。
// EPOLLHUP: 表示关联的fd挂起了, epoll_wait会一直等待这个事件, 所以一般没必要设置这个属性。
// EPOLLET: 设置关联的fd为ET的工作方式, epoll的默认工作方式是LT。
// EPOLLONESHOT(since Linux 2.6.2): 设置关联的fd为one-shot的工作方式。表示只监听一次事件, 如果要再次监听, 需要把socket放入到epoll队列中。
3. 事件等待函数
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
上面两个函数的参数含义:
第一个参数:表示epoll_wait等待epfd上的事件。
第二个参数:events指针携带有epoll_data_t数据。
第三个参数:maxevents告诉内核events有多大, 该值必须大于0。
第四个参数:timeout表示超时时间(单位: 毫秒), 为0的时候表示马上返回, 为-1的时候表示一直等下去, 直到有事件返回, 为任意正整数的时候表示等这么长的时间, 如果一直没有事件, 则返回。一般情况下, 如果网络主循环是单独的线程的话, 可以用-1来等, 这样可以保证一些效率, 如果是和主逻辑在同一个线程的话, 则可以用0来保证主循环的效率。
epoll_pwait(since linux 2.6.19)允许一个应用程序安全的等待, 直到fd设备准备就绪, 或者捕获到一个信号量。其中sigmask表示要捕获的信号量。
函数如果等待成功, 则返回fd的数字; 0表示等待fd超时, 其他错误号请查看errno。
4. 句柄关闭函数
int close(int fd);
返回值: 若文件顺利关闭则返回0, 发生错误时返回-1。
三、epoll的2种触发模式
1. Level Triggered (LT) 水平触发
LT是epoll默认的触发方式, 如下:
socket接收缓冲区不为空, 有数据可读, 则读事件一直触发;
socket发送缓冲区不满, 可以继续写入数据, 则写事件一直触发;
LT的处理过程:
accept一个连接, 添加到epoll中监听EPOLLIN事件;
当EPOLLIN事件到达时, read fd中的数据并处理;
当需要写入数据时, 先直接把数据write到fd中; 如果数据较大, 无法一次性写入, 那么在epoll中监听EPOLLOUT事件;
当EPOLLOUT事件到达时, 继续把数据write到fd中; 如果数据写入完毕, 那么在epoll中关闭EPOLLOUT事件;
2. Edge Triggered (ET) 边沿触发
socket的接收缓冲区状态变化时触发读事件, 即空的接收缓冲区刚接收到数据时触发读事件;
socket的发送缓冲区状态变化时触发写事件, 即满的缓冲区刚空出空间时触发读事件;
仅在状态变化时触发事件
ET的处理过程:
accept一个连接, 添加到epoll中监听EPOLLIN|EPOLLOUT事件;
当EPOLLIN事件到达时, read fd中的数据并处理, read需要一直读, 直到返回EAGAIN为止;
当需要写出数据时, 把数据write到fd中, 直到数据全部写完, 或者write返回EAGAIN;
当EPOLLOUT事件到达时, 继续把数据write到fd中, 直到数据全部写完, 或者write返回EAGAIN;
ET模式下, 正确的accept要考虑2个问题:
(1) 阻塞模式下, accept存在的问题
考虑这种情况: TCP连接被客户端夭折, 即在服务器调用accept之前, 客户端主动发送RST终止连接, 导致刚刚建立的连接从就绪队列中移出, 如果套接口被设置成阻塞模式, 服务器就会一直阻塞在accept调用上, 直到其他某个客户建立一个新的连接为止。但是在此期间, 服务器单纯地阻塞在accept调用上, 就绪队列中的其他描述符都得不到处理。
解决办法是把监听套接口设置为非阻塞, 当客户在服务器调用accept之前中止某个连接时, accept调用可以立即返回-1, 这时源自Berkeley的实现会在内核中处理该事件, 并不会将该事件通知给epoll, 而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。
(2)ET模式下accept存在的问题
考虑这种情况: 多个连接同时到达, 服务器的TCP就绪队列瞬间积累多个就绪连接, 由于是边缘触发模式, epoll只会通知一次, accept只处理一个连接, 导致TCP就绪队列中剩下的连接都得不到处理。
解决办法是用while循环抱住accept调用, 处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。
综合以上两种情况, 服务器应该使用非阻塞地accept, accept在ET模式下的正确使用方式为:
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
handle_client(conn_sock);
}
if (conn_sock == -1) {
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
perror("accept");
}
3. 总结
从ET的处理过程中可以看到, ET的要求是需要一直读写, 直到返回EAGAIN, 否则就会遗漏事件。而LT的处理过程中, 直到返回EAGAIN不是硬性要求, 但通常的处理过程都会读写直到返回EAGAIN, 但LT比ET多了一个开关EPOLLOUT事件的步骤。LT的编程与poll/select接近,符合一直以来的习惯,不易出错。ET的编程可以做到更加简洁,某些场景下更加高效,但另一方面容易遗漏事件,容易产生bug。