IO复用:poll

张开发
2026/4/16 8:36:40 15 分钟阅读

分享文章

IO复用:poll
一、poll的含义poll 是 Unix-like 系统例如 Linux中一个重要的系统调用用于实现 I/O 多路复用。它允许程序同时监视多个文件描述符检查它们是否就绪进行读、写或其他操作而无需阻塞进程。这比传统的 select() 系统调用更高效尤其在处理大量文件描述符时。二、API接口poll() 的原型定义在 头文件中#include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);参数说明fds一个指向 struct pollfd 数组的指针。该结构体定义如下struct pollfd { int fd; /* 文件描述符 */ short events; /* 监视的事件掩码输入 */ short revents; /* 发生的事件掩码输出由内核填充 */ };fd要监视的文件描述符。如果 fd 0该元素会被忽略。events位掩码表示感兴趣的事件。常见值包括POLLIN有数据可读包括普通数据、优先数据、EOF。POLLPRI有紧急数据可读例如 TCP 的 out-of-band 数据。POLLRDHUP流套接字对端关闭连接GNU 扩展。POLLERR错误发生输出事件只在 revents 中设置。POLLHUP挂起输出事件。POLLNVAL无效请求输出事件。revents由内核返回指示实际发生的事件。如果为 0表示无事件。nfdsfds 数组的元素个数类型为 unsigned long通常用 int 即可。timeout超时时间毫秒0等待指定毫秒。0立即返回轮询模式。-1无限等待直到有事件发生。返回值0超时无事件三、工作原理poll() 调用时内核会复制 fds 数组到内核空间然后检查每个 fd 的状态。如果有事件就绪内核填充 revents 并返回。poll() 是 level-triggered水平触发的如果事件未处理下次 poll() 仍会报告它。内部实现内核使用等待队列wait queue机制当 fd 状态变化时唤醒进程。注意poll() 会阻塞调用线程直到超时或事件发生。如果需要非阻塞可以结合线程或异步机制使用。四、用poll实现多客户端服务器#include stdio.h #include stdlib.h #include unistd.h #include string.h #include poll.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define MAXFD 10 int socket_init(); void fds_init(struct pollfd fds[]) { for (int i 0; i MAXFD; i) { fds[i].fd -1; fds[i].events 0; fds[i].revents 0; } } void fds_add(int fd, struct pollfd fds[]) { for (int i 0; i MAXFD; i) { if( fds[i].fd -1) { fds[i].fd fd; fds[i].events POLLIN;//读 fds[i].revents 0; break; } } } void fds_del(int fd, struct pollfd fds[]) { for(int i 0; i MAXFD; i) { if( fds[i].fd fd) { fds[i].fd -1; fds[i].events 0; fds[i].revents 0; break; } } } void accept_client(int sockfd,struct pollfd fds[]) { int c accept(sockfd,NULL,NULL); if( c 0 ) { return ; } printf(accept c%d\n,c); fds_add(c,fds); } void recv_data(int c,struct pollfd fds[]) { char buff[128] {0}; int n recv(c,buff,127,0); if( n 0) { close(c); fds_del(c,fds); printf(client close\n); return; } printf(buff(%d)%s\n,c,buff); send(c,ok,2,0); } int main() { int sockfd socket_init(); if (sockfd -1) { exit(1); } struct pollfd fds[MAXFD]; // 收集描述符和事件 fds_init(fds); fds_add(sockfd,fds);//添加sockfd到数组fds while( 1 ) { int n poll(fds,MAXFD,5000); if( n -1) { printf(poll err\n); } else if ( n 0 ) { printf(time out\n); } else { for(int i 0; i MAXFD; i ) { if( fds[i].fd -1) { continue; } if( fds[i].revents POLLIN) { if( fds[i].fd sockfd) { accept_client(sockfd,fds); } else { recv_data(fds[i].fd,fds); } } //if ( fds[i].revents POLLOUT) //{} } } } } int socket_init() { int sockfd socket(AF_INET, SOCK_STREAM, 0); if (sockfd -1) { return -1; } struct sockaddr_in saddr; memset(saddr, 0, sizeof(saddr)); saddr.sin_family AF_INET; saddr.sin_port htons(6000); saddr.sin_addr.s_addr inet_addr(127.0.0.1); int res bind(sockfd, (struct sockaddr *)saddr, sizeof(saddr)); if (res -1) { printf(bind err\n); return -1; } res listen(sockfd, 5); if (res -1) { return -1; } return sockfd; }五、 select与poll对比维度selectpoll文件描述符上限有硬上限通常 1024FD_SETSIZE 超过要改内核或用技巧理论无上限只受内存限制数据结构3 个 fd_set 位图读、写、异常一个 struct pollfd 数组每次调用是否破坏输入参数会修改 fd_set必须每次 FD_ZERO FD_SET 重新设置只改 reventsevents 不变事件类型丰富度只有读、写、异常三种POLLIN / POLLOUT / POLLERR / POLLHUP / POLLRDHUP / POLLPRI 等告诉内核“最高 fd”是多少必须传 nfds max_fd 1不需要传数组长度就行性能时间复杂度O(最高 fd 编号) —— 即使你只关心 10000 号 fd也要扫 0~9999O(你实际传入的 fd 数量)大并发实际表现1 万连接明显变慢CPU 浪费在扫描空位图比 select 快很多但仍线性扫描可移植性几乎所有 Unix Windows 原生支持POSIX 标准Windows 需要 WSAPollWin10 支持好代码可读性与维护性位图操作繁琐容易写错数组位掩码现代 C 代码风格适合的连接规模≤ 500 个连接小项目、嵌入式、旧代码500 ~ 5000 个连接中型服务器六、poll 内核里到底存了什么数据结构用户传一个 pollfd 大数组。内核为了防拷贝太大和栈溢出会切成每页一个struct poll_list链表节点柔性数组 entries[0]。这个链表只在本次 poll 调用期间存在用完就销毁。唯一持久的是每个 socket 的sk_sleep等待队列用来睡眠/唤醒。所以下次 poll 还得从头拷贝、从头遍历、从头注册。七、poll 是怎么睡眠和被唤醒的poll 调用进入内核 do_poll()。第一遍遍历所有 fd 调用 .poll() 检查是否已就绪快速路径。如果都没就绪就注册 poll_table 的 qproc __pollwait。第二遍遍历时每个 socket 的 .poll() 会通过 __pollwait 把当前进程添加到 socket 的sk_sleep等待队列上。然后进程调用 schedule_timeout() 睡眠TASK_INTERRUPTIBLE。当网卡收到包 → tcp_data_queue() → sock_def_readable() → wake_up_interruptible(sk-sk_sleep)。进程被唤醒后回到 do_poll()再来一轮遍历检查谁就绪了。所以唤醒是精准的只唤醒这个 socket 的等待者但所有poll进程都会被同一个socket唤醒惊群问题。

更多文章