3、直接内存映射区(ZONE_NORMAL/DMA):线性映射的原理,物理内存与虚拟地址的固定偏移关系

好,咱们今天聊一个内核里最基础、也最容易被忽视的概念——直接内存映射区。说白了,就是内核怎么把物理内存“搬”到虚拟地址空间里,而且搬得特别直、特别快。

我个人习惯把这块叫做“内核的直通车”。为什么?因为一旦你理解了它,很多内存分配、DMA传输、页表操作的坑,你一眼就能看穿。

3.1 什么是线性映射?

线性映射,也叫直接映射。它的核心思想很简单:虚拟地址 = 物理地址 + 固定偏移

你想想看,CPU访问内存时用的是虚拟地址。但硬件(比如DMA控制器)只认物理地址。内核怎么协调?最粗暴也最高效的办法,就是让虚拟地址和物理地址之间保持一个固定的数学关系。

在32位系统上,这个偏移通常是 0xC0000000。比如物理地址 0x100000,映射到内核空间就是 0xC0100000。嗯,就是这么简单。

核心公式:

虚拟地址 = 物理地址 + PAGE_OFFSET
// 在x86 32位上,PAGE_OFFSET = 0xC0000000

我在项目中遇到过一个问题:某块ARM开发板,内核启动后死活访问不了高地址内存。查了半天,发现是线性映射区只覆盖了低896MB,高地址那块压根没映射。这就是典型的“偏移关系没搞对”。

3.2 ZONE_NORMAL 和 ZONE_DMA 的区别

直接映射区里,又分两个区域:ZONE_NORMALZONE_DMA。很多人以为它们只是名字不同,其实背后的硬件限制才是关键。

特性 ZONE_DMA ZONE_NORMAL
物理地址范围 0 - 16MB(x86) 16MB - 896MB(x86 32位)
主要用途 DMA传输、老设备兼容 常规内核内存分配
映射方式 线性映射 线性映射
访问限制 某些ISA设备只能访问此区域 无特殊限制

为什么会这样?因为早期的ISA总线设备,DMA控制器只有24位地址线,最多寻址16MB。你给它一个高于16MB的物理地址,它直接罢工。我刚开始做驱动时,就踩过这个坑——分配了一个DMA缓冲区,结果设备死活不干活,打印出来一看,地址在0x20000000。嗯,从那以后我再也不敢忽略ZONE_DMA了。

3.3 线性映射的优缺点

任何设计都有取舍。线性映射也不例外。

优点

  • 访问速度快:不需要查页表,直接加减偏移就行。内核里大量使用 __va()__pa() 宏,就是干这个的。
  • 实现简单:页表项在初始化时一次性建立,运行时几乎不需要修改。
  • 缓存友好:连续的虚拟地址对应连续的物理地址,TLB命中率高。

缺点

  • 浪费虚拟地址空间:32位系统上,内核只有1GB虚拟空间,线性映射就占了896MB。剩下的128MB要分给vmalloc、固定映射等。
  • 无法映射超过896MB的物理内存:这就是著名的“896MB瓶颈”。超过的部分,内核得用其他机制(如高端内存)来处理。
  • 碎片化问题:物理内存碎片会导致线性映射区无法分配大块连续内存。

避坑指南:

我曾经在调试一个网络驱动时,发现skb的数据缓冲区总是分配失败。后来用 get_free_pages(GFP_DMA, order) 才搞定。记住:如果你的设备需要DMA,分配内存时一定要指定 GFP_DMA 标志,否则可能拿到ZONE_NORMAL的地址,设备根本访问不了。

3.4 核心API:__va 和 __pa

这两个宏是线性映射的灵魂。内核里到处都在用。

// 物理地址转虚拟地址
static inline void *__va(unsigned long x)
{
    return (void *)(x + PAGE_OFFSET);
}

// 虚拟地址转物理地址
static inline unsigned long __pa(void *x)
{
    return (unsigned long)(x - PAGE_OFFSET);
}

注意:这两个宏只适用于直接映射区。如果你传进去一个vmalloc分配的地址,结果就是灾难。我见过一个同事,把vmalloc返回的地址传给__pa,结果算出来的物理地址完全不对,导致系统随机崩溃。嗯,调试了两天才找到原因。

3.5 64位系统下的变化

到了64位系统,情况就舒服多了。虚拟地址空间大到用不完,线性映射区可以覆盖所有物理内存。比如x86_64上,PAGE_OFFSET是 0xffff880000000000,直接映射区可以覆盖整个物理地址空间。

所以64位系统上,你基本不用担心“896MB瓶颈”的问题。但原理是一样的:虚拟地址 = 物理地址 + 固定偏移。

注意事项:

虽然64位系统没有896MB限制,但线性映射区仍然不是万能的。比如某些设备需要非缓存(non-cacheable)的内存映射,或者需要映射到特定物理地址,这时候还得用ioremap或固定映射。线性映射只解决“常规内存”的访问问题。

3.6 一张图看懂直接映射

下面我用SVG画了一张图,帮你理清整个逻辑。你盯着看30秒,应该就能明白。

直接内存映射区结构图 虚拟地址空间 (3GB+ 内核空间) 线性映射区 (ZONE_NORMAL + ZONE_DMA) 虚拟地址 = 物理地址 + PAGE_OFFSET 范围: 0xC0000000 ~ 0xF7FFFFFF 对应物理: 0 ~ 896MB ZONE_DMA (0 ~ 16MB) 用于DMA传输,老设备兼容 其他内核区域 vmalloc区、固定映射区 高端内存映射区等 物理地址空间 物理内存 (0 ~ 896MB) 通过线性映射直接访问 物理 ZONE_DMA (0 ~ 16MB) 高端内存 (> 896MB) 需要动态映射才能访问 通过kmap/vmalloc等机制 线性映射 +PAGE_OFFSET 动态映射 需要页表操作 注:32位系统典型布局,64位系统线性映射区可覆盖全部物理内存

这张图里,左边是虚拟地址空间,右边是物理地址空间。中间的绿色箭头就是线性映射——直接加一个偏移量。红色虚线箭头表示高端内存需要动态映射,那是另一回事了。

3.7 实战:如何查看当前系统的线性映射

你可以用 /proc/iomem/proc/vmallocinfo 来查看。但最直接的方式是看内核日志:

# dmesg | grep -i "memory"
[    0.000000] Memory: 1024MB = 1024MB total
[    0.000000] Memory: 896MB of linear mapping available
[    0.000000] DMA zone: 4096 pages, LIFO batch:0
[    0.000000] Normal zone: 225280 pages, LIFO batch:31

看到没?896MB的线性映射区,DMA zone只有16MB。这就是你系统的直接内存映射现状。

小技巧:

如果你想在代码里判断一个地址是否属于线性映射区,可以用 virt_addr_valid() 宏。它检查虚拟地址是否在 PAGE_OFFSEThigh_memory 之间。我在写内存调试工具时经常用这个。

3.8 总结

直接内存映射区,说白了就是内核给自己留的一条“快速通道”。它用固定的偏移关系,把物理内存映射到虚拟地址空间,让内核能高效访问。ZONE_DMA和ZONE_NORMAL都在这个区域里,区别只在于物理地址范围不同。

记住三点:

  • 线性映射 = 虚拟地址 = 物理地址 + 固定偏移
  • ZONE_DMA用于DMA,ZONE_NORMAL用于常规分配
  • 64位系统没有896MB限制,但原理一样

嗯,这块内容就到这里。下次你写驱动分配内存时,记得想想你的设备需不需要ZONE_DMA。别像我当年那样,踩了坑才回头补课。


专注资料整理