应用场景
Linux下多线程编程经常会碰到,一个线程需要响应多个外部事件。如C/S架构下Server端即需要处理Client发起的连接(Server Socket),又需要从Client接收数据。这时候采用什么样的编程模型能够使得用户能够及时响应呢?
1. 根据关注的对象及事件采用非阻塞方式轮询
2. 一个线程只处理一个事件,多出来的事件通过创建专门的线程进行处理。
第一种方式在无事可做的时候,轮询会浪费处理器时间;第二种方式很容易让线程之间存在临界资源,而且完成相同的工作需要创建更多的线程资源。
Linux 2.05.xx的版本上,引入了epoll机制,使用触发的方式,既提高了效率,又节省了资源。
机制介绍
epoll机制可以简单的认为是一个管理fd的数据库。在需要相应fd的时候,我们把该fd添加到epoll管理库中,并告知关注的事件。当fd对应的应用触发了关注的事件时,查找epoll管理库中相关的项,如果找到,通知关注的线程及时处理。
数据结构
监听管理数据结构
struct eventpoll {
spinlock_t lock;
struct mutex mtx;
/* 线程阻塞在epoll_wait系统调用时的等待队列 */
wait_queue_head_t wq;
/* 当前管理fd被监听时对应的等待队列 */
wait_queue_head_t poll_wait;
/* 可能存在事件需要处理的epoll单元链表 */
struct list_head rdllist;
/* fd管理库中管理的所有单元,通过树结构组织 */
struct rb_root rbr;
/* 在epoll_wait处理就绪fd时,通知就绪的所有fd链表 */
struct epitem *ovflist;
}
监听节点数据结构
struct epitem {
/* 树节点 */
struct rb_node rbn;
/* 就绪链表节点 */
struct list_head rdllink;
/* ovflist链表节点 */
struct epitem *next;
/* 被监听的fd及文件 */
struct epoll_filefd ffd;
int nwait;
/* 该fd事件发生后需要触发的waitq链表,理论上只有一个 */
struct list_head pwqlist;
struct eventpoll *ep;
/*对应file结构的ep_links链表中的节点 */
struct list_head fllink;
/* 关注的事件 */
struct epoll_event event;
}
数据结构关系图1
接口
epoll_create
sys_epoll_create(int size)
通过sys_epoll_create系统调用创建epoll管理数据。2.6.26版本中,传入的size参与只用作参数校验,没有实际的用处。
linux把epoll管理结构也当成文件来看待。所以除了创建管理结构外,还需要申请一个fd,创建一个file,dentry结构与当前的epoll管理结构相对应。
epoll_ctl
sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event)
该接口用来把fd添加到epfd对应的epoll管理结构中统一管理。
op为操作类型,允许进行添加,删除及修改三种操作:
#define EPOLL_CTL_ADD 1
#define EPOLL_CTL_DEL 2
#define EPOLL_CTL_MOD 3
用户管理的fd,在发生事件时对应的处理函数可能不同,一般需要用户在调用该接口的时候把对应的回调函数设置在event参数中,事件发生后由内核返回给用户;或者用户在用户空间自己组织起fd及对应回调函数之间的关系。
对于EPOLL_CTL_ADD事件,该接口主要完成:
Ø 创建监听节点;
Ø 在fd对应的实体与监听节点之间建立关联;
Ø 在fd对应的file结构与监听节点之间建立关联;
Ø 把监听节点添加到管理结构的树中;
Ø 检查该fd对应的实体是否已经就绪,就绪则添加到就绪链表中;
Ø Epoll管理结构如果处于阻塞状态,触发等待队列唤醒。
对于EPOLL_CTL_DEL事件,该接口主要完成:
Ø 取消fd对应的实体与监听节点之间的关联;
Ø 取消fd对应的file结构与监听节点之间的关联;
Ø 从管理结构的树中移除;
Ø 如果存在与就绪链表中的话,出其中移除;
Ø 释放监听节点。
对于EPOLL_CTL_MOD事件,该接口主要完成:
Ø 修改监听节点关注的事件;
Ø 重新检查该fd对应的实体是否已经就绪,就绪则添加到就绪链表中;
Ø Epoll管理结构如果处于阻塞状态,触发等待队列唤醒。
epoll_wait
sys_epoll_wait(int epfd, struct epoll_event __user *events, int maxevents, int timeout)
该接口负责检查epoll管理结构中是否存在就绪的fd,如果有,从内核带回相关的信息,由用户进行针对处理;如果没有,通过该接口阻塞在内核态。当存在多个fd就绪时,只返回maxevents个就绪fd。
timeout参数提供了定时阻塞的机制。0代表非阻塞查询就绪fd,小于0则认为永久阻塞,否则根据传入的值决定阻塞的时间。
该接口处理逻辑:
Ø 查看就绪链表,如果存在fd已经就绪,拷贝到用户态空间带回;
Ø 拷贝过程中就绪的fd,缓存到ovflist链表中,结束后转移到就绪链表中;
close
sys_close-> filp_close-> fput
void __fput(struct file *file)
{
……;
eventpoll_release(file);
if (file->f_op && file->f_op->release)
file->f_op->release(inode, file);
……;
}
这里区分关闭epoll管理fd与监听fd。
对于关闭管理fd的情况,epoll机制提供了ep_eventpoll_release函数,主要完成:
Ø 删除各监听节点(具体参考epoll_ctl中的DEL处理);
Ø 释放管理结构。
对于监听fd,eventpoll_release函数主要完成:
Ø 摘除并释放与该file关联的所有监听fd。
关注点
限制
Ø 允许epoll管理fd添加到其他的epoll中进行管理;
Ø 不允许epoll管理fd添加到自己的管理结构中进行管理;
LT&ET
从处理上看,LT和ET方式最大的差别在于:
if (!(epi->event.events & EPOLLET) &&
(revents & epi->event.events))
list_add_tail(&epi->rdllink, &ep->rdllist);
对于LT的情况,只要当前poll返回的事件是自己关注的话,会把该监听节点继续添加到就绪链表中。这样子下次epoll_wait的时候还会调用对应的poll函数查看是否还有事件没有处理。而ET方式不会。
所以当从epoll_wait返回的时候,对于ET方式的fd,需要一次性把事情全部做完。
比如ET方式的fd接收到报文A,B,然后epoll_wait返回给用户空间了,此时需要用户确保一次性读出A,B两个报文,否则后续该fd没有事件触发的话,报文B永远无法接收到,或者后续接收到报文C,但是用户读取时处理的却是报文B,导致用户处理延迟或者错误。