[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
结构的基本框架和分区扫描逻辑。 - 与设备模型的集成:最重要的演进是与
kobject
和sysfs
的深度集成。这使得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/块层之间的桥梁。
一个块设备的典型注册流程如下:
- 驱动探测设备:一个块设备驱动(如SATA驱动)首先通过总线(如PCIe)发现了一个物理设备(如一个SSD)。
- 分配
gendisk
:驱动调用alloc_disk()
函数。这个函数由genhd.c
提供,它会分配一个struct gendisk
对象和一个关联的请求队列(request_queue
)。gendisk
是内核中代表一个“可分区的块设备”的通用对象。 - 填充
gendisk
:驱动程序需要填充这个gendisk
对象的各个字段:major
,first_minor
: 设备号。disk_name
: 设备名,如sda
。fops
: 一个block_device_operations
结构,包含了.open
,.release
,.ioctl
等回调函数,定义了当用户空间打开设备文件时VFS应如何操作。queue
: 指向刚刚创建的请求队列。private_data
: 指向驱动自己的私有数据结构。
- 注册设备 (
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
目录下创建相应的设备文件。
- 设备注销 (
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
。
- RAM Disk (
是否有不推荐使用该技术的场景?为什么?
如上所述,任何不属于“随机访问的、以块为单位进行寻址的存储设备”的场景,都不应使用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
就是管理这些分区所属的物理设备容器。
一、 核心功能
- 设备注册与注销: 提供一套 API 供块设备驱动程序使用,用于向内核注册一个新的物理块设备或将其移除。
- 分区表解析: 包含扫描和解析磁盘分区表(如 MBR 和 GPT)的逻辑。当一个新磁盘被注册时,它负责识别出磁盘上的各个分区。
- 设备编号管理: 负责为注册的磁盘及其分区分配主/次设备号(
dev_t
)。 - 用户空间接口: 通过
sysfs
文件系统(在/sys/block/
目录下)向用户空间导出磁盘的各种属性(如大小、只读状态、调度器信息等)。 - 事件通知: 当磁盘或分区状态发生改变时(如添加、移除、大小变更),负责生成
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 | struct gendisk { |
major
,first_minor
,minors
: 定义了该设备在/dev
目录下的命名空间。例如,sda
占用一个主设备号,以及 16 个次设备号(sda
本身,sda1
到sda15
)。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()
将该设备正式注册到内核中。此函数会执行以下操作:- 分配主/次设备号。
- 在
sysfs
中创建对应的设备目录(例如/sys/block/sda
)。 - 调用
bdev_disk_changed()
,后者会触发分区表扫描。 - 为找到的每个分区创建对应的
block_device
对象。 - 向用户空间发送
uevent
,通知udevd
创建设备节点(如/dev/sda
和/dev/sda1
)。
del_gendisk(struct gendisk *disk)
:
从系统中注销一个磁盘。它会清理sysfs
条目,释放设备号,并通知用户空间设备已被移除。bdev_revalidate_disk(struct gendisk *disk)
:
强制内核重新读取并验证指定磁盘的分区表。用户空间的partprobe
或hdparm -z
命令最终会触发调用此函数。这对于在不重启的情况下让内核识别新的分区表更改非常重要。
五、 场景贯穿:插入一个 U 盘
- 硬件层: USB 子系统检测到新设备,并为其加载
usb-storage
驱动。 - 驱动层:
usb-storage
驱动通过 SCSI 中间层与设备通信,识别出这是一个块设备(磁盘)。 - 调用
genhd.c
API:- 驱动调用
alloc_disk()
创建一个新的gendisk
实例。 - 驱动获取 U 盘的容量、名称等信息,并填充到
gendisk
结构中(如disk->capacity
,disk->disk_name = "sdb"
)。同时,将disk->fops
指向自己实现的block_device_operations
。 - 驱动调用
add_disk(disk)
。
- 驱动调用
genhd.c
执行:add_disk
分配设备号,创建/sys/block/sdb
。- 它触发分区扫描,读取 U 盘的 MBR 或 GPT,发现一个分区。
- 它为该分区创建一个
block_device
对象(代表sdb1
)。 - 它生成
uevent
。
- 用户空间响应:
udevd
收到uevent
。- 根据规则,
udevd
在/dev/
目录下创建sdb
和sdb1
这两个设备文件。
- 完成: 此时,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
目录。
此初始化的关键作用是建立一个统一的框架, 以便:
- 当新的块设备(在内核中由
gendisk
结构体表示)被添加到系统时, 它们可以自动地出现在/sys/class/block
下。 - 为块设备定义一个通用的
uevent
(用户空间事件)生成规则, 以便udev
或mdev
等用户空间守护进程能够接收到设备添加/移除/改变的通知, 并自动执行创建/删除设备节点(如/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 | /* |
genhd_device_init
: “block”设备类和相关子系统的初始化入口
这是在内核启动时被调用的主初始化函数。它执行了一系列注册操作, 奠定了整个块设备层的基础。
1 | /* |