不可变 biovec 和 biovec 迭代器¶
Kent Overstreet <kmo@daterainc.com>
从 3.13 开始,biovec 在提交 bio 后不应再修改。相反,我们有一个新的结构 bvec_iter,它表示 biovec 的一个范围 - 迭代器会随着 bio 的完成而修改,而不是 biovec。
更具体地说,旧的代码需要部分完成 bio,会更新 bi_sector 和 bi_size,并将 bi_idx 前进到下一个 biovec。 如果最终停留在 biovec 的中间位置,它将递增 bv_offset 并递减 bv_len 以表示在该 biovec 中完成的字节数。
在新的方案中,为了部分完成 bio 而必须更改的所有内容都隔离到结构 bvec_iter 中:bi_sector、bi_size 和 bi_idx 已移动到那里;并且,struct bvec_iter 没有修改 bv_offset 和 bv_len,而是具有 bi_bvec_done,它表示当前 bvec 中已完成的字节数。
这里有很多新的 helper 宏用于隐藏 gory 细节 - 特别是,呈现部分完成的 biovec 的错觉,以便普通代码不必处理 bi_bvec_done。
驱动程序代码不应再直接引用 biovec; 我们现在有 bio_iovec() 和 bio_iter_iovec() 宏,它们返回字面量的 struct biovec,由原始 biovec 构建,但考虑到 bi_bvec_done 和 bi_size。
bio_for_each_segment() 已更新为采用 bvec_iter 参数而不是整数(该整数对应于 bi_idx); 对于很多代码,转换只需要更改 bio_for_each_segment() 参数的类型。
bvec_iter 的前进使用 bio_advance_iter() 完成;
bio_advance()
是 bio_advance_iter() 的包装器,它在 bio->bi_iter 上运行,并且如果存在,还会前进 bio 完整性的 iter。有一个较低级别的提前函数 - bvec_iter_advance() - 它接受指向 biovec 的指针,而不是 bio; 这由 bio 完整性代码使用。
从 5.12 开始,不支持 bv_len 为零的 bvec 段。
这一切对我们有什么好处?¶
拥有一个真正的迭代器,并使 biovec 不可变,有很多优点
以前,当您没有一次处理一个 bvec 时,迭代 bio 非常笨拙 - 例如,block/bio.c 中的
bio_copy_data()
,它将一个 bio 的内容复制到另一个 bio 中。 因为 biovec 的大小不一定相同,所以旧代码非常棘手 - 它必须同时遍历两个不同的 bio,并为每个 bio 保留 bi_idx 和当前 biovec 的偏移量。新代码更加直接 - 看一看。 这种模式出现在很多地方; 很多驱动程序以前本质上是开放编码 bvec 迭代器,并且具有通用实现大大简化了大量代码。
以前,在 bio 完成后可能需要使用 biovec 的任何代码(可能将数据复制到其他地方,或者如果在发生错误时将其重新提交到其他地方)都必须保存整个 bvec 数组 - 同样,这在很多地方都在做。
Biovec 可以在多个 bio 之间共享 - bvec iter 可以表示现有 biovec 的任意范围,从 biovec 的中间开始和结束。 这使得可以有效地拆分任意 bio。 请注意,这意味着我们_只_使用 bi_size 来确定何时到达 bio 的末尾,而不是 bi_vcnt - 并且 bio_iovec() 宏在构造 biovec 时会考虑 bi_size。
现在拆分 bio 更加简单。 旧的
bio_split()
甚至不能在具有多个 bvec 的 bio 上运行! 现在,我们可以有效地拆分任意大小的 bio - 因为新的 bio 可以共享旧 bio 的 biovec。但是,必须小心确保在拆分后的 bio 仍在使用的同时不会释放 biovec,以防原始 bio 首先完成。 拆分 bio 时使用
bio_chain()
有助于解决此问题。现在可以完美地提交部分完成的 bio - 这偶尔会在堆叠块驱动程序中出现,并且各种代码(例如 md 和 bcache)对此有一些难看的解决方法。
过去,提交部分完成的 bio 可以完美地用于_大多数_设备,但是由于访问原始 bvec 数组是常态,因此并非所有驱动程序都会尊重 bi_idx,并且这些驱动程序会崩溃。 现在,由于所有驱动程序_必须_通过 bvec 迭代器 - 并且已经过审核以确保它们是 - 因此可以完美地提交部分完成的 bio。
其他含义:¶
现在几乎所有对 bi_idx 的使用都是不正确的并且已被删除; 相反,之前您使用 bi_idx 的地方,现在可以使用 bvec_iter,可能会将其传递给其中一个 helper 宏。
即,而不是使用 bio_iovec_idx()(或 bio->bi_iovec[bio->bi_idx]),现在可以使用 bio_iter_iovec(),它接受 bvec_iter 并返回字面量的 struct bio_vec - 从原始 biovec 动态构造,但考虑到 bi_bvec_done(和 bi_size)。
驱动程序代码不能信任或依赖 bi_vcnt - 即任何实际上不拥有 bio 的代码。 原因有两方面:首先,不再需要它来迭代 bio - 我们只使用 bi_size。 其次,当克隆 bio 并重用(原始 bio 的一部分)biovec 时,为了计算新 bio 的 bi_vcnt,我们将不得不迭代新 bio 中的所有 biovec - 这很愚蠢,因为它不是必需的。
所以,不要再使用 bi_vcnt 了。
当前的接口允许块层根据需要拆分 bio,因此我们可以消除大量的复杂性,尤其是在堆叠驱动程序中。 创建 bio 的代码可以创建任何方便大小的 bio,更重要的是,堆叠驱动程序不必同时处理它们自己的 bio 大小限制和底层设备的限制。 因此,无需为单个块驱动程序定义 ->merge_bvec_fn() 回调。
helper 的使用:¶
名称后缀为 _all 的以下 helper 只能在非 BIO_CLONED bio 上使用。 它们通常由文件系统代码使用。 驱动程序不应使用它们,因为 bio 可能在到达驱动程序之前已被拆分。
bio_for_each_segment_all()
bio_for_each_bvec_all()
bio_first_bvec_all()
bio_first_page_all()
bio_first_folio_all()
bio_last_bvec_all()
以下 helper 迭代单页段。 传递的 ‘struct bio_vec’ 将在迭代期间包含一个单页 IO 向量
bio_for_each_segment() bio_for_each_segment_all()
以下 helper 迭代多页 bvec。 传递的 ‘struct bio_vec’ 将在迭代期间包含一个多页 IO 向量
bio_for_each_bvec() bio_for_each_bvec_all() rq_for_each_bvec()