select、poll、epoll、io_uring

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 后,将结果写入该队列,用户态读取结果即可。

upload successful

文章目录
  1. 1. select
  2. 2. poll
  3. 3. epoll
  4. 4. io_uring
| 本站总访问量次 ,本文总阅读量