本章主要介绍Linux epoll 的实现方式,但在此之前,我们首先要知道一个 socket 的创建流程,以及当数据就绪时 socket 如何被唤醒。当我们了解这些之后,再去理解 epoll 的实现,会更加深刻。
注:本章内容主要来源公众号《开发内功修炼》
socket 的创建
进程的结构
在这里,只介绍socket 相关的部分。
每个用户进程都会维护一份进程打开的文件列表。当创建一个 socket 时,即创建一个对应的socket 文件,并且在文件列表中维护。
struct socket
创建一个 socket ,即实例化一个 struct socket,这里我们只关心以下几个字段:
sk_receive_queue | 接收队列,用于接收网络数据 |
sk_wq | 等待队列,用于存放等待 socket 数据的对象 |
void(*sk_data_ready)(...) | 数据就绪处理函数 |
其中 sk_data_ready 会默认设置成 sock_def_readable(),该函数用于唤起等待队列中的进程。
socket 等待接收数据
当 socket 创建完成,进⼊系统调⽤后,⽤户进程就进⼊到了内核态,通过执⾏⼀系列的内核协议层函数,然后到 socket 对象的接收队列中查看是否有数据,没有的话就把⾃⼰添加到 socket 对应的等待队列⾥,最后让出 CPU。这里我们主要关注等待项:
private | 会被设置成当前进程的文件描述符 |
func | 被设置 autoremove_wake_function,用于唤醒 private 的进程 |
当数据到达网卡之后,会通过软中断接收数据,并将数据保存到 socket 的接收队列中,并执行 socket 中 sk_data_ready 设置的数据就绪数量函数(这里是 sock_def_readable())。 sock_def_readable 主要是获取 socket 等待队列中等待项,并唤起对应的进程。
小结
以上就是一个 socket 对象创建和接收数据的流程,总结来说:
- socket 创建时,会设置对应的数据就绪处理函数,当数据就绪时调用。
- 当 socket 接收队列没有数据或者数据不足够时,会将新建一个等待项,插入到等待队列,休眠,让出 CPU。
- 当 socket 接收到足够数据,会通过数据就绪处理函数,唤起等待队列中,等待项的进程。
接下来,将说明 epoll 基于上述的实现
EPOLL
这里不会具体介绍 EPOLL 的细节(具体细节将在下章介绍),只说明 epoll 对上述流程的改造。
1.epoll 相关的函数是如下三个:
epoll_create | 创建一个 epoll 对象 |
epoll_ctl | 向 epoll 对象中添加要管理的连接 |
epoll_wait | 等待其管理的连接上的 IO 事件 |
2.首先在用户进程调用 epoll_create 时,内核会创建一个 struct eventpoll 的内核对象。并同样把它关联到当前进程的已打开文件列表中。
eventpoll 在文件列表:
eventpoll 结构:
3.在创建 socket 除了struct socket 正常创建流程外,同时会通过 epoll_ctl 将 socket 添加的 eventpoll 中进行管理。在 epoll_ctl 执行的时候,内核为每一个 socket 上都添加了一个等待队列项。这里与前面的不同点是:
(1)之前等待项的 private 指向 当前进程描述符,但在 epoll 中,private 指向 null。
(2)之前等待项的 func 设置成 autoremove_wake_function,但在 epoll 中被设置成 ep_poll_callback。
(3)会新增一个 base 参数,用于指向 epitem。
4.通过epoll_wait 获取 rdllist中就绪的 epitem,如果没有,创建一个 eventpoll 等待项,插入到eventpoll 的等待队列中,然后把自己阻塞掉,这里等待项主要是包括:
(1)private 指向当前进程
(2)func 设置成 default_wake_function 用于唤起进程。
5.数据就绪时,默认调用sock_def_readable,调用socket 等待队列中的等待项,在这里会调用 ep_poll_callback,在 ep_poll_callback 根据等待任务队列项上的额外的 base 指针可以找到 epitem, 进而也可以找到 eventpoll对象。首先它做的第一件事就是把自己的 epitem 添加到 epoll 的就绪队列中。接着它又会查看 eventpoll 对象上的等待队列里是否有等待项(epoll_wait 执行的时候会设置)。如果有等待项,那就查找到等待项里设置的回调函数。
6.唤起等待项中对应的进程,从 epoll_wait 函数继续执行,获取rdllist 中的 epitem,然后执行相关操作。
总结
对比常规的 socket 和 epoll 的流程可以发现,其实 epoll 的本质是改变了 sock_def_readable 的 func 行为。 在常规的 socket 中, func 用于唤起进程,而在 epoll 中,被设置成了 ep_poll_callback 以执行 epoll 的相关行为。至于具体 epoll 介绍,将在下一章说明。