1. 块设备层概述
1.1 块设备与字符设备的本质区别
在 Linux 内核的 I/O 体系中,设备被划分为两大类型:块设备(Block Device)和字符设备(Character Device)。理解二者的差异是掌握块设备层的前提。
| 对比维度 | 块设备 | 字符设备 |
|---|---|---|
| 数据访问单位 | 以固定大小的块(通常为 512B ~ 4KB)为单位进行读写 | 以字节流形式连续读写,无固定块边界 |
| 随机访问能力 | 支持随机寻址(通过块号定位任意位置) | 通常只支持顺序访问(如键盘、鼠标) |
| 缓存与调度 | 内核提供页缓存(Page Cache)和I/O 调度器,合并与排序请求 | 无缓存层,数据直接传递到驱动程序 |
| 典型设备 | 硬盘(HDD/SSD)、U 盘、SD 卡、NVMe、loop 设备 | 串口、声卡、键盘、鼠标、终端(tty) |
| 设备节点类型 | b(如 /dev/sda) |
c(如 /dev/tty0) |
| 关键系统调用 | read()/write() 经过 VFS → 页缓存 → 块设备层 |
read()/write() 直接调用驱动 file_operations |
核心要点:块设备的核心特征是可随机访问的固定大小块,这决定了内核必须引入专门的调度层来优化 I/O 顺序与合并,而字符设备则不需要。
1.2 块设备层在内核中的位置
块设备层位于 Linux 内核 I/O 栈的中间枢纽位置,向上承接虚拟文件系统(VFS)和页缓存,向下对接具体设备驱动程序。其核心职责是:将文件系统发来的逻辑 I/O 请求,转化为物理设备可执行的块 I/O 请求,并通过调度策略优化性能。
关键观察:块设备层处于 I/O 路径的核心调度位置。所有文件系统 I/O 都必须经过块设备层的请求队列,由 I/O 调度器决定请求的合并、排序与分发策略。这也是我们课程优化的主要战场。
1.3 块设备层核心数据结构概览
块设备层的实现围绕三个核心数据结构展开:block_device、gendisk 和 request_queue。它们构成了从“设备文件”到“物理磁盘”再到“I/O 请求队列”的完整链路。
1.3.1 block_device:块设备实例
block_device 结构体代表一个已打开或已发现的块设备实例。它关联了设备号、分区信息、引用计数以及指向 gendisk 的指针。
// include/linux/fs.h (简化核心字段)
struct block_device {
dev_t bd_dev; // 设备号(主+次)
struct gendisk *bd_disk; // 指向所属的 gendisk
struct block_device *bd_contains; // 指向整个磁盘设备(分区时使用)
unsigned bd_block_size; // 块大小(通常 512 或 4096)
struct super_block *bd_super; // 挂载的文件系统超级块
int bd_openers; // 打开计数
spinlock_t bd_size_lock; // 大小变更锁
// ...
};
理解要点:每个分区都有一个独立的
block_device 实例,它们通过 bd_contains 指向整个磁盘的 block_device,再通过 bd_disk 找到对应的 gendisk。
1.3.2 gendisk:通用磁盘描述
gendisk 是内核中通用磁盘的抽象,代表一个物理磁盘或虚拟块设备(如 RAM disk、loop 设备)。它包含了磁盘的容量、分区表、设备号范围以及最重要的——指向 request_queue 的指针。
// include/linux/genhd.h (简化核心字段)
struct gendisk {
int major; // 主设备号
int first_minor; // 第一个次设备号
int minors; // 次设备号数量(分区数+1)
char disk_name[DISK_NAME_LEN]; // 设备名,如 "sda"
struct disk_part_tbl *part_tbl; // 分区表
struct request_queue *queue; // 指向该磁盘的请求队列(核心!)
struct block_device_operations *fops; // 块设备操作函数集
unsigned long state; // 设备状态
int flags; // 标志位
// ...
};
1.3.3 request_queue:I/O 请求队列
request_queue 是块设备层的调度核心。所有发往该设备的 I/O 请求(struct request)都会进入此队列,由 I/O 调度器(如 CFQ、Deadline、MQ-Deadline、BFQ)进行合并、排序和分发。
// include/linux/blkdev.h (简化核心字段)
struct request_queue {
struct list_head queue_head; // 请求链表头
struct elevator_queue *elevator; // I/O 调度器实例
struct blk_mq_ctx *mq_ctx; // 多队列上下文
struct blk_mq_hw_ctx **queue_hw_ctx; // 多队列硬件上下文
unsigned long nr_requests; // 队列中最大请求数
unsigned int queue_depth; // 队列深度
spinlock_t queue_lock; // 队列自旋锁
struct gendisk *disk; // 反向指向所属磁盘
request_fn_proc *request_fn; // 请求处理函数
// ...
};
1.4 三大数据结构的关系
下图展示了 block_device、gendisk 和 request_queue 之间的协作关系:
1.5 关键设计要点总结
- 分层解耦:
block_device关注设备实例与分区,gendisk关注磁盘整体属性,request_queue专注 I/O 调度。三者职责清晰,便于扩展。 - 共享队列:同一物理磁盘的所有分区共享同一个
gendisk和request_queue。这意味着 I/O 调度器看到的是整个磁盘的请求流,而非单个分区。 - 调度入口:
request_queue中的elevator字段指向具体的 I/O 调度器实例。切换调度器(如从 CFQ 切换到 Deadline)本质上是替换这个指针指向的调度算法实现。 - 多队列演进:现代内核(Linux 5.0+)已全面采用 blk-mq(多队列块层),
request_queue内部包含多个软件/硬件上下文,以适配 NVMe 等多队列设备的高并发需求。
常见误区:初学者常误以为
block_device 和 gendisk 是一对一关系。实际上,一个 gendisk 可以对应多个 block_device(每个分区一个),但所有分区共享同一个 request_queue。理解这一点对后续学习 I/O 调度和 QoS 隔离至关重要。