poll系统调用及示例

poll系统调用及示例

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 poll 函数,它是一种高效的 I/O 多路复用机制,允许一个进程同时监视多个文件描述符,等待其中任何一个或多个文件描述符变我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 poll 函数,它是一种高效的 I/O 多路复用机制,允许一个进程同时监视多个文件描述符,等待其中任何一个或多个文件描述符变为“就绪”状态(例如可读、可写或发生异常)。

相关文章:ppoll系统调用及示例 epoll_create1系统调用及示例 epoll_ctl系统调用及示例

1. 函数介绍

poll 是一个 Linux 系统调用,用于实现 I/O 多路复用 (I/O multiplexing)。它的核心思想是让进程能够同时检查多个文件描述符(如套接字、管道、终端等)的状态,看它们是否准备好进行 I/O 操作(例如读取、写入),而无需对每个文件描述符都进行阻塞式等待。

在没有 poll(或 select、epoll)的情况下,如果一个程序需要同时处理多个网络连接或文件,它可能需要创建多个线程或进程,或者在一个文件描述符上阻塞等待,这会非常低效或复杂。poll 允许一个线程/进程在一个调用中“监听”所有感兴趣的文件描述符,当其中任何一个准备好时,poll 返回,程序就可以处理那个就绪的文件描述符。

你可以把它想象成一个“服务员”,同时照看多张餐桌(文件描述符)。服务员不需要一直站在某一张餐桌旁等客人点菜(数据),而是可以走一圈看看哪张餐桌的客人举手了(数据就绪),然后去为那张餐桌服务。

2. 函数原型

1
2
3
4
#include <poll.h> // 必需

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

3. 功能

  • 监视文件描述符集合: poll 会检查 fds 数组中列出的 nfds 个文件描述符的状态。

等待就绪: 调用 poll 的进程会阻塞(挂起),直到以下情况之一发生:

  • fds 数组中的至少一个文件描述符变为“就绪”状态(根据 events 字段指定的条件)。

  • 调用被信号中断(返回 -1,并设置 errno 为 EINTR)。

  • 达到指定的超时时间 timeout(如果 timeout >= 0)。

  • 返回就绪数量: 当 poll 返回时,它会报告有多少个文件描述符已就绪。

  • 更新状态: poll 会修改 fds 数组中每个元素的 revents 字段,以指示该文件描述符上实际发生的事件。

4. 参数

struct pollfd *fds: 这是一个指向 struct pollfd 类型数组的指针。这个数组包含了所有需要监视的文件描述符及其感兴趣的事件。struct pollfd 的定义如下:struct pollfd { int fd; // 要监视的文件描述符 short events; // 程序关心的事件 (输入) short revents; // 实际发生的事件 (输出) };

  • fd: 要监视的文件描述符。如果 fd 为负数,则忽略该数组元素。

events: 这是一个位掩码,指定了应用程序对这个 fd 感兴趣的事件。常用的值包括:

  • POLLIN: 数据可读(对于普通文件,通常总是可读的)。

  • POLLOUT: 数据可写(对于普通文件,通常总是可写的)。

  • POLLPRI: 高优先级数据可读(例如 TCP 带外数据)。

  • POLLERR: 发生错误(作为 revents 返回,不能在 events 中设置)。

  • POLLHUP: 挂起(例如对端套接字关闭)(作为 revents 返回)。

  • POLLNVAL: 文件描述符无效(作为 revents 返回)。

revents: 这个字段由 poll 调用填充,返回该 fd 上实际发生的事件。程序需要检查这个字段来确定 fd 是否就绪以及发生了什么事件。

nfds_t nfds: 这是 fds 数组中的元素个数,即要监视的文件描述符总数。

int timeout: 指定 poll 调用阻塞等待的超时时间(以毫秒为单位)。

  • timeout == -1: poll 会无限期阻塞,直到至少一个文件描述符就绪或被信号中断。

  • timeout == 0: poll 执行非阻塞检查,立即返回,报告当前有多少文件描述符已就绪。

  • timeout > 0: poll 最多阻塞 timeout 毫秒。如果在超时前没有文件描述符就绪,则返回 0。

5. 返回值

成功时:

  • 返回 就绪的文件描述符的数量(即 revents 非零的 fds 元素个数)。这个数字可以是 0(表示超时)。

失败时:

  • 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EFAULT fds 指针无效,EINTR 调用被信号中断,EINVAL nfds 负数等)。

超时:

  • 如果在 timeout 指定的时间内没有任何文件描述符就绪,返回 0。

6. 相似函数,或关联函数

  • select: 一个更老的 I/O 多路复用函数,功能与 poll 类似,但在处理大量文件描述符时效率较低,且文件描述符集合有大小限制 (FD_SETSIZE)。

  • epoll_wait / epoll_ctl / epoll_create: Linux 特有的、更高效的 I/O 多路复用机制,特别适合处理大量的并发连接。它使用一个内核事件表来管理监视的文件描述符,避免了 poll/select 每次调用都需要传递整个文件描述符集合的开销。

  • read, write: 在 poll 返回某个文件描述符就绪后,通常会调用 read 或 write 来执行实际的 I/O 操作。

7. 示例代码

示例 1:监视标准输入和一个管道

这个例子演示如何使用 poll 同时监视标准输入(键盘)和一个管道的读端,看哪个先有数据可读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <poll.h>     // poll, struct pollfd
#include <unistd.h> // pipe, read, write, close, STDIN_FILENO
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
#include <string.h> // strlen

int main() {
int pipefd&#91;2];
struct pollfd fds&#91;2];
int num_fds = 2;
int timeout_ms = 5000; // 5 秒超时
int ret;
char buffer&#91;100];
ssize_t bytes_read;

// 1. 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}

// 2. 设置要监视的文件描述符数组
// 监视标准输入 (stdin)
fds&#91;0].fd = STDIN_FILENO; // 通常是 0
fds&#91;0].events = POLLIN; // 关心可读事件
fds&#91;0].revents = 0; // 内核会填充

// 监视管道的读端
fds&#91;1].fd = pipefd&#91;0];
fds&#91;1].events = POLLIN; // 关心可读事件
fds&#91;1].revents = 0; // 内核会填充

printf("Waiting up to %d ms for input from stdin or data in pipe...\n", timeout_ms);
printf("Type something in the terminal, or run 'echo hello > /proc/%d/fd/%d' in another terminal.\n",
getpid(), pipefd&#91;1]); // 提示用户如何向管道写入

// 3. 调用 poll 进行等待
ret = poll(fds, num_fds, timeout_ms);

// 4. 检查 poll 的返回值
if (ret == -1) {
perror("poll");
close(pipefd&#91;0]);
close(pipefd&#91;1]);
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("Timeout occurred! No data within %d ms.\n", timeout_ms);
} else {
printf("%d file descriptor(s) became ready.\n", ret);

// 5. 检查哪个文件描述符就绪了
for (int i = 0; i < num_fds; ++i) {
if (fds&#91;i].revents != 0) {
printf("fd %d (originally fd %d) is ready. revents = 0x%04x\n",
fds&#91;i].fd, fds&#91;i].fd, fds&#91;i].revents);

if (fds&#91;i].revents & POLLIN) {
printf(" -> POLLIN event on fd %d\n", fds&#91;i].fd);
if (fds&#91;i].fd == STDIN_FILENO) {
printf(" -> Reading from standard input:\n");
bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer&#91;bytes_read] = '\0'; // 确保字符串结束
printf(" -> Read from stdin: %s", buffer); // buffer 可能已包含 \n
}
} else if (fds&#91;i].fd == pipefd&#91;0]) {
printf(" -> Reading from pipe:\n");
bytes_read = read(pipefd&#91;0], buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer&#91;bytes_read] = '\0';
printf(" -> Read from pipe: %s", buffer);
}
}
}
// 可以检查其他 revents,如 POLLERR, POLLHUP 等
if (fds&#91;i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
printf(" -> Error or hangup or invalid fd on fd %d\n", fds&#91;i].fd);
}
}
}
}

// 6. 清理资源
close(pipefd&#91;0]);
close(pipefd&#91;1]);

return 0;
}

代码解释:

使用 pipe() 创建一个管道,得到读端 pipefd[0] 和写端 pipefd[1]。

定义一个 struct pollfd 数组 fds,包含两个元素。

第一个元素监视 STDIN_FILENO(标准输入),关心 POLLIN 事件。

第二个元素监视管道的读端 pipefd[0],也关心 POLLIN 事件。

调用 poll(fds, 2, 5000),等待最多 5 秒钟。

检查 poll 的返回值:

  • -1:错误。

  • 0:超时。

  • 0:就绪的文件描述符数量。

如果有文件描述符就绪(ret > 0),遍历 fds 数组,检查每个元素的 revents 字段。

如果 revents 包含 POLLIN,则调用 read 从对应的文件描述符读取数据。

最后关闭管道的两端。

示例 2:简单的 TCP 服务器(非阻塞 accept 和客户端 socket)

这个例子演示如何在 TCP 服务器中使用 poll 来同时监听监听套接字(用于接受新连接)和已建立连接的客户端套接字(用于接收数据)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include <poll.h>      // poll, struct pollfd
#include <sys/socket.h> // socket, bind, listen, accept, recv, send
#include <netinet/in.h> // sockaddr_in
#include <arpa/inet.h> // inet_addr, inet_ntoa (简化版,非线程安全)
#include <unistd.h> // close, read, write
#include <stdio.h> // perror, printf, fprintf
#include <stdlib.h> // exit
#include <string.h> // memset, strlen

#define PORT 8080
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct pollfd fds&#91;MAX_CLIENTS + 1]; // +1 for the listening socket
int nfds = 1; // Initially, only the listening socket
int timeout_ms = -1; // Block indefinitely
int activity;
char buffer&#91;BUFFER_SIZE] = {0};
char *hello = "Hello from server";

// 1. 创建服务器套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}

// 2. 配置服务器地址
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地接口
address.sin_port = htons(PORT);

// 3. 绑定套接字
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 4. 监听连接
if (listen(server_fd, 3) < 0) { // backlog=3
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}

printf("Server listening on port %d\n", PORT);

// 5. 设置 poll 监视的初始文件描述符:监听套接字
fds&#91;0].fd = server_fd;
fds&#91;0].events = POLLIN; // 关心可读事件 (有新连接)
fds&#91;0].revents = 0;

// 初始化其他客户端槽位
for(int i = 1; i < MAX_CLIENTS + 1; i++) {
fds&#91;i].fd = -1; // -1 表示槽位空闲
fds&#91;i].events = POLLIN;
fds&#91;i].revents = 0;
}

// 6. 主循环
while(1) {
// 7. 调用 poll 等待事件
activity = poll(fds, nfds, timeout_ms);

if (activity < 0) {
perror("poll error");
break; // 或 exit(EXIT_FAILURE);
}

if (activity == 0) {
// 不应该发生,因为 timeout_ms = -1
printf("poll timeout (unexpected)\n");
continue;
}

// 8. 检查监听套接字 (fds&#91;0]) 是否有活动
if (fds&#91;0].revents & POLLIN) {
// 有新的客户端连接请求
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
continue; // 继续处理其他事件
}

printf("New connection, socket fd is %d, ip is : %s, port : %d\n",
new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));

// 将新连接的套接字添加到 poll 监视集合中
int i;
for (i = 1; i < MAX_CLIENTS + 1; i++) {
if (fds&#91;i].fd == -1) {
fds&#91;i].fd = new_socket;
fds&#91;i].events = POLLIN;
fds&#91;i].revents = 0;
if (i >= nfds) nfds = i + 1; // 更新监视的 fd 数量
// 发送欢迎信息
send(new_socket, hello, strlen(hello), 0);
printf("Welcome message sent\n");
break;
}
}
if (i == MAX_CLIENTS + 1) {
printf("Too many clients, connection rejected\n");
close(new_socket);
}
}

// 9. 检查已连接的客户端套接字是否有活动
for (int i = 1; i < nfds; i++) {
if (fds&#91;i].fd != -1 && (fds&#91;i].revents & POLLIN)) {
// 有数据从客户端发来
int sd = fds&#91;i].fd;
ssize_t valread = read(sd, buffer, BUFFER_SIZE - 1);
if (valread == 0) {
// 客户端断开连接
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
printf("Host disconnected, ip %s, port %d\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
close(sd);
fds&#91;i].fd = -1; // 标记槽位为空闲
} else {
// 处理收到的数据
buffer&#91;valread] = '\0';
printf("Received message from socket %d: %s", sd, buffer);
// Echo 回去
send(sd, buffer, strlen(buffer), 0);
}
}
// 可以检查 POLLERR, POLLHUP 等错误事件
if (fds&#91;i].fd != -1 && (fds&#91;i].revents & (POLLERR | POLLHUP))) {
printf("Error or hangup on client socket %d\n", fds&#91;i].fd);
close(fds&#91;i].fd);
fds&#91;i].fd = -1;
}
}
}

// 10. 清理 (在真实应用中,需要更优雅的退出机制)
for(int i = 0; i < nfds; i++) {
if(fds&#91;i].fd != -1) {
close(fds&#91;i].fd);
}
}
close(server_fd);
printf("Server closed.\n");
return 0;
}

代码解释:

创建、绑定、监听 TCP 套接字。

初始化 struct pollfd 数组 fds。第一个元素 (fds[0]) 用于监视监听套接字 server_fd,关心 POLLIN 事件(表示有新的连接请求)。

数组的其余元素(fds[1] 到 fds[MAX_CLIENTS])用于监视已建立连接的客户端套接字。初始时,它们的 fd 被设置为 -1,表示空闲槽位。

进入主循环,调用 poll(fds, nfds, -1)。nfds 跟踪当前需要监视的 fds 数组元素个数(通常是已使用的槽位数)。

poll 返回后,检查返回值 activity。

如果 fds[0].revents & POLLIN 为真,说明监听套接字就绪,调用 accept 接受新连接。

将新获得的客户端套接字 new_socket 放入 fds 数组的一个空闲槽位中(fd 为 -1 的位置),并更新 nfds。

遍历 fds 数组中用于客户端的槽位(从索引 1 开始),检查 revents。

如果某个客户端套接字的 revents & POLLIN 为真,说明该客户端有数据可读,调用 read 读取数据。

如果 read 返回 0,表示客户端关闭了连接,关闭该套接字,并将 fds 中对应的 fd 设置回 -1。

如果 read 返回正数,表示读到了数据,这里简单地将其 echo 回客户端。

同样检查 POLLERR 和 POLLHUP 等错误事件。

这个例子展示了 poll 如何在一个单线程服务器中高效地管理多个并发连接。与为每个连接创建一个线程或进程相比,poll(以及更高效的 epoll)是构建高性能网络服务器的基础技术之一。

理解 poll 的关键是掌握 struct pollfd 数组的使用、events 和 revents 的含义,以及如何根据返回的就绪文件描述符数量和状态来处理相应的 I/O 操作。

poll系统调用详解, poll函数使用示例, linux poll系统调用, poll函数原理与应用, linux系统编程poll函数, poll函数如何工作, poll系统调用教程, poll函数在Linux中的应用, poll系统调用实例解析, poll函数编程指南

data-ad-format="auto" data-full-width-responsive="true">