I/O多路复用之epoll
1、select、poll的些许缺点
先回忆下select和poll的接口
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
这两个多路复用实现的特点是:
- 每次调用select和poll都要把用户关心的事件集合(select为readfds,writefds,exceptfds集合,poll为fds结构体数组)从用户空间到内核空间。
- 如果某一时间段内,只有少部分事件是活跃的(用户关心的事件集合只有少部分事件会发生),会浪费cpu在对无效事件轮询上,使得效率较低,比如,用户关心1024个tcp socket的读事件,当是,每次调用select或poll时只有1个tcp链接是活跃的,那么对其他1023个事件的轮询是没有必要的。
select支持的文件描述符数量较小,一般只有1024,poll虽然没有这个限制,但基于上面两个原因,poll和select存在同样一个缺点,就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而且不论这些文件描述符是否就绪,每次都会轮询所有描述符的状态,使得他们的开销随着文件描述符数量的增加而线性增大。epoll针对这几个缺点进行了改进,不再像select和poll那样,每次调用select和poll都把描述符集合拷贝到内核空间,而是一次注册永久使用;另一方面,epoll也不会对每个描述符都轮询时间是否发生,而是只针对事件已经发生的文件描述符进行资源抢占(因为同一个描述符资源(如可读或可写)可能阻塞了多个进程,调用epoll的进程需要与这些进程抢占该相应资源)。下面记录一下自己对epoll的学习和理解。
2、epoll的几个接口
上面说到每次调用select和poll都把描述符集合拷贝到内核空间,这是因为select和poll注册事件和监听事件是绑定在一起的,为甚这么说呢,我们看select和poll的编程模式就明白了:
while(true){
select(maxfd+,readfds,writefds,execpfds,timeout)/poll(pollfd,nfds,timeout);
}
在I/O多路复用之select中说到了select的实现,调用select时就会进行一次用户空间到内核空间的拷贝。epoll的改进其实就是把注册事件和监听事件分开了,epoll使用了一个特殊的文件来管理用户关心的事件集合,这个文件存在于内核之中,由特殊的数据结构和一组操作构成,这样的话,用户就可以提前告知内核自己关心的事件,然后再进行监听,因此,就只需要一次用户空间到内核空间的拷贝了。其中管理事件集合的文件通过epoll_create创建,注册用户行为通过epoll_ctl实现,监听通过epoll_wait实现。那么编程模型大概是这个样子:
epoll_fd=epoll_create(size);
epoll_ctl(epoll_fd,operation,fd,event);
while(true){
epoll_wait(epoll_fd,events,max_events,timeout);
}
2.1、epoll_create接口
#include <sys/epoll.h>
int epoll_create(int size);
epoll_create创建epoll文件,其返回epoll的句柄,size用来告诉内核监听文件描述符的最大数目,这个参数不同于select()中的第一个参数(给出最大监听的fd+1的值)。需要注意的是,当创建好epoll句柄后,它会占用一个fd值,在linux下如果查看/proc/进程id/fd/,能够看到这个fd,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。(摘自epoll精髓)
epoll_create会在内核初始化完成epoll所需的数据结构,其中一个关键的结构就是rdlist,表示就绪的文件描述符链表,epoll_wait函数就是直接检查该链表,从而抢占准备好的事件;另一个关键的结构是一颗红黑树,这棵树专门用于管理用户关心的文件描述符集合。
注:关于epoll文件的核心数据结构以及epoll_create的源码请参考这两份资料
2.2、epoll_ctl接口
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl用于用户告知内核自己关心哪个描述符(fd)的什么事件(event),
- epfd,使用epoll_create函数创建的epoll句柄,epfd文件描述符对应的结构中,有一颗红黑树,专门用于管理用户关心的事件集合。
- op,用于指定用户行为,op参数有三种取值:fd,用户关心的文件描述符
- EPOLL_CTL_ADD,注册新的fd到epfd中;
- EPOLL_CTL_MOD,修改已注册fd的事件;
- EPOLL_CTL_DEL,从epfd中删除一个fd;
- event,用户关心的事件(读,写)
参数event的结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable,内核会修改该属性 */
};
events可以是以下几个宏的集合:
- EPOLLIN ,表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT,表示对应的文件描述符可以写;
- EPOLLPRI,表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR,表示对应的文件描述符发生错误;
- EPOLLHUP,表示对应的文件描述符被挂起;
- EPOLLET,将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT,只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
2.2.1、EPOLL_CTL_ADD
重点说一下这个取值,当op=EPOLL_CTL_ADD时,epoll_ctl主要做了四件事:
- 把当前文件描述符及其对应的事件(fd,epoll_event)加入红黑树,便于内核管理
- 注册设备驱动poll的回调函数ep_ptable_queue_proc,当调用f_op->poll()时,最终会调用该回调函数ep_ptable_queue_proc()
- 在ep_ptable_queue_proc回调函数中,注册回调函数ep_poll_callback,ep_poll_callback表示当描述符fd上相应的事件发生时该如何告知进程。
- 在ep_ptable_queue_proc回调函数中,检测是文件描述符fd对应的设备的epoll_event事件是否发生,如果发生则把fd及其epoll_event加入上面提到的就绪队列rdlist中
注:关于epoll_ctl、ep_ptable_queue_proc、ep_poll_callback的原理及源码请参考这两份资料
2.3、epoll_wait接口
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- epfd,使用epoll_create函数创建的epoll句柄,epfd文件描述符对应的结构中,有一颗红黑树,专门用于管理用户关心的事件集合。
- events,传出参数,表示发生的事件
- maxevents,传入参数,表示events数组的最大容量,其值不能超过epoll_create函数的参数size
- timeout,0,不阻塞;整数,阻塞timeout时间;负数,无限阻塞
epoll_wait函数的原理就是去检查上面提到的rdlist链表中每个结点,rdlist的每一个结点能够索引到监听的文件描述符,就可以调用该文件描述符对应设备的poll驱动函数f_op->poll,用以检查该设备是否可用。这里有个问题需要思考一下,既然rdlist就表示就绪的事件,也就是设备对应的资源可用了,为什么还要进行检查?这是因为设备的某个资源可能被多个进程等待,当设备资源准备好后,设备会唤醒阻塞在这个资源上的所有进程,当前调用epoll_wait的进程未必能抢占这个资源,所以需要再调用检查一次资源是否可用,以防止被其他进程抢占而导致再次不可用,检查的方法就是调用fd设备的驱动f_op->poll。
这也是为什么epoll效率可能比较高的原因,epoll每次只检查已经就绪的设备,不像select、poll,不管有没有就绪,都去检查。
注:关于epoll_wait的原理及源码请参考这两份资料
3、epoll的两种触发模式ET<
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket,et模式注重的是状态发生改变的时候才触发。下面两幅图清晰反映了二者区别,这两幅图摘自Epoll在LT和ET模式下的读写方式

在ET模式下,在使用epoll_ctl注册文件描述符的事件时,应该把描述符设置为非阻塞,为什么呢?以上面左边这幅图为例,当数据到来之后,该socket实例从不可读状态边为可读状态,从该socket读取一部分数据后,再次调用epoll_wait,由于socket的状态没有发生改变(buffer上一次空到有数据可读触发了et,而这一次buffer还有数据可读,状态没改变),所以该次调用epoll_wait并不会返回这个socket的可读事件,而且之后也不会再发生改变,这个socket实例将永远也得不到处理。这就是为什么将监听的描述符设置为非阻塞的原因。
使用ET模式时,正确的读写方式应该是这样的:
设置监听的文件描述符为非阻塞
while(true){
epoll_wait(epoll_fd,events,max_evens);
读,只要可读,就一直读,直到返回0,或者-,errno=EAGAIN/EWOULDBLOCK
}
正确的写方式应该是这样的:
设置监听的文件描述符为非阻塞
while(true){
epoll_wait(epoll_fd,events,max_evens);
写,只要可写,就一直写,直到返回0,或者-,errno=EAGAIN/EWOULDBLOCK
}
4、两个问题
使用单进程单线程IO多路复用,服务器端该如何正确使用accept函数?
应该将监听的socket实例设置为非阻塞。
使用io多路复用时,一般会把监听连接的socket实例listen_fd交给select、poll或epoll管理,如果使用阻塞模式,假设,select、poll或epoll调用返回时,有大量描述符的读或写事件准备好了,而且listen_fd也可读,
我们知道,从select、poll或epoll返回到调用accept接收新连接是有一个时间差的,如果这个时间内,发起请求的一端主动发送RST复位请求,服务器会把该连接从ACCEPT队列(socket原理详解,3.6节)中取出,并把该连接复位,这个时候再调用accept接收连接时,服务器将被阻塞,那其他的可读可写的描述符将得不到处理,直到有新连接时,accept才得以返回,才能去处理其他早已准备好的描述符。所以应该将listen_fd设置为非阻塞。
腾讯后台开发面试题。使用Linux epoll模型,LT触发模式,当socket可写时,会不停的触发socket可写的事件,但并不总是需要写,该如何处理?
第一种最普遍的方式,步骤如下:
- 需要向socket写数据的时候才把socket加入epoll,等待可写事件。
- 接受到可写事件后,调用write或者send发送数据,直到数据写完。
- 把socket移出epoll。
这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移出epoll,有一定操作代价。
一种改进的方式,步骤如下:
- 设置socket为非阻塞模式
- 调用write或者send发送数据,直到数据写完
- 如果返回EAGAIN,把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。
这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。
参考资料:
Epoll在LT和ET模式下的读写方式(搞不懂这两个谁是原创,很多同样的博文,都标志着原创的字样)
I/O多路复用之epoll的更多相关文章
- 用C写一个web服务器(二) I/O多路复用之epoll
.container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .conta ...
- python 网络编程 IO多路复用之epoll
python网络编程——IO多路复用之epoll 1.内核EPOLL模型讲解 此部分参考http://blog.csdn.net/mango_song/article/details/4264 ...
- IO多路复用之epoll总结
1.基本知识 epoll是在2.6内核中提出的,是之前的select和poll的增强版本.相对于select和poll来说,epoll更加灵活,没有描述符限制.epoll使用一个文件描述符管理多个描述 ...
- 知识联结梳理 : I/O多路复用、EPOLL(SELECT/POLL)、NIO、Event-driven、Reactor模式
为了形成一个完整清晰的认识,将概念和关系梳理出来,把坑填平. I/O多路复用 I/O多路复用主要解决传统I/O单线程阻塞的问题.它通过单线程管理多个FD,当监听的FD有状态变化的时候的,调用回调函数, ...
- I/O多路复用之epoll实战
概念 IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程 通俗理解(摘自网上一大神) 这些名词比较绕口,理解涵义就好.一个epoll场景:一个酒吧服务员(一个线程),前 ...
- unix网络编程——I/O多路复用之epoll
1. 基本概念 当程序进行IO时,如果数据尚未准备好,那么IO将处于阻塞状态.当某个进程有多个打开的文件,比如socket,那么其后的所有准备好读写的文件将受到阻塞的影响而不能操作.不借助线程,单一进 ...
- IO多路复用之epoll
1.基本知识 epoll是在2.6内核中提出的,是之前的select和poll的增强版本.相对于select和poll来说,epoll更加灵活,没有描述符限制.epoll使用一个文件描述符管理多个描述 ...
- 关于非阻塞I/O、多路复用、epoll的杂谈
本文主要是想解答一下这样几个问题: - 什么是非阻塞I/O - 非阻塞I/O和异步I/O的区别 - epoll的工作原理 文件描述符 文件描述符在本文有多次出现,难免有的朋友不太熟悉,有必要简单说明一 ...
- IO多路复用与epoll机制浅析
epoll是Linux中用于IO多路复用的机制,在nginx和redis等软件中都有应用,redis的性能好的原因之一也就是使用了epoll进行IO多路复用,同时epoll也是各大公司面试的热点问题. ...
随机推荐
- OpenGL ES 2.0 曲面物体的构建
球体构建的基本原理构建曲面物体最重要的就是找到将曲面恰当拆分成三角形的策略. 最基本的策略是首先按照一定的规则将物体按行和列两个方向进行拆分,这时就可以得到很多的小四边形.然后再将每个小四边形拆分成两 ...
- Mvc htmlhelper that generates a menu from a controller
Simple menu system that grabs a list of actions from a single controller and creates an unordered li ...
- Nutshell.ThreadWorkerPool .Net线程池设计
功能描述: 支持创建多个线程池,并统一管理 支持不同线程池的容量控制,以及最少活动线程的设置 支持不同线程池中活动线程的闲时设置,即线程空闲时间到期后即自动被回收 结构设计: ThreadWorker ...
- 临时表妙用、连表更新、sqlserver group contant
一.临时表妙用 -- 1.将老库中的mediaid和新库中的regionid对应上,然后插入到临时表中 SELECT * INTO #TempMediaRegion FROM (SELECT ww.C ...
- 迁移笔记:php截取文字的方法
php内置函数 1. iconv iconv_set_encoding('internal_encoding', 'UTF-8'); $str; //字符串的声明 $num=iconv_strlen( ...
- #Java编程题-百钱百鸡
问题: 百钱百鸡问题.用100钱买100只鸡,公鸡一只五钱,母鸡一只三钱,雏鸡三只一钱,编程计算共有几种买法(要求每种鸡至少要买1只). 自己的实现,没有什么数据结构,算法,求大神指点!! packa ...
- Winform(C#)限制程序只运行一个实例
C#控制只运行开启一个程序 在这个例子中不需要调用ReleaseMutex,mutex会在程序结束时自动释放.为了防止mutex过早释放,在程序的最后调用下GC.KeepAlive (mutex). ...
- win7 x64 jdk1.7.0_51
1:我的 jdk与jre默认安装在:D:\Program Files\Java 2:配置环境变量(系统变量): (1)新建JAVA_HOME (2)新建CLASSPATH (3)编辑Path,%JAV ...
- 关于hash
http://rapheal.iteye.com/blog/1142955 关于javascript hash
- 福建省队集训被虐记——DAY1
今天算是省冬的第一天--早上柯黑出题,说是"信心欢乐赛",其实是"使你失去信心.不再欢乐的比赛" 顺便orz一下来看这篇文章的各路神犇--求轻虐 水题 (py. ...