验证地址空间
父子进程的变量值不同但是地址相同,说明该地址绝对不是物理地址
我们叫这种地址为虚拟地址/线性地址


分析与结论
上述实验表明,父子进程的变量地址相同但内容不同,说明地址为虚拟地址,且父子进程有各自独立的物理地址映射 。这验证了虚拟地址的概念,即我们在C/C++中看到的地址是虚拟地址,由操作系统负责将其转化为物理地址。
进程地址空间
让每一个进程都认为自己是独占系统物理内存大小,进程彼此之间不知道,不关心对方存在,从而实现一定程度的隔离
程序地址空间 实际上是进程地址空间的子集,是系统级的概念。进程地址空间通过虚拟地址映射实现内存独立性,确保进程间互不干扰。
程序地址空间回顾
看以下C语言代码,感受进程地址空间
cpp
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4
5 int gval = 100;
6 int unval;
7
8 int main()
9 {
10 printf("code addr:%p\n",main);
11 printf("gval addr:%p\n",&gval);
12 printf("unval addr:%p\n",&unval);
13
14 int *mem = (int*)malloc(10*sizeof(int));
15 printf("heap addr:%p\n",mem);
16
17 int a,b,c;
18 printf("stack addr:%p\n",&a);
19 printf("stack addr:%p\n",&b);
20 printf("stack addr:%p\n",&c);
return 0
}

运行结果:
地址整体依次增大,堆区向地址增大方向增长,栈区向地址减少方向增长,验证了堆和栈的挤压式增长方向。
虚拟地址
是进程看到的抽象内存地址,由操作系统和硬件映射到物理内存或磁盘
虚拟进程地址空间本质是一个内核数据结构对象(类似PCB)
进程地址空间通过 mm_struct 结构体来管理各个区域。每个区域的定义如下:
cpp
struct mm_struct {
long code_start;
long code_end;
long init_start;
long init_end;
long uninit_start;
long uninit_end;
long heap_start;
long heap_end;
long stack_start;
long stack_end;
...
}
下面是linux内核中区域划分的源代码:

空间划分的本质:只要告诉我开始和结束即可
理解地址空间的地址
1.地址本质就是一个数字,可以保存在unsigned long
2.空间范围内的地址,可以随便使用,暂时不需要记录它的地址
地址空间和物理内存的关系
我们写了三个程序,将这三个程序运行起来,生成了可执行程序,此时系统存在三个进程,我们有三个task_struct结构体,那么对应的三个进程都有各自的进程地址空间mm_struct,这三个task_struct里面各自会有一个指针指向对应的进程地址空间,我们知道可执行程序运行起来需要将代码和数据加载到内存当中,那么是怎么加载到内存当中的呢?进程将自己的代码和数据首先放在虚拟地址空间的对应的区域,在这其中会有一种表结构,叫做页表 ,页表的核心工作就是完成虚拟地址到物理地址之间的映射,最终我们的可执行程序的代码和数据可以加载到物理内存的任意位置,因为最终只需要建立代码和数据与物理内存之间的映射关系,就可以通过虚拟地址找到物理内存的对应地址
不同进程的虚拟地址可以完全一样吗?答案是可以完全一样,因为每个进程都有各自的页表,每个进程都是独立的进行通过各自页表中虚拟地址和物理内存的映射关系去找代码和数据

那么不同进程的虚拟地址在页表中映射的物理地址可能会重吗?答案是不会的,如果会重操作系统就挂掉了,有一种可能性会重,但这是我们可以刻意为之,比如创建子进程:
子进程以父进程的PCB为模版创建PCB,子进程也要有自己的进程地址空间,继承父进程的进程地址空间和页表,父子映射到同样的代码段内存区 ,所以父子代码共享。子进程/父进程不修改变量的时候,数据也是共享的。当要修改/写入时,OS会重新开辟一块目标内存空间,修改页表中的物理地址,即改变页表的映射关系(写时拷贝)

页表
页表标志位:存在位,读写权限位

之前学习C语言时,知道code区域是只读的,char* str = "hello world",*str不能修改
本质是因为这段区域的权限是只读的,对应页表读写权限位是r没有w权限
存在位(isexist):目标内容是否在内存中 (分批操作/挂起等操作)
地址空间mm_struct
每一个可执行程序的代码量都不一样,那么怎样初始化mm_struct这个结构体变量呢?
从可执行程序中来!可执行程序编译的时候,各个区域的大小信息已经有了
指令readelf -S 可以查看各个区域的大小信息

统一框架 :mm_struct
的初始化采用固定的框架,确保所有进程都有一致的内存管理基础结构。
动态适配:通过虚拟内存区域和页表的按需分配,内核可以根据程序实际代码量和内存需求进行动态调整,实现高效的内存利用。
进程地址空间的意义
虚拟地址空间+页表可以保护内存
什么是野指针?为什么程序中有野指针就崩溃了?
野指针的地址全是虚拟地址,要么没有映射到物理内存/要么权限不对
进程管理和内存管理在系统层面解耦合了
让进程以统一的视角看待内存
可执行程序的代码和数据可以加载到物理内存的任意位置处,页表+映射可以将"无序"变"有序"
拓展:os 对大文件的分批加载是怎么实现的呢
采用惰性加载的方式
存在 缺页中断 ,重新申请 填写页表
缺页中断:
当一个进程访问虚拟内存中的某一页时,操作系统会先检查该页是否当前已经被加载到物理内存中。如果这一页已经在物理内存中,CPU就可以直接访问它。但是,如果这一页并没有在物理内存中,就会发生缺页中断。
当发生缺页中断时,CPU会暂停当前的执行,并将控制权交给操作系统内核。操作系统内核会首先查找页表,寻找到相关的页面对应的磁盘地址。然后,操作系统会将磁盘上的内容读取到空闲的物理内存页中。
一旦内容被加载到物理内存中,操作系统会更新页表,将该页面的映射关系添加到页表中,然后将控制权交还给进程并重新开始执行。这样,进程可以继续访问所需的内存页面。
整个过程用于解决虚拟内存中的页面不在物理内存中的问题,使得系统看起来好像比它实际拥有的更多内存一样,从而使得多个进程能够共享有限的内存资源,提高内存利用率和系统的整体性能。
就达到分批加载的效果啦
所以 进程 应该是先创建内核数据结构,再执行可执行程序的
全局变量,字符常量具有全局性,在程序运行期间都会有效,因为在地址空间中,随着进程一直存在,全局变量的虚拟地址,会被一直看到