使用 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 有帮助——以及何时没有帮助的经验教训。

典型机器学习数据集规模

数据集文件数典型大小
ImageNet1.28 M100–200 KB JPEG
COCO330 K50–500 KB
MNIST70 K784 字节
CIFAR‑1060 K3 KB

使用 cp -r 复制这些文件非常慢,因为每个文件需要多个系统调用(openreadwriteclose),内核会顺序处理。对于 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 -ruring‑sync加速比
100 K × 4 KB 文件 (400 MB)7.67 s5.14 s1.5×
100 K × 100 KB 文件 (10 GB)22.7 s5.4 s4.2×

云 SSD(例如 GCP Compute Engine)

工作负载cp -ruring‑sync加速比
100 K × 4 KB 文件67.7 s31.5 s2.15×
100 K × 100 KB 文件139.6 s64.7 s2.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 虚拟机进行。

Back to Blog

相关文章

阅读更多 »