前言

上一期我们对进程优先级、命令行参数以及环境和变量做了介绍!以前我们就提到过一个问题有了运行队列为什么还要有优先级?本期将带你揭晓!

本期内容介绍

虚拟地址空间的引入

虚拟地址空间的介绍

如何理解地址空间

为什么要有地址空间

如何进一步理解页表和写时拷贝

解决历史遗留问题

虚拟地址空间

1.1虚拟地址空间的引入

OK,我们先来看看这样的一段代码:

这段代码是创建了父子进程,分别打印其pid、ppid以及一个全局变量的值和地址, 5秒后子进程把g_val给修改成了300;OK,我们来看看结果:

这里的结果是,前5秒父子进程都是打印的100,5秒后子进程把g_val修改了变成了300, 所以以后子进程的g_val就是300,而父进程的不变!这一点也可以理解,我们知道进程=task_struct(PCB)+其代码和数据;fork之后产生了子进程,而子进程的本质是在操作系统中多了一个进程即多了一套task_struct(PCB)+代码和数据,而以前介绍过子进程的代码是可以继承父进程的,如果子进程不对数据修改,子进程和父进程共享数据(注意:这里可没说子进程继承父进程的数据!!);这里子进程虽然修改了数据但父子进程具有独立性,所以父子进程数据也得想办法独立,可以理解的!但是让我们震惊的根本就不是这个,而是父子进程的g_val的地址是一样的!按照以往的理解就是一个变量既等于100又等于300,这怎么可能??所以我们可以得出一个结论:上面g_val的地址绝对是物理地址!!!,不是物理地址(物理内存的地址)那是啥呢?其实这是虚拟地址!!

1.2虚拟地址空间的介绍

虚拟地址空间我们是学习过的,在C语言、以及C++的时候都是介绍过的;就是下面这个:

不过当时在C语言和C++没有介绍是因为,这个东西本身就不是语言的问题,而是操作系统层面的知识!例如上面的例子,如果没有学系统,看到上面的代码后三观直接震碎!OK,言归正传,继续上回到虚拟地址空间上,以前我们介绍过:当一个进程在被调度时,会先把他的代码和数据加载到内存,然后操作系统会为其创建一个PCB(task_struct)来描述该进程的属性,并把PCB以某种数据结构组织起来,方便管理,即"先描述,在组织"!其实,当一个进程加载到内存时系统并不会只创建一个PCB,还会创建地址空间和页表来一起管理该进程!

我们以前所有程序的地址都是虚拟地址,并非物理地址!这里的虚拟地址就是你虚拟地址空间的地址,每个进程再被加载前都会为其分配一个虚拟地址空间!

页表是进行虚拟地址和物理地址转换的!

页表和虚拟地址空间本质也是和PCB一样的进程内核数据结构,所以系统也会对其进行管理的,如何管理?先描述,在组织!

如下图:

OK,和以前的一样不在多说~!所以,有了虚拟地址空间和页表的介绍,我们就可以理解上述的g_val父子进程的地址为什么是一样的了!

fork之后有了子进程,即有了子进程的代码和数据、PCB、虚拟地址空间、以及页表!其中代码、PCB(pid、ppid等除外)、虚拟地址空间、页表都是继承自父进程的;此时,父进程和子进程的地址空间的内容是一模一样的,所以他们指向的数据也是一样的,即此时数据形成浅拷贝!也就是我们前面所说的父子进程共享数据,而后面子进程对数据修改了,此时操作系统检测到了之后会对子进程的数据进行写实拷贝(OS自主完成的,用户感受不到),然后让子进程修改,所以子进程修改的是父进程数据的拷贝,所以子进程修改后父进程的g_val不变!这也符合我们介绍的,进程具有独立性!

如下图:

OK,理解了这里就可回答下面的问题了!

1、如果父子进程不对数据进行修改,是不是父子进程共享的呢?

是的,刚刚上面介绍了,父子进程如果不对数据进行修改,他们是都是共享一份数据的!

2、为什么要实行写实拷贝?可不可以在创建子进程时,把父进程的数据全部给子进程拷贝一份?

写实拷贝的本质是一种按需申请,通过拷贝的时间顺序达到有效节省空间的目的!

例如,进程的数据有环境变量、命令行参数等这些子进程几乎是不会修改的,如果你直接在创建子进程的时候拷贝一份话,会造成不必要的空间浪费!因为这些东西很少修改,父子进程共享即可,如果等到子进程真正的需要修改的时候给他开空间,写入即可!另一方面,即使子进程真的有内存需要,例如他在5秒后要一段malloc或new的空间,如果是一开始给子进程拷贝一份的话,前5秒他不用,这样一来是不是在5秒的时间内浪费了这一块空间?系统说不一定在这5秒内可以拿着这些空间做更多的事情,如果是写实拷贝的话,一开始只给页表一个虚拟地址,其对应的物理地址可以不给,等到真正用的时候再去申请即可!这样就提升了内存实时的使用效率!

1.3如何理解地址空间?

地址空间的本质就是内核的一个struct结构体!内部有很多的start和end,表示某个区域的范围!

我们举个例子就可以理解了:

假设你是一个上小学的文静的小男生或小女生,你的同桌是一个非常邋遢的同学,是不是把这放桌上,把那放桌上,你有一次受不了了,就在桌子上画了一条38线, 假设桌子长100cm,38线是在50cm处;左边属于你,右边的属于他!此时你对你们的桌子进行了区域划分,同理,你有一块内存空间,要分为很多的区,是不是也可以向上面的38线一样分区!

假设你同桌今天又把鞋子放到你桌边上了,你一起之下,把38线向右移动了20厘米直接73开:

所以这样就把区域划分变成了对每个对象的start和end的修改!其实内核中也是类似这样的,OK,来看看内核中的地址空间的区域划分:

1.4为什么要有地址空间?

要理解这个问题就先得理解好地址空间!OK,还是来一个例子:

在遥远的漂亮国有一个土豪,他有n个私生子,这些私生子相互不知道对方的存在!现在土豪先找到大儿子,说Tom最近老爹听说你在微软上班了?大儿子说嗯,土豪说可以不愧是我儿子,好好干老爹我现在身价100亿美金,等以后都是你的!大儿子一听好呀好呀~!然后土豪又找到二儿子,他二儿子在考博士,土豪说儿子听说你在考律师的博士?二儿子说是的老爹,土豪同样又说,好好学等你以后成功了,老爹的这100亿都是你的,二儿子也是很高兴的同意了~!过了几天这个土豪又找到他的私生女,说小爱呀听说你在做珠宝生意?小爱说是的呢,然后同样的套路给小爱说老爹现在又100亿你好好干以后都是你的~!小爱也点了头,然后又找到了它的小儿子说儿子听说你在准备申请上大学??小儿子点了点头,土豪又说,你只要能够申请到中国的清华大学老爹的这100亿都是你的.....

这里的土豪就是操作系统,这里的每个私生子就是每个进程,土豪给每个私生子(进程)画的大饼就是虚拟地址空间!

理解了这些现在再来谈一下为什么要有地址空间!主要有以下3个原因:

1、将无序变有序,让进程以统一的视角看待物理内存,以及自己运行的各个区域!

2、进程管理和内存管理进行解耦

3、拦截非法的请求(对物理内存的保护)

4、使每个进程保持独立性

什么意思呢?就是通过虚拟地址空间可以让每一个进程都以为自己的数据永远在某个地址处(虚拟地址)无需关心物理内存在哪里,其实它的真实的数据可能在物理内存的另一地址处!这样进程就变得更加好管理进程和内存了,这其实也是一种封装和C++STL迭代器类似,看似都一样,其实底层天差地别!这样每个进程都觉得每一个进程都是操作系统的唯一,而其实并不是~!关于拦截非法的请求,也是举个例子:比如说数组越界,其实是虚拟地址空间映射到页表时页表就已经检查出来了,这样就防止了对物理内存的随便乱访问,即保护了物理内存!

1.5如何进一步的理解页表和写实拷贝?

关于页表,我们这里只是非常简单的介绍了一下,什么多级页表都没提,这里只是先简单的见一下,后面动静态库以及多线程会在次介绍的!!这想说的意思是不要把页表像的过于简单了!OK,页表不光是对虚拟与物理地址的映射,还可以对非法的请求进行拦截即权限标志位的检查、是否在内存等!OK,再举个栗子:

char* str = "hello";
*str = 'H';

这段代码一执行就挂了,原因是给静态区的常量字符串赋值了!OK,语言层面是这样讲的,但是现在的问题是为什么不能够对静态区的常量赋值呢?其实是在赋值的时候再页表的时候被页表给拦截了!因为页表内部有一个标记位标记的是该地址处的数据是否有相应的权限,常量区的数据那必然只有写权限,你一写入页表就拦截了反馈给OS,操作系统就把你的进程给搞挂了~!

也就是,如果你的进程在进京某种操作时,都会经过页表的检查,如果合法让你操作物理内存,否则反馈给OS,OS会交给判断处理!例如,你的代码比如说由于内存压力大给把一部分挂起到磁盘了,当你进程在访问时页表一查虚拟地址存在但是物理地址不存在,此时发生错误,交给OS,OS检查完后发现是发生了缺页中断;再比如说在写入时发现要该数据被多个进程同时引用,此时反馈给OS,发现需要写实拷贝!如果上述都不是就需异常处理!

1.6历史遗留问题

我们在以前刚刚接触子进程的时候,这段代码:

当时看到一个变量的值即是大于0又是等于0,感觉三观直接崩塌了,其实现在介绍了虚拟地址空间后,现在再来看是很好理解的,fork后要return xxx;本质是对同一个变量id的写入,不管是谁先返回即写入就会发生写时拷贝,然后父子进程有了自己独立的物理内存的id而正在虚拟地址层面上他们的地址还是一样的!

OK,本期内容就介绍到这里,好兄弟,我们下期再见~!

结束语:我们不应该被眼前的一些所谓的华丽所迷惑,更应坚守初心,一步步的朝目标前进!

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部