一、为什么需要 poll?

1.1 select 的局限

在之前的教程中,我们学习了 select 模型,它存在几个难以忍受的缺点:

  • 文件描述符上限FD_SETSIZE 通常为 1024,难以支持大量连接。
  • 效率低下:每次调用都要将整个 fd_set 从用户态拷贝到内核态,且内核需要遍历所有 fd。
  • 集合被修改select 会修改传入的 fd_set,每次都要重新设置。

1.2 poll 的改进

poll 是 UNIX 系统(包括 Linux)提供的另一种 I/O 复用机制,它解决了 select 的部分问题:

  • 无描述符上限:使用数组管理 fd,理论上受系统内存限制。
  • 数据结构更清晰:使用 pollfd 结构体数组,分别存储事件和返回事件,不需要每次重新构造。
  • 效率与 select 相当:仍然需要遍历所有 fd,但避免了 fd_set 大小限制。

注意poll 并没有解决“O(n) 遍历”的问题,它仍然需要应用程序遍历所有 fd 来找出就绪的。在大量连接但只有少数活跃时,epoll 才是更优的选择。但 pollselect 更现代,且跨平台(Linux、BSD、macOS 等都支持)。


二、poll 的核心数据结构与函数

2.1 struct pollfd

#include <poll.h>

struct pollfd {
    int   fd;         // 要监视的文件描述符
    short events;     // 感兴趣的事件(位掩码)
    short revents;    // 实际发生的事件(由内核填充)
};
  • fd:要监视的套接字或文件描述符。
  • events:应用程序关心的事件(可读、可写、异常等)。
  • revents:内核返回时填充,表示真正发生的事件。

常用事件标志

含义
POLLIN 有数据可读(包括连接请求、对方关闭)
POLLOUT 可写(输出缓冲区空闲)
POLLRDHUP 对方关闭连接或半关闭(Linux 2.6.17 开始支持)
POLLERR 发生错误(revents 中返回)
POLLHUP 挂起(如管道写端关闭)
POLLNVAL 无效的 fd(未打开)

注意poll 没有 select 中异常集合的单独标志,而是通过 POLLERR 等返回。

2.2 poll 函数原型

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:指向 pollfd 结构体数组的指针。
  • nfds:数组中元素的个数(即要监视的 fd 数量)。
  • timeout:超时时间(毫秒)。
    • -1:无限等待。
    • 0:立即返回(非阻塞)。
    • >0:等待指定毫秒数。
  • 返回值
    • >0:就绪的文件描述符数量。
    • 0:超时。
    • -1:出错。

三、poll 与 select 的对比

特性 select poll
描述符上限 FD_SETSIZE(通常 1024) 无(受内存限制)
数据结构 fd_set 位图 pollfd 数组
每次调用拷贝 拷贝整个 fd_set 拷贝整个 pollfd 数组
修改集合 会修改,需要重新设置 events 不变,revents 单独返回
事件检查 遍历 fd_set 所有位 遍历 pollfd 数组
触发模式 水平触发 水平触发
跨平台 几乎所有平台 UNIX 类系统,Windows 也有(但非原生)

四、实战:poll 回声服务器

下面实现一个使用 poll 的单进程回声服务器,支持多个客户端。

4.1 服务器代码(逐行注释)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <poll.h>

#define BUF_SIZE 100
#define MAX_CLNT 256   // 最大客户端数量(可调)

void error_handling(char *msg);

int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    socklen_t adr_sz;
    int str_len, i;
    char buf[BUF_SIZE];

    // poll 相关变量
    struct pollfd fds[MAX_CLNT];   // pollfd 数组,存储所有监视的 fd
    int nfds;                      // 当前数组中有效元素个数
    int fd_num;                    // poll 返回的就绪 fd 数量

    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }

    // ========== 1. 创建 TCP 套接字 ==========
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    // ========== 2. 初始化 pollfd 数组 ==========
    // 第一个元素放监听套接字
    fds[0].fd = serv_sock;
    fds[0].events = POLLIN;   // 关心可读事件(连接请求)
    nfds = 1;                 // 目前只有 1 个有效元素

    // ========== 3. 事件循环 ==========
    while (1) {
        // 调用 poll,无限等待
        fd_num = poll(fds, nfds, -1);
        if (fd_num == -1) {
            error_handling("poll() error");
        }

        // 检查监听套接字是否有事件
        if (fds[0].revents & POLLIN) {
            // 有新连接
            adr_sz = sizeof(clnt_adr);
            clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
            if (clnt_sock == -1)
                error_handling("accept() error");

            // 将新客户端加入 pollfd 数组
            if (nfds < MAX_CLNT) {
                fds[nfds].fd = clnt_sock;
                fds[nfds].events = POLLIN;   // 关心数据到达
                nfds++;
                printf("connected client: %d (total: %d)\n", clnt_sock, nfds-1);
            } else {
                // 客户端数量已达上限,拒绝连接
                puts("Too many clients, connection refused");
                close(clnt_sock);
            }
        }

        // 遍历其他 fd(客户端套接字)
        for (i = 1; i < nfds; i++) {
            if (fds[i].revents & POLLIN) {
                // 有数据到达或连接关闭
                str_len = read(fds[i].fd, buf, BUF_SIZE);
                if (str_len == 0) {   // 客户端正常关闭
                    close(fds[i].fd);
                    // 从数组中移除该元素(后面的往前移动)
                    for (int j = i; j < nfds - 1; j++)
                        fds[j] = fds[j + 1];
                    nfds--;
                    i--;   // 因为数组向前移动,需要继续检查当前位置
                    printf("closed client\n");
                } else {
                    // 回声:将数据原样写回
                    write(fds[i].fd, buf, str_len);
                }
            }
        }
    }

    close(serv_sock);
    return 0;
}

void error_handling(char *msg)
{
    fputs(msg, stderr);
    fputc('\n', stderr);
    exit(1);
}

4.2 代码关键点解析

关键点 解释
fds[0] 专门用于监听套接字,始终存在。
fds[0].revents & POLLIN 判断监听套接字是否有连接请求。
nfds 当前 pollfd 数组中有效元素的个数,每次添加或删除时更新。
数组元素移除 当客户端关闭时,需要将后面的元素向前移动,并减小 nfds,同时调整循环索引 i
回声逻辑 对普通客户端,读到数据后直接写回(回声)。

4.3 客户端代码

与 select 和 epoll 教程中的客户端完全相同,此处不再重复。


五、poll 的优缺点总结

5.1 优点

  • 无描述符上限:只受系统内存限制,可通过 ulimit -n 调整。
  • 数据结构清晰eventsrevents 分离,不需要每次重新构造。
  • 跨平台:几乎所有 UNIX 系统(包括 Linux、BSD、macOS)都支持,Windows 也有 WSAPoll(功能类似)。
  • 编程简单:比 select 稍好,比 epoll 简单。

5.2 缺点

  • O(n) 遍历:每次 poll 返回后,应用程序需要遍历整个数组来找出就绪的 fd,当连接数很大且活跃连接很少时,效率低下。
  • 内核拷贝:每次调用仍然需要将整个 pollfd 数组从用户态拷贝到内核态,与 select 类似。
  • 不支持边缘触发:只有水平触发(与 select 相同),无法像 epoll 那样使用边缘触发。

5.3 适用场景

  • 连接数不太多(几百到几千)的场景。
  • 需要跨平台(epoll 仅 Linux)的场景。
  • 代码简单,比 select 更现代,比 epoll 更易移植。

六、poll 与 select、epoll 的综合对比

特性 select poll epoll
描述符上限 1024(通常) 无限制 无限制
每次调用拷贝 整个 fd_set 整个 pollfd 数组 仅注册时拷贝,无需每次拷贝
就绪通知方式 修改集合,需遍历 修改 revents,需遍历 直接返回就绪列表
时间复杂度 O(n) O(n) O(1)
触发模式 水平触发 水平触发 水平触发 + 边缘触发
跨平台 几乎所有 UNIX 类系统 仅 Linux
适用场景 少量连接 中等连接 海量连接

七、总结

  • pollselect 的改进版,它去掉了描述符上限,使用数组管理,避免了 fd_set 的大小限制。
  • 但仍需遍历所有 fd,在大量连接但少数活跃时效率不如 epoll
  • 如果你需要编写跨 UNIX 平台的网络程序,且连接数在几百到几千之间,poll 是一个不错的选择。
  • 在 Linux 下追求极致性能,应使用 epoll;在 Windows 下使用 IOCPselect
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐