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_NORMAL 和 ZONE_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秒,应该就能明白。
这张图里,左边是虚拟地址空间,右边是物理地址空间。中间的绿色箭头就是线性映射——直接加一个偏移量。红色虚线箭头表示高端内存需要动态映射,那是另一回事了。
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_OFFSET 到 high_memory 之间。我在写内存调试工具时经常用这个。
3.8 总结
直接内存映射区,说白了就是内核给自己留的一条“快速通道”。它用固定的偏移关系,把物理内存映射到虚拟地址空间,让内核能高效访问。ZONE_DMA和ZONE_NORMAL都在这个区域里,区别只在于物理地址范围不同。
记住三点:
- 线性映射 = 虚拟地址 = 物理地址 + 固定偏移
- ZONE_DMA用于DMA,ZONE_NORMAL用于常规分配
- 64位系统没有896MB限制,但原理一样
嗯,这块内容就到这里。下次你写驱动分配内存时,记得想想你的设备需不需要ZONE_DMA。别像我当年那样,踩了坑才回头补课。