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 调度器 → 块设备驱动。
字符设备 vs 块设备 I/O 路径对比 用户态应用 (open/read/write) 字符设备驱动 (file_operations) 硬件设备 (串口/GPIO) 无缓冲 · 无调度 · 字节流 用户态应用 (open/read/write) VFS + Page Cache (页高速缓存) I/O 调度器 + 请求队列 块设备驱动 (gendisk + bio) 有缓冲 · 有调度 · 块对齐

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 卡等顺序访问友好的设备。
请求队列 (request_queue) 内部结构 request_queue queue_head (链表) elevator (调度器) make_request_fn queuedata Request #1 Request #2 Request #3

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 可能小于页大小。
bio 结构体与 bio_vec 向量化 I/O struct bio bi_sector: 0x1000 bi_size: 4096 bi_vcnt: 3 bi_idx: 0 bi_io_vec → bi_end_io: callback bio_vec[0] page=0x1234 len=2048 off=0 bio_vec[1] page=0x5678 len=1024 off=0 bio_vec[2] page=0x9abc len=1024 off=0 向量化 I/O 优势 • 一次系统调用传输多个 不连续内存片段 • 减少 DMA 映射次数 • 支持 Scatter-Gather DMA • 提升大 I/O 吞吐量

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() 设置的扇区数必须与设备物理容量一致,否则文件系统会检测到容量不匹配错误。
gendisk 与块设备驱动架构关系 用户空间 (VFS / 文件系统 / 应用) 内核块设备层 (block layer) I/O 调度器 · 请求队列 · bio 处理 struct gendisk major / first_minor / fops / queue / private_data 块设备驱动 (request_fn / make_request_fn + 硬件访问)

3.5 核心概念总结

块设备驱动的四个核心概念构成了完整的 I/O 路径:

  • 块设备 vs 字符设备:块设备以块为单位、有内核缓冲、支持 I/O 调度和文件系统挂载;字符设备以字节流方式工作,无缓冲无调度。
  • 请求队列 (request_queue):I/O 请求的调度中心,支持合并、排序和流控,是块设备驱动的入口。
  • bio 结构体:I/O 操作的基本单元,通过 bio_vec 实现向量化传输,是高效 DMA 的基础。
  • gendisk 结构体:块设备的抽象表示,关联请求队列、操作函数表和私有数据,是驱动与内核块设备层的接口。
下一阶段预告:在掌握这些核心概念后,第 4 章将进入实战环节——编写一个完整的 RAM 磁盘块设备驱动,实现 bio 处理、请求队列管理和 gendisk 注册,为后续 inode 分配提供底层存储环境。