[toc]

block/genhd.c 通用硬盘驱动(Generic Hard Disk Driver) 内核中块设备的表示与管理

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了在Linux内核中创建一个统一的、抽象的块设备表示,以解决如何管理和表示各种各样的块存储设备(如硬盘、软盘、U盘、RAID卷等)的问题。

genhd.c(Generic Hard Disk)所代表的框架出现之前,内核中对不同类型块设备的管理是零散和不一致的。genhd.c的诞生旨在解决以下核心问题:

  • 设备抽象:需要一个通用的数据结构来描述一个块设备,无论它是物理硬盘、一个硬盘上的分区,还是一个由多个磁盘组成的逻辑卷。这个结构就是struct gendisk
  • 分区管理:一个物理磁盘通常被划分为多个分区。内核需要一个标准的机制来发现、解析和表示这些分区,并将每个分区也作为一个独立的块设备呈现给上层。genhd.c负责扫描分区表并为每个分区创建设备节点。
  • 设备注册与命名:需要一个统一的机制来向内核注册新的块设备,并为其分配一个唯一的设备号(major/minor number)和设备名(如sda, sdb1)。
  • 与sysfs和udev的集成:需要将块设备的拓扑结构(如哪个分区属于哪个磁盘)和属性(如大小、只读状态)暴露给用户空间,以便udev等工具可以自动创建设备节点(/dev/sda1)并执行相应配置。

简而言之,genhd.c是块设备在内核中的“户籍管理处”,它负责给每个块设备(及其分区)一个身份(gendisk),一个地址(设备号),一个名字,并管理它们的出生(注册)和消亡(注销)。

它的发展经历了哪些重要的里程碑或版本迭代?

genhd.c是块设备层最古老、最基础的部分之一,其发展与整个Linux设备模型的演进紧密相连。

  • 早期实现:最初的实现提供了gendisk结构的基本框架和分区扫描逻辑。
  • 与设备模型的集成:最重要的演进是与kobjectsysfs的深度集成。这使得gendisk所代表的块设备能够以目录和文件的形式,层次化地呈现在/sys/block/sys/class/block下。
    • 动态设备号:从静态分配主设备号,演变为支持动态分配,使得块设备驱动的加载更加灵活。
  • 支持GPT分区表:随着磁盘容量的增长,分区扫描逻辑从只支持传统的MBR分区表,扩展为支持现代的GUID分区表(GPT)。
  • 热插拔支持:为了支持USB存储和热插拔SATA盘,genhd.c的逻辑被增强,以处理设备的动态添加和移除。

目前该技术的社区活跃度和主流应用情况如何?

genhd.c是块设备层绝对的核心和基石,其代码非常稳定。

  • 核心地位:任何一个块设备驱动(无论是sd_mod for SATA/SCSI, nvme, virtio_blk, 还是md for RAID)都必须使用genhd.c提供的API(如alloc_disk, add_disk, del_gendisk)来将其设备注册给内核。
  • 社区状态:其代码库很少发生大的结构性变化。社区的活跃度主要体现在对新分区表格式的支持、修复与udev交互的边界问题,以及进行代码清理和现代化上。

核心原理与设计

它的核心工作原理是什么?

genhd.c的核心是围绕struct gendisk对象的生命周期管理,并充当设备驱动与VFS/块层之间的桥梁。

一个块设备的典型注册流程如下:

  1. 驱动探测设备:一个块设备驱动(如SATA驱动)首先通过总线(如PCIe)发现了一个物理设备(如一个SSD)。
  2. 分配gendisk:驱动调用alloc_disk()函数。这个函数由genhd.c提供,它会分配一个struct gendisk对象和一个关联的请求队列(request_queue)。gendisk是内核中代表一个“可分区的块设备”的通用对象。
  3. 填充gendisk:驱动程序需要填充这个gendisk对象的各个字段:
    • major, first_minor: 设备号。
    • disk_name: 设备名,如sda
    • fops: 一个block_device_operations结构,包含了.open, .release, .ioctl等回调函数,定义了当用户空间打开设备文件时VFS应如何操作。
    • queue: 指向刚刚创建的请求队列。
    • private_data: 指向驱动自己的私有数据结构。
  4. 注册设备 (add_disk):驱动填充完gendisk后,调用add_disk()。这是关键的一步,genhd.c中的add_disk()会执行一系列重要操作:
    • 注册块设备:将设备的主次设备号注册到内核。
    • 创建sysfs条目:在/sys/block/下创建一个以disk_name命名的目录,并在其中创建各种属性文件(如size, ro)。
    • 扫描分区:调用rescan_partitions(),它会读取设备的第一个扇区,尝试识别分区表类型(MBR, GPT等),并为找到的每个分区创建一个新的块设备(但共享同一个gendisk,通过分区号区分)。每个分区也会在sysfs中拥有自己的目录(如/sys/block/sda/sda1)。
    • 通知udev:通过kobject_uevent()向用户空间发送事件,通知udevd守护进程:“一个新的块设备(和它的分区)出现了!” udevd随后会在/dev目录下创建相应的设备文件。
  5. 设备注销 (del_gendisk):当设备被移除时,驱动会调用del_gendisk(),它会执行与add_disk相反的操作:删除sysfs条目、注销设备、并通知udev移除设备节点。

它的主要优势体現在哪些方面?

  • 高度抽象:提供了一个单一的gendisk模型来代表所有类型的块设备,极大地简化了内核的管理逻辑。
  • 自动化:自动处理了复杂的分区扫描和sysfs/udev事件通知,将驱动开发者从这些繁琐的事务中解放出来。
  • 代码复用:所有块设备驱动共享genhd.c中提供的通用管理代码。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 非块设备的局限gendisk模型是为块设备量身定做的。它不适用于字符设备(如终端、串口)或网络设备,这些设备有自己完全不同的注册和管理框架。
  • 分区扫描的刚性:其内置的分区扫描逻辑是基于标准的磁盘布局。对于一些具有非常规分区方案或完全没有分区的设备,可能需要驱动程序自己处理或绕过部分逻辑。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是Linux内核中注册和管理块设备的唯一且标准的解决方案。

  • 物理硬盘驱动:SATA, SAS, SCSI, NVMe驱动都使用gendisk来代表物理磁盘。
  • 可移动媒体驱动:USB U盘、SD卡驱动使用gendisk来代表这些设备。
  • 虚拟块设备驱动
    • RAM Disk (brd):在内存中模拟一个块设备。
    • Loop Device (loop):将一个常规文件模拟成一个块设备。
    • Device Mapper (dm) / LVM:将多个物理块设备组合成一个逻辑卷,这个逻辑卷在内核中就是一个gendisk
    • Software RAID (md):将多个磁盘组成RAID阵列,这个阵列就是一个gendisk
    • Virtio Block Driver (virtio_blk):在虚拟机中,呈现给客户机的虚拟硬盘就是一个gendisk

是否有不推荐使用该技术的场景?为什么?

如上所述,任何不属于“随机访问的、以块为单位进行寻址的存储设备”的场景,都不应使用gendisk。例如,一个流式磁带机就应该被实现为字符设备,因为它不支持随机块访问。

对比分析

请将其 与 其他相似技术 进行详细对比。

gendisk (块设备) vs. cdev (字符设备)

特性 gendisk (block/genhd.c) cdev (fs/char_dev.c)
抽象模型 可分区的、随机访问的块数组 字节流设备
核心数据结构 struct gendisk struct cdev
I/O单位 块/扇区 (通常是512B或4KB)。 字节 (Byte)
I/O路径 通过请求队列 (request_queue)bio结构进行I/O。 直接通过file_operations中的.read, .write, .ioctl等函数进行I/O。
缓存机制 通常使用页面缓存 (Page Cache) 来缓存磁盘块。 通常没有统一的缓存层(但驱动可以自己实现)。
典型设备 硬盘 (HDD, SSD), U盘, SD卡, LVM卷, RAID阵列。 终端 (/dev/tty), 串口 (/dev/ttyS), 鼠标, 打印机, 声卡 (/dev/snd/*), 零设备 (/dev/zero)。

gendisk vs. net_device (网络设备)

特性 gendisk (block/genhd.c) net_device (net/core/dev.c)
抽象模型 块存储。 数据包收发接口
核心数据结构 struct gendisk struct net_device
I/O单位 块/扇区。 网络数据包 (struct sk_buff)。
I/O路径 通过请求队列。 通过NAPI(中断/轮询)、ndo_start_xmit等函数进行数据包收发。
典型设备 硬盘, SSD。 以太网卡, Wi-Fi适配器, 虚拟网桥, loopback接口。

block/genhd.c 负责 Linux 内核中通用硬盘(General Hard Disk)对象的管理

block/genhd.c 负责 Linux 内核中通用硬盘(General Hard Disk)对象的管理。它的核心功能是为整个块存储设备(如一个完整的硬盘、SSD 或 U 盘)提供一个内核级的抽象表示,并管理其生命周期、分区信息以及与用户空间的通信

如果说 bio.c 定义了 I/O 操作,blk-mq.c 实现了 I/O 队列,bdev.c 管理着逻辑设备(主要是分区),那么 genhd.c 就是管理这些分区所属的物理设备容器


一、 核心功能

  1. 设备注册与注销: 提供一套 API 供块设备驱动程序使用,用于向内核注册一个新的物理块设备或将其移除。
  2. 分区表解析: 包含扫描和解析磁盘分区表(如 MBR 和 GPT)的逻辑。当一个新磁盘被注册时,它负责识别出磁盘上的各个分区。
  3. 设备编号管理: 负责为注册的磁盘及其分区分配主/次设备号(dev_t)。
  4. 用户空间接口: 通过 sysfs 文件系统(在 /sys/block/ 目录下)向用户空间导出磁盘的各种属性(如大小、只读状态、调度器信息等)。
  5. 事件通知: 当磁盘或分区状态发生改变时(如添加、移除、大小变更),负责生成 uevent 事件,通知 udev 等用户空间守护进程来执行相应操作(如创建/删除 /dev 下的设备节点)。

二、 解决的技术问题

genhd.c 的存在解决了以下关键问题:

  • 硬件驱动的标准化: 不同的存储设备(SATA, SCSI, NVMe, USB Mass Storage)其驱动实现细节千差万别。genhd.c 提供了一个名为 struct gendisk 的统一结构,使得任何块设备驱动都能以一种标准化的方式将其代表的物理设备呈现给内核的其余部分。
  • 物理设备与逻辑分区的统一管理: 一个物理磁盘可以被划分为多个逻辑分区。genhd.c 负责维护这种“容器”与“内容”的关系,确保对整个磁盘的操作(如重新扫描分区表)和对单个分区的操作能够被正确区分和管理。
  • 动态设备管理 (热插拔): 在系统运行时插入或移除存储设备(如 U 盘)需要一个可靠的机制来通知整个系统。genhd.c 通过 uevent 机制,成为了内核与用户空间就设备热插拔事件进行通信的核心枢纽。

三、 关键数据结构

struct gendisk

这是 genhd.c 中最核心的数据结构,它代表一个独立的、完整的块设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct gendisk {
int major; // 主设备号
int first_minor; // 第一个次设备号
int minors; // 占用的次设备号数量 (1 表示只有主设备, >1 表示有分区)

char disk_name[DISK_NAME_LEN]; // 磁盘名称, 如 "sda", "nvme0n1"

const struct block_device_operations *fops; // 指向驱动程序实现的操作函数集
struct request_queue *queue; // 指向该设备的请求队列 (由 blk-mq.c 管理)
void *private_data; // 驱动程序的私有数据

struct hd_struct *part_tbl; // 指向一个描述分区表的结构
struct block_device *part0; // 指向代表整个设备的 block_device 对象

sector_t capacity; // 设备容量 (以 512 字节扇区为单位)

// ... 用于 sysfs 和设备模型的字段
struct device dev;
};
  • major, first_minor, minors: 定义了该设备在 /dev 目录下的命名空间。例如,sda 占用一个主设备号,以及 16 个次设备号(sda 本身, sda1sda15)。
  • fops: 这是一个函数指针表,链接到具体设备驱动所实现的 open, release, ioctl 等操作。这是通用块层调用特定驱动代码的机制。
  • queue: 将 gendisk 对象与 blk-mq.c 管理的 I/O 队列关联起来。
  • part_tbl: 指向一个内部数据结构,该结构维护了一个分区列表。
  • capacity: 描述了设备的总大小,供 lsblk, fdisk 等工具读取。

四、 关键源码函数分析

genhd.c 向驱动程序提供了一套明确的 API 来管理 gendisk 的生命周期。

  • alloc_disk(int minors):
    这是 gendisk 的构造函数。它负责分配一个 struct gendisk 及其关联的数据结构。驱动程序在探测到一个新设备时首先调用此函数。minors 参数指定了该磁盘需要多少个次设备号(即最多支持多少个分区)。

  • put_disk(struct gendisk *disk):
    gendisk 的析构函数。当设备被移除时,用于释放 gendisk 占用的所有资源。

  • add_disk(struct gendisk *disk):
    这是最关键的函数之一。当驱动程序已经用设备信息(名称、容量、操作函数等)填充好 gendisk 结构体后,调用 add_disk() 将该设备正式注册到内核中。此函数会执行以下操作:

    1. 分配主/次设备号。
    2. sysfs 中创建对应的设备目录(例如 /sys/block/sda)。
    3. 调用 bdev_disk_changed(),后者会触发分区表扫描
    4. 为找到的每个分区创建对应的 block_device 对象。
    5. 向用户空间发送 uevent,通知 udevd 创建设备节点(如 /dev/sda/dev/sda1)。
  • del_gendisk(struct gendisk *disk):
    从系统中注销一个磁盘。它会清理 sysfs 条目,释放设备号,并通知用户空间设备已被移除。

  • bdev_revalidate_disk(struct gendisk *disk):
    强制内核重新读取并验证指定磁盘的分区表。用户空间的 partprobehdparm -z 命令最终会触发调用此函数。这对于在不重启的情况下让内核识别新的分区表更改非常重要。


五、 场景贯穿:插入一个 U 盘

  1. 硬件层: USB 子系统检测到新设备,并为其加载 usb-storage 驱动。
  2. 驱动层: usb-storage 驱动通过 SCSI 中间层与设备通信,识别出这是一个块设备(磁盘)。
  3. 调用 genhd.c API:
    • 驱动调用 alloc_disk() 创建一个新的 gendisk 实例。
    • 驱动获取 U 盘的容量、名称等信息,并填充到 gendisk 结构中(如 disk->capacity, disk->disk_name = "sdb")。同时,将 disk->fops 指向自己实现的 block_device_operations
    • 驱动调用 add_disk(disk)
  4. genhd.c 执行:
    • add_disk 分配设备号,创建 /sys/block/sdb
    • 它触发分区扫描,读取 U 盘的 MBR 或 GPT,发现一个分区。
    • 它为该分区创建一个 block_device 对象(代表 sdb1)。
    • 它生成 uevent
  5. 用户空间响应:
    • udevd 收到 uevent
    • 根据规则,udevd/dev/ 目录下创建 sdbsdb1 这两个设备文件。
  6. 完成: 此时,U 盘及其分区已经完全对系统可见,用户可以对其进行挂载和读写操作。

六、 总结

block/genhd.c 是 Linux 块设备层的设备注册与管理中心。它通过 struct gendisk 这一核心数据结构,为所有物理块设备提供了一个统一的、标准的内核表示。它不仅管理着设备的生命周期和分区结构,更扮演着内核与用户空间之间就块设备存在性与状态变化进行通信的关键桥梁角色。没有 genhd.c,系统将无法识别、管理或与用户交互任何物理存储设备。

Block Device Class Initialization

此代码片段负责在Linux内核启动时, 初始化并注册”block”设备类(device class)。设备类是Linux设备模型中的一个核心概念, 它提供了一种将具有相似功能或由同类驱动程序管理的设备进行分组的方式。在这里, 它创建了所有块设备(如硬盘、SD卡、U盘、RAID阵列等)共有的sysfs基础设施, 并在sysfs中创建了/sys/class/block目录。

此初始化的关键作用是建立一个统一的框架, 以便:

  1. 当新的块设备(在内核中由gendisk结构体表示)被添加到系统时, 它们可以自动地出现在/sys/class/block下。
  2. 为块设备定义一个通用的uevent(用户空间事件)生成规则, 以便udevmdev等用户空间守护进程能够接收到设备添加/移除/改变的通知, 并自动执行创建/删除设备节点(如/dev/sda, /dev/mmcblk0)等操作。

block_uevent: 为块设备uevent添加特定信息

这是一个回调函数, 它的作用是在内核为任何”block”类的设备生成uevent时, 向该事件的环境变量中添加一个额外的、块设备特有的变量DISKSEQ

DISKSEQ是一个序列号, 每当块设备的媒体状态发生变化时(例如, 插入或拔出SD卡, 或者一个CD-ROM驱动器更换了光盘), 这个序列号就会增加。用户空间的udev规则可以监控这个变量的变化。当udev检测到DISKSEQ改变时, 它就知道需要重新扫描该设备的分区表, 并相应地更新/dev下的分区设备节点(如/dev/sda1, /dev/sda2), 即使设备的主节点/dev/sda本身没有改变。这对于可靠地处理可移动媒体至关重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* block_uevent: "block"设备类的 uevent 回调函数.
* @dev: 指向一个 const struct device 的指针, 代表正在为其生成 uevent 的设备.
* @env: 指向一个 struct kobj_uevent_env 的指针, 这是一个用于累积 uevent 环境变量的缓冲区.
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
static int block_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
/*
* dev_to_disk 是一个宏, 它通过 container_of 从内嵌的 device 结构体指针反向计算出其容器 gendisk 结构体的地址.
* struct gendisk 代表一个独立的块设备或分区.
*/
const struct gendisk *disk = dev_to_disk(dev);

/*
* 调用 add_uevent_var 将一个自定义的环境变量添加到 uevent 中.
* "DISKSEQ=%llu": 变量名为 DISKSEQ, %llu 表示其值为一个64位无符号整数.
* disk->diskseq: gendisk 结构体中的磁盘序列号.
*/
return add_uevent_var(env, "DISKSEQ=%llu", disk->diskseq);
}

genhd_device_init: “block”设备类和相关子系统的初始化入口

这是在内核启动时被调用的主初始化函数。它执行了一系列注册操作, 奠定了整个块设备层的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*
* 定义一个 const struct class 实例, 代表 "block" 设备类.
*/
const struct class block_class = {
/* .name: 类的名称. 这将在 sysfs 中创建 /sys/class/block 目录. */
.name = "block",
/* .dev_uevent: 一个函数指针, 指向当此类中的任何设备生成 uevent 时要调用的回调函数. */
.dev_uevent = block_uevent,
};
/*
* genhd_device_init: 内核启动时的初始化函数.
* __init 宏表示此函数仅在初始化期间使用, 之后其内存可被回收.
*/
static int __init genhd_device_init(void)
{
int error;

/*
* 步骤1: 调用 class_register 将上面定义的 block_class 注册到内核中.
* 这是设备模型的核心步骤.
*/
error = class_register(&block_class);
/*
* unlikely() 是一个编译器优化提示, 表明此处的错误分支很少会发生.
*/
if (unlikely(error))
return error;
/*
* 步骤2: 调用 blk_dev_init(), 这是一个内部函数(未在此代码片段中显示),
* 用于初始化块设备层更深层的数据结构.
*/
blk_dev_init();

/*
* 步骤3: 注册一个主设备号 BLOCK_EXT_MAJOR (通常是259), 名称为 "blkext".
* 这个主设备号主要用于那些没有静态主设备号、需要动态分配次设备号的块设备.
*/
register_blkdev(BLOCK_EXT_MAJOR, "blkext");

/*
* 步骤4: 创建一个顶层的 kobject, 名为 "block".
* kobject_create_and_add("block", NULL) 会在 sysfs 的根目录下创建 /sys/block 目录.
* 这是一个历史遗留的、为了向后兼容而保留的目录, 现代内核主要使用 /sys/class/block.
*/
block_depr = kobject_create_and_add("block", NULL);
return 0;
}

/*
* subsys_initcall() 将 genhd_device_init 函数注册为在"子系统初始化"阶段被调用.
* 这个阶段确保了在它执行时, kobject 和 sysfs 等基础框架已经准备就绪.
*/
subsys_initcall(genhd_device_init);