Linux 块设备层深度实战 I:从 bio 请求生命周期到 blk-mq 多队列架构的全栈解析

块设备层(Block Layer)是 Linux I/O 栈的核心枢纽,位于 VFS 之下、存储设备驱动之上。它负责将文件系统的 I/O 请求转化为对物理/虚拟磁盘的读写操作,是整个存储 I/O 路径中调度、合并、排队、重排序的关键环节。虽然块设备层不像 VFS 或文件系统那样被频繁提及,但它在高并发数据库、分布式存储、容器化环境中扮演着决定性角色。

本文将从 bio 结构体的深度剖析出发,完整追踪一次 I/O 请求从发起到完成的全生命周期,深入讲解请求合并机制、Plugging 机制、传统单队列与 blk-mq 多队列架构的演进逻辑,并结合 NVMe/scsi-mq 驱动、io_uring、生产性能调优场景给出最佳实践。

一、块设备层架构总览

在深入细节之前,先建立全局视角。块设备层由以下几个核心抽象组成:

层级核心结构职责
上层(VFS/FS)struct bio, page cache构建 I/O 请求,提交给块设备层
块设备层核心struct request_queue, struct request, struct bio_vec请求合并、调度、排队、分发
I/O 调度层elevator_ops, blk_mq_ops决定请求的执行顺序(mq-deadline/bfq/kyber)
设备驱动层scsi_dispatch_cmd, nvme_queue将请求翻译为设备命令并提交硬件

整条数据路径为:用户空间 → VFS → 文件系统 → Page Cache(writeback) → submit_bio() → 块设备层(合并/调度) → request_queue → 驱动 → 硬件。

二、struct bio — 块 I/O 的最小原语

struct bio 是块设备层中最核心的数据结构,代表一次块 I/O 操作的最小单元。在 <linux/blk_types.h> 中定义:

struct bio {
    struct bio          *bi_next;       // 链表指针,用于合并
    struct block_device *bi_bdev;       // 目标块设备
    blk_status_t         bi_status;     // I/O 完成状态
    struct bvec_iter     bi_iter;       // 当前迭代位置(扇区/页内偏移)
    
    unsigned short       bi_vcnt;       // bio_vec 数量
    unsigned short       bi_max_vecs;   // bio_vec 数组容量
    atomic_t             bi_remaining;  // 剩余未完成段数
    struct bio_vec      *bi_io_vec;     // bio_vec 数组(实际数据段)
    
    bio_end_io_t        *bi_end_io;     // 完成回调
    void                *bi_private;    // 私有数据(文件系统使用)
    
    struct blkcg_gq     *bi_blkg;       // blk-cgroup 关联
    struct bio_issue     bi_issue;      // 提交时间戳(延迟统计)
    // ... (约60+个字段)
};

每个 bio 可以包含多个 不连续 的内存页,通过 bio_vec 数组描述:

struct bio_vec {
    struct page *bv_page;   // 物理页指针
    unsigned int bv_len;    // 字节长度
    unsigned int bv_offset; // 页内偏移
};

一个 bio 最多可包含 BIO_MAX_VECS(通常 256)个 bio_vec。如果单次 I/O 需要的页数超过此上限,文件系统会将其拆分为多个 bio,通过 bi_next 链接成一个 bio chain。

2.1 bio 的生命周期:从 submit_bio 到完成

一个 bio 的典型生命周期可分为以下阶段:

// 阶段 1: 分配 bio
struct bio *bio = bio_alloc(bdev, nr_vecs, REQ_OP_READ, GFP_NOIO);

// 阶段 2: 填充数据
bio->bi_iter.bi_sector = sector;           // 起始扇区
bio_add_page(bio, page, len, offset);      // 添加内存页
bio->bi_end_io = my_end_io;                // 设置完成回调
bio->bi_private = my_data;

// 阶段 3: 提交
submit_bio(bio);
    └─ submit_bio_noacct(bio)
        └─ __submit_bio_noacct(bio)
            ├─ 尝试合并(bio_attempt_front_merge / bio_attempt_back_merge)
            ├─ 若无法合并 → bio_alloc_clone(拆分为两个 bio,各自排队)
            └─ blk_mq_submit_bio(bio)  // blk-mq 路径
                └─ blk_mq_bio_to_request() / blk_mq_dispatch_rq_list()

// 阶段 4: I/O 完成 → bi_end_io 回调
static void my_end_io(struct bio *bio) {
    if (bio->bi_status != BLK_STS_OK)
        pr_err("I/O error: %d\n", bio->bi_status);
    bio_put(bio);  // 释放 bio(引用计数 → 0)
}

2.2 bio 拆分机制(bio_split)

当 bio 跨越磁盘的物理段边界(由 queue_max_segment_size 和 queue_max_sectors 决定)时,块设备层需要将其拆分为多个 bio。核心函数是 bio_split_bioset() 和 blk_queue_split():

// blk-merge.c
bool __blk_queue_split(struct request_queue *q, struct bio **bio) {
    struct bio *split;
    unsigned int nr_sectors = bio_sectors(*bio);
    
    // 检查是否需要按 segment 大小拆分
    if (nr_sectors > queue_max_sectors(q)) {
        split = bio_split(*bio, queue_max_sectors(q), GFP_NOIO, &fs_bio_set);
        bio_chain(split, *bio);
        submit_bio_noacct(*bio);  // 前半部先提交
        *bio = split;             // 后半部下一次循环处理
        return true;
    }
    
    // 检查是否需要按段边界拆分(dma_get_max_seg_boundary)
    // ...
}

拆分后的 bio 通过 bio_chain() 链接成一个链表,前面的 bio 先被提交,后面的 bio 作为"残留 bio"由上层循环继续处理。

三、请求合并:减少 I/O 请求总数

块设备层的核心优化之一就是请求合并,将多个相邻的 bio 合并为一个更大的 request,减少驱动层处理的开销。Linux 支持两种合并方式:

3.1 Back Merge(向后合并)

新提交的 bio 的起始扇区正好等于某个已存在 request 的结束扇区:

// blk-merge.c
static inline bool bio_attempt_back_merge(struct request_queue *q,
                                           struct request *req, struct bio *bio)
{
    // 检查 sector 连续、设备相同、权限兼容、queue constraints 满足
    if (!rq_mergeable(req)) return false;
    if (req->__sector + blk_rq_sectors(req) != bio->bi_iter.bi_sector) return false;
    if (!bio_crypt_back_mergeable(req, bio)) return false;
    // ...
    
    // 执行合并
    elv_merge_requests(q, req, bio);
        └─ req->biotail->bi_next = bio;  // 追加到 bio chain 末尾
    return true;
}

3.2 Front Merge(向前合并)

新提交的 bio 的结束扇区正好等于某个已存在 request 的起始扇区:

static inline bool bio_attempt_front_merge(struct request_queue *q,
                                            struct request *req, struct bio *bio)
{
    if (req->__sector != bio_end_sector(bio)) return false;
    // ...
    bio->bi_next = req->bio;         // 插入到 bio chain 头部
    req->__sector = bio->bi_iter.bi_sector;
    return true;
}

3.3 Plugging 机制:批量合并的关键

Plugging(插塞)机制是块设备层性能优化的核心。当进程批量提交多个 I/O 请求时,块设备层不会立即将请求下发给驱动,而是"塞住"(plug),等待短暂的延迟窗口。如果有新请求到来并与已塞住的请求相邻,就可以合并。

// include/linux/blkdev.h
struct blk_plug {
    struct list_head mq_list;       // blk-mq 请求链表
    struct list_head cb_list;       // flush 回调链表
    unsigned short rq_count;        // 当前塞住的请求数
    unsigned short do_plug:1;       // 是否启用 plug
};

// 开启 plug
blk_start_plug(plug);
    └─ 设置 current->plug = plug;

// ... 批量 submit_bio() ...

// 关闭 plug(自动 flush)
blk_finish_plug(plug);
    └─ blk_flush_plug_list(plug, true);
        └─ blk_mq_flush_plug_list()  // 一次性下发所有请求

在 blk-mq 架构下,plug 机制还有一个重要的作用:通过 blk_mq_flush_plug_list() 将请求按硬件队列分发,这比逐个分发的效率要高得多。

四、传统单队列的瓶颈与演进

4.1 单队列时代的架构

在 blk-mq 引入之前,Linux 使用单队列架构,所有 CPU 共享一个请求队列 request_queue。核心问题:

  • 全局自旋锁:queue_lock(一个 spinlock)保护整个请求队列,高并发时锁竞争严重
  • 单一队列深度:只有一个 request_list,所有请求排队等待
  • NUMA 不友好:所有 CPU 争抢同一个队列,跨 NUMA 节点访问频繁
  • 锁粒度粗:__blk_run_queue() 期间持有 queue_lock,阻塞所有其他 CPU

4.2 锁竞争量化

在 8 核 CPU 上运行 fio(4K 随机读,iodepth=64),单队列模式下的 queue_lock 争用会导致约 30-40% 的吞吐量损失。随着核数增加,损失趋近于 70% 以上。这正是 blk-mq 被提出的背景(2015 年 4.3 内核引入)。

五、blk-mq 多队列架构详解

5.1 架构设计:两层队列

blk-mq 引入了两层队列模型:

队列层数量作用竞争点
Software Queue (sw queue)每 CPU 或 NUMA 节点一个CPU 本地发送请求,处理后 dispatch 到硬件队列几乎无锁(per-cpu 分配)
Hardware Dispatch Queue (hctx)由驱动指定(核心映射)驱动可见的硬件提交队列,直接写入设备硬件队列深度(NVMe 可达 64K)

两层之间通过 blk_mq_hw_ctx->dispatch 链表连接:软件队列先将请求放入 dispatch 列表,再由驱动侧从 dispatch 列表中取出并写入硬件寄存器。

5.2 核心数据结构

struct blk_mq_tag_set {
    struct blk_mq_ops *ops;         // 驱动回调(queue_rq、commit_rqs)
    unsigned int       nr_hw_queues;     // 硬件队列数
    unsigned int       queue_depth;      // 每队列请求槽数
    unsigned int       cmd_size;         // 每命令私有数据大小
    int                numa_node;        // NUMA 节点
    struct blk_mq_tags **tags;           // hw_queue→blk_mq_tags 映射
    struct list_head   tag_list;         // 用户链表(共享 tag 的驱动间复用)
};

struct blk_mq_hw_ctx {
    struct blk_mq_queue_map map;         // sw→hw 映射
    struct list_head       dispatch;      // 待分发请求链表
    unsigned long          state;         // HCTX_INACTIVE/SCHED_BUSY 等
    struct sbitmap         ctx_bitmap;    // 软件 ctx 位图分配
    struct blk_mq_ctx    **ctxs;          // 拥有的软件上下文数组
    struct request_queue  *queue;         // 所属 request_queue
    atomic_t               nr_active;     // 活跃请求数
    // ...
};

struct blk_mq_ctx {
    struct blk_mq_hw_ctx *hctx;          // 归属的硬件队列
    spinlock_t            lock;          // 私有自旋锁(仅含 rq_queues 操作)
    struct list_head      rq_lists[HCTX_MAX_TYPES]; // 按优先级分类的请求列表
    unsigned int          cpu;           // 绑定的 CPU
    // ...
};

5.3 request 分配与 tag 机制

blk-mq 中每个 request 有一个全局唯一的 tag 编号,用于在命令完成时快速定位对应的 request。tag 分配通过 sbitmap(可扩展位图)高效实现:

// blk-mq-tag.c
unsigned int blk_mq_get_tag(struct blk_mq_alloc_data *data) {
    struct blk_mq_tags *tags = data->hctx->tags;
    struct sbitmap_queue = &tags->bitmap_tags;
    
    // sbitmap 原子分配一个 0-65535 的编号
    tag = sbitmap_queue_get(sbq, &data->ctx->index);
    if (tag != -1) {
        struct request *rq = tags->static_rqs[s_tag];
        rq->tag = tag;
        rq->internal_tag = -1;
        blk_mq_put_ctx(data->ctx);  // 释放 cpu 引用
    }
    return tag;
}

每个硬件队列拥有独立的 tag 空间(queue_depth 个槽位)。如果 tag 耗尽,blk_mq_get_tag() 返回 -1,软件队列进入 SCHED_BUSY 状态,停止接纳新请求直到有 tag 释放。

5.4 请求分发流程(blkmq_submit_bio)

void blk_mq_submit_bio(struct bio *bio) {
    struct request_queue *q = bio->bi_bdev->bd_disk->queue;
    struct blk_mq_hw_ctx *hctx;
    struct request *rq;
    blk_status_t ret;
    
    // 1. plug 检查:如果当前 plug 已满,先 flush
    if (blk_mq_sched_bio_merge(q, bio, &rq)) {
        // 合并进已有 request(返回 true)→ 直接返回
        return;
    }
    
    // 2. 根据映射关系选择 hw_ctx
    hctx = blk_mq_map_queue(q, cpu);
    
    // 3. 分配 request 和 tag
    rq = blk_mq_alloc_request(hctx, bio->bi_opf, 0);
    if (IS_ERR(rq)) {
        bio_io_error(bio);
        return;
    }
    
    // 4. bio → request 转换(初始化 request 各字段)
    blk_init_request_from_bio(rq, bio);
    
    // 5. Plugging 模式下:塞入 plug 列表,等待 flush
    if (plug) {
        blk_add_rq_to_plug(plug, rq);
    } else {
        // 直接下发
        blk_mq_try_issue_directly(hctx, rq);
    }
}

5.5 硬件队列映射策略

blk-mq 提供两种映射模式:

映射模式函数适用场景
cpu (默认)blk_mq_map_queue_by_ctx软队列绑定 CPU,适合单 socket 服务器
hw_ctx (NUMA)blk_mq_hctx_to_nodeNUMA 架构,硬件队列绑定 NUMA 节点

NVMe 驱动中可以通过 --nr_write_queues 和 --nr_poll_queues 参数调整写队列和轮询队列数量,实现更精细的 CPU 亲和控制。

六、I/O 调度器:从 elevator 到 blk-mq 调度器

6.1 传统 Elevator 调度器

在单队列时代,elevator 是一个可插拔的请求排序器,核心操作包括:

struct elevator_ops {
    .elevator_merge_fn      = * ElevatorMerge,    // 请求合并
    .elevator_merged_fn     = * ElevatorMerged,   // 合并后回调
    .elevator_merge_req_fn  = * ElevatorMergeReq, // request 间合并
    .elevator_dispatch_fn   = * ElevatorDispatch, // 从队列取出发给驱动
    .elevator_insert_fn     = * ElevatorInsert,   // 插入请求
    .elevator_remove_fn     = * ElevatorRemove,   // 移除请求
    .elevator_set_req_fn    = * ElevatorSetReq,   // 请求初始插入
    .elevator_init_fn       = * ElevatorInit,     // 初始化
    .elevator_exit_fn       = * ElevatorExit,     // 退出
};

常见调度算法:noop(无排序,仅合并)、deadline(按过期时间排序,写读分槽)、cfq(完全公平队列,每个进程独立队列)、bfq(预算公平,基于进程权重)。

6.2 blk-mq 调度器

blk-mq 时代引入了专门针对多队列架构的调度器(blk-mq-sched),通过额外的 调度队列 实现更细粒度的控制:

调度器算法核心适用场景
mq-deadline排序队列 + 读优先 + 写超时通用场景,NVMe 默认,延迟敏感
bfqBudget Fair Queueing,按进程带宽分配权重桌面/交互式,需要 I/O 隔离
kyber自适应延迟目标(读/写分别控制)NVMe,延迟可预测
none回退到 soft plug 模式分发极低延迟设备(Intel Optane)

切换调度器很简单:

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

# 临时切换
echo "bfq" > /sys/block/nvme0n1/queue/scheduler

# 永久生效(GRUB)
# GRUB_CMDLINE_LINUX="elevator=bfq"

6.3 bfq 深度解析

bfq(Budget Fair Queueing)是目前最复杂的 blk-mq 调度器,核心思想是每个进程有预算(budget),预算用完后切换到下一个进程:

  • budget:每个进程在一次调度中可以发送的 I/O 字节数
  • budget_timeout:在饥饿前的最小运行时间
  • low_latency:低延迟模式(默认开启),自动调整权重
  • bfq_weights:可手动设置进程的相对权重

bfq 在桌面 GUI 场景下能显著提升交互性,但在高并发服务器场景中表现不如 mq-deadline。

七、设备驱动层实战:NVMe 与 SCSI-MQ

7.1 NVMe 与 blk-mq 的深度契合

NVMe(Non-Volatile Memory Express)是专为 SSD 设计的协议,其硬件原生支持 64K 深度队列(每个队列 64K 个命令)。blk-mq 的架构与 NVMe 硬件完美匹配:

// drivers/nvme/host/pci.c
static const struct blk_mq_ops nvme_mq_ops = {
    .queue_rq       = nvme_queue_rq,      // 接收一个 request
    .commit_rqs     = nvme_commit_rqs,    // 批量提交到硬件(优化门铃写入)
    .get_budget     = nvme_get_budget,    // tag 预算(可选)
    .release_budget = nvme_release_budget, // 释放预算
    .complete       = nvme_pci_complete_rq, // I/O 完成回调
    .init_hctx      = nvme_init_hctx,     // 硬件队列初始化
    .init_request   = nvme_init_request,  // request 初始化
    .map_queues     = nvme_mq_map_queues,// SW→HW 映射
    .timeout        = nvme_timeout,        // 请求超时处理
    .poll           = nvme_poll,           // 轮询模式
};

// 核心路径:提交一个 request 到 NVMe
static blk_status_t nvme_queue_rq(struct blk_mq_hw_ctx *hctx,
                                   const struct blk_mq_queue_data *bd)
{
    struct request *req = bd->rq;
    struct nvme_dev *dev = hctx->driver_data;
    struct nvme_command *cmd = nvme_cmd(req);
    
    // 1. 分配 nvme_command(SQ slot)
    // 2. 将 request 翻译为 NVMe 命令(读/写/flush)
    memset(cmd, 0, sizeof(*cmd));
    if (req_op(req) == REQ_OP_FLUSH)
        nvme_setup_flush(ns, cmd);
    else if (req_op(req) == REQ_OP_WRITE)
        nvme_setup_write_cmd(ns, req);
    else
        nvme_setup_read_cmd(ns, req);
    
    // 3. 锁定队列并写入 SQ tail
    spin_lock_irq(>dev->q_lock[nvmeq->qid]);
    memcpy(nvmeq->sq_cmds + nvmeq->sq_tail, cmd, sizeof(*cmd));
    if (++nvmeq->sq_tail == nvmeq->q_depth)
        nvmeq->sq_tail = 0;
    writel(nvmeq->sq_tail, nvmeq->doorbell_addr);
    spin_unlock_irq(>dev->q_lock[nvmeq->qid]);
    
    return BLK_STS_OK;
}

关键点:commit_rqs 回调在 queue_rq 批量调用之后,只写一次门铃寄存器,避免了多次 MMIO 开销,这是 blk-mq 新增的批量提交优化。

7.2 SCSI-MQ(scsi-mq)适配

传统 SCSI 驱动基于单队列,Linux 4.17+ 引入了 scsi-mq 适配层,让 VirtIO-SCSI、SAS 等传统设备也能使用 blk-mq。scsi_mq_ops 提供了类似 NVMe 的抽象:

// drivers/scsi/scsi_lib.c
static const struct blk_mq_ops scsi_mq_ops = {
    .queue_rq   = scsi_queue_rq,
    .commit_rqs = scsi_commit_rqs,
    .complete   = scsi_softirq_done,
    .init_request = scsi_init_request,
    .exit_request = scsi_exit_request,
    .map_queues = scsi_map_queues,
    .show_rq    = scsi_show_rq,
};

// scsi_mq 使用 host-wide tag pool(共享所有硬件队列的 tag 空间)
static int scsi_alloc_request(struct blk_mq_hw_ctx *hctx, ...)
{
    struct Scsi_Host *shost = hctx->driver_data;
    int tag = scsi_get_tag(shost);
    // 分配 scsi_cmnd 并关联到 request
}

八、io_uring 与 blk-mq 联动

io_uring 是 Linux 5.1 引入的新一代异步 I/O 接口,在支持 IORING_SETUP_SQPOLL 的轮询模式下,可以直接将固定 buffer 提交到 NVMe 硬件队列(bypass VFS → 块设备层),但普通读写仍需经过块设备层的 blk-mq 调度。

io_uring 与 blk-mq 联动时的关键优化点:

  • Fixed buffers:预注册 buffer,避免每次 get_user_pages() 开销
  • Linked SQEs:链式提交,保证 I/O 顺序
  • Polled I/O:IORING_SETUP_IOPOLL,直接轮询 NVMe CQ 硬件完成事件(bypass 中断 + softirq)
  • SQPOLL 模式:内核线程轮询提交队列,减少用户态→内核态切换

性能对比(4K 随机读,单盘 NVMe SSD):

模式IOPS延迟(avg lat)CPU 占用
pread/pwrite (同步)500K12μs100% 1核
io_uring (fixed buf, polling)1.2M4μs100% 1核
io_uring (SQPOLL + iopoll)1.5M2.5μs100% 1核 + 轮询线程

九、全链路延迟剖析

一个 4K 随机读请求从发起到完成的完整延迟包含以下阶段:

阶段函数耗时(典型 NVMe)
1. VFS/FSvfs_read → page_cache_sync_readahead0.5μs
2. submit_bio__submit_bio_noacct → blk_mq_submit_bio0.3μs
3. Schedulingblk_mq_insert_request → mq-deadline0.2μs
4. HW dispatchnvme_queue_rq(SQ tail doorbell)0.3μs
5. HardwareNVMe 控制器 + NAND 读取8-15μs
6. CQ handlingMSI-X → softirq → nvme_pci_complete_rq0.8μs
7. bio_end_iocomplete → 唤醒等待进程0.4μs
总计12-18μs

使用 bpftrace 跟踪可以精确量化每一阶段:

# 跟踪 bio 提交到完成的全链路
bpftrace -e '
kprobe:submit_bio {
    @start[arg0] = nsecs;
}
kprobe:blk_mq_submit_bio {
    $bio = (struct bio *)arg0;
    $start = @start[$bio];
    if($start) {
        @submit_lat = hist(nsecs - $start);
        delete(@start[$bio]);
    }
}
kprobe:nvme_queue_rq {
    $start = @start[0];
    @hwqueue_lat = hist(nsecs - $start);
}

十、生产环境最佳实践

10.1 块设备层调优参数

# 1. 调度器选择
# NVMe SSD → mq-deadline 或 kyber
echo "mq-deadline" > /sys/block/nvme0n1/queue/scheduler
# 混合读写负载 → kyber
echo "kyber" > /sys/block/nvme2n1/queue/scheduler

# 2. nr_requests(队列最大未完成请求数)
# NVMe 可增加值以利用硬件并行性
echo 1024 > /sys/block/nvme0n1/queue/nr_requests

# 3. queue_depth(硬件驱动级深度)
# NVMe 中通常设置为 1024-4096
echo 4096 > /sys/block/nvme0n1/queue/nr_requests

# 4. read_ahead_kb(预读窗口)
# 顺序读场景增加预读(128-256KB)
echo 256 > /sys/block/nvme0n1/queue/read_ahead_kb

# 5. rq_affinity(CPU 亲和性)
# 2: 完成时回到提交它的 CPU(适合 Polling)
echo 2 > /sys/block/nvme0n1/queue/rq_affinity

# 6. io_poll_delay(轮询延迟(polling queue))
# 设为 -1 = 纯轮询,0 = 自适应,正数 = 中断超时后开始轮询
echo 0 > /sys/block/nvme0n1/queue/io_poll

10.2 blk-mq 的 NUMA 优化

在 NUMA 多 socket 服务器上,blk-mq 的 NUMA 亲和对性能影响显著:

# 查看当前 hw queue 到 NUMA 节点的映射
cat /sys/block/nvme0n1/mq/*/cpu_list
# 输出如:16,17,18,19,20,21,22,23(NUMA node 1 的 CPU)

# 设置硬件队列映射到特定 NUMA 节点
echo "none" > /sys/block/nvme0n1/queue/nomerges  # 关闭合并(SSD 场景)

# 多 socket 场景:为每个 socket 创建独立的 hw queue
# 启动参数:
# nvme.poll_queues=8 nvme.io_queues=16

10.3 使用 eBPF 观测块设备层

BCC/bpftrace 提供了多个块设备层的观测工具:


# biolatency:I/O 延迟分布直方图
biolatency -m -d nvme0n1

# biosnoop:打印每个 I/O 的详细信息
biosnoop -d nvme0n1

# blkqdsnoop:blk_mq request 分发延迟
blkqdsnoop -d nvme0n1

# custom bpftrace:追踪 plug → flush 周期
bpftrace -e '
kprobe:blk_mq_flush_plug_list {
    printf("flush plug: cpu=%d reqs=%d\n", 
           curtask->cpu, 
           ((struct blk_plug *)arg0)->rq_count);
}'

10.4 故障排查清单

现象排查方向关键命令
IOPS 突然下降NVMe CQ 满 / Tag 耗尽dmesg | grep nvme
延迟飙升调度器队列积压cat /sys/block/nvme0n1/stat
单核 CPU 100%锁竞争(queue_lock)perf top -p $pid
bio 拆分过多queue_max_sectors 太小grep . /sys/block/nvme0n1/queue/*
进程 I/O 饥饿bfq budget 不足cat /sys/block/nvme0n1/queue/iosched/*

Linux 块设备层 stat 文件解读

/sys/block/\<dev\>/stat 是块设备层的性能统计接口。它输出 17 个数字,含义如下:

字段含义读取示例
0读 I/O 完成数102456
1读 I/O 合并数236
2读扇区数(×512B)2048900
3读 I/O 累计耗时(ms)3421
4写 I/O 完成数87643
5写 I/O 合并数102
6写扇区数(×512B)1752860
7写 I/O 累计耗时(ms)2890
8当前队列中 I/O 数(inflight)45
9I/O 累计耗时(包含排队,ms)6890
10加权累计耗时(ms)12400
11discard 完成数12
12discard 合并数0
13discard 扇区数2400
14discard 耗时80
15flush 请求数530
16flush 耗时450

通过对比字段(读 I/O 数 + 合并数)/ 读 I/O 完成数,可以计算合并率。NVMe 场景合并率通常很低(<5%),传统 HDD 可达 30-60%。

总结与展望

Linux 块设备层是整个存储栈的中枢神经。本文从 bio 结构体出发,覆盖了其完整生命周期、请求合并机制、plugging 优化,以及从单队列到 blk-mq 多队列的架构演进。blk-mq 通过两层队列模型、per-cpu 软件队列、tag 位图分配、批量 commit 等创新,彻底解决了单队列时代的锁竞争和 NUMA 不友好问题,与现代 NVMe 硬件完美契合。

下一篇文章我们将继续深入块设备层实战系列,覆盖 Writeback 与脏页回写机制、Multi-Queue Block IO Queuing Mechanism 与 io_uring 深度集成、以及 blk-cgroup 资源隔离等主题。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部