I/O多路复用:poll机制
前文介绍了基于 select 的 I/O 多路复用技术。select 通过描述符集合(fd_set)表示待检测的 I/O 对象,通过三个独立的集合分别描述可读、可写和异常事件。然而 select 存在一个显著的缺陷:所支持的文件描述符数量受 FD_SETSIZE 限制,在 Linux 系统中默认最大值为 1024。
poll 函数是另一种广泛使用的 I/O 多路复用技术,通过与 select 不同的数据结构设计,突破了文件描述符的数量限制。
poll 函数接口
函数原型
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
/* 返回值:就绪描述符数目;超时返回0;出错返回-1 */参数说明
fds:pollfd 结构体数组,每个元素描述一个待检测的文件描述符及其感兴趣的事件。
nfds:数组 fds 的大小,即待检测的描述符个数。
timeout:超时时间,单位为毫秒。
| timeout 值 | 行为 |
|---|---|
< 0 | 永久等待,直到有事件发生 |
0 | 立即返回,不阻塞 |
> 0 | 等待指定毫秒数后返回 |
pollfd 结构体
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 待检测的事件(输入) */
short revents; /* 返回的事件(输出) */
};与 select 的关键区别在于:poll 将输入(events)和输出(revents)分离到不同字段,检测完成后内核仅修改 revents,不影响原始的 events 设置。因此,应用程序无需在每次调用前重置事件集合。
此外,若将 fd 成员设置为负值,poll 函数将忽略该 pollfd 条目,且对应的 revents 置为 0。这提供了一种简洁的方式来临时屏蔽某个描述符的检测,而无需从数组中移除。
poll 工作流程
事件类型
可读事件类
#define POLLIN 0x0001 /* 有可读数据 */
#define POLLPRI 0x0002 /* 带外/紧急数据可读 */
#define POLLRDNORM 0x0040 /* 普通数据可读 */
#define POLLRDBAND 0x0080 /* 优先级带数据可读 */实际编程中通常使用 POLLIN 即可。套接字可读事件的语义与 select 的 readset 一致:内核通知应用程序有数据可读,read 操作不会阻塞。
可写事件类
#define POLLOUT 0x0004 /* 描述符可写 */
#define POLLWRNORM POLLOUT /* 无写类型区分 */
#define POLLWRBAND 0x0100 /* 优先级带数据可写 */实际编程中通常使用 POLLOUT。套接字可写事件的语义与 select 的 writeset 一致:内核通知发送缓冲区已就绪,write 操作不会阻塞。
错误事件类(仅通过 revents 返回)
#define POLLERR 0x0008 /* 错误发生 */
#define POLLHUP 0x0010 /* 描述符挂起 */
#define POLLNVAL 0x0020 /* 描述符无效 */此类事件无法通过 events 字段请求检测,仅由内核通过 revents 返回。
poll 与 select 对比
| 对比维度 | select | poll |
|---|---|---|
| 数据结构 | fd_set 位图 | pollfd 结构体数组 |
| 描述符上限 | FD_SETSIZE(通常 1024) | 受系统资源限制(ulimit -n) |
| 事件分离 | 输入输出共用同一集合 | events/revents 分离 |
| 重置需求 | 每次调用后必须重置 | 无需重置 |
| 屏蔽描述符 | 不支持 | fd = -1 即可屏蔽 |
| 跨平台 | POSIX 兼容 | POSIX 兼容 |
基于 poll 的服务器程序
以下实现一个基于 poll 的回显(echo)服务器,支持同时处理多个客户端连接:
#define INIT_SIZE 128
int main(int argc, char **argv) {
int listen_fd, connected_fd;
int ready_number;
ssize_t n;
char buf[MAXLINE];
struct sockaddr_in client_addr;
listen_fd = tcp_server_listen(SERV_PORT);
/* 初始化 pollfd 数组,首元素为监听套接字 */
struct pollfd event_set[INIT_SIZE];
event_set[0].fd = listen_fd;
event_set[0].events = POLLRDNORM;
/* 未使用的槽位 fd 设为 -1,poll 将自动忽略 */
int i;
for (i = 1; i < INIT_SIZE; i++) {
event_set[i].fd = -1;
}
for (;;) {
if ((ready_number = poll(event_set, INIT_SIZE, -1)) < 0) {
error(1, errno, "poll failed ");
}
/* 监听套接字事件处理 */
if (event_set[0].revents & POLLRDNORM) {
socklen_t client_len = sizeof(client_addr);
connected_fd = accept(listen_fd, (struct sockaddr *) &client_addr, &client_len);
/* 在数组中寻找空闲槽位 */
for (i = 1; i < INIT_SIZE; i++) {
if (event_set[i].fd < 0) {
event_set[i].fd = connected_fd;
event_set[i].events = POLLRDNORM;
break;
}
}
if (i == INIT_SIZE) {
error(1, errno, "can not hold so many clients");
}
if (--ready_number <= 0)
continue;
}
/* 已连接套接字事件处理 */
for (i = 1; i < INIT_SIZE; i++) {
int socket_fd;
if ((socket_fd = event_set[i].fd) < 0)
continue;
if (event_set[i].revents & (POLLRDNORM | POLLERR)) {
if ((n = read(socket_fd, buf, MAXLINE)) > 0) {
if (write(socket_fd, buf, n) < 0) {
error(1, errno, "write error");
}
} else if (n == 0 || errno == ECONNRESET) {
close(socket_fd);
event_set[i].fd = -1;
} else {
error(1, errno, "read error");
}
if (--ready_number <= 0)
break;
}
}
}
}代码解析
1. 初始化 pollfd 数组
第 13-15 行将监听套接字 listen_fd 及其 POLLRDNORM 事件加入数组首元素,表示需要检测监听套接字上的连接建立事件。
第 19-21 行将数组中其余槽位的 fd 设为 -1,poll 函数会自动忽略这些条目。
2. 事件检测
第 24 行调用 poll 进行事件检测。传入 INIT_SIZE 作为数组大小——poll 能自动跳过 fd = -1 的条目,无需计算有效元素个数。timeout 设为 -1 表示永久等待。
3. 监听套接字事件处理
第 28 行通过位与操作检查 revents 是否包含 POLLRDNORM 事件。事件类型通过二进制掩码位表示,一个文件描述符可同时对应多种事件,因此必须使用位与而非等值比较。
第 33-38 行在数组中查找空闲槽位(fd = -1),将新连接套接字注册到数组中。
4. 已连接套接字事件处理
第 45-63 行遍历数组,处理已连接套接字上的可读事件。读取数据后回显给客户端;若读到 EOF 或连接重置,则关闭连接并将槽位的 fd 重置为 -1。
5. 提前退出优化
第 41 行和第 63 行利用 ready_number 计数器:当本次 poll 返回的所有事件已处理完毕,直接跳过后续遍历,重新进入 poll 调用。
实验
启动服务器程序,通过多个 telnet 客户端连接验证:
客户端 1:
$ telnet 127.0.0.1 43211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
a
a
aaaaaaaaaaa
aaaaaaaaaaa
^]
telnet> quit
Connection closed.客户端 2:
$ telnet 127.0.0.1 43211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
b
b
bbbbbbb
bbbbbbb
^]
telnet> quit
Connection closed.多个客户端互不影响,每个客户端输入的字符被即时回显。一个客户端断开连接不影响其他客户端。
总结
poll 是 POSIX 标准定义的 I/O 多路复用技术,与 select 相比具有以下优势:
- 突破了
FD_SETSIZE的文件描述符数量限制,数组大小由应用程序自行决定 events/revents分离设计,无需每次调用前重置事件集合- 支持
fd = -1临时屏蔽描述符,操作更加灵活
然而,poll 仍然存在与 select 相似的性能瓶颈:每次调用都需要将整个 pollfd 数组从用户态拷贝到内核态,返回时再拷贝回用户态;内核仍需线性扫描所有注册的描述符,时间复杂度为 O(n)。在大规模并发场景下,epoll 是更优的选择。
思考题
-
本程序中
event_set数组大小固定为INIT_SIZE,在生产环境中如何改进以支持动态扩展? -
若实现了动态扩展的
pollfd数组,新连接描述符加入数组的逻辑应如何配合调整?
版本信息
| 项目 | 说明 |
|---|---|
| 更新日期 | 2026-06-09 |
| 目标内核 | Linux 7.0 |
| 关键接口 | poll() — POSIX.1-2001, POSIX.1-2008 |
| 描述符上限 | 受 RLIMIT_NOFILE 限制,可通过 ulimit -n 调整 |