我们首先先了解一下什么是I/O复用:
我们用一个单进程或者单线程的服务器程序去监听多个文件描述符上是否有关注的事件发生,如果某些文件描述符上有事件发生,则程序接着处理有事件发生的文件描述符,其他的不用理会,这样就可以极大的提高程序的性能。
常用的I/O复用技术有select,poll,epoll
我们介绍前两种,主要探索第三种epoll
一:select
#include
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中又有四个函数用来控制文件描述符集合:
#include
所以之后提出了poll
二:poll
#include
int poll(struct pollfd fds[], unsigned int nfds, long timeout);
struct pollfd
{
int fd; //文件描述符
short events; //注册的事件,监听的事件
short revents; //就绪的事件,由内核填充
};
poll的事件类型:
poll的缺陷:虽然相比于select,poll解决了前两个问题,监听的文件描述符也多了,监听的事件也不仅仅是读写和异常了,但是第三个问题还是没有解决(poll和select返回的都是已就绪的文件描述符个数,哪些就绪哪些没有就绪,依旧得遍历一遍,遍历就绪的文件描述符的时间复杂度O(n))
需要注意的是在linux2.6之后使用的是epoll,因为epoll完美的解决的了poll和select存在的问题。
三:epoll
epoll的用法:将原来的poll和select调用分成了三个部分
#include
①:int epoll_create(int size); //创建内核事件表,返回事件表标识
epoll_create()函数是用来创建一个内核事件表,也就是创建一个文件,底层结构是红黑树。
epoll_create()调用会返回一个文件描述符,之后所有的使用都通过这个文件描述符来访问。
②:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //对内核事件表的操作,比如插入,删除,修改。
epoll_ctl系统调用,通过此调用可以向内核事件表中添加,删除,修改感兴趣的事件,返回0表示成功,-1表示失败
int op:EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL
struct epoll_event
{
__uint32_t events; //epoll事件
epoll_data data; //用户数据 存放fd
}
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
③:int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
//epoll_wait()系统调用,通过此调用可以收集已经就绪的事件
①:当进程调用epoll_create()的时候,linux内核会创建一个eventpoll结构体出来,这个结构体中有两个成员,具体结构如下:
struct eventpoll
{
struct rb_root rbr; //红黑树的根节点,这个树中存储着所有添加到epoll中需要监控的事件
struct list_head rdlist; //双链表中存储着将要通过epoll_wait返回给用户就绪的事件
};
特别注意:
struct epitem
{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
epoll数据结构示意图:
内核事件表的底层数据结构是红黑树(插入的是关注的事件)
就绪描述符的底层数据结构是双链表(插入的是就绪的对象)
epoll_wait()的功能就是不断查看rdlist双链表中是否有epitem结构体,如果没有就一直检查,直到超时,如果有,则将其fd收集返回给用户。
这个时候需要介绍一下ET和LT模式:
LT模式:电平触发
当epoll_wait检测到文件描述符上有事件发生,并将此事件通知应用程序之后,应用程序可以不立即处理该事件,当下次调用epoll_wait时,还会向应用程序通知这个事件,直到此事件被处理。
如果用户没有处理就绪的文件描述符或者没有处理完,则内核会再次提醒
ET模式:当epoll_wait检测到文件描述符上有事件发生,并将此事件通知应用程序之后,应用程序必须立即处理该事件,并且需要将该事件处理完成,因为epoll_wait下次再被调用时,不会再向应用程序通知该事件。从而降低了同一事件被重复触发的次数,从而效率比LT模式高一些。
内核只会将就绪描述符通知用户一次,如果用户没有处理就绪的文件描述符或者没有处理完,则内核不会再次提醒,只能等下次事件触发,内核将fd重新插入到rdllist中去。
ET高效模式是epoll系统调用独有的,ET就是边缘触发的意思,有了ET模式,重复的事件就不会总出来打扰程序的判断,故而经常被使用,那么EPOLLET的原理是什么呢?
上面我们讲到,epoll给fd关注的事件都挂上了一个回调函数ep_epoll_callback,当关注的事件就绪时,将fd放入到rdllist双链表中,这样epoll_wait只需要检查这个rdllist双链表就可以知道哪些fd有事件就绪了。
那么把rdllist里的fd拷贝到用户空间,这个任务是ep_events_transfer这个函数做的:
其中关键步骤是将rdllist中的fd挪到txlist里(挪完rdllist就空了),接着才将txlist中的fd返回给用户空间,但是最后会有一部分的fd从txlist中“返还”给rdllist,以便下次还能从rdllist中拷贝。
那么是将哪一部分fd返还给了rdllist呢?
我们可以看源代码中的判断:
重新被返还给rdllist的fd,是“没有标上EPPOLLET模式”且“事件还被关注”了的fd。
那么下次epoll_wait当然会又把rdllist中的fd拿出来拷贝给用户空间了。
四:最后,我们将select,poll,以及epoll做一个总结对比: