Skip to content

Latest commit

 

History

History
2468 lines (1972 loc) · 77.9 KB

File metadata and controls

2468 lines (1972 loc) · 77.9 KB

Linux 块设备层与 I/O 调度深度解析

基于 Linux Kernel 源码深度分析 路径:block/ | include/linux/blk_types.h | include/linux/blkdev.h | include/linux/blk-mq.h


目录

  1. 块设备层概述与整体架构
  2. 核心数据结构:bio 与 bio_vec
  3. request 结构体详解
  4. bio 生命周期:从提交到完成
  5. bio 合并机制
  6. blk-mq 多队列框架
  7. blk-mq 请求分发路径
  8. 标签(tag)分配机制
  9. plug/unplug 批量提交
  10. I/O 调度器框架
  11. mq-deadline 调度器
  12. BFQ 调度器
  13. Kyber 与 none 调度器
  14. NVMe 多队列驱动
  15. NVMe-oF(NVMe over Fabrics)
  16. 块设备层架构:gendisk 与 block_device
  17. 分区与 bd_mapping 页缓存集成
  18. Direct I/O 与 Buffered I/O
  19. I/O 统计与 blkcg 限速
  20. 错误处理与重试机制
  21. block/ 目录主要文件一览
  22. 性能调优指南

1. 块设备层概述与整体架构

在存储栈中的位置

Linux 存储 I/O 栈是一个层次化的软件架构,块设备层处于中间枢纽位置,将上层文件系统的 I/O 请求转化为对底层硬件驱动的命令序列。

+-----------------------------------------------------------+
|              用户空间 (User Space)                        |
|   read() / write() / io_uring / direct I/O               |
+-----------------------------------------------------------+
                           |
+-----------------------------------------------------------+
|              VFS (Virtual File System)                    |
|   struct file_operations  ->  address_space_operations    |
+-----------------------------------------------------------+
                           |
+----------------------------+------------------------------+
|   Page Cache / Writeback  |  Direct I/O (绕过 Page Cache)|
+----------------------------+------------------------------+
                           |
+-----------------------------------------------------------+
|           块设备层 (Block Layer)   <-- 本文重点           |
|                                                           |
|   submit_bio()  -->  blk-mq  -->  I/O Scheduler          |
|   bio / request / request_queue / elevator                |
|                                                           |
+-----------------------------------------------------------+
                           |
+-----------------------------------------------------------+
|           设备驱动层 (Device Driver)                      |
|   NVMe / SCSI / virtio-blk / loop                        |
+-----------------------------------------------------------+
                           |
+-----------------------------------------------------------+
|              物理硬件 (Hardware)                          |
|   SSD / HDD / NVMe SSD / Network Block Device            |
+-----------------------------------------------------------+

块设备层的核心职责

  1. I/O 抽象:为上层提供统一的 bio 接口,屏蔽底层硬件差异。
  2. 请求排队:通过 request_queue 管理待处理的 I/O 请求。
  3. 合并优化:将相邻扇区的多个 I/O 合并为一个大请求,减少调度开销。
  4. I/O 调度:通过 elevator(调度器)对请求重排序,提升吞吐量或降低延迟。
  5. 多队列支持:blk-mq 框架支持现代 NVMe 等多队列设备,消除单队列锁竞争。
  6. 流量控制:blkcg(块设备 cgroup)实现资源隔离与带宽限速。

扇区与基本单位

include/linux/blk_types.h:30 定义了基本常量:

#define SECTOR_SHIFT  9
#define SECTOR_SIZE   (1 << SECTOR_SHIFT)  /* 512 字节 */

所有块 I/O 以扇区(512 字节)为基本单位。sector_t 类型表示扇区偏移量或大小。


2. 核心数据结构:bio 与 bio_vec

bio 结构体完整分析

bio 是块 I/O 层的基本传输单元,定义在 include/linux/blk_types.h:210

struct bio {
    struct bio          *bi_next;       /* 请求队列链表 */
    struct block_device *bi_bdev;       /* 目标块设备 */
    blk_opf_t           bi_opf;        /* 操作类型 + 标志位 */
    unsigned short      bi_flags;      /* BIO_* 标志 */
    unsigned short      bi_ioprio;     /* I/O 优先级 */
    enum rw_hint        bi_write_hint; /* 写入提示 */
    u8                  bi_write_stream; /* 写流标识 */
    blk_status_t        bi_status;     /* 完成状态 */
    u8                  bi_bvec_gap_bit; /* 段间隙位,DMA 优化用 */
    atomic_t            __bi_remaining; /* 引用计数 */

    /* scatter-gather 列表 */
    struct bio_vec      *bi_io_vec;    /* bvec 数组指针 */
    struct bvec_iter     bi_iter;      /* 当前迭代位置 */

    union {
        blk_qc_t        bi_cookie;     /* 轮询 bio 使用 */
        unsigned int    __bi_nr_segments; /* zone 写插件 */
    };
    bio_end_io_t        *bi_end_io;   /* 完成回调 */
    void                *bi_private;  /* 提交者私有数据 */

#ifdef CONFIG_BLK_CGROUP
    struct blkcg_gq     *bi_blkg;     /* blkcg 组关联 */
    u64                  issue_time_ns; /* 发起时间戳 */
#endif
    unsigned short      bi_vcnt;      /* bio_vec 数量 */
    unsigned short      bi_max_vecs;  /* 已分配的 bvec 数 */
    atomic_t            __bi_cnt;     /* pin 引用计数 */
    struct bio_set      *bi_pool;     /* 内存池 */
};

bi_opf 字段解析

bi_opf 是一个 32 位字段(blk_opf_t),低 8 位编码操作类型(req_op),高 24 位编码标志位(req_flag_bits)。

操作类型(include/linux/blk_types.h:347):

操作码 含义
REQ_OP_READ 0 从设备读取扇区
REQ_OP_WRITE 1 向设备写入扇区
REQ_OP_FLUSH 2 刷新易失性写缓存
REQ_OP_DISCARD 3 丢弃扇区(TRIM)
REQ_OP_SECURE_ERASE 5 安全擦除
REQ_OP_ZONE_APPEND 7 追加写到当前区写指针
REQ_OP_WRITE_ZEROES 9 写零填充

最低位标识数据传输方向:置 1 表示写入设备,清 0 表示从设备读取。

常见标志位(include/linux/blk_types.h:382):

REQ_SYNC      /* 同步请求 */
REQ_META      /* 元数据 I/O */
REQ_FUA       /* Forced Unit Access(强制落盘) */
REQ_PREFLUSH  /* 请求前先刷缓存 */
REQ_RAHEAD    /* 预读,可随时失败 */
REQ_NOWAIT    /* 不阻塞,设备忙则返回 */
REQ_POLLED    /* 调用方轮询完成 */
REQ_ATOMIC    /* 原子写操作 */

bi_flags 枚举

/* include/linux/blk_types.h:301 */
enum {
    BIO_PAGE_PINNED,       /* 页面已锁定,完成时释放 */
    BIO_CLONED,            /* 克隆的 bio,不拥有数据 */
    BIO_QUIET,             /* 抑制错误消息 */
    BIO_CHAIN,             /* 链式 bio */
    BIO_REFFED,            /* bi_cnt 已增加 */
    BIO_BPS_THROTTLED,     /* 已经过带宽限速 */
    BIO_TRACE_COMPLETION,  /* bio_endio() 时追踪完成 */
    BIO_CGROUP_ACCT,       /* 已计入 cgroup */
    BIO_QOS_THROTTLED,     /* 已过 rq_qos 限速路径 */
    BIO_REMAPPED,          /* 已重映射(DM/MD 使用)*/
    BIO_ZONE_WRITE_PLUGGING, /* zone 写插件处理 */
};

bio_vec:scatter-gather 列表

bio_vec 定义在 include/linux/bvec.h:28,描述一段连续的物理内存地址范围:

struct bio_vec {
    struct page    *bv_page;   /* 起始页帧 */
    unsigned int    bv_len;    /* 字节长度 */
    unsigned int    bv_offset; /* 相对 bv_page 起始的偏移 */
};

一个 bio 包含一个 bio_vec 数组(bi_io_vec),代表 scatter-gather DMA 列表。每个 bio_vec 描述了一段连续的内存区域,多个 bio_vec 组成完整的 I/O 缓冲区。

bio
 +------------------+
 | bi_io_vec -----> | bio_vec[0]: page=A, offset=0,   len=4096
 | bi_vcnt = 3      | bio_vec[1]: page=B, offset=512, len=2048
 | bi_iter.bi_idx=0 | bio_vec[2]: page=C, offset=0,   len=1024
 +------------------+

bvec_iter 追踪 bio 当前的迭代状态(include/linux/bvec.h:77):

struct bvec_iter {
    sector_t     bi_sector;  /* 当前设备扇区偏移 */
    unsigned int bi_size;    /* 剩余字节数 */
    unsigned int bi_idx;     /* 当前 bio_vec 索引 */
    unsigned int bi_bvec_done; /* 当前 bvec 中已处理字节数 */
};

遍历 bio 的标准宏:

/* 遍历 bio 的每个段 */
bio_for_each_segment(bvl, bio, iter) { ... }

/* 遍历 request 的每个段 */
rq_for_each_segment(bvl, rq, iter) { ... }

bio_set 与内存池

bio_set 是 bio 的内存池管理结构。bio_alloc() 会从 bio_set 的 mempool 中分配 bio,确保在内存压力下仍能分配(通过预留池):

struct bio *bio_alloc(struct block_device *bdev,
                      unsigned short nr_vecs,
                      blk_opf_t opf,
                      gfp_t gfp_mask);

内联 bvec 优化:当 nr_vecs 较小时,bio_vec 数组直接追加在 bio 结构体后面(include/linux/blk_types.h:293):

static inline struct bio_vec *bio_inline_vecs(struct bio *bio)
{
    return (struct bio_vec *)(bio + 1);
}

这种内联布局减少了一次额外的内存分配,提升了 cache 局部性。


3. request 结构体详解

request 是调度器操作的基本单元,定义在 include/linux/blk-mq.h:105

struct request {
    struct request_queue    *q;        /* 所属请求队列 */
    struct blk_mq_ctx      *mq_ctx;   /* 关联的软件队列 */
    struct blk_mq_hw_ctx   *mq_hctx;  /* 关联的硬件队列 */

    blk_opf_t  cmd_flags;             /* 操作类型 + 公共标志 */
    req_flags_t rq_flags;             /* 请求内部标志 */

    int tag;          /* 硬件队列标签(dispatch 后分配)*/
    int internal_tag; /* 调度器标签(入队时分配)*/

    unsigned int timeout;             /* 请求超时时间 */
    unsigned int __data_len;          /* 总数据长度 */
    sector_t __sector;                /* 扇区游标 */

    struct bio *bio;      /* bio 链表头 */
    struct bio *biotail;  /* bio 链表尾 */

    union {
        struct list_head queuelist;   /* 在调度器队列中 */
        struct request  *rq_next;    /* plug 列表链 */
    };

    struct block_device *part;        /* 目标分区 */
    u64 start_time_ns;                /* 请求分配时间 */
    u64 io_start_time_ns;             /* 提交到设备的时间 */

    unsigned short nr_phys_segments;  /* DMA 物理段数量 */
    unsigned char  phys_gap_bit;      /* 段间隙位 */

    enum mq_rq_state state;           /* MQ_RQ_IDLE/IN_FLIGHT/COMPLETE */
    atomic_t ref;                     /* 引用计数 */
    unsigned long deadline;           /* 截止时间(调度器用)*/

    union {
        struct hlist_node hash;  /* 合并哈希表节点 */
        struct llist_node ipi_list; /* softirq 完成队列 */
    };
    union {
        struct rb_node rb_node;   /* 调度器排序树节点 */
        struct bio_vec special_vec; /* RQF_SPECIAL_PAYLOAD 时使用 */
    };

    struct {
        struct io_cq *icq;    /* I/O context 队列 */
        void *priv[2];        /* 调度器私有数据(最多 2 个指针)*/
    } elv;

    struct {
        unsigned int seq;
        rq_end_io_fn *saved_end_io;
    } flush;                          /* flush 操作序列 */

    u64 fifo_time;           /* FIFO 到期时间 */
    rq_end_io_fn *end_io;    /* 完成回调 */
    void *end_io_data;
};

req_flags_t 内部标志

/* include/linux/blk-mq.h:34 */
enum rqf_flags {
    __RQF_STARTED,          /* 驱动已开始处理 */
    __RQF_FLUSH_SEQ,        /* flush 序列请求 */
    __RQF_MIXED_MERGE,      /* 不同类型请求合并 */
    __RQF_DONTPREP,         /* 不调用 prep */
    __RQF_SCHED_TAGS,       /* 使用调度器标签 */
    __RQF_USE_SCHED,        /* 使用 I/O 调度器 */
    __RQF_FAILED,           /* 驱动内部错误 */
    __RQF_IO_STAT,          /* 记录 I/O 统计 */
    __RQF_PM,               /* 运行时电源管理请求 */
    __RQF_HASHED,           /* 在调度器合并哈希中 */
    __RQF_STATS,            /* 追踪 I/O 完成时间 */
    __RQF_ZONE_WRITE_PLUGGING, /* 需通知 zone 写插件 */
    __RQF_TIMED_OUT,        /* 已超时 */
};

request 状态机

分配时: MQ_RQ_IDLE
         |
         | blk_mq_start_request()
         v
      MQ_RQ_IN_FLIGHT
         |
         | blk_mq_end_request() / blk_mq_set_request_complete()
         v
      MQ_RQ_COMPLETE
         |
         | 释放回标签池
         v
      (free)

bio 链与 request 的关系

一个 request 可包含多个合并的 bio,通过 bio->bi_next 链接。bio 链上的所有 bio 必须满足:

  • 属于同一个块设备
  • 操作类型相同
  • 扇区地址连续(BACK_MERGE 或 FRONT_MERGE)
request
  +---------+
  | bio  ---+---> bio[0] --bi_next--> bio[1] --bi_next--> bio[2] --> NULL
  | biotail -+--------------------------------------------^
  +---------+

4. bio 生命周期:从提交到完成

submit_bio() 路径

应用/文件系统
    |
    | bio_alloc() + bio_add_page()
    v
submit_bio(bio)                         [block/bio.c]
    |
    | generic_make_request() [已废弃] 或
    v
submit_bio_noacct(bio)                  [block/blk-core.c]
    |
    | 检查递归提交(bi_opf & REQ_NOWAIT)
    v
blk_mq_submit_bio(bio)                  [block/blk-mq.c]
    |
    +---> 尝试 bio 合并(plug merge / sched merge)
    |
    +---> 分配 request(blk_mq_get_new_requests)
    |
    +---> 加入 plug 列表 或 直接提交
    v
blk_mq_run_hw_queue(hctx, async)
    |
    v
blk_mq_dispatch_rq_list()
    |
    v
ops->queue_rq(hctx, &bd)               [驱动层]

blk_mq_submit_bio 核心逻辑

block/blk-mq.c:3141 是 blk-mq 路径上提交 bio 的入口函数:

void blk_mq_submit_bio(struct bio *bio)
{
    struct request_queue *q = bdev_get_queue(bio->bi_bdev);
    struct blk_plug *plug = current->plug;
    // ...
    // 1. 尝试合并
    // 2. 分配 request
    // 3. 加入 plug 或直接分发
}

bio 完成路径

硬件中断 / 轮询
    |
    v
驱动调用 blk_mq_end_request(rq, error)
    |
    v
blk_update_request(rq, error, nr_bytes)
    |
    v
bio_endio(bio)                          [遍历 bio 链]
    |
    v
bio->bi_end_io(bio)                     [调用完成回调]
    |
    v
文件系统 / 应用层完成处理

5. bio 合并机制

合并类型

块层支持三种 bio 合并方式:

ELEVATOR_BACK_MERGE  (后向合并)
  existing_rq: [sector 100 ... 200]
  new_bio:                       [sector 200 ... 250]
  结果:        [sector 100 .................. 250]

ELEVATOR_FRONT_MERGE (前向合并)
  new_bio:  [sector 50 ... 100]
  existing_rq:              [sector 100 ... 200]
  结果:      [sector 50 ........................ 200]

ELEVATOR_DISCARD_MERGE (discard 合并)
  合并多个 discard 范围

blk_attempt_plug_merge

block/blk-merge.c:1085 是 plug 路径上的合并尝试函数:

bool blk_attempt_plug_merge(struct request_queue *q, struct bio *bio,
        unsigned int nr_segs)
{
    struct blk_plug *plug = current->plug;
    struct request *rq;

    if (!plug || rq_list_empty(&plug->mq_list))
        return false;

    /* 优先检查最近一个请求(尾部) */
    rq = plug->mq_list.tail;
    if (rq->q == q)
        return blk_attempt_bio_merge(q, rq, bio, nr_segs, false) ==
            BIO_MERGE_OK;
    else if (!plug->multiple_queues)
        return false;

    /* 多队列情况:遍历 plug 列表 */
    rq_list_for_each(&plug->mq_list, rq) {
        if (rq->q != q)
            continue;
        if (blk_attempt_bio_merge(q, rq, bio, nr_segs, false) ==
            BIO_MERGE_OK)
            return true;
        break;
    }
    return false;
}

关键设计:plug 合并在无锁路径下进行(per-task plug 列表),完全避免了队列锁竞争。

合并检查链路

blk_mq_attempt_bio_merge()          [block/blk-mq.c:3034]
    |
    +---> blk_attempt_plug_merge()  [plug 列表合并,无锁]
    |
    +---> blk_mq_sched_bio_merge()  [调度器合并,使用哈希]
              |
              v
          elv_merge()               [遍历 elevator 合并逻辑]
              |
              +---> elv_rqhash_find()   [通过扇区号快速查找]
              +---> ops->request_merge()

合并约束

合并必须满足的硬件限制(struct queue_limitsinclude/linux/blkdev.h:370):

struct queue_limits {
    unsigned int max_sectors;          /* 最大扇区数 */
    unsigned int max_segment_size;     /* 单段最大字节数 */
    unsigned short max_segments;       /* 最大段数(BLK_MAX_SEGMENTS=128)*/
    unsigned long seg_boundary_mask;   /* 段边界限制 */
    unsigned int dma_alignment;        /* DMA 对齐要求 */
    // ...
};

REQ_NOMERGE_FLAGS = REQ_NOMERGE | REQ_PREFLUSH | REQ_FUA:带有这些标志的请求不能被合并。


6. blk-mq 多队列框架

架构概述

blk-mq(Multi-Queue Block I/O Queueing Mechanism)是 Linux 3.13 引入的多队列块 I/O 框架,专为现代 NVMe SSD、高速网络存储等支持多队列的设备设计。

                    CPU 0    CPU 1    CPU 2    CPU 3
                      |        |        |        |
              +-------+--------+--------+--------+
              |  软件队列(Software Queues)       |
              |  blk_mq_ctx (per-CPU)              |
              +---+--------+--------+-------------+
                  |        |        |
           +------+  +-----+  +----+
           | HW Q0|  | HW Q1|  | HW Q2|       <- 硬件队列 blk_mq_hw_ctx
           +------+  +------+  +------+
               |          |          |
           +---v----------v----------v---------+
           |         NVMe 控制器                |
           |  SQ0    SQ1    SQ2   (提交队列)   |
           |  CQ0    CQ1    CQ2   (完成队列)   |
           +-----------------------------------+

blk_mq_ctx:软件队列

软件队列(Software Queue)定义在 block/blk-mq.h:19,每个 CPU 对应一个:

struct blk_mq_ctx {
    struct {
        spinlock_t      lock;
        struct list_head rq_lists[HCTX_MAX_TYPES]; /* 每种请求类型一个列表 */
    } ____cacheline_aligned_in_smp;

    unsigned int        cpu;                 /* 所属 CPU */
    unsigned short      index_hw[HCTX_MAX_TYPES]; /* 硬件队列索引 */
    struct blk_mq_hw_ctx *hctxs[HCTX_MAX_TYPES]; /* 关联的硬件队列 */

    struct request_queue *queue;             /* 所属请求队列 */
    struct blk_mq_ctxs   *ctxs;
    struct kobject        kobj;
} ____cacheline_aligned_in_smp;

获取当前 CPU 的软件队列(block/blk-mq.h:155):

static inline struct blk_mq_ctx *blk_mq_get_ctx(struct request_queue *q)
{
    return __blk_mq_get_ctx(q, raw_smp_processor_id());
}

blk_mq_hw_ctx:硬件队列

硬件队列(Hardware Queue)定义在 include/linux/blk-mq.h:322,对应设备的一个 MSI-X 中断/队列对:

struct blk_mq_hw_ctx {
    struct {
        spinlock_t       lock;
        struct list_head dispatch;  /* 待派发但暂时无法提交的请求 */
        unsigned long    state;     /* BLK_MQ_S_* 状态位 */
    } ____cacheline_aligned_in_smp;

    struct delayed_work  run_work;   /* 延迟运行工作项 */
    cpumask_var_t        cpumask;    /* 可运行此队列的 CPU 掩码 */
    int                  next_cpu;   /* 轮询 CPU 选择 */
    int                  next_cpu_batch;

    unsigned long        flags;      /* BLK_MQ_F_* 标志 */
    void                *sched_data; /* I/O 调度器私有数据 */
    struct request_queue *queue;
    struct blk_flush_queue *fq;      /* flush 请求队列 */
    void                *driver_data; /* 驱动私有数据 */

    struct sbitmap       ctx_map;    /* 软件队列的位图(有请求时置位)*/
    struct blk_mq_ctx   *dispatch_from; /* 无调度器时的分发来源 */
    unsigned int         dispatch_busy; /* EWMA 繁忙度估计 */

    unsigned short       type;       /* HCTX_TYPE_DEFAULT/READ/POLL */
    unsigned short       nr_ctx;     /* 关联的软件队列数 */
    struct blk_mq_ctx  **ctxs;       /* 软件队列数组 */

    struct blk_mq_tags  *tags;       /* 硬件标签集 */
    struct blk_mq_tags  *sched_tags; /* 调度器标签集 */

    unsigned int         numa_node;
    unsigned int         queue_num;  /* 队列编号 */
    atomic_t             nr_active;  /* 活跃请求数(共享标签集时用)*/
};

硬件队列类型(hctx_type)

/* include/linux/blk-mq.h:488 */
enum hctx_type {
    HCTX_TYPE_DEFAULT,  /* 所有 I/O(默认)*/
    HCTX_TYPE_READ,     /* 只读 I/O */
    HCTX_TYPE_POLL,     /* 轮询 I/O(不依赖中断)*/
    HCTX_MAX_TYPES,
};

操作类型到队列类型的映射(block/blk-mq.h:90):

static inline enum hctx_type blk_mq_get_hctx_type(blk_opf_t opf)
{
    if (opf & REQ_POLLED)
        return HCTX_TYPE_POLL;
    else if ((opf & REQ_OP_MASK) == REQ_OP_READ)
        return HCTX_TYPE_READ;
    return HCTX_TYPE_DEFAULT;
}

blk_mq_ops:驱动操作接口

include/linux/blk-mq.h:576 定义了 blk-mq 驱动必须实现的回调接口:

struct blk_mq_ops {
    /* 核心:提交单个请求到硬件 */
    blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *,
                             const struct blk_mq_queue_data *);

    /* 批量提交后的收尾(bd->last=false 时需要) */
    void (*commit_rqs)(struct blk_mq_hw_ctx *);

    /* 批量提交请求列表(可选,优化路径) */
    void (*queue_rqs)(struct rq_list *rqlist);

    /* 获取/释放设备资源预算 */
    int  (*get_budget)(struct request_queue *);
    void (*put_budget)(struct request_queue *, int);

    /* 超时处理 */
    enum blk_eh_timer_return (*timeout)(struct request *);

    /* I/O 轮询(POLL 队列使用)*/
    int (*poll)(struct blk_mq_hw_ctx *, struct io_comp_batch *);

    /* 标记请求完成 */
    void (*complete)(struct request *);

    /* 初始化/清理硬件队列 */
    int  (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int);
    void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);

    /* 初始化/清理请求(分配驱动私有空间)*/
    int  (*init_request)(struct blk_mq_tag_set *, struct request *,
                         unsigned int, unsigned int);
    void (*exit_request)(struct blk_mq_tag_set *, struct request *,
                         unsigned int);

    /* CPU 到队列的自定义映射 */
    void (*map_queues)(struct blk_mq_tag_set *set);
};

blk_mq_tag_set:标签集

include/linux/blk-mq.h:534 定义全局标签集,在注册块设备时初始化,多个 request_queue 可共享(如 SCSI 多路径):

struct blk_mq_tag_set {
    const struct blk_mq_ops *ops;
    struct blk_mq_queue_map  map[HCTX_MAX_TYPES]; /* CPU->HW Queue 映射 */
    unsigned int             nr_maps;
    unsigned int             nr_hw_queues;  /* 硬件队列数 */
    unsigned int             queue_depth;   /* 每个队列的标签数 */
    unsigned int             reserved_tags; /* 预留标签数 */
    unsigned int             cmd_size;      /* 每个 request 附加数据大小 */
    int                      numa_node;
    unsigned int             timeout;
    unsigned int             flags;         /* BLK_MQ_F_* */
    void                    *driver_data;
    struct blk_mq_tags     **tags;          /* 每个 hctx 的标签结构 */
    struct blk_mq_tags      *shared_tags;   /* 共享标签(可选)*/
};

7. blk-mq 请求分发路径

完整分发流程

submit_bio(bio)
    |
    v
blk_mq_submit_bio()             [block/blk-mq.c]
    |
    +--- 尝试 bio 合并
    |    blk_mq_attempt_bio_merge()
    |         |-- blk_attempt_plug_merge()  (plug 合并)
    |         `-- blk_mq_sched_bio_merge()  (调度器合并)
    |
    +--- 分配 request
    |    blk_mq_get_new_requests()
    |         `-- __blk_mq_alloc_requests()
    |              `-- blk_mq_get_tag()   (从 sbitmap 分配标签)
    |
    +--- current->plug 存在?
    |    YES: 加入 plug->mq_list(延迟提交)
    |    NO:  直接进入队列
    |
    v
blk_mq_run_hw_queue(hctx, async)
    |
    v
__blk_mq_run_hw_queue()
    |
    v
blk_mq_sched_dispatch_requests()
    |
    +--- 有调度器?
    |    YES: elevator->ops.dispatch_request(hctx)
    |    NO:  blk_mq_dequeue_from_ctx()
    |
    v
blk_mq_dispatch_rq_list()
    |
    v
blk_mq_dispatch_rq_list()  循环调用:
    +---> blk_mq_prep_dispatch_rq()
    +---> ops->queue_rq(hctx, &bd)
    |        返回 BLK_STS_OK:       请求已提交
    |        返回 BLK_STS_RESOURCE: 设备暂时无资源,重排队
    |        返回 BLK_STS_DEV_RESOURCE: 设备繁忙
    `---> bd.last=true 时调用 ops->commit_rqs()

队列运行触发时机

blk_mq_run_hw_queue() 被以下情况触发:

  1. unplug 时blk_finish_plug()blk_mq_flush_plug_list()
  2. 请求完成时:空闲标签释放后检查等待队列
  3. 调度延迟hctx->run_work delayed_work 到期
  4. requeue 时blk_mq_kick_requeue_list()

软件队列到硬件队列的映射

CPU ID  -->  blk_mq_ctx (per-CPU)  -->  blk_mq_hw_ctx
                                              ^
                  ctx->index_hw[type] ---------+
                  ctx->hctxs[type]   ----------+

映射通过 blk_mq_queue_map 在驱动初始化时建立(include/linux/blk-mq.h:475):

struct blk_mq_queue_map {
    unsigned int *mq_map;      /* CPU ID -> HW 队列索引 */
    unsigned int  nr_queues;
    unsigned int  queue_offset;
};

8. 标签(tag)分配机制

blk_mq_tags 结构

标签是 request 的唯一标识符,定义在 include/linux/blk-mq.h:774

struct blk_mq_tags {
    unsigned int nr_tags;           /* 总标签数 */
    unsigned int nr_reserved_tags;  /* 预留标签数 */
    unsigned int active_queues;     /* 活跃队列数 */

    struct sbitmap_queue bitmap_tags;    /* 普通标签位图 */
    struct sbitmap_queue breserved_tags; /* 预留标签位图 */

    struct request **rqs;          /* tag -> request 指针数组 */
    struct request **static_rqs;   /* 预分配的 request 数组 */
    struct list_head page_list;    /* 内存页列表 */
    spinlock_t lock;
};

sbitmap_queue:可扩展位图队列

标签分配基于 sbitmap_queue,这是一个支持等待的可扩展稀疏位图。其设计目标是在高并发场景下最小化原子操作的 cache line 竞争。

sbitmap_queue
  +------------------+
  | sb (sbitmap)     |  --> 位图数组,按 WORD 分散到不同 cache line
  | ws (wait states) |  --> 等待队列数组
  | round_robin      |  --> 轮询分配位置
  +------------------+

位图布局(假设 nr_tags=1024):
word[0]: bits 0..63
word[1]: bits 64..127
...
word[15]: bits 960..1023

标签分配流程

blk_mq_get_tag(data)            [block/blk-mq-tag.c]
    |
    +--- 尝试从 bitmap_tags 分配
    |    sbitmap_queue_get(&tags->bitmap_tags, &tag)
    |
    +--- 失败?
    |    BLK_MQ_REQ_RESERVED: 尝试 breserved_tags
    |    BLK_MQ_REQ_NOWAIT: 立即返回 BLK_MQ_NO_TAG
    |    否则: 进入等待队列睡眠
    |
    v
成功: tags->rqs[tag] = rq   (建立 tag -> request 映射)

双级标签体系

blk-mq 维护两套标签:

  1. hctx->tags(硬件标签):在 queue_rq 之前由 blk-mq 核心分配。标签数等于 queue_depth,直接对应设备硬件资源(NVMe 的 SQ entry)。

  2. hctx->sched_tags(调度器标签):当存在 I/O 调度器时,请求入队时分配此标签(RQF_SCHED_TAGS 标志)。调度器标签数 = nr_requests(通常 = 2 × queue_depth),允许更多请求在调度器内缓冲。

有调度器时:
  bio --> sched_tag 分配 --> 调度器队列 --> 派发 --> hw_tag 分配 --> 提交硬件

无调度器时:
  bio --> hw_tag 分配 --> 直接提交硬件

tag 与 request 的互查

/* include/linux/blk-mq.h:794 */
static inline struct request *blk_mq_tag_to_rq(struct blk_mq_tags *tags,
                                               unsigned int tag)
{
    if (tag < tags->nr_tags) {
        prefetch(tags->rqs[tag]);  /* 提前预取,减少 cache miss */
        return tags->rqs[tag];
    }
    return NULL;
}

/* 全局唯一 tag(包含 hctx 编号)*/
u32 blk_mq_unique_tag(struct request *rq);
/* 高 16 位:hctx 编号;低 16 位:本地 tag */

9. plug/unplug 批量提交

blk_plug 结构

include/linux/blkdev.h:1172 定义了 plug 机制:

struct blk_plug {
    struct rq_list mq_list;    /* blk-mq 请求列表(已分配的 request)*/
    struct rq_list cached_rqs; /* 缓存的 request(批量预分配)*/
    u64    cur_ktime;          /* 当前 ktime 快照(避免重复调用)*/
    unsigned short nr_ios;     /* 预期 I/O 数(批量分配用)*/
    unsigned short rq_count;   /* plug 中的 request 数量 */
    bool multiple_queues;      /* 是否有多个队列的请求 */
    bool has_elevator;         /* 是否有请求使用调度器 */
    struct list_head cb_list;  /* unplug 回调(md 等使用)*/
};

plug/unplug 使用模式

/* 典型使用模式(文件系统中)*/
struct blk_plug plug;

blk_start_plug(&plug);           /* 设置 current->plug = &plug */

/* 提交多个 bio,它们会积累在 plug->mq_list */
submit_bio(bio1);
submit_bio(bio2);
submit_bio(bio3);

blk_finish_plug(&plug);          /* 清除 current->plug,批量提交 */
    |
    v
blk_mq_flush_plug_list(&plug, false)
    |
    v
对每个 hctx 调用 blk_mq_sched_insert_requests()  blk_mq_run_hw_queue()

批量标签预分配

blk_start_plug_nr_ios(plug, nr_ios) 可以提示 plug 将要提交的 I/O 数量,使标签分配可以批量进行:

/* block/blk-mq.c:3065 */
if (plug) {
    data.nr_tags = plug->nr_ios;  /* 一次分配多个标签 */
    plug->nr_ios = 1;
    data.cached_rqs = &plug->cached_rqs;
}

批量分配通过 blk_mq_get_tags() 实现,使用单次 sbitmap 操作分配多个连续标签,显著减少原子操作开销。

unplug 触发时机

1. 显式调用 blk_finish_plug()
2. 进程调度:schedule() -> blk_flush_plug(plug, true)
3. 系统调用返回:检查 TIF_NOTIFY_RESUME
4. blk_plug_invalidate_ts() 使时间戳失效

10. I/O 调度器框架

elevator 框架概述

I/O 调度器框架(elevator)提供了可插拔的 I/O 请求排序和合并策略。定义在 block/elevator.h

request_queue
    |
    v
elevator_queue (运行时实例)
    +-> type (elevator_type: 调度器类型)
    +-> elevator_data (调度器私有数据)
    +-> et (elevator_tags: 调度器标签)
    +-> hash[64] (合并哈希表)

elevator_type:调度器类型注册

block/elevator.h:97 定义调度器类型:

struct elevator_type {
    struct kmem_cache *icq_cache;   /* I/O context 缓存 */
    struct elevator_mq_ops ops;     /* 操作接口集 */
    size_t  icq_size;               /* io_cq 大小 */
    size_t  icq_align;              /* io_cq 对齐 */
    const struct elv_fs_entry *elevator_attrs; /* sysfs 属性 */
    const char *elevator_name;      /* 调度器名称 */
    const char *elevator_alias;     /* 别名 */
    struct module *elevator_owner;
    char icq_cache_name[ELV_NAME_MAX + 6];
    struct list_head list;          /* 全局注册链表 */
};

elevator_mq_ops:调度器回调接口

block/elevator.h:57 定义 blk-mq 调度器的操作集:

struct elevator_mq_ops {
    /* 初始化/销毁 */
    int  (*init_sched)(struct request_queue *, struct elevator_queue *);
    void (*exit_sched)(struct elevator_queue *);
    int  (*init_hctx)(struct blk_mq_hw_ctx *, unsigned int);
    void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);

    /* 合并相关 */
    bool (*allow_merge)(struct request_queue *, struct request *, struct bio *);
    bool (*bio_merge)(struct request_queue *, struct bio *, unsigned int);
    int  (*request_merge)(struct request_queue *q, struct request **, struct bio *);
    void (*request_merged)(struct request_queue *, struct request *, enum elv_merge);
    void (*requests_merged)(struct request_queue *, struct request *, struct request *);

    /* 深度限制(限制并发度)*/
    void (*limit_depth)(blk_opf_t, struct blk_mq_alloc_data *);

    /* 请求生命周期 */
    void (*prepare_request)(struct request *);
    void (*finish_request)(struct request *);

    /* 入队与分发 */
    void (*insert_requests)(struct blk_mq_hw_ctx *hctx,
                            struct list_head *list, blk_insert_t flags);
    struct request *(*dispatch_request)(struct blk_mq_hw_ctx *);
    bool (*has_work)(struct blk_mq_hw_ctx *);

    /* 完成与重排队 */
    void (*completed_request)(struct request *, u64);
    void (*requeue_request)(struct request *);

    /* 邻居请求查找(merge 用)*/
    struct request *(*former_request)(struct request_queue *, struct request *);
    struct request *(*next_request)(struct request_queue *, struct request *);

    /* I/O Context 初始化/清理 */
    void (*init_icq)(struct io_cq *);
    void (*exit_icq)(struct io_cq *);
};

elevator_queue:运行时实例

block/elevator.h:146

struct elevator_queue {
    struct elevator_type *type;         /* 调度器类型 */
    struct elevator_tags *et;           /* 调度器标签 */
    void *elevator_data;                /* 调度器私有数据 */
    struct kobject kobj;                /* sysfs 节点 */
    struct mutex sysfs_lock;
    unsigned long flags;
    DECLARE_HASHTABLE(hash, ELV_HASH_BITS); /* 合并哈希(64桶)*/
};

合并哈希表通过 elv_rqhash_find(q, sector) 可以在 O(1) 时间内找到以目标扇区结尾的请求,用于 BACK_MERGE 合并。

调度器注册与切换

/* 注册调度器 */
int elv_register(struct elevator_type *e);

/* 通过 sysfs 切换调度器 */
/* echo mq-deadline > /sys/block/sda/queue/scheduler */
ssize_t elv_iosched_store(struct gendisk *disk, const char *page, size_t count);

/* 切换需要先冻结队列 */
/* blk_mq_freeze_queue() -> 切换 -> blk_mq_unfreeze_queue() */

11. mq-deadline 调度器

设计哲学

mq-deadline 是传统 deadline 调度器的 blk-mq 适配版(block/mq-deadline.c),核心目标:

  1. 防止饥饿:每个请求都有截止时间,超时后强制派发
  2. 读写分离:读请求优先(延迟敏感),写请求可以稍延迟(吞吐优先)
  3. 扇区排序:在截止时间允许范围内,按扇区顺序派发,减少磁头移动

数据结构

block/mq-deadline.c:41 定义核心数据结构:

/* 优先级分类 */
enum dd_prio {
    DD_RT_PRIO  = 0,  /* 实时优先级 (IOPRIO_CLASS_RT) */
    DD_BE_PRIO  = 1,  /* 最佳效率 (IOPRIO_CLASS_BE) */
    DD_IDLE_PRIO = 2, /* 空闲 (IOPRIO_CLASS_IDLE) */
};

/* 每优先级、每方向的数据 */
struct dd_per_prio {
    struct rb_root    sort_list[DD_DIR_COUNT];  /* 按扇区排序的红黑树 */
    struct list_head  fifo_list[DD_DIR_COUNT];  /* 按到期时间排序的 FIFO */
    sector_t          latest_pos[DD_DIR_COUNT]; /* 最近派发位置 */
    struct io_stats_per_prio stats;
};

/* 调度器主数据 */
struct deadline_data {
    struct list_head    dispatch;              /* 立即派发队列 */
    struct dd_per_prio  per_prio[DD_PRIO_COUNT]; /* 3个优先级 */
    enum dd_data_dir    last_dir;              /* 最近派发方向 */
    unsigned int        batching;              /* 批处理计数 */
    unsigned int        starved;               /* 写饥饿计数 */

    /* 可调参数 */
    int fifo_expire[DD_DIR_COUNT]; /* 默认: 读=HZ/2, 写=5*HZ */
    int fifo_batch;                /* 批量处理数: 默认 16 */
    int writes_starved;            /* 最大读饥饿写次数: 默认 2 */
    int front_merges;              /* 是否允许前向合并 */
    int prio_aging_expire;         /* 低优先级老化时间: 10*HZ */
    spinlock_t lock;
};

截止时间参数(block/mq-deadline.c:30):

static const int read_expire  = HZ / 2;    /* 读请求最大等待: 500ms */
static const int write_expire = 5 * HZ;    /* 写请求最大等待: 5s */
static const int prio_aging_expire = 10 * HZ; /* 低优先级老化: 10s */
static const int writes_starved = 2;       /* 读可饥饿写的最大次数 */
static const int fifo_batch = 16;          /* 批量处理大小 */

双队列结构

每个优先级维护两套排序结构:

sort_list[READ]   (rb_tree, 按扇区排序)
  +-- sector 100
  +-- sector 200
  +-- sector 300

fifo_list[READ]   (链表, 按到期时间排序)
  +-- [100, deadline=T+100ms]
  +-- [200, deadline=T+200ms]
  +-- [300, deadline=T+50ms]  <-- 先到期的先派发

sort_list[WRITE]  (rb_tree)
fifo_list[WRITE]  (链表)

派发决策流程

dd_dispatch_request(hctx)
    |
    +-- 检查 dispatch 列表(未派发的请求)
    |
    +-- 优先级循环(RT > BE > IDLE)
    |       |
    |       +-- 检查 FIFO 是否有到期请求?
    |       |   是: 强制派发(deadline 强制)
    |       |
    |       +-- 批量计数未超 fifo_batch?
    |       |   是: 从 sort_list 顺序派发
    |       |
    |       +-- 切换方向(读/写平衡)
    |           writes_starved 超限: 强制切到写
    |
    v
派发请求(elv_dispatch_sort 或直接出队)

读写平衡机制

每次派发读请求: starved++
每次派发写请求: starved = 0

当 starved >= writes_starved(默认2)时:
    强制派发写请求,防止写饥饿

mq-deadline ASCII 架构图

                  ┌─────────────────────────────────┐
                  │          deadline_data           │
                  │                                  │
                  │  per_prio[RT]                    │
                  │    sort_list[R]: rb_tree─────────┼──> R[s=100] R[s=200] ...
                  │    fifo_list[R]: FIFO────────────┼──> R[dl=10ms] R[dl=50ms] ...
                  │    sort_list[W]: rb_tree─────────┼──> W[s=50]  W[s=150] ...
                  │    fifo_list[W]: FIFO────────────┼──> W[dl=1s]  W[dl=3s] ...
                  │                                  │
                  │  per_prio[BE]  (类似结构)        │
                  │  per_prio[IDLE](类似结构)        │
                  │                                  │
                  │  dispatch: list_head─────────────┼──> 立即派发的请求
                  └─────────────────────────────────┘
                                  │
                                  v
                         blk_mq_dispatch_rq_list()
                                  │
                                  v
                          ops->queue_rq()

12. BFQ 调度器

设计原理

BFQ(Budget Fair Queueing)是一个比例共享 I/O 调度器,定义在 block/bfq-iosched.c

核心思想(block/bfq-iosched.c:22):

BFQ 向进程分配预算(以扇区数度量),而不是时间片。设备不是在给定时间片内服务于进程,而是直到进程耗尽分配的预算。这一变化使 BFQ 能够按预期分配设备吞吐量,不受吞吐量波动或设备内部队列的影响。

B-WF2Q+ 算法

BFQ 使用 B-WF2Q+(Budgeted Worst-case Fair Weighted Fair Queueing)内部调度器:

  1. 每个进程/队列有权重 w_i
  2. 虚拟时间(virtual time)V(t) 以全局速率推进
  3. 每个队列的虚拟截止时间 vd = V + budget / w
  4. 服务树(service tree)按虚拟截止时间排序
服务树(augmented red-black tree):
            [vd=100]
           /         \
        [vd=50]    [vd=150]
       /       \
    [vd=30]  [vd=70]

每次派发 vd 最小的队列(等效于 EDF)

低延迟启发式

BFQ 识别两类时间敏感应用并赋予特权(block/bfq-iosched.c:40):

  1. 交互式应用:周期性进行短时 I/O 的进程(如桌面程序)。检测方式:队列只在有限时间内持续非空,然后变空。

  2. 软实时应用:需要固定速率 I/O 的进程(如视频播放器)。通过 bfq_bfqq_softrt_next_start 函数检测。

对这两类应用,BFQ 临时提升其权重(weight-raising),确保低延迟。

bfq_queue:进程 I/O 队列

每个进程的每种 I/O 类型对应一个 bfq_queue,包含:

bfq_queue
  +-- sort_list (rb_tree: 按扇区排序)
  +-- fifo (链表: FIFO 顺序)
  +-- weight (I/O 权重)
  +-- budget (当前预算:扇区数)
  +-- vdisktime (虚拟磁盘时间)
  +-- service_tree -> 全局服务树
  +-- bfqd -> 调度器全局数据

BFQ cgroup 支持

BFQ 通过 bfq_group 结构支持完整的层级调度:

cgroup hierarchy:
  /sys/fs/cgroup/io/
    ├── io.weight = 100  (根组默认权重)
    ├── app_group/
    │   ├── io.weight = 200  (高优先级组)
    │   └── process A, B
    └── bg_group/
        ├── io.weight = 50   (低优先级组)
        └── process C, D

BFQ 使用 H-WF2Q+ 在 cgroup 层级上递归应用公平调度。


13. Kyber 与 none 调度器

Kyber 调度器

Kyber 是为快速存储设备(NVMe SSD)设计的轻量级调度器,基于令牌桶原理:

核心设计

每种 I/O 类型(READ / SYNC_WRITE / OTHER)维护独立的令牌桶

令牌桶参数:
  - target_lat: 延迟目标(READ: 2ms, WRITE: 10ms)
  - tokens:     当前可用令牌数
  - tokens_per_s: 令牌生成速率(动态调整)

派发逻辑:
  FOR each hctx:
    IF tokens[type] > 0:
        派发该类型请求
        tokens[type]--
    ELSE:
        限流(排队等待令牌)

动态调整:
  测量实际延迟 vs target_lat
  延迟 < 目标: 增加令牌(放宽限流)
  延迟 > 目标: 减少令牌(加强限流)

适用场景:NVMe 等低延迟设备。在这类设备上,deadline 的读写分离反而增加不必要的开销,Kyber 的轻量级令牌机制更合适。

令牌桶 ASCII 图

READ  令牌桶  [■■■■■□□□□□] 50/100 tokens  <- 延迟目标 2ms
WRITE 令牌桶  [■■■■■■■□□□] 70/100 tokens  <- 延迟目标 10ms
OTHER 令牌桶  [■■□□□□□□□□] 20/100 tokens

每次派发消耗1个令牌
每个测量周期根据延迟反馈补充令牌

none 调度器

none(也叫 "noop")调度器不做任何重排序:

insert_requests: 直接追加到 dispatch 队列
dispatch_request: FIFO 顺序取出
bio_merge: 只做基本的 FIFO 合并

适用场景

  1. NVMe 等设备本身有硬件队列和内部调度,软件调度无益
  2. 虚拟化环境(hypervisor 层已做调度)
  3. 基准测试(排除调度器影响)

在 blk-mq 中,当只有一个硬件队列或硬件队列共享时,BLK_MQ_F_NO_SCHED_BY_DEFAULT 标志会默认选择 none:

/* include/linux/blk-mq.h:709 */
BLK_MQ_F_NO_SCHED_BY_DEFAULT = 1 << 6,
/* 单队列或共享队列时,默认不使用调度器 */

调度器比较

特性 mq-deadline BFQ Kyber none
适用设备 HDD/SATA SSD 所有 NVMe SSD NVMe/虚拟化
防饥饿 截止时间 权重+预算 令牌桶
延迟保证 硬截止 软实时 延迟目标
吞吐优化 扇区排序 顺序提升 设备自身
cgroup 支持 有限 完整层级 基本
CPU 开销 极低 极低

14. NVMe 多队列驱动

NVMe 与 blk-mq 的天然契合

NVMe 协议原生支持多队列(最多 65535 个队列),每个队列对独立工作:提交队列(Submission Queue,SQ)+ 完成队列(Completion Queue,CQ)。这与 blk-mq 框架完全对齐:

blk-mq HW Queue  <-->  NVMe I/O Queue (SQ/CQ pair)

nvme_dev:设备表示

drivers/nvme/host/pci.c:294 定义 NVMe PCI 设备:

struct nvme_dev {
    struct nvme_queue     *queues;        /* 所有队列数组(0=admin)*/
    struct blk_mq_tag_set  tagset;        /* I/O 标签集 */
    struct blk_mq_tag_set  admin_tagset;  /* Admin 标签集 */
    u32 __iomem           *dbs;           /* 门铃寄存器基址 */
    struct device         *dev;
    unsigned               online_queues; /* 在线队列数 */
    unsigned               max_qid;       /* 最大队列 ID */
    unsigned               io_queues[HCTX_MAX_TYPES]; /* 每类型队列数 */
    unsigned int           num_vecs;      /* MSI-X 向量数 */
    u32                    q_depth;       /* 队列深度 */
    int                    io_sqes;       /* SQ Entry Size(log2)*/
    u32                    db_stride;     /* 门铃寄存器步长 */
    void __iomem          *bar;           /* BAR 映射 */
    bool                   cmb_use_sqes; /* 使用 CMB(控制器内存缓冲)*/
    struct nvme_ctrl       ctrl;          /* 通用控制器数据 */

    /* shadow doorbell 支持(减少 MMIO 写操作)*/
    __le32                *dbbuf_dbs;
    __le32                *dbbuf_eis;

    unsigned int           nr_write_queues; /* 写专用队列数 */
    unsigned int           nr_poll_queues;  /* 轮询队列数 */
};

nvme_queue:单个 SQ/CQ 对

drivers/nvme/host/pci.c:365

struct nvme_queue {
    struct nvme_dev    *dev;
    spinlock_t          sq_lock;        /* 提交队列自旋锁 */
    void               *sq_cmds;        /* SQ 命令数组(DMA 内存)*/
    spinlock_t          cq_poll_lock;   /* 轮询队列的 CQ 锁 */
    struct nvme_completion *cqes;       /* CQ 完成条目数组(DMA 内存)*/
    dma_addr_t          sq_dma_addr;    /* SQ 的 DMA 地址 */
    dma_addr_t          cq_dma_addr;    /* CQ 的 DMA 地址 */
    u32 __iomem        *q_db;           /* 门铃寄存器指针 */
    u32                 q_depth;        /* 队列深度 */
    u16                 cq_vector;      /* MSI-X 中断向量号 */
    u16                 sq_tail;        /* SQ 尾指针(Host 维护)*/
    u16                 last_sq_tail;   /* 上次写入门铃的 sq_tail */
    u16                 cq_head;        /* CQ 头指针(Host 维护)*/
    u16                 qid;            /* 队列 ID(0=admin)*/
    u8                  cq_phase;       /* CQ phase bit */
    u8                  sqes;           /* SQ Entry Size(log2)*/
    unsigned long       flags;          /* NVMEQ_ENABLED 等标志 */
    __le32             *dbbuf_sq_db;    /* shadow SQ doorbell */
    __le32             *dbbuf_cq_db;    /* shadow CQ doorbell */
};

Admin Queue vs I/O Queue

Queue ID = 0: Admin Queue (管理队列)
  - 专用于控制命令:Identify、Create I/O Queue、Set Features 等
  - 深度通常较小(64 或 128)
  - 使用独立的 blk_mq_tag_set (admin_tagset)

Queue ID = 1..N: I/O Queue (数据队列)
  - 用于读写数据命令
  - 深度由 io_queue_depth 参数控制(默认 1024,最大 4095)
  - 映射到 blk-mq 的 blk_mq_hw_ctx

SQ/CQ 提交/完成流程

                    Host                           NVMe 控制器
                      |                                 |
  blk_mq_submit_bio   |                                 |
          |           |                                 |
          v           |                                 |
  nvme_queue_rq()     |                                 |
          |           |                                 |
          v           |  写 SQE(64字节命令)到 sq_cmds[sq_tail]
  nvme_submit_cmd()   |                                 |
          |           |  sq_tail = (sq_tail+1) % q_depth|
          |           |                                 |
          v           |  写门铃寄存器(MMIO写)         |
  writel(sq_tail,     |----> q_db (SQ Tail Doorbell) -->|
         q_db)        |                                 |
                      |                                 | 控制器处理命令
                      |                                 | 执行 DMA 传输
                      |                                 |
                      |  (中断或轮询)                   |
                      |<-- MSI-X 中断 <--- 写 CQE ------+
                      |                                 |
  nvme_irq()          |                                 |
  nvme_process_cq()   |                                 |
          |           |  读取 cqes[cq_head](phase bit)|
          |           |  cq_head = (cq_head+1) % q_depth|
          v           |  写 CQ Head 门铃                |
  blk_mq_end_request()|----> cq_db (CQ Head Doorbell) ->|
                      |                                 |

phase bit 机制:CQ 不需要额外的写操作清零,而是通过 phase bit(0/1 交替)判断 CQE 是否有效:

/* phase=1 表示当前轮次的完成条目有效 */
if ((cqe->status & 1) == nvme_queue->cq_phase)
    /* 这是一个新的完成条目 */

门铃优化:Shadow Doorbell

在高 IOPS 场景下,频繁的 MMIO 写(写门铃寄存器)开销显著。NVMe 1.3 引入了 Controller Memory Buffer(CMB)技术,允许将门铃寄存器映射到控制器内存中,通过 DMA 写而非 MMIO 写更新门铃:

/* drivers/nvme/host/pci.c */
#define SQ_SIZE(q)  ((q)->q_depth << (q)->sqes)
#define CQ_SIZE(q)  ((q)->q_depth * sizeof(struct nvme_completion))

/* shadow doorbell: 先更新内存中的 shadow 值,
 * 只在真正需要时才写 MMIO 门铃 */
if (*nvmeq->dbbuf_sq_db != sq_tail)
    writel(sq_tail, nvmeq->q_db);

队列数量配置

/sys/module/nvme/parameters/
  io_queue_depth  = 1024     # 每队列深度
  write_queues    = 0        # 0=读写共用同一队列集
  poll_queues     = 0        # 轮询队列数(需 BLK_FEAT_POLL)

blk-mq 队列分配(以 8 核系统 + NVMe 8队列为例):
  HCTX_TYPE_DEFAULT: 8个HW队列(写/其他)
  HCTX_TYPE_READ:    8个HW队列(读,若 write_queues>0)
  HCTX_TYPE_POLL:    2个HW队列(轮询,若 poll_queues>0)

15. NVMe-oF(NVMe over Fabrics)

架构概述

NVMe-oF 将 NVMe 协议扩展到网络 fabric(如 RDMA、TCP、FC),使远程 NVMe 设备如同本地 NVMe 一样使用。

Host 端:
  blk-mq HW Queue
       |
  nvme_tcp / nvme_rdma / nvme_fc (传输层驱动)
       |
  网络 Fabric (TCP/RDMA/FC)
       |
  Target 端: nvmet_tcp / nvmet_rdma
       |
  nvmet (NVMe Target 核心)
       |
  后端存储(本地 NVMe / 文件)

传输类型

传输 模块 延迟 适用场景
NVMe/TCP nvme-tcp ~100μs+ 通用以太网
NVMe/RDMA nvme-rdma ~10μs+ InfiniBand/RoCE
NVMe/FC nvme-fc ~10μs+ 光纤通道 SAN
NVMe/Loop nvme-loop 极低 本地测试

与本地 NVMe 的关键差异

  1. 连接管理:NVMe-oF 需要建立 fabric 连接(队列对),类似 TCP 连接
  2. 传输开销:数据通过网络传输,延迟远高于本地 PCIe
  3. 队列映射:每个 fabric 连接对应一个 I/O 队列
  4. Capsule:NVMe-oF 将命令和数据封装在 capsule 中传输

NVMe-oF 主机侧初始化

nvme_tcp_alloc_io_queues()     <- 连接 target 的 I/O 队列
    |
    v
blk_mq_alloc_tag_set(&ctrl->tagset)
    |
    v
nvme_alloc_io_tag_set(ctrl, &ctrl->tagset, ops, nr_maps, cmd_size)
    |
    v
blk_mq_alloc_disk() / nvme_alloc_ns()

16. 块设备层架构:gendisk 与 block_device

gendisk:磁盘的内核表示

include/linux/blkdev.h:144 定义通用磁盘结构:

struct gendisk {
    int major;          /* 主设备号 */
    int first_minor;    /* 起始次设备号 */
    int minors;         /* 次设备号数量(分区数+1)*/

    char disk_name[DISK_NAME_LEN]; /* 设备名(如 nvme0n1)*/

    struct xarray         part_tbl;  /* 分区表(XArray 替代旧数组)*/
    struct block_device  *part0;     /* 整盘的 block_device */

    const struct block_device_operations *fops; /* 设备操作集 */
    struct request_queue *queue;     /* 请求队列 */
    void                 *private_data;

    struct bio_set  bio_split;       /* bio 分割用的内存池 */

    int             flags;           /* GENHD_FL_* */
    unsigned long   state;           /* GD_DEAD / GD_READ_ONLY 等 */

    struct mutex    open_mutex;
    unsigned        open_partitions;

    /* zoned 设备支持 */
    unsigned int    nr_zones;
    unsigned int    zone_capacity;
    // ...

    int         node_id;    /* NUMA 节点 */
    u64         diskseq;    /* 磁盘序列号(每次 add/del 递增)*/
    blk_mode_t  open_mode;
};

磁盘标志(include/linux/blkdev.h:87):

enum {
    GENHD_FL_REMOVABLE  = 1 << 0,  /* 可移除媒体 */
    GENHD_FL_HIDDEN     = 1 << 1,  /* 隐藏(多路径底层设备)*/
    GENHD_FL_NO_PART    = 1 << 2,  /* 不扫描分区 */
};

磁盘状态位(state 字段):

#define GD_NEED_PART_SCAN    0  /* 需要扫描分区 */
#define GD_READ_ONLY         1  /* 只读 */
#define GD_DEAD              2  /* 设备已失效 */
#define GD_NATIVE_CAPACITY   3  /* 使用原始容量 */
#define GD_ADDED             4  /* 已注册到系统 */
#define GD_SUPPRESS_PART_SCAN 5 /* 抑制分区扫描 */
#define GD_OWNS_QUEUE        6  /* disk 拥有 queue */

block_device:块设备实例

include/linux/blk_types.h:41 定义块设备(整盘或分区):

struct block_device {
    sector_t bd_start_sect;   /* 分区起始扇区(整盘=0)*/
    sector_t bd_nr_sectors;   /* 扇区数量 */
    struct gendisk   *bd_disk;  /* 所属 gendisk */
    struct request_queue *bd_queue; /* 请求队列(= disk->queue)*/
    struct disk_stats __percpu *bd_stats; /* per-CPU I/O 统计 */
    unsigned long    bd_stamp;
    atomic_t         __bd_flags;   /* 分区号 + BD_* 标志 */

    dev_t bd_dev;              /* 设备号 (major:minor) */
    struct address_space *bd_mapping; /* 页缓存 */

    atomic_t  bd_openers;     /* 打开计数 */
    spinlock_t bd_size_lock;
    void      *bd_claiming;   /* 独占声明者 */
    void      *bd_holder;     /* 持有者(文件系统超级块)*/
    int        bd_holders;    /* 持有计数 */

    atomic_t  bd_fsfreeze_count; /* 冻结请求计数 */
    struct mutex bd_fsfreeze_mutex;

    struct partition_meta_info *bd_meta_info; /* GPT 分区元数据 */
    struct device bd_device;  /* 内嵌 device(用于 sysfs)*/
} __randomize_layout;

bd_flagsinclude/linux/blk_types.h:48):

#define BD_PARTNO       255        /* 低8位:分区号 */
#define BD_READ_ONLY    (1u<<8)    /* 只读策略 */
#define BD_WRITE_HOLDER (1u<<9)    /* 有写持有者 */
#define BD_HAS_SUBMIT_BIO (1u<<10) /* fops->submit_bio 存在 */
#define BD_RO_WARNED    (1u<<11)   /* 已警告只读 */

gendisk 与 block_device 的关系

gendisk (磁盘)
    |
    +-- part0 ---------> block_device (整盘, partno=0)
    |                        bd_start_sect=0
    |                        bd_nr_sectors=全部
    |
    +-- part_tbl[1] --> block_device (第1分区, partno=1)
    |                        bd_start_sect=2048
    |                        bd_nr_sectors=1024000
    |
    +-- part_tbl[2] --> block_device (第2分区, partno=2)
                             bd_start_sect=1026048
                             bd_nr_sectors=8192000

request_queue:请求队列

include/linux/blkdev.h:478 定义请求队列:

struct request_queue {
    void               *queuedata;    /* 驱动私有数据 */
    struct elevator_queue *elevator;  /* I/O 调度器 */
    const struct blk_mq_ops *mq_ops;  /* blk-mq 操作集 */
    struct blk_mq_ctx __percpu *queue_ctx; /* per-CPU 软件队列 */
    unsigned long       queue_flags;
    unsigned int        queue_depth;  /* 总队列深度 */
    unsigned int        nr_hw_queues; /* 硬件队列数 */
    struct blk_mq_hw_ctx * __rcu *queue_hw_ctx; /* 硬件队列数组 */

    struct request     *last_merge;   /* 最近合并的请求(热路径优化)*/
    spinlock_t          queue_lock;

    struct gendisk     *disk;
    struct queue_limits limits;       /* 硬件限制 */

    struct blk_mq_tags *sched_shared_tags; /* 共享调度器标签 */
    struct blk_flush_queue *fq;       /* flush 队列 */
    struct rq_qos      *rq_qos;      /* QoS 链表 */
    struct throtl_data *td;           /* 节流数据(blk-throttle)*/

    struct blk_mq_tag_set *tag_set;   /* 标签集 */
    spinlock_t          requeue_lock;
    struct list_head    requeue_list;  /* 重排队列表 */
    struct delayed_work requeue_work;  /* 重排队延迟工作项 */
};

queue_flags 中重要的标志(include/linux/blkdev.h:653):

QUEUE_FLAG_DYING       /* 队列正在销毁 */
QUEUE_FLAG_NOMERGES    /* 禁用合并 */
QUEUE_FLAG_NOXMERGES   /* 禁用扩展合并 */
QUEUE_FLAG_QUIESCED    /* 队列已静止(不派发)*/
QUEUE_FLAG_SQ_SCHED    /* 单队列风格派发 */

17. 分区与 bd_mapping 页缓存集成

XArray 分区表

旧内核使用 disk_part_tbl(数组+RCU),新内核使用 xarray part_tblinclude/linux/blkdev.h:158):

struct gendisk {
    struct xarray  part_tbl;   /* key: partno, value: block_device* */
    struct block_device *part0; /* 整盘(partno=0)*/
    // ...
};

访问分区:

/* 通过分区号访问 */
struct block_device *bdev = xa_load(&disk->part_tbl, partno);

/* 遍历所有分区 */
xa_for_each(&disk->part_tbl, idx, bdev) {
    // 处理每个分区
}

bd_mapping:块设备页缓存

block_device.bd_mappinginclude/linux/blk_types.h:58)指向该块设备的 address_space,用于实现块设备级别的页缓存。

block_device
    |
    +-- bd_mapping --> address_space (块设备页缓存)
                           |
                           +-- i_pages (XArray: 块设备的页缓存)
                           +-- a_ops (address_space_operations)
                                  |
                                  +-- readpage:  block_read_full_folio
                                  +-- writepage: block_write_full_folio

作用

  1. 缓冲 I/O(Buffered I/O):文件系统通过 mapping 实现数据缓存,减少实际磁盘访问。
  2. 元数据缓存:读取分区表、超级块等元数据时通过 bd_mapping 缓存。
  3. 回写机制:脏页通过 writeback 机制异步写回磁盘。

bd_inode(已废弃/重构)

在旧版内核中,block_device 通过 bd_inode 关联到一个特殊的块设备 inode。新版内核将 address_space 直接嵌入或通过 bd_mapping 指针管理,简化了结构层次。


18. Direct I/O 与 Buffered I/O

Buffered I/O 路径

write(fd, buf, len)
    |
    v
vfs_write()
    |
    v
generic_file_write_iter()       [文件系统通用写路径]
    |
    v
pagecache_write_begin()         [获取页缓存页]
    |
    v
copy_from_user_enhanced()       [从用户空间复制数据到页缓存]
    |
    v
pagecache_write_end()           [标记页为脏]
    |
    v
balance_dirty_pages_ratelimited() [检查脏页是否超限]
    |
    (后台) writeback_inodes_sb_nr()
              |
              v
         write_cache_pages()
              |
              v
         submit_bio(bio)         [最终提交到块设备层]

Direct I/O 路径

Direct I/O 绕过页缓存,直接将用户缓冲区 DMA 到设备:

write(fd, buf, len) with O_DIRECT 或 io_uring with IORING_OP_WRITE
    |
    v
generic_file_write_iter()
    |
    | kiocb->ki_flags & IOCB_DIRECT
    v
iomap_dio_rw() / direct_IO()     [文件系统 direct I/O 实现]
    |
    v
bio_iov_iter_get_pages()         [将用户页面 pin 住(不复制数据)]
    |
    v
submit_bio(bio)                  [直接提交 bio,bi_io_vec 指向用户页]
    |
    v
设备 DMA 直接读写用户内存

O_DIRECT 的限制

  1. 缓冲区地址必须满足设备对齐要求(通常 512 字节)
  2. 传输长度必须是逻辑块大小的整数倍
  3. 文件偏移也必须对齐
  4. 读写需要等待完成(或使用 io_uring 异步)

io_uring:新一代异步 I/O

io_uring(Linux 5.1+)提供了真正的异步提交和完成机制,通过共享内存的 SQ/CQ 环形队列与内核通信:

用户空间                    内核空间
  |                             |
  | io_uring_enter()            |
  |                             |
SQE ring ──提交──────────────> io_uring_submit()
  |                             |    |
  |                             |    v
  |                             |  blk-mq / 直接 I/O 路径
  |                             |    |
  |                             |    v
CQE ring <──完成──────────────  io_uring_complete()
  |                             |
  | (轮询或 eventfd 通知)       |

19. I/O 统计与 blkcg 限速

part_stat:I/O 统计

块设备的 I/O 统计通过 disk_stats 结构(per-CPU)维护:

/* include/linux/part_stat.h */
#define part_stat_lock()    preempt_disable()
#define part_stat_unlock()  preempt_enable()

/* 读取统计(any context)*/
#define part_stat_read(part, field) \
    ({ \
        typeof_field(struct disk_stats, field) res = 0; \
        unsigned int cpu; \
        for_each_possible_cpu(cpu) \
            res += per_cpu_ptr(part->bd_stats, cpu)->field; \
        res; \
    })

/* 修改统计(需在 preempt_disable 区间内)*/
#define __part_stat_add(part, field, addnd) \
    (part_stat_get(part, field) += (addnd))

iostat 中的字段来源

iostat 字段 内核统计字段 描述
r/s ios[STAT_READ] 读操作数/秒
w/s ios[STAT_WRITE] 写操作数/秒
rkB/s sectors[STAT_READ] 读扇区数/秒
wkB/s sectors[STAT_WRITE] 写扇区数/秒
r_await nsecs[STAT_READ] / ios 平均读延迟
w_await nsecs[STAT_WRITE] / ios 平均写延迟
%util in_flight[0] / 采样时间 设备利用率

blkcg:块设备 cgroup

blkcg 是块设备层的 cgroup 控制器,实现资源隔离与限速。

cgroup v2 接口

/sys/fs/cgroup/<group>/
    io.weight       - I/O 权重(1-10000,默认100)
    io.max          - 带宽/IOPS 限制
    io.stat         - I/O 统计
    io.pressure     - I/O 压力

io.max 格式:

echo "8:0 rbps=10485760 wbps=20971520" > io.max
# 设置 /dev/sda (8:0) 的读带宽限制为 10MB/s,写为 20MB/s

echo "8:0 riops=1000 wiops=2000" > io.max
# 设置读 1000 IOPS,写 2000 IOPS

blk-throttle:带宽/IOPS 限速

blk-throttle 是实现 io.max 的核心模块(block/blk-throttle.c):

bio 提交路径:
    submit_bio()
        |
        v
    rq_qos_throttle()           [rq_qos 框架钩子]
        |
        v
    tg_throttle_down()          [blk-throttle 节流]
        |
        +-- 读取当前组的 bps/iops 限制
        +-- 计算是否超限
        +-- 超限:io_schedule() 等待令牌补充
        +-- 未超限:更新计数,继续

令牌补充通过定时器(throtl_pending_timer_fn)周期性执行。

io.weight 实现(BFQ/BFQ-based)

当使用 BFQ 调度器时,io.weight 直接映射到 BFQ 的队列权重:

io.weight=100  ->  bfq_group.weight=100
io.weight=200  ->  bfq_group.weight=200 (获得2倍 I/O 份额)

使用 mq-deadline 或 none 时,io.weight 通过 bfq 的 cgroup 权重处理层实现(如果配置了的话)。

request_queue 的 rq_qos

rq_qos(Request Quality of Service)是 blk-mq 的 QoS 框架,允许链式挂载多个 QoS 策略:

request_queue.rq_qos (链表):
    +-- blk-throttle   (throttl_data)  <- io.max 实现
    +-- wbt            (rq_wb)         <- 写回节流
    +-- iocost         (ioc_data)      <- 基于代价的 I/O 控制

每个 rq_qos 实现以下回调:

struct rq_qos_ops {
    void (*throttle)(struct rq_qos *, struct bio *);
    void (*track)(struct rq_qos *, struct request *, struct bio *);
    void (*merge)(struct rq_qos *, struct request *, struct bio *);
    void (*issue)(struct rq_qos *, struct request *);
    void (*requeue)(struct rq_qos *, struct request *);
    void (*done)(struct rq_qos *, struct request *);
    void (*done_bio)(struct rq_qos *, struct bio *);
    void (*cleanup)(struct rq_qos *, struct bio *);
    void (*queue_depth_changed)(struct rq_qos *);
    void (*exit)(struct rq_qos *);
};

20. 错误处理与重试机制

blk_status_t 错误码

include/linux/blk_types.h:96 定义块层错误码:

typedef u8 __bitwise blk_status_t;

#define BLK_STS_OK              0   /* 成功 */
#define BLK_STS_NOTSUPP         1   /* 操作不支持 */
#define BLK_STS_TIMEOUT         2   /* 超时 */
#define BLK_STS_NOSPC           3   /* 无空间 */
#define BLK_STS_TRANSPORT       4   /* 传输层错误(路径相关)*/
#define BLK_STS_TARGET          5   /* 目标设备错误 */
#define BLK_STS_RESV_CONFLICT   6   /* 预留冲突 */
#define BLK_STS_MEDIUM          7   /* 介质错误(坏块)*/
#define BLK_STS_PROTECTION      8   /* 数据完整性保护失败 */
#define BLK_STS_RESOURCE        9   /* 内存等系统资源不足 */
#define BLK_STS_IOERR          10   /* 通用 I/O 错误 */
#define BLK_STS_DM_REQUEUE     11   /* DM 重排队 */
#define BLK_STS_AGAIN          12   /* 非阻塞请求会阻塞 */
#define BLK_STS_DEV_RESOURCE   13   /* 设备资源不足(有飞行中 I/O 时会释放)*/
#define BLK_STS_ZONE_OPEN_RESOURCE 14 /* Zone 打开资源超限 */
#define BLK_STS_ZONE_ACTIVE_RESOURCE 15 /* Zone 活跃资源超限 */
#define BLK_STS_OFFLINE        16   /* 设备离线 */
#define BLK_STS_DURATION_LIMIT 17   /* 命令时长超限 */
#define BLK_STS_INVAL          19   /* 无效大小或对齐 */

blk_path_error:路径相关错误分类

include/linux/blk_types.h:185

static inline bool blk_path_error(blk_status_t error)
{
    switch (error) {
    case BLK_STS_NOTSUPP:
    case BLK_STS_NOSPC:
    case BLK_STS_TARGET:
    case BLK_STS_RESV_CONFLICT:
    case BLK_STS_MEDIUM:
    case BLK_STS_PROTECTION:
        return false; /* 不可重试(路径切换无用)*/
    }
    return true;  /* 其他错误可通过故障转移路径重试 */
}

这个函数被多路径(DM multipath / NVMe multipath)使用,判断是否需要切换路径重试。

blk-mq 超时处理

每个请求都有超时定时器,由 request_queue.timeout 定时器周期检查:

blk_mq_timeout_work()           [超时工作项]
    |
    v
blk_mq_check_expired()          [对每个 in-flight 请求检查]
    |
    +-- rq->deadline > jiffies? 继续等待
    +-- 超时:调用 ops->timeout(rq)
               |
               返回 BLK_EH_DONE:
                   blk_mq_force_complete_rq() 强制完成
               返回 BLK_EH_RESET_TIMER:
                   blk_add_timer(rq) 重置定时器

SCSI Error Recovery 与 blk-mq

SCSI 错误恢复层(EH,Error Handler)是一个独立线程,负责处理复杂的错误场景:

SCSI 命令完成(错误)
    |
    v
scsi_io_completion()
    |
    +-- 可重试错误(如 UNIT_ATTENTION)
    |       v
    |   scsi_queue_insert() -> 重新插入队列
    |
    +-- 需要 EH 的错误
    |       v
    |   scsi_eh_scmd_add() -> EH 线程
    |           |
    |           v
    |       scsi_error_handler()
    |           +-- TUR (Test Unit Ready)
    |           +-- REQUEST SENSE
    |           +-- 设备/总线/主机 复位
    |           +-- 成功: scsi_eh_flush_done_q() 重新提交
    |           +-- 失败: blk_mq_fail_request()
    |
    +-- 不可恢复错误
            v
        blk_mq_end_request(rq, BLK_STS_IOERR)

blk_mq_requeue_request

include/linux/blk-mq.h:926 是请求重排队的主要接口:

void blk_mq_requeue_request(struct request *rq, bool kick_requeue_list);

使用场景:

  1. 资源临时不足queue_rq 返回 BLK_STS_RESOURCEBLK_STS_DEV_RESOURCE 时,blk-mq 自动调用此函数
  2. 驱动显式重排队:多路径切换时,失败路径的请求需要重排队到成功路径
  3. DM 重映射:Device Mapper 目标设备在重映射时重排队

重排队流程:

blk_mq_requeue_request(rq, kick)
    |
    v
blk_mq_add_to_requeue_list(rq)      [加入 q->requeue_list]
    |
    +-- kick=true:
    |   blk_mq_kick_requeue_list(q) [立即调度 requeue_work]
    |
    +-- kick=false:
        等待下次调度
    |
    v
blk_mq_requeue_work()               [工作项执行]
    |
    v
blk_mq_dispatch_rq_list() 或 elevator 重新入队

错误处理决策树

queue_rq() 返回值
    |
    +-- BLK_STS_OK:
    |       请求已接受,等待完成回调
    |
    +-- BLK_STS_RESOURCE:
    |       系统资源不足(内存等)
    |       -> blk_mq_requeue_request(rq, true)
    |       -> 停止队列直到资源释放
    |
    +-- BLK_STS_DEV_RESOURCE:
    |       设备资源不足(飞行中 I/O 完成后会释放)
    |       -> blk_mq_stop_hw_queue(hctx)
    |       -> 完成回调中调用 blk_mq_start_hw_queue()
    |
    +-- BLK_STS_IOERR / 其他:
            请求失败
            -> blk_mq_end_request(rq, error)
            -> bio->bi_end_io(bio) 通知上层

重试与 noretry

blk_noretry_request() 宏(include/linux/blkdev.h:697)检查请求是否有 FAILFAST 标志:

#define blk_noretry_request(rq) \
    ((rq)->cmd_flags & (REQ_FAILFAST_DEV |
                        REQ_FAILFAST_TRANSPORT |
                        REQ_FAILFAST_DRIVER))
  • REQ_FAILFAST_DEV:不重试设备错误
  • REQ_FAILFAST_TRANSPORT:不重试传输错误
  • REQ_FAILFAST_DRIVER:不重试驱动错误

设置了任一 FAILFAST 标志的请求,在遇到对应错误时立即失败,不进行重试。


21. block/ 目录主要文件一览

block/
├── blk-core.c          # 块层核心:submit_bio、queue 管理
├── blk-mq.c            # blk-mq 多队列核心实现
├── blk-mq-tag.c        # 标签分配/释放(sbitmap)
├── blk-mq-sched.c      # blk-mq 调度器接口层
├── blk-merge.c         # bio/request 合并逻辑
├── blk-flush.c         # flush/fua 命令处理
├── blk-throttle.c      # blk-throttle(io.max 实现)
├── blk-cgroup.c        # blkcg cgroup 控制器
├── blk-iocost.c        # iocost I/O 代价控制器
├── blk-wbt.c           # writeback throttle(写回节流)
├── blk-zoned.c         # Zoned Block Device 支持
├── blk-integrity.c     # T10 DIF 数据完整性
├── blk-stat.c          # I/O 统计框架
├── elevator.c          # I/O 调度器框架核心
├── elevator.h          # elevator 内部头文件
├── mq-deadline.c       # mq-deadline 调度器
├── bfq-iosched.c       # BFQ 调度器主文件
├── bfq-wf2q.c          # B-WF2Q+ 调度算法
├── bfq-cgroup.c        # BFQ cgroup 支持
├── kyber-iosched.c     # Kyber 调度器
├── partitions/         # 分区表解析(GPT/MBR/...)
│   ├── efi.c           # GPT/EFI 分区解析
│   └── msdos.c         # MBR 分区解析
└── ...

22. 性能调优指南

调度器选择

# 查看当前调度器
cat /sys/block/nvme0n1/queue/scheduler
# 输出: [none] mq-deadline kyber

# 切换调度器
echo "mq-deadline" > /sys/block/sda/queue/scheduler

# 推荐配置:
# NVMe SSD:  none 或 kyber(低延迟,高吞吐)
# SATA SSD:  mq-deadline(防止写饥饿)
# HDD:       mq-deadline(扇区排序,减少磁头移动)
# 虚拟机磁盘: none(hypervisor 层已做调度)
# 数据库服务器(混合 I/O): BFQ 或 mq-deadline

队列深度调整

# 查看队列深度
cat /sys/block/nvme0n1/queue/nr_requests

# 增大队列深度(适合高 IOPS 场景)
echo 256 > /sys/block/nvme0n1/queue/nr_requests

# 对于 HDD,较小的队列深度可以减少延迟
echo 16 > /sys/block/sda/queue/nr_requests

合并控制

# 查看合并配置
cat /sys/block/sda/queue/nomerges
# 0: 允许所有合并
# 1: 只允许简单合并
# 2: 不允许合并

# 禁用合并(适合随机 I/O 为主的 NVMe)
echo 2 > /sys/block/nvme0n1/queue/nomerges

I/O 限速(blkcg)

# 创建 cgroup
mkdir /sys/fs/cgroup/io/myapp

# 设置带宽限制(8:0 = sda)
echo "8:0 rbps=104857600 wbps=52428800" > /sys/fs/cgroup/io/myapp/io.max

# 设置 IOPS 限制
echo "8:0 riops=1000 wiops=500" > /sys/fs/cgroup/io/myapp/io.max

# 将进程加入 cgroup
echo $PID > /sys/fs/cgroup/io/myapp/cgroup.procs

关键 sysfs 参数

/sys/block/<dev>/queue/
├── scheduler           # I/O 调度器
├── nr_requests         # 队列深度
├── read_ahead_kb       # 预读大小(KB)
├── nomerges            # 合并控制
├── rotational          # 旋转设备标志(影响调度决策)
├── rq_affinity         # 请求完成 CPU 亲和性
│   # 0: 不限制; 1: 在提交 CPU 完成; 2: 在提交 CPU 组完成
├── max_sectors_kb      # 单请求最大扇区数
├── discard_max_bytes   # discard 最大字节数
└── wbt_lat_usec        # writeback throttle 延迟目标(微秒)

延迟优化(deadline 调度器)

# 减小读超时(适合延迟敏感型读)
echo 250 > /sys/block/sda/queue/iosched/read_expire  # 250ms

# 增大写超时(允许写更多等待,提升读吞吐)
echo 10000 > /sys/block/sda/queue/iosched/write_expire  # 10s

# 调整批处理大小
echo 8 > /sys/block/sda/queue/iosched/fifo_batch

NVMe 特有调优

# 查看 NVMe 队列数
ls /sys/class/nvme/nvme0/

# 设置轮询队列(用于低延迟 I/O)
modprobe nvme poll_queues=2

# 使用 io_uring + 轮询获得最低延迟
# io_uring_setup() with IORING_SETUP_IOPOLL

常见性能问题排查

# 查看 I/O 统计
iostat -x 1

# 查看 blk-mq 统计(debugfs)
cat /sys/kernel/debug/block/nvme0n1/hctx0/run

# 查看合并效率
cat /proc/diskstats
# 字段 9: 合并的读请求数; 字段 11: 合并的写请求数

# 查看 I/O 调度器队列
cat /sys/kernel/debug/block/sda/sched/

附录:关键函数调用汇总

bio 相关

函数 文件 功能
bio_alloc() block/bio.c 分配 bio
bio_add_page() block/bio.c 添加页面到 bio
submit_bio() block/blk-core.c 提交 bio
bio_endio() block/bio.c 完成 bio,调用 bi_end_io
bio_split() block/bio.c 分割超限的 bio

request 相关

函数 文件 功能
blk_mq_alloc_request() block/blk-mq.c 分配 request
blk_mq_start_request() block/blk-mq.c 标记开始处理
blk_mq_end_request() block/blk-mq.c 完成请求
blk_mq_requeue_request() block/blk-mq.c 重排队
blk_update_request() block/blk-mq.c 更新请求进度

队列操作

函数 文件 功能
blk_mq_run_hw_queue() block/blk-mq.c 运行硬件队列
blk_mq_stop_hw_queue() block/blk-mq.c 停止硬件队列
blk_mq_freeze_queue() block/blk-mq.c 冻结队列(切换调度器用)
blk_start_plug() block/blk-core.c 开始 plug
blk_finish_plug() block/blk-core.c 结束 plug,触发提交

完整数据流 ASCII 图

应用进程
  |  write()/io_uring
  v
VFS Layer
  |  vfs_write() -> generic_file_write_iter()
  v
Page Cache (Buffered I/O)        或        Direct I/O
  |  标记脏页                               |  pin 用户页
  |  writeback daemon                       |
  v                                         v
bio_alloc() + bio_add_page()         bio_alloc() + bio_iov_iter_get_pages()
  |                                         |
  +-------------------+--------------------+
                      |
                      v
               submit_bio(bio)
                      |
                      v
            blk_mq_submit_bio()           [block/blk-mq.c]
                      |
           +----------+----------+
           |                     |
    尝试 plug 合并         分配 request (tag)
    blk_attempt_plug_merge  blk_mq_get_tag()
           |                     |
           +----------+----------+
                      |
               current->plug?
               /           \
             YES             NO
              |               |
         加入 plug           直接 run_hw_queue
         mq_list              |
              |               |
     blk_finish_plug()        |
              |               |
              +-+-------------+
                |
                v
    blk_mq_dispatch_rq_list()
                |
    I/O 调度器存在?
    /             \
  YES              NO
   |                |
elevator            blk_mq_dequeue_from_ctx()
dispatch_request()  |
   |                |
   +-------+--------+
           |
           v
    ops->queue_rq(hctx, &bd)      [驱动层]
           |
    (NVMe) nvme_queue_rq()
           |
    写 SQE + 敲门铃
           |
    (NVMe 控制器执行 DMA)
           |
    MSI-X 中断 / io_uring 轮询
           |
           v
    nvme_process_cq()
           |
    blk_mq_end_request(rq, status)
           |
    bio_endio(bio)
           |
    bi_end_io(bio)                 [回调上层:文件系统/应用]
           |
           v
    read()/write() 返回

由 Claude Code 分析生成