为什么使用虚拟内存

一句话:引入虚拟内存后,进程与进程之间的虚拟内存地址空间是相互隔离,互不干扰的。

640

虚拟内存地址格式

都以4K为基本页框大小

image-20231225145536867

image-20231225145517347

进程的虚拟内存空间

用户空间

640 (1)

  • 用于存放进程程序二进制文件中的机器指令的代码段
  • 用于存放程序二进制文件中定义的全局变量和静态变量的数据段和 BSS 段。其中数据段中为指定了初始值的全局变量和静态变量;BSS段中为为指定初始值的全局变量和静态变量(初始化为0)
  • 用于在程序运行过程中动态申请内存的堆。堆空间中地址的增长方向是从低地址到高地址增长。
  • 用于存放动态链接库以及内存映射区域的文件映射与匿名映射区。
  • 用于存放函数调用过程中的局部变量和函数参数的栈。

虚拟内存空间分布

虚拟内存空间包括用户态和内核态。

32位机器

640 (2)

用户态虚拟内存空间中的代码段并不是从 0x0000 0000 地址开始的,而是从 0x0804 8000 地址开始。

0x0000 0000 到 0x0804 8000 这段虚拟内存地址是一段不可访问的保留区,因为在大多数操作系统中,数值比较小的地址通常被认为不是一个合法的地址,这块小地址是不允许访问的。比如在 C 语言中我们通常会将一些无效的指针设置为 NULL,指向这块不允许访问的地址。

BSS 段的上边就是我们经常使用到的堆空间,从图中的红色箭头我们可以知道在堆空间中地址的增长方向是从低地址到高地址增长

内核中使用 start_brk 标识堆的起始位置,brk 标识堆当前的结束位置。当堆申请新的内存空间时,只需要将 brk 指针增加对应的大小,回收地址时减少对应的大小即可。比如当我们通过 malloc 向内核申请很小的一块内存时(128K 之内),就是通过改变 brk 位置实现的。

堆空间的上边是一段待分配区域,用于扩展堆空间的使用。

接下来就来到了文件映射与匿名映射区域。在文件映射与匿名映射区的地址增长方向是从高地址向低地址增长

接下来用户态虚拟内存空间的最后一块区域就是栈空间了,在这里会保存函数运行过程所需要的局部变量以及函数参数等函数调用信息。栈空间中的地址增长方向是从高地址向低地址增长

在内核中使用 start_stack 标识栈的起始位置,RSP 寄存器中保存栈顶指针 stack pointer,RBP 寄存器中保存的是栈基地址。在栈空间的下边也有一段待分配区域用于扩展栈空间。

64位机器

虽然64位机器的指针寻址范围是2^64,但是在目前的 64 位系统下只使用了 48 位来描述虚拟内存空间,寻址范围为 2^48 ,所能表达的虚拟内存空间为 256TB。

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

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

这样一来就在用户态虚拟内存空间与内核态虚拟内存空间之间形成了一段 0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000 的地址空洞,我们把这个空洞叫做 canonical address 空洞

640 (4)

如上图,还可以看到:在代码段跟数据段的中间还有一段不可以读写的保护段,它的作用是防止程序在读写数据段的时候越界访问到代码段,这个保护段可以让越界访问行为直接崩溃,防止它继续往下运行。

内核空间

内核态虚拟内存空间是所有进程共享的,不同进程进入内核态之后看到的虚拟内存空间全部是一样的。

直接看参考文献第7节吧:参考文献

进程虚拟内存空间的管理

内核中的进程描述符task_struct结构

include/linux/sched.h

1
2
3
4
5
6
7
8
struct task_struct {
// 进程id
pid_t pid;
// 进程打开的文件信息
struct files_struct *files;
// 该进程的虚拟地址空间结构体指针
struct mm_struct *mm;
}

include/linux/mm_types.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct mm_struct 
{
unsigned long task_size; // 用户态和内核态的分界线
// 使用如下属性定义虚拟内存中的不同区域
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long mmap_base; /* base of mmap area */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long data_vm; /* VM_WRITE & ~VM_SHARED & ~VM_STACK */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE & ~VM_STACK */
unsigned long stack_vm; /* VM_STACK */
}

task_size定义了用户态地址空间与内核态地址空间之间的分界线。32 位系统中用户态虚拟内存空间为 3 GB,虚拟内存地址范围为:0x0000 0000 - 0xC000 000 。内核态虚拟内存空间为 1 GB,虚拟内存地址范围为:0xC000 000 - 0xFFFF FFFF。32 位系统中task_size 为 0xC000 000。同理,64 位系统中用户地址空间和内核地址空间的分界线在 0x0000 7FFF FFFF F000 地址处,那么自然进程的 mm_struct 结构中的 task_size 为 0x0000 7FFF FFFF F000 。

start_code 和 end_code 定义代码段的起始和结束位置,start_data 和 end_data 定义数据段的起始和结束位置;start_brk 定义堆的起始位置,brk 定义堆当前的结束位置,start_stack 是栈的起始位置在RBP寄存器中,栈的结束位置也就是栈顶指针 stack pointer 在 RSP 寄存器中存储;arg_start 和 arg_end 是参数列表的位置, env_start 和 env_end 是环境变量的位置。它们都位于栈中的最高地址处;mmap_base 定义内存映射区的起始地址

还定义了一些虚拟内存与物理内存映射内容相关的统计变量。

mm_struct 结构体中的 total_vm 表示在进程虚拟内存空间中总共与物理内存映射的页的总数。

当内存吃紧的时候,有些页可以换出到硬盘上,而有些页因为比较重要,不能换出。locked_vm 就是被锁定不能换出的内存页总数,pinned_vm 表示既不能换出,也不能移动的内存页总数。

data_vm 表示数据段中映射的内存页数目,exec_vm 是代码段中存放可执行文件的内存页数目,stack_vm 是栈中所映射的内存页数目,这些变量均是表示进程虚拟内存空间中的虚拟内存使用情况。

下图为一个总览图:

640 (5)

虚拟内存区域

内核中使用结构体 vm_area_struct,来管理像代码段、数据段等不同的虚拟内存区域VMA

同样在include/linux/mm_types.h

1
2
3
4
5
6
7
8
9
10
11
12
13
struct vm_area_struct {
/*定义虚拟内存区域起始地址*/
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
/*省略*/
/*定义访问权限*/
pgprot_t vm_page_prot;
unsigned long vm_flags;

struct anon_vma *anon_vma; /* Serialized by page_table_lock */
struct file * vm_file; /* File we map to (can be NULL). */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
}

vm_start 指向了这块虚拟内存区域的起始地址(最低地址),vm_start 本身包含在这块虚拟内存区域内。vm_end 指向了这块虚拟内存区域的结束地址(最高地址),而 vm_end 本身包含在这块虚拟内存区域之外,所以 vm_area_struct 结构描述的是 [vm_start,vm_end) 这样一段左闭右开的虚拟内存区域。

vm_page_prot 和 vm_flags 都是用来标记 vm_area_struct 结构表示的这块虚拟内存区域的访问权限和行为规范。

接下来的三个属性 anon_vma,vm_file,vm_pgoff 分别和虚拟内存映射相关,虚拟内存区域可以映射到物理内存上,也可以映射到文件中,映射到物理内存上我们称之为匿名映射,映射到文件中我们称之为文件映射。

当我们调用 malloc 申请内存时,如果申请的是小块内存(低于 128K)则会使用 do_brk() 系统调用通过调整堆中的 brk 指针大小来增加或者回收堆内存。

如果申请的是比较大块的内存(超过 128K)时,则会调用 mmap 在上图虚拟内存空间中的文件映射与匿名映射区创建出一块 VMA 内存区域,如果进行匿名映射,其匿名映射区域就用 struct anon_vma 结构表示;如果进行文件映射,vm_file 属性就用来关联被映射的文件,vm_pgoff 则表示映射进虚拟内存中的文件内容,在文件中的偏移。

将虚拟内存区域组织进虚拟内存中

同一个进程的虚拟内存空间的不同虚拟内存区域是如何组织起来的呢?

1
2
3
4
struct vm_area_struct {
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb; //红黑树节点
}

在内核中使用两种方式对虚拟内存区域进行组织

一是通过一个 struct vm_area_struct 结构的双向链表将虚拟内存空间中的这些虚拟内存区域 VMA 串联起来的。

vm_area_struct 结构中的 vm_next ,vm_prev 指针分别指向 VMA 节点所在双向链表中的后继节点和前驱节点,内核中的这个 VMA 双向链表是有顺序的,所有 VMA 节点按照低地址到高地址的增长方向排序。双向链表中的最后一个 VMA 节点的 vm_next 指针指向 NULL,双向链表的头指针存储在内存描述符 struct mm_struct 结构中的 mmap 中,正是这个 mmap 串联起了整个虚拟内存空间中的虚拟内存区域。

二是为了高效查询,将VMA作为红黑数的节点,以红黑树来组织。

每个 VMA 区域都是红黑树中的一个节点,通过 struct vm_area_struct 结构中的 vm_rb 将自己连接到红黑树中。而红黑树中的根节点存储在内存描述符 struct mm_struct 中的 mm_rb

640 (6)

参考文献:参考文献