[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 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);

磁盘 Sysfs 属性定义:disk_attrs, disk_visible 与 disk_attr_groups

本代码片段是 Linux 内核设备模型中一个声明式的定义,其核心功能是为所有通用磁盘设备(gendisk)构建它们在 sysfs 文件系统(通常挂载于 /sys)中的属性文件接口。它通过一系列静态定义的数组和结构体,精确地描述了哪些文件应该被创建(disk_attrs),这些文件是否应该对特定设备可见(disk_visible),以及如何根据内核配置模块化地添加更多的属性组(disk_attr_groups)。

实现原理分析

此代码是 sysfs 工作原理的教科书式范例。它本身不包含复杂的执行逻辑,而是为 sysfs核心框架提供了一份“蓝图”或“元数据”,sysfs 核心会依据这份蓝图来动态地创建和管理文件。

  1. 属性定义 (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数) 等。
  2. 动态可见性 (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 这个文件,使得接口更加干净和准确。
  3. 模块化扩展 (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
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
54
55
56
57
58
59
60
61
62
63
64
65
66
// 定义一个属性指针数组,列出了所有磁盘设备通用的 sysfs 属性。
static struct attribute *disk_attrs[] = {
&dev_attr_range.attr, // 分区数量范围
&dev_attr_ext_range.attr, // 扩展分区数量范围
&dev_attr_removable.attr, // 设备是否可移除
&dev_attr_hidden.attr, // 设备是否为隐藏设备
&dev_attr_ro.attr, // 设备是否为只读
&dev_attr_size.attr, // 设备大小 (以512字节扇区计)
&dev_attr_alignment_offset.attr, // 对齐偏移量
&dev_attr_discard_alignment.attr,// 丢弃对齐
&dev_attr_capability.attr, // 设备能力掩码
&dev_attr_stat.attr, // I/O 统计信息 (与 diskstats 格式类似)
&dev_attr_inflight.attr, // 在途 I/O 数量
&dev_attr_badblocks.attr, // 坏块管理接口
&dev_attr_events.attr, // 媒体事件标志
&dev_attr_events_async.attr, // 异步媒体事件标志
&dev_attr_events_poll_msecs.attr,// 媒体事件轮询间隔
&dev_attr_diskseq.attr, // 磁盘序列号
&dev_attr_partscan.attr, // 强制分区扫描
#ifdef CONFIG_FAIL_MAKE_REQUEST
&dev_attr_fail.attr, // 故障注入:通用失败
#endif
#ifdef CONFIG_FAIL_IO_TIMEOUT
&dev_attr_fail_timeout.attr, // 故障注入:I/O 超时
#endif
NULL // 数组结束标记
};

/**
* @brief disk_visible - 决定一个特定的 sysfs 属性对一个特定的磁盘是否可见。
* @param kobj: 指向设备的 kobject。
* @param a: 正在被检查的 attribute。
* @param n: 未使用。
* @return umode_t: 如果可见,则返回文件的权限模式;如果不可见,则返回 0。
*/
static umode_t disk_visible(struct kobject *kobj, struct attribute *a, int n)
{
// 从 kobject 反向推导出 device 指针。
struct device *dev = container_of(kobj, typeof(*dev), kobj);
// 从 device 指针反向推导出 gendisk 指针。
struct gendisk *disk = dev_to_disk(dev);

// 特殊逻辑:如果属性是 'badblocks',并且该磁盘没有配置坏块支持...
if (a == &dev_attr_badblocks.attr && !disk->bb)
return 0; // ...则返回模式0,使该文件不可见。
// 对于所有其他属性,返回其预定义的默认权限模式。
return a->mode;
}

// 定义一个属性组,将属性数组和可见性回调函数绑定在一起。
static struct attribute_group disk_attr_group = {
.attrs = disk_attrs, // 包含的属性列表
.is_visible = disk_visible, // 属性可见性判断函数
};

// 定义一个属性组指针数组,用于模块化地组合多个属性组。
static const struct attribute_group *disk_attr_groups[] = {
&disk_attr_group, // 基础属性组
#ifdef CONFIG_BLK_DEV_IO_TRACE
&blk_trace_attr_group, // I/O 追踪属性组 (条件编译)
#endif
#ifdef CONFIG_BLK_DEV_INTEGRITY
&blk_integrity_attr_group, // 数据完整性属性组 (条件编译)
#endif
NULL // 数组结束标记
};

Gendisk 资源释放:disk_release

本代码片段展示了 Linux 内核中通用磁盘(gendisk)对象的“析构函数”——disk_release。其核心功能是在一个 gendisk 对象的生命周期结束时(即其最后一个引用被释放时),以严格的、反向依赖的顺序,系统地拆除和释放该磁盘所占用的所有内核资源。这包括 I/O 追踪、内存池、分区表、请求队列的引用以及驱动自定义资源等,最终释放 gendisk 结构本身占用的内存。

实现原理分析

disk_release 是内核资源管理中“对称性原则”的典范,它精确地“撤销”了磁盘初始化时所进行的所有资源分配和注册操作。其设计的关键在于严格的、分阶段的、有条件的资源释放顺序

  1. 触发机制(由设备模型驱动):

    • 此函数被注册为 disk_type.release 回调。当一个代表磁盘的 struct device 的最后一个引用被 put_device() 释放时,设备模型核心会自动调用 disk_release。这保证了只有在内核中没有任何地方再使用这个设备对象时,清理工作才会开始。
  2. 严格的逆序清理:

    • 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. 接下来是关键的解耦步骤:断开 gendiskrequest_queue 之间的双向链接,并释放 gendiskrequest_queue 的引用 (blk_put_queue)。此时,如果 gendiskrequest_queue 的唯一持有者,blk_put_queue 将会触发请求队列自身的释放。
      d. 之后,调用驱动程序提供的可选回调 fops->free_disk,允许驱动释放其私有的、与 gendisk 关联的资源。
      e. 最后,也是最重要的一步,调用 bdev_drop(disk->part0)part0 代表了整个磁盘的 block_device 结构,释放对它的引用最终会触发 gendisk 结构体本身被 kfree
  3. 状态依赖的健壮性:

    • 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
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
54
55
56
57
58
59
60
/**
* @brief disk_release - 释放 gendisk 的所有已分配资源。
* @param dev: 代表此磁盘的 device 结构体。
* @note 此函数是 device_type->release 的回调,在设备的最后一个引用被释放时调用。
* 它可以睡眠。
*/
static void disk_release(struct device *dev)
{
// 将通用的 device 指针转换为具体的 gendisk 指针。
struct gendisk *disk = dev_to_disk(dev);

// 告知锁分析器,此函数可能会睡眠。
might_sleep();
// 这是一个健全性检查:当此函数被调用时,磁盘不应该再处于“活动”状态(即被打开)。
WARN_ON_ONCE(disk_live(disk));

// 移除附加在此磁盘请求队列上的 I/O 追踪。
blk_trace_remove(disk->queue);

/*
* 为了撤销 blk_mq_init_allocated_queue 的所有初始化,
* 在探测失败且 add_disk 从未被调用的情况下,我们必须在此处调用 blk_mq_exit_queue。
*/
// 这是一个处理初始化失败路径的特殊情况。
if (queue_is_mq(disk->queue) && // 如果队列是多队列模式
test_bit(GD_OWNS_QUEUE, &disk->state) && // 并且 gendisk “拥有”这个队列
!test_bit(GD_ADDED, &disk->state)) // 并且磁盘从未被成功“添加”到系统中
blk_mq_exit_queue(disk->queue); // 则调用特殊的队列退出函数。

// 清理与此磁盘相关的块设备 cgroup 资源。
blkcg_exit_disk(disk);

// 销毁用于 bio 拆分的 bioset 内存池。
bioset_exit(&disk->bio_split);

// 释放与磁盘媒体事件相关的资源。
disk_release_events(disk);
// 释放用于随机数生成的资源 (如果已分配)。
kfree(disk->random);
// 释放 system zone 相关的资源。
disk_free_zone_resources(disk);
// 销毁用于存储分区表的 XArray。
xa_destroy(&disk->part_tbl);

// 释放对此磁盘队列的 kobject 的引用 (用于 sysfs)。
kobject_put(&disk->queue_kobj);
// 断开从请求队列到此磁盘的反向指针。
disk->queue->disk = NULL;
// 释放此磁盘对请求队列的引用。如果这是最后一个引用,将触发队列的释放。
blk_put_queue(disk->queue);

// 如果磁盘曾被成功添加到系统,并且驱动定义了 free_disk 回调...
if (test_bit(GD_ADDED, &disk->state) && disk->fops->free_disk)
// ...则调用驱动的自定义清理函数。
disk->fops->free_disk(disk);

// 释放代表整个磁盘的分区0 (part0) 的 block_device 结构。
// 这一步最终会触发 gendisk 结构体本身的 kfree。
bdev_drop(disk->part0);
}

磁盘设备类型定义与设备节点创建:disk_type 与 block_devnode

本代码片段定义了 Linux 内核中所有通用磁盘(gendisk)的标准 device_type,即 disk_type。其核心功能是为内核设备模型提供一个统一的接口集合,用于处理所有磁盘类设备的通用操作,例如 sysfs 属性的创建、设备释放时的清理,以及最重要的——决定该设备在 /dev 目录下的设备节点(device node)应如何创建block_devnode 函数正是这个设备节点创建策略的默认实现。

实现原理分析

此机制是内核设备模型与 devtmpfs(或 uevent 守护进程)协作,自动在 /dev 目录下创建设备文件的关键环节。

  1. 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: 这是最重要的回调。当 devtmpfsueventd 需要为这个新出现的设备创建设备文件时,它们会调用此函数来获取设备文件名和权限。
  2. block_devnode 的分发(Dispatcher)逻辑:

    • block_devnode 本身并不直接决定设备名。它扮演着一个分发者或策略选择器的角色。
    • 它首先将通用的 struct device 指针转换回具体的 struct gendisk 指针。
    • 然后,它检查该 gendisk 关联的文件操作集(disk->fops)中,是否定义了一个更具体的 devnode 回调。
    • 场景一(驱动自定义): 某些特殊的块设备驱动(例如,一个虚拟磁盘驱动)可能希望对其设备有特殊的命名规则或权限设置。这样的驱动会在其 block_device_operations 结构中提供自己的 devnode 函数。在这种情况下,block_devnode 就会调用这个驱动专属的函数,并将结果返回。
    • 场景二(使用默认): 对于绝大多数标准块设备驱动(如 sd_mod for SATA/SCSI, mmc_block for SD/eMMC),它们不会提供自己的 devnode 函数。在这种情况下,disk->fops->devnodeNULL,于是 block_devnode 也返回 NULL
    • NULL 的含义: 返回 NULL 是一个明确的信号,它告诉 devtmpfs:“请使用默认的命名规则”。对于块设备,这个默认规则就是使用 gendisk->disk_name 字段中存储的名字(如 “sda”, “mmcblk0”, “mmcblk0p1”)来创建设备节点。

代码分析

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
/**
* @brief block_devnode - 为块设备确定其在/dev下的设备节点名和属性。
* @param dev: 指向设备的通用 device 结构体指针。
* @param mode: 指向 umode_t 的指针,用于传出设备文件的权限模式。
* @param uid: 指向 kuid_t 的指针,用于传出设备文件的所有者ID。
* @param gid: 指向 kgid_t 的指针,用于传出设备文件的所属组ID。
* @return char*: 成功则返回一个由 kmalloc 分配的设备名字符串,
* 如果希望使用默认名称,则返回 NULL。
* @note 这是一个通用的 devnode 回调,它会尝试调用更具体的驱动回调。
*/
static char *block_devnode(const struct device *dev, umode_t *mode,
kuid_t *uid, kgid_t *gid)
{
// 将通用的 device 指针转换为 gendisk 指针。
struct gendisk *disk = dev_to_disk(dev);

// 检查此 gendisk 关联的文件操作集(fops)中是否定义了它自己的 devnode 函数。
if (disk->fops->devnode)
// 如果定义了,则调用驱动提供的特定 devnode 函数,并返回其结果。
return disk->fops->devnode(disk, mode);
// 如果驱动未提供特定的 devnode 函数,则返回 NULL,
// 告知上层调用者(如 devtmpfs)使用默认的设备命名方案。
return NULL;
}

/**
* @var disk_type
* @brief 所有通用磁盘 (gendisk) 设备的标准 device_type。
*
* 这个结构体为设备模型提供了处理所有磁盘类设备的通用回调函数。
*/
const struct device_type disk_type = {
.name = "disk", // 设备类型的名称。
.groups = disk_attr_groups, // 指向一个属性组数组,用于在 sysfs 中创建文件。
.release = disk_release, // 当设备最后引用被释放时调用的清理函数。
.devnode = block_devnode, // 创建 /dev 节点时调用的回调函数。
};

块设备迭代器与 /proc/partitions 实现:disk_seqf_start, show_partition 等

本代码片段展示了 Linux 内核中 /proc/partitions 虚拟文件的完整实现。其核心功能是提供一个标准的、可遍历的迭代器,用于安全地枚举系统中所有的通用磁盘设备(gendisk),并利用这个迭代器来生成 /proc/partitions 文件的内容,向用户空间展示所有磁盘及其分区的布局信息,包括主/次设备号、大小和名称。

实现原理分析

此代码是 procfsseq_file 机制与内核设备模型(Device Model)相结合的典范。它将复杂的、需要加锁的设备链表遍历过程,封装在一组标准的迭代器接口 (start, next, stop) 之后,从而为上层提供了简洁、高效、安全的数据生成方式。

  1. 设备类迭代器 (class_dev_iter):

    • 遍历内核设备不是一个简单的链表遍历。设备可以被动态添加和删除,需要正确的锁来保护。class_dev_iter 是内核设备模型提供的标准工具,专门用于安全地迭代一个特定“类”(class)下的所有设备。
    • disk_seqf_start 函数通过 class_dev_iter_init(iter, &block_class, ...) 来初始化一个迭代器,使其专门遍历所有注册到 block_class(块设备类)的设备。这确保了它能找到系统中所有的磁盘。
  2. seq_file 迭代模型与状态保持:

    • start: disk_seqf_start 不仅初始化迭代器,还处理 *pos(位置偏移量)。当用户空间程序多次调用 read() 来读取一个大文件时,*pos 会记录当前已经读取到的条目数。start 函数通过一个 do-while 循环来跳过 (skip) 前 *pos 个设备,从而实现从上一次读取结束的位置继续生成内容。它将迭代器状态 (iter) 存储在 seqf->private 中,以便 nextstop 函数可以访问。
    • next: disk_seqf_next 的职责非常简单:调用 class_dev_iter_next 从迭代器中获取下一个设备,并递增 *pos
    • stop: disk_seqf_stop 是清理函数。它必须调用 class_dev_iter_exit 来释放迭代器持有的锁和资源,并 kfree 掉在 start 中分配的内存。代码中的 if (iter) 检查是一个健壮性设计,因为 stop 即使在 start 失败返回 NULLERR_PTR 的情况下也可能被调用。
  3. /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
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#ifdef CONFIG_PROC_FS
/* 迭代器函数 */

/**
* @brief disk_seqf_start - seq_file 迭代器的开始函数。
* @param seqf: 指向 seq_file 结构体的指针。
* @param pos: 指向当前文件位置偏移量的指针。
* @return void*: 成功则返回指向第一个要处理的 gendisk 对象的指针,
* 失败返回 ERR_PTR,遍历结束返回 NULL。
*/
static void *disk_seqf_start(struct seq_file *seqf, loff_t *pos)
{
loff_t skip = *pos; /// < 需要跳过的条目数。
struct class_dev_iter *iter; /// < 设备类迭代器。
struct device *dev; /// < 通用设备指针。

// 为迭代器状态分配内存。
iter = kmalloc(sizeof(*iter), GFP_KERNEL);
if (!iter)
return ERR_PTR(-ENOMEM);

// 将迭代器状态存储在 seq_file 的私有数据中。
seqf->private = iter;
// 初始化迭代器,使其遍历 block_class(块设备类)下的 disk_type 设备。
class_dev_iter_init(iter, &block_class, NULL, &disk_type);
do {
// 获取下一个设备。
dev = class_dev_iter_next(iter);
// 如果没有更多设备,则返回 NULL 表示遍历结束。
if (!dev)
return NULL;
} while (skip--); // 循环跳过 pos 指定数量的设备。

// 将 device 指针转换为 gendisk 指针并返回。
return dev_to_disk(dev);
}

/**
* @brief disk_seqf_next - seq_file 迭代器的 "移动到下一个" 函数。
* @param seqf: 指向 seq_file 结构体的指针。
* @param v: 指向当前 gendisk 对象的指针。
* @param pos: 指向当前文件位置偏移量的指针。
* @return void*: 成功则返回下一个 gendisk 对象,遍历结束返回 NULL。
*/
static void *disk_seqf_next(struct seq_file *seqf, void *v, loff_t *pos)
{
struct device *dev;

// 位置加一。
(*pos)++;
// 从存储在 private 成员中的迭代器获取下一个设备。
dev = class_dev_iter_next(seqf->private);
if (dev)
return dev_to_disk(dev);

return NULL;
}

/**
* @brief disk_seqf_stop - seq_file 迭代器的停止/清理函数。
* @param seqf: 指向 seq_file 结构体的指针。
* @param v: 指向最后一个处理的 gendisk 对象的指针 (可能为 NULL)。
*/
static void disk_seqf_stop(struct seq_file *seqf, void *v)
{
struct class_dev_iter *iter = seqf->private;

// stop 即使在 start 失败后也会被调用,所以需要检查 iter 是否有效。
if (iter) {
// 退出/清理设备类迭代器,释放其持有的锁等资源。
class_dev_iter_exit(iter);
// 释放为迭代器分配的内存。
kfree(iter);
// 清理私有数据指针,防止悬挂。
seqf->private = NULL;
}
}

/**
* @brief show_partition_start - /proc/partitions 的 start 函数。
* @note 这是一个包装函数,它调用通用的 disk_seqf_start,并额外打印表头。
*/
static void *show_partition_start(struct seq_file *seqf, loff_t *pos)
{
void *p;

// 调用通用的磁盘迭代器 start 函数。
p = disk_seqf_start(seqf, pos);
// 如果 start 成功,并且是文件的开头 (pos 为 0),则打印表头。
if (!IS_ERR_OR_NULL(p) && !*pos)
seq_puts(seqf, "major minor #blocks name\n\n");
return p;
}

/**
* @brief show_partition - /proc/partitions 的 show 函数。
* @param seqf: 指向 seq_file 结构体的指针。
* @param v: 指向当前 gendisk 对象的指针。
* @return int: 始终返回0。
*/
static int show_partition(struct seq_file *seqf, void *v)
{
struct gendisk *sgp = v; /// < 当前处理的通用磁盘对象。
struct block_device *part; /// < 用于遍历分区的块设备指针。
unsigned long idx; /// < XArray 遍历用的索引。

// 如果磁盘没有容量,或者是被标记为隐藏的磁盘,则不显示。
if (!get_capacity(sgp) || (sgp->flags & GENHD_FL_HIDDEN))
return 0;

// 进入 RCU 读侧临界区,以安全地遍历分区表。
rcu_read_lock();
// 遍历当前磁盘的分区表。
xa_for_each(&sgp->part_tbl, idx, part) {
// 跳过大小为0的分区。
if (!bdev_nr_sectors(part))
continue;
// 格式化输出:主设备号、次设备号、块数(扇区数/2)、分区名。
seq_printf(seqf, "%4d %7d %10llu %pg\n",
MAJOR(part->bd_dev), MINOR(part->bd_dev),
bdev_nr_sectors(part) >> 1, part);
}
rcu_read_unlock();
return 0;
}

// 定义 /proc/partitions 的 seq_operations 结构体。
static const struct seq_operations partitions_op = {
.start = show_partition_start, // 绑定 start 函数
.next = disk_seqf_next, // 绑定 next 函数
.stop = disk_seqf_stop, // 绑定 stop 函数
.show = show_partition // 绑定 show 函数
};
#endif

/proc/diskstats 接口实现:diskstats_show 与 proc_genhd_init

本代码片段展示了 Linux 内核中 /proc/diskstats 虚拟文件的实现。其核心功能是为用户空间提供一个统一的、聚合的接口,用于获取系统中所有磁盘及其分区的详细 I/O 统计信息。它通过 procfsseq_file 机制来实现,当用户读取该文件时,diskstats_show 函数被调用,该函数会遍历系统中的所有磁盘和分区,并格式化输出每个设备的 I/O 统计数据,如读写次数、合并次数、读写扇区数以及 I/O 处理时间等。

实现原理分析

此代码是内核通过 procfs 暴露性能监控数据的经典范例,其实现依赖于块设备层的统计子系统和 seq_file 迭代器接口。

  1. seq_file 迭代器模型:

    • /proc/diskstats 的内容可能会很长,seq_file 是内核提供的标准机制,用于高效、安全地生成这类可能跨越多个内存页的大文件内容。
    • 它不是一次性生成所有内容,而是采用迭代器模式。diskstats_op 结构定义了一组操作函数:
      a. .start: disk_seqf_start 被首先调用,用于初始化遍历,通常是锁定资源并找到第一个要显示的条目(第一个 gendisk)。
      b. .next: disk_seqf_next 在每次 show 调用后被调用,用于找到下一个要显示的条目。
      c. .show: diskstats_show 是核心函数,负责将当前条目(由 startnext 提供,通过 v 参数传入)格式化输出。
      d. .stop: disk_seqf_stop 在遍历结束或中断时被调用,用于释放资源(如解锁)。
  2. 数据源与遍历 (diskstats_show):

    • 外层循环(磁盘): 由 seq_filestart/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() 保护。这允许在不阻塞分区热插拔等操作的情况下,安全地进行只读遍历。
  3. 实时统计数据更新:

    • 数据结构: 每个 block_device 结构体中都嵌入了一个 disk_stats 结构,用于原子地累积各项 I/O 统计值。
    • inflight I/O 更新: bdev_count_inflight(hd) 获取当前正在处理、尚未完成的 I/O 请求数。如果存在 inflight 请求,代码会调用 update_io_ticks。这是一个重要的实时更新步骤,它将从上一次更新到当前时刻 (jiffies) 的时间差,累加到设备的 io_ticks 计数器中。这反映了设备处于“繁忙”(即有 I/O 在进行)的总时长。
    • 原子读取: part_stat_read_all(hd, &stat) 用于原子地读取指定设备的所有统计数据到一个临时的 stat 变量中,避免了在读取多个字段时发生数据不一致的竞态条件。
  4. 格式化输出:

    • 函数使用 seq_put_decimal_ull*seq_printfseq_file 提供的函数,将获取到的统计数据格式化为十进制文本,并输出到 /proc/diskstats 文件中。
    • 输出的字段包括主/次设备号、设备名、读/写/丢弃/刷新 I/O 的次数、合并次数、扇区数、总耗时(纳秒转换为毫秒)等,为上层监控工具(如 iostat)提供了丰富的数据源。

代码分析

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#ifdef CONFIG_PROC_FS
/**
* @brief diskstats_show - 为 /proc/diskstats 生成单行输出的函数。
* @param seqf: 指向 seq_file 结构体的指针,用于输出。
* @param v: 一个迭代器指针,指向当前的 gendisk 结构体。
* @return int: 始终返回0。
*/
static int diskstats_show(struct seq_file *seqf, void *v)
{
struct gendisk *gp = v; /// < 从迭代器获取当前处理的通用磁盘对象。
struct block_device *hd; /// < 用于遍历分区表的块设备指针。
unsigned int inflight; /// < 当前正在处理的 I/O 请求数。
struct disk_stats stat; /// < 用于存储原子读取的统计数据的临时变量。
unsigned long idx; /// < XArray 遍历用的索引。

// 进入 RCU 读侧临界区,以安全地遍历分区表。
rcu_read_lock();
// 遍历通用磁盘 gp 的分区表 (一个 XArray),对每个分区执行操作。
xa_for_each(&gp->part_tbl, idx, hd) {
// 如果是分区且大小为0,则跳过。
if (bdev_is_partition(hd) && !bdev_nr_sectors(hd))
continue;

// 获取当前正在进行的 I/O 数量。
inflight = bdev_count_inflight(hd);
// 如果有正在进行的 I/O...
if (inflight) {
// ...则获取分区统计锁,并更新 I/O 繁忙时间。
part_stat_lock();
update_io_ticks(hd, jiffies, true);
part_stat_unlock();
}
// 原子地读取该设备的所有 I/O 统计数据。
part_stat_read_all(hd, &stat);
// 以下为格式化输出部分
seq_put_decimal_ull_width(seqf, "", MAJOR(hd->bd_dev), 4); // 主设备号
seq_put_decimal_ull_width(seqf, " ", MINOR(hd->bd_dev), 7); // 次设备号
seq_printf(seqf, " %pg", hd); // 设备名
seq_put_decimal_ull(seqf, " ", stat.ios[STAT_READ]); // 读I/O次数
seq_put_decimal_ull(seqf, " ", stat.merges[STAT_READ]); // 读合并次数
seq_put_decimal_ull(seqf, " ", stat.sectors[STAT_READ]); // 读扇区数
seq_put_decimal_ull(seqf, " ", (unsigned int)div_u64(stat.nsecs[STAT_READ], NSEC_PER_MSEC)); // 读耗时(ms)
seq_put_decimal_ull(seqf, " ", stat.ios[STAT_WRITE]); // 写I/O次数
seq_put_decimal_ull(seqf, " ", stat.merges[STAT_WRITE]); // 写合并次数
seq_put_decimal_ull(seqf, " ", stat.sectors[STAT_WRITE]); // 写扇区数
seq_put_decimal_ull(seqf, " ", (unsigned int)div_u64(stat.nsecs[STAT_WRITE], NSEC_PER_MSEC)); // 写耗时(ms)
seq_put_decimal_ull(seqf, " ", inflight); // 当前in-flight的I/O数
seq_put_decimal_ull(seqf, " ", jiffies_to_msecs(stat.io_ticks)); // 总I/O繁忙时间(ms)
seq_put_decimal_ull(seqf, " ", (unsigned int)div_u64(stat.nsecs[STAT_READ] + stat.nsecs[STAT_WRITE] + stat.nsecs[STAT_DISCARD] + stat.nsecs[STAT_FLUSH], NSEC_PER_MSEC)); // I/O总加权耗时(ms)
seq_put_decimal_ull(seqf, " ", stat.ios[STAT_DISCARD]); // 丢弃I/O次数
seq_put_decimal_ull(seqf, " ", stat.merges[STAT_DISCARD]); // 丢弃合并次数
seq_put_decimal_ull(seqf, " ", stat.sectors[STAT_DISCARD]);// 丢弃扇区数
seq_put_decimal_ull(seqf, " ", (unsigned int)div_u64(stat.nsecs[STAT_DISCARD], NSEC_PER_MSEC)); // 丢弃耗时(ms)
seq_put_decimal_ull(seqf, " ", stat.ios[STAT_FLUSH]); // 刷新I/O次数
seq_put_decimal_ull(seqf, " ", (unsigned int)div_u64(stat.nsecs[STAT_FLUSH], NSEC_PER_MSEC)); // 刷新耗时(ms)
seq_putc(seqf, '\n');
}
// 退出 RCU 读侧临界区。
rcu_read_unlock();

return 0;
}

// 定义 seq_file 操作集,将迭代器函数与 show 函数绑定。
static const struct seq_operations diskstats_op = {
.start = disk_seqf_start, // 开始遍历函数
.next = disk_seqf_next, // 移动到下一个元素函数
.stop = disk_seqf_stop, // 停止遍历函数
.show = diskstats_show // 显示当前元素函数
};

/**
* @brief proc_genhd_init - 初始化 /proc/diskstats 和 /proc/partitions 文件。
* @return int: 始终返回0。
*/
static int __init proc_genhd_init(void)
{
// 创建 /proc/diskstats 文件,使用 diskstats_op 操作集。
proc_create_seq("diskstats", 0, NULL, &diskstats_op);
// 创建 /proc/partitions 文件,使用 partitions_op 操作集。
proc_create_seq("partitions", 0, NULL, &partitions_op);
return 0;
}
// 将初始化函数注册为内核模块初始化入口。
module_init(proc_genhd_init);
#endif /* CONFIG_PROC_FS */

块设备在途 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)获取设备繁忙程度数据的关键。其实现原理的核心在于如何在不引入性能瓶颈(如全局锁)的前提下,尽可能准确地统计一个高度并发的计数值。

  1. 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 路径的性能。
  2. 非精确快照与并发问题:

    • 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。这确保了函数至少不会返回一个无意义的负数,虽然它承认了结果只是一个在特定时间点上的“最佳估计”。
  3. 多队列(MQ)驱动路径:

    • if (mq_driver) 分支显示了代码对现代块设备驱动框架的适应性。多队列驱动有其自己的一套更复杂的 I/O 提交和计数机制。因此,该函数简单地将任务委托给 blk_mq_in_driver_rw,由 blk-mq 子系统自身来提供在途 I/O 的数量。

代码分析

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
54
55
56
/**
* @brief bdev_count_inflight_rw - 分别计算读和写的在途 I/O 数量。
* @param part: 目标块设备。
* @param inflight: 一个包含两个元素的无符号整型数组,用于存储结果 (inflight[0] for READ, inflight[1] for WRITE)。
* @param mq_driver: 布尔值,如果为 true,表示这是一个多队列驱动。
*/
static void bdev_count_inflight_rw(struct block_device *part,
unsigned int inflight[2], bool mq_driver)
{
int write = 0; /// < 写的在途 I/O 临时累加器。
int read = 0; /// < 读的在途 I/O 临时累加器。
int cpu;

// 如果是多队列(Multi-Queue)驱动...
if (mq_driver) {
// ...则调用 blk-mq 子系统提供的专用函数来获取数量。
blk_mq_in_driver_rw(part, inflight);
return;
}

// 对于传统的非 MQ 驱动,遍历所有可能的 CPU。
for_each_possible_cpu(cpu) {
// 累加每个 CPU 上的读在途 I/O 计数。
read += part_stat_local_read_cpu(part, in_flight[READ], cpu);
// 累加每个 CPU 上的写在途 I/O 计数。
write += part_stat_local_read_cpu(part, in_flight[WRITE], cpu);
}

/*
* 在遍历所有 CPU 期间,某些 I/O 可能在一个已经遍历过的 CPU 上发出,
* 然后在一个尚未遍历的 CPU 上完成,这可能导致在途数量变为负数。
*/
// 将最终结果存入输出数组,如果结果为负则钳位到0。
inflight[READ] = read > 0 ? read : 0;
inflight[WRITE] = write > 0 ? write : 0;
}

/**
* @brief bdev_count_inflight - 获取一个块设备上总的在途 I/O 数量。
* @param part: 目标块设备。
* @return unsigned int: 总的在途 I/O 数量(读 + 写)。
* @note "在途"指的是已经开始 I/O 计数,但尚未完成的请求。
*/
unsigned int bdev_count_inflight(struct block_device *part)
{
unsigned int inflight[2] = {0}; /// < 初始化结果数组为0。

// 调用核心函数来填充 inflight 数组,假定为非 MQ 驱动路径。
// (注意: 更通用的代码可能会先检查设备类型)
bdev_count_inflight_rw(part, inflight, false);

// 返回读和写的在途 I/O 数量之和。
return inflight[READ] + inflight[WRITE];
}
// 将此函数导出,以便其他 GPL 许可的内核模块(如特定文件系统)可以使用。
EXPORT_SYMBOL_GPL(bdev_count_inflight);

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 对象。

实现原理分析

  1. 责任分离设计: 这两个函数的结构体现了清晰的责任分离。__blk_alloc_disk 负责处理与 I/O 调度和请求处理直接相关的 request_queue 的创建。而 __alloc_disk_node 则专注于 gendisk 这个更高层抽象的创建,它代表了磁盘的物理属性和逻辑结构(如分区)。这种设计是合乎逻辑的:一个磁盘(gendisk)拥有一个请求队列(request_queue),而不是相反。

  2. 资源聚合中心 (__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/ 目录下)并参与内核设备生命周期管理的前提。
  3. 健壮的错误处理: __alloc_disk_node 中使用了 goto 标签链来进行错误处理。这是一个在C语言内核编程中非常标准和高效的模式。当任何一步资源分配或初始化失败时,程序会跳转到相应的标签处,并依次执行后续的清理步骤,确保所有之前已成功分配的资源都被按相反的顺序干净地释放掉,从而避免任何资源泄漏。

源码及逐行注释

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
* @brief 分配并初始化一个 gendisk 结构及其关联的核心资源。
* @param[in] q 指向与此磁盘关联的请求队列。
* @param[in] node_id NUMA 节点 ID。
* @param[in] lkclass 用于 lockdep 的锁类密钥。
* @return 成功则返回指向新分配的 gendisk 的指针,失败则返回 NULL。
*/
struct gendisk *__alloc_disk_node(struct request_queue *q, int node_id,
struct lock_class_key *lkclass)
{
struct gendisk *disk;

// 在指定的 NUMA 节点上为 gendisk 结构体分配内存并清零。
disk = kzalloc_node(sizeof(struct gendisk), GFP_KERNEL, node_id);
if (!disk)
return NULL;

// 初始化一个 bio 内存池,用于 bio 的拆分。
if (bioset_init(&disk->bio_split, BIO_POOL_SIZE, 0, 0))
goto out_free_disk;

// 分配一个 backing_dev_info 结构,用于页缓存回写管理。
disk->bdi = bdi_alloc(node_id);
if (!disk->bdi)
goto out_free_bioset;

// 将 gendisk 与请求队列关联起来。
disk->queue = q;

// 为分区0(代表整个磁盘)分配一个 block_device 结构。
disk->part0 = bdev_alloc(disk, 0);
if (!disk->part0)
goto out_free_bdi;

disk->node_id = node_id; //!< 保存 NUMA 节点 ID。
mutex_init(&disk->open_mutex); //!< 初始化用于设备打开操作的互斥锁。
xa_init(&disk->part_tbl); //!< 初始化用于管理分区表的 xarray。
// 将分区0插入到分区表中。
if (xa_insert(&disk->part_tbl, 0, disk->part0, GFP_KERNEL))
goto out_destroy_part_tbl;

// 初始化磁盘的块 I/O 控制组(cgroup)支持。
if (blkcg_init_disk(disk))
goto out_erase_part0;

disk_init_zone_resources(disk); //!< 为分区式块设备初始化资源(如果支持)。
rand_initialize_disk(disk); //!< 初始化用于随机数生成的熵源。
disk_to_dev(disk)->class = &block_class; //!< 将设备的类设置为 "block"。
disk_to_dev(disk)->type = &disk_type; //!< 设置设备的类型。
device_initialize(disk_to_dev(disk)); //!< 初始化内嵌的 device 结构,集成到设备模型。
inc_diskseq(disk); //!< 增加磁盘序列号,用于事件通知。
q->disk = disk; //!< 在请求队列中反向指向此 gendisk。
lockdep_init_map(&disk->lockdep_map, "(bio completion)", lkclass, 0); //!< 初始化锁依赖验证器的映射。

mutex_init(&disk->rqos_state_mutex); //!< 初始化用于 I/O 服务质量(QoS)的互斥锁。
kobject_init(&disk->queue_kobj, &blk_queue_ktype); //!< 初始化队列的 kobject。
return disk; //!< 成功,返回磁盘对象。

// --- 错误处理回滚路径 ---
out_erase_part0:
xa_erase(&disk->part_tbl, 0);
out_destroy_part_tbl:
xa_destroy(&disk->part_tbl);
disk->part0->bd_disk = NULL;
bdev_drop(disk->part0);
out_free_bdi:
bdi_put(disk->bdi);
out_free_bioset:
bioset_exit(&disk->bio_split);
out_free_disk:
kfree(disk);
return NULL;
}

/**
* @brief 分配一个请求队列和一个 gendisk,并将它们关联起来。
* @param[in] lim 指向队列限制的指针,若为NULL则使用默认值。
* @param[in] node NUMA 节点 ID。
* @param[in] lkclass 用于 lockdep 的锁类密钥。
* @return 成功则返回指向新分配的 gendisk 的指针,失败则返回错误指针。
*/
struct gendisk *__blk_alloc_disk(struct queue_limits *lim, int node,
struct lock_class_key *lkclass)
{
struct queue_limits default_lim = { }; //!< 定义一个默认的、空的队列限制。
struct request_queue *q;
struct gendisk *disk;

// 分配一个请求队列。
q = blk_alloc_queue(lim ? lim : &default_lim, node);
if (IS_ERR(q))
return ERR_CAST(q);

// 使用新创建的队列来分配和初始化 gendisk。
disk = __alloc_disk_node(q, node, lkclass);
if (!disk) {
blk_put_queue(q); //!< 如果 gendisk 分配失败,释放已分配的队列。
return ERR_PTR(-ENOMEM);
}
// 设置一个状态位,表示此 gendisk “拥有”其请求队列,
// 在释放 gendisk 时需要一并释放队列。
set_bit(GD_OWNS_QUEUE, &disk->state);
return disk;
}
EXPORT_SYMBOL(__blk_alloc_disk); //!< 导出符号,供其他内核模块使用。