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

Tokio io_uring 支持:异步 I/O 在 Linux 上的新路径

Tokio io_uring 支持:异步 I/O 在 Linux 上的新路径

一、epoll 挺好,但还不够

Tokio 是目前 Rust 异步生态的绝对核心。它的TcpListenerFile、定时器这些组件,让异步 I/O 在 Rust 里变得和同步 I/O 一样顺手。但如果你把它的底层引擎拆开来看,会发现 Tokio(在 Linux 上)依赖的是 epoll——一种问世于 2002 年的 I/O 多路复用系统调用。

epoll 的设计思路是用一个事件通知机制告诉应用层"这个 fd 可读了"。每次读写实际还是需要真正的系统调用——read()write()accept()——来把数据从内核空间拷贝到用户空间。在网络场景下,epoll 表现已经足够好;但在磁盘 I/O 密集的场景下,频繁的用户态与内核态切换、缓冲区拷贝,就开始成为瓶颈。

一个参考数字:在大量小文件的随机 4KB 读取场景中,epoll + 常规preadv的 IOPS 大约在 15 万到 20 万左右,而 io_uring 可以做到 80 万以上。差距不是百分之几十,而是几倍。这就是 io_uring 被引入 Linux 内核(5.1 版本)的动机——用一种全新的方式组织异步 I/O。

二、epoll 和 io_uring 走的根本不是同一条路

理解 io_uring 和 epoll 的区别,最直观的方法是看它们的数据流动路径。epoll 的流程是"事件通知 + 独立系统调用":先知道可以读了,再发一次系统调用去读。而 io_uring 把请求和完成合并进了一对共享内存环形缓冲区(Submission Queue 和 Completion Queue),应用和内核直接通过内存读写来交互,省掉了大量重复的系统调用。

sequenceDiagram participant App as "用户程序" participant Kernel as "内核" rect rgb(240, 248, 255) Note over App,Kernel: "epoll 路径 — 三次系统调用 + 一次数据拷贝" App->>Kernel: "1. epoll_wait() — 等待事件通知" Kernel-->>App: "返回可读/可写的 fd 列表" App->>Kernel: "2. read(fd, buf) — 发起读请求" Kernel->>Kernel: "从磁盘/网卡获取数据" Kernel-->>App: "拷贝数据到用户空间缓冲区" end rect rgb(255, 250, 240) Note over App,Kernel: "io_uring 路径 — 零额外系统调用 + 共享内存" App->>App: "1. 将 I/O 请求写入 SQ\n(Submission Queue — 共享内存)" App->>App: "2. 可选: io_uring_enter()\n提交一批请求(一次调用)" Kernel->>Kernel: "3. 内核直接读取 SQ\n处理所有 I/O 请求" Kernel->>App: "4. 结果写入 CQ\n(Completion Queue — 共享内存)" App->>App: "5. 用户态直接读取 CQ\n获取完成结果" end

关键的区别有两点。第一,io_uring 允许批量提交和批量完成,epoll 在每次epoll_wait返回后需要逐个read处理。第二,io_uring 可以通过IORING_SETUP_SQPOLL开启内核轮询模式——内核的一个独立线程会自动检查 SQ 队列,应用层连io_uring_enter系统调用都可以省掉。在低延迟场景下,这个模式能把 tail latency 压到稳定在几十微秒级别。

三、用 tokio-uring 替换传统 Tokio I/O

use std::fs::File; use std::io; // 添加依赖: tokio-uring = "0.5" use tokio_uring::fs::File as UringFile; /// 传统的 Tokio + epoll 方式 — 每个读请求一次系统调用 async fn read_with_epoll(path: &str) -> io::Result<Vec<u8>> { use tokio::io::AsyncReadExt; let mut file = tokio::fs::File::open(path).await?; let mut buf = Vec::new(); file.read_to_end(&mut buf).await?; Ok(buf) } /// io_uring 方式 — 零拷贝读取 async fn read_with_uring(path: &str) -> io::Result<Vec<u8>> { // 打开文件 (io_uring 兼容 O_DIRECT 和普通模式) let file = UringFile::open(path).await?; // 获取文件大小, 用于预分配缓冲区 let metadata = std::fs::metadata(path)?; let len = metadata.len() as usize; // 分配固定缓冲区 — 避免内核和用户态之间的内存拷贝 let buf = vec![0u8; len]; let (res, _file, buf) = file.read_at(buf, 0).await; // 返回所有权链 let n = res?; let mut result = buf; result.truncate(n); Ok(result) } /// 批量读取多个文件 — io_uring 的优势场景 async fn batch_read_with_uring(paths: &[&str]) -> io::Result<Vec<Vec<u8>>> { // 并行打开所有文件 let open_futures: Vec<_> = paths .iter() .map(|p| UringFile::open(p)) .collect(); let files = futures::future::join_all(open_futures) .await .into_iter() .collect::<Result<Vec<_>, _>>()?; // 并行读取所有文件 let read_futures: Vec<_> = files .iter() .map(|file| { let buf = vec![0u8; 4096]; // 简化: 假设每个文件 ≤ 4KB file.clone().read_at(buf, 0) }) .collect(); let results = futures::future::join_all(read_futures).await; results .into_iter() .map(|(res, _, buf)| res.map(|n| { let mut b = buf; b.truncate(n); b })) .collect() } fn main() -> io::Result<()> { // tokio-uring 需要在自己的 runtime 中启动 tokio_uring::start(async { let data = read_with_uring("/etc/hostname").await?; println!("读取: {} 字节", data.len()); Ok(()) }) }

需要注意的是,tokio-uring和标准的tokio不共享同一个 runtime。tokio-uring必须通过tokio_uring::start()启动它自己的运行时,因为它的底层完全绕开了 epoll,直接操作 io_uring 的 SQ 和 CQ。如果你的程序既需要网络 I/O(Tokio 擅长)又需要磁盘 I/O(io_uring 擅长),可以在 Tokio 的 runtime 中 spawn 一个单独的线程来运行 tokio-uring,两边通过 channel 交换数据。

四、io_uring 的边界——不是什么场景都该用

第一个也是最硬的限制:只支持 Linux,而且需要内核 5.1 及以上。对于需要跨平台的 Rust 项目,io_uring 不能作为唯一的 I/O 后端。如果在 macOS 或 Windows 上编译,需要回退到标准的 Tokio 异步 I/O。

第二个限制是内核版本的兼容性。Linux 5.1 引入了基础 io_uring,但很多高级特性是后续版本才加入的——比如IORING_SETUP_SQPOLL需要 5.11、固定缓冲区支持在 5.6 之后才稳定。如果你在 Docker 容器中部署,还要检查宿主机的内核版本,因为容器共享宿主内核。

第三个是复杂性。io_uring 的 API 数量远超 epoll:队列管理、固定缓冲区注册、eventfd 集成、多线程共享队列——这些概念的学习成本不低。对大多数应用来说,epoll 能搞定的场景就不要为了追求数字好看而上 io_uring。只有当你的 load 图上磁盘 I/O 成了最粗的那条线,再来考虑迁不迁移。

还有一个最近社区在讨论的问题:io_uring 的安全性。因为内核态和用户态共享了环形缓冲区,如果应用有漏洞导致 SQ/CQ 区域被污染,攻击者可能构造假的 I/O 请求让内核执行恶意操作。Linus 本人也多次在邮件列表中强调 io_uring 需要更严格的安全审查。如果你在构建一个需要高隔离性的系统,这些都值得认真评估。

五、总结

io_uring 不是在替代 epoll,而是在解决 epoll 解决不了的那类问题——高并发磁盘 I/O、极低延迟、批量操作。对于绝大多数 Rust 后端服务来说,Tokio 默认的 epoll 后端加上spawn_blocking已经能处理大部分磁盘操作。io_uring 更像是留给那些已经把所有优化手段都试过,IOPS 图还是不够看的项目的。

但作为 Rust 开发者,理解 io_uring 是迟早的事。Linux 内核团队在 6.x 系列中持续扩大 io_uring 的操作覆盖面,未来它可能会成为 Linux 上默认的异步 I/O 原语。现在把概念搞清楚,等那个时候到来时就不会措手不及了。

http://www.cnnetsun.cn/news/3232317.html

相关文章:

  • ISC DHCP完全指南:网络自动配置的终极解决方案
  • 2025终极指南:8大网盘直链下载助手,彻底告别限速烦恼![特殊字符]
  • AI Agent频繁执行失败?5个工作流配置问题必须检查
  • 每日 AI 研究简报 · 2026-07-08
  • T156:移动类型主表
  • 段件库 (Duanjian):段言(Duan)的官方段件包管理器和注册表。
  • STM32L041C6与PAM8904构建超低功耗音频提示系统
  • BMS AFE芯片开路诊断逻辑深度解析(硬件原理+工程实现+避坑方案)
  • 【C++】set和map
  • OpenC3命令控制系统详解
  • BLS 宽度学习系统 PyTorch 实现:2种网络结构对比与增量学习代码解析
  • AI编程Agent缺的不是智商,是记忆——Mneme开源认知运行时介绍
  • DDPM 扩散模型 PyTorch 复现:MNIST 数据集 30 轮训练 Loss 降至 0.023
  • 软件团队管理中的领导风格适配
  • 国外护照翻译如何办理?很简单,只需要这三步!
  • 运行maven项目时问题——java: 错误: 不支持发行版本 5
  • 06-高级模式与实战项目——18. 实战项目二:博客系统
  • 掌握Git rebase与merge的区别
  • NLP自然语言处理实战
  • 缠论可视化分析系统:基于C++的K线结构识别与动态链接库实现
  • Mysql,使用B+树存储的索引增删改查效率(七)
  • HarmonyKit | 鸿蒙新特性应用:颜色转换 HEX↔RGB↔HSL 三色空间互转算法
  • Python 数据可视化之 Matplotlib——从基础到高级图表
  • HarmonyOS 小游戏《对战五子棋》开发第18篇 - 棋盘设计
  • HarmonyKit | 鸿蒙新特性实践:进制转换器 2/8/10/16 四进制通用互转
  • DeepSeek V3 vs Claude 3.5 Sonnet:程序员日常开发场景深度横向对比
  • SFT数据质量评估:3种自动化方法筛选千条指令数据,PPL降低20%
  • 2026抖音动图去水印免费方法,无水印保存抖音GIF技巧
  • Transformer 与 RNN/LSTM 对比:5 个维度解析长序列建模效率差异
  • YOLOv8 训练数据配置避坑:3个常见 data.yaml 错误与修复方案