高内存处理

作者: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 的指针以及标识该页面在对开本中的字节偏移量。

    应该始终使用这些函数,而 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() 的调用之间的代码可能隐式地依赖于原子映射的副作用,即禁用页面错误或抢占,或两者。 在这种情况下,必须将对 pagefault_disable() 或 preempt_disable() 或两者的显式调用与 kmap_local_page() 一起使用。

    [旧文档]

    这允许单个页面的非常短时间的映射。 由于映射仅限于发出它的 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,则内核将尝试创建一个仅使用一些算法的映射,该算法会将页面结构地址转换为指向页面内容的指针,而不是映射about。 在这种情况下,取消映射操作可能为空操作。

如果未设置 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'ed 地址的页面

参数

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 在此虚拟地址的页面。它可能从高端内存或可移动区域分配。架构可以提供自己的实现。

返回

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

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

从 folio 复制一个字节范围。

参数

char *to

要复制到的内存。

struct folio *folio

要从中读取的 folio。

size_t offset

要读取的 folio 中的第一个字节。

size_t len

要复制的字节数。

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

将一个字节范围复制到 folio。

参数

struct folio *folio

要写入的 folio。

size_t offset

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

const char *from

要从中复制的内存。

size_t len

要复制的字节数。

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

将 folio 的尾部置零。

参数

struct folio *folio

要置零的 folio。

size_t offset

folio 中开始置零的字节偏移量。

void *kaddr

当前 folio 映射到的地址。

描述

如果您已经使用 kmap_local_folio() 来映射一个 folio,向它写入一些数据,现在需要将 folio 的末尾置零(并刷新 dcache),您可以使用此函数。如果您没有对 folio 进行 kmap(例如,folio 已被 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)

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

参数

struct folio *folio

目标 folio。

size_t offset

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

const char *from

要复制的数据。

size_t len

要复制的字节数。

描述

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

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

从文件 folio 复制一些字节。

参数

char *to

目标缓冲区。

struct folio *folio

要从中复制的 folio。

loff_t pos

文件中的位置。

size_t len

要复制的最大字节数。

描述

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

返回

从 folio 复制的字节数。

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

将 folio 中的两个字节范围置零。

参数

struct folio *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

要写入的 folio。

size_t start

要置零的第一个字节。

size_t xend

比要置零的最后一个字节大一。

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

将 folio 中的一个字节范围置零。

参数

struct folio *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() 返回的地址。

描述

通常,例如在目录处理中,对一个 folio 进行 kmap。此函数取消映射 folio 并删除被保持的引用计数,以在我们访问它时保持 folio 活动。

void *kmap_high(struct page *page)

将一个 highmem 页面映射到内存中

参数

struct page *page

要映射的 struct page

描述

返回页面的虚拟内存地址。

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

void *kmap_high_get(struct page *page)

将一个 highmem 页面固定到内存中

参数

struct page *page

要固定的 struct page

描述

返回页面的当前虚拟内存地址,如果不存在映射,则返回 NULL。如果且仅当返回一个非空地址时,才需要匹配调用 kunmap_high()

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

void kunmap_high(struct page *page)

将一个 highmem 页面取消映射到内存中

参数

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()