LWN:利用page folio来明确内存操作!
共 3742字,需浏览 8分钟
·
2021-04-07 10:39
关注了就能看到更多这么棒的文章哦~
Clarifying memory management with page folios
By Jonathan Corbet
March 18, 2021
DeepL assisted translation
https://lwn.net/Articles/849538/
内存管理(memory management)一般是以 page 为单位进行的,一个 page 通常包含 4,096 个字节,也可能更大。内核已经将 page 的概念扩展到所谓的 compound page(复合页),即一组组物理连续的单独 page 的组合。这又使得 "page" 的定义变得有些模糊了。Matthew Wilcox 从去年开始就一直在研究一个叫做 "page folios" 的概念,希望能让人们重新关注这个问题。不过,目前还不清楚内存管理社区是否会接受这个概念。
从最底层来说,page 是一个由硬件实现的概念。对内存的跟踪以及判定是否位于 RAM 中这都是以 page 为基本单位来进行的。无论是哪种 CPU 架构,尽管可能会提供有限几种 page size,但一定会从中选择一个 "base" page size,最常见的选择仍然是 4,096 字节——这与 30 年前第一个 Linux 内核发布时的情况一样。
不过,内核经常需要获取更大块的内存(memory in larger chunks)。其中一个例子是管理 "huge pages",huge page 同样也是由硬件实现的。例如,x86 架构可以使用 2MB 的 huge page,在恰当的地方使用 huge page 可以提高性能。内核也会分配用其他 size 为单位来分配一组 page,通常是用于 DMA buffer 或其他那些需要使用连续的若干 page 的场景下。这种页面分组(grouping of pages)在内核中被称为 "compound page(复合页)"。
内核所管理的内存中,每一个 base page 都是用 system memory map 中的一个 page structure 来代表的。如果利用一组 base page 创建出了一个 compound page,那么就会对这组 pages 中的第一个 page("head page")的 page structure 打上一个特殊标记,从而明确指出这是一个 compound page。这个 head page 的 page structure 中的其他信息都是对整个 compound page 生效的。所有其他 page("tail pages")也都被标记出来,使用一个指针指向相关的 head page 的 page structure。关于 compound pages 的组织方式,请参见An introduction to compound pages。
这种机制可以很容易从 tail page 的 page structure 找到整个 compound page 的 head page。内核中的许多接口都利用了这一特性,但它带来一个歧义问题:如果某个函数收到参数是一个指向了 tail page 的 page structure,那么这个函数是应该针对这个 tail page 上执行、还是在整个 compound page 上执行?或者,正如 Wilcox 在 12 月对 folio 系列 patch 的第一封邮件中所说的:
一个有 struct page 参数的函数通常预期收到的是一个 head page 或者 base page,如果给它一个 tail page 的话它就会出现 BUG。有的函数也许对任何 page 都可以支持,直接操作 PAGE_SIZE 这么多字节;有的函数可能针对 head page 会操作 page_size()这么多字节,而对 base page 或者 tail page 则操作 PAGE_SIZE 这么多字节;还有的可能收到 head page 或者 tail page 的时候都是按照 page_size()字节数来处理。我们现在的代码中以上情况都有出现。
(PAGE_SIZE 是一个 base page 的 size,而 page_size()则返回一个 page 的整体大小(它可能是个 compound page)。这个特殊的 API 似乎在历史上并没有出现过很多 bug,但是作为一个定义得这么含糊的接口,迟早会引发问题。
为了让这种情况的处理更加明晰,Wilcox 提出了 "page folio" 的概念,它实际上仍然是一个 page structure,只是保证了它一定不是 tail page。任何接受 folio page 参数的函数都会是对整个 compound page 进行操作(如果传入的确实是一个 compound page 的话),这样就不会有任何歧义。从而可以使内核里的内存管理子系统更加清晰;也就是说,如果某个函数被改为只接受 folio page 作为参数的话,很明确,它们不适用于对 tail page 的操作。
当 Wilcox 第一次发布这一组 patch 时,他强调的是这个改动会带来的另一个好处:任何函数,如果它必须要对整个 compound page 操作,但是却传入的只是一个 tail page,都必须将指向 tail page 结构的指针改为指向 head page 的指针。这通常是通过调用这个函数来实现的:
struct page *compound_head(struct page *page);
这个函数相对来说耗时很少,但是在对页面进行一次操作的过程中可能会被调用多次。这使得内核变大了(因为它是一个 inline 函数),并且也变慢了。一个接受 folio page 参数的函数就知道它收到的不会是 tail page,因此不需要调用 compound_head()。这样既节省了时间又节省了内存。
folio 类型本身被定义为一个简单的封装结构:
struct folio {
struct page page;
};
根据这个结构,建立起一套新的基础设施。例如,get_folio()和 put_folio() 函数就跟 get_page()和 put_page() 函数一样,用来管理对 folio page 的引用,但不需要再调用 current_head()。此外还有一整套更高级的函数。其实这里真正耗费时间的工作是将各个内核子系统中的 API 转换为使用新的类型;Wilcox 并没有对这项工作的性质进行美化:
这里会是大量的繁重工作,并且都是破坏性的。包括修改每一个文件系统,以及许多设备驱动程序!不过我觉得这个工作是值得的。
到 3 月 5 日发布这组 patch 的第四个版本时,核心 patch 和转换 API 的 patch(Wilcox 没有发布出来)加起来大约有 100 个 commit,对于 review 来说,工作量已经非常大了。
也许是由于 patch 太大了,之前的邮件并没有引起许多讨论。不过针对最新的一版,Andrew Morton 看了一下,感到很担心:
天啊,这些改动真是太繁杂了。我们需要记住更多的东西,今后我们将永远有一个 "page "和 "folio "的混搭,到处都是从一个转换到另一个的代码。不断地增加 folio 访问代码以及操作代码来覆盖现有的 page 访问代码和操作代码,等等。
我不清楚这一切是否真的值得。
Hugh Dickins,也表示对这项工作缺乏兴趣。另一方面,Kirill Shutemov 和 Michal Hocko 都表示支持,至少在概念上支持。Dave Chinner 说,对于文件系统开发者来说,"这种抽象是绝对必要的",尤其是当 page cache 今后能够管理多种不同大小的 compound page 的时候。
所以,换句话说,对于这项工作是否能改善内核,目前核心开发者之间还没有达成共识。随着时间的推移,情况可能会发生变化,因为会有更多的人关注它,它的优势(或缺乏优势)将变得更加明显。但一般来说,在内存管理子系统中,变化往往是缓慢发生的,哪怕不是这么巨大、杂乱的改动也是慢慢进行的。还要注意的是命名的问题,很明显,"folio" 并不受欢迎,尽管目前还没有什么更合适的名字。因此,至少有一个结论是明确的:内核很可能会有类似 folio 这样的东西,但似乎不太可能很快发生。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~