#include
#include
#include
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0)
{
g_val=1000;
printf("child:change-> g_val:%d,%p\n",g_val,&g_val);
}
else
{
printf("father:my->g_val:%d,%p\n",g_val,&g_val);
sleep(3);
}
sleep(1);
return 0;
}
我们知道,子进程会继承父进程的代码和数据。但是我在子进程中修改数据会发生写时拷贝。写时拷贝后面具体讲。上面的代码非常明显:我在子进程中将g_val修改为1000,父进程的g_val不受影响,每个进程都有独立性,这个大家都懂。那么g_val的地址应该会不同,因为同一个地址只能有一个值,我们来看看子进程和父进程的g_val地址是否相同。
惊奇的发现,g_val的地址尽然相同。这是打破以往常识的。
每个进程都有自己的代码和空间,进程可能需要对物理地址进行操作。但是如何管理进程代码在内存中的存储呢?让每个进程都可以直接和物理内存打交道,是威胁的行为,而且管理起来很复杂。
比如:我在内存中有两个进程,如果直接让进程操作物理地址,那么它有没有可能会占用另一个进程的物理地址,是有可能的,会造成很大的危害。所以引入虚拟地址。
为了方便管理所有的进程,每个进程都由struct mm_struct这一虚拟地址空间来管理。每个进程都认为自己拥有os的所有内存。
进程是由PCB来管理的,PCB中就有一个指针指向了mm_struct。mm_struct就存储了进程的代码和数据。
可以看到上图中,mm_struct被分成多段,如何实现的呢?其实比较简单。
我们来假设实现,不是源代码哦。
struct mm_struct
{
unsignde int code_start;
unsignde int code_end;
unsignde int date_start;
unsignde int date_end;
}
在32位下,内存是4GB,每个进程都按照4GB来规划虚拟地址空间。就是如上那样 [_start ,_end]为一个
段。那么就是从0X00000000……000~0Xfffffffff……fffff这样的来规划虚拟地址空间的。
虚拟地址是按照操作系统所有的内存,来规划的。那么该如何真正的使用物理地址呢?那就是构建映射关系:页表。
利用页表我们可以使虚拟地址和物理地址产生映射关系。
子进程会继承父进程的代码和数据,子进程的PCB是以父进程的PCB为模板来创建的,当然不是完全拷贝父进程的PCB,如PID,PRI等就不一样。那代码和数据是如何共享的呢?
嗯,就是这样的继承的。
页表中有一个权限,不知道大家注意到没有,每次操作都会对于页表中的权限,如果子进程一直都是读代码,那很简单,啥呀不用管;要是子进程要进行写入操作呢?就比如一上来的代码要进行修改变量的值,该怎么办呢?->写时拷贝。
假如子进程要修改变量,g_val。
(1)一开始是这样的,
(2)但是子进程,要修改g_val的值了。页表上的权限也表示你可以修改,但是在此之前你先稍等,这就是断页中断,你的先拷贝一下,物理地址中的g_val需要拷贝一份来供你修改,这就是写时拷贝。
(3)可以看到,发生写时拷贝后,g_val在物理地址上多了一份,这就保证了父子进程的独立性。在页表中断时,父子进程什么影响都没有,子进程只不过是要修改数据,页表中断,发生写时拷贝,这都是操作系统干的事。
总结:子进程会默认和父进程代码数据指向同一个物理地址,如果只读那么就相安无事,若要修改数据,那么会发生页表中断来完成写时拷贝,供给子进程来修改。所以默认情况下,只读的代码和数据,操作系统只维护一份;如果要求写入操作,那么会根据具体情况,发生写时拷贝。
有了以上内容的了解,基本上我们都懂了,g_val的虚拟地址是一样的,但是物理地址是分开的。物理地址不同,所以其实本质上是两个变量了。
昂,虚拟的确实不可信哟,但是虚拟的确实很香,香在哪里呢?