在 Linux 操作系统中,每个进程都有独立的虚拟地址空间。虚拟地址空间是操作系统为每个进程提供的抽象内存模型,它使得每个进程都觉得自己拥有独立的内存,而不需要关心物理内存的具体布局。本文将深入探讨 Linux 进程的虚拟地址空间及其管理机制。
1. 虚拟地址空间的概念
虚拟地址空间(也可以叫进程地址空间)是每个进程能够访问的内存地址范围。操作系统通过内存管理单元(MMU,Memory Management Unit)将虚拟地址转换为物理地址,实现内存虚拟化。
虚拟地址空间本质上是一个结构体对象struct mmu_struct,用来管理每个进程的虚拟地址。
1.1 虚拟内存与物理内存的区别
虚拟内存是操作系统提供的一种抽象,允许每个进程认为自己拥有一个连续的内存区域。物理内存则是实际的硬件内存,是有限的,而虚拟内存的大小通常远大于物理内存。并且一个进程一个虚拟地址空间(一个task_struct, 一个虚拟地址空间)。
1.2 地址空间的布局
每个进程的虚拟地址空间通常分为几个区域,包括:
- 文本段(Text Segment): 存放可执行的机器代码,正文代码。
- 数据段(Data Segment): 存放已初始化的全局变量和静态变量。
- BSS段: 存放未初始化的全局变量和静态变量。
- 堆(Heap): 用于动态分配内存,程序运行时可以向堆中申请内存。
- 栈(Stack): 用于存储局部变量和函数调用时的返回地址。
下图中用户空间的数据,入堆、栈、初始化数据等拿到地址就可以直接访问。虚拟地址空间是操作系统为每个进程分配的逻辑地址范围,在内核中通过特定的数据结构(结构体)来管理这些地址范围及其映射关系。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int g_unval;
6 int g_val = 100;
7
8 int main(int argc, char *argv[], char *env[])
9 {
10 const char *str = "helloworld";
11 printf("code addr: %p\n", main);
12 printf("init global addr: %p\n", &g_val);
13 printf("uninit global addr: %p\n", &g_unval);
14 static int test = 10;
15 char *heap_mem = (char*)malloc(10);
16 char *heap_mem1 = (char*)malloc(10);
17 char *heap_mem2 = (char*)malloc(10);
18 char *heap_mem3 = (char*)malloc(10);
19 printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
20 printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
21 printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
22 printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
23
24 printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
25 printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
26 printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
27 printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
28 printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
29
30 printf("read only string addr: %p\n", str);
31 for(int i = 0 ;i < argc; i++)
32 {
33 printf("argv[%d]: %p\n", i, argv[i]);
34 }
35 for(int i = 0; env[i]; i++)
36 {
37 printf("env[%d]: %p\n", i, env[i]);
38 }
39 return 0;
40 }
1.3 虚拟地址与物理地址映射
虚拟地址和物理地址通过页表进行映射。变量只是程序运行中的逻辑抽象,存储的是它们对应的内存地址。操作系统使用分页技术,将虚拟内存划分为小块(通常为4KB一页),并将这些虚拟页映射到物理内存中的页面。MMU 根据页表将虚拟地址转换为物理地址。下图中页表左侧记录进程的虚拟地址,右侧记录虚拟地址对应的物理地址。
页表的作用是将虚拟地址映射到物理地址,而不是直接存储变量。
一个进程,一个页表。
1.4 证明虚拟地址
下面的父子进程地址一样,如果是内存地址,那就是BUG, 所以它不是内存地址,它叫做虚拟地址。C/C++指针用到的地址都是虚拟地址。
子进程被创建时会共享父进程的虚拟地址空间,虚拟地址映射指向相同的物理地址。此时,父子进程对相同内存区域的访问是只读的。一旦任一进程尝试写入共享内存区域时,会触发“写时拷贝”(Copy-On-Write,COW)机制,为写入操作分配新的物理内存,从而实现进程间内存的独立性。
-
共享虚拟地址空间: 子进程创建时(例如通过 fork() 系统调用),初始状态下会完全复制父进程的虚拟地址空间,但虚拟地址的映射指向相同的物理地址。这是一种内存优化机制,避免在进程创建时立即复制大量内存。
-
写时拷贝机制: 父子进程共享的物理内存区域在写入时触发写时拷贝,操作系统会分配新的物理内存块,将原内容复制到新内存块中,以确保每个进程的写入操作互不干扰。
-
读写行为区分: 在共享期间,读操作不会引发写时拷贝,只有写操作才会触发物理内存的复制,保证效率和独立性。
2. 虚拟地址与进程地址空间
2.1 是什么?
虚拟地址是操作系统为进程提供的一种逻辑地址,用于访问内存。它是程序编写和执行时所使用的地址,由操作系统和硬件(如内存管理单元 MMU)将其映射到实际的物理地址。
虚拟地址空间是操作系统为进程定义的一整个虚拟地址范围,是虚拟地址的集合。它表示进程可以使用的虚拟地址范围和布局。
进程地址空间是虚拟地址空间的一个实例,它表示某个特定进程在运行时可以使用的所有虚拟地址范围。它包含该进程的代码、数据、堆、栈等内存段。
2.2 程序加载为进程
程序加载为进程的过程主要涉及虚拟地址空间的分配、程序的加载以及虚拟地址到物理地址的映射。以下是详细的三个步骤解析:
(1)虚拟地址空间中申请同样大小的内存
概念:
当操作系统加载一个程序时,会在该进程的虚拟地址空间中为程序的各个部分(如代码段、数据段、BSS 段等)预留相应的地址范围。
细节:
- 这一步并不会实际分配物理内存,而是仅在虚拟地址空间中划分出程序运行所需的逻辑地址范围。
- 根据程序的二进制文件(如 ELF 格式)描述,操作系统会按照不同的段(代码段、数据段等)的大小和属性进行布局。
- 这些划分通常基于程序的逻辑需求,例如代码段是只读的,而数据段是可读写的。
(2)加载程序,申请物理空间
概念:
操作系统根据程序的实际需求,将程序的部分内容(如可执行代码)从磁盘加载到内存中,并为需要动态分配的区域(如堆)分配物理内存。
过程:
- 代码段和数据段: 操作系统从可执行文件中读取代码段和已初始化的数据段,将其内容加载到物理内存中。
- BSS 段: 未初始化的数据段不需要加载实际数据,而是直接在物理内存中申请相应大小的内存,并初始化为 0。
- 堆和栈: 堆和栈的内存区域通常在程序运行时动态分配。
按需加载:
- 程序首次访问某个虚拟地址时,才将对应的物理内存分配到该地址。
- 这减少了程序启动时的内存消耗和加载时间。
- 为了优化性能,现代操作系统通常采用**按需加载(Lazy Loading)**策略
(3)页表进行映射
概念:
页表是虚拟地址到物理地址映射的核心数据结构,操作系统通过页表建立虚拟地址与物理内存的对应关系。
过程:
- 建立映射: 在程序加载过程中,操作系统会根据虚拟地址空间的布局和已分配的物理内存,更新页表,记录虚拟地址与物理地址的对应关系。例如,代码段的虚拟地址
0x400000
可能映射到物理地址0x1A0000
。 - 按页管理: 虚拟地址空间被划分为多个固定大小的页(通常为 4KB),每个虚拟页对应一个物理页框(Page Frame)。
- 保护属性: 页表不仅负责映射,还记录地址的访问权限(如只读、可执行),以防止非法访问。
- TLB 缓存: 为加速地址转换,页表的部分内容会缓存在**TLB(Translation Lookaside Buffer)**中。
写时拷贝(Copy-on-Write)
- 当多个进程共享某段内存时(如子进程继承父进程的虚拟地址空间),页表会标记为只读
- 只有当某进程尝试写入时,操作系统才会触发写时拷贝机制,分配新的物理内存并更新页表。
完整过程总结
- 虚拟地址空间分配: 操作系统在虚拟地址空间中划分各个区域,预留程序所需的逻辑地址范围。
- 程序加载与物理内存分配: 将程序的代码段、数据段加载到物理内存中,并根据需要动态分配堆和栈的内存。
- 页表映射: 操作系统通过页表将虚拟地址与物理地址进行映射,并维护访问权限和动态调整。
图示:程序加载与映射过程
虚拟地址空间:
+----------------------+ <--- 栈区 (动态增长)
| Stack |
+----------------------+ <--- 堆区 (动态增长)
| Heap |
+----------------------+ <--- 数据段 (静态分配)
| Initialized Data |
+----------------------+ <--- BSS 段
| Uninitialized Data |
+----------------------+ <--- 代码段
| Code |
+----------------------+ <--- 虚拟地址空间起始
物理内存:
+----------------------+
| 物理页框 X | <---> 虚拟地址页 A (代码段)
+----------------------+
| 物理页框 Y | <---> 虚拟地址页 B (数据段)
+----------------------+
| 物理页框 Z | <---> 虚拟地址页 C (堆区)
+----------------------+
页表:
+----------------+----------------+
| 虚拟地址页 A | 物理页框 X |
| 虚拟地址页 B | 物理页框 Y |
| 虚拟地址页 C | 物理页框 Z |
+----------------+----------------+
通过这一系列操作,程序被加载为一个独立的进程,并能通过虚拟地址访问对应的物理内存,从而实现进程的高效运行和内存隔离。
2.3 为什么要有虚拟地址空间
(1)将地址从“无序”变“有序”
(2)地址转换过程中,也可以对你的地址和操作进行合法性判定,保护物理内存
(3)让内存管理和进程管理进行一定程度的解耦合
3. 进程相关信息之间的关系
在操作系统中,进程、PCB(进程控制块)、进程地址空间、内核数据结构、物理内存、虚拟内存、代码和数据等概念是紧密相互关联的,它们共同构成了操作系统管理和调度进程的基础。下面是这些概念之间的关系解析:
1. 进程(Process)
进程是操作系统管理的基本单位,是正在运行的程序的实例。一个进程通常包含以下内容:
- 程序代码: 要执行的机器指令。
- 数据: 程序执行过程中需要的数据(如全局变量、动态分配的内存等)。
- 状态信息: 如程序计数器、寄存器值等,表示进程当前的执行状态。
- 进程控制块(PCB): 进程的管理信息,操作系统用来管理进程的状态。
2. 进程控制块(PCB, Process Control Block)
进程控制块(PCB)是操作系统用来存储与进程相关的信息的内核数据结构。每个进程都对应一个PCB,主要包含:
- 进程ID、状态、优先级、程序计数器 等进程的基本信息。
- CPU寄存器的值,保存进程的上下文。
- 内存管理信息,例如虚拟地址空间的映射信息。
- 文件描述符、信号等信息,记录进程使用的文件资源和系统信号。
关系:
进程控制块(PCB)在操作系统中是进程的标识,它为操作系统提供了管理进程生命周期和调度的信息。每个进程有一个对应的PCB,操作系统通过PCB来追踪和管理进程。
3. 进程地址空间(Process Address Space)
进程地址空间是操作系统为每个进程创建的一个虚拟空间。它为进程提供了一个统一的、连续的地址空间,通常包括:
- 代码段(Text Segment): 存储可执行的程序代码。
- 数据段(Data Segment): 存储已初始化的全局变量、静态变量等。
- BSS段(BSS Segment): 存储未初始化的全局变量、静态变量等。
- 堆(Heap): 用于动态分配内存。
- 栈(Stack): 用于函数调用时存储局部变量和调用信息。
关系:
进程地址空间是进程的虚拟内存的映射,为程序提供一个统一的内存视图。操作系统通过虚拟内存管理将进程地址空间映射到物理内存上,而不必让程序直接与物理内存打交道。
4. 内核数据结构(Kernel Data Structures)
内核数据结构是操作系统在内核模式下用于管理和调度系统资源的数据结构。常见的内核数据结构包括:
- 进程控制块(PCB): 存储进程信息。
- 调度队列: 存储准备运行的进程。
- 页表: 存储虚拟内存到物理内存的映射信息。
- 文件系统管理结构: 管理文件及其元数据。
关系:
内核数据结构是操作系统内部的关键组成部分,支持进程调度、内存管理、设备管理等操作。它们与进程地址空间、物理内存的管理紧密相连,确保操作系统能够有效地管理硬件资源和进程执行。
5. 物理内存(Physical Memory)
物理内存是计算机硬件中用于存储数据的实际内存(如RAM)。它是计算机硬件资源的一部分。
关系:
物理内存是操作系统管理的核心资源之一。操作系统通过虚拟内存管理将虚拟地址空间映射到物理内存。物理内存存储进程的实际数据和代码,在进程执行时提供直接的内存访问支持。
6. 虚拟内存(Virtual Memory)
虚拟内存是操作系统提供的一种抽象机制,允许进程使用比物理内存更多的内存,并且为每个进程提供一个独立的虚拟地址空间。虚拟内存通过分页或分段机制实现虚拟地址与物理地址的映射。
关系:
虚拟内存允许操作系统将进程的虚拟地址空间与物理内存解耦合。通过虚拟内存,进程能够访问一个看似连续且独立的地址空间,而操作系统负责将虚拟地址映射到物理内存或磁盘的交换空间上。
7. 代码和数据(Code and Data)
- 代码(Code): 进程的可执行指令,通常存放在进程的代码段(Text Segment)中。
- 数据(Data): 进程在运行时使用的所有数据,包括全局变量、静态变量、局部变量等,存放在数据段(Data Segment)、BSS段、堆、栈等区域。
关系:
代码和数据是进程在运行时的核心内容。操作系统通过进程地址空间管理它们,并将其加载到物理内存中进行执行。代码存放在内存的代码段中,而数据存放在数据段、堆、栈等区域。操作系统通过虚拟内存管理这些区域,并通过页表映射虚拟地址到物理内存。
整体关系总结
- 进程由操作系统创建并管理,其控制信息存储在PCB中。
- 每个进程有独立的进程地址空间,该地址空间由操作系统管理,通过虚拟内存提供对物理内存的访问。
- 内核数据结构(如PCB、页表、调度队列等)用于支持操作系统对进程、内存和资源的管理。
- 物理内存是计算机硬件提供的实际内存,存储进程的数据和代码,操作系统通过虚拟内存将进程的虚拟地址空间映射到物理内存。
- 代码和数据是进程执行的核心内容,操作系统将它们分配到虚拟地址空间的不同区域,并通过物理内存存储和执行。
通过这些元素的协同工作,操作系统能够高效地管理进程、内存和硬件资源,并确保系统的稳定与安全。