[TOC]

block/fops.c 块设备文件操作(Block Device File Operations) 用户空间直接访问块设备的桥梁

历史与背景

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

block/fops.c 的存在是为了实现UNIX哲学的核心思想之一:“一切皆文件”。它专门为了解决一个根本性需求:为用户空间提供一个标准的、基于文件接口的、直接与块设备(如硬盘、SSD)进行底层交互的方式

在文件系统被创建和挂载之前,操作系统需要一种方法来对原始的块设备本身进行操作。block/fops.c 实现了这个接口,使得以下基础任务成为可能:

  • 分区(Partitioning):工具如 fdisk, parted 需要能够读取和写入磁盘的第一个扇区来管理分区表。
  • 文件系统创建(Formatting):工具如 mkfs.ext4 需要能够直接向一个分区写入文件系统的元数据(超级块、inode表等)。
  • 裸数据传输(Raw Data Transfer):工具如 dd 需要能够进行块级别的、不经过文件系统解释的磁盘克隆或镜像创建。
  • 底层硬件控制(Hardware Control):工具如 hdparm, smartctl 需要通过 ioctl 系统调用发送特定的命令到底层硬件,以查询健康状况、设置参数等。

如果没有block/fops.c提供的这层“文件化”抽象,上述所有系统管理任务都将无法实现。

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

block/fops.c 作为内核的基础部分,其演进与Linux I/O子系统的发展紧密相连:

  • UNIX起源:其基本设计——通过设备特殊文件(如/dev/sda)来代表硬件,并为其提供一套file_operations——源自早期的UNIX系统。
  • O_DIRECT的引入(关键里程碑):这是一个重大的性能里程碑。O_DIRECT标志允许应用程序在读写块设备时绕过内核的页缓存(Page Cache)。这对于那些拥有自己高效缓存机制的应用程序(尤其是数据库)至关重要,因为它避免了“双重缓存”(应用缓存一次,内核再缓存一次)带来的内存浪费和性能开销。block/fops.c中的读写实现必须能够处理这个标志。
  • ioctl的扩展:随着存储技术的发展,block/fops.c中的ioctl处理函数(blkdev_ioctl)不断膨胀,加入了对新技术的支持,例如:获取设备大小、TRIM/DISCARD命令(用于SSD)、NVMe passthrough命令等。
  • I/O调度器接口:虽然fops.c不直接实现I/O调度,但它是将来自用户空间的I/O请求(struct bio)提交给I/O调度器的入口点。

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

block/fops.c 是Linux块层中最基础、最稳定的组件之一。它不经常经历颠覆性改变,但任何与块设备I/O相关的核心改进都可能触及它。它被所有需要进行底层存储管理的Linux工具所依赖,是系统管理员、存储工程师和数据库开发者日常工作中不可或缺的底层支撑。

核心原理与设计

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

block/fops.c 的核心是定义了一个名为 blkdev_fopsstruct file_operations 实例。当用户空间的程序 open() 一个块设备文件(如 /dev/sda1)时,VFS(虚拟文件系统)会识别出这是一个块特殊文件,并将其文件操作定向到 blkdev_fops

  1. 打开 (blkdev_open):当 open() 被调用时,blkdev_open 函数会执行。它的关键任务是找到对应的 struct block_device 内核对象,并将其附加到 struct fileprivate_data 字段上。这个关联是后续所有操作的基础。它还会处理独占打开(O_EXCL)等逻辑。
  2. 读/写 (blkdev_read_iter, blkdev_write_iter):当 read()write() 被调用时:
    • 函数会从 struct file 中获取关联的 struct block_device
    • 它将文件偏移量(offset)转换为设备上的逻辑扇区地址(sector address)。
    • 它创建一个或多个struct bio(Block I/O astructure)对象,这个对象描述了要进行的I/O操作(是读还是写、目标扇区、内存缓冲区、长度等)。
    • 最后,它调用 submit_bio()bio 提交给块层的通用处理逻辑,后者会将其传递给I/O调度器,并最终由设备驱动程序执行。
  3. 控制 (blkdev_ioctl):当 ioctl() 被调用时,blkdev_ioctl 函数会接管。它内部是一个巨大的 switch 语句,根据传入的命令码(cmd)来执行不同的操作。这些操作范围极广,从简单的“获取扇区大小”到复杂的“向NVMe设备发送原生命令”。

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

  • 统一接口:为所有不同类型的块设备(SATA硬盘、NVMe SSD、USB存储、RAID卷等)提供了统一的、标准的VFS文件接口。
  • 强大灵活:暴露了块层的全部底层能力,允许用户空间对设备进行完全的、低级别的控制。
  • 抽象:向用户隐藏了I/O调度、请求合并、驱动交互等复杂细节,用户只需关心“读/写/控制”这些高级概念。

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

  • 危险性:其最大的优势(直接访问)也是其最大的风险。绕过文件系统意味着没有任何安全网。一个错误的 dd 命令或 mkfs 命令可以瞬间永久性地销毁整个磁盘的数据,且无法恢复。
  • 性能考量(缓存):默认情况下,对块设备的读写会经过页缓存。这对于重复读取同一区域是有益的,但对于一次性的大量顺序读写(如磁盘备份),页缓存会成为不必要的开销。使用 O_DIRECT 可以解决这个问题,但也需要应用自己处理数据对齐等问题。
  • 非结构化:它提供的是对一个线性地址空间(扇区0到N)的访问,没有任何文件、目录或权限的结构化概念。

使用场景

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

block/fops.c 是所有需要绕过文件系统、直接与块存储硬件交互场景的唯一解决方案。

  • 初始化存储:在新硬盘上使用 fdisk 创建分区,然后使用 mkfs.ext4 在分区上创建文件系统。
  • 系统备份与恢复:使用 dd if=/dev/sda of=/path/to/backup.img 创建整个硬盘的精确镜像。
  • 数据库存储引擎:高性能数据库(如Oracle, MySQL/InnoDB)通常会选择使用裸设备或带有O_DIRECT标志的文件,通过block/fops.c提供的接口来直接管理I/O,以实现自己的缓存、预取和I/O调度策略。
  • 虚拟化:虚拟机监视器(QEMU/KVM)可以直接将宿主机的块设备(如LVM卷)作为虚拟机的虚拟硬盘,I/O请求会通过block/fops.c的路径进行处理。

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

任何常规的文件存储和访问都不应该使用该技术。普通用户和应用程序应该始终通过文件系统来操作文件。直接访问块设备会:

  • 破坏数据:极易意外覆盖文件系统元数据或属于其他文件的数据。
  • 效率低下:对于非连续、小文件的访问,文件系统的数据布局和缓存优化通常远胜于直接的块访问。
  • 丧失功能:无法利用文件系统提供的所有功能,如文件名、目录结构、权限控制、扩展属性等。

对比分析

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

block/fops.c (裸块设备访问) vs. 文件系统访问

特性 裸块设备访问 (/dev/sda) 文件系统访问 (/mnt/data/file)
抽象层次 。操作对象是线性的扇区/块地址空间。 。操作对象是文件目录,有名称和路径。
数据安全 。没有日志、CoW等保护机制。用户对所有块都有完全的写权限。 。文件系统提供日志、权限控制、空间管理等机制来保护数据完整性和安全性。
性能模型 对于大量、连续的I/O(尤其配合O_DIRECT),性能非常高。 对于通用工作负载,通过页缓存、预读等机制提供了很好的性能。
管理单元 整个设备或分区。 单个文件。
主要用途 存储初始化、系统工具、高性能数据库后端。 所有常规的应用程序数据存储。

block/fops.c (块设备) vs. drivers/char/raw.c (字符设备)
在历史上,Linux提供过一个/dev/raw/rawN字符设备接口,它也可以绑定到块设备上,其主要目的就是提供一个绕过页缓存的接口。这在O_DIRECT在块设备上得到良好支持之前是一种替代方案。

  • 现代用法:现在,/dev/raw设备接口已经废弃block/fops.c通过在open()时检查O_DIRECT标志,已经完全统一并取代了/dev/raw的功能。因此,现代Linux系统中,实现无缓存I/O的标准方式就是直接在块设备文件(如/dev/sda)上使用O_DIRECT

块设备层初始化:blkdev_ini

本代码片段展示了 Linux 内核通用块设备层的核心初始化过程。其主要功能有两个:第一,定义并初始化了一个名为 blkdev_dio_poolbio_set 结构,这是一个专为块设备直接 I/O (Direct I/O) 设计的高性能内存池,用于分配 I/O 请求的基本单元 bio 结构体;第二,定义了一个名为 def_blk_fopsfile_operations 结构体,它为所有块设备提供了一套默认的文件操作函数接口,将通用的 VFS 文件操作(如 read, write, ioctl)桥接到块设备层的具体实现上。

实现原理分析

此代码是 VFS(虚拟文件系统)层与具体块设备驱动之间的关键“胶水层”和性能基础设施。

  1. 高性能 Bio 分配池 (struct bio_set):

    • 在块设备 I/O 中,bio 结构体被海量地创建和销毁。为了避免 kmalloc 带来的性能开销和内存碎片,内核设计了 bio_set 作为一个专用的 bio 对象分配器。
    • 核心组件:
      a. bio_slab: 一个 SLAB 缓存,是 bio 对象的主要来源。
      b. per-cpu cache: bio_set 的一个关键性能优化。它为每个 CPU 核心都维护一个本地的 bio 对象缓存。当一个 CPU 需要 bio 时,它可以极快地从自己的本地缓存中获取,完全避免了锁竞争。这在多核系统上效果显著。
      c. mempool_t: bio_poolbvec_pool 是内存池(mempool)。内存池是一种保证分配成功的机制。它会预留一部分内存。当 SLAB 分配器因内存紧张而失败时,可以从内存池中获取备用的对象。这对于 I/O 路径至关重要,可以防止因内存不足而导致的 I/O 死锁。
      d. 救援机制 (rescue_lock, rescue_list, rescue_work): 这是一个应对极端低内存情况的最终保障。当连内存池都耗尽时,分配请求会被放入 rescue_list,并通过一个工作队列 (rescue_work) 在稍后(希望届时内存已得到释放)异步地重试。这进一步增强了系统的健壮性。
  2. 初始化入口 (blkdev_init):

    • 该函数在内核启动时被调用,其唯一任务是调用 bioset_init 来初始化 blkdev_dio_pool 这个全局的 bio_set
    • bioset_init 会根据传入的参数(如预留空间大小、标志位等),完成 SLAB 缓存的创建、per-cpu 缓存的设置以及内存池的初始化工作。BIOSET_PERCPU_CACHE 标志明确启用了前面提到的 per-cpu 缓存优化。

代码分析

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
/**
* @struct bio_set
* @brief 一个用于管理 bio 结构分配的集合/池。
*
* 它封装了 bio 的 SLAB 缓存、内存池以及为避免死锁而设计的救援机制,
* 旨在提供高性能、高可靠性的 bio 对象分配。
*/
struct bio_set {
struct kmem_cache *bio_slab; /// < bio 对象的主要来源:SLAB 缓存。
unsigned int front_pad; /// < 为 bio 结构预留的前端填充空间大小。

/**
* @var cache
* @brief per-cpu 的 bio 分配缓存。
* 这是关键的性能优化,允许在无锁的情况下快速分配 bio。
*/
struct bio_alloc_cache __percpu *cache;

mempool_t bio_pool; /// < bio 对象的内存池,用于在 SLAB 分配失败时提供备用对象。
mempool_t bvec_pool; /// < bio_vec 对象的内存池,bio_vec 用于描述 bio 中的数据段。

unsigned int back_pad; /// < 为 bio 结构预留的后端填充空间大小。

/**
* @var rescue_lock
* @brief 用于保护救援列表的自旋锁。
* 这是为堆叠块驱动程序避免死锁而设计的。
*/
spinlock_t rescue_lock;
struct bio_list rescue_list; /// < 救援列表,用于存放因内存不足而分配失败的 bio 请求。
struct work_struct rescue_work; /// < 一个工作项,用于异步地重试救援列表中的分配。
struct workqueue_struct *rescue_workqueue; /// < 执行救援工作的专用工作队列。

/**
* @var cpuhp_dead
* @brief 用于处理 CPU 热插拔事件的通知节点。
* 当一个 CPU 被移除时,用于清理其上的 per-cpu 缓存。
*/
struct hlist_node cpuhp_dead;
};

// 为块设备直接I/O(Direct I/O)定义一个全局的 bio_set 实例。
static struct bio_set blkdev_dio_pool;

/**
* @brief blkdev_init - 通用块设备层的初始化函数。
* @return int: bioset_init 的返回值,成功为0。
*/
static __init int blkdev_init(void)
{
// 初始化 blkdev_dio_pool 这个 bio_set。
// 参数:池指针, 预留4个bio作为备用, bio内嵌数据区的偏移量, 标志位。
// 标志位表示需要 bvec 池和 per-cpu 缓存。
return bioset_init(&blkdev_dio_pool, 4,
offsetof(struct blkdev_dio, bio),
BIOSET_NEED_BVECS|BIOSET_PERCPU_CACHE);
}
// 将 blkdev_init 注册为内核的初始化函数,在启动时调用。
module_init(blkdev_init);