深入Linux内存管理:从ioremap源码看VMALLOC区页表构建全过程
最近在调试一块新的ARM64开发板时,遇到了一个奇怪的问题:驱动里用ioremap映射的寄存器地址,在某个特定配置下访问会触发数据异常。排查了半天,最后发现是页表属性设置的问题。这件事让我重新审视了ioremap这个看似简单的接口——它背后隐藏着Linux内核虚拟内存管理中最精妙的设计之一。今天,我们就抛开表面的API调用,直接钻进内核源码,看看当你在驱动里写下ioremap(0x48000000, 0x1000)时,内核到底在背后做了什么。
这篇文章适合那些已经熟悉Linux驱动开发,但对内存子系统工作原理感到好奇的内核开发者。我们会聚焦在ARM64架构上,从虚拟地址空间的分配到页表项的填充,完整走一遍VMALLOC区的映射流程。你会发现,ioremap不仅仅是“获取一个虚拟地址”那么简单,它涉及虚拟内存区域的管理、红黑树的查找、页表层级的遍历,以及内存属性的精确控制。
1. VMALLOC区:内核的“动态映射”空间
在深入ioremap之前,我们需要先理解它操作的舞台——VMALLOC区。内核的虚拟地址空间被划分成几个主要区域,每个区域都有特定的用途。对于ARM64架构,典型的布局如下:
0xffff000000000000 - 0xffff7fffffffffff : vmalloc区域
0xffff800000000000 - 0xffffffffffffffff : 内核镜像和线性映射区域
VMALLOC区位于内核空间的高端,它的核心特点是虚拟地址连续,但背后的物理页面可以不连续。这与kmalloc使用的线性映射区域形成鲜明对比——线性映射区域的虚拟地址和物理地址保持着固定的偏移关系。
那么内核为什么需要VMALLOC区?主要有几个场景:
- 大块非连续物理内存的映射:比如DMA缓冲区,物理上可能是分散的,但驱动希望用连续的虚拟地址访问。
- 外设寄存器的映射:这正是
ioremap的主要用途,将物理地址空间中的设备寄存器映射到内核可以访问的虚拟地址。 - 内核模块的加载:模块的代码和数据需要被映射到内核空间,但物理位置不确定。
- vmalloc()函数:为用户态
malloc的内核版本,用于分配大块虚拟内存。
VMALLOC区的管理核心是两个数据结构:struct vm_struct和struct vmap_area。它们的关系有点像进程地址空间中的struct vm_area_struct和实际物理页面的关系——一个管“视图”,一个管“分配”。
// 简化的结构示意
struct vm_struct {
void *addr; // 虚拟地址起始
unsigned long size; // 区域大小
const void *caller; // 调用者信息
struct vmap_area *va; // 指向底层的管理单元
};
struct vmap_area {
unsigned long va_start; // 区域起始地址
unsigned long va_end; // 区域结束地址
struct rb_node rb_node; // 红黑树节点,用于地址查找
struct list_head list; // 链表节点,用于顺序遍历
struct vm_struct *vm; // 指向上层的vm_struct
};
vmap_area是VMALLOC区的“账本”,记录着哪些虚拟地址范围已经被占用。内核用红黑树来快速查找地址,用链表来维护地址的顺序。当你调用ioremap时,内核首先要做的就是在这个“账本”上找到一段空闲的虚拟地址范围。
2. 虚拟地址的寻址:get_vm_area_caller如何找到“空洞”
ioremap的第一步是调用get_vm_area_caller在VMALLOC区分配一段虚拟地址范围。这个过程的核心算法可以概括为:在已分配的地址区间中寻找足够大的“空洞”。
想象一下VMALLOC区就像一条很长的停车带,已经停了一些车(已分配的区域),我们需要找到一段足够长的空位来停新车。内核的查找策略相当聪明:
- 缓存优先:首先检查
free_vmap_cache。这是一个最近成功分配时记录的vmap_area节点,它的va_end地址之后很可能还有空间。如果缓存有效且满足要求,就直接从那里开始。 - 红黑树搜索:如果缓存不适用,就从红黑树的根节点开始,找到第一个结束地址大于等于目标地址的
vmap_area节点。 - 链表遍历:从这个节点开始,沿着链表向后遍历,检查每个已分配区域之间的“空洞”是否足够大。
- 地址对齐:找到的空洞地址需要按照请求的对齐要求进行调整(通常是页对齐)。
这里有个实际的查找示例。假设VMALLOC区当前的状态如下表所示:
| 已分配区域 | 起始地址 | 结束地址 | 大小 |
|---|---|---|---|
| 区域A | 0xffff00000000 | 0xffff00001000 | 4KB |
| 区域B | 0xffff00002000 | 0xffff00003000 | 4KB |
| 区域C | 0xffff00005000 | 0xffff00006000 | 4KB |
现在请求分配一个8KB的区域。查找过程会是:
- 从区域A之后开始(0xffff00001000),但到区域B之前只有4KB空间,不够。
- 从区域B之后开始(0xffff00003000),到区域C之前有8


1942

被折叠的 条评论
为什么被折叠?



