一、几个概念点
先来说几个概念:eventpoll结构体(fd管理器)、ep_poll_callback(回调)、rdlist(双向链表)、epitem(epoll管理的结点)
linux内核提供的epoll包括三个函数,分别是:epoll_create()、epoll_ctl、epoll_wait
1、int epoll_create(int size);
【简介】创建一个epoll的句柄。该函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
【参数】size用来告诉内核这个监听的数目一共有多大,自从linux2.6.8之后,size参数是被忽略的,但是依然要大于0。
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
【简介】一个epoll的描述符的控制接口,该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
【参数】epfd:由 epoll_create 生成的epoll专用的文件描述符
op:要进行的操作例如注册事件
fd:关联的文件描述符,可以是需要监听的socket句柄,或连接的socket句柄
event:指向epoll_event的指针
3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
【简介】该函数用于轮询I/O事件的发生
【参数】epfd:由 epoll_create 生成的epoll专用的文件描述符
events:返回的事件数组
maxevents:每次能处理的最大事件数
timeout:等待I/O事件发生的超时值
二、redis中epoll的使用
- 首先redis会调用epoll_create()建立一个epoll对象(在内核申请一空间)
- 调用epoll_ctl向epoll对象中添加连接的套接字
- 调用epoll_wait收集发生的事件的连接
三、底层实现
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体。eventpoll结构体如下所示:
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
总结来说:
我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
可以说:内核中红黑树+双向链表+回调机制决定了epoll的高效低耗。epoll基本上不受连接数影响。
四、epoll有2种工作方式:LT和ET
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET (edge-triggered)是高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。
另外:epoll 没有使用mmap加速内核与用户空间的消息传递
知乎上有个帖子讲的很好: