BlueStore硬盘IO调用链分析

BlueStore硬盘IO调用链分析

目录

  1. 问题定义
  2. 总体结论
  3. BlueStore如何发起读写
  4. BlockDevice如何选择后端
  5. 内核AIO路径
  6. SPDK路径
  7. 回调与完成通知
  8. 读写调用链小结

问题定义

本文回答的问题是:

  • BlueStore如何调用aio接口读写硬盘
  • BlueStore如何调用SPDK接口读写硬盘
  • 这两条路径分别在代码中的哪些函数里发生

这里的重点不是 BlueStore 的完整对象模型,而是 从 BlueStore 发起一次磁盘读写,到最终落到内核 AIO 或 SPDK NVMe 命令 的调用链。


总体结论

BlueStore 自己并不直接调用 libaioio_uringspdk_nvme_ns_cmd_readv/writev

它采用的是一层统一抽象:

1
2
3
4
BlueStore
-> BlockDevice 抽象接口
-> KernelDevice(内核 AIO / io_uring)
-> NVMEDevice(SPDK)

也就是说:

  • BlueStore 只调用 bdev->aio_read() / bdev->aio_write() / bdev->aio_submit()
  • 真正决定走内核后端还是 SPDK 后端的是 BlockDevice::create()
  • 真正下发到底层设备的是 KernelDeviceNVMEDevice

BlueStore如何发起读写

1. 打开块设备时创建 bdev

BlueStore 在 _open_bdev() 中创建主块设备对象:

1
2
3
4
5
6
7
8
bdev = BlockDevice::create(
cct,
p,
aio_cb,
static_cast<void*>(this),
discard_cb,
static_cast<void*>(this),
"bluestore");

这里有两个关键信息:

  • bdev 的静态类型是 BlockDevice*
  • 创建时传入了完成回调 aio_cb

因此 BlueStore 后续读写都只需要调用 bdev 抽象接口,而不用关心底层到底是内核设备还是 SPDK 设备。

2. 读路径如何发起

BlueStore 在 _do_read() 中,会把需要从块设备读取的物理区间收集起来,然后调用:

  • bdev->aio_read(...)
  • bdev->aio_submit(&ioc)
  • ioc.aio_wait()

这表示:

  1. 先把一个或多个读请求挂入 IOContext
  2. 再统一提交
  3. 然后等待完成

3. 写路径如何发起

写路径也一样。

BlueStore 不会每产生一个小写就立刻自己直接打到底层,而是:

  1. 先调用 bdev->aio_write(...) 把 IO 挂入当前事务的 IOContext
  2. 等事务状态机推进到需要下发数据 IO 时
  3. 再由 _txc_aio_submit() 调用 bdev->aio_submit(&txc->ioc)

所以 BlueStore 的设计是:

  • 读路径:边组装边挂请求,最后统一 submit
  • 写路径:事务收集写 IO,状态机推进时统一 submit

BlockDevice如何选择后端

1. 入口函数

设备类型选择发生在:

  • BlockDevice::create()
  • BlockDevice::create_with_type()

2. 选择逻辑

BlockDevice::create() 会先看配置项 bdev_type

  • 如果用户显式配置了 aio,就走内核设备
  • 如果显式配置了 spdk,就走 SPDK 设备
  • 如果没有显式配置,则自动探测

自动探测逻辑是:

  • 如果路径支持 NVMEDevice::support(path),则走 spdk
  • 否则默认走 aio

3. 分派结果

最终在 create_with_type() 中实例化具体对象:

  • KernelDevice
  • NVMEDevice
  • PMEMDevice

对于本文主题,重点是前两者:

  • KernelDevice:传统内核块设备路径
  • NVMEDevice:SPDK NVMe 直通路径

内核AIO路径

1. 具体实现类

内核后端由 KernelDevice 实现,文件在:

  • src/blk/kernel/KernelDevice.cc
  • src/blk/kernel/KernelDevice.h

2. aio_write() 做什么

KernelDevice::aio_write() 的核心动作是:

  1. 检查 IO 对齐
  2. 必要时把 bufferlist 重建成满足 direct I/O 对齐要求的形式
  3. 生成 aio_t
  4. 把请求挂到 ioc->pending_aios

也就是说,aio_write() 本身通常只是“登记请求”,不是立刻提交。

3. aio_read() 做什么

KernelDevice::aio_read() 逻辑类似:

  1. 分配对齐缓冲
  2. 构造 aio_t
  3. 调用 aio.preadv(off, len)
  4. 把它挂到 IOContext::pending_aios

4. aio_submit() 做什么

KernelDevice::aio_submit() 才是真正提交批量 IO 的地方。

它会:

  1. pending_aios 挪到 running_aios
  2. 增加 num_running
  3. 调用 io_queue->submit_batch(...)

这里的 io_queue 有两种实现:

  • ioring_queue_t
  • aio_queue_t

初始化时如果系统支持且配置启用 bdev_ioring,优先用 io_uring
否则回退到传统 libaio

所以“内核 AIO 路径”实际上又分成:

  • io_uring 路径
  • libaio 路径

但对 BlueStore 来说,这两者都被 KernelDevice 屏蔽了。

5. 完成通知

内核设备有自己的 AIO 处理线程 _aio_thread()

底层提交后,完成事件由内核队列返回,KernelDevice 会:

  1. 回收完成的 aio_t
  2. 更新 IOContext
  3. 在适当时机唤醒等待者
  4. 通过创建时注册的回调把完成事件交回上层

因此 BlueStore 并不自己轮询内核完成队列,而是由 KernelDevice 负责。


SPDK路径

1. 具体实现类

SPDK 后端由 NVMEDevice 实现,文件在:

  • src/blk/spdk/NVMEDevice.cc
  • src/blk/spdk/NVMEDevice.h

2. aio_write() 做什么

NVMEDevice::aio_write() 并不直接调用 SPDK 写命令。

它会先调用 write_split(...),把大写请求拆成若干个 Task

  • 每个 Task 记录偏移、长度、命令类型
  • 再把这些 Task 追加到 IOContext

这一步本质上是“把写请求转换成 SPDK 可提交的任务链表”。

3. aio_read() 做什么

NVMEDevice::aio_read() 也类似。

它会:

  1. 准备目标缓冲
  2. 调用 make_read_tasks(...)
  3. 把读请求拆成多个 Task
  4. 追加进 IOContext

4. aio_submit() 做什么

NVMEDevice::aio_submit() 会:

  1. IOContext 里取出挂好的任务链
  2. num_pending 转成 num_running
  3. 创建或复用当前线程的 SharedDriverQueueData
  4. 调用 queue_t._aio_handle(t, ioc)

5. _aio_handle() 如何真正调用 SPDK

SharedDriverQueueData::_aio_handle() 是 SPDK 路径的核心。

对每个任务:

  • 写命令调用 spdk_nvme_ns_cmd_writev(...)
  • 读命令调用 spdk_nvme_ns_cmd_readv(...)
  • flush 调用 spdk_nvme_ns_cmd_flush(...)

这就是 BlueStore 最终触发 SPDK 读写硬盘的真正位置。

6. SPDK完成处理方式

SPDK 路径不是依赖内核 io_getevents,而是用户态轮询:

  • spdk_nvme_qpair_process_completions(...)

_aio_handle() 在循环里持续轮询 completions:

  1. 如果队列里有在飞 IO,就处理 completions
  2. 如果没有完成,就按配置 sleep 很短时间
  3. 完成后由 SPDK completion callback 回调 io_complete(...)

所以 SPDK 路径的特征是:

  • 用户态 qpair
  • 用户态提交
  • 用户态 polling completion

回调与完成通知

BlueStore 在创建 BlockDevice 时传了:

  • aio_cb
  • discard_cb

其中 aio_cb 的作用是把设备层完成通知重新送回 BlueStore:

  1. 设备层完成某个 AIO
  2. 调用注册回调
  3. BlueStore 的 AioContext::aio_finish(store) 被触发
  4. 事务状态机继续向前推进

因此:

  • BlueStore 不直接处理内核完成队列
  • BlueStore 也不直接处理 SPDK completion queue
  • 这些都先由具体设备实现处理,再通过回调交回 BlueStore

读写调用链小结

1. 读路径

1
2
3
4
5
6
7
8
9
10
BlueStore::_do_read()
-> bdev->aio_read(...)
-> bdev->aio_submit(&ioc)
-> KernelDevice::aio_submit()
-> io_uring/libaio submit

-> NVMEDevice::aio_submit()
-> spdk_nvme_ns_cmd_readv()
-> ioc.aio_wait()
-> aio_cb -> BlueStore::AioContext::aio_finish()

2. 写路径

1
2
3
4
5
6
7
8
9
10
11
BlueStore::_write() / _do_write() / Writer.cc
-> bdev->aio_write(...)
-> BlueStore::_txc_aio_submit()
-> bdev->aio_submit(&txc->ioc)
-> KernelDevice::aio_submit()
-> io_uring/libaio submit

-> NVMEDevice::aio_submit()
-> spdk_nvme_ns_cmd_writev()
-> 完成回调 aio_cb
-> 事务状态机继续推进

3. 一句话总结

BlueStore 的关键点只有一句话:

BlueStore 只面向 BlockDevice 抽象编程,而 KernelDeviceNVMEDevice 分别把同一套 aio_read/aio_write/aio_submit 接口落到内核 AIO 与 SPDK NVMe 命令。


附:关键源码位置

  • src/os/bluestore/BlueStore.cc
    • _open_bdev()
    • _do_read()
    • _txc_aio_submit()
  • src/blk/BlockDevice.cc
    • BlockDevice::create()
    • BlockDevice::create_with_type()
  • src/blk/BlockDevice.h
    • BlockDevice::aio_read()
    • BlockDevice::aio_write()
    • BlockDevice::aio_submit()
  • src/blk/kernel/KernelDevice.cc
    • KernelDevice::aio_read()
    • KernelDevice::aio_write()
    • KernelDevice::aio_submit()
    • _aio_thread()
  • src/blk/spdk/NVMEDevice.cc
    • NVMEDevice::aio_read()
    • NVMEDevice::aio_write()
    • NVMEDevice::aio_submit()
    • SharedDriverQueueData::_aio_handle()

文章互动

阅读 --

留言

0 条留言

正在加载留言…