当前位置: 首页 > news >正文

深入理解 Linux `poll` 模型:`select` 的增强版

在 Linux I/O 多路复用模型中,poll 紧随 select 之后,作为其功能更强大、限制更少的继任者。虽然 select 在处理并发连接方面迈出了重要一步,但其自身的一些缺陷促使了 poll 的诞生。poll 模型同样允许单个进程同时监控多个文件描述符,等待 I/O 事件,但在文件描述符数量限制和接口使用上进行了优化。

poll 为什么比 select 更优?

select 的一个主要痛点是其对文件描述符数量的硬性限制(通常为 1024),这使得它难以应对大规模并发连接的场景(即所谓的 C10K 问题)。此外,select 每次调用都需要重新构建并拷贝 fd_set 结构,并且在返回后需要遍历整个集合来查找就绪的文件描述符,效率较低。

poll 模型解决了 select 的这些局限性:

  1. 无文件描述符数量限制poll 不受 FD_SETSIZE 宏的限制,它通过一个 pollfd 结构体数组来管理文件描述符,这个数组的大小可以动态调整,理论上只受限于系统内存。
  2. 更灵活的事件类型poll 使用 eventsrevents 字段来指定和返回事件类型,这使得事件的表示更加清晰和灵活。
  3. 更高效的事件通知:虽然 poll 同样需要遍历文件描述符数组来查找就绪的事件,但由于不再需要每次都重新构建并拷贝整个位图,其效率有所提升。

poll 的核心工作原理

poll 函数通过一个 struct pollfd 结构体数组来工作。数组中的每个元素都对应一个待监控的文件描述符及其关注的事件。

struct pollfd 结构体定义如下:

struct pollfd {int fd;        // 待监控的文件描述符short events;  // 关注的事件short revents; // 实际发生的事件
};
  • fd: 要监控的文件描述符。
  • events: 一个位掩码,表示你希望监控的事件类型。常见的事件标志包括:
    • POLLIN: 文件描述符可读(有数据可读)。
    • POLLPRI: 有紧急数据可读。
    • POLLOUT: 文件描述符可写(可以写入数据)。
    • POLLRDHUP: 对端连接关闭或半关闭(仅限 Linux 2.6.17+)。
    • POLLERR: 发生错误。
    • POLLHUP: 对端连接挂起(通常表示对端关闭)。
    • POLLNVAL: 无效的文件描述符。
  • revents: 由内核填充的位掩码,表示实际发生的事件。它会包含在 events 中指定的一些事件,以及可能发生的其他事件(如 POLLERR, POLLHUP, POLLNVAL)。

当调用 poll 函数时,它会阻塞直到:

  • pollfd 数组中有一个或多个文件描述符上发生了关注的事件。
  • 指定的超时时间到达。
  • 一个信号被捕获。

函数返回后,应用程序可以遍历 pollfd 数组,检查每个元素的 revents 字段来确定哪些文件描述符已经就绪,并执行相应的 I/O 操作。

poll 函数原型

#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds: 指向 struct pollfd 数组的指针,其中包含要监控的文件描述符及其事件信息。
  • nfds: fds 数组中元素的数量,表示要监控的文件描述符的总数。
  • timeout: poll 函数的超时时间,单位为毫秒。
    • -1: poll 会一直阻塞直到有事件发生。
    • 0: poll 会立即返回,不阻塞(非阻塞轮询)。
    • 正整数: poll 会阻塞指定的时间(毫秒),或直到有事件发生。
  • 返回值:
    • 成功时,返回就绪文件描述符的总数。
    • 超时时,返回 0。
    • 失败时,返回 -1 并设置 errno

poll 模型示例:简单的 TCP 服务器

下面是一个使用 poll 模型实现的简单 TCP 服务器示例。这个服务器可以同时处理多个客户端连接,并在接收到客户端消息后将其回显。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <poll.h> // poll 函数头文件#define BUF_SIZE 1024
#define PORT 8080
#define MAX_CLIENTS 30 // 假设最大客户端数量int main() {int serv_sock;struct sockaddr_in serv_addr;struct sockaddr_in clnt_addr;socklen_t clnt_addr_size;char buf[BUF_SIZE];int str_len;struct pollfd client_fds[MAX_CLIENTS + 1]; // 包含服务器套接字和客户端套接字int client_count = 0; // 当前连接的客户端数量// 1. 创建服务器套接字serv_sock = socket(PF_INET, SOCK_STREAM, 0);if (serv_sock == -1) {perror("socket() error");exit(1);}// 允许地址重用,避免 TIME_WAIT 状态导致端口占用int optval = 1;setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));// 2. 绑定地址和端口memset(&serv_addr, 0, sizeof(serv_addr));serv_addr.sin_family = AF_INET;serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);serv_addr.sin_port = htons(PORT);if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {perror("bind() error");exit(1);}// 3. 监听连接if (listen(serv_sock, 5) == -1) {perror("listen() error");exit(1);}// 初始化 pollfd 数组// 将服务器套接字加入到监控列表的第一个位置client_fds[0].fd = serv_sock;client_fds[0].events = POLLIN; // 关注可读事件(即新连接请求)client_count = 1; // 当前监控的文件描述符数量printf("Server started on port %d\n", PORT);while (1) {// 4. 调用 poll 监听文件描述符// timeout = -1 表示无限期等待int fd_num = poll(client_fds, client_count, -1);if (fd_num == -1) {perror("poll() error");break;}// 5. 遍历就绪的文件描述符for (int i = 0; i < client_count; i++) {if (client_fds[i].revents & POLLIN) { // 检查是否有可读事件if (client_fds[i].fd == serv_sock) { // 服务器套接字就绪,有新连接请求if (client_count >= MAX_CLIENTS + 1) { // 检查是否达到最大客户端数printf("Max clients reached, new connection rejected.\n");int temp_sock = accept(serv_sock, NULL, NULL); // 接收但立即关闭if (temp_sock != -1) close(temp_sock);continue;}int clnt_sock;clnt_addr_size = sizeof(clnt_addr);clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);if (clnt_sock == -1) {perror("accept() error");continue;}// 将新的客户端套接字加入到监控列表client_fds[client_count].fd = clnt_sock;client_fds[client_count].events = POLLIN; // 关注可读事件client_fds[client_count].revents = 0;     // 清零 reventsclient_count++; // 增加监控的文件描述符数量printf("Connected client: %s:%d (fd: %d). Total clients: %d\n",inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port), clnt_sock, client_count -1);} else { // 客户端套接字就绪,有数据可读str_len = read(client_fds[i].fd, buf, BUF_SIZE);if (str_len == 0) { // 客户端关闭连接printf("Closed client (fd: %d). Total clients: %d\n", client_fds[i].fd, client_count - 2);close(client_fds[i].fd);// 将关闭的fd从数组中移除,用最后一个fd补位,并减少计数for (int j = i; j < client_count - 1; j++) {client_fds[j] = client_fds[j+1];}client_count--;i--; // 确保检查当前位置的新fd} else if (str_len > 0) {buf[str_len] = '\0'; // 确保字符串以 null 结尾printf("Received from fd %d: %s", client_fds[i].fd, buf);write(client_fds[i].fd, buf, str_len); // 回显数据} else { // read 错误perror("read() error");printf("Error on fd %d. Closing.\n", client_fds[i].fd);close(client_fds[i].fd);// 从数组中移除错误fdfor (int j = i; j < client_count - 1; j++) {client_fds[j] = client_fds[j+1];}client_count--;i--; // 确保检查当前位置的新fd}}} else if (client_fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {// 处理错误、挂起或无效文件描述符printf("Error/Hangup/Invalid FD on fd %d. Closing.\n", client_fds[i].fd);close(client_fds[i].fd);// 从数组中移除错误fdfor (int j = i; j < client_count - 1; j++) {client_fds[j] = client_fds[j+1];}client_count--;i--; // 确保检查当前位置的新fd}}}close(serv_sock);return 0;
}

如何编译和运行示例

  1. 保存代码: 将上述代码保存为 poll_server.c

  2. 编译: 打开终端,使用 GCC 编译器编译代码:

    gcc poll_server.c -o poll_server
    
  3. 运行服务器:

    ./poll_server
    

    服务器将启动并在 8080 端口监听。

  4. 使用客户端测试: 你可以使用 netcat (nc) 或编写一个简单的 C 语言客户端来测试服务器。

    使用 netcat:
    打开另一个终端,连接到服务器:

    nc 127.0.0.1 8080
    

    然后输入消息并按回车,服务器会回显你的消息。你可以打开多个终端来模拟多个客户端连接,观察 poll 如何高效地处理它们。

通过这个 poll 示例,我们可以看到它在处理大量并发连接时的优势,尤其是在避免 select 的文件描述符数量限制方面。虽然 poll 相比 select 有显著改进,但其每次调用仍然需要遍历整个 pollfd 数组来发现就绪事件,这在极端大规模并发场景下(例如几十万甚至上百万连接)依然会带来性能瓶颈。这也是后来更高效的 epoll(Linux 特有)和 kqueue(BSD 特有)模型出现的原因。

http://www.lqws.cn/news/543403.html

相关文章:

  • 记录一次飞书文档转md嵌入vitepress做静态站点
  • 微信小程序进度条progress支持渐变色
  • Stable Diffusion入门-ControlNet 深入理解-第三课:结构类模型大揭秘——深度、分割与法线贴图
  • 【LeetCode 热题 100】42. 接雨水——(解法三)单调栈
  • FPGA在嵌入式图像处理中的深度应用!
  • 深圳中青宝互动网络股份有限公司游戏运维工程师面试题(笔
  • python实战项目79:采集知乎话题下的所有回答
  • 【用户权限】超级用户(二)
  • win7实现永恒之蓝ms17_010漏洞之445端口
  • matlab实现相控超声波成像
  • 推荐一个基于C#开发的跨平台构建自动化系统!
  • 通信无BUG,ethernet ip转profinet网关,汽车焊接设备通信有心机
  • 面向大语言模型幻觉的关键数据集:系统性综述与分类法_DEEPSEEK
  • Spring Boot整合Redis指南
  • 从电费追缴到碳减排:一个预付费系统如何重塑校园能源生态
  • 使用 Vcpkg 安装 Qt 时的常见问题与解决方法
  • CloudFormation 实现 GitHub Actions OIDC 与 AWS ECR 的安全集成
  • pikachu漏洞练习---File Inclusion(文件包含漏洞)和Unsafe Fileupload(不安全的文件上传)
  • 为什么body{height:100%}会有滚动条?
  • 悦己汉服体验馆小程序(协同过滤算法、WebSocket即时聊天)
  • Solidity学习 - 代理模式中的初始化漏洞
  • Outlook总是提示登录微软,怎么办?
  • 非功能测试
  • 操作系统之文件管理(王道)
  • Linux内核启动:深入理解Initramfs与Initrd机制
  • 深入剖析 CVE-2021-3560 与 CVE-2021-4034:原理、区别与联系
  • 【C/C++】C++26新特性前瞻:全面解析未来编程
  • 【网络】Linux 内核优化实战 - net.ipv4.tcp_rmem 和 net.core.rmem_default 关系
  • 极客时间·AI 数据分析训练营(1期)·毕业总结
  • 免费AI助手工具深度测评:Claude4本地化部署与实战应用指南