news 2026/7/9 2:05:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 原语。现在把概念搞清楚,等那个时候到来时就不会措手不及了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 2:04:40

ISC DHCP完全指南:网络自动配置的终极解决方案

ISC DHCP完全指南&#xff1a;网络自动配置的终极解决方案 【免费下载链接】dhcp ISC DHCP is a collection of software that implements all aspects of the DHCP (Dynamic Host Configuration Protocol). 项目地址: https://gitcode.com/openeuler/dhcp 前往项目官网…

作者头像 李华
网站建设 2026/7/9 2:00:52

AI Agent频繁执行失败?5个工作流配置问题必须检查

摘要AI Agent执行失败&#xff0c;不一定是模型能力不足&#xff0c;更常见的原因是工具参数、任务边界、上下文状态、终止条件和日志追踪没有配置完整。本文整理5个常见工作流问题&#xff0c;帮助快速定位Agent调用失败、重复执行和任务卡住的原因。很多人在搭建AI Agent后&a…

作者头像 李华
网站建设 2026/7/9 1:53:42

每日 AI 研究简报 · 2026-07-08

&#xff08;本文借助 AI 大模型及工具辅助整理&#xff09; 一句话总结&#xff1a;今天的主线是「Agent 化」全面渗透——从 Claude Cowork 上云、Notion 推出 Agent 应用&#xff0c;到 GitHub 趋势里一大波 Agent 框架/工具井喷&#xff1b;与此同时&#xff0c;GPT-5.6 获…

作者头像 李华
网站建设 2026/7/9 1:52:26

T156:移动类型主表

你可以把它理解为一张基础信息登记表。 主要作用&#xff1a;定义了所有可用的移动类型代码&#xff0c;并存储其最基本、最通用的描述信息。它是 T156SC 中 BWART 字段的检查表&#xff0c;保证了数据的合法性。T156是“户口本”。所有合法的移动类型及其基础定义&#xff0c…

作者头像 李华
网站建设 2026/7/9 1:49:30

段件库 (Duanjian):段言(Duan)的官方段件包管理器和注册表。

段件库 (Duanjian) 段言&#xff08;Duan&#xff09;的官方段件包管理器和注册表。 快速开始 # 安装段件 duanpkg install 网络请求# 搜索段件 duanpkg search 数据库# 按命名空间过滤 duanpkg search 工具 --namespace community# 按分类过滤 duanpkg list --category dev…

作者头像 李华