Linux的I/O多路复用

之前学习Linux的I/O多路复用的时候,因为当时自己也是一知半解,所以也没能写一篇实质性文章来记录一下。现在通过几次实践之后,对I/O多路复用略有了解,写下一点心得以记录自己所学。

1 I/O多路复用的基础

1.0 基础知识

socket上定义了几个IO事件:状态改变事件、有数据可读事件、有发送缓存可写事件、有IO错误事件。
对于这些事件,socket中分别定义了相应的事件处理函数,也称回调函数。
Socket I/O事件的处理过程中,要使用到sock上的两个队列:等待队列和异步通知队列,这两个队列中都保存着等待该Socket I/O事件的进程. 等待队列上的进程会睡眠,直到Socket I/O事件的发生,然后在事件处理函数中被唤醒。

异步通知队列上的进程则不需要睡眠,Socket I/O事件发时,事件处理函数会给它们发送到信号,这些进程事先注册的信号处理函数就能够被执行

1.1 对等待队列的使用

Linux通过socket睡眠队列来管理所有等待socket的某个事件的进程,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的进程,通知进程相关事件发生。
通常一个进程会在等待队列上发生以下步骤:

  • select、poll、epoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前进程构建一个wait_entry节点,然后插入到监控socket的sleep_list
  • 进入循环的schedule直到关心的事件发生了
  • socket的事件发生了,然后socket顺序遍历其睡眠队列,依次调用每个wait_entry节点的callback函数

2 对比与介绍

2.0 介绍

一般我们说I/O多路复用一般是说select,poll,epoll。下面对这三者进行对比与介绍

2.1 select

select 同时插入并阻塞在N个socket的sleep_list上等待任意一个socket可读事件发生而被唤醒,当select被唤醒的时候,其callback里面有个逻辑去检查具体哪些socket可读了。
实际上select为每个socket引入一个poll逻辑,该poll逻辑用于收集socket发生的事件。 下面介绍调用select并且关注读取事件的过程:

  1. select会将需要监控的readfds集合拷贝到内核空间
  2. select遍历自己监控的socket sk,挨个调用sk的poll逻辑以便检查该sk是否有可读事件,遍历完所有的sk后,如果没有任何一个sk可读,那么select会调用schedule_timeout进入schedule循环,使得process进入睡眠
  3. 如果在timeout时间内某个sk上有数据可读了,或者等待timeout了,则调用select的process会被唤醒,接下来select就是遍历监控的sk集合,挨个收集可读事件并返回给用户

实际上select有以下三个问题:

  1. 被监控的fds需要从用户空间拷贝到内核空间
  2. 被监控的fds集合限制为1024
  3. 每次通知事件,我们都需要遍历整个可读等事件列表

2.2 poll

poll仅仅改变了fds集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024。但是也需要将描述符拷贝到内核空间,每次唤醒也需要遍历所有描述符集合。

2.3 epoll

2.3.0 epoll基础

epoll引入了epoll_ctl系统调用,将高频调用的epoll_wait和低频的epoll_ctl隔离开. 对于高频epoll_wait的可读就绪的fd集合返回的拷贝问题,epoll通过内核与用户空间mmap(内存映射)同一块内存来解决. epoll通过引入了一个中间层,一个双向链表(ready_list),一个单独的睡眠队列(single_epoll_wait_list)来组织那些已经就绪的fd。 epoll工作过程:

  1. epoll_wait插入到中间层的epoll的单独睡眠队列中,睡眠在epoll的单独队列上,等待事件的发生. 2.wait_entry_sk作为中间层与某个socket sk密切相关,wait_entry_sk睡眠在sk的睡眠队列上,其callback函数逻辑是将当前sk排入到epoll的ready_list中,并唤醒epoll的single_epoll_wait_list。
  2. single_epoll_wait_list上睡眠的process的回调函数就明朗了:遍历ready_list上的所有sk,挨个调用sk的poll函数收集事件,然后唤醒process从epoll_wait返回。

2.3.1 epoll add及Wait工作过程

  1. EPOLL_CTL_ADD逻辑

    1. 构建中间睡眠实体wait_entry_sk,将当前socket sk关联给wait_entry_sk,并设置wait_entry_sk的回调函数为epoll_callback_sk
    2. 将wait_entry_sk排入当前socket sk的睡眠队列上
    
  2. 回调函数epoll_callback_sk的逻辑如下。

    1. 将之前关联的sk排入epoll的ready_list
    2. 然后唤醒epoll的单独睡眠队列single_epoll_wait_list
    
  3. epoll_wait逻辑

    1. 构建睡眠实体wait_entry_proc,将当前process关联给wait_entry_proc,并设置回调函数为epoll_callback_proc
    2. 判断epoll的ready_list是否为空,如果为空,则将wait_entry_proc排入epoll的single_epoll_wait_list中,随后进入schedule循环,这会导致调用epoll_wait的process睡眠。
    3. wait_entry_proc被事件唤醒或超时醒来,wait_entry_proc将被从single_epoll_wait_list移除掉,然后wait_entry_proc执行回调函数epoll_callback_proc
    
  4. 回调函数逻辑

    1. 遍历epoll的ready_list,挨个调用每个sk的poll逻辑收集发生的事件
    2. 将每个sk收集到的事件,通过epoll_wait传入的events数组回传并唤醒相应的process。
    

参考资料

unix环境高级编程
https://cloud.tencent.com/developer/article/1005481
https://blog.csdn.net/zhangskd/article/details/45770323