epoll的实现原理(2)
笔记,内容翻译自:
The Implementation of epoll(3)
The Implementation of epoll(4)
回调函数 ep_poll_callback()
前面提到的ep_insert()
函数将epoll实例附加到监视文件描述符fd的等待队列,注册ep_poll_callback()
为队列唤醒的回调函数。下面剖析一下这个回调函数:
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{ //↑ pwq->wait
int pwake = 0;
unsigned long flags;
struct epitem *epi = ep_item_from_wait(wait); //通过strcut eppoll_entry找到epitem
struct eventpoll *ep = epi->ep; //进一步找到eventpoll
接下来,使用自旋锁锁定eventpoll
spin_lock_irqsave(&ep->lock, flags);
**然后检查事件是否是用户让epoll监视的。**前面ep_insert()
将注册事件为~0
,有两个原因:
1.用户可能频繁改变需要监视的事件,但是重新注册poll
回调效率不高。
2.其次,并非所有的事件都遵循系统的事件掩码设置,因此完全依靠系统的设置不太可靠。
if (key && !((unsigned long) key & epi->event.events))
goto out_unlock;
如果没有监控任何事件,跳到解锁out_unlock
接下来检查epoll实例是否正尝试将事件传输到用户态(也就是在调用ep_send_events_proc()
时)。
如果是的话,将当前epitem
加入到一个链表头在 struct eventpoll
中的单链表里。代码如下:
if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) { //正在传输为NULL(默认状态是EP_UNACTIVE_PTR)
if (epi->next == EP_UNACTIVE_PTR) {
epi->next = ep->ovflist; //前面已经取得了ep的锁
ep->ovflist = epi;
if (epi->ws) {
__pm_stay_awake(ep->ws);
}
}
goto out_unlock; //完成后跳到解锁
}
不是的话,接下来ep_poll_callback()
检查当前的struct epitem
是否已经在就绪队列中。
在用户没有没有机会调用epoll_wait()
时可能发生这种情况(以前加入过了,用户还没机会处理)。
如果不在的话,函数会将struct epitem
加入到就绪队列(struct eventpoll
的成员rdllist
)中。
if (!ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake_rcu(epi);
}
然后 ep_poll_callback()
唤醒等待wq
和poll_wait
的进程。
wq
用于在超时时间(timeout)没到时,用户正在通过epoll_wait()
等待events的时候(最多唤醒一个进程)。
poll_wait
是epoll实现的文件系统的poll()
,请记住epoll同样是一个可轮询的文件描述符。
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq); //加锁wq(正在等待的wait_qunue)
if (waitqueue_active(&ep->poll_wait)) //
pwake++; //poll_wait类比前文Tcp文件描述符的poll_wait
最后的out_unlock部分,ep_poll_callback()
释放自旋锁,并且唤醒(eventpoll的)poll_wait
(poll()
就绪队列&ep->rdllist
)
我们不能在保持自旋锁的时候唤醒,因为epoll可以将自身fd加入到受监视文件中(这种情况下有锁唤醒会导致死锁)。
out_unlock:
spin_unlock_irqrestore(&ep->lock, flags);
if (pwake)
ep_poll_safewake(&ep->poll_wait);
return 1;
struct eventpoll
的成员rdllink
在epoll中,rdllink
是存储就绪文件描述符的双链表的头结点,链表中的节点就是有事件发生的struct epitem
。
函数epoll_wait()
和 ep_poll()
该部分讨论用户程序调用epoll_wait()
时,是如何将文件描述符传输到用户态的。
epoll_wait()
很简单,它做了一些简单的错误检查,从epoll的文件描述符取出 struct eventpoll
然后调用 ep_poll()
函数去执行真正的拷贝工作。
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
{
int res = 0, eavail, timed_out = 0;
unsigned long flags;
long slack = 0;
wait_queue_t wait;
ktime_t expires, *to = NULL;
if (timeout > 0) { //阻塞
struct timespec end_time = ep_set_mstimeout(timeout);
slack = select_estimate_accuracy(&end_time);
to = &expires;
*to = timespec_to_ktime(end_time);
} else if (timeout == 0) { //非阻塞
timed_out = 1;
spin_lock_irqsave(&ep->lock, flags);
goto check_events;
}
ep_poll()
函数根据timeout确定是否阻塞来执行不同的调用方法。
阻塞时,根据timeout来计算到期时间;非阻塞则直接加锁->执行check_events
阻塞时执行:
fetch_events:
spin_lock_irqsave(&ep->lock, flags); //获取锁
if (!ep_events_available(ep)) { //检查是否有新事件,没有的话执行下面的
init_waitqueue_entry(&wait, current); //用当前任务初始化wait_queue_t wait
__add_wait_queue_exclusive(&ep->wq, &wait);//加入等待队列
for (;;) {
set_current_state(TASK_INTERRUPTIBLE); //信号或者wake_up()唤醒
if (ep_events_available(ep) || timed_out) //如果有新事件或者time_out = 1
break;
if (signal_pending(current)) { //或者信号
res = -EINTR;
break;
}
spin_unlock_irqrestore(&ep->lock, flags); //解锁
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1; /* resumed from sleep */ //如果到期,置位1,break,唤醒
spin_lock_irqsave(&ep->lock, flags);
}
__remove_wait_queue(&ep->wq, &wait); //唤醒后移出自身
set_current_state(TASK_RUNNING); //任务设回TASK_RUNNING
}
检查是否有新事件,如果没有的话,将当前进程加入到epoll的等待队列,函数将当前任务设定为TASK_INTERRUPTIBLE
,解锁自旋锁并告诉程序重新调度,还会设置内核定时器在指定超时到期或者收到任何信号时重新调度。当唤醒时(超时、新事件或者信号中断),将任务设置回TASK_RUNNING
,然后进行check_events
检查是否有监控事件发生。 [这一部分有些难以理解,后面会补充说明]
TASK_INTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比方等待键盘输入、socket连接、信号等等),但能够被中断唤醒.普通情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态.(单个CPU时),假设不是绝大多数进程都在睡眠,CPU又怎么响应得过来.
check_events:
首先,在持有锁的情况下确认是否有监控的事件,然后才解锁。
如果没有事件并且超时未到期(time_out = 0),唤醒过早会发生这种情况。会返回到fetch_events
并再次进行等待。
check_events:
eavail = ep_events_available(ep);
spin_unlock_irqrestore(&ep->lock, flags);
if (!res && eavail && !(res = ep_send_events(ep, events, maxevents)) && !timed_out)
goto fetch_events;
return res;
非阻塞时:
直接进入了check_events
,而不是等待事件。(timed_out = 1)
用户空间交互
ep_poll()
函数的调用
error = ep_poll(ep, events, maxevents, timeout); //ep:epollevent
如果指定了timeout并没有可用事件,ep_poll()
则将自己添加到ep->wq
(waitqueue),这是因为,在前一段中提到的,ep_poll_callback()
将唤醒ep->wq
在执行期间等待的任何进程。(实现了新事件到达后唤醒!)
然后调用了schedule_hrtimeout_range()
进行休眠(高精度的内核定时器,时间到后唤醒)。
所以,加上前面设定任务为TASK_INTERRUPTIBLE
,有4种唤醒:
- 任务超时(time_out = 1)
- 该任务收到了一个信号(TASK_INTERRUPTIBLE)
- 有一个新的事件发生,(
ep_poll_callback()
将唤醒ep->wq
) - 什么都没发生,只是唤醒了(TASK_INTERRUPTIBLE,wake_up())
对于方案1,2和3,该函数设置适当的标志并退出等待循环。对于最后一个,回到睡眠状态。
接下来是check_events:
首先确保实际上有可用的事件,然后它调用了
ep_send_events(ep, events, maxevents)
sys_epoll_wait() > ep_send_evnets() > ep_scan_ready_list() > ep_send_events_proc()
此函数依次调用ep_scan_ready_list()
,并且传递ep_send_events_proc()
作为回调函数。ep_scan_ready_list()
遍历就绪列表并对它找到的每个就绪事件调用ep_send_events_proc()
。
ep_send_events
首先将eventpoll
中的就绪队列(ready list)拼接到了(spliced away)函数的局部变量中,然后将它的ovflist
设定为NULL
(相对的,它的默认值为EP_UNACTIVE_PTR
)。
下面来解释这是为什么:(因为效率!)就绪队列从eventpoll
拼接以后, ep_scan_ready_list()
设置 ovflist
为 NULL
,在回调 ep_poll_callback()
发生时,回调函数不会把正在传输到用户态的事件链接到ep->rdllist
(如果这么做了就是灾难,仔细思考一下原因),复制内存的消耗是非常昂贵的。通过 ovflist
,ep_scan_ready_list()
不需要持有锁。
下面是ep_send_events_proc()
的执行过程
ep_send_events_proc()
对就绪事件再次调用文件描述符的poll()
以确保事件确实被触发。这是为了确保用户注册的事件确实触发了。例如,EPOLLOUT
在用户程序写入文件描述符时将文件描述符添加到就绪列表中。用户程序完成写入后,文件描述符可能不能写了,如果epoll不处理这种情况,那么用户将在不能写的情况下收到EPOLLOUT
(可写)。
提醒:尽管尽力确保用户空间程序获得正确的通知,但是用户程序仍然是有可能收到不再存在的事件通知的()。这就是为什么在使用epoll时,总是使用非阻塞的套接字是一个好习惯。
在检查完后,将event struct
拷贝到用户态。
ET/LT实现区别
else if (!(epi->event.events & EPOLLET)) { //在ep_send_events_proc()中
list_add_tail(&epi->rdllink, &ep->rdllist);
}
在LT模式下,ep_send_events_proc()
最后会把事件重新添加到就绪队列中,这样在下一次调用ep_poll()
时,程序会再次检查这些事件是否可用。
由于在事件返回到用户态空间之前,ep_send_events_proc()
每次都要通过poll()
检查这些事件,所以这些事件如果再也不可用(没发生监控事件)的话,会略微增加开销(相对于ET模式)。但是保证不报告任何不可用事件更加重要。
到这里,ep_send_events_proc()
函数返回了, ep_scan_ready_list()
继续执行一些清理工作。它首先将一些没被 ep_send_events_proc()
消耗(处理)的事件拼接回就绪队列(用户缓冲区事件已满时会发生)。
ep_send_events_proc()
还可以快速的把所有ovflist
中的事件添加到就绪,这样新的事件会被添加到主就绪队列,在这之后把 ovflist
设回 EP_UNACTIVE_PTR
。如果仍然还有其他事件可用,函数会唤醒其他进程并结束。下面是该函数的源码:
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;
struct wakeup_source *ws;
poll_table pt;
init_poll_funcptr(&pt, NULL);
/*
* We can loop without lock because we are passed a task private list.
* Items cannot vanish during the loop because ep_scan_ready_list() is
* holding "mtx" during this call.
*/
for (eventcnt = 0, uevent = esed->events;
!list_empty(head) && eventcnt < esed->maxevents;) {
epi = list_first_entry(head, struct epitem, rdllink);
/*
* Activate ep->ws before deactivating epi->ws to prevent
* triggering auto-suspend here (in case we reactive epi->ws
* below).
*
* This could be rearranged to delay the deactivation of epi->ws
* instead, but then epi->ws would temporarily be out of sync
* with ep_is_linked().
*/
ws = ep_wakeup_source(epi);
if (ws) {
if (ws->active)
__pm_stay_awake(ep->ws);
__pm_relax(ws);
}
list_del_init(&epi->rdllink);
revents = ep_item_poll(epi, &pt); // 调用f_op->poll,但不是sys_poll
/*
* If the event mask intersect the caller-requested one,
* deliver the event to userspace. Again, ep_scan_ready_list()
* is holding "mtx", so no operations coming from userspace
* can change the item.
*/
if (revents) {
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
list_add(&epi->rdllink, head);
ep_pm_stay_awake(epi);
return eventcnt ? eventcnt : -EFAULT;
}
eventcnt++;
uevent++;
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;
else if (!(epi->event.events & EPOLLET)) {
/*
* If this file has been added with Level
* Trigger mode, we need to insert back inside
* the ready list, so that the next call to
* epoll_wait() will check again the events
* availability. At this point, no one can insert
* into ep->rdllist besides us. The epoll_ctl()
* callers are locked out by
* ep_scan_ready_list() holding "mtx" and the
* poll callback will queue them in ep->ovflist.
*/
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake(epi);
}
}
}
return eventcnt;
}