虚拟地址空间布局架构
一、内存管理架构
1.Linux内核整体架构以及子系统
内存管理子系统架构分为用户空间、内核空间及硬件部分 3 个层面:
- 用户空间:应用程序使用
malloc()
申请内存资源,通过free()
释放内存资源。 - 内核空间:内核是操作系统的一部分,始终驻留内存。内核空间为内核保留,不允许应用程序读写该区域内容或直接调用内核代码定义的函数。
- 硬件:处理器包含内存管理单元(Memory Management Unit, MMU)部件,负责将虚拟地址转换为物理地址。
Linux内核只是操作系统当中的一部分,对下管理所有硬件设备;对上通过系统调用向Library Routine或者其他应用程序提供API接口。
-
用户空间
相当于应用程序使用malloc()
申请内存,通过free()
释放内存。malloc()/free()
是 glibc 库的内存分配器ptmalloc
提供的接口,ptmalloc
使用系统调用brk
或mmap
向内核以页为单位申请内存,再分成小内存块分配给对应应用程序。 -
内核空间
- 虚拟内存管理:负责从进程虚拟地址空间分配虚拟页,
sys_brk
扩大或收缩堆,sys_mmap
在内存映射区域分配虚拟页,sys_munmap
释放虚拟页。页分配器(伙伴分配器)负责分配物理页。 - 扩展功能:不连续页分配器提供
vmalloc
(分配内存)和vfree
(释放内存)接口。内存碎片化时,可申请不连续物理页并映射到连续虚拟页(虚拟地址连续,物理地址不连续)。 - 内存控制:内存控制组控制进程内存资源;内存碎片化时,通过迁移整理碎片以获取连续物理页;内存不足时,页回收负责回收物理页。
3.硬件MMU 包含页表缓存,保存最近使用的页表映射,避免虚拟地址转物理地址时频繁查询内存页表。为解决处理器与内存速度不匹配问题,增加缓存:一级缓存分数据缓存和指令缓存,二级缓存协调一级缓存与内存的工作效率。
2.系统调用视图
【代码案例】
sbrk.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#define MAX 1024
int main(int argc,char *argv[]){
int *p = sbrk(0);
int *old = p;
printf("old: %p\n",old);
p = sbrk(MAX * MAX);
if(p == (void *)(-1)){
perror("sbrk error.\n");
}
printf("old: %p\tp = %p\n",p,old);
int *new = sbrk(0);
printf("new: %p\n",new);
printf("\npid = %d\n\n",getpid());
while(1){
sleep(1);
}
sbrk(-MAX * MAX);
return 0;
}
运行结果:
二、虚拟地址空间布局架构
因为目前应用程序没有那么大的内存需求,所以 ARM64 处理器不支持完全的 64 位虚拟地址。
在 ARM64 架构的 Linux 内核中,内核虚拟地址和用户虚拟地址的宽度相同。
所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。
1.用户虚拟地址空间划分
进程的用户虚拟空间起始地址是 0,长度由处理器架构定义的宏TASK_SIZE
决定。以 ARM64 架构定义的TASK_SIZE
为例:
- 32 位用户空间程序:
TASK_SIZE
取值为TASK_SIZE_32
,即0x100000000
(等同于 4GB)。 - 64 位用户空间程序:
TASK_SIZE
取值为TASK_SIZE_64
,即2^VA_BITS
字节。
VA_BITS 是编译内核的时候选择的虚拟地址位数。
进程的用户虚拟地址空间包含区域:
- 代码段、数据段、未初始化数据段;
- 动态库的代码段、数据段和未初始化数据段;
- 存放动态生成的数据的堆;
- 存放局部变量和实现函数调用的栈;
- 把文件区间映射到虚拟地址空间的内存映射区域;
- 存放在栈底部的环境变量和参数字符串。
Linux内核使用内存描述符mm_struct,描述进程的用户虚拟地址空间,内核源码分析如下:
一个进程的虚拟地址空间主要由两个数据结构描述:
- mm_struct:最高层次结构,描述一个进程整个虚拟地址空间,每个进程只有一个,在进程的
task_struct
结构中有专门指针指向它,是对整个用户空间的描述。 - vm_area_struct:较高层次结构,描述虚拟地址空间的一个区间(虚拟区)。
- 进程基本组成部分对应的 vm_area_struct 节点数量
一个典型进程至少有几个 vm_area_struct 节点描述不同内存区域:
- 代码段,进程可执行代码部分通常由一个 vm_area_struct 描述,包含程序机器指令,起始地址是代码在虚拟内存的加载位置,结束地址是代码段末尾下一字节地址,且区域通常只读可执行。
- 数据段,已初始化全局变量和静态变量所在数据段由一个 vm_area_struct 描述,用于存储程序运行需使用的固定数据,属性可读写。
- BSS 段(未初始化数据段),未初始化全局变量和静态变量所在 BSS 段对应一个 vm_area_struct,程序启动时初始化为零,可读写。
- 堆,进程堆空间由一个或多个 vm_area_struct 描述,用于动态内存分配,随 malloc 和 free 操作,堆大小和位置变化,对应 vm_area_struct 也更新。
- 栈,栈区域由一个 vm_area_struct 描述,存储函数调用相关信息(如局部变量、函数参数、返回地址等),大小和位置随函数调用返回动态变化。
- 共享库和内存映射引入的额外 vm_area_struct 节点
若进程使用共享库,每个共享库在虚拟内存的映射区域有一个 vm_area_struct,共享库代码和数据部分分别对应 vm_area_struct,描述其在虚拟内存的位置、大小、权限等信息。进程通过 mmap 系统调用将文件或设备内存映射到虚拟地址空间时,每个映射区域有一个 vm_area_struct。例如,映射配置文件到进程虚拟内存,该区域由 vm_area_struct 管理。- 具体数量因程序而异
简单程序(如仅含基本代码段、数据段和栈的小程序)可能只有 3 - 5 个 vm_area_struct 节点。复杂大型程序(使用大量共享库、复杂内存映射操作,如数据库程序、大型图形处理软件等)可能有几十个甚至上百个 vm_area_struct 节点,用于精细管理程序在虚拟内存空间的各种不同区域,满足复杂内存需求和功能实
2.内核地址空间布局
ARM64处理器架构内核地址空间布局如下:
1. 线性映射区域
- 功能:直接映射物理内存,虚拟地址与物理地址存在一一对应关系(虚拟地址连续,物理地址也连续)。
- 作用:内核可通过该区域快速访问物理内存,适用于需要直接操作内存的场景,如内存分配与管理。
2. vmemmap 区域
- 功能:存储内存页的元数据(如页的使用状态、引用计数等)。
- 作用:为内核内存管理系统(如页分配器)提供数据支持,用于跟踪和管理物理内存页。
3. 2MB 间隙
- 功能:作为隔离带,分隔不同功能区域。
- 作用:避免相邻区域地址重叠,增强内核地址空间布局的稳定性,防止不同模块间的地址冲突。
4. PCI I/O 区域(16MB)
- 功能:映射 PCI 设备的寄存器和内存空间。
- 作用:内核通过该区域与 PCI 设备进行交互,实现对硬件设备的配置和数据读写。
5. 固定映射区域
- 功能:使用固定虚拟地址映射特定物理资源(如特定硬件寄存器、高端内存等)。
- 作用:确保内核以固定地址访问关键资源,提升访问效率,适用于对地址敏感的场景。
6. vmalloc 区域
- 组成:包含间隙、内核镜像、内核模块区域。
- 间隙:隔离不同子区域,保证地址空间布局的规范性。
- 内核镜像:存放内核代码和初始化数据,是内核运行的基础。
- 内核模块区域(128MB):动态加载内核模块(如驱动程序),扩展内核功能。
- 作用:
vmalloc
用于分配物理地址不连续但虚拟地址连续的内存,满足内核非连续内存的映射需求。7. KASAN 影子区域
- 功能:存储内存访问的 “影子数据”,记录内存区域的访问权限、状态等信息。
- 作用:配合内核地址消毒剂(KASAN)检测内存越界、使用已释放内存等错误,辅助调试内存相关问题。
https://github.com/0voice