IO多路复用——Poll底层原理深度分析
文章目录
- 1. 工作流程
- 2. 详细文字描述
- 2.1 初始化阶段
- 2.2 监听阶段
- 2.3 事件处理阶段
- 2.4 循环监听
- 3.Poll与Select底层原理对比
- 3.1 系统调用层面
- Select系统调用
- Poll系统调用
- 3.2 核心数据结构差异
- Select的数据结构
- Poll的数据结构
- 3.3 底层实现机制对比
- 3.4 详细底层原理分析
- 文件描述符传递方式
- 内核处理流程
- 3.5 性能差异分析
- 时间复杂度
- 内存使用
- 3.6 系统调用开销对比
- 3.7 实际性能测试场景
- 场景1:监听少量文件描述符(< 100)
- 场景2:监听大量文件描述符(> 1000)
- 场景3:高并发场景
- 3.8 底层实现细节对比
Poll是IO多路复用的一种实现方式,它允许一个进程同时监听多个文件描述符,当其中任何一个文件描述符就绪时,进程就可以进行相应的IO操作。
- 核心特点:
- 非阻塞:不会因为某个文件描述符未就绪而阻塞整个进程
- 轮询机制:通过轮询检查所有被监听的文件描述符状态
- 事件驱动:基于事件通知机制,只有文件描述符状态发生变化时才返回
1. 工作流程
2. 详细文字描述
2.1 初始化阶段
- 创建pollfd结构体数组:为每个需要监听的文件描述符分配一个pollfd结构体
- 设置监听参数:
fd
:要监听的文件描述符events
:要监听的事件类型(读、写、异常等)revents
:实际发生的事件(由内核填充)
2.2 监听阶段
- 调用poll函数:将pollfd数组传递给内核
- 内核处理:
- 检查所有文件描述符的状态
- 将就绪的文件描述符标记在revents字段中
- 返回就绪的文件描述符数量
2.3 事件处理阶段
- 遍历pollfd数组:检查每个文件描述符的revents字段
- 事件分类处理:
POLLIN
:数据可读POLLOUT
:数据可写POLLERR
:发生错误POLLHUP
:连接断开
2.4 循环监听
处理完当前事件后,重新调用poll函数继续监听,形成事件循环。
3.Poll与Select底层原理对比
3.1 系统调用层面
Select系统调用
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
Poll系统调用
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
3.2 核心数据结构差异
Select的数据结构
typedef struct {long fds_bits[FD_SETSIZE / NFDBITS];
} fd_set;// 位图表示,每个bit代表一个文件描述符
// 例如:fds_bits[0] = 0x00000001 表示文件描述符0被设置
Poll的数据结构
struct pollfd {int fd; // 文件描述符short events; // 请求的事件short revents; // 返回的事件
};
3.3 底层实现机制对比
都有两次用户空间、内核空间之间的拷贝。
3.4 详细底层原理分析
文件描述符传递方式
Select:
- 使用三个独立的位图(readfds, writefds, exceptfds)
- 每个位图使用long数组存储,每个bit代表一个文件描述符
- 文件描述符范围限制:0 到 FD_SETSIZE-1(通常是1024)
Poll:
- 使用pollfd结构体数组
- 每个结构体包含文件描述符和事件信息
- 无文件描述符数量限制
内核处理流程
Select内核处理:
// 伪代码展示select内核处理
int do_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds) {// 1. 遍历所有文件描述符(0到nfds-1)for (int fd = 0; fd < nfds; fd++) {// 2. 检查每个fd是否在对应的位图中if (FD_ISSET(fd, readfds)) {// 3. 检查该fd是否可读if (is_readable(fd)) {// 4. 设置对应的位FD_SET(fd, readfds);} else {// 5. 清除对应的位FD_CLR(fd, readfds);}}// 类似处理writefds和exceptfds}return ready_count;
}
Poll内核处理:
// 伪代码展示poll内核处理
int do_poll(struct pollfd *fds, nfds_t nfds) {// 1. 遍历pollfd数组for (int i = 0; i < nfds; i++) {int fd = fds[i].fd;short events = fds[i].events;// 2. 检查该fd的状态short revents = 0;if (events & POLLIN && is_readable(fd)) {revents |= POLLIN;}if (events & POLLOUT && is_writable(fd)) {revents |= POLLOUT;}// 3. 设置revents字段fds[i].revents = revents;}return ready_count;
}
3.5 性能差异分析
时间复杂度
Select:
- 遍历复杂度:O(n),其中n是最大文件描述符值
- 即使只监听文件描述符1000,也要检查0-999的所有fd
Poll:
- 遍历复杂度:O(n),其中n是实际监听的文件描述符数量
- 只检查用户实际设置的pollfd结构体
内存使用
Select:
// 内存布局示例
fd_set readfds; // 128字节(1024位)
fd_set writefds; // 128字节
fd_set exceptfds; // 128字节
// 总计:384字节固定大小
Poll:
// 内存布局示例
struct pollfd fds[100]; // 每个结构体12字节
// 总计:1200字节(可动态调整)
3.6 系统调用开销对比
3.7 实际性能测试场景
场景1:监听少量文件描述符(< 100)
- Select性能:较好,因为位图操作效率高
- Poll性能:相当,结构体数组开销不大
场景2:监听大量文件描述符(> 1000)
- Select性能:较差,需要遍历大量无效fd
- Poll性能:较好,只处理实际监听的fd
场景3:高并发场景
- Select性能:每次都要重新设置位图
- Poll性能:可以复用pollfd数组
3.8 底层实现细节对比
特性 | Select | Poll |
---|---|---|
数据结构 | 位图 | 结构体数组 |
文件描述符限制 | 1024 | 无限制 |
内核遍历方式 | 线性扫描位图 | 线性扫描数组 |
用户空间处理 | 位操作 | 数组遍历 |
内存效率 | 固定大小 | 动态大小 |
系统调用参数 | 多个独立参数 | 单个结构体数组 |