文章目录
进程地址空间
引入

如上图:
先定义一个全局变量gval
父进程创建出子进程之后,子进程每次循环会++gval,父进程则只打印
最后都打印一下gval的地址
可以看到,子进程打印的gval一值再增加,而父进程的则不变
这符合进程独立性
但是
他们的打印的gval的地址竟然是一样的
这说明我们打印出来的gval的地址一定不是真实的物理地址
因为真实的物理地址不可能做到这样
我们语言(C/C++/java)层面里打印的地址,是进程地址空间的地址
进程地址空间是什么?

假设操作系统总共只有10G内存
但是它会对每一个进程都会给它画一个饼:
这10个G的内存都是你的
这个饼就是进程地址空间
进程地址空间其实就是一个结构体[mm_struct
]
所以每个进程使用的时候即使没有使用10个G的内存,也都认为自己拥有10个G的内存可以使用
但是每个进程都不会一下就申请使用10个G的内存,因为操作系统还没死,它自己也还要用内存
所以一个进程a申请了1G内存使用,另一个进程b也可以申请1G内存
但是它们彼此之间并不认为它们是从同一个地方申请的内存
也就是a,b都认为自己还有9个G的内存可以使用
换句话说:
进程申请内存的时候,认为申请的内存是总的物理内存的一部分
所以每一个进程中的虚拟地址的范围,是总的物理内存的地址
即虚拟内存的编号是从[0,到总物理内存的字节大小]
进程地址空间的区域划分
计算机中划分一个区域,只需要知道这个区域的起始地址(begin)和终止地址(end)的值就行了
所以进程地址空间中的区域(栈区,堆区等)的划分
其实在mm_struct中就是根据总的物理内存的地址范围
[前面提到过的进程都认为自己独占物理内存所以给自己的区域划分的时候,按总的物理内存来]
按一定的规则分别记录了它们的起始地址和终止地址
物理内存和进程地址空间之间的关系
一个进程的代码和数据加载到内存之后,是不可能
真的存储在进程地址空间中的
必须把它们保存在真实的物理内存中
因为进程地址空间只是一个结构体,它自己都要存储在物理内存中
所以如果进程要使用虚拟地址,那么这个==被使用的虚拟地址
就必须要与真实的物理地址
建立关系==
所以进程被的代码和数据被加载到内存之后,一定会被存储在真实的物理内存中
进程的PCB中会有一个指针指向mm_struct(进程地址空间)
我们在进程运行时,使用的所有的代码和数据都会被映射到合适区域中的虚拟地址上
而这些虚拟地址,都会再映射到对应的物理地址中,它们的映射关系使用页表存储
虚拟地址本来是完全虚无的,内存中根本就不会给它开空间,而且每个进程的
虚拟内存和物理内存一样大,所以很多虚拟地址其实不会被使用
只有幸运地被进程使用到的虚拟地址
,才会被存储进页表,通过页表建立与物理内存的联系,间接地化虚为实
我们使用地址时,看到的都是虚拟地址,使用的也是虚拟地址
由操作系统自动去实现虚拟地址和物理地址之间的转换
如果再创建子进程
我们使用fork创建子进程的时候
子进程会把父进程的PCB,mm_struct,页表,代码和数据等
全部都继承下来[因为是写时拷贝,如果想修改再拷贝要修改的东西]
所以子进程刚刚被创建的时候
①它和父进程的代码和数据的虚拟地址的值都是一样的,对应的物理地址也是一样的
②代码和数据也是共享的
③页表里面存储的虚拟地址和物理地址都是一样的[页表不是同一张
]
④父子进程的PCB,mm_struct,页表不是同一个,数据则已写时拷贝到方式实现独立
只有子(父)进程修改数据,此时触发写时拷贝
所以就再给这个修改的数据开辟一些物理内存,并改变页表中存储的对应的物理地址修改,虚拟地址不变
所以环境变量之所以会被子进程看见(继承)
是因为环境变量也在mm_struct中,并通过页表映射,存储在了物理内存中
子进程又会继承父进程的mm_struct和页表,而且不修改的时候,里面的值是完全一样的,虚拟地址和物理地址也是一样的
所以子进程继承之后,也能通过自己的页表看见从父进程那里来的环境变量存储在的物理地址
父进程的全局变量为什么能被子进程看见?
其实和上一个问题一样,因为子进程继承了父进程的mm_struct和页表
所以子进程能通过自己的页表,找到父进程全局变量对应的物理地址,也就能看见里面的值了
页表
页表中的标志位
rwx这3个标志位:
标识这页表中这一项数据/代码的读,写,可执行权限
即
这一项数据有rw权限,那它就能在进程运行时被读和修改
比如我们代码中的int,double等类型的数据
如果只有r权限,那它就只能读,不能被修改。如果尝试修改,就会报错 进程都有可能死掉
比如,C语言中的
char *p="我爱我的祖国"
*p="abc"
编译是能通过的,因为编译器不知道p指向的数据能不能改
但是运行时会报错,因为操作系统知道p指向的数据不能改
这也是为什么语言中会出现const关键字的原因
因为一项数据能不能改是运行时的问题,是操作系统该做的事
所以编译器根本不知道这个数据能不能改,只能由程序员告诉编译器它不能改
即
const char*p="我爱我的祖国"
所以给普通变量加const的作用是告诉编译器这个变量不能改
而不是真的把它变成了常量,页表中这一项数据大概率还是"rw"权限
只不过修改const修饰的变量编译根本就不会通过,根本没有运行的机会
isexists
标识这一项数据/代码是否在内存中
因为如果一个进程的代码和数据很大的话,它是不会一次性被加载到内存的
且不说内存是否放得下
多数情况只会加载,大概一次时间片能运行的代码和数据
而且一次时间片使用完之后,这个进程等待时,还要把使用完的加载到内存的这一部分在释放掉,节约内存
又比如,进程阻塞挂起时,它的代码和数据也不在内存中
如果进程要使用这项数据了,但是它不在,那么操作系统就帮进程再加载进内存
这样就能分段操作以尽可能少的内存执行尽可能多的进程/进程操作
所以
页表虚拟内存和物理内存之间并非是实时的
甚至大多时候是:
进程先把虚拟地址放进页表里,页表中这些虚拟地址右侧的物理地址都是空的
当进程通过页表找内存要的时候,才把对应的物理地址放进页表,给进程使用
mm_struct等结构体如何初始化
其实源文件编译形成可执行文件时,就会自己把代码和数据按照规则进行划分了,也记录了对应分区的一些属性(大小等)
操作系统调用进程的时候,直接读取对应分区的代码和数据
再轻微加工就能知道
①mm_struct中的代码区开多大,栈区开多大
②页表的初始化,页表里面的数据的权限是什么
③以及其他的这个进程相关的结构体如何初始化
为什么要有进程地址空间
①进程地址空间+页表可以很好地保护内存
因为进程要访问物理地址,就必须先通过页表的检查(权限是否支持修改,这个虚拟地址有没有对应的物理地址[是不是野指针]等
)
只有你的操作合法操作系统才会让你访问物理内存
什么是野指针?
其实就是用户使用的虚拟地址,因为没有指向物理地址/指向了错误的物理地址
,没有通过页表的检测,操作系统就杀掉了这个进程
②进程管理和内存管理在系统层面进行了解藕合
进程因为操作系统给它画的大饼(进程地址空间)它就认为我想干嘛是一定有内存的,根本不用考虑内存是否充足
进程不管三七二十一就直接把需要的内存转换成虚拟内存往页表里面放
所以
进程需要使用页表中对应的数据的时候就直接向物理内存要
物理内存知道了之后只需要开辟一块空间,再把这块空间的地址放在页表右侧,根本不需要知道进程拿着这块内存干嘛
③让内存以统一的视角看待物理内存
进程使用的虚拟进程地址空间中会分区
但是其实这些虚拟内存对应的物理内存是没有区域划分的,没有只能读/只能写的物理内存
只是通过页表进行了权限控制
所以进程的代码和数据可以加载到内存的任意位置
甚至可以这里放一些代码/数据,隔老远再放一些代码/数据
大大提高了物理内存的利用率
所以一个进程的代码和数据即使在物理内存存放时并不连续
但是因为页表的映射,在虚拟内存中所有的代码和数据是在一起的,进程使用的时候也十分方便
相当于物理内存中也存放在一起
虽然可以不把一个进程的代码和数据放在一起,但是操作系统还是会尽量地把它们放在一起
因为这样可以提高CPU高速缓冲命中率
④编译器在编译代码的时候,直接使用虚拟地址,不用管物理地址
所以把操作系统与编译器地址方面进行了解藕
操作系统如何管理进程地址空间?
进程地址空间(mm_struct)与进程的PCB是强绑定的
也就是PCB在mm_struct就在
又因为PCB有一个指针指向mm_struct,所以通过PCB一定能找到mm_struct
所以操作系统管理mm_struct,只需要管理好PCB就间接管理好了mm_struct