一、不连续页分配器
1.系统接口
-
不连续页分配器提供的接口如下:
void *vmalloc(unsigned long size);
分配不连续的物理页并且把物理页映射到连续的虚拟地址空间。void vfree(const void *addr);
释放vmalloc
分配的物理页和虚拟地址空间。void *vmap(struct type **pages, unsigned int count, unsigned long flags, pgprot_t prot);
把已经分配的不连续物理页映射到连续的虚拟地址空间。void vunmap(const void *addr);
释放使用vmap
分配的虚拟地址空间。
-
内核提供接口函数
void *kvmalloc(size_t size, gfp_t flags);
首先尝试使用kmalloc
分配内存块,若失败则使用vmalloc
函数分配不连续的物理页。void kvfree(const void *addr);
若内存块由vmalloc
分配,则使用vfree
释放;否则使用kfree
释放。
Linux 中常用内存分配函数:
用户空间:
malloc/calloc/realloc/free
:不保证物理连续,大小受堆申请限制,单位为字节。场景:calloc
初始化为 0,realloc
改变内存大小。mmap/munmap
:场景为将文件利用虚拟内存技术映射到内存。brk/sbrk
:场景是虚拟内存到内存的映射。内核空间:
vmalloc/vfree
:虚拟连续、物理不连续,大小受vmalloc
区限制,单位为页(vmalloc
区域)。场景:可能睡眠,不能从中断上下文或不允许阻塞的场景中调用。- slab(
kmalloc/kcalloc/krealloc/kfree
):物理连续,大小限制为 64b-4MB,单位为 2order 字节(Normal 区域)。场景:大小有限,适合固定大小数据的频繁分配释放,通过kmem_cache_create
管理,分配从缓存池获取,释放时不一定真正释放内存,由 slab 管理。- 伙伴系统:
__get_free_pages/get_free_pages
:物理连续,4MB(1024 页),单位为页(Normal 区域)。场景:__get_free_pages
限定不能使用HIGHMEM
。alloc_page/alloc_pages/free_pages
:物理连续,4MB,单位为页(Normal/Vmalloc 区域均可)。场景:配置定义最大页面数 211,一次最多分配 1024 页。
2.内核源码数据结构
- 每个虚拟内存区域对应一个
vmap_area
实例; - 每个
vmap_area
实例关联一个vm_struct
实例。
以下是 vmalloc
分配内存时的详细流程和关键数据结构的关系:
1.
vmalloc
的核心流程当调用
vmalloc(size)
时,内核会完成以下步骤:步骤 1:分配虚拟地址空间
内核通过
vmap_area
管理器(全局红黑树)找到一个大小合适的 空闲虚拟地址区域。创建一个新的
struct vmap_area
,记录该区域的起始和结束虚拟地址(va_start
和va_end
)。步骤 2:分配物理内存页
内核根据请求的大小,分配 多个物理页(通过
alloc_page()
或alloc_pages()
)。这些物理页可能 物理地址不连续,但会被分配到一个 `struct page 数组** 中。
步骤 3:建立映射关系
创建一个
struct vm_struct
,记录以下信息:
虚拟地址范围(
addr
字段,对应vmap_area
的va_start
)。物理页数组(
pages
字段,指向分配的struct page
数组)。区域类型(
flags
字段,例如VM_ALLOC
表示vmalloc
分配)。将
vmap_area
的vm
字段指向这个vm_struct
,建立两者的关联。步骤 4:建立页表映射
内核通过 页表 将虚拟地址(
vm_struct->addr
)映射到物理页(vm_struct->pages
中的页)。对于每个物理页,内核修改页表项,设置虚拟地址到物理地址的映射关系。
2. 关键数据结构的关系
vmap_area
:负责管理内核虚拟地址空间的分配,记录虚拟地址范围。
vm_struct
:描述具体的内存区域,记录虚拟地址和物理页数组。
struct page
:表示物理内存页,通过vm_struct->pages
数组管理。关系示意图:
vmap_area (管理虚拟地址) │└──→ vm_struct (关联虚拟地址和物理页) │└──→ pages[] (物理页数组) │├──→ page 0 ├──→ page 1 └──→ ...
3. 如何通过
vm_struct
找到物理内存?
从虚拟地址到
vm_struct
:
给定一个
vmalloc
返回的虚拟地址addr
,内核可以通过遍历vmap_area
的红黑树,找到对应的vmap_area
。通过
vmap_area->vm
字段获取关联的vm_struct
。从
vm_struct
到物理页:
vm_struct->pages
是一个struct page*
数组,每个元素指向一个物理页。通过
vm_struct->pages[i]
可以获取第i
个物理页的struct page
。从
struct page
到物理地址:
通过
page_to_phys(vm_struct->pages[i])
可以获取该物理页的 物理地址。
4. 示例:访问
vmalloc
分配的物理内存假设通过
void *ptr = vmalloc(4096 * 4);
分配了 16KB 内存:
虚拟地址分配:
vmap_area
记录va_start=0xffff888800000000
,va_end=0xffff888800004000
。物理页分配:
分配 4 个物理页(假设物理地址为
0x1000
,0x5000
,0x9000
,0xd000
)。
vm_struct
关联:
vm_struct->addr = 0xffff888800000000
。
vm_struct->pages[0]
指向物理页0x1000
的struct page
。
vm_struct->pages[1]
指向物理页0x5000
的struct page
,依此类推。页表映射:
虚拟地址
0xffff888800000000
映射到物理地址0x1000
。虚拟地址
0xffff888800001000
映射到物理地址0x5000
,依此类推。
5. 物理内存的非连续性
vmalloc
的物理页可能不连续:这是vmalloc
的核心特点!它适用于需要 大块连续虚拟地址,但物理内存可以零散的场景。
kmalloc
vs.vmalloc
:
kmalloc
分配的物理内存是连续的(虚拟和物理均连续)。
vmalloc
分配的物理内存不保证连续(虚拟连续,物理不连续)。
6. 释放内存(
vfree
)的流程
通过虚拟地址找到对应的
vmap_area
。通过
vmap_area->vm
获取vm_struct
。释放
vm_struct->pages
中的所有物理页。解除虚拟地址的页表映射。
删除
vmap_area
和vm_struct
。
其中,即使已经通过 struct page
分配了物理内存,也必须设置页表映射,因为 CPU 访问内存时只能通过 虚拟地址,而虚拟地址到物理地址的转换需要依赖页表。以下是详细的解释:
1. 核心原因:CPU 只认虚拟地址
现代 CPU 的指令和硬件设计决定了:
所有内存访问都基于虚拟地址:无论是用户程序还是内核代码,CPU 执行的指令中涉及的地址都是虚拟地址。
页表是地址翻译的硬件依赖:CPU 的 MMU(内存管理单元)通过页表将虚拟地址转换为物理地址,没有页表映射,虚拟地址无法被正确翻译。
即使内核也需要虚拟地址:内核代码和数据虽然运行在高特权级,但依然需要通过虚拟地址访问内存。
2.
struct page
只是物理内存的描述符
struct page
的作用:记录物理页的元数据(如引用计数、状态等),但它本身不提供访问物理内存的路径。物理内存的访问必须通过虚拟地址:内核需要通过虚拟地址才能读写物理内存,而虚拟地址必须通过页表映射到物理地址。
3. 以
vmalloc
为例的完整流程(1) 分配物理页(
struct page
)
内核调用
alloc_page()
分配物理页,获得struct page*
。此时物理页已存在,但没有虚拟地址可以访问它。
(2) 分配虚拟地址(
vmalloc
)
vmalloc
从内核虚拟地址空间分配一段连续的虚拟地址范围(由vmap_area
管理)。此时虚拟地址范围已分配,但未与物理页关联。
(3) 建立页表映射
内核将虚拟地址范围与物理页通过页表映射关联:
for (每个分配的物理页) {虚拟地址 = vmalloc分配的起始地址 + i * PAGE_SIZE;物理地址 = page_to_phys(pages[i]);修改页表项,将虚拟地址映射到物理地址; }
只有完成这一步,才能通过虚拟地址访问物理内存。
4. 如果不设置页表会发生什么?
访问虚拟地址会触发缺页异常(Page Fault):CPU 发现页表中没有对应的物理地址映射。
内核无法处理这种异常:因为
vmalloc
分配的虚拟地址没有关联物理页,缺页处理程序无法自动修复映射。结果:访问未映射的虚拟地址会导致内核崩溃(如 "Unable to handle kernel paging request")。
5. 页表映射的本质
页表是硬件与软件的桥梁:
硬件依赖:CPU 的 MMU 必须通过页表完成地址翻译。
软件管理:内核负责维护页表,确保虚拟地址与物理地址的映射关系正确。
即使内核知道物理地址,也必须通过虚拟地址访问内存:
// 示例:通过虚拟地址访问物理内存 char *vaddr = vmalloc(4096); // 返回虚拟地址 strcpy(vaddr, "Hello"); // 通过虚拟地址写入数据(依赖页表映射)
6. 例外情况:直接物理地址访问
某些特殊场景(如设备驱动)可能需要直接操作物理地址,但仍需间接映射:
ioremap
:将设备的物理地址映射到内核虚拟地址空间。
phys_to_virt
:在内核线性映射区域中,将物理地址转换为固定的虚拟地址(仅适用于低端内存)。但这些都是通过隐式或显式的页表映射实现的。
3.技术原理
malloc 虚拟地址空间的范围是(VMALLOC_START, VMALLOC_END),每种处理器架构都需要定义这两个宏,比如:ARM64 架构定义的宏如下:
MODULES_END 是内核模块区域的结束地址;PAGE_OFFSET 是线性映射区域的起始地址;PUD_SIZE 是一个页上层目录表项映射的地址空间长度;VMEMMAP_SIZE 是 vmemmap 区域的长度。
vmalloc 虚拟地址空间的起始地址 = 内核模块区域的结束地址
vmalloc 虚拟地址空间的结束地址 = 线性映射区域的起始地址 - 一个页上层目录表项映射的地址空间长度 - vmemmap 区域的长度 - 64KB
vmalloc 函数执行过程分为三步:
1、分配虚拟内存区域
2、分配物理页
3、在内核的页表中把虚拟页映射到物理页
备注:函数vmap和vmalloc的区别在于不需要分配物理页。
一、
vmalloc
虚拟地址空间起始地址设定原因
- 内核内存布局规划
内核空间中,不同区域承担不同功能。MODULES_END
标识内核模块区域的结束,模块加载后,后续地址空间可用于其他动态内存分配场景。vmalloc
用于分配物理地址不连续、虚拟地址连续的内存,将其起始地址设为内核模块区域结束地址,既能充分利用内存空间,又能保证模块区与vmalloc
区互不干扰,维持内核内存布局的清晰性。二、
vmalloc
虚拟地址空间结束地址设定原因
- 避开线性映射区域
PAGE_OFFSET
是线性映射区域(直接映射物理内存的虚拟地址区域)的起始地址。vmalloc
区域不能与线性映射区重叠,因此需从PAGE_OFFSET
向前推算结束地址。- 为其他内存管理结构预留空间
PUD_SIZE
:页上层目录表项(如PUD
,页全局目录的上一级目录)映射的地址空间长度。内核页表层级管理需要预留这类结构的空间,避免地址冲突。VMEMMAP_SIZE
:vmemmap
区域用于管理内存页的元数据(如页状态、引用计数等),需在内核地址空间中占据固定区域,因此计算时要减去该长度。- 64KB:通常是为地址对齐、边界保护或其他内核内部机制预留的缓冲空间,确保地址划分的严谨性,避免因边界误差导致内存管理错误。
三、整体布局意义
这种地址计算方式是内核内存管理的系统性设计:既保证
vmalloc
区域与内核模块区、线性映射区、内存元数据管理区等功能模块的地址隔离,又通过精确计算实现内存空间的高效利用,最终确保内核内存管理的稳定性和功能性。
二、页表
页表是一种特殊数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。每一个进程都有自己的页表,PCB表中有指针指向页表。
分页逻辑地址 = P 页号.D 页内位移,分页物理地址 = F 页帧号.D 页内位移。
P = 线性逻辑地址 / 页面大小,D = 线性逻辑地址 - P * 页面大小。
CPU 不直接访问物理内存地址,而是通过虚拟地址空间间接访问。虚拟地址空间是操作系统为执行进程分配的逻辑地址(如 32 位系统范围 0 - 4G - 1),操作系统在虚拟地址空间与物理内存地址间建立映射。
通常将虚拟地址空间以 512byte - 8K 为单位(称页,从 0 编号,该单位大小即页面),物理地址按同样大小为单位(称框或块,从 0 编号),OS 维护表记录页与框的映射关系。Windows 系统页面大小为 4KB。
系统为每个进程建立一个页表,在进程逻辑地址空间中每一页,依次在页表中有一个表项,记录该页对应的物理块号。通过查找页表就可以很容易地找到该页在内存中的位置。页表具有逻辑地址到物理地址映射作用。
【ARM处理器页表】
Linux 内核把页表直接分为 4 级:页全局目录(PGD)、页上层目录(PUD)、页中间目录(PMD)、直接页表(PT)。如果选择三级(页全局目录、页中间目录、直接页表)。如果选择二级(页全局目录和直接页表)。
五级页表的结构,每个进程有独立的页表,进程的 mm_struct 实例成员 pgd 指向页全局目录。前面四级页表的表项存放下一级页表的起始地址,直接页表的表项存放页帧号(PFN)。
查询页表,把虚拟地址转换成物理地址流程:
- 根据页全局目录的起始地址和页全局目录索引得到页全局目录表项的地址,然后再从表项得到页四级目录的起始地址;
- 根据页四级目录的起始地址和页四级目录索引得到页四级目录表项的地址,然后从表项得到页上层目录的起始地址;
- 根据页上层目录的起始地址和页上层目录索引得到页上层目录表项的地址,然后从表项得到页中间目录的起始地址;
- 根据页中间目录的起始地址和页中间目录索引得到页中间目录表项的地址,然后从表项得到直接页表的起始地址;
- 根据直接页表的起始地址和直接页表索引得到页表项的地址,然后从表项得到页帧号;
- 把页帧号和页内偏移组合形成物理地址。
更多细节见:
四级页表与五级页表_arm64支持几级页表-CSDN博客
https://github.com/0voice