主题
第 4 章 · poll 详解
学完本章你将知道:poll 用了什么数据结构干掉了 select 的 1024 限制?为什么它仍然是 O(n)、为什么大型项目还是不爱它?poll 和 select 的真正差异究竟在哪?
1. 一句话开场
**poll 是 1986 年 System V 引入的多路复用调用。**它把 select 的"位图"换成了"结构体数组"——每个 fd 一个
struct pollfd,理论上不再有数量上限,使用方式也更直观。
生活类比:select 是给前台 3 张本子(读 / 写 / 错),上面每个 fd 都用打勾的方式登记。 poll 改成发一摞登记卡——每张卡上写"客户编号是 X、我关心 Y 事件、出事了请在反面写 Z"。 一摞卡可以有任意多张,没了 1024 这个上限。
但前台还是要一张张翻——O(n) 的根本性问题没解决。
2. 函数签名
c
#include <poll.h>
struct pollfd {
int fd; // 要监听的文件描述符
short events; // 输入: 我关心哪些事件(bitmask)
short revents; // 输出: 实际发生了哪些事件(内核填)
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);| 参数 | 含义 |
|---|---|
fds | pollfd 结构体数组首地址 |
nfds | 数组长度(没有 1024 上限) |
timeout | -1=永久阻塞;0=立即返回;正数=毫秒超时 |
返回值:
> 0:返回的就绪 fd 个数0:超时-1:出错
3. events / revents:事件位掩码
3.1 常用事件类型
| 宏 | 含义 |
|---|---|
POLLIN | 普通数据可读(最常用) |
POLLOUT | 可以写数据 |
POLLPRI | 紧急数据(带外数据 OOB) |
POLLRDHUP | 对端关闭连接(Linux 扩展,常用!) |
POLLERR | 输出专用:发生错误(你不需要在 events 里设) |
POLLHUP | 输出专用:对端挂断 |
POLLNVAL | 输出专用:fd 非法 |
关键规则:
events是输入字段(你想关心啥就 OR 上去),revents是输出字段(内核告诉你发生了啥)。POLLERR / POLLHUP / POLLNVAL不需要你显式监听——任何时候发生了内核都会写到 revents。
3.2 典型用法
c
struct pollfd pfd;
pfd.fd = sockfd;
pfd.events = POLLIN | POLLOUT; // 我关心可读 + 可写
pfd.revents = 0;
poll(&pfd, 1, 5000); // 等 5 秒
if (pfd.revents & POLLIN) printf("可读\n");
if (pfd.revents & POLLOUT) printf("可写\n");
if (pfd.revents & POLLERR) printf("出错了\n");
if (pfd.revents & POLLHUP) printf("对端挂断\n");细节:检查时必须用按位与
&,不要直接==——因为 revents 可能同时有多个事件位被置 1。
4. select vs poll:到底差在哪?
4.1 数据结构层面
text
select (1983) poll (1986)
┌────────────────────┐ ┌─────────────────────────────┐
│ fd_set readfds │ │ struct pollfd fds[N] │
│ ┌──┬──┬──┬──┐ │ │ ┌─────────┬─────────┐ │
│ │0 │1 │1 │0 │... │ │ │ fd=3 │ events │ ... │
│ └──┴──┴──┴──┘ │ │ ├─────────┼─────────┤ │
│ ┌──┬──┬──┬──┐ │ │ │ fd=8 │ POLLIN │ │
│ │ 同时另两份 │ │ │ ├─────────┼─────────┤ │
│ │ writefds │ │ │ │ fd=15 │ POLLOUT │ │
│ │ exceptfds │ │ │ └─────────┴─────────┘ │
│ └────────────┘ │ │ N 任意大小 │
│ 固定 1024 bit │ │ │
└────────────────────┘ └─────────────────────────────┘4.2 核心差异对比
| 维度 | select | poll | 谁更好 |
|---|---|---|---|
| fd 数量上限 | 1024(编译期) | 仅受系统限制(ulimit -n) | poll ✅ |
| 数据结构 | 3 个位图 | 1 个 pollfd 数组 | poll ✅ |
| 关心事件 | 拆 3 个表 | 用 events 字段统一 | poll ✅ |
| input/output 分离 | 否(fd_set 被改写) | 是(events / revents 分开) | poll ✅ |
| 跨平台 | 所有 Unix-like 都有 | Linux/BSD/Solaris 有,Windows 没有 | select ✅ |
| 遍历复杂度 | O(n) | O(n) | 平 |
| 拷贝开销 | O(n) | O(n) | 平 |
结论:poll 在易用性上完胜 select,但性能本质没变——它依然是「用户提交全量、内核返回全量」的模型。要想质变还得等 epoll。
5. 标准用法模板
5.1 基础:监听 stdin 5 秒
c
struct pollfd pfd = {.fd = STDIN_FILENO, .events = POLLIN};
int n = poll(&pfd, 1, 5000);
if (n == 0) printf("超时\n");
else if (n > 0 && (pfd.revents & POLLIN)) {
char buf[256];
read(STDIN_FILENO, buf, sizeof(buf));
}5.2 多客户端服务器骨架
c
struct pollfd pfds[MAX_CLIENTS + 1];
int nfds = 1;
pfds[0].fd = listen_fd;
pfds[0].events = POLLIN;
while (1) {
int n = poll(pfds, nfds, -1); // -1 = 永久阻塞
if (n < 0) { perror("poll"); break; }
/* 处理新连接 */
if (pfds[0].revents & POLLIN) {
int conn = accept(listen_fd, NULL, NULL);
pfds[nfds].fd = conn;
pfds[nfds].events = POLLIN;
nfds++;
}
/* 处理已连接客户端 */
for (int i = 1; i < nfds; i++) {
if (pfds[i].revents & POLLIN) {
char buf[1024];
ssize_t r = read(pfds[i].fd, buf, sizeof(buf));
if (r <= 0) {
close(pfds[i].fd);
pfds[i] = pfds[--nfds]; // 紧凑数组:用末尾的填空
} else {
write(pfds[i].fd, buf, r);
}
}
if (pfds[i].revents & (POLLERR | POLLHUP)) {
close(pfds[i].fd);
pfds[i] = pfds[--nfds];
}
}
}三个细节:
- 不需要 master/working 两份——poll 不会改写 events,只写 revents
- 删除 fd 用"末尾填空"技巧——保持数组紧凑,避免空洞
- 每次循环都要把整个 pfds 数组传给内核——这是 poll 的根本性能瓶颈
6. 内核流程
图解说明:和 select 的流程几乎一样——4 次 O(n) + 1 次 O(n) 遍历。这就是为什么 poll 监听 1 万个 fd、只有 1 个就绪时,仍然要做 4 万次操作。
7. poll 真的没限制吗?现实约束
7.1 系统级 fd 上限仍存在
bash
# 进程级(软)
ulimit -n # 默认 1024
# 进程级(硬)
ulimit -Hn # 默认 4096
# 系统全局
cat /proc/sys/fs/file-max**要监听 10 万个 fd?**先
ulimit -n 200000,否则 fd 用尽。
7.2 数组太大本身就是问题
监听 10 万个 fd 时:
- pfds 数组占用 = 100000 × 8 字节 = 800 KB
- 每次 poll 都要把这 800 KB 整个用户↔内核往返拷贝
- 即使只有 1 个 fd 就绪,内核也要遍历 10 万次
结论:poll 解决了「能不能监听这么多」,但没解决「性能够不够好」——这才是 epoll 要解决的问题。
8. 动手实践
请直接看本页底部 [💻 示例代码] 区,包含 3 个示例:
| 文件 | 内容 |
|---|---|
01_simple_poll.c | 入门:等 stdin 5 秒,理解 events/revents |
02_echo_server.c | 实战:poll 版多客户端 echo 服务器 |
03_pollfd_array_pitfalls.c | 进阶:紧凑数组管理 + 演示 POLLHUP 检测 |
实践:测试 poll 服务器
bash
gcc 02_echo_server.c -o poll_srv && ./poll_srv 9999
# 在另一个终端,用 nc 模拟很多并发:
for i in {1..50}; do
(echo "msg$i"; sleep 1) | nc localhost 9999 &
done9. 常见陷阱
🪤 陷阱 1:忘记清零 revents
c
// ❌ 复用 pollfd 时如果 revents 不清零,可能误判
pfd.events = POLLIN;
poll(&pfd, 1, -1);
// pfd.revents 现在被内核写入
poll(&pfd, 1, -1); // 第二次:内核会重新写,但中间业务里别用旧的实际上:内核每次 poll 都会完全覆盖 revents,所以一般不需要手动清零。但代码可读性角度建议显式
pfd.revents = 0;。
🪤 陷阱 2:用 == 检查 revents
c
if (pfd.revents == POLLIN) { ... } // ❌ 错的!
// revents 可能同时有 POLLIN | POLLERR
if (pfd.revents & POLLIN) { ... } // ✅ 对的🪤 陷阱 3:删除 fd 时移动数组导致"漏看"
c
for (int i = 0; i < nfds; i++) {
if (need_close) {
pfds[i] = pfds[--nfds]; // i 位置换成了原末尾
// ❌ 如果 continue 到下一轮 i++ ,就漏看了"原末尾的元素"
}
}正确做法:删除后让 i 不变(不 i++),下次循环重新检查 i 位置:
c
for (int i = 0; i < nfds; ) {
if (need_close) {
pfds[i] = pfds[--nfds];
continue; // 不要 i++
}
i++;
}🪤 陷阱 4:忘了监听 POLLHUP
如果对端关连接了,read 返回 0 + POLLHUP 触发。如果你只查 POLLIN 不查 POLLHUP,可能会一直 epoll/poll 出来这个 fd 但 read 总是 0,造成死循环。
10. 何时该用 poll?
| 场景 | 是否推荐 poll |
|---|---|
| 新写 Linux 高并发服务器 | ❌ 用 epoll |
| 跨平台(macOS / BSD / Linux) | ✅ poll 最稳(kqueue/epoll 都不通用) |
| 监听数 < 100,注重简洁 | ✅ poll API 比 epoll 简单一截 |
| 只监听 stdin / 子进程几个 fd | ✅ poll 一行搞定 |
真实例子:libuv 在 macOS 上用 kqueue,Linux 上用 epoll,但部分边缘场景退回 poll——poll 没死,它是兜底方案。
11. 本章小结
text
poll 的关键事实清单
─────────────────────────────────
✅ 用结构体数组替代位图,去掉 1024 限制
✅ events / revents 分离,不需要 master/working 双份
✅ events 字段表达力比 select 三张表强
❌ 性能仍是 O(n),每次都全量拷贝
❌ Windows 没有 poll
✅ 跨平台 Unix-like 写法首选12. 真实面试题
Q1:poll 比 select 强在哪里?
A:① 没有 1024 上限;② events/revents 分离,不需要每次重置;③ 单一数组比 3 张表更直观。性能本质没变(仍 O(n))——这是面试官最爱追问的点,要主动说出来。
Q2:select 的位图和 poll 的数组哪个更省内存?
A:在监听 fd 数量等于 FD_SETSIZE 时,select 更省(128 字节 vs 至少 8000 字节);但只要 fd 数量稀疏(比如只监听 fd=1000 一个),poll 更省(8 字节 vs 128 字节)。
Q3:poll 的 events 和 revents 为什么要分开?
A:避免数据污染——select 的 fd_set 在调用前后都用同一个变量,内核会改写,导致用户必须保存一份副本;poll 通过分离明确了"用户写 events,内核写 revents",更清晰、更不容易出错。
Q4:POLLERR、POLLHUP、POLLNVAL 需要在 events 里设吗?
A:不需要!这三个是"内核主动通知"的事件——你只管 OR 上 POLLIN/POLLOUT 等,内核任何时候发现错误/挂断/无效 fd 都会写到 revents,必须检查。
Q5:为什么 epoll 比 poll 快得多?
A:① poll 每次都要全量拷贝整个数组,epoll 只在 epoll_ctl 时一次性注册;② poll 内核要 O(n) 遍历找就绪 fd,epoll 用回调机制把就绪 fd 直接放到就绪链表,O(1) 拿;③ poll 唤醒后用户还要再 O(n) 遍历,epoll 直接给你就绪 fd 数组。减少了 3 次 O(n) 操作。
下一站 → 第 5 章:epoll 深度解析,重头戏来了!我们将拆开 epoll 的两大法宝——红黑树 + 就绪链表,看它如何把 O(n) 干到 O(1)。
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
c
/*
* 01_simple_poll.c —— 入门:poll 监听 stdin 5 秒
*
* 编译: gcc 01_simple_poll.c -o simple_poll
* 运行: ./simple_poll
*
* 体会 events 和 revents 的"输入/输出"职责分离。
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
int main(void) {
struct pollfd pfd;
pfd.fd = STDIN_FILENO;
pfd.events = POLLIN; /* 用户写: 我关心可读事件 */
pfd.revents = 0; /* 显式清零(实际不必,内核会覆盖) */
printf("⏰ 5 秒内输入任意文字...\n");
fflush(stdout);
int n = poll(&pfd, 1, 5000); /* 第 3 个参数是 ms */
if (n < 0) {
perror("poll error");
return 1;
} else if (n == 0) {
printf("😴 5 秒到了,啥也没等到\n");
} else {
/* 必须用按位与判断 —— revents 可能同时有多个事件 */
if (pfd.revents & POLLIN) {
char buf[256];
ssize_t r = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (r > 0) {
buf[r] = '\0';
printf("✅ 收到: %s", buf);
}
}
if (pfd.revents & POLLERR) printf("⚠️ POLLERR\n");
if (pfd.revents & POLLHUP) printf("⚠️ POLLHUP\n");
}
return 0;
}c
/*
* 02_echo_server.c —— poll 版多客户端 echo 服务器
*
* 编译: gcc 02_echo_server.c -o poll_srv
* 运行: ./poll_srv 9999
* 测试: nc localhost 9999 (开多个终端体验同时连接)
*
* 与 select 版相比,主要差异:
* 1) 没有 master/working 双份(events 不会被改写)
* 2) 用 pollfd 数组管理,紧凑结构
* 3) 没有 1024 上限
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define MAX_CLIENTS 1024
#define BUF_SIZE 1024
static int make_listen_socket(int port) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); exit(1); }
int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0 ||
listen(fd, 64) < 0) {
perror("bind/listen"); exit(1);
}
printf("🚀 poll 服务器已启动,端口 %d\n", port);
return fd;
}
int main(int argc, char *argv[]) {
int port = (argc > 1) ? atoi(argv[1]) : 9999;
int listen_fd = make_listen_socket(port);
struct pollfd pfds[MAX_CLIENTS + 1];
pfds[0].fd = listen_fd;
pfds[0].events = POLLIN;
int nfds = 1;
while (1) {
int n = poll(pfds, nfds, -1); /* -1 = 永久阻塞 */
if (n < 0) {
if (errno == EINTR) continue;
perror("poll");
break;
}
/* 1) 处理新连接 */
if (pfds[0].revents & POLLIN) {
struct sockaddr_in cli;
socklen_t cli_len = sizeof(cli);
int conn = accept(listen_fd, (struct sockaddr *)&cli, &cli_len);
if (conn >= 0 && nfds < MAX_CLIENTS + 1) {
pfds[nfds].fd = conn;
pfds[nfds].events = POLLIN | POLLRDHUP; /* POLLRDHUP 是 Linux 扩展 */
pfds[nfds].revents = 0;
nfds++;
printf("✅ 新客户端 fd=%d, 来自 %s:%d (当前在线 %d)\n",
conn, inet_ntoa(cli.sin_addr), ntohs(cli.sin_port), nfds - 1);
} else {
printf("⚠️ 连接被拒:accept 失败或槽位满\n");
if (conn >= 0) close(conn);
}
}
/* 2) 处理已连接客户端(注意删除时不要 i++) */
for (int i = 1; i < nfds; ) {
short re = pfds[i].revents;
if (re == 0) { i++; continue; }
int closed = 0;
if (re & POLLIN) {
char buf[BUF_SIZE];
ssize_t r = read(pfds[i].fd, buf, sizeof(buf));
if (r <= 0) {
closed = 1;
if (r == 0) printf("👋 fd=%d 正常断开\n", pfds[i].fd);
else perror("read");
} else {
write(pfds[i].fd, buf, r);
printf("📨 fd=%d 收到 %ld 字节,已 echo\n", pfds[i].fd, r);
}
}
if (re & (POLLERR | POLLHUP | POLLRDHUP | POLLNVAL)) {
printf("⚠️ fd=%d 触发 ERR/HUP\n", pfds[i].fd);
closed = 1;
}
if (closed) {
close(pfds[i].fd);
pfds[i] = pfds[--nfds]; /* 末尾填空,紧凑数组 */
/* 不 i++,下一轮重新检查 i 位置 */
} else {
i++;
}
}
}
close(listen_fd);
return 0;
}c
/*
* 03_pollfd_array_pitfalls.c —— 紧凑数组管理与 POLLHUP 检测陷阱
*
* 编译: gcc 03_pollfd_array_pitfalls.c -o pitfalls
* 运行: ./pitfalls 9999
* 测试: nc localhost 9999 → 立刻按 Ctrl+] / Ctrl+D 断开
*
* 这个示例专门展示三个常见坑:
* ① events != revents → 用按位与而非 ==
* ② 删除元素用末尾填空,且循环不要 i++
* ③ 对端关闭时 POLLIN 也会触发,read 返回 0 才知道断开
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#define MAX_FDS 1024
static const char *flag_str(short e) {
static char buf[128];
buf[0] = '\0';
if (e & POLLIN) strcat(buf, "POLLIN ");
if (e & POLLOUT) strcat(buf, "POLLOUT ");
if (e & POLLERR) strcat(buf, "POLLERR ");
if (e & POLLHUP) strcat(buf, "POLLHUP ");
if (e & POLLNVAL) strcat(buf, "POLLNVAL ");
if (e & POLLRDHUP) strcat(buf, "POLLRDHUP ");
if (buf[0] == '\0') strcat(buf, "(none)");
return buf;
}
int main(int argc, char *argv[]) {
int port = (argc > 1) ? atoi(argv[1]) : 9999;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
int yes = 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(lfd, (struct sockaddr*)&addr, sizeof(addr)) < 0 || listen(lfd, 8) < 0) {
perror("bind/listen"); exit(1);
}
printf("🚀 端口 %d,连上来后立刻断开试试\n", port);
struct pollfd pfds[MAX_FDS];
pfds[0].fd = lfd; pfds[0].events = POLLIN; pfds[0].revents = 0;
int n = 1;
while (1) {
int r = poll(pfds, n, -1);
if (r < 0 && errno != EINTR) { perror("poll"); break; }
if (pfds[0].revents & POLLIN) {
int c = accept(lfd, NULL, NULL);
pfds[n].fd = c;
pfds[n].events = POLLIN | POLLRDHUP; /* 加上 POLLRDHUP */
pfds[n].revents = 0;
n++;
printf("➕ accept fd=%d, 当前 n=%d\n", c, n);
}
for (int i = 1; i < n; ) {
short re = pfds[i].revents;
if (re == 0) { i++; continue; }
printf("🔔 fd=%d 触发: %s\n", pfds[i].fd, flag_str(re));
int dead = 0;
/* ⚠️ 即使对端先关,POLLIN 通常也会同时被设置 */
if (re & POLLIN) {
char buf[256];
ssize_t got = read(pfds[i].fd, buf, sizeof(buf));
if (got == 0) {
printf(" read=0, 对端正常关闭 → 必须 close 否则就泄漏\n");
dead = 1;
} else if (got < 0) {
perror(" read");
dead = 1;
} else {
printf(" 收到 %ld 字节: '%.*s'\n", got, (int)got, buf);
}
}
if (re & (POLLHUP | POLLERR | POLLNVAL | POLLRDHUP)) dead = 1;
if (dead) {
close(pfds[i].fd);
pfds[i] = pfds[--n];
printf(" 🗑 fd 已关,数组紧凑后 n=%d\n", n);
/* 关键:不 i++,下次循环重新检查 i 位置(原末尾元素移到这里了) */
} else {
i++;
}
}
}
return 0;
}01_simple_poll.c ↗ · 02_echo_server.c ↗ · 03_pollfd_array_pitfalls.c ↗