一.前言
在上一篇文章-进程的概念,最后我们提到了创建进程的方式有两种方式,一种是手动的创建出进程,还有一种就是我们今天要学习的使用代码的方式创建出子进程-fork。
而学习fork创建出进程的过程中,我们会遇到以下问题,待会我们会一一的解决掉。
I fork干了什么?
II 为什么要创建出子进程?
III 为什么fork的两个返回值,给父进程返回子进程的pid,给子进程返回0
IV fork之后,父子进程谁先运行
V 为什么fork会有两个返回值
VI 同一个变量为什么会有两个值
二.fork干了什么?
fork是一个系统调用函数,我们可以使用fork,也就是代码的方式创建出一个进程出来。
这里文档说的很清楚,fork可以创建出一个子进程出来,是不是呢?我们验证一下便知。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("这是一个父进程,pid:%d,ppid:%d\n",getpid(),getppid());
fork();//不带返回值
printf("这是一个进程,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
return 0;
}
在fork之前,打印了父进程这一行,在fork之后,打印了两行,可以看出第三行的ppid是第一行和第二行的pid。
由此验证成功:fork之前父进程执行代码,fork之后,创建出子进程,和父进程一起执行后续的代码。
三.为什么要创建出子进程?
我们创建出子进程具体是为了干嘛呢?显然是为了让子进程完成和父进程不一样的工作,执行不一样的代码。
那怎么保证父子进程可以执行不一样的代码呢?
上述我们可以看出fork是有返回值的,通过返回值我们判断谁是子,谁是父,然后让它们执行不同的代码片段。
比如说需要执行边下载边播放的任务,这可能是单进程完成不了的,这时候就需要我们使用fork创建出子进程,然后通过返回值,将代码进行分流,父进程执行播放任务,子进程执行下载任务。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("这是一个父进程,pid:%d,ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)//子进程的运行
{
while (1)
{
printf("我是一个子进程,我正在进行下载任务,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else//父进程的运行
{
while (1)
{
printf("这是一个父进程,我正在进行播放任务,pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
sleep(1);
return 0;
}
fork创建出子进程,系统中就会多一个进程,子进程会以父进程为模版,为子进程创建出PCB,子进程PCB的大部分信息和父进程PCB中的内容是一致的,其中少部分不一样,比如进程的pid和ppid等。
但是现在创建出来的子进程是没有代码和数据的,那子进程的代码和数据从哪里来呢?目前子进程和父进程共享代码和数据!
所以fork之后,子进程会和父进程执行一样的代码。
那fork之前呢?子进程可以看见fork之前的代码吗?
答案是子进程可以看见父进程之前的代码,那子进程为什么没有执行fork之前的代码呢?
因为eip寄存器保存了代码的执行逻辑,当执行到fork之后,eip指向fork位置,eip也会被子进程继承。
所以这就是为什么子进程不执行fork之前的代码,而执行fork之后的代码。
四.为什么fork的两个返回值,给父进程返回子进程的pid,给子进程返回0
在我们现实生活中,一个父亲可以有很多子女,而一个子女只能有一个父亲。而在进程中也是一样的,父进程:子进程=1:n,作为父进程,如何将子进程给跟踪和管理好呢?父进程就需要知道子进程的pid,因为进程的pid具有唯一性。
而子进程通过返回0,知道自己是子进程,从而执行特定于子进程的逻辑。
五.fork之后,父子进程谁先运行
在上面我们可以看到,fork之后,都是父进程先运行,然后子进程再运行。每次都是遵行这种逻辑吗?
当创建出子进程之后,这只是一个开始,接下来是父进程,子进程和系统中的其他进程等待cpu调度执行。当父子进程的pcb都被创建并在运行队列中排队的时候,哪一个进程先被调度,哪一个进程就先运行!
其中谁先被调度,这是不确定,这是由各自PCB中的调度算法(时间片,优先级)+调度器算法等共同决定的。
六.为什么fork会有两个返回值
对于一个函数来说,代码逻辑执行到return了,它的核心工作做完了吗?答案是做完了。
fork函数中大致的做了哪些工作呢?
fork之后,代码共享,那return是代码吗?这是肯定的,return是代码,那么return也要被共享。
父进程被调度的时候,要执行return。子进程被调度的时候,也要执行return。所以说这就是为什么一个fork会有两个返回值。
真实情况是操作系统通过一些寄存器做到返回值返回两次。
七.同一个变量为什么有两个值
刚刚说到,为什么fork会有两个返回值,但是fork只有一个变量,一个变量为什么会有两个值啊?
当我们启动QQ和微信的时候,我们将QQ进程杀死时,会影响微信的运行吗?显然是不会的,所以说进程之前是具有独立性的。
那么父子进程之间是否具体独立性呢?
杀死父进程,子进程一样在运行
杀死子进程,父进程一样在运行
所以说不管是父子进程,还是什么进程,只要是不同的两个进程,他们都是互相独立的,各自有各自的PCB。
进程=可执行程序+PCB,其中pcb是各自私有的,可执行程序中包含代码和数据,代码本身就是可读的,也没谁去修改代码,
但是数据父子进程是可能会修改的。
比如,父进程中有个全局变量,当全局变量是0的时候,父进程就退出了,而子进程会修改全局变量为0,这样父进程不就因为子进程的修改,而导致退出了吗?这样还怎么保证进程的独立性呢?
所以说,数据各个进程必须各自私有一份!这里用到的技术是写时拷贝!
写时拷贝(Copy-on-Write,简称 COW)是一种计算机程序设计中的优化策略。
当多个进程或线程共享一块数据时,一开始它们共享相同的物理内存。只有当其中一个进程或线程尝试修改这块数据时,才会真正为其创建一份独立的副本进行修改,而其他未修改的进程或线程仍然共享原来的数据。
这种策略的好处是可以减少不必要的数据复制操作,节省内存和系统资源,提高程序的性能和效率。特别是在涉及到大量数据的共享和少量修改的情况下,写时拷贝能显著优化性能。
写时拷贝就相当于深拷贝和浅拷贝的结合!
这里的id是父进程定义的变量,这个变量是数据,返回的时候,因为父子进程的返回id是不同的,各自写入了id值,发生了写时拷贝,所以一个变量会有不同的值。
按道理说,不同的值,他们的地址肯定不同才对,我们验证一下:
太奇怪了,同一个变量,他们是不同的值,但是他们的地址却是一样的,现在我们只能知道:该地址绝对不是物理地址!!!