欢迎关注本公众号,专注面试题拆解
分享一套视频课程:《C++实现百万并发服务器》 面试需要项目的可以找我获取,免费分享。 欢迎V:fb964919126
网络编程IO多路复用之poll模式
在网络编程中,除了select 模式外,另一种常用的I/O多路复用技术是 poll 模式,poll 模式相对于 select 模式有一些改进。(这篇文章介绍poll模式的基础,掌握好的可以直接拉到最后看9道思考题是否可以答出,欢迎评论区留言)
01
poll函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明
1、struct pollfd *fds:一个指向 pollfd 结构体数组的指针,每个结构体表示一个要监控的文件描述符。
2、nfds_t nfds:要监控的文件描述符数量。
3、int timeout:等待的时间(以毫秒为单位)。如果设置为 -1,则 poll 将无限期阻塞,直到至少一个文件描述符准备好为止。如果设置为 0,则 poll 立即返回,不阻塞。
pollfd 结构体
struct pollfd {
int fd; // 文件描述符
short events; // 请求的事件
short revents; // 实际发生的事件
};
fd:要监控的文件描述符。
events:请求的事件,可以是以下值的组合:
POLLIN:文件描述符可读。
POLLOUT:文件描述符可写。
POLLERR:发生错误。
POLLHUP:挂起(例如,对端关闭连接)。
POLLNVAL:无效的请求。
revents:实际发生的事件,返回时会被填充。
返回值
> 0:准备好的文件描述符的数量。
0:超时,没有文件描述符准备好。
-1:发生错误,可以通过 errno 获取具体的错误码。
02
系统调用过程
用户初始化:用户程序初始化pollfd结构体数组,指定需要检测的文件描述符和事件类型。
// 使用方式
struct pollfd fds[100]; // 数组形式
系统调用:用户程序通过poll系统调用将这些结构体传递给内核,并指定一个超时时间。在内核中保存pollfd的数据结构是poll_list,poll_list链表单个元素可以存储固定数量的pollfd对象。
// 内核态的 poll_list 结构
struct poll_list {
struct poll_list *next; // 指向下一个 poll_list
int len; // 当前片段包含的 pollfd 数量
struct pollfd entries[]; // 柔性数组
};
// POLL_ENTRY_SIZE:每个 poll_list 节点最大能存储的 pollfd 数量
sizeof(struct pollfd))
内核轮询:内核开始轮询这些结构体中的文件描述符,检查是否有指定的事件发生。一次poll调用需完成整个poll_list链表轮询工作,轮询socket的过程中会创建socket等待队列项,并加入socket等待队列(用于socket唤醒进程)
事件检测:如果检测到某个文件描述符处于就绪状态,内核会将此文件描述符对应的revents字段设置为实际发生的事件。poll系统调用完成一次轮询后,如果检测到有socket处于就绪状态,则将poll_list链表所有的 pollfd通过copy_to_user拷贝至用户 pollfd数组。如果未检测到有socket处于就绪状态,根据超时时间确定是否返回或者阻塞进程。
if (copy_to_user(ufds, list->entries,
nfds * sizeof(struct pollfd))) {
return -EFAULT;
}
超时处理:如果在指定时间内没有文件描述符变为就绪状态,则poll会阻塞进程直到超时时间到达。
结果返回:一旦有文件描述符变为就绪状态,或者超时时间到达,内核会通过注册到socket等待队列的回调函数poll_wake将进程唤醒,唤醒的进程将再次轮询poll_list链表。
超时机制
poll支持非阻塞模式(立即返回)和阻塞模式(等待超时)。用户可以通过提供一个int值来指定超时时间。如果在指定时间内没有任何文件描述符变为就绪状态,poll会阻塞当前线程直到超时时间到达。
// timeout 的三种情况:
timeout = -1 // 永久阻塞
timeout = 0 // 立即返回
timeout > 0 // 等待指定毫秒数
03
poll编程模型图
+------------------+ 1. 创建 socket +------------------+
| |--------------------> | |
| | | Socket 1 |
| | 2. bind/listen | |
| |--------------------> | |
| | +------------------+
| |
| | +------------------+
| | 1. 创建 socket | |
| 应用程序 |--------------------> | Socket 2 |
| | | |
| +----------+ | 2. connect | |
| | pollfd[] | |--------------------> | |
| |----------| | +------------------+
| | fd1 | |
| | events1 | | +------------------+
| | revents1 | | 1. 创建 socket | |
| |----------| |--------------------> | Socket 3 |
| | fd2 | | | |
| | events2 | | 2. connect | |
| | revents2 | |--------------------> | |
| |----------| | +------------------+
| | ... | |
| +----------+ | +------------------+
| | | | 等待队列 |
| | | 3. poll() 调用 | +--------+ |
| +----------------------------------------> 进程 | |
| | | 节点 | |
| | | +--------+ |
| | +------------------+
| |
| | 4. 事件就绪 +------------------+
| | <-------------------- | 事件源 |
| | | (数据/连接等) |
+------------------+ +------------------+
执行流程:
1. 应用程序创建多个 socket
2. 创建 pollfd 数组,设置感兴趣的事件(events)
3. 调用 poll():
- 如果没有就绪事件,进程被挂起
- 进程节点加入等待队列
4. 当有事件发生:
- 设置对应的 revents
- 唤醒进程
5. poll() 返回,应用程序处理就绪事件
6. 重复步骤 3-5
事件类型:
POLLIN: 可读
POLLOUT: 可写
POLLERR: 错误
POLLHUP: 挂起
这个模型图展示了:
1. pollfd 数组的结构
2. 进程与多个 socket 的关系
3. poll 调用的等待机制
4. 事件通知的流程
5. 整体的事件循环模式
04
poll事件
1. 基本事件定义
// <poll.h> 中的事件定义
2. 组合事件定义
// 常用的组合事件
3. 事件使用示例
// 1. 基本读写监控
struct pollfd pfd = {
.fd = sockfd,
.events = POLLIN | POLLOUT, // 监控读写事件
};
// 2. 全面监控
struct pollfd pfd = {
.fd = sockfd,
.events = POLLIN | POLLPRI | POLLOUT | POLLRDHUP,
};
// 3. 只读监控
struct pollfd pfd = {
.fd = sockfd,
.events = POLLIN | POLLRDNORM,
};
4. 常见事件组合场景
// TCP 服务器监听套接字
struct pollfd listen_fd = {
.fd = listenfd,
.events = POLLIN, // 只关注新连接到达
};
// TCP 客户端数据处理
struct pollfd client_fd = {
.fd = clientfd,
.events = POLLIN | POLLOUT | POLLRDHUP, // 关注读写和连接关闭
};
// 管道读端
struct pollfd pipe_read = {
.fd = pipefd[0],
.events = POLLIN | POLLPRI, // 关注普通数据和优先级数据
};
5. 事件触发条件
// POLLIN 触发条件
if (POLLIN & revents) {
// 1. 接收缓冲区中有数据可读
// 2. 对端关闭连接(读到EOF)
// 3. 监听socket有新连接请求
// 4. 有错误待处理
}
// POLLOUT 触发条件
if (POLLOUT & revents) {
// 1. 发送缓冲区有空闲空间
// 2. 非阻塞connect连接完成
}
// POLLERR 触发条件
if (POLLERR & revents) {
// 1. 发生错误
// 注意:不需要在 events 中设置,自动监测
}
6. 完整的事件处理示例
void handle_poll_events(struct pollfd *pfds, nfds_t nfds)
{
for (nfds_t i = 0; i < nfds; i++) {
// 检查是否有错误
if (pfds[i].revents & POLLERR) {
handle_error(pfds[i].fd);
continue;
}
// 检查连接是否断开
if (pfds[i].revents & (POLLHUP | POLLRDHUP)) {
handle_disconnect(pfds[i].fd);
continue;
}
// 检查是否可读
if (pfds[i].revents & (POLLIN | POLLRDNORM)) {
if (pfds[i].fd == listen_fd) {
handle_new_connection();
} else {
handle_read(pfds[i].fd);
}
}
// 检查是否可写
if (pfds[i].revents & (POLLOUT | POLLWRNORM)) {
handle_write(pfds[i].fd);
}
// 检查无效文件描述符
if (pfds[i].revents & POLLNVAL) {
cleanup_fd(pfds[i].fd);
}
}
}
7. 自动监测的事件
// 这些事件不需要在 events 中设置
POLLERR // 错误
POLLHUP // 挂起
POLLNVAL // 无效fd
05
总结
1:对比select,poll不是跨平台的。
2: poll 和 select一样,需要遍历所有的文件描述符,poll是遍历的poll_list链表
3:poll 对比 select,没有1024限制。
4:poll 返回后,和 select类似,程序需要手动遍历所有已经注册的文件描述符来确定哪些描述符已经准备好。
5:在再次调用 poll的时候,对于文件描述符数组,poll 不需要再次赋值,select 在每次调用之前都需要重新赋值文件描述符集合,因为上次的调用对集合做了修改。
06
思考延伸5问
问题1:poll_list链表具体是怎么工作的?
问题2:poll返回的revent处理完毕后需要清零处理吗?
问题3:poll是同步还是异步?
问题4:poll如何设置非阻塞模式?
问题5: poll对比select有哪些优点?
问题6: poll在内核为什么要使用poll_list链表结构而不是数组?
问题7: poll在处理大量文件描述符时的性能瓶颈在哪里?
问题8:如何处理 poll 被信号中断的情况?
问题9:poll为什么要将events 和 revents分开?
end
CppPlayer
关注,回复【电子书】珍藏CPP电子书资料赠送
精彩文章合集
专题推荐