I/O 多路复用
I/O 多路复用
简介
I/O 多路复用是指-允许程序员检查和阻止多个 I/O 流(或其他“同步”事件),每当任何一个流处于活动状态时都会收到通知,以便它可以处理该流上的数据。
在 Linux 系统中存在以下三种实现方式:
- select
- poll
- epoll
select
函数概览:
1 |
|
样例实现方式:
1 |
|
函数的执行流程:
- select 是一个阻塞函数,当没有数据时会阻塞在当前行。
- 当有数据时会将 rset 中对应的位置置为 1。
- select 函数返回不再阻塞。
- 遍历文件描述符数组,判断为 1 的描述符。
- 读取数据进行处理。
函数缺点:
- rset 采用了 bitmap 的形式默认大小为 1024。
- rset 每次循环都必须重新置位,不可重复使用
- 尽管将 rset 的判断是从内核态进行的,但是仍然有拷贝的开销
- select 并不知道哪一个文件描述符下有数据,需要遍历。
函数特性:
- 我们需要在每次调用之前构建每个集合
- 该函数检查任何位 - O(n)
- 我们需要遍历文件描述符以检查它是否存在于 select 返回的集合中
- select 的主要优点是它非常普遍 - 在每一个 unix 系统中都存在
poll
函数概览:
1 |
|
poll 模型的数据结构如下:
1 |
|
样例如下:
1 |
|
函数的执行流程:
- 将描述符从用户态转到内核态
- poll 是一个阻塞函数,当没有数据时会阻塞在当前行,如果有数据则标识 fd 的 revents 为 POLLIN。
- poll 方法返回
- 遍历 fd,定位文件描述符
- 重置对象
- 读取和处理
函数缺点:
- 有拷贝的开销
- poll 并不知道哪一个文件描述符下有数据,需要遍历。
函数特性:
- poll() 不要求用户计算最高编号的文件描述符的值+1
- poll() 对于大值文件描述符更有效。想象一下,通过 select() 观察一个值为 900 的文件描述符——内核必须检查每个传入集合的每一位,直到第 900 位。
- select() 的文件描述符集是静态大小的。
- 使用 select(),文件描述符集在返回时被重建,因此每个后续调用都必须重新初始化它们。 poll() 系统调用将输入(events 字段)与输出(revents 字段)分开,允许数组无需更改即可重用。
- select() 的超时参数在返回时未定义。需要独立是实现。
- select() 更普遍,因为一些 Unix 系统不支持 poll()
epoll
在使用 select 和 poll 时,我们管理用户空间上的所有内容,并在每次调用时发送集合并进行等待。 要添加另一个套接字,我们需要将其添加到集合中并再次调用 select/poll。
Epoll 系统调用可以帮助我们在内核中创建和管理上下文。
我们将任务分为 3 个步骤:
- 使用 epoll_create 在内核中创建上下文
- 使用 epoll_ctl 在上下文中添加和删除文件描述符
- 使用 epoll_wait 在上下文中等待事件
样例实现:
1 |
|
函数的执行流程:
- 有数据时会将文件描述符放在队首。
- epoll 会返回有数据的文件描述符的个数
- 根据返回的个数读取文件描述符即可
- 读取处理
函数特性:
- 我们可以在等待时添加和删除文件描述符
- epoll_wait 只返回文件描述符就绪的对象
- epoll 有更好的性能——O(1) 而不是 O(n)
- epoll 可以表现为级别触发或边缘触发
- epoll 是 Linux 特定的,因此不可移植
参考资料
https://www.bilibili.com/video/BV1qJ411w7du?from=search&seid=14828976220028495409
https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll/#.XYD0TygzaUl
https://notes.shichao.io/unp/ch6/
https://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
I/O 多路复用
https://wangqian0306.github.io/2021/io-multiplexing/