第一部分:必要的背景知识
在讲 io_uring 之前,我们必须先搞懂:我们的程序到底为什么慢? 搞不懂这个,就看不懂 io_uring 的价值。
1. 什么是"用户态"和"内核态"?(快递大厅 vs 核心仓库)
想象一下,你的电脑是一个巨大的快递公司。
-
内核态 (Kernel Mode): 这里是核心仓库,存放着所有硬件资源(网卡、硬盘、内存)。只有拥有最高权限的管理员(内核)才能进出,普通人严禁入内。
-
用户态 (User Mode): 这里是客户大厅。你的程序(比如你写的 Python 脚本或 C 代码)就是客户,只能坐在大厅里填单子。
2. 什么是"系统调用 (Syscall)"?(填单子窗口)
你的程序(客户)想要读取硬盘里的一个文件(从仓库取货)。 因为你不能进仓库,你必须:
-
走到柜台窗口。
-
填一张申请单(调用
read函数)。 -
把单子递给窗口里的管理员。
-
管理员停下子手里的活,接过单子,转身走进仓库,找到货,搬出来,递给你。
这个过程,就叫系统调用 。 痛点: 每次都要排队、填单、递单。如果你的程序要读 1000 个文件,就要在窗口来回跑 1000 次。这非常浪费时间。
3. Spectre/Meltdown 漏洞是什么?为什么它让电脑变慢了?
大概在 2018 年,英特尔等 CPU 爆出了严重的"幽灵 (Spectre)"和"熔断 (Meltdown)"漏洞。黑客可以利用 CPU 的设计缺陷,在"用户态"偷窥"内核态"的机密数据。
为了修补这个漏洞,操作系统做了一件事:加厚安检。
-
以前(补丁前): 客户递单子给管理员,管理员扫一眼就进仓库了。
-
现在(补丁后): 客户递单子,必须先经过一道繁琐的安检门,隔离措施做得非常严,防止你偷看。
后果: 每一次"系统调用"(每一次从用户态切换到内核态)的成本变高了。以前进出一趟耗时 1 份,现在可能要耗时 1.5 份甚至 2 份。 这对 epoll 打击很大 ,因为 epoll 需要频繁地调用系统接口。所以,我们需要一种减少进出窗口次数的技术。
4. 什么是"数据拷贝"?
管理员从仓库把货物(数据)搬出来,放在了柜台上(内核缓冲区)。 但是,你不能直接在柜台上用,你必须把货物从柜台搬回你自己的座位上 (用户缓冲区),这叫 Memory Copy。 货物搬来搬去,累死人。
第二部分:io_uring 之前的世界
为了解决慢的问题,Linux 经历了几代进化:
阶段一:同步阻塞 I/O(傻等)
你递交了申请单,然后你就站在窗口死等。管理员没出来之前,你哪儿也不去,电话也不接。
- 缺点: 效率极低,一个人一次只能办一件事。
阶段二:Epoll(BB机 / 呼叫器)
你把单子递进去,然后拿着一个"呼叫器"回座位玩手机。管理员找到货了,按一下呼叫器,你再跑去窗口拿。
-
优点: 你可以同时等待 1000 个包裹。
-
缺点: 你还是得去窗口(系统调用),而且货物还得从柜台搬到你座位上(数据拷贝)。
第三部分:主角登场 ------ io_uring 是什么?
到了 Linux 5.1 版本,工程师们想通了:既然"窗口"和"搬运"这么慢,我们干脆把窗口砸了!
io_uring 的核心思想是:共享内存(Shared Memory)。
1. 它是如何工作的?(拆掉柜台)
现在,快递大厅和核心仓库之间,打通了一张桌子。 这张桌子一半在仓库里(内核能看),一半在大厅里(你能看)。
-
不需要填单子递进去。
-
不需要管理员递出来。
-
大家直接往桌子上放东西!
2. 两个环形队列(桌上的两个转盘)
为了不乱套,这张桌子上放了两个旋转的大盘子(这就是你之前问的环形队列):
-
左边的盘子叫 SQ (Submission Queue - 提交队列):
-
你的任务: 你把要干的活(读文件、发数据)写在便签上,贴在这个盘子里。
-
内核的任务: 内核看到盘子里有便签,直接拿走去干活。
-
生产者是你,消费者是内核。
-
-
右边的盘子叫 CQ (Completion Queue - 完成队列):
-
内核的任务: 内核干完活了,把结果("成功"或"失败")写在便签上,贴在这个盘子里。
-
你的任务: 你有空就来看看这个盘子,把结果取走。
-
生产者是内核,消费者是你。
-
3. 为什么这就能"起飞"?
还记得之前的痛点吗?
-
解决"Spectre/Meltdown 导致系统调用变慢": 在最极致的模式下(后面会讲),你贴便签,内核拿便签。大家都在看同一张桌子,中间没有任何"窗口交接"的动作。没有系统调用,就没有安检,自然就不受漏洞补丁的影响!
-
解决"数据拷贝": 这个桌子(环形队列)本身就是通过
mmap技术映射的。简单说,你看到的内存地址,和内核看到的物理内存地址,是同一块地。就像你们在看同一张纸,不需要复印一份给你。
3.1. 核心解密:其实是"两个人"在同时干活
你之所以觉得需要切换,是因为你潜意识里认为只有一个 CPU 在干活:一会儿切成用户态干活,一会儿切成内核态干活。
但在高性能网络编程(io_uring + SQPOLL)中,真实的场景是这样的:
我们有 CPU 1 和 CPU 2 两个核心。
-
CPU 1(跑你的程序):
-
身份: 用户态。
-
动作: 往环形队列里写数据。
-
状态: 始终穿着便服(用户态),从未 换过装,也从未暂停。
-
-
CPU 2(跑内核线程
io_uring-sq):-
身份: 内核态。
-
动作: 盯着环形队列看,看到有数据就拿走去处理。
-
状态: 始终穿着制服(内核态),从未 脱过装,也从未离开过内核。
-
看到区别了吗?
-
用户态的动作发生在 CPU 1 上。
-
内核态的动作发生在 CPU 2 上。
-
它们是"并行"的(Parallel),而不是"交替"的(Concurrent)。
3.2. 它们怎么沟通?(那块神奇的玻璃)
你可能会问:"两个 CPU 隔这么远,CPU 2 怎么知道 CPU 1 写了数据?"
答案就是 共享内存(那块被 mmap 的内存区域)。
想象 CPU 1 和 CPU 2 中间隔了一层透明的玻璃(这就是共享内存):
-
CPU 1 在玻璃这边写了一行字:"读文件 A"。(这是写内存操作,不需要特权,用户态就能做)。
-
CPU 2 在玻璃那边一直盯着看。它可以透过玻璃直接看到这行字。(这是读内存操作)。
-
CPU 2 看到字后,直接转身去读文件。
在这个过程中:
-
CPU 1 没有翻过玻璃墙(没有切换到内核态)。
-
CPU 2 也没有翻过玻璃墙(没有切换到用户态)。
-
数据(那行字)穿过了墙,但人(执行流)没有穿过墙。
第四部分:io_uring 的三种超能力模式
io_uring 厉害在它有三个档位,就像跑车有 舒适模式、运动模式、赛道模式。
模式 1:普通模式 (Default)
-
操作: 你贴好便签(放入 SQ),然后按一下桌上的铃铛(调用
io_uring_enter),告诉管理员"来活了"。 -
解析: 这里还有一次"按铃铛"的系统调用。但好处是,你可以贴 100 张便签,只按一次铃。批量处理 ,效率依然比
epoll高。
模式 2:IOPOLL 模式
- 操作: 专门针对文件读写的优化。管理员会更积极地查水表,减少等待时间。
模式 3:SQPOLL 模式(神之模式 / 赛道模式)
-
这是 io_uring 封神的理由。
-
操作: 你告诉内核:"请专门派一个管理员,死死盯着左边的盘子(SQ),绝对不要眨眼。"
-
效果:
-
你要发数据?直接往盘子里一贴。
-
管理员(内核线程)毫秒级发现,直接拿走处理。
-
全程不需要按铃铛!全程没有系统调用!
-
你的程序和内核就像两个配合默契的流水线工人,一个放,一个拿,没有任何废话。
-
第五部分:总结
理解 io_uring 的意义在于理解极限。
-
它是未来的标准: 随着网卡速度越来越快(100G 网卡),CPU 的处理速度已经跟不上了。传统的 I/O 方式注定被淘汰。新的高性能数据库、Web 服务器都在往
io_uring迁移。 -
它是"真"异步: Linux 以前没有真正的异步文件 I/O,
io_uring补上了这一课。
一句话总结 io_uring: 它通过两个环形队列 和共享内存,拆掉了用户态和内核态之间的"安检门"和"传输带",让数据像水流一样在程序和硬件之间无缝流转。