本文主要分析 epoll 实现的原理,为了更加清晰的思路,分析原理然后主要记录一下线索,根据这些线索基本就能完成源码分析或者看别的每个部分的博客文章等。我是下载了 linux 2.6 的源码用 ide 跳跳乐跳出来的,不过线索基本够用了。
先记录一下 epoll 的接口再看原理。首先
复习 select,poll,和 pselect
- select 是传一个 set 给他,内核收到动静返回,我们再检查 set 有谁可用。
- pselect 是为了解决信号缺失的问题他保证在睡觉之前检查一下 mask (需要配合 sigprocmask)如果有马上返回。
- poll 的主要解决问题只是 ns 的超时而已。linux 也提供了 ppoll 实现 pselect 相似的功能。还有一个要点是 pselect 只用了 bitset ,而 poll 用的是fd结构体数组。由于一个 fd 用一个结构体存储,所以内核还会返回实际发生了什么事件。
- 这些共同问题是如果要管理很多个 fd 的话醒来之后需要遍历所有的文件描述符用 FD XXX 宏查看掩码,成千的时候就麻烦了。
epoll API。
- 首先不用每次都传一个结构体数组或者fd set 了,他直接用注册机制来绑定 fd。但是这样需要提前建立一个内核数据结构。
- 相关调用:
int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- epoll 的 fd 是没有限制的!create 之后就获得一个 epoll 的 fd。和 select 的不一样。
- ctl 是控制选项的,其中 op 是进行 epoll 操作,ADD,DEL,MOD 三个事件(命令式)。
- fd 是某个监听的 fd,所以说他是可以单个 fd 管理,特别高级。
- event 是用来指代监听的事情的,主要还是用一堆幂宏或上去的。比如 POLLIN,POLLOUT,PRI 等(这些继承 POLL),然后还有 ET edge triggered和 LT level trigger(by default)。然后 event 里面还有一个 结构体:
- epoll wait 就是睡觉了,而 event 是 kernel 传回来的。所以说每次传回来还会包括一个要被激活的 fd!
- 那么还有一个问题,epoll wait 返回的还是有多少个事件(和 select poll 一样),但是他 event 结构体里面那个 union 只有一个 fd,所以怎么破?哈哈,关键在于那个指针参数!我们传的和 poll 的数组一样,他是传的一个数组来的。所以 epoll 的神奇之处就是他不再使用我们之前的那些贪便宜的参数-值传递了,而是说指定一块内存给 epoll 直接返回 ready 的 fd。我还是不懂,这不是搞笑的吧,当初设计 select 的时候不就已经在内核知道哪些就绪了吗,既然他都能把 fd set 给设置了,为什么不直接做成今天 epoll 这个样子呢?只能说好的接口设计(函数签名)直接就能避免很多后续的问题!
实现原理
首先是内核怎么才能实现这个功能,很明显的做法,就是轮询一遍。
- 首先知道硬件中断的原理先,对于还没有注册io复用,或者没有进行函数调用的时候,此时各种连接已经建立好了。
- 如果的确可读可写事件发生,那么一定在网络协议栈里。最下层是 网卡 收到了一个 packet,触发硬件中断(留在 if 的 buffer ring 里),硬件中断触发某个核进入 kernel 里(这部分想法只是在os 课程看那篇 receive livelock的论文里的信息,我还没有完全理解 PLIC 和 CLINT 的各种中断,最近 8086 学到 NMI 和INTR,之后再补充),
- 这里说的硬件中断总是高优先级的,必须挂起某些进程然后在时间片里面完成任务,最简单的是处理 ring buffer 的拷贝,如果有 dma 的话,DMA 从 网卡 复制完到 ram 之后,CPU 也要负责来拆包。这些 intr 会进入到内核进程里(trap 之后判断是某个硬件,然后路由给硬件的驱动模块处理)
- 然后网络栈搞定了拆包检查和丢掉垃圾之后,一路到 tcp udp 包里,然后进行路由,根据 ip port 四元组用 hashmap 或者 红黑树把他们导到对应 descriptor 的内核对应的 buffer 里面,这个时候应该更新他们的 file 结构体!
- 因为 read 和 write 的时候是要阻塞的,所以一般这种描述符会提供一个接口方便直接判断缓冲区有没有变化,不然每次去查缓冲区也不靠谱。首先复习一下 struct file 先,以前学的时候都是看简化的,书也是直接保留几个重要成员而已,实际要实现 poll 还是要做这些 meta 的管理的。在 struct file 里面除了记录哪些 offset 和 打开的 mode 文件锁之外,还会有一个 file_operator 结构体指针,这个结构体是由 vfs 定义支持的各种操作的,具体实现会绑定到驱动里面,像 aio,sendfile,splice,read,write 这些,这个其实很好理解的,就是个虚函数表嘛!由于新的 linux 的代码面目全非,可能还是看 2.x 版本的源码好看点。为了简化笔记,放有这个源码的链接供参考:Linux中的file,inode,file_operations三大结构体
- 所以总之 poll 本身就是个 vfs 提供的接口可以用来查询是否有数据(which 是网络栈接收到包后会更新的信息)。
- 然后还有一个关键点,是 epoll 睡死的时候如果有网络数据来了或者发生各种事件了怎么实现唤醒功能呢?最简单的方法是回调!所以这个回调放到哪里注册呢?这个要涉及 kernel 是怎么实现 sleep 的 条件 的,这里会有一个 wait queue 管理所有会阻塞的调用的,这个东西实际是在 struct file 的一个 void * 里面的!void *private_data 。看着不起眼,但是他会是驱动访问的一个东西。为什么是 void * 呢?我们知道 vfs 搞了一大套东西只是为了抽象通用的而已,比如我们用的阻塞,有的文件 vfs 过来他不会阻塞的啊,所以这部分数据就不好抽象了。那么既然我们能知道 fd 实际是什么类型(或者说 fd 被特定的驱动管理着),那么我们只需要设置一个 void* 域就行了,谁要写什么自己分配内存定制结构体就行了!!!所以这个字段就可以用来实现我们的 wake up 条件 了,套接字文件里,这里会是一个 struct socket 来 reinterpret_cast 的。这个 socket 结构体里面会有一个 wait_queue_head_t wait 字段,他就是到时候会进行唤醒的所有回调设置,这个 epoll 回调函数就是注册到这里的!
- 然后还有一个事情就是当回调被激发的时候 epoll 实际还是要查询其他的 fd 的,因为要实现把所有的 fd 都给了解然后传上来。实际上这里的方法是如果被触发了,上面说这个回调函数会把这个 fd 链到我们的一个返回链表上!double linked list 的好处就是方便插入删除(O1).
- 然后 file 结构体还有一个链表,这个链表是用来放 hook 的,其实就是把这个 struct file 链接到 epitem 上(相当于数据库里的 foreign key 吧),然后 epitem 会连到红黑树节点上。
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
spinlock_t f_ep_lock;
#endif /* #ifdef CONFIG_EPOLL */
这个同样在 file 结构体里面,然后 list_head 是一个内核数据结构-- double linkedlist。只要一个结构体有这个东西,他就成为了双向链表的一部分。
这个题目是源码分析的问题,这篇主要记录的是实现 epoll 的线索,根据上面这些过程和结构体关键字,配合各种资料应该就能搞明白了。因为 linux 源码分析本来是很多部分的,而 epoll 涉及的方面比较多,网上也很少有完整流程的资料,主要是这里有 epoll 的实现,vfs 的实现,还有 linux 文件驱动的实现。至于红黑树这个分析和epoll为什么快其实铺天盖地了,这里就不写了。
总结一下所以为什么 epoll 快
- 首先是 fop 的 poll 接口用来查询是否有事件。
- 然后是回调函数,select 和 poll 都会注册回调函数,不过这个回调函数做的是简单的唤醒而已。唤醒了之后,select 和 poll 就会去遍历整个 table,然后调用那个fop poll 函数判断有没有。而且每遍历 n 个文件会尝试 yield 避免占用太多时间。处理一下超时的事情,之后再返回用户。用户还要再遍历检查一下 fdset。每次调用select,都需要从0bit一直遍历到最大的fd,并且每隔32个fd还有调度一次(2次上下文切换)
- select 好歹节约了用 bitset 但是支持少,poll 还涉及一大堆数组的 copyin 和 copyout。
- 总之 epoll 和 select 这些的区别就是一个是 RAII incremental,amotized 之后开销很好,一个是 stop the world 。。。不知道当初 select 为什么这样设计。