[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_fops 的 struct file_operations 实例。当用户空间的程序 open() 一个块设备文件(如 /dev/sda1)时,VFS(虚拟文件系统)会识别出这是一个块特殊文件,并将其文件操作定向到 blkdev_fops。
- 打开 (
blkdev_open):当open()被调用时,blkdev_open函数会执行。它的关键任务是找到对应的struct block_device内核对象,并将其附加到struct file的private_data字段上。这个关联是后续所有操作的基础。它还会处理独占打开(O_EXCL)等逻辑。 - 读/写 (
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调度器,并最终由设备驱动程序执行。
- 函数会从
- 控制 (
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_pool 的 bio_set 结构,这是一个专为块设备直接 I/O (Direct I/O) 设计的高性能内存池,用于分配 I/O 请求的基本单元 bio 结构体;第二,定义了一个名为 def_blk_fops 的 file_operations 结构体,它为所有块设备提供了一套默认的文件操作函数接口,将通用的 VFS 文件操作(如 read, write, ioctl)桥接到块设备层的具体实现上。
实现原理分析
此代码是 VFS(虚拟文件系统)层与具体块设备驱动之间的关键“胶水层”和性能基础设施。
高性能 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_pool和bvec_pool是内存池(mempool)。内存池是一种保证分配成功的机制。它会预留一部分内存。当 SLAB 分配器因内存紧张而失败时,可以从内存池中获取备用的对象。这对于 I/O 路径至关重要,可以防止因内存不足而导致的 I/O 死锁。
d. 救援机制 (rescue_lock,rescue_list,rescue_work): 这是一个应对极端低内存情况的最终保障。当连内存池都耗尽时,分配请求会被放入rescue_list,并通过一个工作队列 (rescue_work) 在稍后(希望届时内存已得到释放)异步地重试。这进一步增强了系统的健壮性。
- 在块设备 I/O 中,
初始化入口 (
blkdev_init):- 该函数在内核启动时被调用,其唯一任务是调用
bioset_init来初始化blkdev_dio_pool这个全局的bio_set。 bioset_init会根据传入的参数(如预留空间大小、标志位等),完成 SLAB 缓存的创建、per-cpu 缓存的设置以及内存池的初始化工作。BIOSET_PERCPU_CACHE标志明确启用了前面提到的 per-cpu 缓存优化。
- 该函数在内核启动时被调用,其唯一任务是调用
代码分析
1 | /** |










