上一篇文章中我们反复提到了一个名词—epoll,好像多路复用与其有着很大联系,这篇文章中我会从 linux 内核的角度为大家分析其底层原理。

epoll模型

上面所描述的三个操作系统方法就是 epoll 模型的核心:

  • epoll_create() 负责创建一个池子,一个监控和管理句柄 fd 的池子;
  • epoll_ctl() 责管理这个池子里的 fd 增、删、改;
  • epoll_wait() 负责阻塞接受池子中发生的事件,只要有事就会立马从这里唤醒

epoll池如何管理注册的fd

最常见的思路用一个 list(数组或者链表),但是 list 这种数据结构来存储元素,查询的时间复杂度有点高 O(n),每次要一次次遍历数组或链表才能找到位置。处理添加处理还好,但是处理更新和删除操作效率随着池子的增大而降低。

epoll 在内核里使用红黑树来跟踪进程所有待检测的 SocketChannel 对应的文件描述字,红黑树是一种平衡二叉树,时间复杂度为 O(log n),就算这个池子就算不断的增删改,也能保持非常稳定的查找性能。把需要监控绑定事件了对应事件的 SocketChannel 通过 epoll_ctl() 注册到 epoll 后,会加入到该红黑树中。

Linux内核中还维护了一个双向链表来记录就绪事件,当某个 socket 链接有事件发生时,通过回调函数,内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
微信截图_20210703191401.png

epoll 如何感知事件发生

在 Linux 中一切皆文件,这个不是说说而已,而是随处可见。实现一个文件系统的时候,就要实现这个文件调用,这个结构体用 struct file_operations 来表示。这个结构体有非常多的函数,精简了一些,如下:

  1. struct file_operations {
  2. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  3. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  4. //定制回调函数
  5. __poll_t (*poll) (struct file *, struct poll_table_struct *);
  6. int (*open) (struct inode *, struct file *);
  7. int (*fsync) (struct file *, loff_t, loff_t, int datasync);
  8. // ....
  9. };

其中 file_operation ->poll() 是对该 fd 定制一个回调函数,当发生读写时间时会回调该方法,从而实现了一种监听机制。

其中在调用 epoll_ctl() 往 epoll 池中注册对应的 fd 时(ADD操作),在添加的过程中还会注册一个回调函数。

  1. static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
  2. struct file *tfile, int fd)
  3. {
  4. ............
  5. // 注册回调函数, 并返回当前文件的状态
  6. revents = tfile->f_op->poll(tfile, &epq.pt);
  7. ...............
  8. }

当计算机网卡接收到网络传输数据的时候,计算机就能检测到事件发生。这是操作系统会产生一个中断。中断一般拆为上下两个部分,上部分为硬件中断,而软中断为是下半部分的一种实现机制,在这过程中就会触发该 fd 的回调函数 file_operation -> poll() 。通过回调函数主要完成两件事:

  1. 把事件就绪的 fd 对应的结构体放到就绪事件链表中(rdlist)
  2. 调用 epoll_wait() 方法唤醒 epoll

image.png