一、块设备子系统全景图

Linux 块设备子系统是内核 I/O 栈中最核心的中间层,它将上层(文件系统、Page Cache、io_uring)的逻辑请求翻译成设备控制器可执行的命令。一个典型的 write() 调用从 VFS 到 NVMe SSD 的旅程中,块层承担了请求合并、排队整形、调度分发、错误重试的枢纽角色。

架构上分为四个层次:

层次核心组件职责
通用块层bio、request、request_queue构造/合并/提交 I/O 请求
I/O 调度层elevator(mq-deadline/bfq/kyber/none)排序/合并/带宽公平性
多队列框架blk-mq(mapqueue/hwqueue)多核 NUMA 亲和、硬件队列分散拥塞
设备驱动层nvme、virtio-blk、scsi、ramdisk启动/完成/错误处理 I/O

从 Linux 5.0 开始,blk-mq(Multi-Queue Block Layer)成为默认架构。它通过 Hardware Dispatch Queue 与 Software Submission Queue 的分离映射,彻底解决了单队列锁竞争问题,让 NVMe 等多核设备可以线速运行。

二、bio 结构体:I/O 请求的基本单元

块层中最核心的数据结构是 struct bio(include/linux/blk_types.h),代表一次不完整的块 I/O 操作。一个 bio 由多个 struct bio_vec 组成的段(segment)数组构成,每个段对应一个内存页内连续的缓冲区。

    struct block_device *bi_bdev;     // 目标块设备
    blk_opf_t        bi_opf;         // op + flags (READ/WRITE/FLUSH/DISCARD)
    unsigned short   bi_ioprio;      // I/O 优先级 (best-effort/realtime/idle)
    blk_status_t     bi_status;      // 完成状态
    atomic_t          __bi_remaining; // 引用计数 (多段拆分追踪)
    struct bio_vec    *bi_io_vec;     // 段数组
    struct bio_set    *bi_pool;       // 内存池来源
    unsigned short   bi_vcnt;        // 当前段数
    unsigned short   bi_max_vecs;    // 段数组容量
    struct bio_vec    bi_inline_vecs[];  // BIO_INLINE_VECS 内联优化
    void               (*bi_end_io)(struct bio *); // 完成回调
    void                *bi_private;    // 私有完成上下文
    bio_io_advance_fn *bi_iotype;  // I/O 统计分类
};

bio_vec 段结构

    unsigned int  bv_len;   // 段长度 (字节)
    unsigned int  bv_offset; // 页内偏移
};

一个典型的文件系统 write() 调用会经过:Page Cache → buffer_head/iov_iter → bio_alloc() → bio_add_page() → submit_bio()。每次 submit_bio() 调用都可能触发合并逻辑后再进入调度器。

三、从 bio 到 request:Elevator 合并魔术

bio 进入块层后,经过 blk_queue_enter → bio_split(如果需要)→ bio_attempt_back/front_merge → blk_mq_bio_to_request 路径。其中关键的合并策略如下:

合并类型触发条件合并后效果
Back merge新 bio 尾扇区 = 已有 request 头扇区新 bio 接到 request 尾部
Front merge新 bio 头扇区 = 已有 request 尾扇区新 bio 接到 request 头部
Plug mergePlug 批处理窗口内相邻请求批量 coalescing 降低 request 数
Cross-sector跨 bio_vec 边界连续扇区仅在同 request 内操作

批处理插入:Plug & Unplug

Linux 使用 blk-plug 机制(struct blk_plug)实现 I/O 批量蓄流。进程提交 I/O 时先攒在 plug->mq_list,到 unplug 时再一次性刷入调度器,大幅减少锁争用和碎片化请求。

void blk_finish_plug(struct blk_plug *plug)
{
    blk_mq_flush_plug_list(plug, false);
    kfree(plug);
}

四、blk-mq 多队列框架:现代存储的性能基石

传统单队列块层(blk-sq)将所有 CPU 的请求塞进一把锁保护的队列,NVMe SSD 随机 4K 读可达百万 IOPS,但单队列锁争用导致多核扩展性极差。blk-mq 通过两个核心抽象解决这一问题:

4.1 队列拓扑映射

  • Software Submission Queue (SQ):per-CPU 或 NUMA-node 本地提交队列。多个 CPU 可同时 push 请求而无需跨核锁。
  • Hardware Dispatch Queue (HWQ):对应设备端实际硬件 submission queue(如 NVMe 的 SQ0..SQ[n-1])。驱动从 HWQ 选取请求填充设备 Doorbell。
  • map:标签集(tag set)中的hw_queue 索引映射,决定 HWQ 到 SQ 的亲和关系。

关键数据结构:

    unsigned int       *map;           // hw_queue 映射表 [map_count]
    unsigned int       nr_maps;        // 映射类型数 (HCTX_TYPE_DEFAULT 等)
    unsigned int       nr_hw_queues;   // 硬件队列总数
    unsigned int       queue_depth;    // 每队列深度限制
    unsigned int       cmd_size;       // 私有命令体扩展大小
    int                 numa_node;        // NUMA 节点偏好
    void                *driver_data;      // 驱动层私有数据
};

4.2 驱动回调操作集

blk-mq 驱动通过 struct blk_mq_ops 与核心层交互:

    .commit_rqs   = my_commit_rqs,    // 批量提交到设备
    .get_budget   = my_get_budget,    // 限流预算获取
    .put_budget   = my_put_budget,    // 预算归还
    .timeout      = my_timeout,       // 超时处理 (用于 error recovery)
    .poll        = my_poll,         // 轮询完成模式 (高 IOPS 场景)
    .map_queue   = blk_mq_map_queue,// 物理映射到 hctx
    .init_hctx   = my_init_hctx,    // per-hw_queue 初始化钩子
    .exit_hctx   = my_exit_hctx,    // 清理钩子
    .init_request = my_init_request,   // request 分配后定制命令体
    .show_rq     = my_show_rq,      // /proc/调试输出
};

4.3 queue_rq 驱动核心循环

my_queue_rq() 是每个 HWQ 在中断上下文(或轮询上下文)中执行的入口。驱动从此函数取出 request 并翻译成设备命令。

        break;
    default:
        return BLK_STS_IOERR;
    }
    return BLK_STS_OK;  // 异步完成,稍后 my_end_rq() 通知
}

完成后驱动调用 blk_mq_end_request(rq, status) 通知核心层回收 tag 并转发到顶层 waiter。

五、I/O 调度器详解

Linux 5.x 多队列时代提供了三种多队列 I/O 调度器,加上 none 直通模式:

调度器设计目标队列模型最佳场景
mq-deadline延迟读写期限sorted_lists + fifo_batch数据库、尾延迟敏感
bfq带宽公平共享预算权重树 (Budget Fair Queueing)交互式、混部容器
kyber低延迟自适应并发目标令牌环NVMe/RDMA 高速设备
none无调度直通直接 submitNVMe 自带仲裁或 flash 直写

5.1 mq-deadline 内核参数

├── scheduler          // 当前调度器 [mq-deadline bfq kyber none]
├── read_expire        // 读请求超时 (ms), 默认 500
├── write_expire       // 写请求超时 (ms), 默认 5000
├── writes_starved     // 写饿死前最大调度读次数, 默认 2
├── fifo_batch         // 单批下发量, 默认 16
├── front_merges       // 是否允许 front_merge, 布尔
├── nr_requests        // 队列深度上限, 默认 128
└── features           // 特性位图 (FUA/HEADMERGE/SG etc)

5.2 调度器切换命令

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

# 切换到低延迟调度
echo none > /sys/block/nvme0n1/queue/scheduler

# 数据库场景启用 mq-deadline + 调大队列深度
echo mq-deadline > /sys/block/sda/queue/scheduler
echo 256 > /sys/block/sda/queue/nr_requests
echo 128 > /sys/block/sda/queue/read_expire

六、完整驱动实战:RAM Disk 字符设备

下面是一个基于 blk-mq 的完整 RAM disk 驱动(简化版),展示请求处理、tag 分配、中断模拟全链路。

#include 
#include 
#include 
#include 

#define RAMDISK_SIZE  (64 * 1024 * 1024)  // 64 MB
#define RAMDISK_NAME  "myram"
#define SECTORSZ      512
#define MINORS         4

static int major;
static struct gendisk *disk;
static struct request_queue *queue;
static struct blk_mq_tag_set tag_set;
static u8 *data;   // vmalloc 后备内存
static DEFINE_SPINLOCK(lock);

/* --- blk-mq 操作集 --- */
static blk_status_t
myram_queue_rq(struct blk_hw_ctx *hctx, const struct blk_queue_ctx *bd)
{
    struct request *rq  = bd->rq;
    sector_t     sector = blk_rq_pos(rq);
    unsigned long offset = sector * SECTORSZ;
    blk_status_t  ret = BLK_STS_OK;
    struct bio_vec bv;
    struct req_iterator iter;

    if (offset + blk_rq_bytes(rq) > RAMDISK_SIZE) {
        blk_mq_end_request(rq, BLK_STS_IOERR);
        return BLK_STS_IOERR;
    }

    /* 遍历 request 中包含的所有 bio 段 */
    rq_for_each_segment(bv, rq, iter) {
        void *mem = kmap_local_page(bv.bv_page);
        unsigned long flags;

        switch (req_op(rq)) {
        case REQ_OP_READ:
            spin_lock_irqsave(&lock, flags);
            memcpy(mem + bv.bv_offset,
                   data + offset + iter.iter.bi_bvec_done,
                   bv.bv_len);
            spin_unlock_irqrestore(&lock, flags);
            break;
        case REQ_OP_WRITE:
            spin_lock_irqsave(&lock, flags);
            memcpy(data + offset + iter.iter.bi_bvec_done,
                   mem + bv.bv_offset,
                   bv.bv_len);
            spin_unlock_irqrestore(&lock, flags);
            break;
        default:
            ret = BLK_STS_IOERR;
            goto out;
        }
        kunmap_local(mem);
    }

out:
    blk_mq_end_request(rq, ret);
    return ret;
}

static const struct blk_mq_ops myram_ops = {
    .queue_rq = myram_queue_rq,
};

/* --- 注册与卸载 --- */
static int __init myram_init(void)
{
    int ret;

    major = register_blkdev(0, RAMDISK_NAME);
    if (major < 0 xss=removed>return major;

    data = vmalloc(RAMDISK_SIZE);
    if (!data) { ret = -ENOMEM; goto out_unreg; }

    disk = alloc_disk(MINORS);
    if (!disk) { ret = -ENOMEM; goto out_free; }

    memset(&tag_set, 0, sizeof(tag_set));
    tag_set.ops        = &myram_ops;
    tag_set.nr_hw_queues = 4;           // 4 个硬件队列
    tag_set.queue_depth  = 128;          // 每队列深度 128
    tag_set.numa_node    = NUMA_NO_NODE;
    tag_set.cmd_size     = 0;

    ret = blk_mq_alloc_tag_set(&tag_set);
    if (ret) goto out_free_disk;

    queue = blk_mq_init_queue(&tag_set);
    if (IS_ERR(queue)) { ret = PTR_ERR(queue); goto out_tagset; }

    blk_queue_logical_block_sizeblk_queue_max_hw_sectorsblk_queue_flag_set// SSD 标识
    queue->queuedata = (void *)disk;

    disk->major         = major;
    disk->first_minor  = 0;
    disk->minors       = MINORS;
    disk->fops         = &myram_fops;
    disk->private_data = data;
    sprintf(disk->disk_name, "%s", RAMDISK_NAME);
    set_capacity(disk, RAMDISK_SIZE / SECTORSZ);
    device_add_disk(NULL, disk, false);

    pr_info("myram: loaded, %u MB / %u queues
",
            RAMDISK_SIZE/(1024*1024), tag_set.nr_hw_queues);
    return 0;

out_tagset:    blk_mq_free_tag_set(&tag_set);
out_free_disk: put_disk(disk);
out_free:      vfree(data);
out_unreg:     unregister_blkdev(major, RAMDISK_NAME);
    return ret;
}

static void __exit myram_exit(void)
{
    del_gendisk(disk);
    blk_cleanup_disk(disk);      // 自动 put_disk+cleanup queue
    blk_mq_free_tag_set(&tag_set);
    vfree(data);
    unregister_blkdev(major, RAMDISK_NAME);
    pr_info("myram: unloaded
");
}

module_init(myram_init);
module_exit(myram_exit);
MODULE_LICENSE("GPL");

七、blk-mq 与 io_uring 深度融合

Linux 5.x 起,io_uring 可以通过 IORING_SETUP_SQPOLL 模式与 blk-mq 协同:设备侧开启 polling(blk_mq_poll())不仅降低 IRQ 开销,还与 io_uring 的零拷贝共享内存(fixed buffer)组合进一步减少内存拷贝。

  • Fixed Buffers:io_uring 预注册 io_uring_register_buffers() 的 page 数组,块层直接用 bio_map_user() 挂载到 bio_vec。
  • Fixed Files:预注册 fd(io_uring_register_files()),每个 SQE 通过 index 指定目标块设备 fd,免去每次 fget/fput 开销。
  • SQPOLL 轮询提交:内核线程轮询 SQ ring 有否新有 SQE,驱动侧调用 blk_mq_run_hw_queues() 无锁触发。
  • IRQ 模式 poll:NVMe 驱动注册 blk_mq_ops.poll 回调,硬中断到达后改为 poll 模式批量收割 CQ entry。

八、生产级调优实战

8.1 性能调优决策树

症状根因诊断调优措施
IOPS 低,CPU 空闲队列深度不足echo 512 > /sys/block/nvme0n1/queue/nr_requests
尾 P99 延迟飙升写饿死 / 调度器顺序切 mq-deadline + writes_starved=2, read_expire=50
多核不扩展单队列争用(旧内核)切 blk-mq,确认 nr_hw_queues > 1
读带宽抖动混合读写干扰切 bfq + low_latency=1
NVMe 中断打满单核RSS 队列数不足echo f > /proc/irq/XX/smp_affinity 手动分发
容器 IO 争抢没有权重隔离blkio.weight + cgroup v2 io.max

8.2 NVMe 多队列数与中断绑定

NVMe 设备驱动支持 io_queue_count 强制指定硬件队列数,echo N > /sys/block/nvme0n1/device/queue_count 可覆盖。中断亲和则通过 /proc/irq//smp_affinity_list 手动绑定,每个队列一个独立 CPU。

8.3 cgroup v2 IO 隔离

// Device: rbps 读上限 wbps 写上限 riops 读IOPS wiops 写IOPS
8:0 rbps=104857600 wbps=52428800 riops=2000 wiops=1000

# 检查 /sys/fs/cgroup/container-a/io.stat
// 字段: rbytes wbytes rios wios dbytes dios

8.4 性能监控三板斧

  • iostat -xmt 2:查看 await、avgqu-sz、%util、r/s、w/s。await > 10ms 即需排查。
  • blktrace:blktrace -d /dev/nvme0n1 -w 10 -o trace 抓取 D(下发)、I(插入)、C(完成)事件时间戳。
  • bpftrace:kprobe:blk_mq_start_request { @[comm] = count(); } 实时统计进程 I/O。

九、最佳实践总结

  1. SSD/NVMe 用 none 或 kyber:NVMe 自带硬件仲裁和 submit queue,无需软件调度开销。
  2. 数据库/mq-deadline 优先:read_expire=min(avg_lat/2, 100),write_expire 放宽到 5000ms。
  3. 队列深度 = 延迟 × IOPS:NVMe 默认 1024 队列深度,但容器场景需按 io.max 限制按预算分配。
  4. 中断与提交队列一一绑定:生产部署每个硬件队列配一个 CPU 核,避免跨节点转发。
  5. 避免 fsync 风暴:批量 flush、使用 O_DSYNC + write barriers、调整 dirty_expire_centisecs=500。
  6. blk-mq 设施位合理配置:QUEUE_FLAG_NONROT(SSD)、QUEUE_FLAG_PCI_P2PDMA(跨 PCIe peer)。
  7. io_uring 直通:高吞吐存储应用优先 fixed buffer + SQPOLL + registered files 三件套。

十、知识点关联图

Linux 块设备层并非孤岛——它是多条路径的交汇点。user space 的 write() 经 VFS 落地 Page Cache;direct path 跳过 cache 直接 submit_bio;io_uring SQE 直接映射 bio_vec;loop device 将块请求"倒回"文件系统;device-mapper(lvm/cache/bcache/thin)进一步截取 reshaping request。理解块层 bc 是整个 I/O 栈能力跃升的关键。

下一篇可深入 Linux AIO + uring 存储引擎实战 或 NVMe 驱动多队列 + Fabrics(TCP/RDMA),敬请期待。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.360862s