📌 核心结论(TL;DR):
poll 的改进:用
struct pollfd数组代替了select的位图,去掉了 1024 个连接的上限限制;分离了监听事件(events)和返回事件(revents),无需每次重置集合。poll 的痛点:依然是O(N)复杂度。每次调用需要全量拷贝结构体数组到内核,内核需要遍历所有 fd 找就绪的。
适用场景:只有少部分活跃连接,或连接数不太多时。高并发场景下依然力不从心。
一、 从 select 到 poll 的进化
select用位图监听,有两个致命缺点:一是最高只能监听 1024 个 fd;二是参数输入输出混用,每次都需重置。poll解决了这两个痛点。它使用struct pollfd数组,你想监听多少个,就申请多大的数组。同时,它把关注的events和就绪的revents分开,让代码逻辑清晰了不少。
二、 代码实战:poll 服务器
struct pollfd fds[MAX_FD]; fds[0].fd = listen_fd; fds[0].events = POLLIN; // 只设置一次 while (1) { int ret = poll(fds, max_index + 1, -1); // -1 表示永久阻塞 for (int i = 0; i <= max_index; i++) { if (!(fds[i].revents & POLLIN)) continue; // 检查 revents 即可 // 处理就绪逻辑 } }相比select繁琐的FD_ZERO和FD_SET,poll的接口明显更加优雅,且支持动态扩容。
三、 为什么 poll 依然没落?
虽然poll没有数量限制了,但面对几十万连接时,性能瓶颈凸显:
海量拷贝:每次调用
poll,都要把十万个pollfd结构体从用户态拷贝到内核态。全量遍历:内核必须遍历这十万个结构体,才能找出那少数几个真正就绪的。
用户态遍历:返回后,用户程序依然要遍历十万个数组元素,检查
revents。
在“高并发、少活跃”的现代互联网场景下,O(N) 的复杂度无异于灾难。这也引出了我们下一节的主角:基于事件回调的epoll。
咱们用poll重写之前的select服务器,你对比一下代码,就能明显感觉到 API 的优雅。
cd ~/linux_study/network_test实战:极简 poll 服务器
vim poll_server.c(输入以下代码,重点看pollfd数组的初始化与遍历)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <poll.h> // 引入 poll 头文件 #define PORT 9999 #define MAX_FD 1024 int main() { int listen_fd = socket(AF_INET, SOCK_STREAM, 0); int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(PORT); server_addr.sin_addr.s_addr = INADDR_ANY; bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); listen(listen_fd, 5); // 1. 初始化 pollfd 数组 struct pollfd fds[MAX_FD]; // 初始化所有 fd 为 -1 (代表空位) for (int i = 0; i < MAX_FD; i++) { fds[i].fd = -1; fds[i].events = 0; fds[i].revents = 0; } // 2. 将监听 socket 放入数组的第 0 位 fds[0].fd = listen_fd; fds[0].events = POLLIN; // 监听可读事件 int max_index = 0; // 记录当前数组最大用到哪个下标 printf("Poll 服务器启动,监听端口 %d...\n", PORT); while (1) { // 3. 调用 poll,传入数组和最大长度。timeout = -1 代表永久阻塞 int ret = poll(fds, max_index + 1, -1); if (ret < 0) { perror("poll"); break; } // 4. 遍历数组 for (int i = 0; i <= max_index; i++) { if (fds[i].fd == -1) continue; // 空位跳过 if (!(fds[i].revents & POLLIN)) continue; // 没有读事件发生 if (fds[i].fd == listen_fd) { // 场景A:有新连接 int conn_fd = accept(listen_fd, NULL, NULL); printf("新连接接入, fd = %d\n", conn_fd); // 找数组中的空位放进去 int pos = 0; for (; pos < MAX_FD; pos++) { if (fds[pos].fd == -1) { fds[pos].fd = conn_fd; fds[pos].events = POLLIN; if (pos > max_index) max_index = pos; // 更新最大下标 break; } } if (pos == MAX_FD) { printf("服务器满了,关闭新连接\n"); close(conn_fd); } } else { // 场景B:普通客户端发来数据 char buf[1024] = {0}; int n = read(fds[i].fd, buf, sizeof(buf) - 1); if (n > 0) { printf("收到 fd=%d 的消息: %s\n", fds[i].fd, buf); write(fds[i].fd, buf, n); // Echo } else if (n == 0) { printf("客户端 fd=%d 断开\n", fds[i].fd); close(fds[i].fd); fds[i].fd = -1; // 标记为空位 fds[i].events = 0; } } } } close(listen_fd); return 0; }编译运行:
gcc poll_server.c -o poll_server ./poll_server👉现象:用你的tcp_client连上来发消息,完美运行。
👉最大的体验提升:你再也不需要像select那样,每次循环都去写一堆FD_ZERO和FD_SET了。我们只设置一次events = POLLIN,剩下的交给内核。