Skip to content

第 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);
参数含义
fdspollfd 结构体数组首地址
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 核心差异对比

维度selectpoll谁更好
fd 数量上限1024(编译期)仅受系统限制(ulimit -npoll ✅
数据结构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];
        }
    }
}

三个细节

  1. 不需要 master/working 两份——poll 不会改写 events,只写 revents
  2. 删除 fd 用"末尾填空"技巧——保持数组紧凑,避免空洞
  3. 每次循环都要把整个 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 &
done

9. 常见陷阱

🪤 陷阱 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 ↗