[Linux]学习笔记系列 -- [block]bio


title: bio

categories:

  • linux
  • block
    tags:
  • linux
  • block
    abbrlink: dd206a7b
    date: 2025-10-03 09:01:49

文章目录

https://github.com/wdfk-prog/linux-study

include/linux/bio.h

BIO 迭代器与数据段访问接口 (bio_iter, bio_advance_iter)

核心功能

该代码片段定义了一系列宏和static inline函数,它们共同构成了一个用于遍历和操作 struct bio(块 I/O 请求的基本单元)中数据段(bio_vec)的API。其核心功能是为块设备驱动提供一个标准、高效且安全的接口,用于:

  1. 访问当前数据段 : 获取当前 I/O 进度(由 bio->bi_iter 迭代器所指向位置)的物理页、页内偏移和数据长度。
  2. 迭代遍历 : 提供遍历 bio 中所有数据段的机制。
  3. 更新进度 : 在驱动处理完一部分数据后,精确地更新 biobi_iter 迭代器,使其指向下一个待处理的数据位置。
  4. 状态查询 : 提供辅助函数来查询 bio 的状态,例如数据方向(读/写)、是否包含有效数据等。

实现原理分析

  1. 迭代器设计模式 : 这套 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 的原始结构。

  2. 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)。这种封装为驱动开发者提供了更简洁、更不易出错的接口。

  3. 非数据传输类 bio 的特殊处理 : bio_advance_iter 函数的实现中包含一个重要的技巧。对于像 DISCARDWRITE_ZEROES 这样的操作,它们没有实际的数据缓冲区需要遍历。因此,bio_no_advance_iter(bio) 会返回 true。在这种情况下,更新进度仅仅是减少 iter->bi_size(剩余待处理大小),而不会去移动指向 bio_vec 数据段的内部指针(通过 bvec_iter_advance)。这使得一套API可以优雅地处理数据和非数据两类请求。

  4. 性能优化 : 所有函数都被定义为 static inline,并且大量使用宏。这是因为这些函数位于 I/O 的最热路径上,每次读写都会被频繁调用。使用内联函数和宏可以完全消除函数调用的开销,将代码直接嵌入到调用点,从而最大化性能。

源码及逐行注释

c 复制代码
/**
 * @def BIO_MAX_VECS
 * @brief 一个 bio 结构体能包含的最大 bio_vec 数量。
 */
#define BIO_MAX_VECS		256U
/* ... */

/**
 * @brief 根据硬件限制调整 bio 能支持的最大段数。
 * @param[in] nr_segs 硬件或队列所能支持的段数。
 * @return 实际可用的最大段数。
 */
static inline unsigned int bio_max_segs(unsigned int nr_segs)
{
	return min(nr_segs, BIO_MAX_VECS); //!< 取硬件限制和 bio 自身限制中的较小值。
}

/**
 * @def bio_iter_iovec
 * @brief 从 bio 和指定的迭代器位置获取当前的 bio_vec 结构体。
 */
#define bio_iter_iovec(bio, iter)				\
	bvec_iter_bvec((bio)->bi_io_vec, (iter))

/**
 * @def bio_iter_page
 * @brief 从 bio 和指定的迭代器位置获取当前的物理页指针。
 */
#define bio_iter_page(bio, iter)				\
	bvec_iter_page((bio)->bi_io_vec, (iter))

/**
 * @def bio_iter_len
 * @brief 从 bio 和指定的迭代器位置获取当前 bio_vec 段的剩余长度。
 */
#define bio_iter_len(bio, iter)					\
	bvec_iter_len((bio)->bi_io_vec, (iter))

/**
 * @def bio_iter_offset
 * @brief 从 bio 和指定的迭代器位置获取当前 bio_vec 段在物理页内的偏移。
 */
#define bio_iter_offset(bio, iter)				\
	bvec_iter_offset((bio)->bi_io_vec, (iter))

/**
 * @def bio_page
 * @brief 获取 bio 当前处理位置(bio->bi_iter)的物理页指针。这是驱动中最常用的宏之一。
 */
#define bio_page(bio)		bio_iter_page((bio), (bio)->bi_iter)

/**
 * @def bio_offset
 * @brief 获取 bio 当前处理位置(bio->bi_iter)在物理页内的偏移。
 */
#define bio_offset(bio)		bio_iter_offset((bio), (bio)->bi_iter)

/**
 * @def bio_iovec
 * @brief 获取 bio 当前处理位置(bio->bi_iter)的 bio_vec 结构体。
 */
#define bio_iovec(bio)		bio_iter_iovec((bio), (bio)->bi_iter)

/**
 * @def bvec_iter_sectors
 * @brief 计算一个迭代器所代表的数据长度对应的扇区数。
 */
#define bvec_iter_sectors(iter)	((iter).bi_size >> 9)

/**
 * @def bvec_iter_end_sector
 * @brief 计算一个迭代器所代表的数据的结束扇区号。
 */
#define bvec_iter_end_sector(iter) ((iter).bi_sector + bvec_iter_sectors((iter)))

/**
 * @def bio_sectors
 * @brief 获取 bio 剩余待处理数据对应的扇区数。
 */
#define bio_sectors(bio)	bvec_iter_sectors((bio)->bi_iter)

/**
 * @def bio_end_sector
 * @brief 获取 bio 剩余待处理数据的结束扇区号。
 */
#define bio_end_sector(bio)	bvec_iter_end_sector((bio)->bi_iter)

/**
 * @brief 获取 bio 的数据传输方向 (READ 或 WRITE)。
 * @param[in] bio 指向 bio 结构体的指针。
 * @return READ 或 WRITE。
 */
#define bio_data_dir(bio) \
	(op_is_write(bio_op(bio)) ? WRITE : READ)

/**
 * @brief 检查一个 bio 是否携带有效的数据。
 * @param[in] bio 指向 bio 结构体的指针,可以为 NULL。
 * @return 如果 bio 包含需要拷贝的数据,则返回 true;否则返回 false。
 */
static inline bool bio_has_data(struct bio *bio)
{
	// bio 必须存在,且剩余大小不为0,并且操作类型不是 DISCARD 等无数据操作。
	if (bio &&
	    bio->bi_iter.bi_size &&
	    bio_op(bio) != REQ_OP_DISCARD &&
	    bio_op(bio) != REQ_OP_SECURE_ERASE &&
	    bio_op(bio) != REQ_OP_WRITE_ZEROES)
		return true;

	return false;
}

/**
 * @brief 检查一个 bio 是否是无需移动数据段指针的类型。
 * @param[in] bio 指向 bio 结构体的指针。
 * @return 如果是 DISCARD 等操作,返回 true。
 */
static inline bool bio_no_advance_iter(const struct bio *bio)
{
	return bio_op(bio) == REQ_OP_DISCARD ||
	       bio_op(bio) == REQ_OP_SECURE_ERASE ||
	       bio_op(bio) == REQ_OP_WRITE_ZEROES;
}

/**
 * @brief 获取 bio 当前数据位置的内核虚拟地址。
 * @param[in] bio 指向 bio 结构体的指针。
 * @return 成功则返回可直接访问的指针,若 bio 无数据则返回 NULL。
 * @note 此函数只对 bio 的页已经在内核地址空间的 "low memory" 中时才有效。
 *       对于 "high memory" 中的页,必须使用 kmap。
 */
static inline void *bio_data(struct bio *bio)
{
	if (bio_has_data(bio))
		return page_address(bio_page(bio)) + bio_offset(bio);

	return NULL;
}

/* ... (bio_for_each_segment_all 定义,驱动不应使用) ... */

/**
 * @brief 将 bio 的迭代器向前推进指定的字节数。
 * @param[in] bio 指向 bio 结构体的指针。
 * @param[in,out] iter 指向要推进的迭代器的指针。
 * @param[in] bytes 要推进的字节数。
 */
static inline void bio_advance_iter(const struct bio *bio,
				    struct bvec_iter *iter, unsigned int bytes)
{
	iter->bi_sector += bytes >> 9; //!< 更新起始扇区号。

	if (bio_no_advance_iter(bio)) //!< 如果是 DISCARD 等无数据操作。
		iter->bi_size -= bytes; //!< 只需减少剩余大小计数器。
	else
		// 否则,调用底层函数来推进指向 bio_vec 数组的内部指针。
		bvec_iter_advance(bio->bi_io_vec, iter, bytes);
}

/**
 * @brief 将 bio 的迭代器向前推进指定的字节数,但保证不跨越当前的 bio_vec 段。
 * @param[in] bio 指向 bio 结构体的指针。
 * @param[in,out] iter 指向要推进的迭代器的指针。
 * @param[in] bytes 要推进的字节数,必须小于等于当前 bio_vec 的剩余长度。
 */
static inline void bio_advance_iter_single(const struct bio *bio,
					   struct bvec_iter *iter,
					   unsigned int bytes)
{
	iter->bi_sector += bytes >> 9; //!< 更新起始扇区号。

	if (bio_no_advance_iter(bio)) //!< 如果是无数据操作。
		iter->bi_size -= bytes; //!< 只减少剩余大小。
	else
		// 否则,调用底层函数在单个 bio_vec 内推进指针。
		bvec_iter_advance_single(bio->bi_io_vec, iter, bytes);
}

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_headI/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)进行的一次重大重构。

  • 引入biobio被引入,成为文件系统/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中的函数提供了以下核心功能:

  1. 分配 (bio_alloc) :从一个内存池(mempool)或slab缓存中高效地分配biobio_vec结构。
  2. 添加页面 (bio_add_page) :将一个数据页添加到bio的向量列表中。bio.c的逻辑能够智能地处理页面合并,如果新添加的页面与前一个bio_vec在物理上是连续的,它会扩展前一个bio_vecbv_len,而不是创建一个新的条目。
  3. 提交 (submit_bio) :这是一个宏,最终会调用generic_make_request()。这个函数是bio生命周期中的一个重要转折点。它将bio传递给I/O调度器(I/O Scheduler)和请求队列(Request Queue)。在这里,多个bio可能被合并或拆分,最终被转换成一个或多个struct request对象,request是真正发送给硬件驱动的命令。
  4. 完成处理 :当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的生命周期管理(特别是其完成路径和在堆叠驱动中的克隆与重定向)相当复杂,是内核中容易出错的部分。
  • biorequest的二元性 :历史上,块层同时存在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

在块层内部,biorequest是两个需要区分的关键概念。

特性 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_slabs XArray在被并发访问(例如, 两个不同驱动同时初始化)时的数据一致性。

bs_bio_slab_size: 计算bio对象总大小

这是一个简单的内联辅助函数, 它的作用是根据bio_set的配置计算出其实际需要的bio对象的总大小。这个大小是整个共享机制的关键索引(key)

c 复制代码
/* 计算一个 bio_set 所需的 bio 对象的总大小. */
static inline unsigned int bs_bio_slab_size(struct bio_set *bs)
{
	/* 总大小 = 前部填充 + bio结构体本身大小 + 后部填充 */
	return bs->front_pad + sizeof(struct bio) + bs->back_pad;
}

create_bio_slab: bio_slab的工厂函数

bio_find_or_create_slab发现需要一个特定大小的slab缓存但它尚不存在时, 就会调用这个内部函数来创建它。

c 复制代码
/* create_bio_slab: 为一个特定大小创建新的 bio_slab 管理结构和底层的 kmem_cache. */
static struct bio_slab *create_bio_slab(unsigned int size)
{
	/* 分配管理结构体. */
	struct bio_slab *bslab = kzalloc(sizeof(*bslab), GFP_KERNEL);
	if (!bslab)
		return NULL;

	/* 创建一个易于识别的名称, 如 "bio-256", 这在 /proc/slabinfo 中很有用. */
	snprintf(bslab->name, sizeof(bslab->name), "bio-%d", size);
	/* 创建真正的 kmem_cache. */
	bslab->slab = kmem_cache_create(bslab->name, size,
			ARCH_KMALLOC_MINALIGN,
			SLAB_HWCACHE_ALIGN | SLAB_TYPESAFE_BY_RCU, NULL);
	if (!bslab->slab)
		goto fail_alloc_slab;

	/* 初始化元数据: 引用计数为1 (给第一个使用者). */
	bslab->slab_ref = 1;
	bslab->slab_size = size;

	/* 将新创建的 bslab 存储到全局的 bio_slabs XArray 中, 以 size 为索引. */
	if (!xa_err(xa_store(&bio_slabs, size, bslab, GFP_KERNEL)))
		return bslab;

	/* 错误处理: 如果存储失败, 回滚所有操作. */
	kmem_cache_destroy(bslab->slab);
fail_alloc_slab:
	kfree(bslab);
	return NULL;
}

bio_find_or_create_slab: 获取bio Slab缓存的主入口

这是bioset_init调用的主函数, 它实现了"查找或创建"的核心逻辑。

c 复制代码
/* bio_find_or_create_slab: 为一个 bio_set 查找一个现有的 slab 缓存, 或者创建一个新的. */
static struct kmem_cache *bio_find_or_create_slab(struct bio_set *bs)
{
	/* 计算所需的大小. */
	unsigned int size = bs_bio_slab_size(bs);
	struct bio_slab *bslab;

	/* 获取全局锁, 保护 bio_slabs XArray. */
	mutex_lock(&bio_slab_lock);
	/* 在 XArray 中根据 size 查找现有的 bslab. */
	bslab = xa_load(&bio_slabs, size);
	if (bslab) {
		/* 如果找到了, 说明已经有其他 bio_set 在使用这个大小的slab. */
		bslab->slab_ref++; /* 简单地增加引用计数. */
	} else {
		/* 如果没找到, 调用工厂函数创建一个新的. */
		bslab = create_bio_slab(size);
	}
	/* 释放全局锁. */
	mutex_unlock(&bio_slab_lock);

	/* 如果成功(无论是找到还是创建), 返回底层的 kmem_cache 指针. */
	if (bslab)
		return bslab->slab;
	return NULL;
}

bio_put_slab: 释放对bio Slab缓存的引用

当一个bio_set被销毁时(bioset_exit), 它必须调用此函数来"归还"它对slab缓存的使用权。

c 复制代码
/* bio_put_slab: 释放一个 bio_set 对其 slab 缓存的引用. */
static void bio_put_slab(struct bio_set *bs)
{
	struct bio_slab *bslab = NULL;
	unsigned int slab_size = bs_bio_slab_size(bs);

	/* 获取全局锁. */
	mutex_lock(&bio_slab_lock);

	/* 根据大小找到对应的管理结构. */
	bslab = xa_load(&bio_slabs, slab_size);
	/* 如果找不到, 这是一个严重的逻辑错误, 打印警告. */
	if (WARN(!bslab, KERN_ERR "bio: unable to find slab!\n"))
		goto out;

	/* ... 一些额外的健全性检查 ... */
	WARN_ON_ONCE(bslab->slab != bs->bio_slab);
	WARN_ON(!bslab->slab_ref);

	/* 递减引用计数. */
	if (--bslab->slab_ref)
		goto out; /* 如果还有其他使用者, 直接解锁并返回. */

	/*
	 * 如果引用计数变为0, 说明我们是最后一个使用者.
	 * 现在可以安全地销毁这个 slab 缓存了.
	 */
	xa_erase(&bio_slabs, slab_size); /* 从全局 XArray 中移除. */
	kmem_cache_destroy(bslab->slab);  /* 销毁 kmem_cache. */
	kfree(bslab);                     /* 释放管理结构体. */

out:
	/* 释放全局锁. */
	mutex_unlock(&bio_slab_lock);
}

BIO 子系统初始化

此代码片段展示了Linux内核块设备I/O层最核心的数据结构------bio ------的内存管理和初始化机制。bio (Block I/O) 结构体是内核中用于描述一个块设备读写请求的基本单元。它像一个"集装箱", 里面包含了目标设备、起始扇区、数据大小以及指向实际数据缓冲区的一组向量(bio_vec)等所有信息。由于系统在运行时会产生海量的bio请求, 如何高效地分配和释放bio结构体本身, 对I/O性能至关重要。

此代码的核心原理是biobio_vec结构体建立多层次、可定制的内存池(mempool)和slab缓存(slab cache), 以实现极快的无锁(lock-free)或低锁(low-contention)分配, 并提供一个"救援"机制来应对内存紧张的状况

关键组件与原理:

  1. kmem_cache / Slab分配器 (最底层):

    • init_bio函数首先会为不同大小的bio_vec数组创建多个kmem_cache实例。Slab分配器是内核中用于高效分配和释放小块、固定大小内存对象的标准机制。它通过预先分配"slab"页并将它们切分成多个对象, 避免了频繁调用伙伴系统的开销。SLAB_HWCACHE_ALIGN标志确保了对象在硬件缓存行上对齐, 提升性能。
  2. mempool (中间层):

    • 在Slab缓存之上, bioset_init函数会创建一个mempool_t实例。内存池(mempool)是一种保证分配成功 的机制。它会预先从Slab分配器中申请并保留一定数量的对象(pool_size)。
    • 当驱动程序需要分配一个bio时, mempool会首先尝试从其内部的预留池中快速、无锁地提供一个对象。
    • 关键 : 如果预留池为空, mempool会尝试从底层的Slab分配器中申请新的对象。如果Slab分配也失败了(因为系统内存不足), mempool可以被配置为睡眠等待, 直到有其他代码释放对象回池中。这保证了即使在内存压力下, 只要等待, I/O请求的描述符最终总能被分配出来, 避免了I/O流程因内存分配失败而死锁。
  3. bio_set (最高层/定制层):

    • bio_set是一个封装了biobio_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提交路径中, 转移到了安全的后台线程中。
  4. Per-CPU 缓存 (BIOSET_PERCPU_CACHE):

    • 为了达到极致的性能, bioset_init还可以选择性地启用per-cpu缓存。这会在每个CPU核心上创建一个小型的、完全无锁的bio对象缓存。当一个CPU需要分配bio时, 它会首先尝试从自己的本地缓存中获取, 这几乎没有任何开销。只有当本地缓存为空时, 它才会去访问有锁的、共享的mempool。这极大地减少了多核系统上的锁竞争。

初始化流程总览 (init_bio)

c 复制代码
static struct biovec_slab {
	int nr_vecs;
	char *name;
	struct kmem_cache *slab;
} bvec_slabs[] __read_mostly = {
	{ .nr_vecs = 16, .name = "biovec-16" },
	{ .nr_vecs = 64, .name = "biovec-64" },
	{ .nr_vecs = 128, .name = "biovec-128" },
	{ .nr_vecs = BIO_MAX_VECS, .name = "biovec-max" },
};

static int __init init_bio(void)
{
	int i;
	BUILD_BUG_ON(BIO_FLAG_LAST > 8 * sizeof_field(struct bio, bi_flags));

	/* 步骤1: 为不同大小的 bio_vec 数组创建底层的 slab 缓存. */
	for (i = 0; i < ARRAY_SIZE(bvec_slabs); i++) {
		struct biovec_slab *bvs = bvec_slabs + i;

		bvs->slab = kmem_cache_create(bvs->name,
				bvs->nr_vecs * sizeof(struct bio_vec), 0,
				SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
	}

	/* 步骤2: 设置CPU热插拔回调, 用于管理per-cpu缓存的生命周期. */
	cpuhp_setup_state_multi(CPUHP_BIO_DEAD, "block/bio:dead", NULL,
					bio_cpu_dead);

	/*
	 * 步骤3: 初始化一个全局的、供文件系统使用的 bio_set, 名为 fs_bio_set.
	 * @pool_size: BIO_POOL_SIZE, 预留的对象数量.
	 * @front_pad: 0, bio前没有额外空间.
	 * @flags:
	 *   - BIOSET_NEED_BVECS: 需要一个独立的 bio_vec 内存池.
	 *   - BIOSET_PERCPU_CACHE: 启用高性能的per-cpu缓存.
	 */
	if (bioset_init(&fs_bio_set, BIO_POOL_SIZE, 0,
			BIOSET_NEED_BVECS | BIOSET_PERCPU_CACHE))
		panic("bio: can't allocate bios\n"); /* 如果失败, 这是致命错误, 系统无法工作. */

	return 0;
}
/* 确保在子系统初始化阶段被调用. */
subsys_initcall(init_bio);

bioset_init 详细流程

c 复制代码
/*
 * fs_bio_set is the bio_set containing bio and iovec memory pools used by
 * IO code that does not need private memory pools.
 */
struct bio_set fs_bio_set;
EXPORT_SYMBOL(fs_bio_set);

int bioset_init(struct bio_set *bs,
		unsigned int pool_size,
		unsigned int front_pad,
		int flags)
{
	/* ... 初始化 front_pad, back_pad, 锁, 救援工作项 ... */
	bs->front_pad = front_pad;
	if (flags & BIOSET_NEED_BVECS)
		bs->back_pad = BIO_INLINE_VECS * sizeof(struct bio_vec);
	else
		bs->back_pad = 0;

	spin_lock_init(&bs->rescue_lock);
	bio_list_init(&bs->rescue_list);
	INIT_WORK(&bs->rescue_work, bio_alloc_rescue);

	/* 为 bio 结构体本身找到或创建一个 slab 缓存. */
	bs->bio_slab = bio_find_or_create_slab(bs);
	if (!bs->bio_slab)
		return -ENOMEM;

	/* 基于 slab 缓存, 初始化 bio 的内存池. */
	if (mempool_init_slab_pool(&bs->bio_pool, pool_size, bs->bio_slab))
		goto bad;

	/* 如果需要, 初始化 bio_vec 的内存池. */
	if ((flags & BIOSET_NEED_BVECS) &&
	    biovec_init_pool(&bs->bvec_pool, pool_size))
		goto bad;

	/* 如果需要, 创建救援工作队列. */
	if (flags & BIOSET_NEED_RESCUER) {
		bs->rescue_workqueue = alloc_workqueue("bioset",
							WQ_MEM_RECLAIM, 0);
		if (!bs->rescue_workqueue)
			goto bad;
	}

	/* 如果需要, 创建 per-cpu 缓存. */
	if (flags & BIOSET_PERCPU_CACHE) {
		bs->cache = alloc_percpu(struct bio_alloc_cache);
		if (!bs->cache)
			goto bad;
		cpuhp_state_add_instance_nocalls(CPUHP_BIO_DEAD, &bs->cpuhp_dead);
	}

	return 0;
bad:
	/* 如果任何步骤失败, 调用 bioset_exit 来回滚所有已成功的初始化操作. */
	bioset_exit(bs);
	return -ENOMEM;
}

好的,我们来分析 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 向量表量身定制的、高度优化的专用内存分配器。其主要功能包括:

  1. Slab 缓存 : 创建一个 kmem_cache (slab 分配器),用于高效地分配和释放大小固定的 bio 对象。
  2. 内存池 (mempool) : 在 slab 缓存之上建立一个 mempool。这是一个带有"备用库存"的内存分配器,它预先分配一定数量的对象,旨在保证在内存紧张的情况下,关键路径(如内存回收)的 bio 分配请求也能够成功
  3. 内存布局优化 : 支持在分配的 bio 结构体前后预留"衬垫"(padding),允许调用者将 bio 嵌入到其他数据结构中,或者将小型的 bio_vec 数组"内联"到与 bio 同一块内存中,以减少内存碎片和分配次数。
  4. 应急处理机制 (rescuer) : 提供一个可选的"救援"工作队列。当 mempool 的备用库存也耗尽时,可以将失败的 I/O 请求放入一个救援列表,由一个专门的工作线程在稍后(系统内存压力可能缓解时)重新尝试提交。

bio_alloc_rescue 则是这个应急机制的具体实现,它在一个工作队列的上下文中执行,负责处理被"救援"的 bio 请求。

实现原理分析

  1. Slab + Mempool:为健壮性而生的双层结构:

    • Slab (kmem_cache) : 这是第一层,负责高效地从操作系统管理的大块内存中切分出大小合适的、用于 bio 的内存块。它能减少内部碎片,并利用 CPU 缓存亲和性来加速分配。
    • Mempool : 这是构建在 Slab 之上的第二层,也是 bio_set 的精髓所在。mempool 维护了一个预先分配好的对象池(大小由 pool_size 决定)。当一个 bio_alloc 请求到来时,它首先尝试从 Slab 分配。如果 Slab 因为内存不足而失败,mempool 会从自己的预留池中拿出一个对象来满足请求。这种机制对于防止系统在内存回收(swapping)等关键路径上发生死锁至关重要------如果内存回收本身因为无法分配一个 bio(用于写入脏页)而卡住,系统就会崩溃。mempool 保证了这些关键操作总是有内存可用。
  2. front_padback_pad:减少内存分配的技巧:

    • front_pad: 驱动程序或子系统有时需要将 bio 作为其自定义数据结构的一部分。通过指定 front_padbio_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 数组进行一次额外的、独立的内存分配。
  3. Rescuer Workqueue:异步的最后一道防线 : 即使有了 mempool,在极端内存压力下,也可能出现分配失败的情况(例如,一个需要拆分的 bio 请求,需要分配多个新的 bio,耗尽了 mempool)。BIOSET_NEED_RESCUER 标志启用了一个最终的 fallback 机制。当分配失败时,相关的 bio 请求不会被立即丢弃,而是被添加到一个临时的 rescue_list 中,然后 rescue_work 被调度。bio_alloc_rescue 函数会在一个专门的 workqueue(一个内核线程)中被执行。它会从列表中取出 bio 并重新提交。这给了系统一个喘息的机会,寄希望于在工作队列运行时,系统的内存状况已经有所好转。WQ_MEM_RECLAIM 标志确保了这个工作队列在内存回收期间有更高的运行优先级,避免死锁。

源码及逐行注释

c 复制代码
/**
 * @brief "救援"工作队列的处理函数。
 * @note  在一个专用的内核线程上下文中执行,用于重新提交因内存不足而失败的 bio 请求。
 * @param[in] work 指向 work_struct 结构的指针。
 */
static void bio_alloc_rescue(struct work_struct *work)
{
	// 从 work_struct 指针找到其所属的 bio_set 结构体。
	struct bio_set *bs = container_of(work, struct bio_set, rescue_work);
	struct bio *bio;

	while (1) {
		spin_lock(&bs->rescue_lock); //!< 锁定救援列表以进行安全操作。
		bio = bio_list_pop(&bs->rescue_list); //!< 从救援列表中弹出一个 bio。
		spin_unlock(&bs->rescue_lock); //!< 解锁。

		if (!bio) //!< 如果列表为空,则退出循环。
			break;

		// 重新提交这个 bio。使用 _noacct 版本因为它是一个内部的、已被记账的请求。
		submit_bio_noacct(bio);
	}
}

/**
 * @brief 初始化一个 bio_set。
 * @param[in,out] bs 指向要初始化的 bio_set 结构体。
 * @param[in] pool_size mempool 中预留的 bio 和 bio_vec 的数量。
 * @param[in] front_pad 在 bio 结构体前预留的字节数。
 * @param[in] flags 行为修饰标志,如 BIOSET_NEED_BVECS 和 BIOSET_NEED_RESCUER。
 * @return 成功返回0,失败返回错误码。
 */
int bioset_init(struct bio_set *bs,
		unsigned int pool_size,
		unsigned int front_pad,
		int flags)
{
	bs->front_pad = front_pad; //!< 设置前衬垫大小。
	if (flags & BIOSET_NEED_BVECS)
		// 如果需要,设置后衬垫大小,用于内联 bio_vec 数组。
		bs->back_pad = BIO_INLINE_VECS * sizeof(struct bio_vec);
	else
		bs->back_pad = 0;

	// 初始化救援机制相关的锁、列表和工作项。
	spin_lock_init(&bs->rescue_lock);
	bio_list_init(&bs->rescue_list);
	INIT_WORK(&bs->rescue_work, bio_alloc_rescue);

	// 查找或创建一个大小合适的 slab 缓存。大小 = 前衬垫 + bio + 后衬垫。
	bs->bio_slab = bio_find_or_create_slab(bs);
	if (!bs->bio_slab)
		return -ENOMEM;

	// 在 slab 缓存之上初始化一个 mempool。
	if (mempool_init_slab_pool(&bs->bio_pool, pool_size, bs->bio_slab))
		goto bad;

	// 如果需要,为 bio_vec 数组也初始化一个独立的 mempool。
	if ((flags & BIOSET_NEED_BVECS) &&
	    biovec_init_pool(&bs->bvec_pool, pool_size))
		goto bad;

	// 如果需要,创建一个专用的工作队列用于救援。
	if (flags & BIOSET_NEED_RESCUER) {
		// WQ_MEM_RECLAIM 标志对避免内存回收时的死锁至关重要。
		bs->rescue_workqueue = alloc_workqueue("bioset",
							WQ_MEM_RECLAIM, 0);
		if (!bs->rescue_workqueue)
			goto bad;
	}
	// 如果需要,分配一个 per-CPU 缓存以提高性能。
	if (flags & BIOSET_PERCPU_CACHE) {
		bs->cache = alloc_percpu(struct bio_alloc_cache);
		if (!bs->cache)
			goto bad;
		/* ... (CPU热插拔相关处理) ... */
	}

	return 0; //!< 成功。
bad:
	bioset_exit(bs); //!< 初始化失败,调用清理函数回滚所有操作。
	return -ENOMEM;
}
EXPORT_SYMBOL(bioset_init); //!< 导出符号,供其他内核模块使用。

include/linux/bvec.h

BIO 向量 (bvec) 迭代器底层实现

核心功能

该代码片段定义了 struct bvec_iter------一个用于精确描述块 I/O 请求 (bio) 处理进度的核心数据结构,并提供了一套底层的、高性能的宏和内联函数来访问和操作这个迭代器。其核心功能是为更高层的 bio API 提供必要的"引擎",实现对 bio_vec 数组(描述 I/O 数据缓冲区的物理内存段列表)的精确遍历。

最关键的是,它实现了两种视图的抽象:

  1. 多页视图 (mp_bvec_iter_*) : 将一个可能跨越多个物理页的 bio_vec 视为一整块连续的缓冲区进行访问。
  2. 单页视图 (bvec_iter_*) : 将多页的 bio_vec 分解成一系列以单个物理页为边界的块,这是大多数硬件 DMA 和内存拷贝操作所需要的视图。

实现原理分析

  1. 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 的处理,并且能够精确地知道下一次应该从哪里开始。

  2. 单页与多页视图的转换技巧 : 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
  3. 迭代器推进逻辑 (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 循环,执行速度更快。

源码及逐行注释

c 复制代码
/**
 * @struct bvec_iter
 * @brief 块 I/O 向量迭代器。
 *        它精确地描述了一个 bio 请求的处理进度。
 */
struct bvec_iter {
	sector_t		bi_sector;	/**< I/O 在设备上的起始地址 (以512字节扇区为单位) */
	unsigned int		bi_size;	/**< I/O 请求剩余的总字节数 */

	unsigned int		bi_idx;		/**< 当前在 bio_vec 数组中的索引 */

	unsigned int            bi_bvec_done;	/**< 在当前的 bio_vec 段中已完成的字节数 */
} __packed __aligned(4);

/* ... */

/**
 * @def __bvec_iter_bvec
 * @brief 获取迭代器当前指向的 bio_vec 结构体的指针。
 */
#define __bvec_iter_bvec(bvec, iter)	(&(bvec)[(iter).bi_idx])

/* --- 多页 (multi-page) bio_vec 视图的辅助宏 --- */
/* 这些宏将一个 bio_vec 视为一个可能跨页的、连续的内存区域。*/

/**
 * @def mp_bvec_iter_page
 * @brief (多页视图) 获取当前 bio_vec 的起始物理页。
 */
#define mp_bvec_iter_page(bvec, iter)				\
	(__bvec_iter_bvec((bvec), (iter))->bv_page)

/**
 * @def mp_bvec_iter_len
 * @brief (多页视图) 获取当前 bio_vec 中剩余待处理的数据长度。
 */
#define mp_bvec_iter_len(bvec, iter)				\
	min((iter).bi_size,					\
	    __bvec_iter_bvec((bvec), (iter))->bv_len - (iter).bi_bvec_done)

/**
 * @def mp_bvec_iter_offset
 * @brief (多页视图) 获取在当前 bio_vec (可能跨页) 中的起始偏移。
 */
#define mp_bvec_iter_offset(bvec, iter)				\
	(__bvec_iter_bvec((bvec), (iter))->bv_offset + (iter).bi_bvec_done)

/* --- 单页 bio_vec 视图的辅助宏 --- */
/* 这些宏将多页的 bio_vec 分解为以单个物理页为边界的块。这是驱动最常用的视图。*/

/**
 * @def bvec_iter_offset
 * @brief (单页视图) 获取在当前物理页内的起始偏移。
 */
#define bvec_iter_offset(bvec, iter)				\
	(mp_bvec_iter_offset((bvec), (iter)) % PAGE_SIZE)

/**
 * @def bvec_iter_len
 * @brief (单页视图) 获取当前物理页内可供处理的数据长度。
 * @note 这个宏是关键:它确保返回的长度不会跨越页边界。
 */
#define bvec_iter_len(bvec, iter)				\
	min_t(unsigned, mp_bvec_iter_len((bvec), (iter)),		\
	      PAGE_SIZE - bvec_iter_offset((bvec), (iter)))

/**
 * @def bvec_iter_page
 * @brief (单页视图) 获取当前正在处理的具体物理页的指针。
 */
#define bvec_iter_page(bvec, iter)				\
	(mp_bvec_iter_page((bvec), (iter)) +			\
	 (mp_bvec_iter_offset((bvec), (iter)) / PAGE_SIZE))

/**
 * @def bvec_iter_bvec
 * @brief (单页视图) 构建一个临时的、描述当前单页数据块的 bio_vec 结构体。
 */
#define bvec_iter_bvec(bvec, iter)				\
((struct bio_vec) {						\
	.bv_page	= bvec_iter_page((bvec), (iter)),	\
	.bv_len		= bvec_iter_len((bvec), (iter)),	\
	.bv_offset	= bvec_iter_offset((bvec), (iter)),	\
})

/**
 * @brief 将迭代器向前推进指定的字节数,可以跨越多个 bio_vec 段。
 * @param[in] bv 指向 bio_vec 数组的指针。
 * @param[in,out] iter 指向要推进的迭代器的指针。
 * @param[in] bytes 要推进的字节数。
 * @return 成功返回 true,若推进超过末尾则返回 false。
 */
static inline bool bvec_iter_advance(const struct bio_vec *bv,
		struct bvec_iter *iter, unsigned bytes)
{
	unsigned int idx = iter->bi_idx;

	if (WARN_ONCE(bytes > iter->bi_size,
		     "试图将迭代器推进到超出末尾的位置\n")) {
		iter->bi_size = 0;
		return false;
	}

	iter->bi_size -= bytes; //!< 首先从总剩余大小中减去。
	bytes += iter->bi_bvec_done; //!< 将要推进的字节数与当前段已完成的字节数相加。

	// 循环处理跨越整个 bio_vec 段的情况。
	while (bytes && bytes >= bv[idx].bv_len) {
		bytes -= bv[idx].bv_len; //!< 减去当前段的长度。
		idx++; //!< 移动到下一个 bio_vec 段。
	}

	iter->bi_idx = idx; //!< 更新 bio_vec 数组的索引。
	iter->bi_bvec_done = bytes; //!< 剩余的字节数即为新段中已完成的量。
	return true;
}

/**
 * @brief 一个简化的 bvec_iter_advance 版本,假定推进的字节数不会跨越当前 bio_vec 段。
 * @param[in] bv 指向 bio_vec 数组的指针。
 * @param[in,out] iter 指向要推进的迭代器的指针。
 * @param[in] bytes 要推进的字节数。
 */
static inline void bvec_iter_advance_single(const struct bio_vec *bv,
				struct bvec_iter *iter, unsigned int bytes)
{
	unsigned int done = iter->bi_bvec_done + bytes;

	if (done == bv[iter->bi_idx].bv_len) { //!< 如果正好完成当前段。
		done = 0; //!< 已完成字节数归零。
		iter->bi_idx++; //!< 索引指向下一个段。
	}
	iter->bi_bvec_done = done; //!< 更新已完成字节数。
	iter->bi_size -= bytes; //!< 从总剩余大小中减去。
}

/**
 * @def for_each_bvec
 * @brief 一个标准的 for 循环宏,用于遍历 bio 中所有以页为边界的数据段。
 */
#define for_each_bvec(bvl, bio_vec, iter, start)			\
	for (iter = (start);						\
	     (iter).bi_size &&						\
		((bvl = bvec_iter_bvec((bio_vec), (iter))), 1);	\
	     bvec_iter_advance_single((bio_vec), &(iter), (bvl).bv_len))

/* ... */
相关推荐
ajassi20001 小时前
开源 Linux 服务器与中间件(十三)FRP服务器、客户端安装和测试
linux·服务器·开源
XH-hui2 小时前
【打靶日记】群内靶机vm1
linux·网络安全
9084869053 小时前
文旅业务相关前沿技术应用
学习·产品经理
GIS学姐嘉欣3 小时前
地信、测绘、遥感等专业免费学习网站推荐
学习·gis开发·webgis
Eric.Lee20213 小时前
ubuntu 安装 Miniconda
linux·运维·python·ubuntu·miniconda
会飞的土拨鼠呀3 小时前
通过Linux进程id找到程序路径
linux·服务器·网络
8***84824 小时前
如何在Linux中找到MySQL的安装目录
linux·运维·mysql
9***J6284 小时前
Linux下PostgreSQL-12.0安装部署详细步骤
linux·运维·postgresql
卡提西亚5 小时前
C++笔记-34-map/multimap容器
开发语言·c++·笔记