当前位置: 首页 > news >正文

Linux 系统 poll 与 epoll 机制1:实现原理与应用实践

1. 引言

在 Linux 系统中,进程对文件描述符(File Descriptor,FD)的 I/O 操作(如读、写)默认是阻塞式的 —— 若 FD 未就绪(如网络连接未收到数据、磁盘文件未读取完成),进程会被挂起,直到 I/O 事件发生后才被唤醒。这种模式在单 FD 场景下简单直观,但面对多 FD 管理(如高并发服务器需同时监听上万个客户端连接)时,会出现严重的效率问题:

1. 若为每个 FD 创建独立进程 / 线程,会导致系统资源(内存、CPU 调度)耗尽;

2. 若用循环轮询所有 FD,会产生大量无效判断。​

I/O 多路复用技术应运而生,其核心思想是:让一个进程 / 线程通过一个 “监控接口”,同时监听多个 FD 的 I/O 状态;当某个 / 某些 FD 就绪(可读 / 可写 / 异常)时,再通知进程进行针对性处理。The simple way to express this: inspect first,  manipulate next.

Linux 系统提供了三种经典的 I/O 多路复用机制:select、poll和epoll。其中,poll是对select的改进,而epoll则是为解决高并发场景下poll的性能瓶颈而生,目前已成为高性能服务器(如 Nginx、Redis)的核心依赖。​

本文将重点剖析poll与epoll的实现原理,对比两者的技术差异,并结合实际应用场景说明其用法,帮助读者理解 Linux 内核如何通过这两种机制支撑高并发 I/O 场景。​

这是截取自https://static.lwn.net/images/pdf/LDD3/ch03.pdf章节的关于file_ops的poll回调的解释。经典值得反复阅读,甚至背诵。后面解释为什么这个东西重要。

unsigned int (*poll) (struct file *, struct poll_table_struct *);The poll method is the back end of three system calls:poll, epoll, and select,
all of which are used to query whether a read or write to one or more
file descriptors would block. The poll method should return a bit mask 
indicating whether non-blocking reads or writes are possible, and, 
possibly,  provide the kernel with information that can be used to
put the calling process to sleep until I/O becomes possible. 
If a driver leaves its poll method NULL, the device is assumed to
be both readable and writable without blocking.

2. poll 机制

poll机制诞生于 Linux 2.1.23 版本,其设计目标是解决select的两个核心缺陷:

一是select通过固定大小的位图(如 32 位系统下默认监听 1024 个 FD)限制 FD 数量;

二是select需在用户空间与内核空间反复拷贝 FD 集合。

poll通过动态数组替代位图,优化了 FD 数量限制,但仍未解决 “遍历所有 FD” 的效率问题,因此仅适用于中低并发场景。​

2.1 poll 的核心接口与数据结构​

2.1.1 核心系统调用​

poll的核心接口是poll()系统调用,其函数原型如下:

#include <poll.h>​
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

各参数含义如下:​

  • fds:指向struct pollfd类型的数组,每个元素对应一个需监听的 FD 及其事件类型;​

  • nfds:fds数组的长度(即需监听的 FD 总数),类型为nfds_t(本质是无符号整数);​

  • timeout:超时时间(单位:毫秒),取值规则为:​

    •  timeout > 0:若在timeout毫秒内无 FD 就绪,函数返回 0;​

    • timeout = 0:不阻塞,立即返回当前就绪的 FD 数量;​

    • timeout = -1:无限阻塞,直到至少有一个 FD 就绪或进程被信号中断。​

函数返回值:​

  • 成功:返回就绪的 FD 数量(若超时则返回 0);​

  • 失败:返回 - 1,并设置errno(如EINTR表示被信号中断,EINVAL表示参数无效)。​

2.1.2 关键数据结构struct pollfd​

poll通过struct pollfd结构体描述每个 FD 的监听需求与就绪状态,定义如下:

struct pollfd {​int fd;         // 需监听的文件描述符(若为-1,该结构体被忽略)​short events;   // 用户设置的“监听事件”(输入参数)​short revents;  // 内核返回的“就绪事件”(输出参数)​
};

其中,events和revents均为事件掩码(通过位或运算组合多个事件),常见事件类型如下:​

事件常量​

含义(针对events)​

含义(针对revents)​

POLLIN​

监听 FD “可读” 事件​

FD 已可读(如网络连接收到数据)​

POLLOUT​

监听 FD “可写” 事件​

FD 已可写(如网络连接发送缓冲区空闲)​

POLLERR​

无需设置(内核自动监听)​

FD 发生错误(如网络连接重置)​

POLLHUP​

无需设置(内核自动监听)​

FD 关联的 “挂起” 事件(如客户端断开)​

POLLPRI​

监听 FD “紧急数据可读” 事件(如 TCP 带外数据)​

FD 有紧急数据可读​

2.2 poll 的内核实现原理​

poll的实现流程可分为"用户空间发起请求、内核监听 FD 状态、进程阻塞与唤醒、返回就绪事件"四个阶段,其核心逻辑依赖 “文件系统的poll方法” 与 “等待队列” 机制。​

2.2.1 阶段 1:用户空间发起poll请求​

用户进程需先构造struct pollfd数组,明确每个 FD 的监听事件(如POLLIN),再调用poll()系统调用。此时,然后执行内核中的sys_poll()函数(poll系统调用的内核实现入口)。​

2.2.2 阶段 2:内核遍历 FD 调用poll方法​

sys_poll()的核心逻辑是遍历fds数组中的所有有效 FD(fd != -1),并通过 FD 对应的struct file结构体(内核中描述文件 / 设备的核心结构),调用该 FD 所对应file的poll方法 —— 这也解释了本文开头那个函数解释的意义:

unsigned int (*poll) (struct file *, struct poll_table_struct *);

该函数是struct file_operations(内核中描述文件操作的函数指针集合)中的一个成员,即设备驱动的poll方法。不同类型的 FD(如网络套接字、磁盘文件、串口设备)对应不同的poll方法实现,例如:​

  • 网络套接字(如 TCP socket)的poll方法由内核网络子系统实现,用于判断套接字的读写缓冲区状态;​

  • 磁盘文件的poll方法由文件系统实现,用于判断文件是否可读写(通常磁盘文件始终就绪,因内核会缓存数据);​

  • 若驱动未实现poll方法(即poll指针为NULL),内核会默认该 FD “既可读也可写”(无阻塞)。​

在调用设备poll方法时,内核会传递一个struct poll_table_struct类型的参数(简称poll_table),其作用是将当前进程注册到 FD 的等待队列中,以便 FD 就绪时唤醒进程。

2.2.3 阶段 3:进程阻塞与唤醒​

若遍历完所有 FD 后,无任何 FD 就绪,则内核会:​

  1. 将当前进程的状态从TASK_RUNNING(可运行)改为TASK_INTERRUPTIBLE(可中断睡眠);​

  2. 通过poll_table将进程添加到所有未就绪 FD 的等待队列中(每个 FD 对应一个等待队列,由设备驱动维护);​

  3. 调用调度器(schedule()),让 CPU 切换到其他可运行进程;​

  4. 此时,当前进程会从 CPU 上挂起,等待 FD 就绪或超时。​

当以下任一条件满足时,进程会被唤醒:​

  • FD 就绪:例如,网络套接字收到数据,内核会触发中断,执行中断处理函数,进而遍历该套接字的等待队列,将所有睡眠的进程唤醒;​

  • 超时:内核会启动一个定时器,超时后触发定时器回调,唤醒当前进程;​

  • 信号中断:若进程收到信号(如SIGINT),内核会唤醒进程并返回EINTR错误。​

进程被唤醒后,会重新回到TASK_RUNNING状态,并再次执行sys_poll()函数 —— 此时内核会重新遍历所有 FD,判断是否有 FD 就绪(避免 “虚假唤醒”,即唤醒后 FD 仍未就绪的情况)。​

2.2.4 阶段 4:返回就绪事件到用户空间​

若遍历后存在就绪 FD,内核会:​

  1. 对于每个就绪的 FD,将对应的revents字段设置为实际就绪的事件(如POLLIN | POLLHUP);​

  1. 统计就绪 FD 的总数,作为poll()函数的返回值;​

  1. 将 CPU 权限从内核态切换回用户态,用户进程即可通过revents字段获取就绪 FD 的信息,并进行后续 I/O 处理。​

2.3 poll 的局限性​

尽管poll解决了select的 FD 数量限制问题,但仍存在三个核心缺陷,导致其在高并发场景(如 FD 数量超过 1000)下性能急剧下降:​

  1. O (n) 遍历开销:无论 FD 是否就绪,poll每次调用都需遍历所有 FD(用户空间构造数组 + 内核空间遍历),当 FD 数量达到万级时,遍历耗时会成为性能瓶颈;​

  2. 户态与内核态数据拷贝:每次调用poll,都需将struct pollfd数组从用户空间拷贝到内核空间;返回时,又需将revents字段的状态从内核空间拷贝回用户空间,拷贝开销随 FD 数量增加而增大;​

  3. 水平触发(LT)的固有问题:poll仅支持水平触发 —— 若 FD 就绪后,用户进程未完全处理数据(如只读取了部分缓冲区数据),下次调用poll时,内核仍会再次通知该 FD 就绪,可能导致重复处理逻辑。

文章太长,分了多篇。下篇分析epool: Linux 系统 poll 与 epoll 机制2。
 

 

http://www.dtcms.com/a/355656.html

相关文章:

  • DINOv2 vs DINOv3 vs CLIP:自监督视觉模型的演进与可视化对比
  • 传统set+new写法与Builder写法的区别
  • LightRAG
  • 客户案例 | 柳钢集团×甄知科技,燕千云ITSM打造智能服务新生态
  • 第1.9节:神经网络与深度学习基础
  • 基于matplotlib库的python可视化:以北京市各区降雨量为例
  • “今年业务是去年5倍以上”,工业智能体掀热潮
  • 拉普拉斯变换求解线性常系数微分方程
  • 数字接龙(dfs)(蓝桥杯)
  • npm install 安装离线包的方法
  • 【论文阅读】健全个体无辅助运动期间可穿戴传感器双侧下肢神经机械信号的基准数据集
  • 如何打造品牌信任护城河?
  • Spark入门:从零到能跑的实战教程
  • 腾讯云重保流程详解:从预案到复盘的全周期安全防护
  • ♻️旧衣回收小程序|线上模式新升级
  • 网页爬虫的实现
  • 苹果ImageIO零日漏洞分析:攻击背景与iOS零点击漏洞历史对比
  • 2025 深度洞察!晶圆背面保护膜市场全景调研与投资机遇解析
  • 推荐一款JTools插件Crypto
  • 基于Spring Session + Redis + JWT的单点登录实现
  • Redis使用简明教程
  • SQL 查询优化全指南:从语句到架构的系统性优化策略
  • 初识分布式事务
  • week5-[一维数组]归并
  • 数据结构与算法-算法-42. 接雨水
  • AI 如何 “看见” 世界?计算机视觉(CV)的核心技术:图像识别、目标检测与语义分割
  • Scrapy 框架实战:构建高效的快看漫画分布式爬虫
  • 试验铁地板在现代工业中的应用与特性
  • AI医疗影像诊断新突破:从肺部CT结节识别到眼底病变筛查,提升疾病早诊效率
  • MTK Linux DRM分析(十四)- Mediatek KMS实现mtk_drm_drv.c(Part.2)