fork函数详解:进程创建与管理的关键技术
fork函数详解:进程创建与管理的关键技术
fork函数的基本概念
fork函数是Unix/Linux系统中用于创建新进程的重要系统调用。其返回值有三种情况:
- 在父进程中,fork返回新创建子进程的进程ID;
- 在子进程中,fork返回0;
- 如果出现错误,fork返回一个负值;
此外,还有两个相关的函数:
getppid()
:获取当前进程的父进程ID;getpid()
:获取当前进程的ID;
注意:在fork函数执行完毕后,如果创建新进程成功,则会出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
fork函数会复制已有的进程及其进程控制块(PCB),并为新进程分配一个PID。子进程的PID通常是父进程PID加1。
一个简单的fork示例
让我们通过一个简单的代码示例来理解fork函数的使用:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
pid_t pid;
// 判断1
if ((pid=fork()) < 0)
{
perror("fork error");
}
// 判断2
else if (pid == 0) // 子进程
{
printf("child getpid()=%d\n", getpid());
}
// 判断3
else if(pid > 0) // 父进程
{
printf("parent getpid()=%d\n", getpid());
}
return 0;
}
运行结果如下:
parent getpid()=13725
child getpid()=13726
这个例子展示了fork函数的一个重要特性:在调用fork后,fork函数后面的代码会执行两遍。这是因为fork函数创建了一个新的子进程,父进程和子进程都会继续执行fork后面的代码。
成功fork的执行流程
让我们梳理一下fork函数成功执行的流程:
- 执行
pid=fork()
,如果成功,pid将获得一个非0正值。如果失败,返回-1。 - 因为pid>0,所以进入判断3,这是在父进程中。
- 父进程的代码执行完毕后,程序会再次执行fork后面的代码,此时pid的值变为0,进入判断2,这是在子进程中。
注意:这里的pid_t
类似于一个类型,就像int型一样,pid_t
定义的变量都是进程号类型。这个语句的意思是定义了一个pid_t
类型的变量pid,fork()函数返回一个进程号,这个进程号赋给了pid。pid_t
在头文件types.h
(sys/types.h
)中定义。pid_t
实际上是一个short类型变量,表示的是内核中的进程表的索引。
更多fork示例
让我们尝试分析下面的代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t fpid; // fpid表示fork函数返回的值
int count=0;
fpid=fork();
if(fpid<0)
printf("error in fork!");
else if(fpid==0)
{
printf("我是子进程,id:%d\n",getpid());
count++;
}
else
{
printf("我是父进程,id:%d\n",getpid());
count++;
}
printf("统计结果是:%d\n",count);
exit(0);
}
父子进程的调用流程
下面我们讲解一下fork调用的细节:
int main(){
fork(); // fork1
fork(); // fork2
printf("love\n");
return 0;
}
上述代码会打印4次"love",创建了4个进程(1个父进程,3个子进程)。假设我们的main进程pid是1001,注意看左边的1,2,4进程其实都是main进程1001。进程3,6是同一个进程1002。所有一共有1001,1002,1003,1004四个进程。也就是只要数叶子节点就行了。其中1个是main进程,其它3个是子进程。有多少个进程就输出多少次hello字符串。也就是只有4,5,6,7执行了printf。
多个fork示例
eg1:
int main()
{
int n=2;
for(;i<n;i++)
{
fork();
printf("A\n"); // 遇到\n会自动刷新缓冲区
}
exit(0);
}
eg2:
int main()
{
int n=2;
for(;i<n;i++)
{
fork();
printf("-"); // 不会刷新缓冲区
}
exit(0);
}
eg3:
int main()
{
fork()||fork();
printf("A\n");
exit(0);
}
结果打印3个A,共创建3个进程。fork()给子进程返回一个零值,而给父进程返回一个非零值。在main这个主进程中,首先执行fork()|| fork(), 左边的fork()返回一个非零值,根据||的短路原则,前面的表达式为真时,后面的表达式不执行,故包含main的这个主进程创建了一个子进程,由于子进程会复制父进程,而且子进程会根据其返回值继续执行,就是说,在子进程中,fork()||fork()这条语句 左边表达式的返回值是0, 所以||右边的表达式要执行,这时在子进程中又创建了一个进程,即main进程->子进程->子进程,一共创建了3个进程。
eg4:
int main()
{
fork()&&fork();
printf("A\n");
exit(0);
}
结果输出3个A,创建3个进程。
注意事项
父子进程相同:
- 刚刚fork后,data段,text段,堆,栈,环境变量,全局变量,宿主目录位置,进程工作目录,信号处理方式
父子进程不同:
- 进程id,返回值,各自父进程,进程创建时间,闹钟,未决信号
父子进程共享:
- 文件描述符
- mmap映射区
- 读时共享,写时复制-----------------全局变量