初识fork函数
fork函数是从已经存在的进程中创建出一个新的进程。新进程为子进程,原进程为父进程。
#include <stdio.h>
#include <unistd.h>
int main()
{
pit_t pid = fork();
return 0;
}
返回值: 子进程返回 0,父进程返回子进程 id,如果出错返回 -1。
进程调用fork函数,当控制转移到内核中的fork代码后,内核会这样做:
- 分配新的内存块和内核数据结构给到子进程;
- 将父进程部分数据结构内容拷贝到子进程中;
- 添加子进程到系统进程列表中;
- fork函数返回,调度器开始调度。
#include <stdio.h>
#include <unistd.h>
int main()
{
int ret = fork();//创建子进程
if(ret<0)//创建失败
{
perror("fork()\n");
return -1;
}
else if(ret==0)//子进程
{
while(1)
{
//分别打印出子进程的返回值,子进程pid以及子进程的ppid
printf("i am child, ret=%d,pid=%d,ppid=%d\n",ret,getpid(),getppid());
sleep(1);
}
}
else//父进程
{
while(1)
{
//分别打印出父进程的返回值,父进程pid以及父进程的ppid
printf("i am father, ret=%d,pid=%d,ppid=%d\n",ret,getpid(),getppid());
sleep(1);
}
}
return 0;
}
从下图中可以看出,父进程的pid为95825,子进程的pid为95826;子进程的ppid也就是父进程pid,而父进程的ppid就是bash(命令行解释器);父进程的返回值是子进程的pid,而子进程的返回值为0。
最终的输出结果不仅可以打印出来。我们也可以使用命令“ps aux | grep test1”显示出来。
我们都知道通过调用fork函数创建出一个子进程,在内核中,它是拷贝了一份父进程的PCB给到了子进程。在执行代码时,先把fork函数之前的代码运行完成,调用fork函数,在PCB中的程序计数器存放将要被执行的下一条指令,在子进程被创建成功的同时我们也得到fork函数的返回值,根据返回去调用该执行的进程。如果返回值时小于0的,那么说明创建子进程失败,如果返回值大于0(也就是子进程的pid),则根据父进程PCB中的程序计数器得到将要执行的下一条语句,再从上下文数据中得到fork函数之前的数据,执行父进程,如果返回值等于0,那么就直接进入子进程执行。
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。父子进程的执行顺序完全根据有调度器决定,也就是看它们的执行优先级。
程序地址空间
#include <stdio.h>
#include <unistd.h>
int g_val = 10;
int main()
{
int ret = fork();
if(ret < 0)
{
perror("fork()\n");
return -1;
}
else if(ret == 0)
{
while(1)
{
printf("i am child,%d,%p\n",g_val,&g_val);
sleep(1);
}
}
else
{
while(1)
{
printf("i am father,%d,%p\n",g_val,&g_val);
sleep(1);
}
}
return 0;
}
输出:
我们可以看到输出的变量值和地址都是一样的,这个很容易理解,子进程拷贝父进程,并没有进行修改,所以它们共用这个全局变量。但是,当我们在父进程中对这个变量重新赋值:
#include <stdio.h>
#include <unistd.h>
int g_val = 10;
int main()
{
int ret = fork();
if(ret < 0)
{
perror("fork()\n");
return -1;
}
else if(ret == 0)
{
while(1)
{
printf("i am child,%d,%p\n",g_val,&g_val);
sleep(1);
}
}
else
{
while(1)
{
g_val=20;
printf("i am father,%d,%p\n",g_val,&g_val);
sleep(1);
}
}
}
我们发现,父子进程的输出地址是一样的,但是变量值发生了变化。所以得出以下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量;
- 但是地址是一样的,说明该地址绝对不是物理地址;
- 在linux地址下,这种地址叫做虚拟地址;
- 我们所能看到的地址都是虚拟地址,物理地址用户一般看不到。
从这个图中可以看出,同一个变量,地址相同(进程的虚拟地址空间),当一个进程对变量进行修改时,地址不会改变,但是会在物理地址中重新开辟一段空间存放修改后的值。
写时拷贝
- 当fork的时候,如果父子进程不修改数据,则页表的映射关系不会改变;
- 当其中有一方修改数据的时候,为了防止导致另外一方读取到的数据是错误的,所以需要在物理内存当中重新开辟一段空间,保存修改后的值,并且将修改的进程的页表结构当中的映射关系重新指向新的物理内存
fork调用失败的原因:
- 系统中有太多的进程;
- 实际用户的进程数超过了限制。