3. 块设备驱动核心概念
在嵌入式存储系统中,块设备驱动是文件系统与物理存储介质之间的桥梁。本章深入剖析块设备与字符设备的本质区别、请求队列的运作机制、bio 结构体的 I/O 单元设计以及 gendisk 结构体的磁盘抽象模型,为后续 inode 分配实战奠定坚实的驱动层基础。
3.1 块设备与字符设备的区别
Linux 设备驱动模型将设备划分为两大类:字符设备和块设备。两者的核心差异在于数据访问方式、缓冲策略以及 I/O 调度模型。
| 对比维度 | 字符设备 (Char Device) | 块设备 (Block Device) |
|---|---|---|
| 数据访问单位 | 字节流,顺序或随机单字节访问 | 块(通常为 512 字节或 4KB),必须按块对齐 |
| 缓冲策略 | 无内核缓冲,直接与硬件交互 | 内核页高速缓存 (Page Cache),延迟写入 |
| I/O 调度 | 无调度,直接传递 | 请求队列 + I/O 调度器(如 CFQ、Deadline) |
| 设备节点类型 | c 类型 |
b 类型 |
| 典型设备 | 串口、键盘、鼠标、触摸屏 | 硬盘、eMMC、SD 卡、NVMe SSD |
| 文件系统挂载 | 不支持 | 支持(ext4、F2FS、Btrfs 等) |
| 主设备号范围 | 1–255(动态分配) | 1–255(动态分配) |
工程要点:块设备驱动必须实现
request_fn 或 make_request_fn 回调,而字符设备只需实现 open/read/write/release 文件操作。块设备的 I/O 路径比字符设备多出至少 3 层:VFS → Page Cache → I/O 调度器 → 块设备驱动。
3.2 块设备请求队列 (Request Queue)
请求队列是块设备驱动的核心调度单元。每个块设备对应一个 request_queue 实例,负责接收、排序、合并和分发 I/O 请求。
3.2.1 请求队列的初始化与注册
在驱动初始化阶段,通过 blk_init_queue() 或 blk_alloc_queue() 创建请求队列,并绑定处理函数:
#include <linux/blkdev.h>
static struct request_queue *q;
/* 方式一:使用 blk_init_queue(传统方式,绑定 request_fn) */
q = blk_init_queue(my_request_fn, &my_lock);
if (!q)
return -ENOMEM;
/* 方式二:使用 blk_alloc_queue + make_request_fn(更灵活) */
q = blk_alloc_queue(GFP_KERNEL);
if (!q)
return -ENOMEM;
blk_queue_make_request(q, my_make_request_fn);
3.2.2 请求队列的核心数据结构
| 成员 | 类型 | 说明 |
|---|---|---|
queue_head |
struct list_head |
请求链表头,所有待处理请求挂载于此 |
elevator |
struct elevator_queue * |
I/O 调度器实例(如 CFQ、Deadline、NOOP) |
make_request_fn |
make_request_fn * |
自定义请求处理函数(直接 bio 模式) |
request_fn |
request_fn_proc * |
标准请求处理函数(传统模式) |
queuedata |
void * |
驱动私有数据指针 |
nr_requests |
unsigned int |
队列中最大请求数(流控用) |
实战建议:在嵌入式场景中,推荐使用
blk_alloc_queue + make_request_fn 模式,跳过 I/O 调度器,直接处理 bio,减少延迟。适用于 eMMC、SD 卡等顺序访问友好的设备。
3.3 bio 结构体
bio 是 Linux 块 I/O 层的基本传输单元,代表一次 I/O 操作。它替代了早期的 buffer_head 结构,支持 向量化 I/O 和 分散/聚集 (Scatter-Gather) 传输。
3.3.1 bio 结构体核心成员
struct bio {
sector_t bi_sector; /* 起始扇区号(512 字节为单位) */
struct bio *bi_next; /* 链表中的下一个 bio */
struct block_device *bi_bdev; /* 目标块设备 */
unsigned long bi_flags; /* 标志位(如 BIO_UPTODATE) */
unsigned short bi_vcnt; /* bio_vec 向量数量 */
unsigned short bi_idx; /* 当前处理的 bio_vec 索引 */
unsigned int bi_size; /* 剩余传输字节数 */
struct bio_vec *bi_io_vec; /* bio_vec 数组指针 */
bio_end_io_t *bi_end_io; /* I/O 完成回调函数 */
void *bi_private; /* 驱动私有数据 */
};
3.3.2 bio_vec 与向量化 I/O
每个 bio 包含多个 bio_vec,每个 bio_vec 描述一个内存页片段:
struct bio_vec {
struct page *bv_page; /* 指向物理页框 */
unsigned int bv_len; /* 片段长度(字节) */
unsigned int bv_offset; /* 页内偏移 */
};
遍历 bio 的标准方式:
struct bio_vec *bvec;
unsigned long flags;
int i;
bio_for_each_segment(bvec, bio, i) {
char *data = kmap_atomic(bvec->bv_page) + bvec->bv_offset;
/* 处理 data,长度为 bvec->bv_len */
kunmap_atomic(data);
}
常见陷阱:bio 的
bi_sector 始终以 512 字节扇区为单位,即使物理设备使用 4KB 扇区。驱动内部必须进行扇区大小转换。另外,bio_for_each_segment 宏在遍历时不会自动处理跨页边界,需注意 bv_len 可能小于页大小。
3.4 gendisk 结构体
gendisk (Generic Disk) 是 Linux 内核中代表一个块设备的抽象结构。每个块设备(如 /dev/sda、/dev/mmcblk0)在内核中对应一个 gendisk 实例。
3.4.1 gendisk 核心成员
| 成员 | 类型 | 说明 |
|---|---|---|
major |
int |
主设备号 |
first_minor |
int |
起始次设备号 |
minors |
int |
支持的次设备号数量(分区数) |
disk_name |
char [32] |
设备名称(如 sda、mmcblk0) |
queue |
struct request_queue * |
关联的请求队列 |
fops |
const struct block_device_operations * |
块设备操作函数表 |
private_data |
void * |
驱动私有数据指针 |
part_tbl |
struct disk_part_tbl * |
分区表 |
flags |
int |
标志位(如 GENHD_FL_REMOVABLE) |
3.4.2 gendisk 生命周期管理
典型的 gendisk 分配、注册与释放流程:
/* 1. 分配 gendisk 结构 */
struct gendisk *gd = alloc_disk(16); /* 16 个次设备号(分区) */
if (!gd)
return -ENOMEM;
/* 2. 初始化 gendisk 字段 */
gd->major = my_major;
gd->first_minor = 0;
gd->minors = 16;
gd->fops = &my_block_ops;
gd->queue = q; /* 关联之前创建的请求队列 */
gd->private_data = my_dev; /* 驱动私有数据结构 */
strcpy(gd->disk_name, "myblk");
/* 3. 设置设备容量(以 512 字节扇区为单位) */
set_capacity(gd, nr_sectors);
/* 4. 注册 gendisk 到内核 */
add_disk(gd);
/* 5. 卸载时释放 */
del_gendisk(gd);
put_disk(gd);
3.4.3 block_device_operations 函数表
static const struct block_device_operations my_block_ops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.ioctl = my_ioctl,
.getgeo = my_getgeo, /* 获取磁盘几何参数(CHS) */
};
关键约束:
alloc_disk() 的参数 minors 必须是 2 的幂(如 1、2、4、8、16、32、64)。set_capacity() 设置的扇区数必须与设备物理容量一致,否则文件系统会检测到容量不匹配错误。
3.5 核心概念总结
块设备驱动的四个核心概念构成了完整的 I/O 路径:
- 块设备 vs 字符设备:块设备以块为单位、有内核缓冲、支持 I/O 调度和文件系统挂载;字符设备以字节流方式工作,无缓冲无调度。
- 请求队列 (request_queue):I/O 请求的调度中心,支持合并、排序和流控,是块设备驱动的入口。
- bio 结构体:I/O 操作的基本单元,通过 bio_vec 实现向量化传输,是高效 DMA 的基础。
- gendisk 结构体:块设备的抽象表示,关联请求队列、操作函数表和私有数据,是驱动与内核块设备层的接口。
下一阶段预告:在掌握这些核心概念后,第 4 章将进入实战环节——编写一个完整的 RAM 磁盘块设备驱动,实现 bio 处理、请求队列管理和 gendisk 注册,为后续 inode 分配提供底层存储环境。