通过十个问题助你彻底理解linux epoll工作原理( 三 )

查阅很多资料后才搞明白其实 epoll 也是一种文件类型,其底层驱动也实现了 file_operations 中的 poll 函数,因此一个 epoll 类型的 fd 可以被其他 epoll 实例监视 。而 epoll 类型的 fd 只会有“读就绪”的事件 。当 epoll 所监视的非 epoll 类型文件有“读就绪”事件时,当前 epoll 也会进入“读就绪”状态 。
因此如果一个 epoll 实例监视了另一个 epoll 就会出现递归 。举个例子,如图所示:

通过十个问题助你彻底理解linux epoll工作原理

文章插图
 
  1. epollfd1 监视了 2 个“非 epoll”类型的 fd
  2. epollfd2 监视了 epollfd1 和 2 个“非 epoll”类型的 fd
如果 epollfd1 所监视的 2 个 fd 中有可读事件触发,fd 的 ep_poll_callback 回调函数会触发将 fd 放到 epollfd1 的 rdllist 中 。此时 epollfd1 本身的可读事件也会触发,就需要从 epollfd1 的 poll_wait 等待队列中找到 epollfd2,调用 epollfd1 的 ep_poll_callback(将 epollfd1 放到 epollfd2 的 rdllist 中) 。因此 ep->poll_wait 是用来处理 epoll 间嵌套监视的情况的 。
Question 5:ep->rdllist 的作用是什么?答案:epoll 实例中包含就绪事件的 fd 组成的链表 。
通过扫描 ep->rdllist 链表,内核可以轻松获取当前有事件触发的 fd 。而不是像 select()/poll() 那样全量扫描所有被监视的 fd,再从中找出有事件就绪的 。因此可以说这一点决定了 epoll 的性能是远高于 select/poll 的 。
看到这里你可能又产生了一个小小的疑问:为什么 epoll 中事件就绪的 fd 会“主动”跑到 rdllist 中去,而不用全量扫描就能找到它们呢? 这是因为每当调用 epoll_ctl 新增一个被监视的 fd 时,都会注册一下这个 fd 的回调函数 ep_poll_callback,当网卡收到数据包会触发一个中断,中断处理函数再回调 ep_poll_callback 将这个 fd 所属的“epitem”添加至 epoll 实例中的 rdllist 中 。
Question 6:ep->ovflist 的作用是什么?答案:在 rdllist 被占用时,用来在不持有 ep->lock 的情况下收集有就绪事件的 fd 。
当 epoll 上已经有了一些就绪事件的时候,内核需要扫描 rdllist 将就绪的 fd 返回给用户态 。这一步通过 ep_scan_ready_list 函数来实现 。其中 sproc 是一个回调函数(也就是 ep_send_events_proc 函数),来处理数据从内核态到用户态的复制 。
/** * ep_scan_ready_list - Scans the ready list in a way that makes possible for the scan code, to call f_op->poll(). Also allows for O(NumReady) performance. * @ep: Pointer to the epoll private data structure. * @sproc: Pointer to the scan callback. * @priv: Private opaque data passed to the @sproc callback. * Returns: The same integer error code returned by the @sproc callback. */static int ep_scan_ready_list(struct eventpoll *ep,int (*sproc)(struct eventpoll *,struct list_head *, void *),void *priv)由于 rdllist 链表业务非常繁忙(epoll 增加监视文件、修改监视文件、有事件触发...等情况都需要操作 rdllist),所以在复制数据到用户空间时,加了一个 ep->mtx 互斥锁来保护 epoll 自身数据结构线程安全,此时其他执行流程里有争抢 ep->mtx 的操作都会因命中 ep->mtx 进入休眠 。
但加锁期间很可能有新事件源源不断地产生,进而调用 ep_poll_callback(ep_poll_callback 不用争抢 ep->mtx 所以不会休眠),新触发的事件需要一个地方来收集,不然就丢事件了 。这个用来临时收集新事件的链表就是 ovflist 。我的理解是:引入 ovflist 后新产生的事件就不用因为想向 rdllist 里写而去和 ep_send_events_proc 争抢自旋锁(ep->lock), 同时 ep_send_events_proc 也可以放心大胆地在无锁(不持有 ep->lock)的情况下修改 rdllist 。
看代码时会发现,还有一个 txlist 链表,这个链表用来最后向用户态复制数据,rdllist 要先把自己的数据全部转移到 txlist,然后 rdllist 自己被清空 。ep_send_events_proc 遍历 txlist 处理向用户空间复制,复制成功后如果是水平触发(LT)还要把这个事件还回 rdllist,等待下一次 epoll_wait 来获取它 。
ovflist 上的 fd 会合入 rdllist 上等待下一次扫描;如果 txlist 上的 fd 没有处理完,最后也会合入 rdllist 。这 3 个链表的关系是这样:
通过十个问题助你彻底理解linux epoll工作原理

文章插图
 
Question 7:epitem->pwqlist 队列的作用是什么?答案:用来保存这个 epitem 的 poll 等待队列 。
首先介绍下什么是 epitem 。epitem 是 epoll 中很重要的一种数据结构,是红黑树和 rdllist 的基本组成元素 。需要监听的文件和事件信息,都被包装在 epitem 结构里 。


推荐阅读