[toc]

在这里插入图片描述

/drivers/block/brd.c: Linux 内核 RAM 块设备驱动

介绍

/drivers/block/brd.c 是 Linux 内核中的一个驱动程序,它实现了一个基于 RAM 的块设备,通常被称为 RAM disk。这个驱动程序会分配一块系统内存,并将其模拟成一个标准的块设备(如 /dev/ram0),使其可以像硬盘或 U 盘一样被格式化、挂载文件系统和进行读写操作。由于所有操作都在内存中完成,其 I/O 速度极快,但存储的数据是易失的,会在系统重启后丢失。

历史与背景

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

brd.c 的诞生主要是为了解决在系统引导早期阶段访问文件系统以及提供一个高速临时存储的需求。

  • 引导过程中的根文件系统:在内核启动初期,可能需要加载特定的驱动模块(例如磁盘控制器或网络驱动)才能访问真正的根文件系统。initrd (initial ramdisk) 机制应运而生,它将一个临时的、包含必要工具和驱动的小型文件系统加载到 RAM disk 中。内核启动后,首先挂载这个 RAM disk 作为临时根目录,执行其中的脚本加载所需模块,然后再切换到真正的根文件系统。brd.c 是实现 initrd 的关键技术之一。
  • 统一和简化实现:在 brd.c 成为标准实现之前,存在一个较老的驱动 rd.cbrd.c 借鉴了 rd.cloop.c 的设计,提供了一个更现代化、更简洁的实现方式,并整合到 Linux 块设备驱动框架中。

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

brd.c 的发展相对稳定,其演进主要体现在与内核块设备子系统的集成和优化上。

  • 早期实现 (rd.c):最初的 RAM disk 功能由 rd.c 提供,功能相对简单。
  • brd.c 的引入brd.c 在 2007 年左右被引入,作为一个更清晰、更通用的 RAM disk 驱动实现。它使用了现代内核的 API,如 request_queuegendisk
  • 动态化和模块化brd.c 被设计为可加载模块,允许系统管理员在运行时通过 modprobe 命令动态地创建和配置 RAM disk,而无需重新编译内核。可以指定 RAM disk 的数量 (rd_nr) 和大小 (rd_size)。
  • 数据结构优化:在其内部实现中,brd.c 使用了基数树(radix tree)来管理和存储构成 RAM disk 内容的内存页面,这是一种高效管理稀疏大空间的方式。

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

brd.c 是一个非常成熟和稳定的内核模块,其核心代码变动不大。社区的活跃度主要体现在维护其与新内核版本的兼容性上。

它的主流应用依然是作为 initrdinitramfs 的基础,并且在某些特定的测试和开发场景中被广泛使用,例如:

  • I/O 性能基准测试:由于其极高的读写速度,常被用来测试文件系统或应用的 I/O 性能上限,排除物理磁盘的瓶颈。
  • 教学与实验:对于学习 Linux 内核块设备驱动的开发者来说,brd.c 是一个代码结构清晰、功能明确的绝佳范例。

核心原理与设计

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

brd.c 的核心原理是创建一个虚拟的块设备,并将所有对该设备的读写请求重定向到一块预先分配或动态增长的系统内存上。

  1. 设备创建:当加载 brd 模块时,它会在内核中注册一个新的块设备驱动。用户可以通过模块参数指定创建的设备数量和大小。每个设备(如 /dev/ram0, /dev/ram1)都会获得一个主设备号和唯一的次设备号。
  2. 内存管理brd.c 使用基数树 (radix_tree) 来管理 backing store(后备存储)的内存页。当有写操作发生时,如果对应的内存页尚未分配,驱动会从系统中申请一个新的内存页,并将其插入到基数树中。这种方式实现了内存的按需分配,只有在数据被写入时才会实际占用内存。
  3. I/O 请求处理:当上层(如文件系统)发起一个 I/O 请求(封装在 bio 结构体中)时,块设备层会将其传递给 brd 驱动的请求处理函数。该函数不会将请求发送给任何物理硬件,而是直接在内存中完成操作:
    • 写操作:将 bio 中携带的数据拷贝到基数树中对应的内存页。
    • 读操作:从基数树中找到对应的内存页,并将数据拷贝到 bio 指定的缓冲区。

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

  • 极高的性能:所有 I/O 操作都在内存中进行,速度远超任何物理存储设备,只受限于内存和 CPU 的速度。
  • 模拟真实块设备:它表现为一个标准的块设备,支持分区、创建文件系统、LVM 等所有适用于块设备的操作,这使其在测试中非常有用。
  • 简单易用:通过 modprobe 加载模块并指定参数即可轻松创建和配置,无需复杂的设置。

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

  • 数据易失性:存储在 brd.c 创建的 RAM disk 上的所有数据在系统断电或重启后会全部丢失。
  • 占用系统内存:RAM disk 使用的内存是从系统可用内存中划分出来的,会减少其他应用程序可用的内存量。
  • 无压缩brd.c 直接存储原始数据,不会对数据进行压缩,对于可压缩的数据来说,空间利用率不如 zram
  • 无法跨节点共享:它完全是本地节点的资源,无法在分布式系统或集群中作为共享存储使用。

使用场景

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

  • 系统引导:作为 initrd 的后端,在系统启动早期加载必要的驱动程序,这是其最经典和核心的用途。
  • 高速临时文件存储:对于需要大量读写临时文件且对性能要求极高的应用(如某些编译任务、数据库测试),可以将临时目录挂载到 RAM disk 上以获得巨大加速。
  • 安全敏感数据的处理:处理临时性的敏感数据(如解密的密钥、证书文件等),利用其易失性,确保数据在重启后被彻底清除。
  • 块设备相关功能的测试:测试文件系统、RAID 配置、LVM 或加密软件(如 dm-crypt)时,使用 RAM disk 可以快速创建和销毁测试用的块设备,而无需操作物理硬盘。

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

  • 持久化数据存储:绝对不能用于存储需要长期保存的关键数据,因为数据会在重启后丢失。
  • 内存紧张的系统:在内存资源本就不足的系统上使用大型 RAM disk 会严重影响系统性能,甚至导致系统因内存不足而崩溃。
  • 替代缓存:不应将其用作应用层缓存的替代品。Linux 内核自身的文件系统缓存(page cache)已经非常高效,对于大多数场景,直接让内核管理缓存是更好的选择。

对比分析

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

特性 brd.c (RAM disk) tmpfs / ramfs zram
实现方式 块设备驱动。在内存中模拟一个完整的块设备,需要格式化文件系统后才能挂载。 虚拟文件系统。直接在内核页缓存中创建一个文件系统实例,无需底层块设备,直接挂载即可使用。 压缩的块设备驱动。与 brd.c 类似,但在写入内存前会对数据进行压缩。
性能开销 极低。直接内存拷贝,无压缩开销。 极低。与 brd.c 性能相似,绕过了块设备层,理论上更直接。 较低。读写时需要额外的 CPU 周期进行解压/压缩,因此 I/O 延迟略高于 brd.ctmpfs
资源占用 直接占用物理内存,大小固定或按需增长,但不会被交换到磁盘。 动态调整大小,只占用实际使用的内存。tmpfs 在内存压力下其内容可以被交换到 swap 分区。 占用物理内存,但由于压缩,实际占用的内存通常远小于其报告的容量。
隔离级别 块设备级别。可以对其进行分区,或用于裸设备访问。 文件系统级别。表现为一个挂载点,无法进行块级别的操作。 块设备级别。可以作为 swap 设备或格式化后用作通用存储。
启动速度 需要 mkfs 格式化,比 tmpfs 略慢。 极快,mount 命令即可立即使用。 需要配置和创建设备,然后根据用途决定是否格式化或 mkswap
主要用途 initrd、块设备功能测试、需要块设备接口的高速临时存储。 /tmp、共享内存 (/dev/shm)、无需块设备接口的临时文件存储。 内存受限设备上的 swap 空间、压缩的临时文件存储。

总结

请为我总结其关键特性,并提供学习该技术的要点建议。

关键特性总结:

  • 块设备模拟brd.c 在内存中创建一个功能完整的块设备,这是其与 tmpfs 最本质的区别。
  • 高性能:基于 RAM 的特性使其读写速度极快。
  • 易失性:所有数据在系统重启后都会丢失。
  • 模块化与可配置性:可以通过内核模块参数灵活地创建多个不同大小的 RAM disk。

学习该技术的要点建议:

  1. 动手实践:学习 brd.c 最好的方式是亲手操作。尝试使用 modprobe brd rd_nr=... rd_size=... 命令加载模块,然后用 mkfs 格式化 /dev/ram0,最后 mount 它并进行文件操作。
  2. 理解块设备层:要深入理解 brd.c,需要对 Linux 的块设备层有一个基本的认识,了解 gendiskrequest_queuebio 等核心数据结构的作用。
  3. 阅读源码brd.c 的源码相对简短且逻辑清晰,是学习 Linux 设备驱动程序开发的优秀范本。通过阅读源码,可以理解 I/O 请求是如何在没有物理硬件的情况下被处理的。
  4. 对比学习:将 brd.ctmpfszram 进行对比,理解它们各自的设计哲学和适用场景。这能帮助你更深刻地理解 Linux 中不同内存使用技术的权衡。例如,思考“为什么 /tmp 通常使用 tmpfs 而不是 RAM disk?”这样的问题。

核心数据结构与按需页面管理 (brd_lookup_page, brd_insert_page, brd_free_pages)

核心功能

该部分代码是 RAM 盘(brd)驱动的基石,负责管理作为块设备后备存储的物理内存页。它定义了核心数据结构 brd_device,其中 xarray 数据结构是实现“稀疏”存储的关键。brd_lookup_page 用于在 xarray 中无锁、安全地查找与给定扇区对应的物理页。brd_insert_page 则在需要写入一个尚未分配的块时,动态地分配一个新的物理页并插入到 xarray 中。brd_free_pages 在设备卸载时,负责遍历 xarray 并释放所有已分配的物理页。

实现原理分析

  1. 稀疏存储与 xarray: brd 驱动的一个核心特性是“按需分配”(on-demand allocation)或称为“精简配置”(thin provisioning)。一个 1GB 的 RAM 盘在刚创建时并不会立即占用 1GB 内存。只有当数据被首次写入某个块时,内核才会为该块分配一个物理页。这种机制是通过 struct xarray brd_pages 实现的。xarray 是一个高度优化的、支持多线程安全访问的基数树(radix tree),非常适合用于管理稀疏的、以整数(这里是页帧号)为索引的数据集合。它仅为实际存在的条目(物理页指针)分配内部节点,从而极大地节省了元数据存储开销。

  2. 无锁查找与 RCU: brd_lookup_page 函数展示了在Linux内核中进行高性能、无锁数据查找的典型范式。它使用 RCU(Read-Copy-Update)来保护对 xarray 的并发读取。

    • rcu_read_lock(): 建立一个RCU读端临界区。在此区域内,可以保证被 RCU 保护的数据结构(xarray的节点)不会被释放。
    • xas_load(): 从 xarray 中加载指针。
    • xas_retry(): 检查在加载过程中,xarray 是否发生了修改。如果返回 true,则意味着查找操作与一个写操作(插入/删除)发生了竞争,当前结果可能无效,需要重试。
    • get_page_unless_zero(): 这是一个关键的原子操作,用于安全地增加物理页的引用计数。它解决了 lookup -> get_page 的经典竞态问题:在查找到一个页但在增加其引用计数之前,另一个线程可能已经释放了这个页(引用计数变为0)。此函数能原子地检查引用计数是否为0,如果不为0则加一并返回 true,否则返回 false
    • xas_reload(): 再次加载指针,并与之前加载的进行比较。这是为了确认在我们成功增加页引用计数之后,该 xarray 条目没有被其他写者修改成指向另一个不同的页。如果变了,我们必须放弃当前获取的页 (put_page) 并重试整个过程。

特定场景分析 (单核、无MMU的 STM32H750)

  1. 并发与锁: 在单核处理器上,由多核并行执行导致的竞态条件不复存在。然而,锁和 RCU 机制依然至关重要。

    • RCU: 在单核、可抢占的内核中,RCU 临界区 (rcu_read_lock/unlock) 的主要作用是禁止内核抢占。这确保了在读端临界区内,代码的执行不会被其他任务中断,从而保证了 xas_loadxas_reload 之间数据的一致性视图。如果内核是不可抢占的,RCU 锁的开销会变得非常小。
    • xa_lock: 在 brd_insert_page 中使用的 xa_lock (自旋锁) 依然是必需的。它可以防止一个进程在执行插入操作的过程中被中断,而中断服务程序(ISR)恰好也尝试访问同一个 RAM 盘,从而破坏 xarray 结构。在单核上,自旋锁的主要作用是禁止中断(或禁止抢占,取决于具体实现),而不是在多核间进行总线同步。
  2. 内存分配与 HIGHMEM: brd_insert_page 中使用了 __GFP_HIGHMEM 标志。在没有MMU的系统架构上(如uClinux for Cortex-M7),“高地址内存”(HIGHMEM)的概念是不存在的。内存是一个单一、平坦的物理地址空间,所有内存对内核来说都是直接可访问的“低地址内存”(LOWMEM)。因此,__GFP_HIGHMEM 标志会被内核的内存分配器忽略。页分配器(如SLOB或SLAB)会从可用的物理内存区域中分配一个标准大小的物理页。

  3. 页操作:

    • struct page: 在无MMU系统中,struct page 依然是管理物理内存的基本单元,它跟踪每个物理页的引用计数、状态等信息。
    • get_page/put_page: 这些函数的功能不变,它们原子地增减 struct page 中的引用计数,用于控制物理页的生命周期。brd 驱动通过这种方式正确地管理着作为其后备存储的所有物理页的生命周期。

源码及逐行注释

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
/**
* @struct brd_device
* @brief 每个RAM盘设备的核心数据结构。
*/
struct brd_device {
int brd_number; //!< RAM盘的设备编号 (例如, 0 表示 /dev/ram0)。
struct gendisk *brd_disk; //!< 指向内核通用磁盘对象的指针, 代表了块设备。
struct list_head brd_list; //!< 用于将所有brd_device实例链接在一起的链表节点。

/**
* @var brd_pages
* @brief 作为后备存储的物理页。这是块设备内容的实际存放位置。
* @note 使用 xarray 数据结构,可以实现按需分配(稀疏存储)。
*/
struct xarray brd_pages;
u64 brd_nr_pages; //!< 已分配的物理页的总数,用于统计。
};

/**
* @brief 查找并返回与给定扇区对应的物理页,并增加其引用计数。
* @param[in] brd 指向 brd_device 实例的指针。
* @param[in] sector 逻辑块设备中的扇区号。
* @return 成功时返回指向 struct page 的指针,失败或未找到则返回 NULL。
*/
static struct page *brd_lookup_page(struct brd_device *brd, sector_t sector)
{
struct page *page; //!< 用于存储查找到的物理页指针。
// 初始化一个 xarray 状态机(xas),用于遍历 brd_pages。索引是页号。
XA_STATE(xas, &brd->brd_pages, sector >> PAGE_SECTORS_SHIFT);

rcu_read_lock(); //!< 进入 RCU 读端临界区,保护对 xarray 的无锁读取。
repeat:
page = xas_load(&xas); //!< 从 xarray 中加载与当前索引对应的页指针。
if (xas_retry(&xas, page)) { //!< 检查加载期间 xarray 是否被修改,若是则重试。
xas_reset(&xas); //!< 重置状态机以从头开始。
goto repeat; //!< 跳转到 repeat 重新加载。
}

if (!page) //!< 如果加载的指针为空,说明该块尚未分配物理页。
goto out; //!< 直接跳转到出口。

if (!get_page_unless_zero(page)) { //!< 原子地尝试增加页的引用计数。
xas_reset(&xas); //!< 如果失败(意味着页正在被释放),则重试整个过程。
goto repeat;
}

// 重新加载指针并与之前获取的进行比较,确保在我们增加引用计数期间,
// 该条目没有被修改成指向一个不同的页。
if (unlikely(page != xas_reload(&xas))) {
put_page(page); //!< 如果变了,释放我们刚才增加的引用计数。
xas_reset(&xas); //!< 重置状态机。
goto repeat; //!< 并重试整个查找过程。
}
out:
rcu_read_unlock(); //!< 退出 RCU 读端临界区。

return page; //!< 返回最终获取到的页指针,或 NULL。
}

/**
* @brief 为给定扇区插入一个新的物理页,前提是该位置尚未有关联的页。
* @param[in] brd 指向 brd_device 实例的指针。
* @param[in] sector 逻辑块设备中的扇区号。
* @param[in] opf bio 请求的操作标志 (用于判断是否允许等待)。
* @return 成功时返回指向新分配或已存在的页的指针 (已增加引用计数),失败时返回错误指针。
*/
static struct page *brd_insert_page(struct brd_device *brd, sector_t sector,
blk_opf_t opf)
{
// 根据请求是否为NOWAIT,来决定内存分配时是否可以休眠。
gfp_t gfp = (opf & REQ_NOWAIT) ? GFP_NOWAIT : GFP_NOIO;
struct page *page, *ret; //!< page是新分配的页, ret是xarray操作的返回值。

// 分配一个物理页,并用零填充。__GFP_HIGHMEM在无MMU系统上会被忽略。
page = alloc_page(gfp | __GFP_ZERO | __GFP_HIGHMEM);
if (!page) //!< 如果分配失败。
return ERR_PTR(-ENOMEM); //!< 返回内存不足错误。

xa_lock(&brd->brd_pages); //!< 锁定 xarray 以进行修改。
// 原子地比较并交换:如果索引处的条目为NULL,则将其设为page,否则返回现有条目。
ret = __xa_cmpxchg(&brd->brd_pages, sector >> PAGE_SECTORS_SHIFT, NULL,
page, gfp);
if (!ret) { //!< 如果返回 NULL,说明交换成功,我们插入了新页。
brd->brd_nr_pages++; //!< 已分配的页计数加一。
get_page(page); //!< 为返回给调用者的指针增加一次引用计数。
xa_unlock(&brd->brd_pages); //!< 解锁 xarray。
return page; //!< 返回新插入的页。
}

if (!xa_is_err(ret)) { //!< 如果返回的是一个有效的页指针 (非错误),说明已有页存在。
get_page(ret); //!< 为这个已存在的页增加引用计数。
xa_unlock(&brd->brd_pages); //!< 解锁 xarray。
put_page(page); //!< 释放我们之前新分配但未使用的页。
return ret; //!< 返回已存在的页。
}

xa_unlock(&brd->brd_pages); //!< 操作出错,解锁 xarray。
put_page(page); //!< 释放我们新分配的页。
return ERR_PTR(xa_err(ret)); //!< 返回转换后的错误码。
}

/**
* @brief 释放所有后备存储的物理页和 xarray 自身。
* @note 此函数必须在没有其他用户访问该设备时调用。
* @param[in] brd 指向 brd_device 实例的指针。
*/
static void brd_free_pages(struct brd_device *brd)
{
struct page *page; //!< 用于遍历时存储页指针。
pgoff_t idx; //!< 用于遍历时存储索引 (页号)。

// 遍历 xarray 中的每一个有效条目。
xa_for_each(&brd->brd_pages, idx, page) {
put_page(page); //!< 减少页的引用计数,当计数为0时页将被释放。
cond_resched(); //!< 在长时间循环中,允许调度器重新调度,避免系统无响应。
}

xa_destroy(&brd->brd_pages); //!< 销毁 xarray 结构本身,释放其元数据占用的内存。
}

BIO 请求处理与数据传输 (brd_submit_bio, brd_rw_bvec, brd_do_discard)

核心功能

这组函数是 brd 驱动的 I/O 引擎。brd_submit_bio 是块设备层调用 brd 驱动处理 I/O 请求的唯一入口点。它首先判断 BIO 请求的类型。如果是 DISCARD 请求,它会调用 brd_do_discard 来释放相应的物理页,实现 TRIM/UNMAP 功能;如果是常规的读写请求,它会循环调用 brd_rw_bvec 来处理 BIO 中的每一个数据段(bio_vec)。brd_rw_bvec 是实际执行数据拷贝的函数,它负责查找或创建目标扇区对应的物理页,然后将用户数据从 BIO 的页面拷贝到 RAM 盘的物理页(写操作),或反之(读操作)。

实现原理分析

  1. BIO 迭代模型: Linux 内核的块 I/O 使用 BIO 结构来描述一个 I/O 请求。一个 BIO 可以包含多个段(bio_vec),这些段可以指向物理上不连续的内存页。brd_submit_bio 中的 do-while 循环和 brd_rw_bvec 中对 bio->bi_iter 的使用,共同构成了一个标准的 BIO 迭代处理模式。brd_rw_bvec 每次只处理 BIO 中的一个 bio_vec,处理完后通过 bio_advance_iter_single 更新迭代器 bi_iter,使其指向下一个待处理的数据位置。外层循环 while (bio->bi_iter.bi_size) 检查是否还有未处理的数据,从而确保整个 BIO 被完整处理。

  2. 跨页边界处理: brd_rw_bvec 函数内部通过 bv.bv_len = min_t(u32, bv.bv_len, PAGE_SIZE - offset); 这行代码,精巧地处理了跨页边界的 I/O 请求。它确保了单次 memcpy 操作绝对不会跨越 RAM 盘后备存储中的物理页边界。offset 计算出请求在目标页内的起始字节偏移。PAGE_SIZE - offset 则是从这个偏移到页末尾的剩余空间。通过取 bio_vec 长度和这个剩余空间的最小值,brd_rw_bvec 保证了本次数据传输只在单个目标物理页内进行。如果一个 bio_vec 本身跨越了两个 RAM 盘的页,那么它会被分两次处理,这由外层的 BIO 迭代循环来驱动。

  3. 直接内存映射 (kmap_local): 为了访问 BIO 向量(bio_vec)中可能位于高地址内存(HIGHMEM)的物理页,brd_rw_bvec 使用了 bvec_kmap_local。这个函数会为 bio_vec 所指向的物理页创建一个临时的内核虚拟地址映射,使得内核可以通过一个普通的指针 kaddr 来访问这块内存。操作完成后,kunmap_local 会撤销这个映射。这是一个在块设备驱动中处理高地址内存页的标准方法。

  4. DISCARD 操作的实现: brd_do_discard 函数将 DISCARD 请求(TRIM/UNMAP)翻译为释放物理页的操作。它首先将请求的扇区范围对齐到 RAM 盘的页边界 (PAGE_SECTORS),因为 brd 的分配粒度是页。然后,它在一个循环中调用 __xa_erasexarray 中移除指向这些页的指针。移除成功后,它调用 put_page 来减少页的引用计数。当引用计数降为零时,这个物理页就会被返还给系统,从而实现了存储空间的“回收”。

特定场景分析 (单核、无MMU的 STM32H750)

  1. kmap 的行为: 在没有 MMU 的系统上,内核虚拟地址空间和物理地址空间是直接或简单偏移映射的。不存在“高地址内存”的概念,所有的物理内存页对于内核来说都是直接可访问的。在这种情况下,bvec_kmap_localkunmap_local 通常会退化为空操作(或者仅仅是简单的地址计算),它们的开销非常小。数据拷贝可以直接在物理地址间进行。

  2. I/O 调度与并发: brd 驱动是一个非常简单的驱动,它没有自己的请求队列,而是直接处理 submit_bio。在单核系统上,BIO 请求是串行提交给 brd_submit_bio 的。由于 brd 的所有操作(查找页、分配页、内存拷贝)都是在 submit_bio 的上下文中同步完成的,并且速度极快(内存到内存拷贝),因此不存在复杂的 I/O 调度问题。请求的处理是即时的,bio_endio(bio) 会在 submit_bio 函数返回前被调用,通知块设备层该 I/O 已完成。

  3. REQ_NOWAIT 的意义: 在 brd_rw_bvec 中,当写操作需要分配新页而 brd_insert_page 失败并返回 -ENOMEM 时,代码会检查 (opf & REQ_NOWAIT)。在内存紧张的嵌入式系统中,这个检查是有意义的。如果请求不允许等待(REQ_NOWAIT),例如来自中断上下文的请求,驱动会通过 bio_wouldblock_error(bio) 通知上层,表示操作如果继续可能会阻塞,应稍后重试。如果请求允许等待,驱动则通过 bio_io_error(bio) 简单地报告一个 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
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
/**
* @brief 处理单个 bio_vec 段的读写操作。
* @note 此函数保证单次操作不跨越RAM盘的页边界。
* @param[in] brd 指向 brd_device 实例的指针。
* @param[in,out] bio 指向当前正在处理的 bio 结构体的指针。
* @return 成功返回 true,失败返回 false。
*/
static bool brd_rw_bvec(struct brd_device *brd, struct bio *bio)
{
struct bio_vec bv = bio_iter_iovec(bio, bio->bi_iter); //!< 从 bio 迭代器中获取当前的 bio_vec 段。
sector_t sector = bio->bi_iter.bi_sector; //!< 获取当前段对应的起始扇区号。
// 计算起始扇区在本页内的字节偏移量。
u32 offset = (sector & (PAGE_SECTORS - 1)) << SECTOR_SHIFT;
blk_opf_t opf = bio->bi_opf; //!< 获取 bio 的操作标志 (读/写/NOWAIT等)。
struct page *page; //!< 指向RAM盘后备存储页的指针。
void *kaddr; //!< 用于访问 bio_vec 内存的内核虚拟地址。

// 限制本次传输的长度,确保它不会超过目标页的末尾。
bv.bv_len = min_t(u32, bv.bv_len, PAGE_SIZE - offset);

page = brd_lookup_page(brd, sector); //!< 查找该扇区是否已有关联的物理页。
if (!page && op_is_write(opf)) { //!< 如果是写操作,且页不存在。
page = brd_insert_page(brd, sector, opf); //!< 则按需分配一个新页。
if (IS_ERR(page)) //!< 如果分配失败。
goto out_error; //!< 跳转到错误处理。
}

kaddr = bvec_kmap_local(&bv); //!< 获取 bio_vec 指向内存的临时内核映射地址。
if (op_is_write(opf)) { //!< 如果是写操作。
// 将数据从 bio_vec 拷贝到 RAM 盘的物理页中。
memcpy_to_page(page, offset, kaddr, bv.bv_len);
} else { //!< 如果是读操作。
if (page) //!< 如果 RAM 盘的物理页存在。
// 将数据从 RAM 盘的物理页拷贝到 bio_vec 指向的内存中。
memcpy_from_page(kaddr, page, offset, bv.bv_len);
else
// 如果页不存在,说明是读一个未被写入过的区域,按规定返回全零。
memset(kaddr, 0, bv.bv_len);
}
kunmap_local(kaddr); //!< 撤销临时内核映射。

// 更新 bio 迭代器,将指针前移本次处理的字节数。
bio_advance_iter_single(bio, &bio->bi_iter, bv.bv_len);
if (page)
put_page(page); //!< 释放对RAM盘物理页的引用计数。
return true; //!< 返回成功。

out_error:
// 根据请求是否允许等待,来决定返回哪种错误。
if (PTR_ERR(page) == -ENOMEM && (opf & REQ_NOWAIT))
bio_wouldblock_error(bio); //!< 报告操作将阻塞。
else
bio_io_error(bio); //!< 报告通用 I/O 错误。
return false; //!< 返回失败。
}

/**
* @brief 处理 DISCARD 请求。
* @param[in] brd 指向 brd_device 实例的指针。
* @param[in] sector DISCARD 操作的起始扇区。
* @param[in] size DISCARD 操作的字节数。
*/
static void brd_do_discard(struct brd_device *brd, sector_t sector, u32 size)
{
// 将请求的起始扇区向上对齐到页边界。
sector_t aligned_sector = round_up(sector, PAGE_SECTORS);
// 将请求的结束扇区向下对齐到页边界。
sector_t aligned_end = round_down(
sector + (size >> SECTOR_SHIFT), PAGE_SECTORS);
struct page *page;

if (aligned_end <= aligned_sector) //!< 如果对齐后没有完整的页可供释放。
return; //!< 直接返回。

xa_lock(&brd->brd_pages); //!< 锁定 xarray 进行修改。
// 循环遍历所有需要释放的页。
while (aligned_sector < aligned_end && aligned_sector < rd_size * 2) {
// 从 xarray 中擦除该页的条目,并返回页指针。
page = __xa_erase(&brd->brd_pages, aligned_sector >> PAGE_SECTORS_SHIFT);
if (page) { //!< 如果成功擦除一个条目。
put_page(page); //!< 减少页的引用计数,使其能被系统回收。
brd->brd_nr_pages--; //!< 更新已分配页的计数器。
}
aligned_sector += PAGE_SECTORS; //!< 前进到下一页。
}
xa_unlock(&brd->brd_pages); //!< 解锁 xarray。
}

/**
* @brief bio 提交入口函数,由块设备层调用。
* @param[in,out] bio 指向要处理的 bio 结构体的指针。
*/
static void brd_submit_bio(struct bio *bio)
{
struct brd_device *brd = bio->bi_bdev->bd_disk->private_data; //!< 从 bio 中获取 brd_device 实例。

if (unlikely(op_is_discard(bio->bi_opf))) { //!< 检查是否是 DISCARD 请求。
brd_do_discard(brd, bio->bi_iter.bi_sector,
bio->bi_iter.bi_size); //!< 调用 discard 处理函数。
bio_endio(bio); //!< 通知块设备层 I/O 完成。
return;
}

do { //!< 循环处理 bio 中的所有段。
if (!brd_rw_bvec(brd, bio)) //!< 调用段处理函数。
return; //!< 如果失败,brd_rw_bvec 内部已报告错误,直接返回。
} while (bio->bi_iter.bi_size); //!< 循环直到 bio 中没有剩余数据。

bio_endio(bio); //!< 所有段都处理完毕,通知块设备层 I/O 完成。
}

/**
* @var brd_fops
* @brief 定义了 brd 驱动的块设备操作函数集。
*/
static const struct block_device_operations brd_fops = {
.owner = THIS_MODULE, //!< 模块所有者。
.submit_bio = brd_submit_bio, //!< 指定 bio 提交函数。
};

模块初始化与设备管理 (brd_init, brd_exit, brd_alloc)

核心功能

这组函数负责 brd 驱动的整体生命周期管理。brd_init 是模块加载时的入口点,它的核心任务是:

  1. 向内核注册一个主设备号为 RAMDISK_MAJOR (通常是 1) 的块设备驱动。
  2. 设置一个“探测”(probe)回调函数 brd_probe,用于按需创建设备实例。
  3. 根据模块参数 rd_nr,预先创建指定数量的 RAM 盘设备。

brd_alloc 是创建单个 RAM 盘设备实例的核心函数。它负责分配 brd_devicegendisk 结构体,初始化它们,设置块设备参数(如容量),并将 gendisk 对象添加到系统中,使其对用户可见(例如,作为 /dev/ram0)。

brd_exit 是模块卸载时的入口点,它调用 brd_cleanup 来执行与 brd_initbrd_alloc 相反的操作:遍历所有已创建的设备,从系统中移除 gendisk 对象,释放所有后备存储页,最后释放设备自身的数据结构。

实现原理分析

  1. 动态设备创建 (probe): brd 驱动实现了一个非常灵活的设备实例化模型。在 brd_init 中,它通过 __register_blkdev 注册驱动时,并没有创建任何设备,而是提供了一个 brd_probe 函数。当用户首次访问一个属于该主设备号但尚未被内核实例化的设备节点时(例如,通过 fdisk /dev/ramX),块设备层就会调用这个 brd_probe 回调。回调函数会解析设备号中的次设备号(minor number),并调用 brd_alloc 来动态地、按需地创建这个新的 RAM 盘实例。这种机制避免了在启动时就创建大量可能永远不会被使用的设备,节省了启动时间和内核内存。

  2. 通用磁盘层 (gendisk): Linux 内核通过 struct gendisk 来表示一个独立的块设备(一个“磁盘”)。brd_alloc 中对 blk_alloc_disk 的调用是创建块设备的关键步骤。这个函数分配并初始化一个 gendisk 对象。随后,驱动需要填充这个对象的各个字段:

    • major, first_minor, minors: 定义设备号。
    • fops: 指向驱动的 block_device_operations 结构体,将 I/O 请求(如 submit_bio)与驱动的实现关联起来。
    • private_data: 一个私有指针,通常指向驱动自己的设备特定数据结构(这里是 brd_device),这是将通用层与驱动私有数据连接起来的标准方法。
    • disk_name: 设备名(如 “ram0”)。
    • set_capacity: 设置磁盘的容量(以512字节扇区为单位)。
    • add_disk: 最后,调用此函数将 gendisk 注册到内核,使其对系统可见。
  3. 模块参数 (module_param): rd_nrrd_size 被声明为模块参数,这允许用户在加载模块时(例如,通过 insmod brd.ko rd_nr=4 rd_size=16384)或在内核启动命令行中指定 RAM 盘的数量和大小。这提供了极大的灵活性,无需重新编译内核即可调整驱动行为。0444 的权限意味着这些参数在加载后是只读的。

  4. 资源清理的安全性: brd_cleanup 中的 list_for_each_entry_safe 宏的使用是安全遍历并删除链表节点的标准做法。_safe 版本的宏会额外保存下一个节点的指针,因此即使当前节点 brd 在循环体内被删除和释放(通过 brd_free_device),循环也能安全地继续进行。del_gendisk 会确保在删除磁盘前,所有进行中的 I/O 都已完成,并阻止新的 I/O 被提交,保证了设备的安全移除。

特定场景分析 (单核、无MMU的 STM32H750)

  1. 设备创建: 在一个典型的嵌入式 STM32H750 系统中,rd_nr 通常会被配置为1或2,以便在启动时创建一个 RAM 盘用于临时文件系统(tmpfs)或其他用途。brd_init 将被调用,它会注册驱动并循环调用 brd_alloc(0), brd_alloc(1) 等来创建这些设备。动态探测功能虽然存在,但在静态配置的嵌入式系统中可能较少使用。

  2. 内存分配 (kzalloc): brd_alloc 中的 kzalloc 调用将在 STM32H750 的 RAM 中分配一小块内存用于 brd_device 结构体。由于这是内核内存,它会在系统的核心内存区域进行分配。

  3. 块设备层交互: 即使在没有真实磁盘的嵌入式系统上,完整的块设备层依然存在。文件系统(如 ext2, VFAT)可以被挂载到 /dev/ram0 上。当文件系统进行读写时,它会生成 BIO 请求,通过 VFS 和块设备层,最终调用到 brd_submit_bio。整个软件栈的功能是完整且一致的。

  4. set_capacity 的计算: set_capacity(disk, rd_size * 2) 这行代码设置了磁盘容量。rd_size 参数的单位是千字节(KB)。由于 set_capacity 的单位是512字节的扇区,所以 rd_size (KB) * 1024 / 512 = rd_size * 2。这个计算对于任何平台都是一样的。

源码及逐行注释

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/* ... (模块参数定义) ... */

/**
* @var brd_devices
* @brief 用于链接所有已创建的 brd_device 实例的全局链表头。
*/
static LIST_HEAD(brd_devices);

/**
* @var brd_devices_mutex
* @brief 用于保护对全局 brd_devices 链表并发访问的互斥锁。
*/
static DEFINE_MUTEX(brd_devices_mutex);

/* ... */

/**
* @brief 分配并初始化一个 brd 设备实例。
* @param[in] i 设备的编号。
* @return 成功返回0,失败返回错误码。
*/
static int brd_alloc(int i)
{
struct brd_device *brd;
struct gendisk *disk;
char buf[DISK_NAME_LEN];
int err = -ENOMEM;
// 定义块设备的限制和特性。
struct queue_limits lim = {
/* 设置物理块大小为 PAGE_SIZE,这有助于分区工具(如fdisk)进行4k对齐。*/
.physical_block_size = PAGE_SIZE,
.max_hw_discard_sectors = UINT_MAX, //!< 支持最大数量的 discard 扇区。
.max_discard_segments = 1, //!< 一次 discard 操作只支持一个段。
.discard_granularity = PAGE_SIZE,//!< discard 的粒度是页大小。
// BLK_FEAT_SYNCHRONOUS 表示 I/O 是同步完成的。
// BLK_FEAT_NOWAIT 表示支持 REQ_NOWAIT 标志。
.features = BLK_FEAT_SYNCHRONOUS |
BLK_FEAT_NOWAIT,
};

// 查找或分配 brd_device 结构体 (此处实现已被简化为总是分配)。
brd = brd_find_or_alloc_device(i);
if (IS_ERR(brd))
return PTR_ERR(brd);

xa_init(&brd->brd_pages); //!< 初始化用于存储物理页的 xarray。

/* ... (debugfs 相关代码) ... */

// 分配一个通用磁盘(gendisk)对象。
disk = brd->brd_disk = blk_alloc_disk(&lim, NUMA_NO_NODE);
if (IS_ERR(disk)) { //!< 如果分配失败。
err = PTR_ERR(disk);
goto out_free_dev;
}
disk->major = RAMDISK_MAJOR; //!< 设置主设备号。
disk->first_minor = i * max_part; //!< 设置第一个次设备号。
disk->minors = max_part; //!< 设置次设备号的数量 (用于分区)。
disk->fops = &brd_fops; //!< 关联块设备操作函数。
disk->private_data = brd; //!< 将 brd_device 设为私有数据。
snprintf(buf, DISK_NAME_LEN, "ram%d", i); //!< 创建设备名。
strscpy(disk->disk_name, buf, DISK_NAME_LEN); //!< 拷贝设备名到 gendisk。
// 设置容量。rd_size 单位是KB,set_capacity 单位是512B扇区,所以乘以2。
set_capacity(disk, rd_size * 2);

err = add_disk(disk); //!< 将此磁盘添加到系统中,使其对用户可见。
if (err)
goto out_cleanup_disk;

return 0; //!< 成功。

out_cleanup_disk:
put_disk(disk); //!< 清理 gendisk 对象。
out_free_dev:
brd_free_device(brd); //!< 释放 brd_device 结构体。
return err;
}

/**
* @brief 在访问设备节点时被块设备层调用的探测函数。
* @param[in] dev 被访问的设备号。
*/
static void brd_probe(dev_t dev)
{
// 根据次设备号计算出设备编号并创建设备实例。
brd_alloc(MINOR(dev) / max_part);
}

/**
* @brief 清理所有 brd 设备和资源。
*/
static void brd_cleanup(void)
{
struct brd_device *brd, *next;

/* ... (debugfs 清理) ... */

// 安全地遍历并删除链表中的每一个 brd 设备。
list_for_each_entry_safe(brd, next, &brd_devices, brd_list) {
del_gendisk(brd->brd_disk); //!< 从系统中移除 gendisk。
put_disk(brd->brd_disk); //!< 减少 gendisk 的引用计数。
brd_free_pages(brd); //!< 释放所有后备存储页。
brd_free_device(brd); //!< 释放 brd_device 结构体。
}
}

/* ... */

/**
* @brief 模块初始化函数。
* @return 成功返回0,失败返回错误码。
*/
static int __init brd_init(void)
{
int err, i;

/* ... (参数检查) ... */

/* ... (debugfs 创建) ... */

// 注册一个块设备驱动,主设备号为 RAMDISK_MAJOR,名称为 "ramdisk"。
// brd_probe 函数将在首次访问设备节点时被调用。
if (__register_blkdev(RAMDISK_MAJOR, "ramdisk", brd_probe)) {
err = -EIO;
goto out_free;
}

// 根据模块参数 rd_nr,预先创建指定数量的 RAM 盘。
for (i = 0; i < rd_nr; i++)
brd_alloc(i);

pr_info("brd: 模块已加载\n");
return 0;

out_free:
brd_cleanup(); //!< 注册失败时,执行清理。
pr_info("brd: 模块加载失败 !!!\n");
return err;
}

/**
* @brief 模块退出函数。
*/
static void __exit brd_exit(void)
{
// 注销块设备驱动。
unregister_blkdev(RAMDISK_MAJOR, "ramdisk");
brd_cleanup(); //!< 清理所有设备和资源。

pr_info("brd: 模块已卸载\n");
}

module_init(brd_init);
module_exit(brd_exit);