在 Linux I/O 多路复用模型中,poll
紧随 select
之后,作为其功能更强大、限制更少的继任者。虽然 select
在处理并发连接方面迈出了重要一步,但其自身的一些缺陷促使了 poll
的诞生。poll
模型同样允许单个进程同时监控多个文件描述符,等待 I/O 事件,但在文件描述符数量限制和接口使用上进行了优化。
poll
为什么比 select
更优?select
的一个主要痛点是其对文件描述符数量的硬性限制(通常为 1024),这使得它难以应对大规模并发连接的场景(即所谓的 C10K 问题)。此外,select
每次调用都需要重新构建并拷贝 fd_set
结构,并且在返回后需要遍历整个集合来查找就绪的文件描述符,效率较低。
poll
模型解决了 select
的这些局限性:
poll
不受 FD_SETSIZE
宏的限制,它通过一个 pollfd
结构体数组来管理文件描述符,这个数组的大小可以动态调整,理论上只受限于系统内存。poll
使用 events
和 revents
字段来指定和返回事件类型,这使得事件的表示更加清晰和灵活。poll
同样需要遍历文件描述符数组来查找就绪的事件,但由于不再需要每次都重新构建并拷贝整个位图,其效率有所提升。poll
的核心工作原理poll
函数通过一个 struct pollfd
结构体数组来工作。数组中的每个元素都对应一个待监控的文件描述符及其关注的事件。
struct pollfd
结构体定义如下:
struct pollfd {
int fd; // 待监控的文件描述符
short events; // 关注的事件
short revents; // 实际发生的事件
};
fd
: 要监控的文件描述符。events
: 一个位掩码,表示你希望监控的事件类型。常见的事件标志包括:
POLLIN
: 文件描述符可读(有数据可读)。POLLPRI
: 有紧急数据可读。POLLOUT
: 文件描述符可写(可以写入数据)。POLLRDHUP
: 对端连接关闭或半关闭(仅限 Linux 2.6.17+)。POLLERR
: 发生错误。POLLHUP
: 对端连接挂起(通常表示对端关闭)。POLLNVAL
: 无效的文件描述符。revents
: 由内核填充的位掩码,表示实际发生的事件。它会包含在 events
中指定的一些事件,以及可能发生的其他事件(如 POLLERR
, POLLHUP
, POLLNVAL
)。当调用 poll
函数时,它会阻塞直到:
pollfd
数组中有一个或多个文件描述符上发生了关注的事件。函数返回后,应用程序可以遍历 pollfd
数组,检查每个元素的 revents
字段来确定哪些文件描述符已经就绪,并执行相应的 I/O 操作。
poll
函数原型#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds
: 指向 struct pollfd
数组的指针,其中包含要监控的文件描述符及其事件信息。nfds
: fds
数组中元素的数量,表示要监控的文件描述符的总数。timeout
: poll
函数的超时时间,单位为毫秒。
-1
: poll
会一直阻塞直到有事件发生。0
: poll
会立即返回,不阻塞(非阻塞轮询)。poll
会阻塞指定的时间(毫秒),或直到有事件发生。errno
。poll
模型示例:简单的 TCP 服务器下面是一个使用 poll
模型实现的简单 TCP 服务器示例。这个服务器可以同时处理多个客户端连接,并在接收到客户端消息后将其回显。
#include
#include
#include
#include
#include
#include
#include // poll 函数头文件
#define BUF_SIZE 1024
#define PORT 8080
#define MAX_CLIENTS 30 // 假设最大客户端数量
int main() {
int serv_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char buf[BUF_SIZE];
int str_len;
struct pollfd client_fds[MAX_CLIENTS + 1]; // 包含服务器套接字和客户端套接字
int client_count = 0; // 当前连接的客户端数量
// 1. 创建服务器套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1) {
perror("socket() error");
exit(1);
}
// 允许地址重用,避免 TIME_WAIT 状态导致端口占用
int optval = 1;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
// 2. 绑定地址和端口
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) {
perror("bind() error");
exit(1);
}
// 3. 监听连接
if (listen(serv_sock, 5) == -1) {
perror("listen() error");
exit(1);
}
// 初始化 pollfd 数组
// 将服务器套接字加入到监控列表的第一个位置
client_fds[0].fd = serv_sock;
client_fds[0].events = POLLIN; // 关注可读事件(即新连接请求)
client_count = 1; // 当前监控的文件描述符数量
printf("Server started on port %d\n", PORT);
while (1) {
// 4. 调用 poll 监听文件描述符
// timeout = -1 表示无限期等待
int fd_num = poll(client_fds, client_count, -1);
if (fd_num == -1) {
perror("poll() error");
break;
}
// 5. 遍历就绪的文件描述符
for (int i = 0; i < client_count; i++) {
if (client_fds[i].revents & POLLIN) { // 检查是否有可读事件
if (client_fds[i].fd == serv_sock) { // 服务器套接字就绪,有新连接请求
if (client_count >= MAX_CLIENTS + 1) { // 检查是否达到最大客户端数
printf("Max clients reached, new connection rejected.\n");
int temp_sock = accept(serv_sock, NULL, NULL); // 接收但立即关闭
if (temp_sock != -1) close(temp_sock);
continue;
}
int clnt_sock;
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if (clnt_sock == -1) {
perror("accept() error");
continue;
}
// 将新的客户端套接字加入到监控列表
client_fds[client_count].fd = clnt_sock;
client_fds[client_count].events = POLLIN; // 关注可读事件
client_fds[client_count].revents = 0; // 清零 revents
client_count++; // 增加监控的文件描述符数量
printf("Connected client: %s:%d (fd: %d). Total clients: %d\n",
inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port), clnt_sock, client_count -1);
} else { // 客户端套接字就绪,有数据可读
str_len = read(client_fds[i].fd, buf, BUF_SIZE);
if (str_len == 0) { // 客户端关闭连接
printf("Closed client (fd: %d). Total clients: %d\n", client_fds[i].fd, client_count - 2);
close(client_fds[i].fd);
// 将关闭的fd从数组中移除,用最后一个fd补位,并减少计数
for (int j = i; j < client_count - 1; j++) {
client_fds[j] = client_fds[j+1];
}
client_count--;
i--; // 确保检查当前位置的新fd
} else if (str_len > 0) {
buf[str_len] = '\0'; // 确保字符串以 null 结尾
printf("Received from fd %d: %s", client_fds[i].fd, buf);
write(client_fds[i].fd, buf, str_len); // 回显数据
} else { // read 错误
perror("read() error");
printf("Error on fd %d. Closing.\n", client_fds[i].fd);
close(client_fds[i].fd);
// 从数组中移除错误fd
for (int j = i; j < client_count - 1; j++) {
client_fds[j] = client_fds[j+1];
}
client_count--;
i--; // 确保检查当前位置的新fd
}
}
} else if (client_fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
// 处理错误、挂起或无效文件描述符
printf("Error/Hangup/Invalid FD on fd %d. Closing.\n", client_fds[i].fd);
close(client_fds[i].fd);
// 从数组中移除错误fd
for (int j = i; j < client_count - 1; j++) {
client_fds[j] = client_fds[j+1];
}
client_count--;
i--; // 确保检查当前位置的新fd
}
}
}
close(serv_sock);
return 0;
}
保存代码: 将上述代码保存为 poll_server.c
。
编译: 打开终端,使用 GCC 编译器编译代码:
gcc poll_server.c -o poll_server
运行服务器:
./poll_server
服务器将启动并在 8080 端口监听。
使用客户端测试: 你可以使用 netcat
(nc) 或编写一个简单的 C 语言客户端来测试服务器。
使用 netcat
:
打开另一个终端,连接到服务器:
nc 127.0.0.1 8080
然后输入消息并按回车,服务器会回显你的消息。你可以打开多个终端来模拟多个客户端连接,观察 poll
如何高效地处理它们。
通过这个 poll
示例,我们可以看到它在处理大量并发连接时的优势,尤其是在避免 select
的文件描述符数量限制方面。虽然 poll
相比 select
有显著改进,但其每次调用仍然需要遍历整个 pollfd
数组来发现就绪事件,这在极端大规模并发场景下(例如几十万甚至上百万连接)依然会带来性能瓶颈。这也是后来更高效的 epoll
(Linux 特有)和 kqueue
(BSD 特有)模型出现的原因。