标签:
先回忆下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支持的文件描述符数量较小,一般只有1024,poll虽然没有这个限制,但基于上面两个原因,poll和select存在同样一个缺点,就是包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而且不论这些文件描述符是否就绪,每次都会轮询所有描述符的状态,使得他们的开销随着文件描述符数量的增加而线性增大。epoll针对这几个缺点进行了改进,不再像select和poll那样,每次调用select和poll都把描述符集合拷贝到内核空间,而是一次注册永久使用;另一方面,epoll也不会对每个描述符都轮询时间是否发生,而是只针对事件已经发生的文件描述符进行资源抢占(因为同一个描述符资源(如可读或可写)可能阻塞了多个进程,调用epoll的进程需要与这些进程抢占该相应资源)。下面记录一下自己对epoll的学习和理解。
上面说到每次调用select和poll都把描述符集合拷贝到内核空间,这是因为select和poll注册事件和监听事件是绑定在一起的,为甚这么说呢,我们看select和poll的编程模式就明白了:
while(true){
select(maxfd+1,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);
}
#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的源码请参考这两份资料
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl用于用户告知内核自己关心哪个描述符(fd)的什么事件(event),
参数event的结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable,内核会修改该属性 */
};
events可以是以下几个宏的集合:
重点说一下这个取值,当op=EPOLL_CTL_ADD时,epoll_ctl主要做了四件事:
注:关于epoll_ctl、ep_ptable_queue_proc、ep_poll_callback的原理及源码请参考这两份资料
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait函数的原理就是去检查上面提到的rdlist链表中每个结点,rdlist的每一个结点能够索引到监听的文件描述符,就可以调用该文件描述符对应设备的poll驱动函数f_op->poll,用以检查该设备是否可用。这里有个问题需要思考一下,既然rdlist就表示就绪的事件,也就是设备对应的资源可用了,为什么还要进行检查?这是因为设备的某个资源可能被多个进程等待,当设备资源准备好后,设备会唤醒阻塞在这个资源上的所有进程,当前调用epoll_wait的进程未必能抢占这个资源,所以需要再调用检查一次资源是否可用,以防止被其他进程抢占而导致再次不可用,检查的方法就是调用fd设备的驱动f_op->poll。
这也是为什么epoll效率可能比较高的原因,epoll每次只检查已经就绪的设备,不像select、poll,不管有没有就绪,都去检查。
注:关于epoll_wait的原理及源码请参考这两份资料
二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。下面两幅图清晰反映了二者区别,这两幅图摘自Epoll在LT和ET模式下的读写方式
参考资料:
标签:
原文地址:http://www.cnblogs.com/zengzy/p/5118336.html