虚拟内存管理

虚拟内存管理

Linux内存管理子系统负责管理虚拟内存、物理内存、地址转换和内存分配等,包含多个层次。
其整体架构如下,之后会一一分析。

虚拟内存管理

先从虚拟地址入手,看名字就知道,就是内核虚拟的内存空间,共各个进程使用。
物理内存是死的,只有那么多,每个进程都想独占,于是内核就创造了个假的,
让每个进程都以为自己在独享物理内存空间。
我们要学习就是内核怎么把这个假的东西造出来的,又是怎么欺上瞒下管理的,就这么简单。

先看一下虚拟内存的分布

从下往上看,首先是代码段,存放的程序编译后的机器码。
程序运行起来后需要的数据被放到数据段和BSS段,
其中,指定了初始值的全局变量和静态变量被放到数据段,

程序在运行期间往往需要动态的申请内存,所以在虚拟内存空间中也需要一块区域来存放这些动态申请的内存,这块区域就叫做堆。

初次之外,程序在运行的时候还依赖动态链接库,这些库也动态链接库也有自己的对应的代码段,数据段,BSS 段,也需要一起被加载进内存中。

还有用于内存文件映射的系统调用 mmap,会将文件与内存进行映射,这块内存(虚拟内存)也需要在虚拟地址空间中有一块区域存储。

这些动态链接库中的代码段,数据段,BSS 段,以及通过 mmap 系统调用映射的共享内存区,在虚拟内存空间的存储区域叫做文件映射与匿名映射区。

最后,程序运行的时候总要调用各种函数,那么调用函数过程中使用到的局部变量和函数参数也需要一块内存区域来保存。这一块区域在虚拟内存空间中叫做栈。

上面只是一个简化分布,但是都是核心部分,
大概就是内核给进程呈现的假象空间。

我们看一下真实机器的虚拟内存布局,以32位和64位机器为例子。

32位机器上,能寻址的范围是2^32,虚拟空间的大小是4GB,范围是0x00000000-0xFFFFFFFF。
用户态大小3GB,范围0x00000000 - 0xC0000000
内核态 1 GB,范围为:0xC0000000 - 0xFFFFFFFF

用户空间虚拟地址从0x08048000开始,保留区为不可访问。

64位机器上,寻址范围为 2^64,理论上所能表达的虚拟内存空间为 16 EB,范围是0x00000000000000000000-0xFFFF FFFFFFFFFFFF。
但其实只使用了48 位来描述虚拟内存空间,寻址范围为 2^48 ,所能表达的虚拟内存空间为 256TB。

其中低 128 T 表示用户态虚拟内存空间,虚拟内存地址范围为:0x0000 0000 0000 0000 -0x0000 7FFF FFFF FFFF 。

高 128 T 表示内核态虚拟内存空间,虚拟内存地址范围为:0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF 。

中间有一段 0x0000 8000 0000 0000 - 0xFFFF 7FFF FFFF FFFF 的地址空洞,canonical address 空洞。

之前讲过task_struct各类内核信息,其中就有内存信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct task_struct {
pid_t pid;
pid_t tgid;
struct files_struct *files;
// 内存描述符表示进程虚拟地址空间
struct mm_struct *mm;

.......... 省略 .......
}
是用mm_struct结构体描述虚拟内存空间。

从代码层面我们看一下内核是如何布局虚拟内存空间的。
```c
struct mm_struct {
struct maple_tree mm_mt;
unsigned long mmap_base;
unsigned long task_size;
pgd_t *pgd;

unsigned long total_vm; //表示在进程虚拟内存空间中总共与物理内存映射的页的总数。
unsigned long locked_vm; // 不能被换出的页的总数
atomic64_t pinned_vm; // 既不能换出,也不能移动的内存页总数
unsigned long data_vm; // 数据段中映射的内存页数目
unsigned long exec_vm; // 代码段中存放可执行文件的内存页数目
unsigned long stack_vm; // 栈中所映射的内存页数目

unsigned long start_code, end_code;
unsigned long start_data, end_data;
unsigned long start_brk, brk;
unsigned long start_stack;
unsigned long arg_start, arg_end;
unsigned long env_start, env_end;

struct percpu_counter rss_stat[NR_MM_COUNTERS];
};

整个映射关系如图

首先是task_size变量,讲虚拟内存空间分为两部分内核空间和用户空间。
start_code和end_code定义代码段的起始和结束位置,存放程序编译后的机器码。

start_stack 是栈的起始位置在 RBP 寄存器中存储,栈顶指针 stack pointer 在 RSP 寄存器中存储。

虚拟地址空间Virtual Memory Area通过上面的mm_stuct进行了划分,而一段具体的、连续的虚拟地址区间
使用struct vm_area_struct进行管理。
核心为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct vm_area_struct {
unsigned long vm_start; // 起始地址
unsigned long vm_end; // 结束地址

struct mm_struct *vm_mm;

pgprot_t vm_page_prot; // 页级别访问权限
unsigned long vm_flags; // VAM虚拟内存区域访问权限

struct anon_vma *anon_vma;

const struct vm_operations_struct *vm_ops;

unsigned long vm_pgoff;
struct file *vm_file;

void *vm_private_data;
};

表示的地址范围[vm_start, vm_end),一个 mm_struct 对应多个 VMA,使用struct maple_tree mm_mt进行管理。

如图所示

每个虚拟内存区域VAM都由许多虚拟页构成,经过页表转换对应到物理页。
每个页面都是有访问权限的。
其中vm_page_prot是页级别的访问权限,
vm_flags则是整个虚拟内存区域的访问权限。
两者通过vma->vm_page_prot = vm_get_page_prot(vma->vm_flags)进行转换。

常用定义的vm_flags

虚拟内存映射由两条路可以走,一条是直接与物理内存映射称为匿名映射,一条是映射到文件中称为文件映射。
后面三个参数anon_vma,vm_file,vm_pgoff就是与文件映射有关的。

以malloc为例子,当申请大内存时,会在文件映射与匿名映射区穿件VMA内存区域,即匿名映射。
并使用struct anon_vma表示,vm_file关联映射的文件,vm_pgoff表示映射进虚拟内存中的文件内容,在文件中的偏移。

继续看剩下的vm_ops,是搭配 VMA 使用的相关操作。

1
2
3
4
5
6
7
8
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area); // VMA建立
void (*close)(struct vm_area_struct * area); // VMA销毁
vm_fault_t (*fault)(struct vm_fault *vmf); // 处理缺页
vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);

.....
}

就是一些回调函数。

现在我们有了VMA虚拟内存区域和它对应的操作函数vm_ops,接下来把多个VAM组织起来就行了。
内核使用mm_mt进行管理,其在mm_struct中。

1
2
3
4
5
6
struct mm_struct {
struct {
struct maple_tree mm_mt; /* 管理该地址空间的全部VMA */
/* ... */
} __randomize_layout;
};

使用一棵 Maple Tree 按虚拟地址范围统一保存全部 VMA;
它既支持快速地址查找,也支持按地址顺序遍历以及空闲区间查找。

根据上面的内容,对虚拟内存的布局,以及如何管理都有了认知。
接下来就是进程的虚拟内存空间 mm_struct 以及虚拟内存区域 vm_area_struct 是如何被创建并初始化的。

一个进程在运行前会变编译成ELF(Executable and Linkable Format)格式的二进制文件,这个文件只有被加载进内存才能执行。
根据过程不同,有两种视图形式,一种是编译链接时候的链接视图,一种是运行过程中运行视图。

其不同之处就在于运行视角中链接器把目标文件中多个 section 整合成一个segment,方便一起加载进内存。

内核专门提供了这个函数完成这个过程,load_elf_binary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
static int load_elf_binary(struct linux_binprm *bprm)
{
/* 1. 检查ELF Header */
elf_ex = (struct elfhdr *)bprm->buf;

if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
return -ENOEXEC;

if (elf_ex->e_type != ET_EXEC &&
elf_ex->e_type != ET_DYN)
return -ENOEXEC;

if (!elf_check_arch(elf_ex))
return -ENOEXEC;

/* 2. 读取主程序Program Header */
elf_phdata = load_elf_phdrs(elf_ex, bprm->file);

/* 3. 查找并打开PT_INTERP指定的动态链接器 */
interpreter = find_and_open_elf_interpreter(
bprm->file, elf_phdata);

/* 4. 用新mm替换旧程序的mm */
retval = begin_new_exec(bprm);

/* 5. 选择mmap布局和ASLR布局 */
setup_new_exec(bprm);

/* 6. 将临时参数栈移动到最终位置 */
retval = setup_arg_pages(
bprm,
randomize_stack_top(STACK_TOP),
executable_stack);

/* 7. 遍历并映射主程序的PT_LOAD */
for_each_program_header(elf_ppnt, elf_phdata) {
if (elf_ppnt->p_type != PT_LOAD)
continue;

elf_prot = make_prot(elf_ppnt->p_flags, ...);

error = elf_map(
bprm->file,
load_bias + elf_ppnt->p_vaddr,
elf_ppnt,
elf_prot,
elf_flags,
total_size);

/* 统计代码、数据和BSS边界 */
update_elf_boundaries(elf_ppnt);
}

/* 8. 清零并映射BSS,设置初始brk */
padzero(elf_bss);
set_brk(elf_bss, elf_brk, bss_prot);

/* 9. 映射动态链接器;静态程序不需要 */
if (interpreter)
elf_entry = load_elf_interp(...);
else
elf_entry = elf_ex->e_entry + load_bias;

/* 10. 构造argc、argv、envp和辅助向量 */
create_elf_tables(...);

/* 11. 记录重要虚拟地址边界 */
current->mm->start_code = start_code;
current->mm->end_code = end_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;

/* 12. 设置用户态PC和SP */
START_THREAD(elf_ex, regs, elf_entry, bprm->p);

return 0;
}'

上面说的都是进程用户空间的虚拟内存布局。
内核态也有自己的虚拟地址空间,内核态的虚拟地址空间是各个进程共享的。

先看32位的内核虚拟地址,前面说过其大小为1GB。
在这1GB的内存里面,最前面有一段896 MiB的直接映射区,它是内核访问低端物理内存的一种虚拟地址窗口。
对应映射的物理地址中存放内核代码和只读数据、内核全局数据和BSS、页表和slab对象等。

受硬件影响,这896MiB的物理内存又被分为两部分,一部分是前16M的ZONE_DMA,一部分是后16M 到 896M这段区域,称之为 ZONE_NORMAL。前 16M 专门让内核用来为 DMA 分配内存,后面就是正常页框。

最上面的区域被称为高端存储区,其的对应关系如下

在向上由8M的空洞区间
继续是vmalloc动态映射区间,vmalloc 分配的内存在虚拟内存上是连续的,但是物理内存是不连续的。


虚拟内存管理
https://cj0510.github.io/2026/07/13/Linux Kernel/Memory Management/虚拟内存管理/
作者
CJ1018
发布于
2026年7月13日
许可协议
CC BY-NC-SA 4.0