更快、更少的页错误

标题:Faster & Fewer Page Faults

日期:2023/10/06

作者:Matthew Wilcox

链接:https://www.youtube.com/watch?v=TEHRMzZ01nE

注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。


我是个非常幸运的家伙。我几乎可以在我想做的任何东西上工作。我的职责就是做些很酷的事情,用 Linux 做些有趣的事情,让 Oracle 看起来很棒。而这实际上很可怕。因为现在我必须选择值得拥有这种自由度的项目来工作。哦。哦。抱歉。一切都还好吗?好的,很好。谢谢你。是的。所以当我第一次被给予这个机会时,我第一次能够这样做。我能够这样做。我能够这样做。我能够这样做。所以当我第一次被给予这个机会时,我花了很长时间试图决定我应该研究什么。我找到了我在这里底部链接的这个演讲。我会把幻灯片放出来。这是 Richard Hamming 的演讲。汉明码、汉明权重、汉明距离。我们都听说过 Hamming。他曾在贝尔实验室工作。他在贝尔实验室做了这个演讲,讲的是如何做能获得诺贝尔奖的研究。我不是在做能获得诺贝尔奖的研究。但原理是一样的。你必须抛开你的谦虚。你必须说,我能做伟大的事情。你必须了解你工作的领域。你必须了解那些大问题是什么。你不必研究所有的大问题。但你应该知道它们是什么。你应该思考它们。不是一直思考。但当某件事出现时,当你学到新东西时,你应该扪心自问,这对我所在领域、我所从事工作的前十大问题中的任何一个有什么影响?这导致我在过去几年里从事了相当多的项目。我从中选择了四个在这个演讲中谈论。因为它们放在一起谈论确实有意义。它们最初并不是被构思成要一起谈论或一起工作的。其中一些是,但大多不是。但放在一起是合理的。它们都汇集在一起,并且都在今年或去年落地了。而且,它们共同构成了一个非常引人入胜的故事。

但在进入正题之前,我想谈谈我的一位同事所做的事情。我不知道他打算这么做。Vguard 整理了这份很棒的小抄。其中包含了链表。也许不是现在,但稍后。我希望你拿支笔划掉幻灯片的这部分。不要使用这些 API。不要使用链表。我将解释为什么。因为链表是一种不道德的数据结构。显然我应该说“不朽的”,因为我们永远不会杀死它们。但如果你在任何事情上使用它们,任何事情。任何你关心性能的地方。任何你关心…的地方…只是…你就是在犯罪。因为你的 CPU… 你写的是顺序代码。但你的 CPU 本质上是并行的。它试图从这串顺序指令中提取它能做的所有工作。而 CPU 制造商已经投入了数十年、数十亿美元来试图提取并行性。这是一个例子。我面前的这台笔记本电脑。我在上面运行了测试。我去找了它的文档。它每个时钟周期可以执行六条指令。哇!而且它运行在 2.8 吉赫兹。现在如果你使用超线程,两个超线程总共得到六条指令。不是每个线程一条。但即便如此,如果你有缓存… 如果你必须等待一次 L3 缓存未命中。比如一直跑到主内存再回来。那几乎是… 那是 1600 条指令。想想用 1600 条指令你能做多少工作。当你本不需要时却发生一次 L3 缓存未命中,这简直是犯罪。或者换句话说,如果你能花费 800 个额外周期来避免一次 L3 缓存未命中,你就领先了。你就赢了。

这就是为什么链表是邪恶的。因为对于链表,CPU 无法跑到你前面去。它能看到你在遍历链表。它理解… CPU 就这么厉害。它能弄明白,哦,是的,他在遍历链表。是的,我懂这个。但它们对此无能为力。因为你可以预取… 你可以预取下一个你要看的东西。但你无法预取下下一个东西。因为它不知道它将指向哪里。这就是为什么我们有像推测执行这样的东西。它在说,嗯… 这大约在一千字节之外。也许下一个也在一千字节之外。但如果你随机打乱你的链表,它就毫无希望了。它做不到。但数组。数组是可以被预取的。因为如果你有一个指针数组,你引用第一个,引用第二个,引用第三个,CPU 很快就会注意到。你将要引用第四、第五、第六、第七和第八个。然后它可以去预取所有这些东西。现在你不再需要等待了。你将要操作的所有数据至少已经到达了 L3 缓存。甚至可能到达了 L2 或 L1 缓存。仅仅从链表切换到数组,你就会获得巨大的速度提升。我在这一点上花了点时间,因为这真的很重要。这是我希望你听完我的演讲后带走的一件事:哦,是的,我不应该使用链表。

所以我写了一个程序来演示它有多糟糕。它快了 12 倍。这实际上是在模拟… 我不是在对满屋子的 MN 人员讲话。但当你进入内存回收时,有一个叫做 LRU 列表的东西,即最近最少使用列表。我在模拟那个。我在模拟一台 4 吉字节的机器遍历它的 LRU 列表。以数组方式做这件事比以链表方式快 12 倍。按照链表当前的写法,按照我们当前做事的方式。我们本可以在获取内存方面快 12 倍。嗯,至少在遍历列表以获取和管理内存方面是如此。我们留下了大量的性能提升空间。我还没有对此采取任何具体行动,特别是遍历 LRU 列表这件事。在我能做那件事之前,我还有大约一千件其他事情需要先做。但我正在接近那里。谈论它也是其中的一部分。而且我并不了解整个内核。但我知道内核中有成千上万,甚至数万个链表。我想摆脱它们所有。这不是我一个人能完成的任务,至少在我死之前不行。我需要帮助。我需要你们这些了解你们那部分内核的人,在可能的地方移除链表。因为它正在扼杀我们。在我的笔记本电脑上性能提升了 12 倍。谁知道在你的手机上会怎样?谁知道在你的服务器上会怎样?这很重要。好的。这是一个关于缺页中断的演讲。我现在要开始谈论缺页中断了。所以我认为重要的是,我理解,我不是在对管理人员讲话。我希望你们明白,这是一个概述。这是对 Linux 如何处理缺页中断的一个极大简化。所以当你调用 mmap 时,我们创建了一个我们称之为 VMA 的数据结构。因为我们不喜欢每次谈论它时都说“虚拟内存区域结构”。所以计算机人员喜欢首字母缩略词,我们称它们为 VMA。所以每次你调用 mmap,你可能都会得到一个新的 VMA。有时内核会合并 VMA。这不重要。关键在于,在缺页中断处理程序中,我们被交给了 CPU 试图解引用的虚拟地址。而它不知道该怎么办。它无法获取从该虚拟地址到包含你数据的物理地址的转换。所以我们能依据的只有虚拟地址。所以我们做的第一件事是,嗯,这是什么类型的内存?这个虚拟地址对应于哪个 mmap 调用?如果答案是没有,那么应用程序做了一些愚蠢的事情,我们会向它发送一个信号,告诉它它现在可能应该死掉了。所以第一件事,是的,所以那是我们做的第一件事。我们查找进程试图访问的是什么类型的内存。然后我们遍历页表。再次,我很感激。我不是在对内存管理人员讲话。所以页表,这些往往是硬件数据结构。一些 CPU 在软件中定义它们。我们别担心那个。我们来谈谈 x86 和 ARM64,它们是硬件定义的。硬件会遍历页表。事实上,硬件已经为我们遍历了页表,因为它试图弄清楚如何进行虚拟地址到物理地址的转换,但它做不到。所以我们也要向下遍历页表,因为我们需要知道它试图查找的位置。并且我们会沿途分配页表,因为它是一个多级树。所以我们会一直向下遍历页表,一路分配内存。好的。现在我们知道该把它放在哪里了,也知道我们拥有的是什么类型的东西。

通常,有两种类型的 VMA。一种是匿名内存。那指的是像你的栈、堆之类的东西。还有其他各种创建匿名 VMA 的方法。但那是重要的,那是我们谈论的那种东西。我们谈论的是没有名字的内存,这就是为什么它是匿名的。另一种是文件支持的。所以如果你 mmap 了一个文件,你知道,也许你 mmap 了你的可执行文件,也许你 mmap 了一些数据。那就是一个文件支持的 VMA。所以如果我们得到一个匿名的,暂时跳过交换的可能性,因为我试图简化这个,我们只是分配一页内存。然后我们把它清零。因为我们知道内存是预先清零的,因为我们不想泄露该内存中可能存在的任何数据。所以我们清零那一页内存,找出它的物理地址,然后将该物理地址放入页表。然后我们返回到用户空间。这次用户空间将重试该缺页中断,这次我们将找到该虚拟地址的物理转换。我们就完成了。如果是文件支持的,那么我们就去页缓存中查找。我们找到那一页。希望它在那里。一旦它在那里,我们将再次把该页插入页表。所以将该页的物理地址放入页表,返回到用户空间。用户空间重试缺页中断。一切都在发生。这是相当多的工作。我的意思是,我们处理它的速度相当快,但我们确实做了相当多的工作。那么,我想谈论的四个项目。第一个是枫树(Maple Tree)。所以,就像我说的,我们首先查找 VMA,这是我们做的第一件事。最初,当 Linux 在 0.98 版本中添加整个 VMA 概念时,因为 Linux 最初不做分页内存,它是一个单链表。所以,遍历 VMA 只是简单地遍历列表。它是排序的。所以,你知道,一旦你越过了它,你就越过去了。但通常,我们只是查看该进程映射的每个 VMA,并说,是这个吗?所以,你知道,令人惊讶的是,直到 1.0 版发布后,才有人说,嘿,也许我们应该用一棵树。于是,一棵 AVL 树被添加了进来。所以,现在我们只需要搜索 log n 次,而不是 n 次,就能找出是哪一个。我们在 2001 年用红黑树替换了 AVL 树。我认为这实际上是一个糟糕的决定。红黑树的平衡性比 AVL 树稍差。红黑树的优势在于插入操作是常数时间的。为了重新平衡红黑树,最多需要旋转树三次。而 AVL 树可能需要 log n 次重新平衡才能完成一次插入。因为 AVL 树要平衡得多。但另一方面,这意味着红黑树可能变得非常不平衡。它的一边可能比另一边深两倍。AVL 树的一边最多只能比另一边深一层。所以,我们为了使插入更快而惩罚了查找。插入和删除。我不知道你们怎么样,但我经历的缺页中断次数远多于调用 mmap 的次数。所以,我认为这是个坏主意。但我认为他们这么做的原因是,他们认为在重新平衡树时延迟缺页中断是一件坏事。而且,嗯,这是一个站得住脚的立场。我觉得这个立场在 2022 年之前本应该被重新评估。但在 2022 年,我们编写了一个名为枫树(Maple Tree)的通用数据结构。我的同事 Liam Howlett 和我,他做了大部分工作。我提供了批评和咖啡。但是,是的,我们实际上能够摆脱链表,这很好。还有红黑树。我们还摆脱了一个… 人们已经注意到在红黑树中查找 VMA 有点慢。所以,实际上在红黑树前面有一个缓存。事实证明,一旦我们有了枫树,就不再需要那个缓存了。仅仅进行实际查找就足够快了。所以,这是人们接受它的一个巨大动力。所以,你可能会问,嗯,好的,你说了这个新的枫树数据结构。它是一个内存中的、RCU 安全的 B 树。B 树最初是为了在磁盘上存储东西而发明的。但现在内存离 CPU 已经足够远了。而且,就像我说的,一次 L3 缓存未命中要 1600 条指令。使用存储数据结构来访问内存中的东西是值得的。所以,红黑树和 AVL 树都是二叉树。在树的每一层,你要么找到了你要找的 VMA,要么它在左边,要么它在右边。嗯,我们的分支因子大约是 8。在每个中间层,它介于 10 到 16 之间,嗯,介于 8 到 12 之间。然后理论上我们可以在数据结构的根节点中容纳多达 16 个 VMA。这极不可能发生,但有可能发生。现在,因为我们不再将数据结构嵌入到每个 VMA 中,红黑树是嵌入到每个 VMA 中的。我们不再那样做了。所以,实际上,我们把 VMA 缩小了五个指针。所以它小了 40 字节。另一方面,我们现在需要分配这些 256 字节的节点来存储指向它们的指针。对于大多数情况来说,这实际上是一个小小的胜利。但这确实意味着当我们必须重新平衡树时,我们必须分配新节点。而且我们必须比你可能想象的更频繁地这样做,因为它是 RCU 安全的,这意味着读取者不再需要加锁。我稍后会回到这一点,因为这真的很重要。我们谈论的典型数量,对于你的普通系统来说,大约在 20 到 1000 之间。如果你使用 electric fence 调试器,你可以有数十万、数百万个 VMA,因为每次内存分配都被放入自己的红区。每个都有自己的 VMA,周围还有保护页等等。它完全淹没了系统。实际上,如果你用 electric fence 进行严肃的调试,你必须增加允许创建的 VMA 数量。这绝对疯狂,但也绝对美妙。而且我认为现在我们有更好的工具了,但它仍然存在。所以,我说过这是 RCU 安全的。我到底是什么意思?正如我想 Paul 之前在这个会议上所说的,我们允许竞争。RCU 基本上是在说,竞争是没问题的,但我们的意思是什么?我们想提供什么保证?我们确定的一个保证是,如果一个 VMA 在获取 RCU 锁之前就存在,并且在持有 RCU 读锁的整个过程中持续存在,那么你肯定会在查找中找到它。Paul 正在竖起大拇指,所以我们说对了。如果在你的遍历过程中添加了一个,我们可能会找到它,也可能不会。如果在你的遍历过程中删除了一个,我们可能会找到它,也可能不会。但没关系,因为我们无法分辨。它可能是在我们获取 RCU 读锁之前被删除的,也可能是在之后被删除的,或者是在之前被添加的,也可能是在之后被添加的,因为这里没有同步。我们故意说我们不做同步。事情可以同时发生竞争。对于缺页中断来说,这是可以的,因为谁会写他们的应用程序,让一个线程在做 mmap 和 mmap,而另一个线程试图在是否有 VMA 的地方发生缺页中断?这不是一个东西。人们不会那样做。Sysbot 会那样做。我们需要非常小心,不要在这样做时搞砸。但我们不必担心普通程序会这样做。我们只需要确保我们没有引入安全漏洞。好的。这就是枫树。它是第一个落地的。嗯,是的。它是第一个落地的。然后我们开始研究每 VMA 锁定(per-VMA locking)。VMA 树,你会注意到在 1996 年的 2.0 版本,或者更确切地说是在我们引入 VMA 之后,那是因为在早期 Linux 是单线程的。一旦我们决定,哦,是的,我们可能想支持单个程序中的多个线程,我们就必须添加一个信号量,因为这个数据结构在所有线程之间共享。所以如果一个线程在调用 mmap,另一个线程在遍历,我们需要确保它们不会冲突。有道理。在 2001 年,我们把它从一个简单的信号量改成了一个读写信号量。这本身就带来了一系列问题,因为如果读取者优先于写入者,那么源源不断的缺页中断可能会无限期地延迟一次 mmap 调用。但如果写入者优先于读取者,你有几个线程发生缺页中断,然后另一个线程调用 mmap,然后每个其他发生缺页中断的线程都必须等待 mmap 完成,而 mmap 又在等待其他缺页中断完成。也许那些缺页中断发生在文件支持的 VMA 上,所以我们实际上是在等待磁盘返回内存。我们绕过了这个问题,但仍然存在问题。所以我们决定需要做什么,我们经历了很多不同的修订版,关于如何做到这一点,我们应该做什么?我们最终添加了每 VMA 读写信号量。那么它是如何工作的呢?嗯,在缺页中断处理程序的开头,我们获取 RCU 读锁,然后遍历枫树,枫树就是为此设计的。所以我们从枫树中加载出 VMA,然后对每 VMA 锁进行读尝试锁定(read try lock)。所以这并不是,我们在读取端并没有真正把它当作信号量使用。在写入端,是的,我们把它当作信号量使用。但实际上我们在这里做的是把它当作一个引用计数(ref count)。它是一个引用计数加上一个等待队列。

而且这是别人为我们写好的,是别人调试好的,是所有锁依赖检查器(lock dep)都理解的。而且,你知道,我们可以自己写代码(open code),因为我们并没有完全把它当作读写信号量来用,但这样做要好得多。

所以我们添加的另一件事是一个序列计数器(sequence counter)。我们有两个序列计数器。我们在 mm 结构体中添加了一个。所以所有线程共享这个 mm 结构体,我们有一个序列计数。然后我们在 VMA 中也有一个。如果两者相等,那就意味着有人锁定了这个 VMA。

你可能会想,嗯,那你又是如何得到读尝试锁定的呢?因为有时写入者需要释放那个锁。这很复杂。但读取端,查找端,非常简单。我们所做的只是加载两个整数,然后检查它们是否匹配。如果它们匹配,那么 VMA 几乎肯定被锁定了。所以在这种情况下我们会回退。我们不保证用这种方法处理所有的缺页中断。我们只是处理,嗯,几乎,我们在四十亿分之一的几率下会失败。我喜欢这种几率。我会用这种几率玩俄罗斯轮盘赌。一旦我们检查了这一点,我们就会释放 RCU 读锁,因为我们保证这个 VMA 不会被释放,因为我们已经对它加了一个读锁,或者说增加了它的引用计数。而且我们不会再查看枫树,所以我们不在乎节点是否正在被释放。所以这很棒。那么,添加对每 VMA 锁定的支持需要什么?不幸的是,我们必须去修改每个架构,或者至少是那些足够关心它的架构来添加它。到目前为止,有五个架构:ARM64、PowerPC、S390、x86 和 RISC-V。RISC-V 在下一个版本中加入了。这没关系。我不介意。如果你维护一个不同的架构,我建议你复制粘贴我们对其他架构所做的一切。有个人试图统一它们所有,这是一个英勇的努力,我祝他好运,但我不认为他会成功。我建议复制粘贴。

所以这是针对匿名 VMA 的。我们首先做匿名 VMA,因为我们认为最大的收益在那里,而且 Suren(他在这方面做了大部分工作)更熟悉匿名内存。他更了解它。他理解它。我对他说,别担心。我会处理页缓存方面。然后我被我其他的一些项目分心了,结果它根本没有进入 6.5 版本,而是进入了 6.6 版本。在此期间,他已经完成了交换(swap)和 userfaultFD 的支持。耶!所以现在有大量新功能正在涌入。但幸运的是,它们都在 6.6 版本中,所以它们会进入下一个大的稳定版本,下一个大的长期支持版本。所以这很好。6.6 版本中对页缓存的支持只适用于,它只在不需要执行 I/O 的情况下才有效。部分原因是我只是没时间确保,部分原因是通常当你要把某个东西调入内存时,我们已经做了预读(read ahead),那些页已经在页缓存中了。所以没问题。这几乎总是有效的。所以这是一个巨大的胜利。它很简单,我做到了。你会注意到在幻灯片的下方,需要读取的页缓存缺页中断,它们可能会进入 6.7 版本。我今天早上发送了实现这一点的补丁。所以还没有人看过它们。没有人审查过它们。我测试过了,但其他人会有他们自己的基准测试、工作负载和测试要运行。所以,你知道,它们有可能错过 6.7 版本的合并窗口,但我很乐观。我的意思是,我们现在处于,嗯,RC3 阶段,所以我想我们还有三周时间来确保它们的状态足够好以进入合并窗口。还有工作要做。直到昨晚,这两件事还在… 更多的支持是可能的,但更多的支持仍然是可能的,因为还有工作要做在巨型 TLBFS(huge TLBFS)上。我们只是还没费心去做那项工作。我认为这不会非常困难。巨型 TLBFS 不需要去执行 I/O 或任何复杂的事情。所以如果你想写一个快速的补丁并获得工作机会,我从之前的演讲中理解到,代码就在那里,你所要做的就是阅读它并理解发生了什么。好的,这很难。但说真的,这应该不是一个很大的补丁。只是还没有人去做这项工作。更大的问题在于设备驱动程序,因为你可以对大量的设备驱动程序调用 mmap。图形驱动程序,你可以对网络套接字进行 mmap,但那个已经被修复了。那已经可以工作了。我应该把它放在幻灯片上。我没想到要那样做。这很傻,因为我认为它是在 6.5 版本中加入的。但是,是的,一些设备驱动程序不幸地依赖于 mmap 锁被持有。但审计所有的设备驱动程序以了解它们是否真的依赖于 mmap 锁被持有,以便与驱动程序的其他部分进行同步。这是一项大工程。所以在某个时候,我们将需要能够将设备驱动程序标记为具有支持,标记为不需要持有 mmap 锁,标记为能够仅持有 VMA 锁时被调用。我在这方面没花太多时间思考。这只是我们知道需要做但尚未做的事情。哦,这听起来像是另一个人可以接手的项目。那是个大项目。那是个复杂的项目。这将涉及与人谈判,而这总是其中最难的部分。所以,从更快的缺页中断(嗯,更快且更具可扩展性的缺页中断)转向更少的缺页中断。大页(Large folios)。我过去做过很多关于 folio 的演讲。我不想在这里过多强调。基本思想是,我们不应该以 4 千字节的块来管理内存,因为这意味着我们在一个列表上会有一百万个条目。而列表是糟糕的。所以这个想法是,你知道,如果我们以 16 千字节、64 千字节的块来管理内存,我们就把列表的大小缩小了 4 倍或 16 倍,事情会变得好得多。这确实需要文件系统的支持,因为现在文件系统需要理解,你知道,它应该写回的页不再是 4 千字节大小了。它是任意大小的,它需要写回整个东西,而不仅仅是它的前 4 千字节。还有许多其他类似的事情。我在 XFS 上做了这项工作。然后 Dave Howells 接手了做 AFS,他也在做其他几个网络文件系统。它们正在进行中。EeroFS 的人在 6.2 版本中加入了他们的代码。这真的很好。其他文件系统。每个人都告诉我他们在研究它。我没有时间为每个人做他们的文件系统。树里大约有 60 个文件系统。我只是不会去做 Amiga Fast 文件系统。我就是不会。抱歉。这低于我的关心程度。但我很乐意与任何想做他们文件系统的人合作。我很乐意为他们提供建议、审查补丁并与他们合作。所以大页的初始代码做了预读。它查看你的访问模式。我们一直都在做预读。但现在预读代码不仅仅是做更大的预读块。它过去通常先预读 128 千字节,如果看起来有用,它会做 256 千字节。嗯,现在它实际上增加了我们分配的内存块的大小。所以它会是 128 千字节。一开始,它做 32 个 4 千字节的页。但如果成功了,它会做 16 个 16 千字节的页。所以它增长到了 256 千字节,并且它增加了它带入内存的每个块的大小。这很有趣。然后在 6.6 版本中,我添加了在写入时创建大页的支持。所以如果你做一个 1 兆字节的写入,我们实际上会为你分配一个 1 兆字节的 folio,填充它,然后告诉文件系统,嘿,你必须一次性写回整个兆字节。这很棒。它真的缩小了列表。它在某个基准测试上将 IOPS 提高了大约五倍。这太疯狂了。这太疯狂了。这太疯狂了。这太疯狂了。但内存管理(MM)领域真正令人兴奋的地方现在也是为匿名内存做这件事。所以当你拿一页,所以,你知道,我们可能不想为栈做这件事。但我们可能想为堆做这件事。因为你可能不想以 64 千字节的块换出你的栈。你可能想把它留在 4 千字节。但是,你知道,如果有人为他们的 malloc 库做了一个 1 吉字节的分配,做了一个 1 吉字节的分配,也许我们想以比一次 4 千字节更大的块来换出。这非常是一项正在进行的工作。我尽力不深入参与其中,因为我不太了解匿名内存。而那些真正了解匿名内存的人已经兴奋起来了。这很棒。ARM 的 Ryan Roberts,我想说,正在领导这项工作。我们有英特尔的人在与他合作。所以我让 ARM 和英特尔一起工作,世界一切都好。那么现在我们有了大页,这如何与减少缺页中断联系起来呢?嗯,我们必须为此创建一些新的 PTE 操作接口。因为,当我们把页表项插入页表时,这些东西是由硬件定义的。所以通用的 Linux 代码不能,你知道,正确地摆弄这些位以使其在每个架构上都能工作。我们需要架构支持。所以我们有一组由每个 CPU 架构实现的 API,它说,这是你将物理地址转换为页表项的方法。所以我们有一个叫做 set_pte 的函数。关于我们如何在这个函数名末尾加上 ‘at’ 有点故事。但它只能插入单个页表项。所以你说,这是我的地址。然后它说,好的。这是另一个地址。那本来也没问题。我们本可以说,是的,我们将在循环中调用它。但接着我们有,但我们有架构。我将选择 ARM64 作为这里的例子,因为 Ryan 做得非常出色。ARM 实际上有两个有趣的设施。大多数架构有其中一个或另一个,但 ARM 实际上两个都有。他们有一个叫做 PTE-cont 的东西。它说,好的,这是一个 4 千字节的条目。但它实际上是一个 16 条目块或 4 条目块的一部分,映射一个 16K 的页。所以当 CPU 看到其中任何一个时,它知道整个 16K 都存在。它们都具有相同的脏状态。它们都具有相同的访问时间。它把它当作一个单一的 16K 或 64K 页来处理。ARM 的另一个设施是你不需要设置那个位。所以你可以只是把八个连续的条目,它们引用八个连续的物理页,放入页表。当 CPU 查看其中一个时,嗯,它实际上是在加载一个缓存行(cache line)。所以它在那里。我会偷偷看一下同一个缓存行中的所有其他条目。哦,它们都是连续的。啊,我知道该怎么办了。我可以优化这个。因为 CPU 人员很擅长优化。AMD 也有那个功能。我不知道英特尔有那个功能。我向英特尔喊话说他们需要添加那个功能。据我所知,他们仍然没有。但有英特尔的人正在研究这个。所以也许他们正在考虑。或者这些是英特尔软件方面的人,他们受够了硬件人员不听他们的话,决定要表明立场。我不知道。那是内部政治。那不是我的问题。但所有这一切意味着我们需要一个接口,说,我想在页表中添加 n 个连续的页。所以这就是这些新函数所做的。嗯,第一个就是那个。set_ptes。我们插入 n 个连续的页表项。但还有其他一些事情我们也需要做。我们需要能够将整个 folio 从数据缓存中刷出(flush)。我们需要能够从指令缓存中刷出连续的页。而且,我不打算解释所有 update_mmu_cache_range 的细节。相信我。我们需要它作用于 n 个连续的页。我们向架构保证,我们将只操作单个 folio。所以如果有两个 folio 彼此相邻,并且它们碰巧在内存中物理连续,那将是两次调用。我们保证这一点。所以,因为这有助于架构知道,哦,是的,好的,我不必遍历 folio。我得到的是一个 folio,但那个 folio 中有多个页。所以,是的,我不会那样做。所以我说,我研究很多项目。这些项目是自一月份以来已经合入树中的,或者我仍在研究它们,或者我有想法并想研究它们。我很忙。所以,有人想帮忙吗?所以,其中一些,嗯,我参与了刚才谈论的所有四个项目,但角色不同,对吧?我在领导大页的努力。我在为枫树提供建议。我为锁定的改变、RCU 查找做出了贡献。我拿了英特尔 Yin Feng Wei 的一个补丁,并通过 set_ptes 的工作把它变成了我自己的项目。所以我有点偷了那个项目。我很乐意担任顾问。如果这些项目中的任何一个引起了你的注意,你说,哦,我一直也想研究那个,让我们谈谈。因为我喜欢与人合作。因为没有人是一座孤岛,至少所有这些人都在某种程度上对项目达到现在的位置起到了重要作用。也许他们审查了补丁,也许他们提供了反馈,也许他们自己写了一些补丁。这里有 XFS 的人。有内存管理的人,我的一些同事。只是非常非常多的人帮助所有这些项目落地。我非常非常感谢他们所有人。因为没有他们我做不到。就像,你知道,我站在这里展示我所做的所有工作,这很棒,对吧?但在此之前是所有的痛苦和折磨,以及那些糟糕的想法。我没有向你们展示那些人们说“你在做什么?来吧,你需要这样做”的糟糕想法。就像,哦,是的,你说得对。所以,你知道,我们并不总是以最好的方式对待彼此,但我确实发现内存管理社区在大多数时候彼此相当友好。而且,是的,如果你想参与进来,内存管理现在正处于一个令人兴奋的时期。我从未想过我会这么说。我曾在 2002 年左右发誓我永远不会成为一名内存管理开发者。而且,是的,我们到了。我站在舞台上说,嘿,内存管理是一个很棒的工作领域。

谢谢。首先,那里有很多非常酷的东西。我的意思是,缺页中断方面的一些改进,是人们长期以来一直在寻找的东西,所以看到它们真正加入进来真的很好。不过,我必须对一件事提出异议。我要断言,任何无条件的声明某事不道德的陈述,包括我现在正在做的这个陈述,其本身就是不道德的。

我认为以这种方式窃取这么多 CPU,以这种方式浪费你的 CPU,是不道德的。我真的这么认为。嗯,如果你有一个百万条目的链表,好的,那是个问题,即使你有某种神奇的内存,就像我们过去当一切都慢的时候,或者没有缓存的时候那样。但如果那个链表在你的 L1 缓存中,而一个小的链表可以,那真的有那么大的问题吗?我同意,事实上,在 RCU 中我们有一些情况,我们可能需要更多,我们曾经有链表,我们把它变成了,它仍然是链表,但它是指针页的链表。所以,我同意我们可能,特别是如果你有一个百万条目的链表,我的意思是,你对自己做了什么,对吧?但我想我们需要确保我们采取平衡的方法,采取一个全面良好的方法,看看实际问题是什么,以及各个地方的实际成本是什么。我的意思是,你的笔记本电脑有多少内存,Paul?我想是 32 兆,哦,是 32 吉,抱歉。32 吉?好的,所以你有一个 800 万条目的 LRU 列表。我不是说这很好。我也不认为这很好。但如果那是一棵深度为 20 的树,它可能没问题。它仍然是链接的。如果那是一个由特定大小的东西组成的链表,那可能没问题。如果是一个百万条目的数组,那可能会因为其他原因而成为问题,对吧?嗯,实际上,它已经是一个数组了。它叫做 MemMap。而且,我的意思是,这就是结构化页(struck pages)分配的方式,对吧?一次 64 字节,但至少在你的笔记本电脑上。它是一个大小为 64 乘以 800 万的单一数组。但遍历整个数组来查找 LRU 可能会因为其他原因而糟糕。也许它比链表好。我不知道,对吧?而且,它是按地址排序的,按物理地址排序,不是?还是我搞混了?是的。所以你的 LRU 列表不太可能按物理地址顺序排列。

是的。但无论如何,我不打算争论。拥有一个百万条目的链表是个坏主意。嗯,对于我的第一台计算机来说,这是不可能使用的。我们没有那么多内存,所以,你知道。实际上,这并不是一个坏主意。CPU 大约在 10 到 15 年前才开始变得这么好。当然,当 Linux 最初被发明时。我在一台,我要说,90 年代末的 PA RISC 机器上运行了同一个显示我笔记本电脑上百万条目数组快 12 倍的基准测试。实际上,遍历链表比遍历数组更快。我能相信这一点。是的,我知道。机器已经改变了形状和大小,你必须改变你的做法。是的。所以我想这对底层程序员来说是永恒的工作。无论如何,谢谢你。很好的演讲。谢谢你。所以有一段时间我没有关注这个了,但我知道有一段时间我们在研究一个问题,如果你有,比如说,持久内存(PMEM),并且你想卸载一些页,如果你的工作集本质上大于你在多 NUMA 系统中拥有的内存量,我们遇到了一个问题,水位线(watermark)有点太低了,所以我们基本上在 PMEM 设备,我应该说 PMEM 节点,和 RAM 之间颠簸页。我对这个问题的解释是,拥有这些静态的水位线来决定何时进行回收或迁移,对我们来说有点脆弱,我只是好奇你对这个问题的看法,以及你是否认为未来我们不会有水位线,或者会有更动态的东西。所以我说我研究内存管理。有内存管理,也有内存管理。我真的认为自己是一个没有文件系统的文件系统开发者。我主要研究页缓存,真的,然后我接触的其他所有东西都是出于必要性。我不认为自己是像 Johannes 或 Mikal 或 Blastomil 那样的真正的内存管理开发者。那些理解水位线细微差别、选择回收哪个页或回收多少页的人。我非常专注于我们存储这些试图回收的页所使用的数据结构机制,而不是选择回收哪些页或回收多少页。我认为那是黑魔法,但我仍然不完全理解。所以,很抱歉,我无法给你一个真正好的答案。如果你不是内存管理人员,那我甚至都不知道如何打开我的电脑,但说得够好了。非常感谢你这么说。抱歉,抱歉,但是抱歉。关于 PTE 操作,这与巨型页(huge pages)和透明巨型页(THP)有关吗,还是完全无关?好的,这实际上是个很好的问题。所以,是也不是,这就是你知道这是一个好问题的原因,因为我必须仔细考虑如何回答。所以,新的 PTE 接口对巨型 TLBFS 没有用,因为巨型 TLBFS 本质上总是对齐的。所以你总是在处理单个 PMD 或单个 PUD,即 2 兆字节或 1 吉字节的页。它们必须按映射对齐,这就是巨型 TLBFS 被定义为工作的方式。这很有道理,因为这就是它的用途。另一方面,透明巨型页,你问题的另一半,这绝对有帮助。所以,我们推进内存管理(MM)的方式是让透明巨型页成为 folio 的一个特例。所以,其中一件事是,如果你现在对一个 VMA 使用 M-advise,然后你想使用巨型页,然后你在它上面发生缺页中断,文件系统支持它,我们现在会分配一个 2 兆字节的页,然后读入它,你会得到一个 2 兆字节的页。但是,因为我们做的是透明巨型页,它可能没有与你的映射对齐。我们确实尝试对齐你的映射,但一个 2 兆字节的页将在文件中对齐。所以,它将占据文件的 0 到 2 兆字节、2 到 4 兆字节。如果你歪斜地映射了它,无论是无意还是有意,有些人确实故意这样做,我们称这些人为 Sysbot,你将看到使用 PTE 来映射一个 THP。这一直是 THP 设计的一部分。只是现在它变得更加突出,这就是 THP 被设计工作的方式。

我希望我回答了你的问题。你回答了,谢谢。好的。

我想我们结束了。谢谢你。