大家好,我是苏貝,本篇博客带大家了解Linux进程(7):地址空间,如果你觉得我写的还不错的话,可以给我一个赞👍吗,感谢❤️
目录
- (A) 直接看代码,看现象
- (B)基本理解
- (C)细节
- 1. 如何理解地址空间
- a.什么是划分区域
- 2. 为什么要有地址空间
- a.将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
- b.进程管理模块和内存管理模块进行解耦
- c.拦截非法请求
- 3. 进一步理解页表
- 4. 进一步理解写时拷贝
(A) 直接看代码,看现象
修改.c文件
运行进程
先看黄色框,这个我们能理解,因为之前说过父子进程的数据,如果任意一个都不对数据写入,那么数据就是共享的,所以它们指向同一块数据空间,所以地址相同
再看红色框,我们也能理解。因为进程具有独立性,所以子进程对数据的修改,父进程是看不到的,所以它们打印同一个全局变量,但值不同
再看蓝色框,这就不能理解了,同一个地址的值怎么可能既是100也是300?所以这个地址绝对不可能是物理地址,是虚拟地址。引出下一个概念:虚拟地址
(B)基本理解
当我们将程序加载进内存中时,OS在物理内存中一定要给这个程序开辟一块空间,来保存进程的代码和数据。OS要创建进程的task_struct。我们之前只说task_struct能指向内存中的代码和数据,事实远比我们想的复杂
在OS内部还要创建地址空间(每个进程都有独立的地址空间)。地址空间在32位机器和64位机器上的大小是不同的。我们以32位机器为例(下面讲的都是在32位机器下),地址空间从低到高一共有4GB的空间。我们之前用C语言打印的地址都是地址空间范围内所对应的地址,而非物理地址
进程的PCB会指向地址空间。
进程的代码和数据都在物理内存里面,进程想访问数据时,数据并不在地址空间上保存,地址空间会给我们提供线性的连续的地址(即虚拟地址),让我们未来提供虚拟地址找到物理地址
如何通过虚拟地址找到物理地址呢?
在计算机体系结构中还存在页表(每个进程都有自己独立的页表)。页表主要负责将地址空间的虚拟地址和对应的物理地址之间建立映射关系。只要建立好了映射关系,未来上层使用虚拟地址访问时,OS会自动拿着虚拟地址查页表转换成物理地址,最后访问到数据
现在有进程中有全局变量g_val,&g_val得到的0x601054是虚拟地址,假如g_val的物理地址是0x11223344,那么在页表中就会存储这些地址
现在进程创建了一个子进程,OS也会为子进程创建task_struct、地址空间和页表(每个进程都有自己独立的地址空间和页表)。
子进程没有代码和数据,它会继承父进程的代码和很多属性,相当于父进程pcb里的很多属性就可以用来初始化子进程。所以子进程的task_struct除了pid,ppid等,大部分属性都和父进程的一样。
子进程的地址空间和页表都是直接拷贝父进程的,所以子进程的地址空间里也有g_val的虚拟地址0x601054,子进程的页表里也有g_val的物理地址
现在让子进程对数据进行写入:将g_val的值从100改为300。
由于进程具有独立性,所以子进程对g_val的修改,不能影响父进程,所以肯定不能在0x11223344对应的空间修改。OS在写入时,发现g_val不仅被子进程在使用,还同时被父进程使用,所以写入暂停,OS在物理内存中重新开辟一块空间,假设物理地址为0x22334455,然后将g_val的值100拷贝到新空间中。再用新的物理地址覆盖页表中老的物理地址,重新构建映射。
上面的工作(叫写时拷贝)做完,OS再继续执行写入操作。将新的空间的值改为300
至此,子进程修改g_val的值,只是修改了物理内存和页表,可是上层用到的虚拟地址依旧是0x601054,虽然虚拟地址相同,但被映射到物理内存的不同的区域,所以出现了我们在(A)直接看代码,看现象里地址一样,但值不同的情况
(C)细节
为什么要写时拷贝?
我们上面说了是为了保证进程的独立性。那为什么不在创建子进程的时候,就把数据全部给子进程拷贝?因为如果有数据是父子进程都不需要修改的话,那将这些数据也给子进程拷贝一份,这不就是在浪费空间吗?所以,写时拷贝的本质就是按需申请
1. 如何理解地址空间
a.什么是划分区域
在小学的时候,大家都应该和同桌在桌子上划过“三八线”吧,现在假设你和同桌2个人共用一个100cm的桌子, 你们每个人50cm,那这如何用计算机语言来描述呢?
只需要构建2个结构体,第二个结构体表示一个课桌分为左右两部分,第一个结构体表示每部分的开始和结束位置,再构建第二个结构体的结构体变量,最后将左右两块空间的起始和终止位置都赋值即可
如果同桌太过分了,每次都侵占了属于你的10cm区域,再用计算机语言来描述
事实上,地址空间本质是内核的一个struct结构体(struct mm_struct),内核的很多属性都是表示start和end的范围。如何证明呢?
让我们来查看Linux的源代码,我们看到有许多表示开始和结束的变量
2. 为什么要有地址空间
a.将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
如果没有地址空间,那么进程的task_struct就要能指向物理内存中对应的所有的数据和代码,这对进程来讲是比较困难的
现在有了地址空间和页表。实际的物理内存中,代码区、数据区、堆区……都是无序的,如果让进程的task_struct直接指向物理内存的对应的各种代码和数据区,那么可能从低地址往高地址,第一个是初始化数据区,第二个是堆区……就不像有了地址空间和页表,在进程的task_struct视角,从低地址往高地址一定是代码区、初始化数据区……
所以地址空间的第一个好处就是将无序变有序(对task_struct),让进程以统一的视角看待自己运行的各个区域
b.进程管理模块和内存管理模块进行解耦
比如现在进程要申请一段堆空间,那先在地址空间的堆区申请一段空间,但是进程不是立马就要用,所以暂时不在物理内存申请空间,也不在页表建立映射关系(只有虚拟地址,没有物理地址),等到进程需要用这段空间,再在物理内存申请空间并建立映射关系
换言之,如果要对进程做各种管理,那么内存管理都可以延迟处理,因此地址空间和页表的存在,能让进程管理模块和内存管理模块解耦
c.拦截非法请求
当我们写的代码在遍历时要访问地址空间的堆区(我们在上层使用的都是虚拟地址),但发生了越界,此时将这个越界的虚拟地址到页表中查,发现没有这个虚拟地址,证明没有对应的映射关系,所以OS就拦截了这次请求,不让它做任何操作,就不会有往物理内存中写入的操作,所以能拦截非法请求
3. 进一步理解页表
CPU内的寄存器(如CR3)能将当前页表的地址保存在CPU内
MMU(硬件):将虚拟地址结合页表转换成物理地址
页表中有许多标记位,比如用来确定当前物理地址指向的空间是否在内存中,是:标记位为1;否:标记位为0
什么情况下,当前物理地址指向的空间不在内存中呢?
进程挂起。如果操作系统内存特别吃紧,且进程处于阻塞态,那么操作系统会将内存中的代码和数据加载到磁盘的swap分区,那么此时物理地址指向的空间就不在内存中,该标记位就为0
页表中还有rwx权限标记位。我们以前见过下面的代码,你觉得这个代码能被VS运行通过吗?
显然不能,在语言层面上讲,因为”hello world”是常量字符串,位于字符常量区,所以不能被修改
在操作系统层面,就是字符常量区的虚拟地址在页表上的rwx权限标记位为r,没有w,所以在想将“web”写入时,系统检测到了错误,在转化成物理地址中将进程终止,所以根本没有写入物理内存
4. 进一步理解写时拷贝
在进程没有创建子进程时,全局变量g_val的虚拟地址被记录在页表中,对应的权限标记位为rw,可读可写
进程创建子进程后,全局变量g_val的虚拟地址被记录在页表中,对应的权限标记位被OS设为r。子进程会拷贝父进程的地址空间和页表,所以子进程的g_val在页表对应权限标记位也是r
当父子进程任意一个想对g_val进行写入时,将g_val的虚拟地址到页表中查,发现其权限标记位为r,OS识别到错误,开始判断
- 是不是数据不在物理内存(进程挂起了,页表中对应标记位为0):如果是,触发缺页中断(让OS重新在物理内存中开辟空间,重新建立映射,把标记位置为1,然后再继续访问)。这属于正常情况
- 是不是数据需要写时拷贝(OS如何知道需要写时拷贝,这个以后再讲),如果是,就发生写时拷贝
- 上面2种都不是,进行异常处理
好了,那么本篇博客就到此结束了,如果你觉得本篇博客对你有些帮助,可以给个大大的赞👍吗,感谢看到这里,我们下篇博客见❤️