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 请求,并通过调度策略优化性能。

Linux 内核 I/O 栈:块设备层位置 用户空间应用程序 虚拟文件系统(VFS) 页缓存(Page Cache) 块设备层(Block Layer) 块设备驱动框架 · I/O 调度器 · 请求队列 通用块层(Generic Block Layer) 具体设备驱动程序(SCSI/NVMe/MMC) 物理存储设备(HDD / SSD / NVMe)
关键观察:块设备层处于 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 之间的协作关系:

块设备层核心数据结构关系图 block_device bd_dev: 设备号 bd_disk → gendisk bd_block_size: 块大小 bd_contains: 主设备 bd_disk gendisk disk_name: "sda" queue → request_queue part_tbl: 分区表 fops: 操作函数集 queue request_queue elevator queue_head nr_requests request_fn I/O 请求流 VFS → block_device → gendisk → request_queue → I/O 调度器 → 设备驱动 分区场景: /dev/sda1 (block_device) → bd_contains → /dev/sda (block_device) → bd_disk → gendisk (sda) 所有分区共享同一个 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 隔离至关重要。