使用 io_uring 构建比 cp 快 4 倍的文件复制器
发布: (2026年1月8日 GMT+8 01:45)
4 min read
原文: Dev.to
Source: Dev.to
介绍
我使用 Linux io_uring 构建了一个高性能的机器学习数据集文件复制器。在合适的工作负载下,它比 cp -r 快 4.2 倍。下面是关于何时异步 I/O 有帮助——以及何时没有帮助的经验教训。
典型机器学习数据集规模
| 数据集 | 文件数 | 典型大小 |
|---|---|---|
| ImageNet | 1.28 M | 100–200 KB JPEG |
| COCO | 330 K | 50–500 KB |
| MNIST | 70 K | 784 字节 |
| CIFAR‑10 | 60 K | 3 KB |
使用 cp -r 复制这些文件非常慢,因为每个文件需要多个系统调用(open、read、write、close),内核会顺序处理。对于 100 000 个文件,这意味着 400 000+ 系统调用 依次执行。
为什么 io_uring 有帮助
- 批量提交 – 将数十个操作排队,并通过一次系统调用提交。
- 异步完成 – 操作可以无序完成,使 CPU 能持续工作。
- 零拷贝 – 通过内核管道直接在文件描述符之间 splice 数据,避免用户空间缓冲区。
Instead of:
open → read → write → close → repeat
we do:
submit 64 opens → process completions → submit reads/writes → batch everything
架构概览
┌──────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│ Main Thread │────▶│ WorkQueue │────▶│ Worker Threads │
│ (scanner) │ │ (thread‑safe)│ │ (per‑thread uring) │
└──────────────┘ └─────────────────┘ └─────────────────────┘
每个文件在状态机中流转:
OPENING_SRC → STATING → OPENING_DST → SPLICE_IN ⇄ SPLICE_OUT → CLOSING
关键设计决策
- 每个工作线程同时处理 64 个在途文件。
- 每线程的 io_uring 实例(避免锁竞争)。
- inode 排序 以实现顺序磁盘访问。
- splice 零拷贝 用于数据传输(
source → pipe → destination)。 - 缓冲池 使用 4 KB 对齐的分配(兼容
O_DIRECT)。
基准测试
NVMe(快速本地存储)
| 工作负载 | cp -r | uring‑sync | 加速比 |
|---|---|---|---|
| 100 K × 4 KB 文件 (400 MB) | 7.67 s | 5.14 s | 1.5× |
| 100 K × 100 KB 文件 (10 GB) | 22.7 s | 5.4 s | 4.2× |
云 SSD(例如 GCP Compute Engine)
| 工作负载 | cp -r | uring‑sync | 加速比 |
|---|---|---|---|
| 100 K × 4 KB 文件 | 67.7 s | 31.5 s | 2.15× |
| 100 K × 100 KB 文件 | 139.6 s | 64.7 s | 2.16× |
较大的文件在快速存储上使用 io_uring 能获得更大的收益,因为 CPU 花在等待 I/O 的时间更少,而更多时间用于重叠操作。
File‑Copy State Machine (C++)
enum class FileState {
OPENING_SRC, // Opening source file
STATING, // Getting file size
OPENING_DST, // Creating destination
SPLICE_IN, // Reading into kernel pipe
SPLICE_OUT, // Writing from pipe to dest
CLOSING_SRC, // Closing source
CLOSING_DST, // Closing destination
DONE
};
完成操作驱动状态转换:当一个完成事件到达时,会查找相应的文件上下文并推进其状态。
零拷贝使用 splice
// Splice from source into pipe
io_uring_prep_splice(sqe, src_fd, offset,
pipe_write_fd, -1,
chunk_size, 0);
// Splice from pipe to destination
io_uring_prep_splice(sqe, pipe_read_fd, -1,
dst_fd, offset,
chunk_size, 0);
数据从不进入用户空间;内核直接在文件描述符之间移动页面。
索引节点排序
std::sort(files.begin(), files.end(),
[](const auto& a, const auto& b) { return a.inode
基准测试在 Ubuntu 24.04(内核 6.14)上使用本地 NVMe 硬盘以及 GCP Compute Engine 虚拟机进行。