深入解析poll函数:高效I/O多路复用技术
本文详细介绍了poll函数作为I/O多路复用机制的实现原理和使用方法。与select相比,poll通过动态数组替代固定位图,解决了1024个描述符的限制问题,并采用events/revents分离的设计避免了每次重建集合的开销。文章从函数原型、数据结构、事件类型到完整服务器实现,系统性地讲解了poll的使用流程,包括监听套接字处理、客户端连接管理和异常检测。同时对比分析了poll与select的核
引言
在上一篇文章中,我们详细讲解了 select 函数的使用。select 作为最基础的 I/O 多路复用机制,虽然简单易用,但存在两个明显的局限性:
-
文件描述符数量限制:默认最多只能监控 1024 个描述符
-
每次调用需要重新构建集合:
fd_set在select返回时会被修改,导致每次循环都需要重建
为了解决 select 的第一个限制(描述符数量上限),POSIX 标准引入了 poll 函数。poll 使用动态数组代替 fd_set 的固定位图,理论上可以监控任意数量的文件描述符。

第一部分:poll 函数基础
一、函数原型
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
二、核心数据结构:struct pollfd
poll 的核心是 struct pollfd 结构体,每个被监控的文件描述符对应一个结构体:
struct pollfd {
int fd; // 要监控的文件描述符(-1 表示忽略此项)
short events; // 请求监控的事件(输入参数,由用户设置)
short revents; // 实际发生的事件(输出参数,由内核设置)
};
重要特性:events 和 revents 是分离的!
-
events由用户设置,不会被poll修改 -
revents由内核设置,poll返回时填充 -
这意味着不需要每次调用前重建事件,这是比
select更优秀的设计
三、参数详解
| 参数 | 类型 | 作用 |
|---|---|---|
| fds | struct pollfd* | 待监控的文件描述符数组 |
| nfds | nfds_t | 数组中的有效元素个数 |
| timeout | int | 超时时间(毫秒) |
四、返回值
| 返回值 | 含义 |
|---|---|
| >0 | 就绪的文件描述符数量 |
| =0 | 超时(没有就绪的描述符) |
| =-1 | 调用失败(可通过 errno 获取错误码) |
五、timeout 参数的特殊值
| 值 | 含义 |
|---|---|
| -1 | 永久阻塞,直到有描述符就绪 |
| 0 | 非阻塞轮询,立即返回 |
| >0 | 等待指定毫秒数后超时 |
六、与 select 超时设置的对比
// select 使用 timeval 结构体(秒 + 微秒)
struct timeval tv;
tv.tv_sec = 5; // 5秒
tv.tv_usec = 500000; // 500000微秒 = 0.5秒
// 总超时:5.5秒
// poll 使用整数(毫秒)
int timeout = 5500; // 5500毫秒 = 5.5秒
poll 的超时设置更加简洁,直接用毫秒整数表示,避免了 select 需要设置两个字段的麻烦。
第二部分:事件类型详解
一、常用事件类型
poll 定义了一系列事件标志,通过位或(|)组合使用:
| 事件常量 | 含义 | 可用于 events | 可用于 revents |
|---|---|---|---|
| POLLIN | 数据可读(包括普通数据和优先数据) | ✅ | ✅ |
| POLLPRI | 有紧急数据可读(TCP 带外数据) | ✅ | ✅ |
| POLLOUT | 写操作不会阻塞 | ✅ | ✅ |
| POLLERR | 发生错误 | ❌ | ✅ |
| POLLHUP | 连接挂起(对方关闭连接) | ❌ | ✅ |
| POLLNVAL | 无效请求(描述符未打开) | ❌ | ✅ |
| POLLRDNORM | 普通数据可读 | ✅ | ✅ |
| POLLRDBAND | 优先带数据可读 | ✅ | ✅ |
| POLLWRNORM | 普通数据可写 | ✅ | ✅ |
| POLLWRBAND | 优先带数据可写 | ✅ | ✅ |
二、事件分类说明

三、关键理解:POLLERR、POLLHUP、POLLNVAL
这三个事件只能出现在 revents 中,不需要(也不能)在 events 中设置。内核会自动检测这些异常状态:
// 检查异常事件的惯用写法
if (fds[i].revents & POLLERR) {
printf("描述符 %d 发生错误\n", fds[i].fd);
}
if (fds[i].revents & POLLHUP) {
printf("描述符 %d 对方关闭连接\n", fds[i].fd);
}
if (fds[i].revents & POLLNVAL) {
printf("描述符 %d 无效\n", fds[i].fd);
}
第三部分:poll 的基本使用
一、监控标准输入
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
int main() {
struct pollfd fds[1];
char buffer[128];
// 初始化 pollfd 结构体
fds[0].fd = STDIN_FILENO; // 监控标准输入
fds[0].events = POLLIN; // 监控读事件
while (1) {
printf("等待输入(5秒超时)...\n");
// 调用 poll,超时 5 秒(5000 毫秒)
int ret = poll(fds, 1, 5000);
if (ret == -1) {
perror("poll error");
break;
} else if (ret == 0) {
printf("5秒内没有输入,超时!\n\n");
} else {
// 检查标准输入是否有数据可读
if (fds[0].revents & POLLIN) {
int n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("输入内容: %s\n", buffer);
}
}
// 检查异常事件
if (fds[0].revents & POLLERR) {
printf("标准输入发生错误\n");
break;
}
if (fds[0].revents & POLLHUP) {
printf("标准输入已关闭\n");
break;
}
}
}
return 0;
}
二、与 select 实现对比

核心优势:poll 的 events 字段保留用户设置,revents 字段由内核填充。因此不需要每次调用前重新初始化,只需要在事件处理完成后清零 revents 即可。
第四部分:poll 实现多客户端服务器
一、数据结构设计
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#define PORT 6000
#define MAX_CLIENTS 1024
#define BUFFER_SIZE 128
二、创建监听套接字
int create_listen_socket() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket error");
return -1;
}
// 端口复用
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind error");
close(listen_fd);
return -1;
}
if (listen(listen_fd, 5) == -1) {
perror("listen error");
close(listen_fd);
return -1;
}
printf("服务器启动成功,端口:%d\n", PORT);
return listen_fd;
}
三、pollfd 数组管理
struct pollfd fds[MAX_CLIENTS + 1]; // +1 用于监听套接字
int nfds = 0; // 当前有效元素个数
// 添加描述符到监控数组
void add_fd(int fd) {
if (nfds >= MAX_CLIENTS + 1) {
printf("已达到最大连接数\n");
return;
}
fds[nfds].fd = fd;
fds[nfds].events = POLLIN; // 监控读事件
fds[nfds].revents = 0;
nfds++;
}
// 从监控数组中移除描述符
void remove_fd(int index) {
// 用最后一个元素覆盖要删除的元素(保持数组连续性)
fds[index] = fds[nfds - 1];
nfds--;
}
删除策略说明:

四、事件处理函数
void handle_events(int listen_fd) {
// 1. 检查监听套接字(新连接)
if (fds[0].revents & POLLIN) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(listen_fd,
(struct sockaddr*)&client_addr, &len);
if (client_fd != -1) {
printf("新客户端连接,fd=%d, IP=%s\n",
client_fd, inet_ntoa(client_addr.sin_addr));
add_fd(client_fd);
}
}
// 2. 检查所有客户端连接(从索引1开始,索引0是监听套接字)
for (int i = 1; i < nfds; i++) {
int fd = fds[i].fd;
// 检查读事件
if (fds[i].revents & POLLIN) {
char buffer[BUFFER_SIZE];
int n = recv(fd, buffer, BUFFER_SIZE - 1, 0);
if (n <= 0) {
// 客户端关闭或出错
printf("客户端 %d 断开连接\n", fd);
close(fd);
remove_fd(i);
i--; // 调整索引,因为数组元素被替换了
} else {
buffer[n] = '\0';
printf("收到数据 (fd=%d): %s\n", fd, buffer);
send(fd, "OK", 2, 0);
}
}
// 检查异常事件
if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
printf("客户端 %d 异常断开\n", fd);
close(fd);
remove_fd(i);
i--;
}
}
}
五、主循环
int main() {
// 创建监听套接字
int listen_fd = create_listen_socket();
if (listen_fd == -1) {
exit(1);
}
// 将监听套接字加入监控数组(始终在索引0)
add_fd(listen_fd);
while (1) {
// 调用 poll,超时 2 秒
int ret = poll(fds, nfds, 2000);
if (ret == -1) {
perror("poll error");
break;
} else if (ret == 0) {
// 超时,可执行定时任务
printf("poll timeout, 当前连接数: %d\n", nfds - 1);
continue;
}
// 处理就绪事件
handle_events(listen_fd);
}
close(listen_fd);
return 0;
}
六、关键设计说明
1. 监听套接字固定放在 fds[0]

2. 监听套接字的 POLLIN 含义
对于监听套接字,POLLIN 表示有新连接到达(accept 不会阻塞):
if (fds[0].revents & POLLIN) {
int client_fd = accept(listen_fd, NULL, NULL);
// 不会阻塞,因为一定有新连接
}
第五部分:poll 的注意事项
一、fd 可以设为 -1 来忽略
与 select 不同,poll 允许将 fd 设为 -1 来跳过某个数组元素:
fds[2].fd = -1; // poll 会忽略此项
这提供了另一种"删除"策略——不删除数组元素,只是标记为无效。但会浪费数组空间。
二、POLLHUP 和 POLLIN 可能同时出现
当对方关闭连接时,poll 可能同时返回 POLLHUP 和 POLLIN:
if (fds[i].revents & POLLIN) {
int n = recv(fd, buffer, size, 0);
if (n == 0) {
// 对方关闭连接,recv 返回 0
}
}
// 同时检查 POLLHUP 作为额外保障
if (fds[i].revents & POLLHUP) {
// 确认对方已关闭
}
推荐做法:优先处理 POLLIN,通过 recv 返回值判断连接状态;将 POLLHUP 作为异常情况的补充检查。
三、缓冲区截断问题
// 如果接收缓冲区太小,一次只能读取部分数据
char buffer[1]; // 只能读1个字节
recv(fd, buffer, 1, 0);
// 剩余数据还在接收缓冲区中,下次 poll 仍会报告 POLLIN
当客户端发送 "hello"(5个字节),而服务器缓冲区只有1字节时,需要多次 poll 才能读完所有数据。
第六部分:select 与 poll 对比
一、核心对比表
| 对比项 | select | poll |
|---|---|---|
| 描述符集合 | fd_set(固定位图) | struct pollfd 数组(动态) |
| 最大描述符数 | FD_SETSIZE(默认1024) | 无内置限制(受系统资源限制) |
| 事件与结果 | 共用同一集合(被修改) | 分离(events/revents) |
| 每次调用前重建 | 需要 | 不需要(events 不变) |
| 超时精度 | 微秒 | 毫秒 |
| 参数含义 | nfds=最大值+1 | nfds=数组元素个数 |
| 可移植性 | 几乎所有平台 | POSIX 系统 |
| 空描述符处理 | 不支持 | fd=-1 跳过该项 |
| 删除元素 | FD_CLR | 多种方式(替换、fd=-1) |
| 性能(少量描述符) | 相当 | 相当 |
| 性能(大量描述符) | 较差(需遍历所有位) | 较差(需遍历整个数组) |
二、poll 的优势
1. 无描述符数量限制:
// select:受 FD_SETSIZE 限制
fd_set readfds; // 最多 1024 个
// poll:动态数组,无固定上限
struct pollfd *fds = malloc(sizeof(struct pollfd) * 100000);
2. 事件与结果分离:
// select:每次都需要重建
while (1) {
FD_ZERO(&readfds); // 必须重建
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
select(...);
// readfds 已被修改
}
// poll:只需设置一次 events
fds[0].events = POLLIN; // 设置一次
fds[1].events = POLLIN;
while (1) {
poll(fds, 2, -1);
// 检查 revents,events 保持不变
}
3. 更简洁的超时设置:
// select:需要分离的结构体
struct timeval tv = {5, 0}; // 5秒
// poll:直接毫秒数
int timeout = 5000; // 5000毫秒
三、poll 的不足
1. 仍然是线性扫描:每次调用 poll,内核需要遍历整个 fds 数组,复杂度 O(n)
2. 大量描述符时内存复制开销大:需要在用户态和内核态之间复制整个 fds 数组
3. 毫秒级精度:相比 select 的微秒精度,poll 只能精确到毫秒(实际应用中通常够用)
4. 不可知具体哪个描述符就绪:与 select 一样,需要遍历检查

第七部分:完整测试代码
服务器端(poll_server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#define PORT 6000
#define MAX_CLIENTS 1024
#define BUFFER_SIZE 128
struct pollfd fds[MAX_CLIENTS + 1];
int nfds = 0;
void add_fd(int fd) {
fds[nfds].fd = fd;
fds[nfds].events = POLLIN;
fds[nfds].revents = 0;
nfds++;
}
void remove_fd(int index) {
fds[index] = fds[nfds - 1];
nfds--;
}
int create_listen_socket() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket error");
return -1;
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind error");
close(listen_fd);
return -1;
}
if (listen(listen_fd, 5) == -1) {
perror("listen error");
close(listen_fd);
return -1;
}
printf("poll 服务器启动,端口:%d\n", PORT);
return listen_fd;
}
int main() {
int listen_fd = create_listen_socket();
if (listen_fd == -1) exit(1);
// 监听套接字在 fds[0]
add_fd(listen_fd);
while (1) {
int ret = poll(fds, nfds, 2000);
if (ret == -1) {
perror("poll error");
break;
}
if (ret == 0) {
printf("超时,当前连接数: %d\n", nfds - 1);
continue;
}
// 处理监听套接字
if (fds[0].revents & POLLIN) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(listen_fd,
(struct sockaddr*)&client_addr, &len);
if (client_fd != -1) {
printf("新连接: fd=%d\n", client_fd);
add_fd(client_fd);
}
}
// 处理客户端连接
for (int i = 1; i < nfds; i++) {
if (fds[i].revents & POLLIN) {
char buffer[BUFFER_SIZE];
int n = recv(fds[i].fd, buffer, BUFFER_SIZE - 1, 0);
if (n <= 0) {
printf("断开: fd=%d\n", fds[i].fd);
close(fds[i].fd);
remove_fd(i);
i--;
} else {
buffer[n] = '\0';
printf("收到 fd=%d: %s\n", fds[i].fd, buffer);
send(fds[i].fd, "OK", 2, 0);
}
}
if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
printf("异常断开: fd=%d\n", fds[i].fd);
close(fds[i].fd);
remove_fd(i);
i--;
}
}
}
close(listen_fd);
return 0;
}
测试
# 编译服务器
gcc poll_server.c -o poll_server
./poll_server# 另开终端,使用 telnet 或 nc 测试
nc 127.0.0.1 6000
# 输入任意内容,应该收到 "OK" 回复
总结
一、poll 使用流程

二、核心函数速查表
| 函数/宏 | 作用 |
|---|---|
poll() |
监控多个文件描述符的状态 |
POLLIN |
数据可读事件 |
POLLOUT |
数据可写事件 |
POLLERR |
错误事件(仅 revents) |
POLLHUP |
连接挂起(仅 revents) |
POLLNVAL |
无效描述符(仅 revents) |
三、select vs poll 选择建议
| 场景 | 推荐 |
|---|---|
| 描述符数量 < 1024 | 两者均可,select 更跨平台 |
| 描述符数量可能超过 1024 | 使用 poll |
| 需要 Windows 兼容 | 使用 select |
| 需要更简洁的代码 | 使用 poll(无需重建事件) |
| 需要微秒精度超时 | 使用 select |
| 追求极致性能(海量连接) | 使用 epoll(下一篇文章) |
四、poll 的优缺点总结
| 优点 | 缺点 |
|---|---|
| ✅ 无描述符数量限制 | ❌ 仍需线性遍历所有描述符 |
| ✅ events/revents 分离,无需重建 | ❌ 大量描述符时内存复制开销大 |
| ✅ 超时设置简洁(毫秒整数) | ❌ 不可知具体就绪的描述符 |
| ✅ fd=-1 可跳过无效项 | ❌ 精确度到毫秒(select 是微秒) |
poll 解决了 select 的描述符数量限制问题,并提供了更优雅的事件管理方式(events 与 revents 分离)。但它仍然需要线性遍历所有描述符,在处理海量连接时效率不够理想。
在下一篇文章中,我们将学习 Linux 下的终极 I/O 多路复用方案 —— epoll,它通过事件驱动和红黑树实现了 O(1) 的性能,是构建高性能服务器的核心技术。
学习建议:
-
动手实现
poll版本的服务器,加深理解 -
对比
select和poll的代码差异,理解各自的设计思想 -
注意
POLLHUP和POLLIN同时出现的情况,正确处理连接关闭 -
重点理解"替换法删除"技巧,这是保证 O(1) 删除的关键
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)