【Linux开发】I/O 复用:poll 模型
·
一、为什么需要 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才是更优的选择。但poll比select更现代,且跨平台(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调整。 - 数据结构清晰:
events和revents分离,不需要每次重新构造。 - 跨平台:几乎所有 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 |
| 适用场景 | 少量连接 | 中等连接 | 海量连接 |
七、总结
- poll 是
select的改进版,它去掉了描述符上限,使用数组管理,避免了fd_set的大小限制。 - 但仍需遍历所有 fd,在大量连接但少数活跃时效率不如
epoll。 - 如果你需要编写跨 UNIX 平台的网络程序,且连接数在几百到几千之间,
poll是一个不错的选择。 - 在 Linux 下追求极致性能,应使用
epoll;在 Windows 下使用IOCP或select。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)