[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_modfor SATA/SCSI,nvme,virtio_blk, 还是mdfor 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 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 | /* |
磁盘 Sysfs 属性定义:disk_attrs, disk_visible 与 disk_attr_groups
本代码片段是 Linux 内核设备模型中一个声明式的定义,其核心功能是为所有通用磁盘设备(gendisk)构建它们在 sysfs 文件系统(通常挂载于 /sys)中的属性文件接口。它通过一系列静态定义的数组和结构体,精确地描述了哪些文件应该被创建(disk_attrs),这些文件是否应该对特定设备可见(disk_visible),以及如何根据内核配置模块化地添加更多的属性组(disk_attr_groups)。
实现原理分析
此代码是 sysfs 工作原理的教科书式范例。它本身不包含复杂的执行逻辑,而是为 sysfs核心框架提供了一份“蓝图”或“元数据”,sysfs 核心会依据这份蓝图来动态地创建和管理文件。
属性定义 (
disk_attrs):disk_attrs是一个以NULL结尾的struct attribute *数组。数组中的每一个元素(如&dev_attr_size.attr)都指向一个device_attribute结构体(未在此代码段中完全展示)。- 每个
device_attribute结构都封装了一个 sysfs 文件的所有信息:文件名(如 “size”)、文件权限(mode),以及最重要的——当文件被读取时要调用的show函数和被写入时要调用的store函数。 - 这个数组定义了所有磁盘设备共同拥有的基础属性集,例如
size(大小),ro(只读状态),removable(是否可移除),inflight(在途I/O数) 等。
动态可见性 (
disk_visible):disk_attr_group中的.is_visible回调是一个强大的特性,它允许 sysfs 接口是动态的,而非静态的。- 当 sysfs 核心准备为一个特定的磁盘设备创建其 sysfs 目录时,它会遍历
disk_attrs数组中的每一个属性,并调用disk_visible函数来“询问”:“对于这个具体的设备,我应该创建这个属性文件吗?” disk_visible的实现逻辑是:
a. 首先,通过container_of宏,从通用的kobject指针反向推导出具体的gendisk对象。这是 sysfs 回调的标准做法。
b. 然后,它包含一个特殊逻辑:if (a == &dev_attr_badblocks.attr && !disk->bb)。这个条件判断的含义是:“如果要创建的属性是badblocks,并且当前这个磁盘没有配置坏块管理功能(disk->bb为 NULL),那么就返回模式0”。返回0会阻止 sysfs 创建这个文件。
c. 对于所有其他属性,它返回a->mode,即该属性预定义的默认文件权限,允许文件被创建。- 原理: 这实现了基于设备能力的 sysfs 接口自适应。一个不支持坏块追踪的磁盘,其 sysfs 目录下就不会出现
badblocks这个文件,使得接口更加干净和准确。
模块化扩展 (
disk_attr_groups):disk_attr_groups是一个struct attribute_group *数组。它允许将多个属性组附加到同一个设备上。- 基本组
disk_attr_group总是存在的。 - 通过
#ifdef条件编译,可以根据内核的配置,动态地将额外的属性组(如blk_trace_attr_group用于 I/O 追踪,blk_integrity_attr_group用于数据完整性功能)添加到这个数组中。 - 当 sysfs 为设备创建文件时,它会遍历这个数组中的所有组,并为每个组中的所有可见属性创建文件。这是一种高度模块化的设计,使得内核的不同子系统可以在不修改块设备核心代码的情况下,向磁盘设备的 sysfs 接口中“注入”自己的属性文件。
代码分析
1 | // 定义一个属性指针数组,列出了所有磁盘设备通用的 sysfs 属性。 |
Gendisk 资源释放:disk_release
本代码片段展示了 Linux 内核中通用磁盘(gendisk)对象的“析构函数”——disk_release。其核心功能是在一个 gendisk 对象的生命周期结束时(即其最后一个引用被释放时),以严格的、反向依赖的顺序,系统地拆除和释放该磁盘所占用的所有内核资源。这包括 I/O 追踪、内存池、分区表、请求队列的引用以及驱动自定义资源等,最终释放 gendisk 结构本身占用的内存。
实现原理分析
disk_release 是内核资源管理中“对称性原则”的典范,它精确地“撤销”了磁盘初始化时所进行的所有资源分配和注册操作。其设计的关键在于严格的、分阶段的、有条件的资源释放顺序。
触发机制(由设备模型驱动):
- 此函数被注册为
disk_type的.release回调。当一个代表磁盘的struct device的最后一个引用被put_device()释放时,设备模型核心会自动调用disk_release。这保证了只有在内核中没有任何地方再使用这个设备对象时,清理工作才会开始。
- 此函数被注册为
严格的逆序清理:
disk_release的执行顺序是精心设计的,以避免 use-after-free 和资源泄漏。它大致遵循了资源依赖关系的反向顺序:
a. 首先,拆除附加在请求队列(request_queue)之上的服务,如 I/O 追踪 (blk_trace_remove) 和块设备 cgroup (blkcg_exit_disk)。
b. 然后,释放gendisk自身拥有的、不直接依赖于请求队列的资源,如用于 bio 拆分的内存池 (bioset_exit)、事件通知机制 (disk_release_events)、分区表 XArray (xa_destroy) 等。
c. 接下来是关键的解耦步骤:断开gendisk和request_queue之间的双向链接,并释放gendisk对request_queue的引用 (blk_put_queue)。此时,如果gendisk是request_queue的唯一持有者,blk_put_queue将会触发请求队列自身的释放。
d. 之后,调用驱动程序提供的可选回调fops->free_disk,允许驱动释放其私有的、与gendisk关联的资源。
e. 最后,也是最重要的一步,调用bdev_drop(disk->part0)。part0代表了整个磁盘的block_device结构,释放对它的引用最终会触发gendisk结构体本身被kfree。
状态依赖的健壮性:
WARN_ON_ONCE(disk_live(disk)): 这是一个重要的健全性检查。disk_live检查磁盘的打开计数器。理论上,当disk_release被调用时,所有用户空间的文件描述符都应已关闭,打开计数应为零。如果此断言触发,表明系统中存在引用计数管理的缺陷。- 处理探测失败:
if (... !test_bit(GD_ADDED, &disk->state))这个条件块专门处理一种特殊情况:驱动在探测(probe)过程中成功分配了请求队列,但在调用add_disk将磁盘正式添加到系统之前就失败了。在这种情况下,需要调用一个不同的清理函数blk_mq_exit_queue来正确地撤销初始化。这体现了代码对初始化失败路径的健壮处理。
代码分析
1 | /** |
磁盘设备类型定义与设备节点创建:disk_type 与 block_devnode
本代码片段定义了 Linux 内核中所有通用磁盘(gendisk)的标准 device_type,即 disk_type。其核心功能是为内核设备模型提供一个统一的接口集合,用于处理所有磁盘类设备的通用操作,例如 sysfs 属性的创建、设备释放时的清理,以及最重要的——决定该设备在 /dev 目录下的设备节点(device node)应如何创建。block_devnode 函数正是这个设备节点创建策略的默认实现。
实现原理分析
此机制是内核设备模型与 devtmpfs(或 uevent 守护进程)协作,自动在 /dev 目录下创建设备文件的关键环节。
struct device_type的角色:- 在 Linux 设备模型中,
device_type允许多个设备共享一组通用的回调函数。所有gendisk对象在注册时,都会将其struct device成员的type字段指向disk_type。 disk_type结构体聚合了几个关键的回调:
a..groups:disk_attr_groups定义了将在/sys/class/block/<devname>/目录下为该磁盘创建的默认 sysfs 属性文件集(如size,ro,queue/scheduler等)。
b..release:disk_release是一个清理函数。当一个gendisk对象的最后一个引用被释放时,此函数会被调用,以确保所有相关内存都被正确回收。
c..devnode: 这是最重要的回调。当devtmpfs或ueventd需要为这个新出现的设备创建设备文件时,它们会调用此函数来获取设备文件名和权限。
- 在 Linux 设备模型中,
block_devnode的分发(Dispatcher)逻辑:block_devnode本身并不直接决定设备名。它扮演着一个分发者或策略选择器的角色。- 它首先将通用的
struct device指针转换回具体的struct gendisk指针。 - 然后,它检查该
gendisk关联的文件操作集(disk->fops)中,是否定义了一个更具体的devnode回调。 - 场景一(驱动自定义): 某些特殊的块设备驱动(例如,一个虚拟磁盘驱动)可能希望对其设备有特殊的命名规则或权限设置。这样的驱动会在其
block_device_operations结构中提供自己的devnode函数。在这种情况下,block_devnode就会调用这个驱动专属的函数,并将结果返回。 - 场景二(使用默认): 对于绝大多数标准块设备驱动(如
sd_modfor SATA/SCSI,mmc_blockfor SD/eMMC),它们不会提供自己的devnode函数。在这种情况下,disk->fops->devnode为NULL,于是block_devnode也返回NULL。 NULL的含义: 返回NULL是一个明确的信号,它告诉devtmpfs:“请使用默认的命名规则”。对于块设备,这个默认规则就是使用gendisk->disk_name字段中存储的名字(如 “sda”, “mmcblk0”, “mmcblk0p1”)来创建设备节点。
代码分析
1 | /** |
块设备迭代器与 /proc/partitions 实现:disk_seqf_start, show_partition 等
本代码片段展示了 Linux 内核中 /proc/partitions 虚拟文件的完整实现。其核心功能是提供一个标准的、可遍历的迭代器,用于安全地枚举系统中所有的通用磁盘设备(gendisk),并利用这个迭代器来生成 /proc/partitions 文件的内容,向用户空间展示所有磁盘及其分区的布局信息,包括主/次设备号、大小和名称。
实现原理分析
此代码是 procfs 中 seq_file 机制与内核设备模型(Device Model)相结合的典范。它将复杂的、需要加锁的设备链表遍历过程,封装在一组标准的迭代器接口 (start, next, stop) 之后,从而为上层提供了简洁、高效、安全的数据生成方式。
设备类迭代器 (
class_dev_iter):- 遍历内核设备不是一个简单的链表遍历。设备可以被动态添加和删除,需要正确的锁来保护。
class_dev_iter是内核设备模型提供的标准工具,专门用于安全地迭代一个特定“类”(class)下的所有设备。 disk_seqf_start函数通过class_dev_iter_init(iter, &block_class, ...)来初始化一个迭代器,使其专门遍历所有注册到block_class(块设备类)的设备。这确保了它能找到系统中所有的磁盘。
- 遍历内核设备不是一个简单的链表遍历。设备可以被动态添加和删除,需要正确的锁来保护。
seq_file迭代模型与状态保持:start:disk_seqf_start不仅初始化迭代器,还处理*pos(位置偏移量)。当用户空间程序多次调用read()来读取一个大文件时,*pos会记录当前已经读取到的条目数。start函数通过一个do-while循环来跳过 (skip) 前*pos个设备,从而实现从上一次读取结束的位置继续生成内容。它将迭代器状态 (iter) 存储在seqf->private中,以便next和stop函数可以访问。next:disk_seqf_next的职责非常简单:调用class_dev_iter_next从迭代器中获取下一个设备,并递增*pos。stop:disk_seqf_stop是清理函数。它必须调用class_dev_iter_exit来释放迭代器持有的锁和资源,并kfree掉在start中分配的内存。代码中的if (iter)检查是一个健壮性设计,因为stop即使在start失败返回NULL或ERR_PTR的情况下也可能被调用。
/proc/partitions 的构建:
partitions_op结构将上述通用磁盘迭代器与特定的内容展示逻辑 (show_partition) 结合起来。show_partition_start是一个装饰器(decorator),它调用通用的disk_seqf_start来获取第一个磁盘对象,但增加了一个逻辑:仅在文件首次被读取时 (!*pos) 才通过seq_puts打印表头。show_partition是核心的显示函数。对于seq_file框架传入的每一个gendisk对象 (sgp),它会执行一个内层循环xa_for_each(&sgp->part_tbl, ...)来遍历该磁盘的所有分区。它使用rcu_read_lock来安全地遍历分区表,并打印每个有效分区的信息。
代码分析
1 |
|
/proc/diskstats 接口实现:diskstats_show 与 proc_genhd_init
本代码片段展示了 Linux 内核中 /proc/diskstats 虚拟文件的实现。其核心功能是为用户空间提供一个统一的、聚合的接口,用于获取系统中所有磁盘及其分区的详细 I/O 统计信息。它通过 procfs 的 seq_file 机制来实现,当用户读取该文件时,diskstats_show 函数被调用,该函数会遍历系统中的所有磁盘和分区,并格式化输出每个设备的 I/O 统计数据,如读写次数、合并次数、读写扇区数以及 I/O 处理时间等。
实现原理分析
此代码是内核通过 procfs 暴露性能监控数据的经典范例,其实现依赖于块设备层的统计子系统和 seq_file 迭代器接口。
seq_file迭代器模型:/proc/diskstats的内容可能会很长,seq_file是内核提供的标准机制,用于高效、安全地生成这类可能跨越多个内存页的大文件内容。- 它不是一次性生成所有内容,而是采用迭代器模式。
diskstats_op结构定义了一组操作函数:
a..start:disk_seqf_start被首先调用,用于初始化遍历,通常是锁定资源并找到第一个要显示的条目(第一个gendisk)。
b..next:disk_seqf_next在每次show调用后被调用,用于找到下一个要显示的条目。
c..show:diskstats_show是核心函数,负责将当前条目(由start或next提供,通过v参数传入)格式化输出。
d..stop:disk_seqf_stop在遍历结束或中断时被调用,用于释放资源(如解锁)。
数据源与遍历 (
diskstats_show):- 外层循环(磁盘): 由
seq_file的start/next机制隐式实现,它会遍历系统中所有的gendisk对象(代表一个物理磁盘或虚拟磁盘)。diskstats_show每次被调用时,v参数就指向一个gendisk。 - 内层循环(分区): 在
diskstats_show内部,xa_for_each(&gp->part_tbl, idx, hd)遍历当前gendisk(gp) 的分区表 (part_tbl,一个 XArray)。这使得它可以依次处理磁盘本身及其所有分区(在内核中,磁盘本身也被看作一个特殊的block_device,即分区0)。 - RCU 安全遍历: 整个分区表的遍历过程被
rcu_read_lock()保护。这允许在不阻塞分区热插拔等操作的情况下,安全地进行只读遍历。
- 外层循环(磁盘): 由
实时统计数据更新:
- 数据结构: 每个
block_device结构体中都嵌入了一个disk_stats结构,用于原子地累积各项 I/O 统计值。 inflightI/O 更新:bdev_count_inflight(hd)获取当前正在处理、尚未完成的 I/O 请求数。如果存在inflight请求,代码会调用update_io_ticks。这是一个重要的实时更新步骤,它将从上一次更新到当前时刻 (jiffies) 的时间差,累加到设备的io_ticks计数器中。这反映了设备处于“繁忙”(即有 I/O 在进行)的总时长。- 原子读取:
part_stat_read_all(hd, &stat)用于原子地读取指定设备的所有统计数据到一个临时的stat变量中,避免了在读取多个字段时发生数据不一致的竞态条件。
- 数据结构: 每个
格式化输出:
- 函数使用
seq_put_decimal_ull*和seq_printf等seq_file提供的函数,将获取到的统计数据格式化为十进制文本,并输出到/proc/diskstats文件中。 - 输出的字段包括主/次设备号、设备名、读/写/丢弃/刷新 I/O 的次数、合并次数、扇区数、总耗时(纳秒转换为毫秒)等,为上层监控工具(如
iostat)提供了丰富的数据源。
- 函数使用
代码分析
1 |
|
块设备在途 I/O 计数: bdev_count_inflight 与 bdev_count_inflight_rw
本代码片段提供了用于获取一个块设备上“在途”(inflight)I/O 请求数量的函数。其核心功能是提供一个快照,反映出有多少 I/O 请求已经被提交给块设备层进行处理,但尚未收到完成通知。它通过 bdev_count_inflight_rw 这个核心函数,采用一种无锁(lockless)的、基于 per-CPU 计数器求和的方式来实现,并为现代的多队列(Multi-Queue, MQ)驱动和传统的单队列驱动提供了不同的处理路径。
实现原理分析
此机制是内核 I/O 统计子系统(如 /proc/diskstats)获取设备繁忙程度数据的关键。其实现原理的核心在于如何在不引入性能瓶颈(如全局锁)的前提下,尽可能准确地统计一个高度并发的计数值。
Per-CPU 计数器:
- 对于传统的非 MQ 驱动,
bdev_count_inflight_rw的核心是for_each_possible_cpu循环。内核为每个 CPU 核心都维护了一套独立的in_flight计数器(一个用于读,一个用于写)。 - 无锁性能: 当一个 I/O 请求在某个 CPU 上启动时,它只会原子地增加该 CPU 本地的
in_flight计数器。同样,当 I/O 完成时,它也只在处理完成中断的那个 CPU 上减少本地计数。这种设计完全避免了使用全局锁,因为不同 CPU 之间不会争抢同一个计数器,极大地提升了 I/O 路径的性能。
- 对于传统的非 MQ 驱动,
非精确快照与并发问题:
bdev_count_inflight_rw的目标是获取一个“快照”,但由于其无锁设计,这个快照不是原子性的,因此可能存在微小的误差。- 负数问题: 注释中精确地描述了这种非原子性可能导致的问题。假设一个系统有两个 CPU (0 和 1)。
for_each_possible_cpu循环先读取了 CPU 0 的计数器。就在此时,一个之前在 CPU 0 上启动的 I/O 请求完成了,而其完成中断恰好在 CPU 1 上被处理。CPU 1 上的完成逻辑会减少 CPU 1 的本地计数器。如果 CPU 1 此前没有启动任何 I/O,它的计数器就可能变为负数。当循环进行到 CPU 1 时,就会读到一个负值,导致最终的总和不准确。 - 解决方案(近似值): 代码并没有尝试解决这个固有的竞态条件(因为完美解决需要昂贵的同步),而是接受了这个近似值,并做了一个简单的修正:
inflight[READ] = read > 0 ? read : 0;。它将任何最终为负数的合计值“钳位”(clamp)到 0。这确保了函数至少不会返回一个无意义的负数,虽然它承认了结果只是一个在特定时间点上的“最佳估计”。
多队列(MQ)驱动路径:
if (mq_driver)分支显示了代码对现代块设备驱动框架的适应性。多队列驱动有其自己的一套更复杂的 I/O 提交和计数机制。因此,该函数简单地将任务委托给blk_mq_in_driver_rw,由blk-mq子系统自身来提供在途 I/O 的数量。
代码分析
1 | /** |
gendisk 对象与请求队列的分配与初始化 (__blk_alloc_disk, __alloc_disk_node)
核心功能
这两个函数是Linux内核块设备子系统的核心工厂函数,负责创建一个完整的、可用的块设备实例。__blk_alloc_disk 是上层接口,它首先为新设备创建一个请求队列(request_queue),然后调用 __alloc_disk_node 来分配并初始化 gendisk 结构体。__alloc_disk_node 是实际的“构造函数”,它不仅分配内存,还将 gendisk 与请求队列关联起来,并初始化该设备所需的各种子系统资源,如BIO内存池、分区表、设备模型接口(sysfs)、I/O控制器(cgroup)等,最终返回一个代表了新块设备的、几乎可用的 gendisk 对象。
实现原理分析
责任分离设计: 这两个函数的结构体现了清晰的责任分离。
__blk_alloc_disk负责处理与 I/O 调度和请求处理直接相关的request_queue的创建。而__alloc_disk_node则专注于gendisk这个更高层抽象的创建,它代表了磁盘的物理属性和逻辑结构(如分区)。这种设计是合乎逻辑的:一个磁盘(gendisk)拥有一个请求队列(request_queue),而不是相反。资源聚合中心 (
__alloc_disk_node):gendisk结构体是块设备在内核中的核心抽象,__alloc_disk_node函数是聚合所有相关资源的中心。它执行了一系列精密的初始化步骤:bioset_init: 为这个设备初始化一个私有的bio_set。这是一个内存池,专门用于分配bio结构体,特别是当上层需要将一个大的bio请求拆分成多个符合设备硬件限制的小bio时,就需要从这个池中分配新的bio来承载拆分后的请求。bdi_alloc: 分配一个backing_dev_info结构。这个结构是内核页缓存回写(writeback)机制的一部分,用于跟踪与该块设备相关的脏页信息和回写策略。bdev_alloc: 这是一个至关重要的步骤。它创建了一个block_device结构体,代表了分区0,即整个磁盘设备本身。在Linux中,即使是未分区的磁盘,也被视为一个占据整个设备的特殊分区(分区0)。xa_init/xa_insert: 初始化一个xarray作为该磁盘的分区表,并将刚刚创建的分区0插入其中。后续添加分区时,都会向这个xarray中添加新的block_device条目。device_initialize: 调用设备模型的核心函数,将gendisk内嵌的struct device初始化。这一步是gendisk能够集成到sysfs(出现在/sys/block/目录下)并参与内核设备生命周期管理的前提。
健壮的错误处理:
__alloc_disk_node中使用了goto标签链来进行错误处理。这是一个在C语言内核编程中非常标准和高效的模式。当任何一步资源分配或初始化失败时,程序会跳转到相应的标签处,并依次执行后续的清理步骤,确保所有之前已成功分配的资源都被按相反的顺序干净地释放掉,从而避免任何资源泄漏。
源码及逐行注释
1 | /** |










