引入问题
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int g_val=100;
int main()
{
pid_t id=fork();
if(id==0)
{
int cnt=5;
while(1)
{
printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
if(cnt)
{
cnt--;
}
else
{
g_val=200;
printf("子进程change g_val:100->200\n");
cnt--;
}
}
}
else
{
while(1)
{
printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
运行结果:
现象:
在子进程把g_val改了之后,父进程的g_val值并没有改变
因为进程本身就是具有独立性的。即便是父子进程在代码层面上共享代码但在数据层面上也不能互相干扰。
当我们所对应的子进程去修改该数据时,要发生对应的写时拷贝,来保证我们双方在数据层面上实现一个互相解耦的状态。
但是,为什么我们从同一个地址处做检测竟然读出来不一样的值?
结论:如果我们对应变量的地址是真正的物理地址,这种现象是绝对不会存在的,要不然我们双方读取的内容应该是一样的。
所以呢,这个地址它绝对不能是物理地址,它称之为线性地址,或者叫做虚拟地址。(我们平时写的C、C++用的指针,指针里面的地址,全部都不是物理地址)
进程地址空间框架
当我们实际在进行运行一个程序时,运行起来它就会变成一个进程,而变成一个进程之后,那么在内核当中,在操作系统内部一定要为该进程创建对应的PCB及test_struct结构体。
此时就会存在父进程,
进程要有对应它的代码和和数据,这叫做加载到内存当中的可执行程序。
简单点说,这个可执行程序有自己的代码,还有自己对应的数据。
进程= 内核数据结构PCB+可执行程序
但实际上没有这么简单:
PCB一旦创建出来,那么紧接着我们对应的系统为了能够让该进程更好的去进行运行,除了内核创建PCB这样的内核结构之外,那么操作系统还要为该进程创建一个叫做进程地址空间的东西。
在我们编码的时候,那么在系统层面上还要为我们当前的父进程构建一个叫做页表的东西。
这个页表的左侧是虚拟地址 ,右侧是物理地址 。
当进程在执行访问这个地址时,操作系统可以自动的根据查页表,根据虚拟地址,将虚拟地址转化成物理地址,之后就可以访问到物理地址当中的内容。
当我们创建子进程时:
我们所对应的子进程,它会基本上以父进程模板来初始化它内部的结构体对象的各种值。那么当然也会有他自己私有的数据。比如说他的pid,他的优先级......
注意: 每一个进程都要有一个叫做进程地址空间 的东西。
所以子进程它也同样要把父进程的地址空间给自己也拷贝一份。
父子进程都必须有独立性。
所以子进程它目前的独立性就体现在它具有独立的PCB,也有自己独立的地址空间,有自己独立的页表结构,虽然很多字段都是以从父进程继承下来的。
由于子进程的页表是从父进程拷贝下来的,页面内容是一样的,跟父进程一样也指向可以指向同样的代码,所以父进程和子进程可以做到共享代码。
当我们把子进程数据修改之后:
就会在物理内存上新开辟一块空间,此时子进程的物理空间就不再指向该数据原父进程的物理空间了,这个过程是写时拷贝,由操作做系统自动完成。但是虚拟地址不会感知到,所以它不会改变。
我们打印的时候对应的地址是一样的,为什么呢?
我们子进程对应的地址空间是继承自于我们的父进程的地质空间,因为父进程和子进程打印变量的地址是虚拟地址 。这个地址就是我们在语言层面上的地址了。
但是呢,当我们进行实际访问的时候,读取是100和200,内容不一样,
本质是因为父进程通过虚拟地址查自己的页表,找到了物理地址的这个地址数,读到的是100,而我们子进程他经过value映射,映射到物理内存的另一个地址,所以看到的就是200。
所以我们在应用层面上看到的就是它们两个值不一样,但是虚拟地址是一样的。
进程地址空间细节深究
1.什么叫做地址空间?
我们对应的CPU,包括你的进程,要读取对应的代码和数据,那么最终一定是因为这个父进程或者子进程一定是要被调度的。
当一个进程它在CPU上正在被运行,要访问我们对应的内存时,它第一件事情一定是要告诉我们对应的内存,它要访问哪一个地址,所以CPU一般它要根据它对应的地址总线来进行访问。
在32位计算机中,有32位的地址和数据总线,地址总线排列组合形成地址范围[0,2^32)
2.如何理解地址空间上的区域划分?
我们不仅仅要看到划分的地址空间的范围
在范围内,连续的空间中,每一个最小单位都可以有地址,这个地址可以被该区域直接使用!!!
综上,什么是地址空间?
所谓的进程地址空间,本质是一个描述进程可视范围的大小地址空间内一定要存在各种区域划分,对线性地址进行start,和end即可;
地址空间本质是内核的一个数据结构对象,类似PCB一样,地址空间也是要被操作系统管理的:先描述,再组织
每一个进程创建时都要有自己对应的test_struct结构体,该结构体一定要能够指向自己对应的mm_struct(我们默认32个系统当中默认划分的区域为2^32次方4GB空间,将各类区域提前划分好,这个就叫做进程地址空间),所以进程就能初步知道代码在哪里,数据在哪里,堆在哪里......
为什么要有进程地址空间?
1.让进程以统一的视角看待内存
2.增加进程虚拟地址空间可以让我们访向内存的时候,增加一个转换的过程,在这个转化的过程
中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,
保护物理内存
3.因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合!
页表
1、页表的起始地址
每个当前正在执行的进程,CPU内部有一个寄存器,叫做cr3寄存器 。这个寄存器会保存当前进程页表的起始地址。这个地址是物理地址 。
问题:那么如果这个进程如果被切换走了,那么担不担心找不到的页表呢?
答案是:不担心。
因为当该进程在运行期间,这个cr3寄存器这个页表的地址也叫做寄存器内当前进程正在运行的临时数据。它本质上属于进程的硬件上下文。
所以呢,如果这个进程不运行了,进程切换走了,它是会把自己寄存器当中的内容带走的。
当进程回来的时候,再把地址空间当中曾经保存的页表的地址会重新恢复 上来。
所以自始至终,这一个进程都可以找到自己对应的页表。
2、页表中的权限标记位
当前我们要访问对应的这个数据的时候,我们怎么知道我要访问的这个区域,它是只能被我读取,还是可以被我写入呢?
答案是:页表中存在权限标记符,只有权限允许才能访问,否则相当于进行了一次非法操作,该进程将会被操作系统干掉。
3.页表中表示你对应的数据是否在内存当中的标志
如果可执行程序的该数据不在内存中,操作系统触发缺页中断帮我们重新设置内存,重新将我们剩下的代码数据加载到内存里,然后再让你再访问。
什么是进程?
进程= 内核数据结构(task struct && mm_struct && 页表)+程序的代码和数据