不可变的 biovecs 和 biovec 迭代器

Kent Overstreet <kmo@daterainc.com>

从 3.13 版本开始,biovecs 在 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 已被移动到那里;并且不是修改 bv_offset 和 bv_len,结构体 bvec_iter 有 bi_bvec_done,它表示当前 bvec 中已完成的字节数。

有很多新的辅助宏用于隐藏细节 - 特别是,呈现部分完成的 biovec 的假象,以便普通代码不必处理 bi_bvec_done。

  • 驱动程序代码不应再直接引用 biovecs;我们现在有 bio_iovec() 和 bio_iter_iovec() 宏,它们返回字面意义上的结构体 biovecs,这些结构体是从原始 biovecs 构建的,但会考虑 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 完整性的迭代器。

    有一个较低级别的提前函数 - bvec_iter_advance() - 它采用指向 biovec 的指针,而不是 bio;这由 bio 完整性代码使用。

从 5.12 版本开始,不支持具有零 bv_len 的 bvec 段。

这一切对我们有什么好处?

拥有一个真正的迭代器,并使 biovecs 不可变,具有许多优点

  • 以前,当你不是一次处理一个 bvec 时,迭代 bio 非常笨拙 - 例如,block/bio.c 中的 bio_copy_data(),它将一个 bio 的内容复制到另一个 bio 中。因为 biovecs 的大小不一定相同,所以旧的代码很复杂 - 它必须同时遍历两个不同的 bio,并为每个 bio 保存 bi_idx 和当前 biovec 中的偏移量。

    新代码更直接 - 看看就知道了。这种模式在很多地方出现;许多驱动程序以前本质上是开放编码的 bvec 迭代器,而使用通用实现可以大大简化许多代码。

  • 以前,任何可能需要在 bio 完成后使用 biovec 的代码(也许是将数据复制到其他地方,或者如果出现错误,也许将其重新提交到其他地方)都必须保存整个 bvec 数组 - 再次,这种情况在很多地方都存在。

  • Biovecs 可以在多个 bio 之间共享 - bvec 迭代器可以表示现有 biovec 的任意范围,从 biovec 的中间开始和结束。这使得可以有效地拆分任意 bio。请注意,这意味着我们_仅_使用 bi_size 来确定何时到达 bio 的末尾,而不是 bi_vcnt - 并且 bio_iovec() 宏在构造 biovecs 时会考虑 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,可能会将其传递给其中一个辅助宏。

    也就是说,现在你不再使用 bio_iovec_idx()(或 bio->bi_iovec[bio->bi_idx]),而是使用 bio_iter_iovec(),它接受一个 bvec_iter 并返回一个字面意义上的结构体 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() 回调。

辅助函数的用法:

  • 名称后缀为 _all 的以下辅助函数只能在非 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()
  • 以下辅助函数遍历单页段。传递的“结构体 bio_vec”将在迭代期间包含一个单页 IO 向量

    bio_for_each_segment()
    bio_for_each_segment_all()
    
  • 以下辅助函数遍历多页 bvec。传递的“结构体 bio_vec”将在迭代期间包含一个多页 IO 向量

    bio_for_each_bvec()
    bio_for_each_bvec_all()
    rq_for_each_bvec()