前言
之前我们提到,创建子进程的时候,需要使用fork()函数,其中分别有id ==0和id >0的if函数,但是实验表明,两个if函数中的内容都得到了实现。按照我们之前所学,一个变量同一时间只能有一个值,那到底是怎么满足两个if都实现的呢?
1. fork函数
1.1 返回值:
fork函数在运行的时候创建新进程,新进程是子进程,旧进程是父进程。
fork函数的返回值:
>0:父进程,具体的数字是子进程的pid
==0:子进程
<0:出错
证明:
#include<stdio.h>
#include<unistd.h>
int main()
{pid_t id = fork();if(id==0){printf("我是子进程");printf("mypid:%d,myppid:%d\n",getpid(),getppid());printf("%p\n",&id);id = 5;// printf("%p\n",&id);// printf("%d\n",id);}else if(id>0){printf("我是父进程");printf("mypid:%d,myppid:%d\n",getpid(),getppid());printf("%p\n",&id);if(id==id){printf("1\n");}}return 0;
}
这里就能看出,子进程和父进程的关系以及>0的具体数字。
1.2 进程执行过程
当子进程创建后,会存在两个二进制代码相同的进程,分别是子进程和父进程,此时子进程中的数据和父进程的数据是相同的。因为子进程的创建是为了执行和父进程不同的任务,那么就避免不了对父进程的数据进行更改,那么此时操作系统就会给子进程重新开辟一块空间,存储子进程自己修改的数据,而未被修改的数据,就还是和父进程用一样的。这便是写时拷贝。
写时拷贝:等到修改数据时才真正分配内存空间,这是对程序性能的优化,可以延迟甚至是避免内存拷贝,当然目的就是避免不必要的内存拷贝。
1.3 页表
知道了进程执行过程,但是我们好像还是没有解决为什么id会有两个值同时出现的问题。
通过在父进程和子进程中分别打印id的地址,我们知道他们的地址是一样的。
但是我们知道,物理地址对应的数据是唯一的,那么我们拿到的地址就肯定不是物理地址。
因此此时打印的地址是虚拟地址。
这是因为页表的存在。
页表中存在着虚拟地址,物理地址,以及标记位(如进程对应的代码和数据是否已经加入到内存中),访问权限等。
如果没有加载到内存当中,那么就会触发缺页中断。即暂停进程执行,直到进程的数据和代码加载到内存中。
为什么会有缺页中断呢?因为CPU加载数据时是采用的惰性加载方式,当需要的时候才进行加载。这样能避免造成内存浪费。
其中虚拟地址和物理地址存在映射的关系。我们需要通过虚拟地址访问物理地址。
需要注意的是,写时拷贝并不影响虚拟地址,只会改变其映射的物理地址。
页表的作用:
1. 让所有进程能以统一角度看待内存。(让进程不必知道具体的物理地址)
2.让非法命令不能直接到达物理地址
3. 由于程序地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合(独立性)
即进程管理模块只需要使用虚拟地址,不用关心物理地址。
此时的进程 =内核(tast_struct + mm_struct)+数据和代码 。
mm_struct存储程序地址空间。
页表的首地址存储在CPU内存中的一个CR3寄存器中,此寄存器属于进程的上下文,当切换进程时,会带走该数据。
切换进程时还要切换程序地址空间,而pcb指向程序地址空间。