面试题:网络编程IO多路复用之poll模式

旅行   2024-11-01 08:26   广东  

欢迎关注本公众号,专注面试题拆解

分享一套视频课程:《C++实现百万并发服务器》
面试需要项目的可以找我获取,免费分享。 
欢迎V:fb964919126







网络编程IO多路复用之poll模式





在网络编程中,除了select 模式外,另一种常用的I/O多路复用技术是 poll 模式,poll 模式相对于 select 模式有一些改进。(这篇文章介绍poll模式的基础,掌握好的可以直接拉到最后看9道思考题是否可以答出,欢迎评论区留言


01

poll函数原型

#include <poll.h>
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 数量#define POLL_ENTRY_SIZE ((PAGE_SIZE - sizeof(struct poll_list)) / \ 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. 应用程序创建多个 socket2. 创建 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> 中的事件定义#define POLLIN      0x0001    // 可读#define POLLPRI     0x0002    // 高优先级可读#define POLLOUT     0x0004    // 可写#define POLLERR     0x0008    // 错误#define POLLHUP     0x0010    // 挂起#define POLLNVAL    0x0020    // 文件描述符未打开

2. 组合事件定义

// 常用的组合事件#define POLLRDNORM  0x0040    // 普通数据可读#define POLLRDBAND  0x0080    // 优先级带数据可读#define POLLWRNORM  0x0100    // 普通数据可写#define POLLWRBAND  0x0200    // 优先级带数据可写#define POLLMSG     0x0400    // 消息可用#define POLLRDHUP   0x2000    // TCP连接被对端关闭,或关闭写半连接

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电子书资料赠送

精彩文章合集

专题推荐

【专辑】计算机网络真题拆解
【专辑】大厂最新真题
【专辑】C/C++面试真题拆解

CppPlayer
一个专注面试题拆解的公众号
 最新文章