一、fork入门知识
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。可以简单地说fork()的作用就是创建一个子进程。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
什么是进程
1)进程是动态的,程序是静态的
当一个程序调用的时候,就创建了一个进程;进程在运行的时候是具有独立性的,不影响其他进程
2)进程=内核数据结构+进程的代码和数据
代码:只读
数据:当有一个执行流尝试修改数据的时候,OS会自动给我们当前进程触发写时拷贝
写时拷贝(copy-on-write, COW)就是等到修改数据时才真正分配内存空间,这是对程序性能的优化,可以延迟甚至是避免内存拷贝,当然目的就是避免不必要的内存拷贝。
这里引入一个概念叫做PCB:进程控制块。也就是linux自己创建的Task_struct结构体。进程控制块是系统为了管理进程设置的一个专门的数据结构,用它来记录进程的外部特征,描述进程的运动变化过程。
子进程完全拷贝父进程的PCB,但并不是同一个;父子进程代码共享,数据独有;同一个变量在父子进程> 的地址完全一样,OS中虚拟内存机制保证父子进程运行独立互不干扰
创建一个子进程实际上是创建了一个PCB,父进程与子进程看到的是同一份代码和数据
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。
二、fork进阶知识
以下面的代码为例,使用<unistd.h>头文件,调用fork()函数。其中getpid()函数的作用是获取进程id,getppid()函数的作用是获取父进程id
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("AAAAA\n");
fork();
printf("BBBBBBBBB pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
return 0;
}
运行后的结果是
可以看出AAAAA只打印了一行,因为此时并没有执行fork()函数,只有初始执行的代码。之后通过fork()函数创建了一个子进程,因此BBBBBBBB打印了两次。
第一次打印B的时候,pid:25251 ppid:25728。而第二次打印B的时候,pid:28252 ppid:25251父进程的进程id与第一次打印的进程id相同。
因此可以说明fork()函数创建了一个id为28252的子进程,它的父进程id为28251
下面来考虑循环中使用fork()的效果
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int i = 0;
printf("i son/pa ppid pid fpid/n");
//ppid指当前进程的父进程pid
//pid指当前进程的pid,
//fpid指fork返回给当前进程的值
for(i = 0; i < 2; i++)
{
pid_t fpid = fork();
if(fpid == 0)
printf("%d child %4d %4d %4d/n", i, getppid(), getpid(), fpid);
else
printf("%d parent %4d %4d %4d/n", i, getppid(), getpid(), fpid);
}
return 0;
}
运行结果是:
i son/pa ppid pid fpid
0 parent 2043 3224 3225
0 child 3224 3225 0
1 parent 2043 3224 3226
1 parent 3224 3225 3227
1 child 1 3227 0
1 child 1 3226 0
第一步:在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是p3224和p3225(后面我都用pxxxx表示进程id为xxxx的进程)。可以看到父进程p3224的父进程是p2043,子进程p3225的父进程正好是p3224。我们用一个链表来表示这个关系:
p2043->p3224->p3225
第一次fork后,p3224(父进程)的变量为i=0,fpid=3225(fork函数在父进程中返向子进程id)
p3225(子进程)的变量为i=0,fpid=0(fork函数在子进程中返回0)
第二步:假设父进程p3224先执行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p3226
对于此时的父进程p2043->p3224(当前进程)->p3226(被创建的子进程)
对于子进程p3225,执行完第一次循环后,i=1,接着执行fork,系统中新增一个进程p3227
p3224->p3225(当前进程)->p3227(被创建的子进程)
从输出可以看到p3225原来是p3224的子进程,现在变成p3227的父进程。父子是相对的
第三步:第二步创建了两个进程p3226,p3227,这两个进程执行完printf函数后就结束了,因为这两个进程无法进入第三次循环,无法fork,该执行return 0;了,其他进程也是如此。
细心的读者可能注意到p3226,p3227的父进程难道不该是p3224和p3225吗,怎么会是1呢?这里得讲到进程的创建和死亡的过程,在p3224和p3225执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。p3224和p3225死亡后,p3226,p3227就没有父进程了,这在操作系统是不被允许的,所以p3226,p3227的父进程就被置为p1了,p1是永远不会死亡的
对于这种N次循环的情况,执行printf函数的次数为2*(1+2+4+……+2N-1)次,创建的子进程数为1+2+4+……+2N-1个。
printf的缓冲机制:printf某些内容时,操作系统仅仅是把该内容放到了stdout的缓冲队列里了,并没有实际的写到屏幕上。
但是,只要看到有/n 则会立即刷新stdout,因此就马上能够打印了