Linux 高内存 学习总结

在早期(特别是 32 位架构时代),Linux 内核面临着一个著名的挑战:如何高效地管理远大于线性地址空间大小的物理内存。例如,在 x86 架构上,32 位系统的内核通常只有 1GB 的线性地址空间(从 0xC0000000 到 0xFFFFFFFF)来映射和访问所有物理内存。当物理内存容量超过 1GB(例如 4GB 或更多)时,内核就无法一次性将全部物理内存线性地映射到自己的地址空间。为了解决这个问题,Linux 内核引入了 高内存 的概念。

本篇博客将深入探讨高内存的由来、工作机制、相关 API 以及在现代系统中的地位,旨在为内核开发者和爱好者提供一份全面的学习总结。

目录#

  1. 背景:为什么需要高内存?

    • 32 位系统的地址空间限制
    • 内核空间与用户空间的分割
    • 物理内存超过 1GB 时的问题
  2. 高内存与低内存

    • 低内存:永久内核映射区
    • 高内存:动态映射区
    • ZONE_NORMAL, ZONE_HIGHMEM 和 ZONE_DMA
  3. 高内存的映射机制

    • 临时映射:kmap_atomic
      • 原理与用途
      • 最佳实践与示例
    • 永久映射:kmap
      • 原理与用途
      • 与 kmap_atomic 的对比
  4. 高内存的使用场景与 API

    • 何时需要处理高内存?
    • 常用函数详解:kmap, kunmap, kmap_atomic, kunmap_atomic
    • 示例:在高内存页上进行操作
  5. 64 位系统上的高内存

    • 为什么 64 位系统通常不需要高内存?
    • ZONE_HIGHMEM 在 64 位系统中的角色
  6. 常见问题与最佳实践

    • 性能考量
    • 避免常见的错误用法
    • 调试技巧
  7. 总结

  8. 参考


1. 背景:为什么需要高内存?#

在 32 位 x86 架构上,线性地址空间为 4GB(2^32)。Linux 内核默认采用经典的 3:1 分割模型:

  • 用户空间: 0 - 3GB(0x00000000 到 0xBFFFFFFF)
  • 内核空间: 3GB - 4GB(0xC0000000 到 0xFFFFFFFF)

这意味着内核只有 1GB 的虚拟地址空间。在这 1GB 中,需要映射很多东西:

  • 内核本身的代码和数据。
  • 所有物理内存的直接映射,即物理地址 0x0 对应虚拟地址 0xC0000000,物理地址 0x1000 对应虚拟地址 0xC0001000,以此类推。
  • vmalloc 分配的区域、持久映射区、固定映射区等。

问题来了:如果物理内存只有 1GB,内核可以轻松地将其全部直接映射到自己的 1GB 地址空间中。但如果物理内存是 4GB 呢?内核的 1GB 地址空间不足以同时容纳 4GB 的物理内存映射、内核代码和其他区域。

解决方案:内核只将一部分物理内存(通常是 896MB)进行永久、线性的直接映射。这部分内存称为低内存。剩余的物理内存(4GB - 896MB)称为高内存,它不会被永久映射到内核地址空间,只有在需要时,才通过特定的机制临时映射到内核地址空间的一个小窗口里。

2. 高内存与低内存#

低内存#

  • 定义: 在 32 位系统中,被内核直接、永久映射到线性地址空间的物理内存。通常指物理地址低于约 896MB 的内存。
  • 虚拟地址: 有一个简单的线性转换关系:虚拟地址 = 物理地址 + PAGE_OFFSET(PAGE_OFFSET 通常是 0xC0000000)。
  • 访问: 内核可以通过直接指针解引用来访问低内存页,非常快速。
  • 内存域: 对应 ZONE_NORMAL 和 ZONE_DMA。

高内存#

  • 定义: 物理地址高于 896MB 的内存,无法被内核直接永久映射。
  • 访问: 必须通过特殊的函数(如 kmap, kmap_atomic)先将其映射到内核地址空间预留的“窗口”中,然后才能访问。访问完毕后需要解除映射。
  • 内存域: 对应 ZONE_HIGHMEM。

内存管理区#

内核将物理内存划分为三个主要区域:

  • ZONE_DMA: 包含可用于 DMA 操作的低端内存页(通常 <16MB)。
  • ZONE_NORMAL: 包含能被正常映射的低内存页(通常 16MB ~ 896MB)。
  • ZONE_HIGHMEM: 包含高内存页(>896MB)。

在 /proc/zoneinfo 中可以查看这些区域的信息。

3. 高内存的映射机制#

内核提供了两种主要机制来映射高内存页。

临时映射:kmap_atomic#

这是一种非常高效的高内存映射方式,也是首选方法。

  • 原理: 它利用每个 CPU 独有的一个很小的映射窗口(通常一页或几页)。当调用 kmap_atomic 时,内核会固定该页,并在当前 CPU 的映射窗口中找到一個槽位,建立映射。由于是每个 CPU 独立的,因此不会引起 CPU 之间的 TLB 冲刷,速度极快。
  • 特性:
    • 原子性: 在原子上下文中使用是安全的。
    • 抢占禁用: 在执行映射的代码段中,内核抢占被禁用。
    • 隐式失效: 下一个 kmap_atomic 调用可能会覆盖当前 CPU 窗口中的上一个映射。因此,映射必须在同一个上下文中(例如,一个函数内)进行和解除。
  • 用途: 用于需要短暂、快速访问高内存页的场景,例如页错误处理、磁盘 I/O 完成操作等。

永久映射:kmap#

这是一种较老、较慢的映射方式。

  • 原理: 它使用内核地址空间中一块固定的、有限的地址池(永久内核映射区)。kmap 会为指定的高内存页在这个池中分配一个槽位并建立映射。这个映射会一直有效,直到显式调用 kunmap 解除。
  • 特性:
    • 睡眠可能: 如果映射池耗尽,kmap 可能会阻塞(睡眠),等待一个槽位被释放。因此不能在原子上下文中使用。
    • 映射持久: 映射会一直保持,直到调用 kunmap。
  • 用途: 现在很少使用,主要用于需要长期保持映射的古老代码中。在新代码中,kmap_atomic 几乎是唯一的选择。

4. 高内存的使用场景与 API#

何时需要处理高内存?#

当你编写的内核代码(如设备驱动程序)需要处理来自用户空间或文件系统的页缓存时,你获得的 struct page 指针可能指向一个高内存页。在直接访问该页的内容之前,你必须先将其映射到内核地址空间。

常用函数详解#

#include <linux/highmem.h>
 
void *kmap(struct page *page);
void kunmap(struct page *page);
 
void *kmap_atomic(struct page *page);
void kunmap_atomic(void *vaddr);
  • kmap(struct page *page)

    • 如果 page 位于低内存,则直接返回其对应的内核虚拟地址。
    • 如果 page 位于高内存,则将其映射到永久内核映射区,返回虚拟地址。
    • 可能睡眠。
  • kunmap(struct page *page)

    • 解除由 kmap 建立的映射。
  • kmap_atomic(struct page *page)

    • 高效地将高内存页映射到当前 CPU 的临时窗口。
    • 不会睡眠,但会禁用抢占。
    • 对于低内存页,同样直接返回虚拟地址。
  • kunmap_atomic(void *vaddr)

    • 解除由 kmap_atomic 建立的映射。注意参数是虚拟地址 vaddr,而不是 struct page。
    • 会重新启用抢占。

示例:在高内存页上进行操作#

以下代码片段展示了如何安全地访问一个可能位于高内存的页。

/* 假设我们有一个指向某内存页的指针 struct page *page */
char *vaddr;
 
/* 使用 kmap_atomic 进行临时映射 */
vaddr = kmap_atomic(page);
 
/* 现在可以通过 vaddr 安全地访问页内容 */
memcpy(vaddr, src_data, PAGE_SIZE);
/* 或者执行其他操作 */
 
/* 操作完成后,立即解除映射 */
kunmap_atomic(vaddr);
 
/* 注意:不要在这之后继续使用 vaddr! */

最佳实践:

  • 优先使用 kmap_atomic: 因为它性能更高。
  • 映射范围最小化: 保持映射的时间尽可能短,不要在映射期间调用可能引起睡眠的函数。
  • 配对使用: 确保每个 kmap/kmap_atomic 都有对应的 kunmap/kunmap_atomic,防止资源泄漏。

5. 64 位系统上的高内存#

在 64 位系统(如 x86_64 或 ARM64)上,情况发生了根本性变化。64 位系统的虚拟地址空间极其巨大(2^64 bytes),远大于任何可想象的物理内存容量。

因此,内核可以轻松地将所有物理内存都进行直接、线性的永久映射,而无需担心地址空间耗尽。例如,在 x86_64 上,内核地址空间是 128TB,足以映射海量物理内存。

结论:在绝大多数 64 位系统上,ZONE_HIGHMEM 区域是空的,甚至不被编译进内核。所有物理内存都属于 ZONE_NORMAL 或 ZONE_DMA。内核代码可以直接通过 page_address(page) 或类似机制获得任何物理页的内核虚拟地址,无需再调用 kmap 系列函数。

然而,一些特殊架构(如 32 位 ARM 的 LPAE)或某些有特殊内存布局的 64 位系统可能仍会使用高内存概念。

6. 常见问题与最佳实践#

  • 性能考量:

    • kmap_atomic 非常快,但频繁映射/解映射仍有开销。
    • 如果可能,尽量在低内存区域进行高性能的内核操作(例如,通过 GFP_KERNEL 或 GFP_ATOMIC 标志分配的内存通常来自低内存区)。
  • 避免常见错误:

    • 在原子上下文使用 kmap: 这会导致内核崩溃,因为 kmap 可能睡眠。
    • 忘记调用 kunmap: 会导致映射槽位泄漏,最终使系统无可用映射窗口。
    • 在 kunmap_atomic 后使用映射地址: 解除映射后,该虚拟地址不再有效,使用它会导致内存 corruption。
  • 调试技巧:

    • 使用 CONFIG_DEBUG_KMAP 等内核调试选项来检测高内存映射的使用错误。
    • 注意内核 oops 信息中与高内存相关的调用栈。

7. 总结#

  • 高内存是 32 位 Linux 内核为解决物理内存大于线性地址空间而设计的一种精巧机制。
  • 它将物理内存分为可直接访问的低内存和需要动态映射的高内存。
  • 映射高内存主要通过 kmap(慢,可阻塞)和 kmap_atomic(快,原子性)两种 API 实现。
  • 在现代 64 位系统中,高内存的概念基本消失,所有物理内存均可被内核直接映射。
  • 对于现代内核开发者,理解高内存有助于维护旧代码和深入理解内存管理,但在编写新驱动时,通常无需担心高内存问题,除非目标平台是 32 位系统。

8. 参考#

  1. Linux Kernel Documentation: Documentation/vm/highmem.txt
  2. 经典书籍:
    • Understanding the Linux Kernel, 3rd Edition - Daniel P. Bovet & Marco Cesati
    • Linux Kernel Development, 3rd Edition - Robert Love
  3. 内核源码: linux/include/linux/highmem.h 和 linux/mm/highmem.c
  4. LWN.net 文章: