当前位置:首页>Linux>Linux select /poll 完整对比(非阻塞 IO 场景)

Linux select /poll 完整对比(非阻塞 IO 场景)

  • 2026-10-11 06:39:47
Linux select /poll 完整对比(非阻塞 IO 场景)

一、基础概念

二者都属于 IO 多路复用,作用:同时监听多个文件描述符 (fd),阻塞等待其中任意 fd 可读 / 可写 / 异常,避免单线程轮询所有 fd 浪费 CPU;常搭配 O_NONBLOCK 非阻塞 IO 使用,二者是两套独立机制:

  1. O_NONBLOCK:单个 fd 读写不阻塞,无数据直接返回 -1, EAGAIN;
  2. select/poll:批量等待一批 fd 事件,不用循环逐个轮询。

二、select 详解

函数原型

#include<sys/select.h>intselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数:

  1. nfds:所有 fd 最大值 + 1;
  2. readfds/writefds/exceptfds:读、写、异常 fd 集合(位图 fd_set);
  3. timeout:超时时间,NULL 永久阻塞,0 非阻塞轮询。
fd_set 操作宏
FD_ZERO(&set);    // 清空集合FD_SET(fd, &set); // 添加fdFD_CLR(fd, &set); // 移除fdFD_ISSET(fd, &set); // 判断fd是否就绪

工作流程(非阻塞搭配示例)

  1. 打开文件 /socket 设置 fcntl(fd, F_SETFL, O_NONBLOCK);
  2. 每次调用 select 前重置 fd_set;
  3. select 阻塞等待事件;
  4. 遍历所有 fd,FD_ISSET 判断就绪,执行非阻塞 read/write;

select 致命缺陷

  1. fd 上限固定:默认 FD_SETSIZE=1024,超过 1024 必须改内核重新编译,高并发不适用;
  2. 每次调用重置集合:内核会修改 fd_set,用户空间每次都要重新填充所有 fd;
  3. 遍历开销大:返回后必须循环 0~nfds-1 逐个检查,fd 越多越慢;
  4. 仅支持水平触发(LT)。

三、poll 详解

函数原型

#include<poll.h>intpoll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {    int   fd;         // 待监听fd    short events;     // 期望监听事件 POLLIN/POLLOUT/POLLERR    short revents;    // 内核返回就绪事件};

常用事件:

  • POLLIN:可读(socket 有数据、管道可读)
  • POLLOUT:可写(缓冲区未满)
  • POLLERR:错误
  • POLLHUP:断开

参数 timeout:

  • -1:永久阻塞
  • 0:立即返回,纯非阻塞轮询
  • >0:毫秒级超时阻塞

流程

  1. fd 设置 O_NONBLOCK;
  2. 维护 pollfd 数组,填入 fd 与监听事件;
  3. 调用 poll ();
  4. 遍历数组,判断 revents 是否有事件;

poll 对比 select 的改进

  1. 无 1024fd 硬限制,仅受进程最大文件句柄 
  2. ulimit -n 约束;区分 events/revents,调用时不用重复重建 fd 集合;
    1. 不用计算最大 fd+1,数组长度直接传入;
    1. API 更直观,不需要一堆 FD_XXX 宏;

poll 仍存在的短板

  1. 每次调用都要把整个 pollfd 数组拷贝进内核;
  2. 返回后仍需遍历全部数组找就绪 fd;高并发上万 fd 场景性能依然差,需要 epoll。
四、select vs poll 核心差异表
对比项
select
poll
fd 上限
固定 1024,不可动态扩展
仅受 ulimit 限制,无硬编码上限
数据结构
fd_set 位图
动态数组 struct pollfd
入参处理
每次调用都会被内核改写,必须重新填充
events 不变,revents 存储结果,无需重填
查找就绪 fd
循环 0~maxfd 全部遍历
只遍历传入的 pollfd 数组
精度
timeval 微秒超时
int 毫秒超时
触发模式
仅水平触发 LT
仅水平触发 LT
性能 (大量 fd)
差
中等,优于 select,弱于 epoll

五、水平触发 LT(select/poll 唯一模式)+ 非阻塞 IO 关键逻辑

select/poll 都是水平触发:只要缓冲区有未读数据,每次调用都会持续返回就绪事件。必须搭配 O_NONBLOCK,否则会卡死:

  1. 若 fd 阻塞,一次读不完缓冲区数据,select 会持续唤醒,重复处理同一批数据,死循环;
  2. 设置 O_NONBLOCK 后,循环 read 直到返回 EAGAIN/EWOULDBLOCK,清空缓冲区。
标准处理模板(poll 示例)
// 读数据循环清空缓冲区staticvoidhandle_read(int fd){    char buf[256];    ssize_t n;    while(1)    {        n = read(fd, buf, sizeof(buf));        if(n > 0)        {            // 处理数据        }        else if(n == 0)        {            // 对端关闭            close(fd);            return;        }        else        {            // EAGAIN,缓冲区空,退出循环            break;        }    }}
六、纯非阻塞轮询用法(不阻塞,立即返回)
select 非阻塞轮询
struct timeval tv = {0, 0}; // 超时0,不阻塞int ret = select(maxfd+1, &rfds, NULL, NULL, &tv);
poll 非阻塞轮询
int ret = poll(fds, cnt, 0); // timeout=0
适用场景:主线程既要监听 IO,又要做其他业务逻辑,不卡住循环。

七、适用场景选择

  1. fd 数量 < 100,简单小程序、嵌入式少量串口 /socket:select/poll 均可,select 兼容性更好;
  2. fd 数量 100~1000,Linux 服务器、多路采集(4-20mA、W5500 多套接字):优先 poll,无 1024 限制;
  3. fd 上万、高并发网络服务:放弃 select/poll,改用 epoll。

八、常见踩坑点

  1. select 忘记 nfds = 最大fd + 1,导致部分 fd 监听失效;
  2. select 每次循环忘记 FD_ZERO/FD_SET,集合错乱;
  3. 不设置 O_NONBLOCK,LT 模式下未读完数据造成 CPU 空转死循环;
  4. poll 数组增减 fd 时未同步维护,出现无效 fd;
  5. 混淆阻塞 / 非阻塞超时参数:timeout=0 是轮询,-1 永久阻塞。

最新文章

随机文章