IO多路复用定义

以内核的形式帮忙检测读缓冲区和写缓冲区中的状态。例如:accpet,正常来说accpet在网络通信的过程中需要阻塞程序,等待客户端的连接。但现在这件事在IO多路复用中则可以交给内核来处,如用select将

selectpollepoll 都是 Linux 系统提供的 I/O 多路复用 机制。它们的核心目标是让单个进程或线程能够高效地监控多个文件描述符(fd),当其中任何一个 fd 就绪(如可读、可写)时,就进行处理。

这三种技术是演进而非简单的替代关系,各自适用于不同的场景。

select:开创者

select 是最早出现的 I/O 多路复用技术,也是 POSIX 标准的一部分,因此具有极好的跨平台性。

定义与核心思想

select 通过维护三个位图(bitmask)来分别监控文件描述符的读、写和异常状态。每次调用 select,内核都会线性扫描所有被监控的 fd,检查其状态是否发生变化。

函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数详解
  • nfds: 一个整数值,指定需要被检查的文件描述符的范围。select 会检查从 0nfds-1 的所有文件描述符。通常设置为所有被监控 fd 中的最大值加 1。
  • readfds: 指向一个 fd_set 结构体的指针,用于监控可读事件(例如,套接字的接收缓冲区有数据到来)。
  • writefds: 指向一个 fd_set 结构体的指针,用于监控可写事件(例如,套接字的发送缓冲区有空闲空间)。
  • exceptfds: 指向一个 fd_set 结构体的指针,用于监控异常事件(例如,带外数据到达)。
  • timeout: 指向 timeval 结构体的指针,用于设置 select 调用的超时时间。
    • NULL: 永久阻塞,直到有事件发生。
    • 秒=0, 微秒=0: 非阻塞,立即返回。
    • 其他值: 阻塞指定的时间。
  • 返回值:通过监控readfds,返回可读事件的fd总数量。(传入其它fd集合时同理)
核心用法与宏

select 的用法围绕 fd_set 和一组宏展开:

  1. 定义集合: fd_set readfds;
  2. 清空集合: FD_ZERO(&readfds);
  3. 添加 fd: FD_SET(fd, &readfds);
  4. 检查 fd: if (FD_ISSET(fd, &readfds)) { /* fd 可读 */ }
  5. 调用 select: 传入设置好的集合。
  6. 处理返回: select 返回后,内核会修改传入的集合,只保留就绪的 fd。需要再次使用 FD_ISSET 宏来遍历检查哪些 fd 真正就绪了。
主要缺点
  • 连接数限制: 受 fd_set 位图大小的限制,默认最多只能监控 1024 个 fd(由 FD_SETSIZE 宏定义)。
  • 性能开销: 每次调用都需要将整个 fd_set 从用户态拷贝到内核态,返回时再拷贝回来。
  • 效率低下: 无论 fd 是否就绪,每次都需要线性扫描所有被监控的 fd,时间复杂度为 O(N)。

poll:改进者

poll 的出现主要是为了解决 select 的连接数限制问题。

定义与核心思想

poll 不再使用位图,而是采用一个 pollfd 结构体数组来管理被监控的文件描述符及其关注的事件。

函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数详解
  • fds: 指向 pollfd 结构体数组的指针,每个元素代表一个被监控的 fd 及其事件。
  • nfds: fds 数组中元素的数量,即被监控的 fd 总数。
  • timeout: 超时时间,单位为毫秒。行为与 select 类似(-1 为永久阻塞,0 为非阻塞)。
pollfd 结构体
struct pollfd {
    int   fd;         // 要监控的文件描述符
    short events;     // 用户关心的事件(输入)
    short revents;    // 实际发生的事件(输出)
};
  • events: 在调用 poll 前设置,告诉内核我们关心哪些事件,如 POLLIN(可读)、POLLOUT(可写)。
  • revents: poll 返回后,由内核填充,告知用户哪些事件真正发生了。
主要缺点

虽然 poll 突破了 1024 个 fd 的限制,但它和 select 一样,每次调用时仍需要将整个 pollfd 数组从用户态拷贝到内核态,并且内核也需要线性扫描所有 fd 来判断状态,时间复杂度依然是 O(N)。


epoll:高性能的终极方案

epoll 是 Linux 2.6 内核引入的,专为解决 selectpoll 在高并发场景下的性能瓶颈而设计。

定义与核心思想

epoll 采用事件驱动模型。它通过 epoll_ctl 将需要监控的 fd 一次性注册到内核的一个红黑树中。当某个 fd 就绪时,内核通过回调机制直接将其加入一个就绪链表epoll_wait 只需检查这个就绪链表即可,无需轮询。

核心函数与用法

epoll 的使用涉及三个独立的函数,职责分离清晰:

  1. epoll_create: 创建一个 epoll 实例。

    int epfd = epoll_create(int size); // size 仅作为提示,内核会动态调整
    

    返回的 epfd 是一个文件描述符,代表这个 epoll 实例。

  2. epoll_ctl: 管理 epoll 实例的兴趣列表(即红黑树)。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
    • epfd: epoll_create 返回的实例描述符。
    • op: 操作类型,EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)。
    • fd: 要操作的目标文件描述符。
    • event: 指向 epoll_event 结构体的指针,用于指定关注的事件和关联数据。
  3. epoll_wait: 等待事件发生。

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • epfd: epoll 实例描述符。
    • events: 用于接收就绪事件的数组。
    • maxevents: events 数组的大小。
    • timeout: 超时时间(毫秒)。
      该函数只返回已经就绪的事件,时间复杂度为 O(1),与总连接数无关。
epoll_event 结构体
struct epoll_event {
    uint32_t     events;      // 监控的事件(如 EPOLLIN, EPOLLOUT)
    epoll_data_t data;        // 用户数据,通常用于存储 fd 或指针
};
两种工作模式

epoll 支持两种事件触发模式,这是其灵活性的关键:

  • 水平触发 (Level Triggered, LT): 默认模式。只要 fd 处于就绪状态(例如,接收缓冲区有数据未读完),每次调用 epoll_wait 都会通知你。这与 select/poll 的行为一致,编程模型简单。
  • 边缘触发 (Edge Triggered, ET): 高性能模式。仅在 fd 状态发生变化时通知一次(例如,缓冲区从空变为有数据)。如果应用程序没有一次性处理完所有数据,epoll_wait 将不会再次通知。因此,使用 ET 模式必须配合非阻塞 I/O,并在收到通知后循环读写,直到返回 EAGAIN 错误。

三者对比总结

特性 select poll epoll
底层数据结构 位图 (fd_set) 数组 (pollfd) 红黑树 + 就绪链表
最大连接数 有限制 (通常 1024) 无限制 (受内存限制) 无限制 (受内存限制)
时间复杂度 O(N) O(N) O(1) (仅与就绪数相关)
内存拷贝 每次调用都拷贝 每次调用都拷贝 通过 epoll_ctl 注册,减少拷贝
工作模式 仅 LT 仅 LT LT (默认) / ET
可移植性 极好 (POSIX) 较好 (POSIX) Linux 特有
适用场景 低并发、跨平台需求 中等并发 高并发、高性能服务器 (如 Nginx, Redis)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐