4. 内存管理:虚拟内存、物理内存、页表映射、内存分配与回收机制
内存管理,说白了就是操作系统怎么管好这块「地盘」。我刚开始接触鸿蒙内核时,第一感觉就是:这玩意儿比 Linux 的某些设计要「轻」得多,但该有的硬骨头一块不少。今天咱们就聊聊虚拟内存、物理内存、页表映射,还有分配与回收这些核心话题。
4.1 虚拟内存:给每个进程一个「假」世界
你想想看,如果所有进程都直接操作物理内存,那不乱套了?A 进程不小心写了个野指针,直接把 B 进程的数据给覆盖了。所以鸿蒙内核给每个进程都画了一个「大饼」——虚拟地址空间。
每个进程看到的地址,都是从 0 开始的连续空间。但实际上,这些地址是假的。鸿蒙内核在背后偷偷做了一件事:把虚拟地址映射到真正的物理地址上。我习惯把这个过程叫做「地址翻译」。
虚拟内存的好处很明显:
- 隔离性:进程之间互不干扰。你写你的,我写我的。
- 安全性:用户态程序没法直接碰内核空间。
- 灵活性:物理内存不够?没关系,换页出去就行。
核心要点:鸿蒙内核的虚拟地址空间布局,用户空间和内核空间是严格分开的。用户空间从 0x0 开始,内核空间通常在高地址区域。我个人建议你在写驱动时,一定要搞清楚当前代码跑在哪个特权级,否则一个非法访问就能让你蓝屏。
4.2 物理内存:真正的「硬通货」
虚拟地址再花哨,最终还是要落到物理内存上。鸿蒙内核对物理内存的管理,用的是「页」的概念。一页通常是 4KB,也有大页(2MB、1GB)的选项。
我在项目中遇到过一个问题:某个多媒体驱动申请了一大块连续物理内存,结果系统跑着跑着就 OOM 了。后来一查,是内存碎片化太严重。嗯,这里要注意:鸿蒙内核的物理内存分配器,默认用的是伙伴系统(Buddy System)。
伙伴系统的核心思想很简单:把内存按 2 的幂次分成块。比如 4KB、8KB、16KB……你申请 5KB,它给你 8KB 的块。这样分配快,回收也快,但会有内部碎片。
| 阶数 | 块大小 | 典型用途 |
|---|---|---|
| 0 | 4KB | 小对象、页表 |
| 1 | 8KB | 小缓冲区 |
| 4 | 64KB | 驱动 DMA 缓冲区 |
| 9 | 2MB | 大页映射、多媒体数据 |
避坑指南:我曾经在调试一个摄像头驱动时,发现申请 1MB 连续内存总是失败。后来发现是伙伴系统的最高阶数配置得太小。鸿蒙内核允许你通过设备树或 Kconfig 调整 MAX_ORDER,但别调太大,否则内存浪费严重。
4.3 页表映射:地址翻译的「地图」
虚拟地址怎么变成物理地址?靠页表。鸿蒙内核用的是多级页表,ARM64 架构下通常是 4 级页表(PGD、PUD、PMD、PTE)。
你想想看,如果只用一级页表,那得有多大?每个进程 4GB 虚拟空间,每页 4KB,就需要 100 万个页表项。每个页表项 8 字节,光页表就得 8MB。多级页表的好处是:按需分配,用多少建多少。
页表项里存了什么?除了物理页帧号(PFN),还有各种权限位:
- 可读/可写/可执行:控制访问权限
- 用户/内核:区分特权级
- 脏位/访问位:用于换页和回收
- 全局位:标记是否所有进程共享
// 鸿蒙内核中页表项的结构(简化版)
typedef struct {
uint64_t pfn : 40; // 物理页帧号
uint64_t reserved : 10; // 保留位
uint64_t global : 1; // 全局页
uint64_t dirty : 1; // 脏位
uint64_t accessed : 1; // 访问位
uint64_t user : 1; // 用户态可访问
uint64_t rw : 1; // 可读写
uint64_t present : 1; // 页是否存在
} pte_t;
我习惯在调试内存问题时,直接打印页表内容。鸿蒙内核提供了 dump_page_table() 接口,但需要开启调试选项。有一次我发现某个驱动映射的页表权限配错了,把只读配成了可执行,结果被 SMAP/SMEP 机制给拦下来了。嗯,这种问题查起来很费劲。
4.4 内存分配与回收:谁申请,谁释放
内存分配,说白了就是「谁申请,谁释放」。但现实往往没那么美好。鸿蒙内核提供了多种分配接口:
- kmalloc:分配小于一页的小块内存,基于 slab 分配器。
- vmalloc:分配虚拟地址连续、物理地址不连续的大块内存。
- dma_alloc_coherent:分配 DMA 用的连续物理内存。
我在项目中遇到过最头疼的问题就是内存泄漏。驱动加载后,每次调用某个 ioctl 就申请一块内存,但忘记释放了。系统跑个几天,内存就耗光了。鸿蒙内核有 kmemleak 机制可以检测,但默认没开。
警告:千万不要在中断上下文里调用 kmalloc(GFP_KERNEL)!因为 GFP_KERNEL 可能会触发睡眠等待。中断上下文里只能用 GFP_ATOMIC。我曾经因为这个 bug 导致系统随机死机,查了整整两天才找到原因。
内存回收机制,鸿蒙内核用的是「页面回收」+「OOM Killer」。当内存紧张时,内核会尝试回收:
- 先回收文件缓存页(干净页直接丢,脏页写回磁盘)。
- 再回收匿名页(换出到 swap 分区)。
- 如果还不行,就触发 OOM Killer,杀掉最「肥」的进程。
你想想看,OOM Killer 选进程是有算法的。它算一个「badness」分数,分数最高的进程被干掉。我建议你在写关键服务时,设置 oom_score_adj 为负值,避免被误杀。
4.5 实战经验:一次内存踩踏的排查
最后分享一个我亲身经历的案例。有个网络驱动,跑着跑着就报「kernel panic」,错误信息指向了 sk_buff 结构体被写坏。
我一开始怀疑是 DMA 地址错了,但检查了好几遍都没问题。后来用 slabinfo 查看 slab 分配器状态,发现 sk_buff 缓存池里有很多「红色区域」被踩了。嗯,红色区域是鸿蒙内核用来检测越界写的一种机制。
最终定位到:驱动在接收数据包时,计算缓冲区长度少算了 4 字节,导致后续数据写到了下一个 sk_buff 的头部。修复方法很简单:把长度计算加 4 就行。但排查过程花了我整整一个周末。
我的建议:写驱动时,一定要用 memset 初始化所有分配的内存,并且多用 BUG_ON 或 WARN_ON 做断言检查。鸿蒙内核的 KASAN(内核地址消毒剂)能帮你自动检测越界和 use-after-free,强烈建议在开发阶段开启。
好了,内存管理这块内容不少,但核心就是三件事:虚拟地址怎么来、物理地址怎么管、映射怎么建。你把这些搞清楚了,写驱动时遇到的内存问题基本都能迎刃而解。