epoll的实现原理(2)

本文深入剖析epoll的回调函数ep_poll_callback()及其作用,详细解释epoll在用户空间交互的过程,包括阻塞与非阻塞模式下的不同处理方式,以及ET和LT模式的区别。内容涵盖了epoll如何处理就绪事件、如何唤醒等待进程,以及在用户态和内核态之间的数据传输策略。
摘要由CSDN通过智能技术生成

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()唤醒等待wqpoll_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_waitpoll()就绪队列&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种唤醒:

  1. 任务超时(time_out = 1)
  2. 该任务收到了一个信号(TASK_INTERRUPTIBLE)
  3. 有一个新的事件发生,(ep_poll_callback()将唤醒ep->wq
  4. 什么都没发生,只是唤醒了(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() 设置 ovflistNULL ,在回调 ep_poll_callback() 发生时,回调函数不会把正在传输到用户态的事件链接到ep->rdllist(如果这么做了就是灾难,仔细思考一下原因),复制内存的消耗是非常昂贵的。通过 ovflistep_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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值