Linux 高内存 学习总结
在早期(特别是 32 位架构时代),Linux 内核面临着一个著名的挑战:如何高效地管理远大于线性地址空间大小的物理内存。例如,在 x86 架构上,32 位系统的内核通常只有 1GB 的线性地址空间(从 0xC0000000 到 0xFFFFFFFF)来映射和访问所有物理内存。当物理内存容量超过 1GB(例如 4GB 或更多)时,内核就无法一次性将全部物理内存线性地映射到自己的地址空间。为了解决这个问题,Linux 内核引入了 高内存 的概念。
本篇博客将深入探讨高内存的由来、工作机制、相关 API 以及在现代系统中的地位,旨在为内核开发者和爱好者提供一份全面的学习总结。
目录#
-
背景:为什么需要高内存?
- 32 位系统的地址空间限制
- 内核空间与用户空间的分割
- 物理内存超过 1GB 时的问题
-
高内存与低内存
- 低内存:永久内核映射区
- 高内存:动态映射区
ZONE_NORMAL,ZONE_HIGHMEM和ZONE_DMA
-
高内存的映射机制
- 临时映射:
kmap_atomic- 原理与用途
- 最佳实践与示例
- 永久映射:
kmap- 原理与用途
- 与
kmap_atomic的对比
- 临时映射:
-
高内存的使用场景与 API
- 何时需要处理高内存?
- 常用函数详解:
kmap,kunmap,kmap_atomic,kunmap_atomic - 示例:在高内存页上进行操作
-
64 位系统上的高内存
- 为什么 64 位系统通常不需要高内存?
ZONE_HIGHMEM在 64 位系统中的角色
-
常见问题与最佳实践
- 性能考量
- 避免常见的错误用法
- 调试技巧
-
总结
-
参考
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. 参考#
- Linux Kernel Documentation:
Documentation/vm/highmem.txt - 经典书籍:
- Understanding the Linux Kernel, 3rd Edition - Daniel P. Bovet & Marco Cesati
- Linux Kernel Development, 3rd Edition - Robert Love
- 内核源码:
linux/include/linux/highmem.h和linux/mm/highmem.c - LWN.net 文章: