一:背景
1. 讲故事
在windows平台上,相信很多人都知道.NET异步机制是借助了Windows自带的 IO完成端口
实现的异步交互,那在 Linux 下.NET 又是怎么玩的呢?主要还是传统的 select,poll,epoll 的IO多路复用,在 coreclr源代码中我们都能找到它们的影子。
- select & poll
在平台适配层的 pal.cpp
文件中,有这样的一句话。
简而言之就是在不支持 poll 的linux版本中使用 select(fakepoll) 模拟,参考代码如下:
- epoll
同样的在 linux 中你也会发现很多,截图如下:
二:select IO多路复用
1. select 解读
在没有 select 之前,我们需要手工管理多句柄的收发,在使用select IO多路复用技术之后,这些多句柄管理就由用户转交给linux系统了,这个也可以从核心的 select
函数看出。
- readfds,writefds,exceptfds
这三个字段依次监视着哪些句柄已成可读状态,哪些句柄已成可写状态,哪些句柄已成异常状态,那技术上是如何实现的呢?在libc 中定义了一个 bit 数组,刚好文件句柄fd值
作为 bit数组的索引,linux 在内核中只需要扫描 __fds_bits 中哪些位为1 即可找到需要监控的句柄。
- nfds,timeout
为了减少扫描范围,提高程序性能,需要用户指定一个最大的扫描值到 nfds 上。后面的timeout即超时时间。
2. select 的一个小例子
说了再多还不如一个例子有说服力,我们使用 select 机制对 Console 控制台句柄 (STDIN_FILENO) 进行监控,一旦有数据进来立马输出,参考代码如下:
稍微解释下代码逻辑。
- 将 STDIN_FILENO=0 塞入到可读句柄监控 (readfds) 中。
- 数据进来之后 select 被唤醒,执行后续逻辑。
- 通过 FD_ISSET 判断 bit=0 的位置(STDIN_FILENO)是否可用,可用的话读取数据。
如果大家对 select 底层代码感兴趣,可以看下 linux 的 do_select
简化实现,大量的遍历逻辑(bit)。
三:epoll IO多路复用
1. epoll 解读
现在主流的软件(Redis,Nigix) 都是采用 epoll,它解决了select低效的遍历,毕竟数组最多支持1024个bit位,一旦句柄过多会影响异步读取的效率。epoll的底层借助了。
- 红黑树:对句柄进行管理,复杂度为 O(logN)。
- 就绪队列:一旦句柄变得可读或可写,内核会直接将句柄送到就绪队列。
libc中使用 epoll_wait
函数监视着就绪队列,一旦有数据立即提取,复杂度 O(1),其实这个机制和 Windows 的IO完成端口 已经很靠近了,最后配一下参考代码。
四:总结
说了这么多,文尾总结下目前主流的 epoll 和 iocp 各自的特点。
特性 | epoll (Linux) | IOCP (Windows) |
模型 | 事件驱动 (Reactor) | 完成端口 (Proactor) |
核心思想 | 通知可读写事件 | 通知I/O操作完成 |
适用场景 | 高并发网络编程 | 高并发I/O操作 |
编程复杂度 | 较低 | 较高 |
网络I/O性能 | 极佳(百万级连接) | 优秀 |
磁盘I/O支持 | 有限 | 完善 |
CPU利用率 | 高 | 中 |
内存开销 | 低 | 中 |