{T}

I/O多路复用:poll机制

前文介绍了基于 select 的 I/O 多路复用技术。select 通过描述符集合(fd_set)表示待检测的 I/O 对象,通过三个独立的集合分别描述可读、可写和异常事件。然而 select 存在一个显著的缺陷:所支持的文件描述符数量受 FD_SETSIZE 限制,在 Linux 系统中默认最大值为 1024。

poll 函数是另一种广泛使用的 I/O 多路复用技术,通过与 select 不同的数据结构设计,突破了文件描述符的数量限制。

poll 函数接口

函数原型

c
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
/* 返回值:就绪描述符数目;超时返回0;出错返回-1 */

参数说明

fdspollfd 结构体数组,每个元素描述一个待检测的文件描述符及其感兴趣的事件。

nfds:数组 fds 的大小,即待检测的描述符个数。

timeout:超时时间,单位为毫秒。

timeout 值行为
< 0永久等待,直到有事件发生
0立即返回,不阻塞
> 0等待指定毫秒数后返回

pollfd 结构体

c
struct pollfd {
    int    fd;       /* 文件描述符 */
    short  events;   /* 待检测的事件(输入) */
    short  revents;  /* 返回的事件(输出) */
};

与 select 的关键区别在于:poll 将输入(events)和输出(revents)分离到不同字段,检测完成后内核仅修改 revents,不影响原始的 events 设置。因此,应用程序无需在每次调用前重置事件集合。

此外,若将 fd 成员设置为负值,poll 函数将忽略该 pollfd 条目,且对应的 revents 置为 0。这提供了一种简洁的方式来临时屏蔽某个描述符的检测,而无需从数组中移除。

poll 工作流程

图表渲染中…

事件类型

可读事件类

c
#define POLLIN     0x0001    /* 有可读数据 */
#define POLLPRI    0x0002    /* 带外/紧急数据可读 */
#define POLLRDNORM 0x0040    /* 普通数据可读 */
#define POLLRDBAND 0x0080    /* 优先级带数据可读 */

实际编程中通常使用 POLLIN 即可。套接字可读事件的语义与 select 的 readset 一致:内核通知应用程序有数据可读,read 操作不会阻塞。

可写事件类

c
#define POLLOUT    0x0004    /* 描述符可写 */
#define POLLWRNORM POLLOUT   /* 无写类型区分 */
#define POLLWRBAND 0x0100    /* 优先级带数据可写 */

实际编程中通常使用 POLLOUT。套接字可写事件的语义与 select 的 writeset 一致:内核通知发送缓冲区已就绪,write 操作不会阻塞。

错误事件类(仅通过 revents 返回)

c
#define POLLERR    0x0008    /* 错误发生 */
#define POLLHUP    0x0010    /* 描述符挂起 */
#define POLLNVAL   0x0020    /* 描述符无效 */

此类事件无法通过 events 字段请求检测,仅由内核通过 revents 返回。

poll 与 select 对比

图表渲染中…
对比维度selectpoll
数据结构fd_set 位图pollfd 结构体数组
描述符上限FD_SETSIZE(通常 1024)受系统资源限制(ulimit -n
事件分离输入输出共用同一集合events/revents 分离
重置需求每次调用后必须重置无需重置
屏蔽描述符不支持fd = -1 即可屏蔽
跨平台POSIX 兼容POSIX 兼容

基于 poll 的服务器程序

以下实现一个基于 poll 的回显(echo)服务器,支持同时处理多个客户端连接:

c
#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:

code
$ 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:

code
$ 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 是更优的选择。

思考题

  1. 本程序中 event_set 数组大小固定为 INIT_SIZE,在生产环境中如何改进以支持动态扩展?

  2. 若实现了动态扩展的 pollfd 数组,新连接描述符加入数组的逻辑应如何配合调整?

版本信息

项目说明
更新日期2026-06-09
目标内核Linux 7.0
关键接口poll() — POSIX.1-2001, POSIX.1-2008
描述符上限RLIMIT_NOFILE 限制,可通过 ulimit -n 调整