I/O多路复用的三种机制(select、poll、epoll)

select、poll、epoll是用于I/O所路服用的三种机制,常见于Linux系统中,用于高效处理多个文件描述符的I/O事件。

select

select是最早的I/O多路复用机制,允许程序监听多个文件描述符,等待其中一个或多个变为可读、可写或出现异常。

  • 特点:

    • 通过fd_set数据结构管理文件描述符。
    • 支持的文件描述符数量有限(通常为1024)。
    • 每次调用都需要将fd_set从用户空间拷贝到内核空间。
    • 每次调用都需要遍历所有文件描述符,时间复杂度为O(n)。
  • 优点:

    • 跨平台支持,几乎所有操作系统都支持了select
  • 缺点:
    • 文件描述符数量受限。
    • 每次调用都需要拷贝fd_set,效率低。
    • 需要遍历所有文件描述符,性能随文件描述符数量增加而下降。

常用API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

FD_ZERO(fd_set *set); // 清空文件描述符集合
FD_SET(int fd, fd_set *set); // 将文件描述符添加到集合
FD_CLR(int fd, fd_set *set); // 从集合中移除文件描述符
FD_ISSET(int fd, fd_set *set); // 检查文件描述符是否在集合中

/*
使用步骤:
1、定义并初始化 fd_set 集合。
2、使用 FD_SET 将需要监视的文件描述符添加到集合。
3、调用 select 等待文件描述符就绪。
4、使用 FD_ISSET 检查哪些文件描述符就绪。
*/

poll

poll是对`select的改进,使用pollfd结构体管理文件描述符

  • 特点:

    • 使用pollfd结构体管理文件描述符,没有数量上的限制,但是收到系统资源的限制。
    • 每次调用仍然需要将pollfd数组从用户空间拷贝到内核空间。
    • 每次调用都需要遍历所有文件描述符,时间复杂度为O(n)。
  • 优点:

    • 支持的文件描述符数量没有硬性限制。
    • 相比于select更加灵活。
  • 缺点:

    • 每次调用都需要拷贝pollfd,效率低。
    • 需要遍历所有文件描述符,性能随文件描述符数量增加而下降。

常用API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd; // 文件描述符
short events; // 监视的事件(如 POLLIN、POLLOUT)
short revents; // 实际发生的事件
};

/*
使用步骤:
1、定义 pollfd 数组,设置需要监视的文件描述符和事件。
2、调用 poll 等待文件描述符就绪。
3、检查 pollfd 中的 revents 字段,判断哪些文件描述符就绪。
*/

epoll

epoll是Linux特有的高校I/O多路复用机制,解决了selectpoll的性能问题。

  • 特点:

    • 使用红黑树和双链表管理文件描述符,支持高校的事件注册和通知。
    • 支持边沿(ET)触发和水平(LT)触发模式。
    • 每次调用不需要拷贝所有文件描述符,只需要返回就绪的事件。
    • 时间复杂度为O(1),性能不受文件描述符数量影响。
  • 优点:

    • 高效处理大量文件描述符,适合高并发场景。
    • 事件驱动,只返回就绪的事件,无需便利所有的文件描述符。
    • 支持边沿触发模式,减少事件通知次数。
  • 缺点:
    • 仅支持Linux系统,无法跨平台。

常用API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int epoll_create(int size); // 创建 epoll 实例 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 管理文件描述符
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待事件

struct epoll_event {
uint32_t events; // 事件类型(如 EPOLLIN、EPOLLOUT)
epoll_data_t data; // 用户数据
};

/*
使用步骤:
1. 调用 epoll_create 创建 epoll 实例。
2. 使用 epoll_ctl 添加、修改或删除需要监视的文件描述符。
3. 调用 epoll_wait 等待文件描述符就绪。
4. 处理就绪的事件。
*/

事件通知模式/触发机制(LT,ET)

水平触发(Level-Triggerred)和边沿触发(Eage_Triggerred)是I/O多路复用机制中的两种时间通知模式,他们决定了何时通知应用程序文件描述符的状态变化。

水平触发(LT)

默认的事件通知模式,当文件描述符处于就绪状态时,会持续通知应用程序。

当被监控的Socket上有可读事件发生时,服务器端不断地从epoll_wait中苏醒,直到内核缓冲区被read函数读完才结束,没必要一次执行尽可能多的读写操作。

  • 特点:

    • 只要文件描述符处于就绪状态(例如可读或可写),就会重复通知应用程序。
    • 应用程序可以在一次通知后不完全处理所有数据,下次调用时仍会收到通知。
    • 适用于对事件处理逻辑要求不高的场景。

边沿触发(ET)

高效的事件通知模式,只有当文件描述符的状态发生变化时(例如从不可读变为可读),才会通知应用程序。

当被监控的Socket上有可读事件发生时,服务器端只会从epoll_wait中苏醒一次,因此程序需要保证一次将内核缓冲区的数据读取完,因此需要循环的从文件描述符中读取数据,如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,因此,边沿触发模式一般搭配非阻塞I/O使用,程序会一直执行I/O操作。

  • 特点:

    • 只有当文件描述符的状态发生变化时(例如从不可读变为可读),才会通知应用程序。
    • 应用程序需要一次性处理完所有数据,否则可能会丢失后续事件。
    • 适用于高性能场景,但需要更复杂的逻辑。

LT和ET对比

特性 水平触发(LT) 边缘触发(ET)
通知时机 只要文件描述符就绪,就持续通知 只有当文件描述符状态变化时通知
事件处理 可以分多次处理数据 需要一次性处理完所有数据,否则会丢失事件
效率 可能频繁通知,效率较低 减少不必要的通知,效率较高
实现复杂度 简单易用 复杂,需要更精细的逻辑
适用场景 低并发、对性能要求不高的场景 高并发、对性能要求高的场景

三者对比

特性 select poll epoll
文件描述符数量限制 有限,通常为1024 无硬性限制 无硬性限制
事件通知机制 遍历所有文件描述符 遍历所有文件描述符 只返回就绪的事件
时间复杂度 O(n) O(n) O(1)
内存拷贝 每次调用拷贝fd_set 每次调用拷贝pollfd 不需要拷贝所有文件描述符
触发模式 LT LT LT和ET都支持
跨平台性 仅支持Linux