英语

高内存处理

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

什么是高内存?

当物理内存的大小接近或超过虚拟内存的最大大小时,会使用高内存(highmem)。此时,内核不可能始终保持所有可用物理内存的映射。这意味着内核需要开始使用物理内存片段的临时映射来访问它们。

未被永久映射覆盖的(物理)内存部分,我们称之为“高内存”。关于边界的确切位置,存在各种架构相关的约束。

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

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

+--------+ 0xffffffff
| Kernel |
+--------+ 0xc0000000
|        |
| User   |
|        |
+--------+ 0x00000000

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

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

临时虚拟映射

内核包含几种创建临时映射的方法。以下列表按使用偏好顺序显示它们。

  • kmap_local_page(), kmap_local_folio() - 这些函数用于创建短期映射。它们可以从任何上下文(包括中断)调用,但映射只能在获取它们的上下文中使用。它们之间的唯一区别在于,第一个函数接受指向 struct page 的指针,第二个函数接受指向 struct folio 的指针以及标识页面在 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_TYPE_NR)嵌套 kmap_local_page()kmap_atomic() 映射,但它们的调用必须严格排序,因为映射实现是基于堆栈的。有关如何管理嵌套映射的详细信息,请参阅 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 的内存 - 尽管更多的内存可能适用于您和您的工作负载,但您基本上只能靠自己 - 如果出现问题,不要指望内核开发人员真正关心。

函数

void *kmap(struct page *page)

映射一个页面以供长期使用

参数

struct page *page

指向要映射的页面的指针

返回

映射的虚拟地址

描述

只能从可抢占的任务上下文中调用,因为在启用 CONFIG_HIGHMEM 的 32 位系统上,此函数可能会休眠。

对于 CONFIG_HIGHMEM=n 的系统和低内存区域中的页面,这将返回直接内核映射的虚拟地址。

返回的虚拟地址是全局可见的,并且在通过 kunmap() 取消映射之前有效。该指针可以传递给其他上下文。

对于 32 位系统上的高内存页面,这可能会很慢,因为映射空间有限并且受到全局锁的保护。如果没有任何映射插槽可用,该函数将阻塞,直到通过 kunmap() 释放插槽。

void kunmap(struct page *page)

取消映射 kmap() 映射的虚拟地址

参数

struct page *page

指向由 kmap() 映射的页面的指针

描述

kmap() 对应。对于 CONFIG_HIGHMEM=n 和低内存区域中页面的映射,这是一个 NOOP。

struct page *kmap_to_page(void *addr)

获取 kmap 映射地址的页面

参数

void *addr

要查找的地址

返回

映射到 addr 的页面。

void kmap_flush_unused(void)

刷新所有未使用的 kmap 映射,以删除多余的映射

参数

void

没有参数

void *kmap_local_page(struct page *page)

映射一个页面以供临时使用

参数

struct page *page

指向要映射的页面的指针

返回

映射的虚拟地址

描述

可以从任何上下文调用,包括中断。

在嵌套多个映射时需要小心处理,因为映射管理是基于堆栈的。取消映射必须以映射操作的相反顺序进行

addr1 = kmap_local_page(page1); addr2 = kmap_local_page(page2); ... kunmap_local(addr2); kunmap_local(addr1);

在 addr2 之前取消映射 addr1 是无效的,会导致故障。

kmap() 映射相反,映射仅在调用者的上下文中有效,不能传递给其他上下文。

在 CONFIG_HIGHMEM=n 的内核上和低内存页面上,这将返回直接映射的虚拟地址。只有真正的高内存页面才会临时映射。

虽然 kmap_local_page() 比高内存情况下的 kmap() 快得多,但它对指针有效性有限制。

在启用 HIGHMEM 的系统上,映射高内存页面具有禁用迁移的副作用,以保持虚拟地址在抢占期间稳定。 kmap_local_page() 的任何调用者都不能依赖于此副作用。

void *kmap_local_folio(struct folio *folio, size_t offset)

映射此页帧中的一个页面以供临时使用

参数

struct folio *folio

包含该页面的页帧。

size_t offset

页帧内标识页面的字节偏移量。

描述

在嵌套多个映射时需要小心处理,因为映射管理是基于堆栈的。取消映射必须以映射操作的相反顺序进行

addr1 = kmap_local_folio(folio1, offset1);
addr2 = kmap_local_folio(folio2, offset2);
...
kunmap_local(addr2);
kunmap_local(addr1);

在 addr2 之前取消映射 addr1 是无效的,会导致故障。

kmap() 映射相反,映射仅在调用者的上下文中有效,不能传递给其他上下文。

在 CONFIG_HIGHMEM=n 的内核上和低内存页面上,这将返回直接映射的虚拟地址。只有真正的高内存页面才会临时映射。

虽然在高内存情况下,它比 kmap() 快得多,但它对指针的有效性有限制。

在启用 HIGHMEM 的系统上,映射高内存页面会产生禁用迁移的副作用,以保持虚拟地址在抢占期间的稳定。 kmap_local_folio() 的任何调用者都不能依赖此副作用。

上下文

可以从任何上下文中调用。

返回

offset 的虚拟地址。

void *kmap_atomic(struct page *page)

原子地映射页面以供临时使用 - 已弃用!

参数

struct page *page

指向要映射的页面的指针

返回

映射的虚拟地址

描述

实际上是 kmap_local_page() 的一个包装器,它还会禁用页面错误,并且根据 PREEMPT_RT 配置,还会禁用 CPU 迁移和抢占。因此,用户不应依赖后两个副作用。

映射应始终由 kunmap_atomic() 释放。

不要在新代码中使用。请改用 kmap_local_page()

当代码想要访问可能从高内存(请参阅 __GFP_HIGHMEM)分配的页面的内容时,例如页面缓存中的页面,它在原子上下文中被使用。该 API 有两个函数,它们可以以类似于以下方式使用

// Find the page of interest.
struct page *page = find_get_page(mapping, offset);

// Gain access to the contents of that page.
void *vaddr = kmap_atomic(page);

// Do something to the contents of that page.
memset(vaddr, 0, PAGE_SIZE);

// Unmap that page.
kunmap_atomic(vaddr);

请注意,kunmap_atomic() 调用采用 kmap_atomic() 调用的结果,而不是参数。

如果您需要映射两个页面,因为您想从一个页面复制到另一个页面,则需要严格嵌套 kmap_atomic 调用,例如

vaddr1 = kmap_atomic(page1); vaddr2 = kmap_atomic(page2);

memcpy(vaddr1, vaddr2, PAGE_SIZE);

kunmap_atomic(vaddr2); kunmap_atomic(vaddr1);

struct folio *vma_alloc_zeroed_movable_folio(struct vm_area_struct *vma, unsigned long vaddr)

为 VMA 分配一个零填充的页面。

参数

struct vm_area_struct *vma

要为其分配页面的 VMA。

unsigned long vaddr

该页面将插入到的虚拟地址。

描述

此函数将分配一个适合插入到此 VMA 的页面,位于此虚拟地址。它可能从高内存或可移动区域分配。架构可以提供自己的实现。

返回

一个包含一个已分配且零填充的页面的页帧,如果内存不足则为 NULL。

void memcpy_from_folio(char *to, struct folio *folio, size_t offset, size_t len)

从页帧复制一段字节。

参数

char *to

要复制到的内存。

struct folio *folio

从中读取的页帧。

size_t offset

要读取的页帧中的第一个字节。

size_t len

要复制的字节数。

void memcpy_to_folio(struct folio *folio, size_t offset, const char *from, size_t len)

将一段字节复制到页帧。

参数

struct folio *folio

要写入的页帧。

size_t offset

要存储到的页帧中的第一个字节。

const char *from

要从中复制的内存。

size_t len

要复制的字节数。

void *folio_zero_tail(struct folio *folio, size_t offset, void *kaddr)

将页帧的尾部清零。

参数

struct folio *folio

要清零的页帧。

size_t offset

页帧中开始清零的字节偏移量。

void *kaddr

当前页帧映射到的地址。

描述

如果您已使用 kmap_local_folio() 映射页帧,向其中写入了一些数据,现在需要将页帧的末尾清零(并刷新 dcache),您可以使用此函数。 如果您没有 kmapped 页帧(例如,页帧已通过 DMA 部分填充),请改用 folio_zero_range()folio_zero_segment()

返回

可以传递给 kunmap_local() 的地址。

void folio_fill_tail(struct folio *folio, size_t offset, const char *from, size_t len)

将一些数据复制到页帧并用零填充。

参数

struct folio *folio

目标页帧。

size_t offset

开始复制的 folio 中的偏移量。

const char *from

要复制的数据。

size_t len

要复制的数据的字节数。

描述

此函数对于支持内联数据的文件系统最有用。当他们想将数据从 inode 复制到页面缓存时,此函数会为他们完成所有操作。它甚至在 HIGHMEM 配置上都支持大型页帧。

size_t memcpy_from_file_folio(char *to, struct folio *folio, loff_t pos, size_t len)

从文件页帧复制一些字节。

参数

char *to

目标缓冲区。

struct folio *folio

要从中复制的页帧。

loff_t pos

文件中的位置。

size_t len

要复制的最大字节数。

描述

从此页帧最多复制 len 个字节。如果页帧来自 HIGHMEM,则可能受 PAGE_SIZE 的限制,并受页帧大小的限制。

返回

从页帧复制的字节数。

void folio_zero_segments(struct folio *folio, size_t start1, size_t xend1, size_t start2, size_t xend2)

将页帧中的两个字节范围清零。

参数

struct folio *folio

要写入的页帧。

size_t start1

将第一个字节置零。

size_t xend1

第一个范围的最后一个字节之后的一个字节。

size_t start2

第二个范围中要置零的第一个字节。

size_t xend2

第二个范围的最后一个字节之后的一个字节。

void folio_zero_segment(struct folio *folio, size_t start, size_t xend)

将 folio 中的字节范围置零。

参数

struct folio *folio

要写入的页帧。

size_t start

将第一个字节置零。

size_t xend

要置零的最后一个字节之后的一个字节。

void folio_zero_range(struct folio *folio, size_t start, size_t length)

将 folio 中的字节范围置零。

参数

struct folio *folio

要写入的页帧。

size_t start

将第一个字节置零。

size_t length

要置零的字节数。

void folio_release_kmap(struct folio *folio, void *addr)

取消映射 folio 并减少引用计数。

参数

struct folio *folio

要释放的 folio。

void *addr

先前调用 kmap_local_folio() 返回的地址。

描述

在目录处理等场景中,通常会 kmap 一个 folio。此函数取消映射 folio 并减少之前为保持 folio 活跃而持有的引用计数。

void *kmap_high(struct page *page)

将高内存页映射到内存中

参数

struct page *page

要映射的 struct page

描述

返回页的虚拟内存地址。

我们不能从中断中调用此函数,因为它可能会阻塞。

void *kmap_high_get(struct page *page)

将高内存页固定到内存中

参数

struct page *page

要固定的 struct page

描述

返回页的当前虚拟内存地址,如果不存在映射则返回 NULL。如果且仅当返回非 NULL 地址时,才必须调用相应的 kunmap_high()

这可以从任何上下文调用。

void kunmap_high(struct page *page)

将高内存页取消映射

参数

struct page *page

要取消映射的 struct page

描述

如果未定义 ARCH_NEEDS_KMAP_HIGH_GET,则只能从用户上下文调用此函数。

void *page_address(const struct page *page)

获取页的映射虚拟地址

参数

const struct page *page

要获取虚拟地址的 struct page

描述

返回页的虚拟地址。

void set_page_address(struct page *page, void *virtual)

设置页的虚拟地址

参数

struct page *page

要设置的 struct page

void *virtual

要使用的虚拟地址

kunmap_atomic

kunmap_atomic (__addr)

取消映射由 kmap_atomic() 映射的虚拟地址 - 已弃用!

参数

__addr

要取消映射的虚拟地址

描述

取消映射先前由 kmap_atomic() 映射的地址并重新启用页面错误。根据 PREEMP_RT 配置,还会重新启用迁移和抢占。用户不应依赖这些副作用。

映射应该以它们映射的相反顺序取消映射。有关嵌套的详细信息,请参阅 kmap_local_page()

__addr 可以是映射页内的任何地址,因此无需减去任何已添加的偏移量。与 kunmap() 不同,此函数采用从 kmap_atomic() 返回的地址,而不是传递给它的页。如果您传递页,编译器会发出警告。

kunmap_local

kunmap_local (__addr)

取消映射通过 kmap_local_page() 映射的页。

参数

__addr

映射的页内的地址

描述

__addr 可以是映射页内的任何地址。通常是 kmap_local_page() 返回的地址,但也可能包括偏移量。

取消映射应按照映射的相反顺序进行。有关详细信息,请参阅 kmap_local_page()