---
title: 块设备层
url: https://doc.liz6.com/linux-kernel/07-block-device-layer/01-block-device-layer
locale: zh
area: linux-kernel
tags:
- linux-kernel
- 块设备层
date: 2026-06-30
modified: 2026-07-11
description: '覆盖: bio → request → plug/unplug → blk-mq (multi-queue) → IO scheduler (mq-deadline/kyber/bfq) → blktrace/blkparse 调试 内核版本: 2.6 ~ 6.x，重点标注 blk-mq 重构 (3.13~5.x)'
---

# 块设备层

> 覆盖: bio → request → plug/unplug → blk-mq (multi-queue) → IO scheduler (mq-deadline/kyber/bfq) → blktrace/blkparse 调试
> 内核版本: 2.6 ~ 6.x，重点标注 blk-mq 重构 (3.13~5.x)

## 概述

块层是文件系统和设备驱动之间的转换层。它的输入是文件系统提交的 `bio`（"读第 N 个扇区，放到这些页面，完成后通知我"），输出是发给设备驱动的 `request`。在这之间，块层做合并、排序、调度，目标是最小化 IO 延迟的同时最大化吞吐。

从 2.6 的单请求队列到 5.x 的多队列 (blk-mq)，Linux 块层经历了一次完整重写——驱动模型从"一个设备一个队列一把锁"变成了"per-CPU 提交队列 + 多个硬件队列"。

---

## bio: 块 IO 的原子单元

```c
// include/linux/blk_types.h
struct bio {
    struct block_device *bi_bdev;        // 目标块设备
    sector_t            bi_sector;       // 起始扇区 (512B 单位)
    unsigned int        bi_size;         // 剩余 IO 字节数
    unsigned int        bi_status;       // 完成状态 (BLK_STS_OK/error)

    struct bio_vec      *bi_io_vec;      // page vector (散列-聚集列表)
    unsigned short      bi_vcnt;         // vector 条目数
    unsigned short      bi_max_vecs;

    bio_end_io_t        *bi_end_io;      // 完成回调
    void                *bi_private;     // 回调上下文

    unsigned short      bi_write_hint;   // IO 优先级提示
    unsigned short      bi_ioprio;
};
```

### bio_vec: 描述一个内存页上的 IO

```c
// include/linux/bvec.h
struct bio_vec {
    struct page     *bv_page;    // 目标页面
    unsigned int    bv_len;      // 此段长度
    unsigned int    bv_offset;   // 页内偏移
};
```

一个 bio 可以包含多个 `bio_vec`——这就是 scatter-gather (散列-聚集) IO。文件系统可以将非连续的内存页面打包成一次 IO，块层和驱动再拆分为设备可接受的大小。

### bio 提交路径

```c
// block/blk-core.c
void submit_bio(struct bio *bio) {
    // 1. 检查是否 using blk-cgroup → account to cgroup
    // 2. 检查 bio 大小是否超过设备限制 → split if needed
    // 3. 调用 submit_bio_noacct() → __submit_bio_noacct_mq()
    //    → blk_mq_submit_bio()  (blk-mq 路径，当前默认)
}
```

---

## blk-mq: 多队列架构

### 为什么从 blk-sq 迁移到 blk-mq

```
blk-sq (单队列, 2.6 ~ 3.x):
  struct request_queue *q;  ← 一把大锁保护所有提交
  所有 CPU 竞争 q->queue_lock → 多核扩展瓶颈
  IO scheduler 的单线程模型 → 高 IOPS 设备 (NVMe) 触顶

blk-mq (3.13+, 5.x 完全替代 blk-sq):
  两层队列:
    Software Queues (ctx): per-CPU 或 per-cgroup → 零锁竞争
    Hardware Queues (hctx): 映射到设备硬件队列 (NVMe SQ, SCSI tag)

  Tags: 用于管理硬件队列的并发请求数
    → 每个硬件队列有固定的 tag pool → 自然限流
```

### 数据结构

```c
// include/linux/blk-mq.h
struct blk_mq_hw_ctx {
    unsigned int        queue_num;       // 硬件队列编号
    struct request_queue *queue;
    struct blk_mq_tags  *tags;           // tag pool (限并发)
    unsigned int        nr_ctx;          // 软件队列数

    struct list_head    dispatch;        // 从调度器出来的请求
    unsigned int        dispatched;

    struct blk_mq_ctx   **ctxs;          // 下属的软件队列
};

struct blk_mq_ctx {
    unsigned int        cpu;             // 绑定的 CPU
    struct blk_mq_hw_ctx *hctxs[];       // 映射的硬件队列
    struct list_head    rq_lists[];      // 提交链表
};
```

### 提交路径: plug → scheduler → dispatch → driver

<svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="blk_mq_submit_bio 提交路径流程图:bio 依次经过拆分检查、plug合并、分配request与tag、初始化、插入调度五个阶段,最终排队到IO scheduler或直接派发给硬件队列">
  <defs>
    <marker id="bioarrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="420" fill="#ffffff"/>
  <text x="360" y="24" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">bio 提交路径:blk_mq_submit_bio() 的五个阶段</text>
  <text x="660" y="38" text-anchor="end" font-size="10" fill="#94a3b8">block/blk-mq.c</text>

  <rect x="60" y="48" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="64" font-size="13" font-weight="700" fill="#3730a3">① bio 太大?</text>
  <text x="76" y="80" font-size="10.5" fill="#4f46e5">超限 → blk_bio_discard_split()/blk_bio_segment_split() 拆分为多个 bio</text>
  <line x1="360" y1="88" x2="360" y2="100" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="100" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="116" font-size="13" font-weight="700" fill="#3730a3">② plug merge 尝试</text>
  <text x="76" y="132" font-size="10.5" fill="#4f46e5">blk_mq_attempt_plug_merge() → front/back merge(合并到现有 request 前/后)</text>
  <line x1="360" y1="140" x2="360" y2="152" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="152" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="168" font-size="13" font-weight="700" fill="#3730a3">③ 分配 request + tag</text>
  <text x="76" y="184" font-size="10.5" fill="#4f46e5">blk_mq_get_new_requests() → blk_mq_get_tag() 取 tag,耗尽则等待</text>
  <line x1="360" y1="192" x2="360" y2="204" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="204" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="220" font-size="13" font-weight="700" fill="#3730a3">④ 初始化 request</text>
  <text x="76" y="236" font-size="10.5" fill="#4f46e5">blk_mq_rq_ctx_init() → 关联 bio,完成初始化</text>
  <line x1="360" y1="244" x2="360" y2="256" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>

  <rect x="60" y="256" width="600" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="272" font-size="13" font-weight="700" fill="#3730a3">⑤ 插入调度</text>
  <text x="76" y="288" font-size="10.5" fill="#4f46e5">blk_mq_sched_insert_request() → 决定走调度器还是直接派发</text>

  <line x1="360" y1="296" x2="200" y2="314" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>
  <text x="280" y="303" text-anchor="middle" font-size="11" fill="#475569">有</text>
  <line x1="360" y1="296" x2="520" y2="314" stroke="#475569" stroke-width="1.6" marker-end="url(#bioarrow)"/>
  <text x="440" y="303" text-anchor="middle" font-size="11" fill="#475569">无</text>

  <rect x="55" y="314" width="290" height="42" rx="8" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="71" y="331" font-size="12.5" font-weight="700" fill="#3730a3">有 IO scheduler</text>
  <text x="71" y="348" font-size="9.5" fill="#4f46e5">blk_mq_sched_try_insert_merge() → 排队到调度器</text>

  <rect x="375" y="314" width="290" height="42" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="391" y="331" font-size="12.5" font-weight="700" fill="#115e59">没有 IO scheduler</text>
  <text x="391" y="348" font-size="9.5" fill="#0f766e">blk_mq_run_dispatch_ops() → 直接 dispatch</text>

  <rect x="60" y="372" width="600" height="44" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="76" y="390" font-size="12.5" fill="#3730a3">五阶段依次执行:超限先拆分,能合并就并入已有 request,tag 不够就等待;</text>
  <text x="76" y="408" font-size="12.5" fill="#3730a3">最后插入调度阶段决定是排队等 IO scheduler,还是直接派发给硬件队列。</text>
</svg>

### Plug: 批量提交

```c
// block/blk-mq.c + block/blk-plug.c
// plug 是 per-task 的 bio 积累机制:
//   blk_start_plug(current)     // 开启 plug
//   submit_bio() × N            // 积累 N 个 bio, 不立即 dispatch
//   blk_finish_plug(current)    // 一次性 flush 所有 bio

// 设计原理:
//   1. 减少锁竞争 (一次拿锁, 插入 N 个请求)
//   2. 增加合并机会 (插 plug 期间可以 front/back merge)
//   3. 文件系统通常在写路径中利用 plug:
//      ext4_writepages() → blk_start_plug() → 多次 submit_bio → blk_finish_plug()
```

---

## IO 调度器

多队列 IO 调度器与旧单队列调度器的根本区别：**调度器不是全局的，是 per-hctx 的**。每个硬件队列独立调度，因为不同队列对应不同 CPU 的提交。

### mq-deadline

```c
// block/mq-deadline.c
// 读写分别维护红黑树 (按 sector 排序) + FIFO (按时钟排序)
// 
// 核心参数:
//   read_expire:  读请求最长等待时间 (默认 500ms)
//   write_expire: 写请求最长等待时间 (默认 5000ms)
//   writes_starved: 写请求配额 (优先处理多少批读后才处理一批写)
//
// 调度逻辑:
//   1. 先查 FIFO: 有超时的请求 → 从红黑树取该 sector 附近的请求 (批量下发)
//   2. 否则: 按 direction batch 下发 (优先读, 但 writs_starved 保证写不被饿死)
//   3. 每次 dispatch 不超过 16 个请求 (避免 starving 其他队列)
//
// 适合: NVMe, 通用 SSD
```

### kyber

```c
// block/kyber-iosched.c
// 基于 token bucket 的自适应调度
// 目标: 控制读/写队列深度，使延迟保持在目标以内
//
// 工作原理:
//   维护读/写的延迟直方图
//   根据延迟动态调整该方向的 token bucket rate
//   延迟低 → 增大 bucket → 更多 IO 并发
//   延迟高 → 减小 bucket → 减少 IO 并发 (降低设备队列深度)
//
// 适合: 对延迟敏感的 SSD 负载 (如数据库)
```

### bfq (Budget Fair Queuing)

```c
// block/bfq-iosched.c
// 按 cgroup/进程分配 IO 带宽
// 每个进程有一个 "budget" (可发出的扇区数)
// 基于 B-WF2Q+ 算法 (类似 CFS 的公平调度)
//
// 特点:
//   - 交互式进程自动获得更高权重 (检测 think time)
//   - 支持 cgroup blkio controller
//   - 顺序读被检测到 → 给予更多 budget → 高吞吐
//
// 适合: 桌面系统 (确保后台备份不影响前台应用响应)
```

### 调度器切换

```bash
cat /sys/block/nvme0n1/queue/scheduler
# [mq-deadline] kyber bfq none

echo kyber > /sys/block/nvme0n1/queue/scheduler
```

---

## 队列参数调优

```bash
# 硬件队列数 (通常等于 CPU 数)
cat /sys/block/nvme0n1/mq/*/cpu_list

# max_sectors_kb: 单次 IO 最大大小 (默认 512KB = 1024 扇区)
cat /sys/block/sda/queue/max_sectors_kb

# nr_requests: 每个硬件队列的最大请求数
cat /sys/block/nvme0n1/queue/nr_requests  # 默认 256

# scheduler 参数
cat /sys/block/nvme0n1/queue/iosched/read_expire  # mq-deadline
```

---

## 调试与观测

```bash
# blktrace: 追踪每个 IO 的完整生命周期
blktrace -d /dev/nvme0n1 -o trace
blkparse -i trace > trace.txt
# 输出: time, CPU, action (Q=queued, G=get, I=inserted, D=dispatch, C=complete)

# iostat: 设备级统计
iostat -x 1 nvme0n1
# r/s, w/s, r_await, w_await, aqu-sz (平均队列深度), %util

# 单设备原始计数
cat /sys/block/nvme0n1/stat
# 字段: read_ios, read_merges, read_sectors, read_ticks, write_ios, ...

# blk-mq 调试
cat /sys/kernel/debug/block/nvme0n1/hctx*/tags
# 每个硬件队列的 tag 使用情况

# ftrace 追踪块层事件
echo 1 > /sys/kernel/debug/tracing/events/block/block_bio_queue/enable
echo 1 > /sys/kernel/debug/tracing/events/block/block_rq_complete/enable
cat /sys/kernel/debug/tracing/trace_pipe
```

---

## 关键配置与编译

```bash
# 内核启动参数
scsi_mod.use_blk_mq=1    # 强制 SCSI 使用 blk-mq (5.0+ 默认)
elevator=mq-deadline      # 默认 IO scheduler
elevator=bfq              # 桌面推荐

# 内核编译选项
CONFIG_BLK_MQ=y           # 启用 blk-mq
CONFIG_IOSCHED_BFQ=y      # BFQ 支持
CONFIG_BLK_DEV_THROTTLING=y  # cgroup blkio
```

---

## 参考与延伸

- **源码**: `block/blk-mq.c` (~4000行, blk-mq 核心), `block/blk-merge.c`, `block/mq-deadline.c`, `block/bfq-iosched.c`, `block/kyber-iosched.c`
- **内核文档**: `Documentation/block/`, `Documentation/block/bfq-iosched.rst`
- **LWN**:
  - "The multi-queue block layer" (lwn.net/Articles/552904/)
  - "blk-mq and the I/O schedulers" (lwn.net/Articles/738449/)

---

*关键词: bio, bio_vec, blk-mq, software queue, hardware queue, plug, mq-deadline, kyber, bfq, blktrace, iostat, IO scheduler*
