高端内存 【ChatGPT】

高内存处理

作者:Peter Zijlstra a.p.zijlstra@chello.nl

什么是高内存?

当物理内存的大小接近或超过虚拟内存的最大大小时,就会使用高内存(highmem)。在这种情况下,内核无法始终将所有可用的物理内存映射到虚拟内存中。这意味着内核需要开始使用对物理内存片段的临时映射来访问它想要访问的内存。

没有永久映射覆盖的(物理)内存部分就是我们所说的“高内存”。对于不同的架构,对于高内存的确切边界存在各种依赖于架构的约束。

例如,在 i386 架构中,我们选择将内核映射到每个进程的虚拟内存空间中,这样我们就不必为内核的进入/退出支付完整的 TLB 失效成本。这意味着可用的虚拟内存空间(在 i386 上为 4GiB)必须在用户空间和内核空间之间划分。

使用这种方法的架构的传统划分比例是 3:1,即用户空间为 3GiB,内核空间为顶部的 1GiB:

+--------+ 0xffffffff
| 内核   |
+--------+ 0xc0000000
|        |
| 用户   |
|        |
+--------+ 0x00000000

这意味着内核最多可以同时映射 1GiB 的物理内存,但由于我们需要虚拟地址空间用于其他目的,包括临时映射以访问其余的物理内存,实际的直接映射通常会更少(通常约为 896MiB)。

其他具有 mm 上下文标记 TLB 的架构可以具有单独的内核和用户映射。然而,一些硬件(如一些 ARM 架构)在使用 mm 上下文标记时具有有限的虚拟空间。

临时虚拟映射

内核包含几种创建临时映射的方法。以下列表按照使用优先级列出了这些方法。

  • kmap_local_page()、kmap_local_folio() - 这些函数用于创建短期映射。它们可以从任何上下文(包括中断)中调用,但映射只能在获取它们的上下文中使用。它们之间的唯一区别在于第一个接受一个指向 struct page 的指针,而第二个接受一个指向 struct folio 的指针以及标识页面的字节偏移量。

    这些函数应始终使用,而 kmap_atomic() 和 kmap() 已被弃用。

    这些映射是线程本地和 CPU 本地的,这意味着映射只能从该线程内部访问,并且在映射处于活动状态时,线程绑定到 CPU。尽管此函数从不禁用抢占,但 CPU 在映射被释放之前不能通过 CPU 热插拔从系统中拔出。

    在本地 kmap 区域中引发页面错误是有效的,除非获取本地映射的上下文由于其他原因不允许它。

    如前所述,页面错误和抢占从未被禁用。无需禁用抢占,因为当上下文切换到不同的任务时,将保存传出任务的映射,并恢复传入任务的映射。

    kmap_local_page() 和 kmap_local_folio() 总是返回有效的虚拟内核地址,并假定 kunmap_local() 永远不会失败。

    在 CONFIG_HIGHMEM=n 内核和低内存页面的情况下,它们返回直接映射的虚拟地址。因此,用户可以为已知不来自 ZONE_HIGHMEM 的页面调用普通的 page_address()。但是,始终可以安全地使用 kmap_local_{page,folio}() / kunmap_local()。

    尽管它们比 kmap() 快得多,但对于高内存情况,它们带有关于指针有效性的限制。与 kmap() 映射相反,本地映射仅在调用者的上下文中有效,不能传递给其他上下文。这意味着用户必须绝对确定将返回地址的使用限制在映射它的线程内。

    大多数代码可以设计为使用线程本地映射。因此,用户应尽量设计其代码,避免使用 kmap(),而是通过在同一线程中映射页面并优先使用 kmap_local_page() 或 kmap_local_folio()。

    允许嵌套使用 kmap_local_page() 和 kmap_atomic() 映射到一定程度(最多 KMAP_TYPE_NR),但它们的调用必须严格有序,因为映射实现是基于堆栈的。有关如何管理嵌套映射的详细信息,请参阅 kmap_local_page() kdocs(包含在“函数”部分)。

  • kmap_atomic()。此函数已被弃用;请使用 kmap_local_page()。

    注意:转换为 kmap_local_page() 必须注意遵循 kmap_local_page() 强加的映射限制。此外,kmap_atomic() 和 kunmap_atomic() 之间的代码可能隐式依赖于原子映射的副作用,即禁用页面错误或抢占,或两者都有。在这种情况下,必须在使用 kmap_local_page() 时显式调用 pagefault_disable() 或 preempt_disable() 或两者都调用。

    [传统文档]

    这允许对单个页面进行非常短暂的映射。由于映射受限于发出它的 CPU,因此它的性能很好,但因此要求发出任务在完成之前必须留在该 CPU 上,以免其他任务取代其映射。

    kmap_atomic() 也可以由中断上下文使用,因为它不会休眠,调用者也可能在调用 kunmap_atomic() 之前不休眠。

    内核中的每次调用 kmap_atomic() 都会创建一个不可抢占的部分并禁用页面错误。这可能是不希望的延迟的来源。因此,用户应优先使用 kmap_local_page() 而不是 kmap_atomic()。

    假定 k[un]map_atomic() 不会失败。

  • kmap()。此函数已被弃用;请使用 kmap_local_page()。

    注意:转换为 kmap_local_page() 必须注意遵循 kmap_local_page() 强加的映射限制。特别是,必须确保内核虚拟内存指针仅在获取它的线程中有效。

    [传统文档]

    这应该用于对单个页面进行短暂映射,而不限制抢占或迁移。由于映射空间受限并受全局锁保护以进行同步,因此它带有开销。当不再需要映射时,必须使用 kunmap() 释放页面映射到的地址。

    映射更改必须在所有 CPU 上传播。当 kmap 的池包装时,kmap() 还需要全局 TLB 失效,并且在映射空间完全被使用直到可用插槽变为可用之前可能会阻塞。因此,kmap() 仅可从可抢占的上下文中调用。

    如果映射必须持续相当长的时间,那么上述所有工作是必要的,但内核中大部分高内存映射都是短暂的,并且仅在一个地方使用。这意味着 kmap() 的成本在这种情况下大部分是浪费的。kmap() 最初并不是用于长期映射,但它已经朝着这个方向发展,并且在新代码中强烈不建议使用它,应优先使用前述函数集。

    在 64 位系统上,对 kmap_local_page()、kmap_atomic() 和 kmap() 的调用实际上没有实际工作要做,因为 64 位地址空间足以寻址所有永久映射的物理内存页面。

  • vmap()。这可用于将多个物理页面长期映射到连续的虚拟空间中。需要全局同步以取消映射。

临时映射的成本

创建临时映射的成本可能相当高。架构必须操作内核的页表、数据 TLB 和/或 MMU 的寄存器。

如果未设置 CONFIG_HIGHMEM,则内核将尝试仅通过一些算术来创建映射,将页面结构地址转换为指向页面内容的指针,而不是操纵映射。在这种情况下,取消映射操作可能是一个空操作。

如果未设置 CONFIG_MMU,则可能没有临时映射和高内存。在这种情况下,也将使用算术方法。

i386 PAE

在某些情况下,i386 架构允许您将高达 64GiB 的 RAM 安装到 32 位机器中。这带来了一些后果:

  • Linux 需要为系统中的每个页面创建一个页面框架结构,并且页面框架需要存在于永久映射中,这意味着:

    您最多可以拥有 896M/sizeof(struct page) 个页面框架;由于 struct page 是 32 字节,这将导致大约 112G 的页面;然而,内核需要在该内存中存储不仅仅是页面框架...

  • PAE 使您的页表变大 - 这会减慢系统的速度,因为需要访问更多数据来遍历 TLB 填充等。一个优点是 PAE 具有更多的 PTE 位,并且可以提供诸如 NX 和 PAT 等高级功能。

一般建议是在 32 位机器上不要使用超过 8GiB 的内存 - 尽管更多的内存可能适合您和您的工作负载,但您基本上是自己一个人 - 不要指望内核开发人员真的太在意如果事情出了问题。

下面是函数说明

https://www.kernel.org/doc/html/v6.6/mm/highmem.html#functions

posted @ 2023-12-09 16:01  摩斯电码  阅读(25)  评论(0编辑  收藏  举报