资讯动态

多路转接Select、Poll

发布时间:2026/10/19 3:41:36 来源:尧图企业网站定制
一、select1.1 认识selectselect是 Linux 下最早的 IO 多路复用接口同时监听多个文件描述符阻塞等待只要任意一个 fd 就绪可读 / 可写 / 异常就返回。seletc只负责等的环节一次可以等待多个文件描述符可读||可写即文件描述符的事件就绪接收缓冲区有空间/发送缓冲区有数据。拷贝时则使用系统调用read/recv/recvfrom。那为什么要有select呢为了减少在单位时间内等的比重提高IO效率。在Linux系统当中存在一个系统调用select第一个参数nfds表示的是它所等待的文件描述符中最大的值1最后一个参数表示select在等待时的等待方式NULL则表示 select没有 timeoutselect 将一直被阻塞直到某个文件描述符上发生了事件0仅检测描述符集合的状态然后立即返回并不等待外部事件的发生。特定的时间值如果在指定的时间段里没有事件发生select 将超时返回。返回值执行成功则返回文件描述词状态已改变的个数如果返回 0 代表在描述词状态改变前已超过 timeout 时间没有返回当有错误发生时则返回 - 1错误原因存于 errno此时参数 readfdswritefdsexceptfds 和 timeout 的值变成不可预测。错误值可能为EBADF 文件描述词为无效的或该文件已关闭EINTR 此调用被信号所中断EINVAL 参数 n 为负值。ENOMEM 核心内存不足文件描述符的事件一般有以下几种情况1.读事件就绪2.写事件就绪3.异常事件就绪即当一台主机关机时就会处于一种既不读也不写的状态select识别到这种异常之后就会关闭连接当我们关心哪个文件描述符的哪个事件就将对应的文件描述符添加到select的中间三个参数中。例如只关心三号文件描述符的读则只添加3到readfds中只关心4号文件描述符的写则只添加4到writefds中关心五号文件描述符的读写就分别添加到readfds和writefds中。而这三个参数的参数类型是fd_set。这个类型是Linux中设计的一种特殊的数据类型叫做文件描述符集可以向select传递多个文件描述符。由于它是一种参数类型所以它有固定大小为128字节。这个文件描述符集的底层其实就是一个位图结构由于是位图结构加之它的大小为128字节所以这个位图能关心的最多的文件描述符个数为128*81024个下面是select中传入的三个文件描述符的说明1. readfds读事件集合输入用户→内核比特位位置 fd 编号该位设为 1表示关心这个 fd 的读就绪事件0 表示不关心。输出内核→用户select 返回时读就绪的 fd 对应 bit 置 1未就绪清 0通知用户哪些 fd 可以读。触发场景socket 收到数据、管道有数据、对方关闭连接等。2. writefds写事件集合输入用户→内核比特位位置 fd 编号该位设为 1表示关心这个 fd 的写就绪事件0 表示不关心。输出内核→用户select 返回时写就绪的 fd 对应 bit 置 1未就绪清 0通知用户哪些 fd 可以写。触发场景内核发送缓冲区有空位可以往 socket 写数据。3. exceptfds异常事件集合输入用户→内核比特位位置 fd 编号该位设为 1表示关心这个 fd 的异常事件0 表示不关心。输出内核→用户select 返回时发生异常的 fd 对应 bit 置 1无异常清 0通知用户哪些 fd 出现异常。触发场景带外数据OOB到达。除了将文件描述符集的相关数据结构设计出来Linux还为用户提供了相关的宏FD_ZERO(fd_set set)作用清空整个位图集合把集合里所有 bit 全部置 0。使用时机每次循环调用 select 之前先清零集合再重新添加需要监听的 fd。一句话初始化全部置 0。*FD_SET(int fd, fd_set set)作用将fd对应的比特位置 1代表把这个文件描述符加入监听集合。一句话把 fd 添加进集合告诉内核 “我关心这个 fd 的事件”。*FD_CLR(int fd, fd_set set)作用将fd对应的比特位置 0把 fd 从监听集合移除。一句话把 fd 从集合中删掉不再监听。*FD_ISSET(int fd, fd_set set)返回值非 0 → 该 fd 对应 bit 为 10 → bit 为 0作用select 返回后用来检查这个 fd 是否事件就绪。一句话查询 fd 在集合里是不是就绪了。细节一般针对一个文件描述符时我们一般不会同时关心文件描述符的读写一般都是先关心读后关心写一般对任意一个fd来说读事件一般都是默认不就绪的而写事件一般都是默认就绪的。读无内容写有空间。所以写事件一般是按需设置不会常设1.2 select的原理select 基于 fd_set 位图机制实现 IO 多路复用用户程序通过 FD 系列宏将需要监听读、写、异常事件的文件描述符在位图中标记为 1调用 select 时将位图拷贝至内核内核轮询所有被标记的 fd若无就绪事件则阻塞进程一旦有 fd 触发对应 IO 事件内核会重写位图仅将就绪 fd 对应的比特置 1再把修改后的位图拷贝回用户态最后用户通过 FD_ISSET 遍历位图识别就绪 fd 完成 IO 处理1.3 select的特点和缺点select的特点由于输入输出参数是同一个每次调用select之前每次都需要重新设定。所以select服务器往往需要结合一个辅助数组才能正常工作。select 缺点1.每次调用 select都需要手动设置 fd 集合从接口使用角度来说也非常不便。2.每次调用 select都需要把 fd 集合从用户态拷贝到内核态这个开销在 fd 很多时会很大3.同时每次调用 select 都需要在内核遍历传递进来的所有 fd这个开销在 fd 很多时也很大4.select 支持的文件描述符数量太小。1.4 select使用示例#include stdio.h #include unistd.h #include sys/select.h #include sys/socket.h int main() { fd_set read_fds; // 位图输入输出共用会被内核修改 int maxfd 0; int fd_arr[10]; // 辅助数组保存所有需要监听的fd int fd_cnt 0; // 把监听套接字放入辅助数组 fd_arr[fd_cnt] listen_fd; fd_cnt; maxfd listen_fd; while(1) { FD_ZERO(read_fds); // 每次调用select前遍历辅助数组重新填充fd_set for(int i 0; i fd_cnt; i) { FD_SET(fd_arr[i], read_fds); if(fd_arr[i] maxfd) maxfd fd_arr[i]; } // selectmaxfd1fd_set既是输入也是输出 int ret select(maxfd 1, read_fds, NULL, NULL, NULL); if(ret 0) continue; // 遍历辅助数组判断哪些fd就绪 for(int i 0; i fd_cnt; i) { int fd fd_arr[i]; if(FD_ISSET(fd, read_fds)) { if(fd listen_fd) { // 新连接 int cfd accept(listen_fd, NULL, NULL); fd_arr[fd_cnt] cfd; } else { // 读客户端数据 char buf[1024]; int n read(fd, buf, sizeof(buf)); if(n 0) { close(fd); // 删除fd数组末尾覆盖 fd_arr[i] fd_arr[fd_cnt - 1]; fd_cnt--; i--; } } } } } return 0; }二、poll2.1 认识pollpoll只负责进行等是就绪事件通知机制作用等同于select。在Linux系统当中存在系统调用poll传入输入结构体参数fds是为了告诉内核要帮用户关心当前 fd 上的 events 事件。而输出信息保存在结构体内部的 revents 字段内核通过修改 revents 告诉用户哪些 fd 上的哪些事件就绪了这样相比于 selectpoll 就做到了输入输出解耦而传入的 nfds 是告诉内核fds 数组里面一共有多少个有效元素。而poll 函数的返回值是内核返回给用户的就绪 fd 数量。传入的struct pollfd *fds的结构体如下struct pollfd { int fd; // 输入告诉内核要监听哪个fd short events; // 【输入参数】用户设置想监听什么事件 short revents; // 【输出参数】内核填充实际发生了什么事件 };events 和 revents 的取值事件描述是否可作为输入是否可作为输出POLLIN数据包括普通数据和优先数据可读是是POLLRDNORM普通数据可读是是POLLRDBAND优先级带数据可读Linux 不支持是是POLLPRI高优先级数据可读比如 TCP 带外数据是是POLLOUT数据包括普通数据和优先数据可写是是POLLWRNORM普通数据可写是是POLLWRBAND优先级带数据可写是是POLLRDHUPTCP 连接被对方关闭或者对方关闭了写操作。它由 GNU 引入是是POLLERR错误否是POLLHUP挂起。比如管道的写端被关闭后读端描述符上将收到 POLLHUP 事件否是POLLNVAL文件描述符没有打开否是这些取值都是一个个的宏本质就是标志位用位图来传递标志位事件宏值十六进制POLLIN0x0001POLLRDNORM0x0040POLLRDBAND0x0080POLLPRI0x0002POLLOUT0x0004POLLWRNORM0x0100POLLWRBAND0x0200POLLRDHUP0x2000POLLERR0x00082.2 poll的原理和select一样poll本质也是遍历数组。用户会在用户态构造 struct pollfd 数组数组里的每一项都要填写文件描述符 fd以及想要监听的 events 事件调用 poll 系统调用会把整个 pollfd 数组拷贝到内核态内核开始遍历数组里面的每一个 fd逐个检查 fd 对应的状态判断这个 fd 有没有发生我们在 events 中关心的事件如果事件触发就把对应的事件标志填写到 revents 里面。完成一轮遍历之后如果存在就绪的 fdpoll 就返回就绪 fd 的数量如果没有任何 fd 就绪进程就阻塞等待直到事件到来或者超时时间到达。poll 系统调用返回用户态之后用户代码依旧需要遍历完整的 pollfd 数组查看每一项里的 revents从中找出已经就绪的 fd再去做业务处理。这里还有一个很关键的缺点无论就绪的 fd 有几个每次调用 poll内核都需要遍历全部传入的 fd而且用户态拿到返回结果之后同样需要遍历整个 pollfd 数组挨个检查 revents当需要监听的 fd 数量很大时两次全量遍历会带来很大的性能开销这也是 select 和 poll 共有的问题。2.3 poll的特点和缺点poll 特点1.不再受fd 上限 1024限制select 受FD_SETSIZE限制2.events和revents分离不用每次调用前重新重置监听集合select 每次都要重置 fd_set3.用结构体数组不需要位图添加删除 fd 更直观。poll 缺点1.每次 poll 返回后仍然需要遍历整个数组查找就绪 fd2.内核每次调用 poll都要把整个fds数组从用户态拷贝到内核态3.当监听 fd 数量非常巨大时性能开销会变大。2.4 poll使用示例#include stdio.h #include unistd.h #include poll.h int main() { struct pollfd fds[10]; int nfds 0; // 添加监听fd fds[nfds].fd listen_fd; fds[nfds].events POLLIN; nfds; while(1) { // nfds告诉内核数组有效元素个数输入参数 int ret poll(fds, nfds, -1); if(ret 0) continue; // 遍历数组检查revents for(int i 0; i nfds; i) { if(fds[i].revents POLLIN) { if(fds[i].fd listen_fd) { // 接收新连接 int cfd accept(listen_fd, NULL, NULL); fds[nfds].fd cfd; fds[nfds].events POLLIN; nfds; } else { char buf[1024]; int n read(fds[i].fd, buf, sizeof(buf)); if(n 0) { close(fds[i].fd); fds[i] fds[nfds - 1]; nfds--; i--; } } } } } return 0; }

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑