select
通过位图(fd_set)来管理要监控的 fd,最大长度1024。
每次调用 select 都需要把 fd 集合从用户态拷贝到内核态,内核还需要遍历所有 fd 检查状态,fd 越多效率越低。
select 只返回就绪的 fd 数量,用户态需要逐个遍历所有监控的 fd 来确认哪些就绪,浪费资源。
poll
poll 解决了 select 的 fd 数量限制问题,它使用结构体数组(struct pollfd) 来管理 fd,每个结构体包含 fd 和要监控的事件(POLLIN/POLLOUT 等),内核遍历数组检查 fd 状态,就绪后会修改结构体的 revents 字段标记就绪事件。
依然存在用户态 - 内核态拷贝和内核遍历所有 fd 的问题,fd 数量大时效率依然低下;用户态仍需遍历所有 pollfd 结构体判断哪些就绪。
epoll
事件驱动:内核维护一个 epoll 实例(通过 epoll_create 创建),用户态通过 epoll_ctl 向实例中添加 / 删除 / 修改要监控的 fd 和事件,内核为每个 fd 注册回调函数,fd 就绪时主动触发事件,无需遍历所有 fd。
内存映射:通过 mmap 实现用户态和内核态的内存共享,避免 fd 集合的频繁拷贝。
- 两种触发模式:
LT(水平触发,默认):只要 fd 有数据可读 / 可写,就会持续触发事件,直到数据被处理完。
ET(边缘触发):仅在 fd 状态从不可就绪变为就绪时触发一次,需要一次性读取 / 写入所有数据,效率更高,但编程更复杂(需处理 EAGAIN 错误)。
- 关键特点 & 优点
无 fd 数量限制:仅受限于系统最大文件描述符数(远大于 1024)。
高效性:fd 就绪时内核主动通知,无需遍历所有 fd,百万级 fd 下仍能保持高性能。
低拷贝开销:内存映射减少用户态 - 内核态数据拷贝。
io_uring
epoll 还是有一个问题。它只能告诉你网络是否可以读写,你还是需要自己写代码来读写网络。由于每次读写网络都会调用内核的函数,这样会造成大量的用户态和内核态切换,浪费很多计算资源。那有没有办法解决这个问题呢?
在2018年Linux内核新增了一个功能叫作 io_uring,它就解决了用户态切换过多的问题。
它解决问题的思路很简单。你在写程序的时候准备一个队列,里面记录了所有你想要做的读写操作,同时也包含了你预先分配的读写内存。
接着你将这个队列一股脑交给内核。内核会先做 epoll 的事情,检查哪些网络链接可以开始读写。然后内核会多做一步,帮你处理网络数据。
如果你的操作是写网络的话,会把你内存的数据写出去。如果你的操作是读操作的话,会把数据读到你预先分配的内存。内核操作完之后会把这些操作的状态记录在另一个列表里,返回给你的用户态进程。
双队列设计:
提交队列(SQ):用户态向内核提交 IO 请求(读 / 写 /accept 等)的队列。
完成队列(CQ):内核完成 IO 后,将结果写入该队列,用户态读取结果即可。
