一、基础概念
二者都属于 IO 多路复用,作用:同时监听多个文件描述符 (fd),阻塞等待其中任意 fd 可读 / 可写 / 异常,避免单线程轮询所有 fd 浪费 CPU;常搭配 O_NONBLOCK 非阻塞 IO 使用,二者是两套独立机制:
O_NONBLOCK:单个 fd 读写不阻塞,无数据直接返回 -1, EAGAIN;select/poll:批量等待一批 fd 事件,不用循环逐个轮询。
二、select 详解
函数原型
#include<sys/select.h>intselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
nfds:所有 fd 最大值 + 1;readfds/writefds/exceptfds:读、写、异常 fd 集合(位图 fd_set);timeout:超时时间,NULL 永久阻塞,0 非阻塞轮询。
FD_ZERO(&set); // 清空集合FD_SET(fd, &set); // 添加fdFD_CLR(fd, &set); // 移除fdFD_ISSET(fd, &set); // 判断fd是否就绪
工作流程(非阻塞搭配示例)
- 打开文件 /socket 设置
fcntl(fd, F_SETFL, O_NONBLOCK); - 遍历所有 fd,
FD_ISSET 判断就绪,执行非阻塞 read/write;
select 致命缺陷
- fd 上限固定:默认 FD_SETSIZE=1024,超过 1024 必须改内核重新编译,高并发不适用;
- 每次调用重置集合:内核会修改 fd_set,用户空间每次都要重新填充所有 fd;
- 遍历开销大:返回后必须循环 0~nfds-1 逐个检查,fd 越多越慢;
三、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:毫秒级超时阻塞
流程
- 维护
pollfd 数组,填入 fd 与监听事件;
poll 对比 select 的改进
ulimit -n 约束;区分 events/revents,调用时不用重复重建 fd 集合;
poll 仍存在的短板
- 每次调用都要把整个
pollfd 数组拷贝进内核; - 返回后仍需遍历全部数组找就绪 fd;高并发上万 fd 场景性能依然差,需要 epoll。
| | |
|---|
| | |
| | |
| | events 不变,revents 存储结果,无需重填 |
| | |
| | |
| | |
| | |
五、水平触发 LT(select/poll 唯一模式)+ 非阻塞 IO 关键逻辑
select/poll 都是水平触发:只要缓冲区有未读数据,每次调用都会持续返回就绪事件。必须搭配 O_NONBLOCK,否则会卡死:
- 若 fd 阻塞,一次读不完缓冲区数据,select 会持续唤醒,重复处理同一批数据,死循环;
- 设置 O_NONBLOCK 后,循环 read 直到返回
EAGAIN/EWOULDBLOCK,清空缓冲区。
// 读数据循环清空缓冲区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; } }}
struct timeval tv = {0, 0}; // 超时0,不阻塞int ret = select(maxfd+1, &rfds, NULL, NULL, &tv);
int ret = poll(fds, cnt, 0); // timeout=0
适用场景:主线程既要监听 IO,又要做其他业务逻辑,不卡住循环。七、适用场景选择
- fd 数量 < 100,简单小程序、嵌入式少量串口 /socket:select/poll 均可,select 兼容性更好;
- fd 数量 100~1000,Linux 服务器、多路采集(4-20mA、W5500 多套接字):优先 poll,无 1024 限制;
- fd 上万、高并发网络服务:放弃 select/poll,改用 epoll。
八、常见踩坑点
- select 忘记
nfds = 最大fd + 1,导致部分 fd 监听失效; - select 每次循环忘记 FD_ZERO/FD_SET,集合错乱;
- 不设置 O_NONBLOCK,LT 模式下未读完数据造成 CPU 空转死循环;
- poll 数组增减 fd 时未同步维护,出现无效 fd;
- 混淆阻塞 / 非阻塞超时参数:timeout=0 是轮询,-1 永久阻塞。