上图父进程的代码和子进程一致,只不过没有改变g_val的值。
子进程中改变了g_val的值,而父子进程互不影响,结果也符合预期,子进程中的g_val是300,父进程中的g_val还是100。
但是,父子进程中g_val的地址却是一样的!直接震碎三观!这里我们要知道,这肯定不是物理地址,是虚拟地址。
1.虚拟地址空间
地址空间本质就是内核中的一个结构体对象。
每一个进程都有自己独立的地址空间,每一个进程都有自己独立的页表。
地址空间就是以前语言部分讲的栈区,堆区,静态区等等,它是虚拟的。
而页表会根据虚拟地址空间映射到物理内存中,操作系统根据页表将虚拟的地址空间转化成物理内存,从而访问到数据。
拿32位的系统来说,地址空间的大小在4G左右。
在父进程创建子进程时,子进程会把父进程很多内核数据结构全拷贝一份,包括PCB,虚拟地址空间和页表。但有些内核不拷贝,如Pid等。
在子进程修改数据时,系统会在物理内存中新开一份空间,将旧空间的数据拷贝过去,把页表中物理内存和虚拟地址映射好,然后再将新空间的数据修改。这样表面看上去,父子进程同一变量的值不一样,但地址是一样的!实际上,它们的物理地址是不一样的!(这个过程叫写时拷贝)。
当然,如果子进程不写数据,父子进程同一变量的物理地址是一样的。
如果在父进程创建子进程时,将父进程的数据全拷贝一份给子进程会浪费空间和时间。
写时拷贝的意义是:按需申请,通过调整拷贝的时间顺序,达到有效节省空间的目的!
2.地址空间的本质
这张图说明了一个问题:地址空间就是内核的一个结构体,start和end划分空间。
3.地址空间存在的意义
1.将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域!
因为在物理内存中开空间是无序的,不方便管理,但在地址空间可以是有序的,方便管理。
2.进程管理模块和内存管理模块进行解耦。
例如:申请空间时可以先在地址空间中开空间,等真正要用的时候才去物理空间中申请,中间的时间差可以把空间先给别的变量用,这样空间利用率更高一些!
3.拦截非法请求->对物理内存的保护。
如果访问越界,操作系统会直接拦截,不会访问和修改到物理内存。
为什么这个常量字符串不让修改呢?
因为在页表处,访问物理内存时,系统发现没有对应的权限,会对命令进行拦截。
创建一个进程一定要创建PCB,创建地址空间,创建页表,在页表中,构建从虚拟地址到物理地址之间的映射关系。
未来,CPU调度执行的时候,都是访问代码区,然后通过页表映射到物理内存,找到对应的代码和数据,然后执行的。
这时就可以理解id是同一个变量,为什么有两个值了。
fork会返回两次,返回的过程中就是对id写入的过程,会发生写时拷贝。此时,id的虚拟地址空间相同,物理内存不同。
4.进程优先级的处理方式
系统中有一个队列叫runqueue。其中有一个东西叫queue[140]。这个数字中每一个元素是一个结构体指针,Linux系统用下标100到139。一共40个等级分别对应进程优先级的60到99。如果有相同优先级的进程,则以链表的形式链接到queue[140]对应下标的元素上。
还有一个东西叫bitmap[5]里面放了5个long型,一个long型是32个bit位,一共160个bit位。前140个bit位中,每一个bit位代表一个下标,为0表示对应的优先级没有进程,为1表示对应的优先级有进程。系统对其遍历的时候也很方便,以一个long为单位进行遍历,为0的表示那32个下标都没有进程,不为0时,在对那32位具体遍历。
然后runqueue队列中还有两个指针:1.active。2.expired。active指向活跃进程在的队列,expired指向过期进程指向的队列。
CPU会直接调度活跃进程,其所在的队列只出不进,另一个队列只进不出。
等活跃队列中的进程指向完毕,同过swap交换两个指针的内容,CPU继续调度另一个队列中的进程。
更改优先级也在这个过程中完成,当用户设置新的nice值时,正在运行的进程的优先级暂时不变,等它的时间片结束,换到另一个队列时,再按照新的nice值给该进程安排优先级。
这种算法是谷歌的一位工程师设计出来的,称为进程调度O(1)算法!
像这种操作系统叫做分时操作系统,讲求公平性,每个进程分配的时间都比较合理。
还有一种操作系统叫实时操作系统,在一些高端的汽车中会装这样的车载系统,它是讲求某必要的进程必须执行完成才能执行其他进程。比如用户要刹车,那么该进程就必须执行完才可以执行其他进程!