[toc]
include/linux/bio.h
BIO 迭代器与数据段访问接口 (bio_iter, bio_advance_iter)
核心功能
该代码片段定义了一系列宏和static inline函数,它们共同构成了一个用于遍历和操作 struct bio(块 I/O 请求的基本单元)中数据段(bio_vec)的API。其核心功能是为块设备驱动提供一个标准、高效且安全的接口,用于:
- 访问当前数据段: 获取当前 I/O 进度(由
bio->bi_iter迭代器所指向位置)的物理页、页内偏移和数据长度。 - 迭代遍历: 提供遍历
bio中所有数据段的机制。 - 更新进度: 在驱动处理完一部分数据后,精确地更新
bio的bi_iter迭代器,使其指向下一个待处理的数据位置。 - 状态查询: 提供辅助函数来查询
bio的状态,例如数据方向(读/写)、是否包含有效数据等。
实现原理分析
迭代器设计模式: 这套 API 的核心设计思想是迭代器模式。
struct bio内部包含一个struct bvec_iter bi_iter成员,它并非指向bio的起始,而是时刻跟踪着当前 I/O 请求处理到的位置。这个迭代器包含了当前处理的扇区号(bi_sector)、剩余未处理的字节数(bi_size)、当前在bio_vec数组中的索引(bi_idx)以及在当前bio_vec内已处理的字节数(bi_bvec_done)。驱动程序通过操作这个bi_iter来与块设备层进行交互,报告自己的处理进度。这种设计使得一个bio可以被上层(如 I/O 调度器)分割,或被驱动程序分块处理,而无需修改bio的原始结构。API 抽象分层: 代码中存在明显的抽象分层。底层是
bvec_iter_*系列函数(定义在别处),它们直接操作bvec_iter结构体和bio_vec数组。上层则是bio_*系列宏,它们将bio自身和bio->bi_iter作为参数,封装了对bvec_iter_*的调用。例如bio_page(bio)宏展开为bio_iter_page((bio), (bio)->bi_iter),最终调用bvec_iter_page((bio)->bi_io_vec, (bio)->bi_iter)。这种封装为驱动开发者提供了更简洁、更不易出错的接口。非数据传输类
bio的特殊处理:bio_advance_iter函数的实现中包含一个重要的技巧。对于像DISCARD或WRITE_ZEROES这样的操作,它们没有实际的数据缓冲区需要遍历。因此,bio_no_advance_iter(bio)会返回true。在这种情况下,更新进度仅仅是减少iter->bi_size(剩余待处理大小),而不会去移动指向bio_vec数据段的内部指针(通过bvec_iter_advance)。这使得一套API可以优雅地处理数据和非数据两类请求。性能优化: 所有函数都被定义为
static inline,并且大量使用宏。这是因为这些函数位于 I/O 的最热路径上,每次读写都会被频繁调用。使用内联函数和宏可以完全消除函数调用的开销,将代码直接嵌入到调用点,从而最大化性能。
源码及逐行注释
1 | /** |
block/bio.c 块I/O核心结构(Block I/O Core Structure) Linux I/O请求的载体
历史与背景
这项技术是为了解决什么特定问题而诞生的?
struct bio 及其管理代码 block/bio.c 是为了解决Linux内核中一个根本性的I/O请求表示和传递问题而诞生的。它旨在取代一个更早、更原始的结构——struct buffer_head (bh)——作为I/O请求的主要载体。
buffer_head 存在以下严重问题,限制了I/O性能和系统的可扩展性:
- 粒度过小:一个
buffer_head严格地代表单个磁盘块(例如512字节或4KB)在内存中的缓存。对于一个大的I/O操作(例如写入64KB数据),内核需要创建和管理16个独立的buffer_head对象,这非常低效。 - 紧密耦合:
buffer_head将I/O请求(“我要写这个块”)和内存缓存(“这个块在内存中的副本”)这两个概念紧紧地耦合在一起。这种设计使得实现绕过页面缓存的直接I/O(Direct I/O)或处理非页对齐的I/O变得异常困难和笨拙。 - 不适合向量化I/O:现代硬件能够处理“分散-聚集”(Scatter-Gather)I/O,即一次操作可以从内存中多个不连续的缓冲区读取数据,然后写入到磁盘上的一个连续区域(反之亦然)。
buffer_head这种单块模型无法自然地表达这种向量化的I/O请求。
struct bio 的诞生就是为了解决这些问题。它的核心设计思想是解耦和向量化:
- 解耦:
bio只负责描述一个I/O操作本身,它不关心数据是否在页面缓存中。它只是一个指向数据页的指针向量。 - 向量化:一个
bio可以包含一个由多个页面(或页面片段)组成的列表(bio_vec),从而能够以一个单一、紧凑的结构来描述一个涉及多个内存段的大型I/O操作。
它的发展经历了哪些重要的里程碑或版本迭代?
bio结构的引入是Linux 2.5/2.6开发周期中对块设备层(Block Layer)进行的一次重大重构。
- 引入
bio:bio被引入,成为文件系统/MM层与块设备驱动之间传递I/O请求的新标准。 - 逐渐取代
buffer_head:内核中的代码被逐步重构,从直接提交buffer_head进行I/O,改为先将信息从buffer_head(如果还在使用)或页面缓存中构建成bio,再提交bio。 - 支持块设备分区:
bio结构中包含了对分区的支持,使得块设备驱动无需关心分区表,上层逻辑可以直接向特定分区提交I/O。 - 支持I/O优先级和类别:为了实现更智能的I/O调度,
bio中加入了字段来表示I/O的优先级(如实时I/O)和类别(读/写/同步/异步)。 - 支持数据完整性(Integrity):为了支持企业级存储,
bio被扩展以携带数据完整性校验信息(DIF/DIX)。 - 支持请求队列拆分(Request Queue Splitting):
bio的提交和完成路径被优化,以更好地适应现代多队列NVMe设备,允许多个CPU核心无锁地向硬件提交I/O。
目前该技术的社区活跃度和主流应用情况如何?
block/bio.c是Linux块设备层最核心、最基础的部分。它的代码非常稳定,但也是内核I/O性能优化的焦点。
- 绝对核心:所有对块设备(HDD, SSD, NVMe, LVM, RAID等)的I/O请求,无论是来自文件系统、交换(swap)还是裸设备访问,最终都必须被封装成一个
struct bio对象才能被块层处理。 - 社区活跃度:社区持续对其进行优化,以适应新的硬件特性(如Zoned Block Devices, NVMe over Fabrics)和新的内核接口(如
io_uring)。任何对块层性能的改进都与bio的创建、合并、拆分和完成路径密切相关。
核心原理与设计
它的核心工作原理是什么?
block/bio.c主要负责struct bio对象的分配、管理和生命周期控制。bio本身是一个描述I/O操作的**“描述符”或“信封”**。
其核心数据结构struct bio包含以下关键信息:
bi_iter: 一个迭代器,指向I/O操作在设备上的目标扇区(bi_sector)和剩余长度(bi_size)。bi_io_vec: 一个bio_vec结构体数组的指针,这是bio的精髓。每个bio_vec条目包含:bv_page: 指向一个物理页面的指针。bv_len: 这个I/O操作涉及该页面的字节数。bv_offset: I/O操作从该页面的哪个偏移量开始。
bi_vcnt:bi_io_vec数组中的条目数量。bi_end_io: 一个完成回调函数的指针。这是异步I/O的关键。当硬件完成I/O操作后,这个函数会被调用。bi_private: 一个私有数据指针,供bi_end_io回调函数使用。bi_opf: 操作码,描述了I/O的类型(读/写/discard/flush等)和属性(同步/异步等)。
block/bio.c中的函数提供了以下核心功能:
- 分配 (
bio_alloc):从一个内存池(mempool)或slab缓存中高效地分配bio和bio_vec结构。 - 添加页面 (
bio_add_page):将一个数据页添加到bio的向量列表中。bio.c的逻辑能够智能地处理页面合并,如果新添加的页面与前一个bio_vec在物理上是连续的,它会扩展前一个bio_vec的bv_len,而不是创建一个新的条目。 - 提交 (
submit_bio):这是一个宏,最终会调用generic_make_request()。这个函数是bio生命周期中的一个重要转折点。它将bio传递给I/O调度器(I/O Scheduler)和请求队列(Request Queue)。在这里,多个bio可能被合并或拆分,最终被转换成一个或多个struct request对象,request是真正发送给硬件驱动的命令。 - 完成处理:当I/O完成后,块设备驱动会调用
bio_endio()或类似函数。这个函数会负责调用bio中注册的bi_end_io回调函数,并最终释放bio结构。
它的主要优势体現在哪些方面?
- 高效的向量化I/O:能够以单一结构描述涉及多个非连续内存页的大型I/O,完美匹配现代硬件的DMA能力。
- 解耦:将I/O操作与数据缓存分离,为直接I/O、交换I/O等多种场景提供了统一的基础。
- 灵活性:通过完成回调机制,完美支持异步I/O,这是高性能服务器应用的基础。
- 可堆叠性:
bio的结构非常适合在块设备层进行“堆叠”。例如,LVM或RAID驱动可以接收上层文件系统发来的bio,对其进行转换(例如,将一个bio拆分成多个bio并发送到不同的物理磁盘),并在其完成回调中重新组合结果。
它存在哪些已知的劣劣势、局限性或在特定场景下的不适用性?
- 非面向字符设备:
bio是为块设备设计的,其模型基于扇区地址。它不适用于串口、终端等面向字节流的字符设备。 - 复杂性:
bio的生命周期管理(特别是其完成路径和在堆叠驱动中的克隆与重定向)相当复杂,是内核中容易出错的部分。 bio与request的二元性:历史上,块层同时存在bio(逻辑请求)和request(物理请求)两种结构,两者之间的关系和转换增加了块层的复杂性。现代的blk-mq(多队列块层)架构在一定程度上简化了这一点,但理解两者的区别仍然是理解块层的关键。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?
它是Linux内核中处理块设备I/O的唯一且标准的解决方案。
- 文件系统:当文件系统需要从磁盘读取或写入数据块时,它会为这些操作创建一个或多个
bio并提交。 - 交换子系统 (Swap):当内核需要将匿名页换出到交换分区或交换文件时,它会创建一个
bio来执行这个写操作。 - s 逻辑卷管理器 (LVM) 和 软件RAID (MD):这些“堆叠”驱动接收上层传来的
bio,然后根据自己的逻辑创建新的bio发送给底层的物理设备。 - 裸设备访问:当用户空间程序直接打开一个块设备文件(如
/dev/sda)并进行读写时,这些操作在内核中也会被转换成bio。
是否有不推荐使用该技术的场景?为什么?
如上所述,它只适用于块设备。在需要与字符设备、网络设备或伪文件系统交互时,需要使用完全不同的内核API(如tty_driver, net_device, VFS file operations等)。
对比分析
请将其 与 其他相似技术 进行详细对比。
struct bio vs. struct buffer_head (传统)
| 特性 | struct bio |
struct buffer_head (bh) |
|---|---|---|
| 核心概念 | I/O操作描述符 (Descriptor of an I/O operation)。 | 内存中的磁盘块缓存 (In-memory cache of a disk block)。 |
| 粒度 | 向量化。一个bio可以代表涉及多个内存页的大型I/O。 |
单块。一个bh只代表一个磁盘块。 |
| 与缓存关系 | 解耦。bio只携带数据页指针,不关心数据来源。 |
紧密耦合。bh本身就是页面缓存的一部分。 |
| 适用性 | 通用。完美支持缓冲I/O、直接I/O、交换等。 | 主要用于缓冲I/O。 |
| 当前状态 | 现代标准 | 遗留结构(仍在使用,但主要作为缓存管理,而不是I/O提交的载体)。 |
struct bio vs. struct request
在块层内部,bio和request是两个需要区分的关键概念。
| 特性 | struct bio (逻辑请求) |
struct request (物理/设备请求) |
|---|---|---|
| 抽象层次 | 较高层。由文件系统或MM层创建,描述“要做什么”。 | 较低层。由I/O调度器和请求队列管理,描述“要发给硬件什么命令”。 |
| 生命周期 | 相对较短。一个bio在被合并到request中后,其生命周期就与request绑定。 |
较长。一个request可能由多个bio合并而成,它会排队等待,直到被驱动程序处理。 |
| 合并与拆分 | bio是被合并或被拆分的对象。 |
request是合并bio的结果。 |
| 与硬件关系 | 间接。 | 直接。request结构最终会被转换为硬件能理解的命令(如SATA的NCQ命令或NVMe的提交队列条目)。 |
| 主要管理者 | 文件系统 / MM层。 | I/O调度器 / 请求队列 / 设备驱动。 |
BIO Slab 缓存管理
此代码片段揭示了Linux内核bio子系统背后一个精巧的内存管理策略。由于bio结构体的大小可以根据bio_set的需求(特别是front_pad的存在)而变化, 系统中可能会同时存在多种不同大小的bio对象。为每一种大小都创建一个独立的kmem_cache(slab缓存)可能会导致管理混乱和资源浪费。
此代码的核心原理是实现一个全局的、共享的、引用计数的kmem_cache池, 并使用bio对象的实际大小作为索引。这允许多个bio_set实例, 只要它们需要的bio对象大小完全相同, 就能共享同一个底层的kmem_cache, 从而极大地提高了内存利用效率并简化了管理。
关键数据结构:
struct bio_slab: 这是一个管理结构, 它将一个真正的kmem_cache(slab) 与其元数据打包在一起, 包括引用计数(slab_ref)和对象大小(slab_size)。bio_slabs(XArray): 这是一个全局的、线程安全的数据结构(由bio_slab_lock保护), 用作一个字典或映射。它将bio对象的大小(一个整数)映射到对应的struct bio_slab管理结构。bio_slab_lock(Mutex): 一个全局互斥锁, 用于保护bio_slabsXArray在被并发访问(例如, 两个不同驱动同时初始化)时的数据一致性。
bs_bio_slab_size: 计算bio对象总大小
这是一个简单的内联辅助函数, 它的作用是根据bio_set的配置计算出其实际需要的bio对象的总大小。这个大小是整个共享机制的关键索引(key)。
1 | /* 计算一个 bio_set 所需的 bio 对象的总大小. */ |
create_bio_slab: bio_slab的工厂函数
当bio_find_or_create_slab发现需要一个特定大小的slab缓存但它尚不存在时, 就会调用这个内部函数来创建它。
1 | /* create_bio_slab: 为一个特定大小创建新的 bio_slab 管理结构和底层的 kmem_cache. */ |
bio_find_or_create_slab: 获取bio Slab缓存的主入口
这是bioset_init调用的主函数, 它实现了”查找或创建”的核心逻辑。
1 | /* bio_find_or_create_slab: 为一个 bio_set 查找一个现有的 slab 缓存, 或者创建一个新的. */ |
bio_put_slab: 释放对bio Slab缓存的引用
当一个bio_set被销毁时(bioset_exit), 它必须调用此函数来”归还”它对slab缓存的使用权。
1 | /* bio_put_slab: 释放一个 bio_set 对其 slab 缓存的引用. */ |
BIO 子系统初始化
此代码片段展示了Linux内核块设备I/O层最核心的数据结构——bio——的内存管理和初始化机制。bio (Block I/O) 结构体是内核中用于描述一个块设备读写请求的基本单元。它像一个”集装箱”, 里面包含了目标设备、起始扇区、数据大小以及指向实际数据缓冲区的一组向量(bio_vec)等所有信息。由于系统在运行时会产生海量的bio请求, 如何高效地分配和释放bio结构体本身, 对I/O性能至关重要。
此代码的核心原理是为bio和bio_vec结构体建立多层次、可定制的内存池(mempool)和slab缓存(slab cache), 以实现极快的无锁(lock-free)或低锁(low-contention)分配, 并提供一个”救援”机制来应对内存紧张的状况。
关键组件与原理:
kmem_cache/ Slab分配器 (最底层):init_bio函数首先会为不同大小的bio_vec数组创建多个kmem_cache实例。Slab分配器是内核中用于高效分配和释放小块、固定大小内存对象的标准机制。它通过预先分配”slab”页并将它们切分成多个对象, 避免了频繁调用伙伴系统的开销。SLAB_HWCACHE_ALIGN标志确保了对象在硬件缓存行上对齐, 提升性能。
mempool(中间层):- 在Slab缓存之上,
bioset_init函数会创建一个mempool_t实例。内存池(mempool)是一种保证分配成功的机制。它会预先从Slab分配器中申请并保留一定数量的对象(pool_size)。 - 当驱动程序需要分配一个
bio时,mempool会首先尝试从其内部的预留池中快速、无锁地提供一个对象。 - 关键: 如果预留池为空,
mempool会尝试从底层的Slab分配器中申请新的对象。如果Slab分配也失败了(因为系统内存不足),mempool可以被配置为睡眠等待, 直到有其他代码释放对象回池中。这保证了即使在内存压力下, 只要等待, I/O请求的描述符最终总能被分配出来, 避免了I/O流程因内存分配失败而死锁。
- 在Slab缓存之上,
bio_set(最高层/定制层):bio_set是一个封装了bio和bio_vec内存池, 以及一个可选的”救援工作队列”的完整管理单元。不同的块设备驱动(如RAID, LVM, or 文件系统)可以创建自己的bio_set, 以隔离它们的bio分配, 避免相互干扰, 并可以定制front_pad(在bio结构体前预留空间)等特性。- 救援工作队列 (
rescue_workqueue): 这是一个非常重要的死锁避免机制。当mempool因为内存紧张而无法立即分配bio时, 它会将等待分配的请求放入一个”救援列表”(rescue_list), 然后调度一个工作项到rescue_workqueue中。这个工作队列被标记为WQ_MEM_RECLAIM, 保证了即使在内存极度紧张的情况下, 它的工作者线程也能够被创建和运行。工作者线程(bio_alloc_rescue)会在一个可以安全睡眠的上下文中, 慢慢地等待内存被释放, 然后再重新尝试处理救援列表中的请求。这就将”等待内存”这个阻塞操作从可能持有锁的、紧急的I/O提交路径中, 转移到了安全的后台线程中。
Per-CPU 缓存 (
BIOSET_PERCPU_CACHE):- 为了达到极致的性能,
bioset_init还可以选择性地启用per-cpu缓存。这会在每个CPU核心上创建一个小型的、完全无锁的bio对象缓存。当一个CPU需要分配bio时, 它会首先尝试从自己的本地缓存中获取, 这几乎没有任何开销。只有当本地缓存为空时, 它才会去访问有锁的、共享的mempool。这极大地减少了多核系统上的锁竞争。
- 为了达到极致的性能,
初始化流程总览 (init_bio)
1 | static struct biovec_slab { |
bioset_init 详细流程
1 | /* |
好的,我们来分析 bioset_init 及其相关的辅助函数。这段代码揭示了 Linux 内核中 struct bio 这一基本 I/O 单元的内存管理机制。它并非由某个特定驱动程序使用,而是块设备层提供给所有驱动(如我们之前分析的 gendisk 初始化过程)和子系统(如 MD/RAID、LVM)的一个通用、高性能、且极为健壮的内存分配器。
BIO 内存池 (bio_set) 的初始化与应急处理
核心功能
该代码片段的核心是 bioset_init 函数,它负责初始化一个 struct bio_set 对象。bio_set 本质上是一个为 struct bio 和其关联的 bio_vec 向量表量身定制的、高度优化的专用内存分配器。其主要功能包括:
- Slab 缓存: 创建一个
kmem_cache(slab 分配器),用于高效地分配和释放大小固定的bio对象。 - 内存池 (
mempool): 在 slab 缓存之上建立一个mempool。这是一个带有“备用库存”的内存分配器,它预先分配一定数量的对象,旨在保证在内存紧张的情况下,关键路径(如内存回收)的bio分配请求也能够成功。 - 内存布局优化: 支持在分配的
bio结构体前后预留“衬垫”(padding),允许调用者将bio嵌入到其他数据结构中,或者将小型的bio_vec数组“内联”到与bio同一块内存中,以减少内存碎片和分配次数。 - 应急处理机制 (
rescuer): 提供一个可选的“救援”工作队列。当mempool的备用库存也耗尽时,可以将失败的 I/O 请求放入一个救援列表,由一个专门的工作线程在稍后(系统内存压力可能缓解时)重新尝试提交。
bio_alloc_rescue 则是这个应急机制的具体实现,它在一个工作队列的上下文中执行,负责处理被“救援”的 bio 请求。
实现原理分析
Slab + Mempool:为健壮性而生的双层结构:
- Slab (
kmem_cache): 这是第一层,负责高效地从操作系统管理的大块内存中切分出大小合适的、用于bio的内存块。它能减少内部碎片,并利用 CPU 缓存亲和性来加速分配。 - Mempool: 这是构建在 Slab 之上的第二层,也是
bio_set的精髓所在。mempool维护了一个预先分配好的对象池(大小由pool_size决定)。当一个bio_alloc请求到来时,它首先尝试从 Slab 分配。如果 Slab 因为内存不足而失败,mempool会从自己的预留池中拿出一个对象来满足请求。这种机制对于防止系统在内存回收(swapping)等关键路径上发生死锁至关重要——如果内存回收本身因为无法分配一个bio(用于写入脏页)而卡住,系统就会崩溃。mempool保证了这些关键操作总是有内存可用。
- Slab (
front_pad和back_pad:减少内存分配的技巧:front_pad: 驱动程序或子系统有时需要将bio作为其自定义数据结构的一部分。通过指定front_pad,bio_set在分配bio时,会在其起始地址前预留出front_pad字节的空间。调用者可以获得指向bio的指针,然后通过指针回退front_pad字节来得到整个自定义结构的起始地址。这使得一次内存分配就能同时获得自定义结构和内嵌的bio,避免了两次kmalloc。back_pad: 类似地,小的 I/O 请求通常只需要几个bio_vec条目。通过BIOSET_NEED_BVECS标志,bio_set可以在bio结构体之后紧接着分配一块空间(back_pad),用于存放一个“内联”的bio_vec数组。这再次避免了为bio_vec数组进行一次额外的、独立的内存分配。
Rescuer Workqueue:异步的最后一道防线: 即使有了
mempool,在极端内存压力下,也可能出现分配失败的情况(例如,一个需要拆分的bio请求,需要分配多个新的bio,耗尽了mempool)。BIOSET_NEED_RESCUER标志启用了一个最终的 fallback 机制。当分配失败时,相关的bio请求不会被立即丢弃,而是被添加到一个临时的rescue_list中,然后rescue_work被调度。bio_alloc_rescue函数会在一个专门的workqueue(一个内核线程)中被执行。它会从列表中取出bio并重新提交。这给了系统一个喘息的机会,寄希望于在工作队列运行时,系统的内存状况已经有所好转。WQ_MEM_RECLAIM标志确保了这个工作队列在内存回收期间有更高的运行优先级,避免死锁。
源码及逐行注释
1 | /** |
include/linux/bvec.h
BIO 向量 (bvec) 迭代器底层实现
核心功能
该代码片段定义了 struct bvec_iter——一个用于精确描述块 I/O 请求 (bio) 处理进度的核心数据结构,并提供了一套底层的、高性能的宏和内联函数来访问和操作这个迭代器。其核心功能是为更高层的 bio API 提供必要的“引擎”,实现对 bio_vec 数组(描述 I/O 数据缓冲区的物理内存段列表)的精确遍历。
最关键的是,它实现了两种视图的抽象:
- 多页视图 (
mp_bvec_iter_*): 将一个可能跨越多个物理页的bio_vec视为一整块连续的缓冲区进行访问。 - 单页视图 (
bvec_iter_*): 将多页的bio_vec分解成一系列以单个物理页为边界的块,这是大多数硬件 DMA 和内存拷贝操作所需要的视图。
实现原理分析
bvec_iter状态机:struct bvec_iter是一个精巧的状态机,它的四个字段共同精确地定义了 I/O 的当前状态:bi_sector: I/O 请求在块设备上的逻辑起始地址。bi_size: 整个 I/O 请求剩余的总字节数。bi_idx: 当前正在处理的bio_vec元素在bio->bi_io_vec数组中的索引。bi_bvec_done: 在bio->bi_io_vec[bi_idx]这个bio_vec内部,已经处理完成的字节数。
这四个字段组合在一起,使得内核可以随时暂停和恢复对一个
bio的处理,并且能够精确地知道下一次应该从哪里开始。单页与多页视图的转换技巧:
bio_vec结构本身可以描述一个跨越多个连续物理页的缓冲区(bv_len>PAGE_SIZE)。然而,驱动程序通常需要以页为单位进行操作。这段代码中的宏优雅地解决了这个转换问题:mp_bvec_iter_*宏提供了对原始bio_vec的视图。例如,mp_bvec_iter_len计算的是在当前bio_vec中还剩下多少字节可供处理。bvec_iter_*宏则是在多页视图的基础上构建出单页视图。bvec_iter_page: 通过mp_bvec_iter_page(...) + mp_bvec_iter_page_idx(...)计算出当前正在处理的具体是哪一个物理页。它将基地址页与页内偏移转换成的页索引相加。bvec_iter_offset: 通过mp_bvec_iter_offset(...) % PAGE_SIZE计算出在当前物理页内的起始偏移。bvec_iter_len: 这是最关键的宏。它通过min_t(...)取两个值的最小值:一个是当前bio_vec剩余的总长度,另一个是从当前页内偏移到页末尾的长度 (PAGE_SIZE - bvec_iter_offset(...))。这个min操作巧妙地保证了通过此宏获取的长度永远不会跨越一个物理页的边界。brd.c中正是利用了这个特性来安全地进行memcpy。
迭代器推进逻辑 (
bvec_iter_advance): 这个函数是推进 I/O 进度的核心。它的逻辑是:
a. 从总剩余字节数iter->bi_size中减去已处理的bytes。
b. 将bytes累加到当前bio_vec的已完成计数值iter->bi_bvec_done上。
c. 进入一个while循环,如果累加后的计数值超过了当前bio_vec的总长度,就从中减去该bio_vec的长度,并将bi_idx索引加一,指向下一个bio_vec。这个循环会一直进行,直到处理完所有跨越的bio_vec。
d. 循环结束后,剩下的计数值就是新的iter->bi_bvec_done。bvec_iter_advance_single是一个优化的版本,它假设bytes不会跨越bio_vec边界,因此省去了while循环,执行速度更快。
源码及逐行注释
1 | /** |









