select、poll与epoll
IO多路复用
I/O 多路复用是一种在单个线程或进程中同时监视多个 I/O 操作的机制。通过使用 I/O 多路复用,程序可以等待多个文件描述符(sockets、文件句柄等)中的任何一个变为可读、可写或发生异常,而无需阻塞并等待每个 I/O 操作完成。这使得程序能够更有效地处理并发 I/O 事件,而无需为每个事件创建一个独立的线程或进程。常见的IO多路复用有:select、poll、epoll。
select:简单介绍
(1)简介
select 是一个用于实现 I/O 多路复用的系统调用,通常用于监视多个文件描述符的状态变化。出现的时间比较早
(2)函数原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
(3)参数说明
- nfds 是待监视的文件描述符中最大的文件描述符加一。
- readfds、writefds 和 exceptfds 是分别指向可读、可写和异常文件描述符集合的指针。这些集合通过 FD_ZERO、FD_SET、FD_CLR 和 FD_ISSET 等宏来操作。
- timeout 是超时时间,用于指定 select 调用在等待事件发生的最长时间。如果设置为 NULL,select 将会一直阻塞,直到有事件发生。
- 返回值:select 返回就绪文件描述符的数量,或者在超时之前无事件发生时返回 0。如果出错,则返回 -1,并设置相应的错误码。
(4)工作原理
调用 select 时,将会阻塞进程,直到监视的文件描述符中的至少一个发生状态变化。
一旦有文件描述符就绪(可读、可写或发生异常),select 将返回,并更新文件描述符集合,以指示哪些文件描述符处于就绪状态。
程序可以使用 FD_ISSET 宏来检查哪些文件描述符已经就绪,并执行相应的 I/O 操作。
(5)底层实现
一般来说Linux内核在实现select时采用的数据结构是位图数组。用于表示文件描述符的状态。
(6)优缺点
- 优点:select 在许多 UNIX 系统上都有广泛的支持。
它可以同时监视多种类型的事件(可读、可写、异常等)。- 缺点:select 有文件描述符数量的限制,通常在 1024 个左右。每次调用 select 都需要将文件描述符集合从用户空间复制到内核空间,这会带来一定的开销。select 的效率在处理大量文件描述符时可能会下降,因为它采用线性扫描的方式遍历所有的文件描述符。
select:实现echo服务器的并发
poll:简单介绍
(1)简介
与 select 类似,poll 允许程序同时等待多个 I/O 事件的发生,而无需阻塞在单个文件描述符上。
(2)函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; // 待监视的文件描述符
short events; // 关注的事件类型(如 POLLIN、POLLOUT)
short revents; // 实际发生的事件类型
};
(3)参数说明
- fds: 是一个指向 struct pollfd 数组的指针,每个 struct pollfd 结构体用于描述一个待监视的文件描述符及其关注的事件。
- nfds: 是 fds 数组的长度,即待监视的文件描述符的数量。
- timeout: 是超时时间,用于指定 poll 调用在等待事件发生的最长时间。如果设置为负数,poll 将会一直阻塞,直到有事件发生。如果设置为 0,poll 将会立即返回,不等待事件发生。
- poll 返回就绪文件描述符的数量,或者在超时之前无事件发生时返回 0。如果出错,则返回 -1,并设置相应的错误码。
(4)工作原理
- 调用 poll 时,程序会阻塞在此处,等待监视的文件描述符中的任何一个发生状态变化。
- 一旦有文件描述符就绪,poll 将返回,并更新 revents 字段,以指示文件描述符的就绪状态。
- 程序可以检查 revents 字段来确定哪些文件描述符已经就绪,并执行相应的 I/O 操作。
(5)底层实现
linux中,poll使用了poll_table来管理文件描述符集合,通常是
为了快速查找和管理文件描述符及其对应的事件,底层数据结构一般会采用哈希表或者红黑树。
(6)优缺点
- 优点:poll 没有文件描述符数量的限制,适用于大规模的并发连接。
它的使用比 select 更简单,因为不需要设置文件描述符的最大值。- 缺点:每次调用 poll 都需要将文件描述符集合从用户空间复制到内核空间,这可能会产生一定的开销。在处理大量文件描述符时,效率可能不如 epoll。
poll:实现echo服务器的并发
epoll:简单介绍
(1)简介
epoll与传统的 select 和 poll 相比,epoll 在处理大量并发连接时具有更好的性能表现。主要原因就是采用了内存映射技术、边缘触发模式、可扩展性强。
(2)函数原型
#include <sys/epoll.h>
//创建epoll实例
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、EPOLLERR 等。
epoll_data_t data; // 包含事件相关的数据,可以是文件描述符或者指针。
};
(3)工作原理
- 创建 epoll 实例:使用 epoll_create 系统调用创建一个 epoll 实例,返回一个文件描述符,该文件描述符代表了这个 epoll 实例。
- 注册文件描述符:使用 epoll_ctl 系统调用向 epoll 实例注册文件描述符,并设置关注的事件类型(如可读、可写、异常等)。也可以通过 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL 操作来添加、修改和删除文件描述符。
- 等待事件发生:使用 epoll_wait 系统调用等待事件发生。
内核会检查注册的文件描述符,如果有文件描述符上发生了关注的事件,则将这些事件返回给用户空间。- 处理事件:用户程序接收到事件后,可以根据事件类型进行相应的处理。可以使用 epoll_data 结构体的 ptr 字段来存储用户定义的数据,以便在事件发生时进行回调操作。
(4)触发模式
- 水平触发模式(LT):此模式下,发生事件若未及时处理,会一直处于就绪状态,直到处理该事件。每次调用wait时都会被返回为就绪状态
- 边缘触发模式(ET):此模式下,发生事件若未及时处理,下次调用wait时,不会返回就绪状态,除非有新的事件发生。
(5)内存映射技术
所谓内存映射技术就是直接将事件表映射到用户空间(用户态)避免内核态与用户态之间切换的开销。减少系统调用次数和数据拷贝次数。