0.阅读引用
1.相关数据结构
最重要的两个数据结构是红黑树和就绪链表,红黑树用于管理所有的文件描述符fd,就绪链表用于保存有事件发生的文件描述符。
当向系统中添加一个fd时,就创建一个epitem结构体.eventpoll用于管理所有的epitem.
struct epitem
{
struct rb_node rbn; // 用于主结构管理的红黑树
struct list_head rdllink; // 事件就绪链表
struct epitem *next; // 用于主结构体中的链表
struct epoll_filefd ffd; // 这个结构体对应的被监听的文件描述符信息
int nwait; // poll操作中事件的个数
struct list_head pwqlist; // 双向链表,保存着被监视文件的等待队列
struct eventpoll *ep; // 该项属于哪个主结构体(多个epitm从属于一个eventpoll)
struct list_head fllink; // 双向链表,用来链接被监视的文件描述符对应的struct
struct epoll_event event; // 注册的感兴趣的事件,也就是用户空间的epoll_event
}
struct eventpoll
{
spin_lock_t lock; // 对本数据结构的访问
struct mutex mtx; // 防止使用时被删除
/* 等待队列可以看作保存进程的容器,在阻塞进程时,将进程放入等待队列; 当唤醒进程时,从等待队列中取出进程*/
wait_queue_head_t wq; // sys_epoll_wait() 使用的等待队列
wait_queue_head_t poll_wait; // file->poll()使用的等待队列
struct list_head rdllist; // 就绪链表
struct rb_root rbr; // 用于管理所有fd的红黑树(树根)
struct epitem *ovflist; // 将事件到达的fd进行链接起来发送至用户空间
}
一些辅助的数据结构的定义
struct list_head {
struct list_head *next, *prev;
};
struct rb_node
{
unsigned long rb_parent_color;
#define RB_RED 0
#define RB_BLACK 1
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
// 与一个文件上的一个wait_queue_head 相关联,因为同一文件可能有多个等待的事件,
//这些事件可能使用不同的等待队列
struct eppoll_entry {
struct list_head llink; // List struct epitem.pwqlist
struct epitem *base; // 所有者
wait_queue_t wait; // 添加到wait_queue 中的节点
wait_queue_head_t *whead; // 文件wait_queue 头
};
struct epoll_event{
__uint32 events;//epoll事件类型
epoll_data_t data;//用户数据
};
EPOLLIN:触发该事件,有可读数据。(包括对端SOCKET正常关闭);
EPOLLOUT:触发该事件,有可以写数据;
EPOLLPRI:紧急数据处理(可读),urgent data available for read
EPOLLERR:发生错误;
EPOLLHUP:暂时被挂断;
EPOLLET: ET模式
EPOLLONESHOT: 只监听一次事件,用完即结束。
其他基础知识:
EPOLL事件有两种模型:
Edge Triggered (ET) 边沿触发
Level Triggered (LT) 水平触发
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是
否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误
可能性要小一点。传统的select/poll都是这种模型的代表.
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll
告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个
文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK错误)。
但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,
ET模式的加速效用仍需要更多的benchmark确认.
2.epoll_create
epoll_create函数的功能是创建eventpoll
ong sys_epoll_create(int size)
{
struct eventpoll *ep;
ep_alloc(&ep); // 为ep分配内存并进行初始化
fd = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
return fd;
}
3. epoll_ctl
epoll_ctl函数的主要功能是对文件描述符进行增删改查,不同的参数还有特别的功能
asmlinkage long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event)
{
int error;
struct file *file,*tfile;
struct eventpoll *ep;
struct epoll_event epds;
error = -FAULT;
// 判断参数的合法性,将 __user *event 复制给 epds。
if(ep_op_has_event(op) && copy_from_user(&epds,event,sizeof(struct epoll_event)))
goto error_return; //省略跳转到的代码
file = fget (epfd); // epoll fd 对应的文件对象
tfile = fget(fd); // fd 对应的文件对象
// 在create时存入进去的(anon_inode_getfd),现在取用。
ep = file->private->data;
mutex_lock(&ep->mtx);
// 防止重复添加(在ep的红黑树中查找是否已经存在这个fd)
epi = epi_find(ep, tfile, fd);
switch(op)
{
...
case EPOLL_CTL_ADD: // 增加监听一个fd
if (!epi)
{
epds.events |= EPOLLERR | POLLHUP; // 默认包含POLLERR和POLLHUP事件。
error = ep_insert(ep, &epds, tfile, fd); // 在ep的红黑树中插入这个fd对应的epitm结构体。
} else // 重复添加(在ep的红黑树中查找已经存在这个fd)。
error = -EEXIST;
break;
...
}
return error;
}
4.ep_insert
case EPOLL_CTL_ADD:
if (!epi) {
epds.events |= POLLERR | POLLHUP;
error = ep_insert(ep, &epds, tfile, fd);
} else
error = -EEXIST;
break;
当操作类型是EPOLL_CTL_ADD时,epoll_ctl做了四件事:
1.对要注册的事件event->events追加关心事件:EPOLLERR | EPOLLHUP
2.创建epitem结构,加入到红黑树中
3.revent = file->f_op->poll,即调用poll,把当前进程放到文件的等待队列上且设置回调函数ep_poll_callback,返回值revent是文件当前已产生事件掩码
4.检查返回事件:如果revent与关心事件event->events有交集(说明ADD之前事件就准备好了)
(1)把此epitem节点拷贝到rdlist链表中;(就绪句柄拷贝到rdlist)
(2)如果有进程在wq等待队列上(即有进程在调用epoll_wait等待),则唤醒之!
(3)如果有进程在poll_wait等待队列上(即有进程调用多路复用来监听当前epoll句柄),则唤醒之!
static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd)
{
/* 初始化epitem */
// 设置监听注册函数为ep_ptable_queue_proc, &epq.pt->qproc = ep_ptable_queue_proc
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
/*
* 执行f_op->poll(tfile, &epq.pt)时,XXX_poll(tfile, &epq.pt)函数会执行poll_wait(),
* poll_wait()会调用epq.pt.qproc函数,即ep_ptable_queue_proc。
*/
revents = tfile->f_op->poll(tfile, &epq.pt);
spin_lock(&tfile->f_ep_lock);
// 将epitem与它需要监听的文件链接起来,list_add_tail是内核函数,表示添加一个结点到链表尾部
list_add_tail(&epi->fllink, &tfile->f_ep_lilnks);
spin_unlock(&tfile->f_ep_lock);
// 将epitem插入红黑树
ep_rbtree_insert(ep, epi);
/*
* 如果要监视的fd状态改变,并且还没有加入到就绪链表中,则将当前的
* epitem加入到就绪链表中。如果有进程正在等待该文件的状态就绪,则
* 唤醒一个等待的进程。
*/
if ((revents & event->events) && !ep_is_linked(&epi->rdllink))
{
list_add_tail(&epi->rdllink, &ep->rdllist);
/* 唤醒epoll_wait()当前epoll实例的用户 */
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
/* 当前epoll文件已就绪 */
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
}
5.ep_poll_callback
ep_poll_callback函数主要的功能是当被监视文件的等待事件就绪时,将文件描述符对应的epitem实例添加到就绪链表中,导致rdlist不空,
进程被唤醒,epoll_wait()得以继续执行,之后内核会将就绪链表中的事件从内核空间拷贝到用户空间。
// 将epitem挂载到资源文件的监听队列上
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt)
{
/* 获取epitem */
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;
/* 从slab分配一个eppoll_entry结构,然后进行相应的初始化 */
if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL)))
{
/*
* 初始化一个等待队列节点,其中唤醒函数设置为ep_poll_callback
* 唤醒回调函数为ep_poll_callback!!!
*/
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
/* 还要保存资源文件监听队列的队列头whead */
pwq->whead = whead;
pwq->base = epi;
/*
* 将eppoll_entry挂载到资源文件的监听队列中,
* add_wait_queue表示将队列元素加入到等待队列头部,并设置非互斥等待
*/
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
/* 增加等待计数 */
epi->nwait++;
}
else
{
epi->nwait = -1;
}
}
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
/* 通过eppoll_entry中的wait获取对应的epitem */
struct epitem *epi = ep_item_from_wait(wait);
/* 获取epitem所属的eventpoll */
struct eventpoll *ep = epi->ep;
/* spinlock加锁(不允许休眠):保护eventpoll的访问 */
spin_lock_irqsave(&ep->lock, flags);
/* 如果我们想要监听的事件events为空,那么资源文件就绪时,nothing to do */
if (!(epi->event.events & ~EP_PRIVATE_BITS))
goto out_unlock;
/* 判断当前fd状态是否是我们关心的事件events */
if (key && !((unsigned long) key & epi->event.events))
goto out_unlock;
if (unlikely(ep->ovflist != EP_UNACTIVE_PTR))
{
if (epi->next == EP_UNACTIVE_PTR)
{
epi->next = ep->ovflist;
ep->ovflist = epi;
}
goto out_unlock;
}
/* 如果epitem没有被挂载到所属eventpoll中的就绪链表,就将其添加到就绪链表尾 */
if (!ep_is_linked(&epi->rdllink))
list_add_tail(&epi->rdllink, &ep->rdllist);
/* 唤醒epoll_wait()当前epoll实例的用户 */
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
/* 当前epoll文件已就绪 */
if (waitqueue_active(&ep->poll_wait))
pwake++;
out_unlock:
spin_unlock_irqrestore(&ep->lock, flags);
if (pwake)
ep_poll_safewake(&ep->poll_wait);
return 1;
}
6.epoll_wait
epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。然后就将就绪的
events和data发送到用户空间(ep_send_events()),如果ep_send_events()返回的事件数为0,并且还有超时时间剩余(jtimeout),
那么我们retry,期待不要空手而归。
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events, int, maxevents, int, timeout)
{
int error;
struct file *file;
struct eventpoll *ep;
/* 参数验证 */
if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
return -EINVAL;
/* 验证events数组区域,当前用户是否能够访问 */
if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event)))
{
error = -EFAULT;
goto error_return;
}
error = -EBADF;
/* 获取eventpoll文件描述符对应的struct file结构 */
file = fget(epfd);
if (!file)
goto error_return;
error = -EINVAL;
/* 验证epfd指向的文件是否是epoll文件 */
if (!is_file_epoll(file))
goto error_fput;
/* 取出挂载到epoll文件中的eventpoll */
ep = file->private_data;
/* 调用ep_poll()等待事件的到来 */
error = ep_poll(ep, events, maxevents, timeout);
error_fput:
fput(file);
error_return:
return error;
}
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
{
int res, eavail;
unsigned long flags;
long jtimeout;
wait_queue_t wait;
/* 处理睡眠时间:将毫秒数转化为HZ */
jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?
MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;
retry:
/* spinlock加锁:保护eventpoll的访问 */
spin_lock_irqsave(&ep->lock, flags);
res = 0;
/* 如果就绪链表为空,说明还没有任何events就绪 */
if (list_empty(&ep->rdllist))
{
/* 初始化等待队列节点,设置等待状态为互斥等待 */
init_waitqueue_entry(&wait, current);
wait.flags |= WQ_FLAG_EXCLUSIVE;
/* 将刚刚初始化的等待队列节点挂载到eventpoll中的等待队列 */
__add_wait_queue(&ep->wq, &wait);
for (;;)
{
/*
* 设置程序运行状态为可中断阻塞,因为我们希望能够接收到
* ep_insert()、ep_modify()、ep_poll_callback()的唤醒
*/
set_current_state(TASK_INTERRUPTIBLE);
/* events就绪或者超时,跳出循环 */
if (!list_empty(&ep->rdllist) || !jtimeout)
break;
/* 出现未决信号,设置返回值为-EINTR并跳出循环 */
if (signal_pending(current))
{
res = -EINTR;
break;
}
spin_unlock_irqrestore(&ep->lock, flags);
/* 休眠...等待超时或者被就绪资源唤醒 */
jtimeout = schedule_timeout(jtimeout);
spin_lock_irqsave(&ep->lock, flags);
}
/* 从等待队列中卸载 */
__remove_wait_queue(&ep->wq, &wait);
/* 恢复程序运行状态 */
set_current_state(TASK_RUNNING);
}
/* 判断是否有资源就绪 */
eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
spin_unlock_irqrestore(&ep->lock, flags);
if (!res && eavail && !(res = ep_send_events(ep, events, maxevents)) && jtimeout)
goto retry;
return res;
}
7.ep_send_events
ep_send_events函数,它扫描txlist中的每个epitem,调用其关联的fd对应的的poll方法,取得fd上较新的events(防止之前events被更新)即revents,之后将revents和相应的data拷贝(__put_user())到用户空间。如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist,如果是ET模式则不再加入rdlist。
// 将就绪的events传递到用户空间
static int ep_send_events(struct eventpoll *ep, struct epoll_event __user *events, int maxevents)
{
struct ep_send_events_data esed;
esed.maxevents = maxevents;
esed.events = events;
return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
}
// 扫描就绪链表
static int ep_scan_ready_list(struct eventpoll *ep,
int (*sproc)(struct eventpoll *, struct list_head *, void *),
void *priv)
{
int error, pwake = 0;
unsigned long flags;
struct epitem *epi, *nepi;
/* 初始化一个链表 */
LIST_HEAD(txlist);
/* mutex加锁 */
mutex_lock(&ep->mtx);
/* spinlock加锁:保护eventpoll的访问 */
spin_lock_irqsave(&ep->lock, flags);
/*
* 将eventpoll就绪链表中的所有节点全部链接到txlist上,
* 之后eventpoll就绪队列为空
*/
list_splice_init(&ep->rdllist, &txlist);
/*
* 设置eventpoll.ovflist,使得接下来新就绪的events被挂载到
* eventpoll.ovflist而不是就绪队列
*/
ep->ovflist = NULL;
spin_unlock_irqrestore(&ep->lock, flags);
error = (*sproc)(ep, &txlist, priv);
/* spinlock加锁:保护eventpoll的访问 */
spin_lock_irqsave(&ep->lock, flags);
/*
* 我们在调用ep_send_events_proc()将就绪队列中的事件交付
* 给用户的期间,新就绪的events被挂载到eventpoll.ovflist
* 所以我们需要遍历eventpoll.ovflist将所有已就绪的epitem
* 重新挂载到就绪链表中,等待下一次epoll_wait()进行交付
*/
for (nepi = ep->ovflist; (epi = nepi) != NULL; nepi = epi->next, epi->next = EP_UNACTIVE_PTR)
{
if (!ep_is_linked(&epi->rdllink))
list_add_tail(&epi->rdllink, &ep->rdllist);
}
ep->ovflist = EP_UNACTIVE_PTR;
/*
* 将调用ep_send_events_proc()之后剩余的未交付的epitem重新splice到
* eventpoll的就绪链表上
*/
list_splice(&txlist, &ep->rdllist);
/*
* 注意到epoll_wait()中,将wait_queue_t的等待状态设置为互斥等待,因此
* 每次被唤醒的只有一个节点。现在我们已经将eventpoll中就绪队列里的事件
* 尽量向用户交付了,但是在交付时,可能没有交付完全(1.交付过程中出现了
* 错误 2.使用了LT模式),也有可能在过程中又发生了新的事件。也就是这次
* epoll_wait()调用后,还剩下一些就绪资源,那么我们再次唤醒一个等待节点
* 让别的用户也享用一下资源
*
* 从这里已经可以看出内核对于epoll惊群的解决方案:ET模式:
* 1. 每次只唤醒一个节点
* 2. 事件交付后不再将事件重新挂载到就绪队列
*/
if (!list_empty(&ep->rdllist))
{
/* 唤醒epoll_wait()当前epoll实例的用户 */
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
/* 当前epoll文件已就绪 */
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
spin_unlock_irqrestore(&ep->lock, flags);
mutex_unlock(&ep->mtx);
if (pwake)
ep_poll_safewake(&ep->poll_wait);
return error;
}
// 将events和data发送到用户空间
static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head, void *priv)
{
struct ep_send_events_data *esed = priv;
int eventcnt;
unsigned int revents;
struct epitem *epi;
struct epoll_event __user *uevent;
/*
* 遍历就绪链表,eventcnt记录已交付的events的数量
* uevent指向esed中封装的events数组,这个数组用于将已就绪events返回给用户
*/
for (eventcnt = 0, uevent = esed->events;!list_empty(head) && eventcnt < esed->maxevents;)
{
epi = list_first_entry(head, struct epitem, rdllink);
/* 将epitem从head就绪队列中卸载 */
list_del_init(&epi->rdllink);
/* 从资源文件当前状态中提取出我们所关心的events,拿到最新的数据 */
revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) & epi->event.events;
/* 如果有我们所关心的events发生 */
if (revents)
{
/*
* 使用__put_user将数据在用户空间和内核空间互相拷贝
*
* 将events拷贝到用户空间
*
* 若拷贝失败,那么就将该epitem重新添加到就绪链表上,
* 然后返回已交付的events的数量
*/
if (__put_user(revents, &uevent->events) || __put_user(epi->event.data, &uevent->data))
{
/* 复制失败了 */
list_add(&epi->rdllink, head);
return eventcnt ? eventcnt : -EFAULT;
}
/* 更新已交付的event的数量 */
eventcnt++;
/* 指向events数组中的下一元素 */
uevent++;
/*
* 如果是ET模式, epitem是不会再进入到就绪链表,
* 除非fd再次发生了状态改变, ep_poll_callback被调用。
* 如果是LT模式,不但会将对应的数据返回给用户,并且会将当前的epitem再次加入到rdllist中。
* 这样如果下次再次被唤醒就会给用户空间再次返回事件。当然如果这个
* 被监听的fd确实没事件也没数据了, epoll_wait会返回一个0。
*/
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;
else if (!(epi->event.events & EPOLLET)) //LT模式,水平触发模式
{
list_add_tail(&epi->rdllink, &ep->rdllist);
}
}
}
return eventcnt;
}
8.结论
通过上面的源码分析可以看出epoll的工作过程如下:
1. epoll_wait()调用ep_poll(),如果就绪链表rdlist为空,则挂起当前进程,直到rdlist不为空时被唤醒,这个时候会调用ep_send_events()将实际发生的事件revents和data从内核空间拷贝到用户空间(拷贝调用的是__put_user,并不存在什么共享内存之类的)。
2. 当文件描述符fd的状态改变时(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback() 被调用。
3. ep_poll_callback()将有事件发生的文件描述符(epitem)加入到就绪链表rdlist 中,这时候就绪链表不为空,epoll_wait() 进程被唤醒。
4. ep_send_events()会扫描就绪链表,调用每个文件描述符的poll函数返回revents,之后将revents和data从内核空间拷贝到用户空间。如果是ET模式, epitem是不会再进入到就绪链表,除非fd再次发生了状态改变, ep_poll_callback被调用。如果是LT模式,不但会将对应的数据返回给用户,并且会将当前的epitem再次加入到rdllist中。这样如果下次再次被唤醒就会给用户空间再次返回事件。