13.1 块设备的 I/O 操作特点
| 维度 | 字符设备 | 块设备 |
|---|---|---|
| 访问单位 | 字节流 | 扇区(512B)/块(通常 4K) |
| 是否可 seek | 通常不可(no_llseek) |
必须支持 |
| 缓冲 | 无 | 页缓存(page cache) |
| 接口 | read/write |
read/write/mmap/mount |
| 内核路径 | 直接调用 fops |
VFS → 页缓存 → bio → blk-mq → 驱动 |
| 典型设备 | 串口、按键、LED | 硬盘、SSD、U 盘、RAM 盘 |
块设备的分层:
用户 read()/write()
-> VFS
-> 页缓存 (page cache) / 直接 I/O (O_DIRECT)
-> struct bio (描述一次 I/O:哪些页、多大、方向)
-> blk-mq (多队列、tag、调度)
-> struct request (被驱动处理的请求)
-> 驱动 (queue_rq 或 submit_bio)
-> 硬件
13.2 Linux 块设备驱动结构
13.2.1 block_device_operations 结构体
7.2.5 的定义(include/linux/blkdev.h):
cpp
struct block_device_operations {
void (*submit_bio)(struct bio *bio);
int (*poll_bio)(struct bio *bio, struct io_comp_batch *iob,
unsigned int flags);
int (*open)(struct gendisk *disk, blk_mode_t mode);
void (*release)(struct gendisk *disk);
int (*ioctl)(struct block_device *bdev, blk_mode_t mode,
unsigned cmd, unsigned long arg);
int (*compat_ioctl)(struct block_device *bdev, blk_mode_t mode,
unsigned cmd, unsigned long arg);
unsigned int (*check_events)(struct gendisk *disk, unsigned int clearing);
void (*unlock_native_capacity)(struct gendisk *);
int (*getgeo)(struct gendisk *, struct hd_geometry *);
int (*set_read_only)(struct block_device *bdev, bool ro);
void (*free_disk)(struct gendisk *disk);
void (*swap_slot_free_notify)(struct block_device *, unsigned long);
int (*report_zones)(struct gendisk *, sector_t sector,
unsigned int nr_zones,
struct blk_report_zones_args *args);
char *(*devnode)(struct gendisk *disk, umode_t *mode);
int (*get_unique_id)(struct gendisk *disk, u8 id[16],
enum blk_unique_id id_type);
struct module *owner;
const struct pr_ops *pr_ops;
int (*alternative_gpt_sector)(struct gendisk *disk, sector_t *sector);
};
与 4.0 的对比(重要):
| 项 | 4.0 | 7.2.5 |
|---|---|---|
open 签名 |
int (*open)(struct block_device *, fmode_t) |
int (*open)(struct gendisk *, blk_mode_t) |
release |
void (*release)(struct gendisk *, fmode_t) |
void (*release)(struct gendisk *) |
ioctl |
int (*ioctl)(struct block_device *, fmode_t, unsigned, unsigned long) |
blk_mode_t 取代 fmode_t |
request_fn |
有 | 已删除 |
make_request |
有 | 已删除 |
submit_bio |
有(与 make_request 并存) | 唯一推荐的直接提交方式 |
poll_bio |
无 | 有 |
free_disk |
无 | 有(释放自定义 gendisk) |
report_zones |
无 | 有(ZNS 分区设备) |
blk_mode_t 是 7.2.5 引入的类型(include/linux/blk_types.h),把 fmode_t 中与块设备相关的位抽出来,取值有 BLK_OPEN_READ/BLK_OPEN_WRITE/BLK_OPEN_EXCL/BLK_OPEN_READ_ONLY 等。
13.2.2 gendisk 结构体
cpp
/* include/linux/blkdev.h */
struct gendisk {
int major;
int first_minor;
int minors;
char disk_name[DISK_NAME_LEN];
const struct block_device_operations *fops;
struct request_queue *queue;
void *private_data;
struct bio_set bio_split;
int flags;
unsigned long state;
struct mutex open_mutex;
unsigned open_partitions;
struct backing_dev_info *bdi;
struct kobject queue_kobj;
...
u64 disk_capacity; /* 实际字段为 capacity,用 set_capacity() 设置 */
};
关键字段:
| 字段 | 说明 |
|---|---|
major / first_minor / minors |
主次设备号范围 |
disk_name |
出现在 /dev/ 与 /proc/partitions |
fops |
block_device_operations |
queue |
请求队列(blk_alloc_disk() 自动创建) |
private_data |
驱动私有数据 |
flags |
GENHD_FL_* 标志 |
bdi |
回写信息 |
关键 API:
cpp
struct gendisk *blk_alloc_disk(struct queue_limits *lim, int node); /* 7.2.5 */
struct gendisk *blk_mq_alloc_disk(struct blk_mq_tag_set *set,
struct queue_limits *lim, void *queuedata);
void put_disk(struct gendisk *disk);
void set_capacity(struct gendisk *disk, sector_t size);
int add_disk(struct gendisk *disk);
int add_disk_no_queue_reg(struct gendisk *disk);
void del_gendisk(struct gendisk *disk);
void blk_cleanup_disk(struct gendisk *disk);
7.2.5 的重要变化:
blk_alloc_disk()现在接受struct queue_limits *参数(替代旧的无参形式),在分配时一次性设定队列限制。struct queue_limits集中描述队列能力:logical_block_size、physical_block_size、max_hw_sectors、max_segments、max_discard_sectors、features等。alloc_disk()/alloc_disk_node()已删除。blk_cleanup_disk()已不再是标准做法,改为del_gendisk()+put_disk()。
13.2.3 bio、request 和 request_queue
struct bio 描述"一次 I/O 请求":
cpp
/* include/linux/blk_types.h */
struct bio {
struct bio *bi_next;
struct block_device *bi_bdev;
blk_opf_t bi_opf; /* 操作码 + 标志 */
unsigned short bi_flags;
unsigned short bi_ioprio;
blk_status_t bi_status;
struct bvec_iter bi_iter; /* 当前迭代位置 */
...
struct bio_vec *bi_io_vec;
struct bio_set *bi_pool;
...
};
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
常用 bio 操作:
| API | 作用 |
|---|---|
bio_data_dir(bio) |
判断读/写 |
bio->bi_iter.bi_sector |
起始扇区 |
bio->bi_iter.bi_size |
剩余字节数 |
bio_for_each_segment(bvl, bio, iter) |
遍历段 |
bio_for_each_segment_all(bvl, bio, i) |
遍历全部段 |
bio_iter_iovec(bio, iter) |
取当前段 |
bio_endio(bio) |
完成 bio(成功) |
bio_io_error(bio) |
完成 bio(失败) |
bio_advance_iter() / bio_advance() |
推进迭代器 |
struct request 是 blk-mq 把多个 bio 合并后交给驱动的单位:
cpp
struct request {
struct request_queue *q;
struct blk_mq_ctx *mq_ctx;
struct blk_mq_hw_ctx *mq_hctx;
blk_opf_t cmd_flags;
blk_status_t status;
struct bio *bio;
struct bio *biotail;
void *special;
...
unsigned int __data_len;
sector_t __sector;
...
};
request_queue 的现代形态 :7.2.5 中 request_queue 仍是核心结构,但不再有 make_request_fn ;队列由 blk_mq_alloc_disk() 自动创建并绑定 blk-mq。
13.2.4 I/O 调度器
7.2.5 的调度器(block/elevator.c):
| 调度器 | 说明 | 适用 |
|---|---|---|
none |
无调度,直接下发(多队列默认) | NVMe、高速 SSD |
mq-deadline |
保证延迟上限 | 通用 |
kyber |
基于目标延迟的轻量调度 | 高速设备 |
bfq |
预算公平队列,交互性好 | 桌面、慢速设备 |
注意 :旧式的 cfq、deadline、noop 已被删除,只剩上面四个。查看与设置:
bash
cat /sys/block/sda/queue/scheduler
echo mq-deadline > /sys/block/sda/queue/scheduler
cat /sys/block/sda/queue/nr_requests
cat /sys/block/sda/queue/scheduler
13.3 Linux 块设备驱动的初始化
7.2.5 标准流程(submit_bio 风格,最简单):
cpp
static int myblk_init(void)
{
struct queue_limits lim = {
.logical_block_size = 512,
.physical_block_size = 512,
.max_hw_sectors = 1024,
};
struct gendisk *disk;
disk = blk_alloc_disk(&lim, NUMA_NO_NODE);
if (IS_ERR(disk))
return PTR_ERR(disk);
disk->major = myblk_major;
disk->first_minor = 0;
disk->minors = 16;
disk->fops = &myblk_fops;
disk->private_data = mydev;
strscpy(disk->disk_name, "myblk0", DISK_NAME_LEN);
set_capacity(disk, MYBLK_SIZE_SECTORS);
add_disk(disk);
return 0;
}
blk-mq 风格(需要请求队列时):
cpp
static struct blk_mq_tag_set tag_set = {
.ops = &my_mq_ops,
.nr_hw_queues = 1,
.queue_depth = 128,
.numa_node = NUMA_NO_NODE,
.cmd_size = sizeof(struct my_cmd),
};
static int myblk_mq_init(void)
{
struct gendisk *disk;
int ret;
ret = blk_mq_alloc_tag_set(&tag_set);
if (ret)
return ret;
disk = blk_mq_alloc_disk(&tag_set, &lim, mydev);
if (IS_ERR(disk)) {
blk_mq_free_tag_set(&tag_set);
return PTR_ERR(disk);
}
...
add_disk(disk);
return 0;
}
13.4 块设备的打开与释放
cpp
static int myblk_open(struct gendisk *disk, blk_mode_t mode)
{
struct myblk_dev *dev = disk->private_data;
if (mode & BLK_OPEN_WRITE && dev->read_only)
return -EROFS;
return 0;
}
static void myblk_release(struct gendisk *disk)
{
/* 递减打开计数、释放独占资源 */
}
注意:
- 7.2.5 的
open第一个参数是struct gendisk *(不再是struct block_device *)。 open/release会成对 调用,但release在最后一次关闭时才调用。- 块设备通常不需要自己维护引用计数,内核的
bd_holder机制会处理。
13.5 块设备驱动的 ioctl 函数
cpp
static int myblk_ioctl(struct block_device *bdev, blk_mode_t mode,
unsigned int cmd, unsigned long arg)
{
struct gendisk *disk = bdev->bd_disk;
struct myblk_dev *dev = disk->private_data;
switch (cmd) {
case BLKFLSBUF: /* 刷新缓冲 */
return 0;
case BLKROSET: { /* 设置只读 */
int ro;
if (get_user(ro, (int __user *)arg))
return -EFAULT;
dev->read_only = ro;
return 0;
}
default:
return -ENOTTY;
}
}
7.2.5 要点:
- 很多标准 ioctl 已由块层通用代码处理 (如分区表、
BLKGETSIZE64、BLKRRPART)。驱动只需处理设备特有的。 - 如果驱动不实现
ioctl,块层会返回-ENOTTY,通用 ioctl 仍可用。 - 必须用
blkdev_compat_ptr_ioctl处理 32 位兼容层(如果需要)。 - 返回
-ENOTTY而非-EINVAL表示"不支持该命令"。
13.6 块设备驱动的 I/O 请求处理
13.6.1 使用请求队列(blk-mq)
cpp
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *rq = bd->rq;
struct myblk_dev *dev = rq->q->queuedata;
blk_status_t status;
blk_mq_start_request(rq); /* 必须调用,标记请求已开始 */
status = process_request(dev, rq); /* 硬件操作,通常是异步的 */
if (status != BLK_STS_OK) {
blk_mq_end_request(rq, status); /* 失败立即完成 */
return BLK_STS_OK;
}
/* 异步场景:在中断/完成回调中 blk_mq_end_request(rq, BLK_STS_OK) */
return BLK_STS_OK;
}
static const struct blk_mq_ops my_mq_ops = {
.queue_rq = my_queue_rq,
.complete = my_complete, /* 可选:请求完成回调 */
.init_request = my_init_request, /* 可选 */
.exit_request = my_exit_request, /* 可选 */
};
关键规则:
- 必须调用
blk_mq_start_request(),否则超时/统计/错误处理都不正确。 - 必须调用
blk_mq_end_request(),否则请求永远不完成,mount/dd会挂死。 queue_rq()可能在原子上下文被调用,因此内部不能睡眠。- 返回
BLK_STS_OK表示"已接受",返回BLK_STS_RESOURCE表示"资源不足,稍后重试"。 - 7.2.5 新增
.queue_rqs(批量提交),能显著提升性能。
blk_status_t 常见值 (include/linux/blk_types.h):
| 值 | 含义 |
|---|---|
BLK_STS_OK |
成功 |
BLK_STS_NOTSUPP |
不支持 |
BLK_STS_TIMEOUT |
超时 |
BLK_STS_NOSPC |
空间不足 |
BLK_STS_TRANSPORT |
传输错误 |
BLK_STS_TARGET |
目标错误 |
BLK_STS_NEXUS |
路径错误 |
BLK_STS_MEDIUM |
介质错误 |
BLK_STS_PROTECTION |
保护信息错误 |
BLK_STS_RESOURCE |
资源不足,可重试 |
BLK_STS_IOERR |
通用 I/O 错误 |
13.6.2 不使用请求队列(submit_bio)
这是7.2.5 最简单、最推荐的虚拟块设备写法 ,参考 drivers/block/brd.c:
cpp
static void myblk_submit_bio(struct bio *bio)
{
struct myblk_dev *dev = bio->bi_bdev->bd_disk->private_data;
struct bvec_iter iter;
struct bio_vec bvec;
sector_t sector = bio->bi_iter.bi_sector;
bio_for_each_segment(bvec, bio, iter) {
unsigned int len = bvec.bv_len;
void *iovec = page_address(bvec.bv_page) + bvec.bv_offset;
if (op_is_write(bio_op(bio)))
memcpy(dev->data + (sector << SECTOR_SHIFT), iovec, len);
else
memcpy(iovec, dev->data + (sector << SECTOR_SHIFT), len);
sector += len >> SECTOR_SHIFT;
}
bio_endio(bio); /* 完成,无错误 */
}
brd.c 的完整初始化流程(7.2.5):
cpp
static int brd_alloc(int i)
{
struct brd_device *brd;
struct gendisk *disk;
struct queue_limits lim = {
.physical_block_size = PAGE_SIZE,
.max_hw_discard_sectors = UINT_MAX,
.max_discard_segments = 1,
.discard_granularity = PAGE_SIZE,
.features = BLK_FEAT_SYNCHRONOUS | BLK_FEAT_NOWAIT,
};
brd = brd_find_or_alloc_device(i);
if (IS_ERR(brd))
return PTR_ERR(brd);
disk = brd->brd_disk = blk_alloc_disk(&lim, NUMA_NO_NODE);
if (IS_ERR(disk))
goto out_free_dev;
disk->major = RAMDISK_MAJOR;
disk->first_minor = i * max_part;
disk->minors = max_part;
disk->fops = &brd_fops;
disk->private_data = brd;
strscpy(disk->disk_name, buf, DISK_NAME_LEN);
set_capacity(disk, rd_size * 2);
err = add_disk(disk);
...
}
卸载流程:
cpp
static void brd_free_disk(struct gendisk *disk)
{
struct brd_device *brd = disk->private_data;
del_gendisk(disk);
put_disk(disk);
...
}
13.7 实例:vmem_disk 驱动
13.7.1 vmem_disk 的硬件原理
原书的 vmem_disk 用 vmalloc 分配的内存 模拟一个块设备(而非 brd 的页数组)。7.2.5 版本改用 submit_bio 实现,代码更短更清晰。
13.7.2 vmem_disk 驱动模块的加载与卸载
cpp
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/blk-mq.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#define VMLINUX_BLK_SIZE (1024 * 1024) /* 1MB */
#define VMLINUX_SECTORS (VMLINUX_BLK_SIZE >> SECTOR_SHIFT)
static int vmem_major;
static struct gendisk *vmem_disk;
static unsigned char *vmem_data;
static struct queue_limits vmem_lim = {
.logical_block_size = 512,
.physical_block_size = 512,
.max_hw_sectors = 256,
};
static int vmem_open(struct gendisk *disk, blk_mode_t mode)
{
pr_info("vmem_disk: open\n");
return 0;
}
static void vmem_release(struct gendisk *disk)
{
pr_info("vmem_disk: release\n");
}
static void vmem_submit_bio(struct bio *bio)
{
struct bvec_iter iter;
struct bio_vec bvec;
sector_t sector = bio->bi_iter.bi_sector;
unsigned long offset = sector << SECTOR_SHIFT;
bio_for_each_segment(bvec, bio, iter) {
void *buf = page_address(bvec.bv_page) + bvec.bv_offset;
unsigned int len = bvec.bv_len;
if (offset + len > VMLINUX_BLK_SIZE) {
bio_io_error(bio);
return;
}
if (op_is_write(bio_op(bio)))
memcpy(vmem_data + offset, buf, len);
else
memcpy(buf, vmem_data + offset, len);
offset += len;
}
bio_endio(bio);
}
static const struct block_device_operations vmem_fops = {
.owner = THIS_MODULE,
.open = vmem_open,
.release = vmem_release,
.submit_bio = vmem_submit_bio,
};
加载与卸载:
cpp
static int __init vmem_init(void)
{
int ret;
vmem_data = vzalloc(VMLINUX_BLK_SIZE);
if (!vmem_data)
return -ENOMEM;
ret = register_blkdev(0, "vmemdisk");
if (ret < 0)
goto err_free;
vmem_major = ret;
vmem_disk = blk_alloc_disk(&vmem_lim, NUMA_NO_NODE);
if (IS_ERR(vmem_disk)) {
ret = PTR_ERR(vmem_disk);
goto err_unreg;
}
vmem_disk->major = vmem_major;
vmem_disk->first_minor = 0;
vmem_disk->minors = 1;
vmem_disk->fops = &vmem_fops;
strscpy(vmem_disk->disk_name, "vmemdisk", DISK_NAME_LEN);
set_capacity(vmem_disk, VMLINUX_SECTORS);
ret = add_disk(vmem_disk);
if (ret)
goto err_put;
pr_info("vmem_disk: loaded, major=%d\n", vmem_major);
return 0;
err_put:
put_disk(vmem_disk);
err_unreg:
unregister_blkdev(vmem_major, "vmemdisk");
err_free:
vfree(vmem_data);
return ret;
}
static void __exit vmem_exit(void)
{
del_gendisk(vmem_disk);
put_disk(vmem_disk);
unregister_blkdev(vmem_major, "vmemdisk");
vfree(vmem_data);
pr_info("vmem_disk: unloaded\n");
}
module_init(vmem_init);
module_exit(vmem_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("vmem_disk: virtual block device, Linux 7.2.5");
13.7.3 vmem_disk 设备驱动的 block_device_operations
见上面的 vmem_fops。要点:
.owner = THIS_MODULE让块层自动维护模块引用计数。.open/.release可选,但建议实现以便观察生命周期。.submit_bio是 7.2.5 最简单的 I/O 入口。
13.7.4 vmem_disk 的 I/O 请求处理
vmem_submit_bio() 的实现要点:
- 用
bio_for_each_segment()遍历,不要假设 bio 只有一个段。 - 用
op_is_write(bio_op(bio))判断方向 ,不要直接比较bi_opf。 - 越界必须
bio_io_error(bio)并返回,不能继续拷贝。 - 完成后必须
bio_endio(bio)。 - 段内偏移是
bvec.bv_offset,不要忘记。
QEMU 验证:
bash
insmod vmemdisk.ko
lsblk
cat /proc/partitions
dd if=/dev/zero of=/dev/vmemdisk bs=1k count=8
dd if=/dev/vmemdisk of=/dev/null bs=1k count=8
mkfs.ext2 /dev/vmemdisk # 需要 CONFIG_EXT2_FS
mkdir /mnt/vmem && mount /dev/vmemdisk /mnt/vmem
echo hello > /mnt/vmem/test.txt
cat /mnt/vmem/test.txt
umount /mnt/vmem
dmesg | tail -10
rmmod vmemdisk
观察点:
lsblk显示vmemdisk 1M;dd读写成功且无错误;mkfs/mount证明块设备语义正确(这是块设备驱动的"及格线");rmmod后dmesg无 UAF。
13.8 Linux MMC 子系统
MMC 子系统是块设备驱动的分层范例:
用户空间 (/dev/mmcblk0)
-> block layer
-> mmc_blk (drivers/mmc/core/block.c) ------ 块设备例化
-> mmc core (drivers/mmc/core/) ------ 核心层:命令、卡识别
-> mmc host (drivers/mmc/host/) ------ 主机控制器驱动
-> 硬件
关键结构:
| 结构 | 头文件 | 作用 |
|---|---|---|
struct mmc_host |
include/linux/mmc/host.h |
主机控制器 |
struct mmc_host_ops |
同上 | 主机操作:request、set_ios、get_ro |
struct mmc_card |
include/linux/mmc/card.h |
卡 |
struct mmc_request / mmc_command / mmc_data |
include/linux/mmc/core.h |
请求/命令/数据 |
struct mmc_driver |
include/linux/mmc/driver.h |
卡驱动 |
关键 API:
cpp
struct mmc_host *mmc_alloc_host(int extra, struct device *dev);
int mmc_add_host(struct mmc_host *host);
void mmc_remove_host(struct mmc_host *host);
void mmc_free_host(struct mmc_host *host);
void mmc_request_done(struct mmc_host *host, struct mmc_request *mrq);
QEMU 验证:
bash
# QEMU 自带 SD 卡控制器
qemu-system-x86_64 ... -device sdhci-pci -drive file=sd.img,if=sd,format=raw
# guest 中
ls /sys/class/mmc_host/
ls /dev/mmcblk*
cat /sys/block/mmcblk0/size
7.2.5 要点:
mmc_host_ops.request是唯一的同步入口,由核心层串行化。- 必须正确实现
set_ios()(时钟、总线宽度、电压)。 - 7.2.5 支持
mmc_cqe_ops(命令队列)、HS400、UHS-II 等高速模式。 - 与 SDIO 的区别:SDIO 走
mmc_io接口而非块设备(见drivers/mmc/core/sdio*.c)。
13.9 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
open 签名仍是 block_device*, fmode_t |
编译错误 | 改成 gendisk*, blk_mode_t |
release 仍有两个参数 |
编译错误 | 改成 gendisk* 单参数 |
make_request/request_fn |
编译失败(已删除) | 改用 submit_bio 或 queue_rq |
忘记 blk_mq_start_request() |
超时/统计异常 | 必须调用 |
忘记 blk_mq_end_request() |
I/O 永久挂起 | 成功/失败都要完成 |
bio_for_each_segment 中忘记偏移 |
数据错位 | 用 bvec.bv_offset |
越界未 bio_io_error |
堆破坏 | 检查范围 |
用 alloc_disk() |
编译失败 | 改用 blk_alloc_disk(&lim, node) |
卸载只 put_disk() 不 del_gendisk() |
设备仍可见 | 先 del_gendisk() |
set_capacity() 单位错误 |
容量错误 | 单位是 512B 扇区 |
忘记 register_blkdev() |
设备号冲突 | 动态分配主设备号 |
13.10 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| vmem_disk(submit_bio) | 学习最简单的块设备 | lsblk 可见,dd 成功 |
| mkfs + mount | 验证块设备语义 | 文件系统可挂载 |
| blk-mq 版本 | 学习请求队列 | queue_rq 被调用 |
| 对比 brd | 学习内核参考实现 | modprobe brd rd_nr=2 rd_size=1024 |
| 调度器切换 | 理解 I/O 调度 | /sys/block/*/queue/scheduler |
| virtio-blk 观察 | 学习真实驱动 | blktrace/iostat |
| MMC/SD 实验 | 学习分层子系统 | /dev/mmcblk0 出现 |
13.11 总结
- 块设备的核心路径是 VFS → 页缓存 → bio → blk-mq → 驱动。
- 7.2.5 中
make_request与request_fn已删除;新驱动用submit_bio(简单)或queue_rq(需要队列时)。 blk_alloc_disk(&lim, node)取代alloc_disk(),队列限制通过struct queue_limits描述。open/release/ioctl的参数类型已改为gendisk/blk_mode_t。blk_mq_start_request()与blk_mq_end_request()必须成对出现。- I/O 调度器只剩
none/mq-deadline/kyber/bfq。 - MMC 子系统是"块设备 + 主机驱动/核心层分离"的最佳范例。
- QEMU 中用内存模拟块设备(vmem_disk/brd)即可完整验证原书的全部知识点。