Linux系统之fork函数详解

1. fork是什么

首先我们要了解fork是什么函数?

复刻(英语:fork,又译作派生分支)是UNIX或类UNIX中的分叉函数,在Linux中执行man fork即可认识fork

根据文档描述我们可以知道,fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。

2. fork函数初识

#include <unistd.h>

pid_t fork(void);

// pid_t是一个宏定义,其实质是 int 被定义在#include <sys/types.h>中,pid_t定义的类型都是进程号类型。
// 返回值:若成功调用一次则返回两个值,子进程返回 0,父进程返回子进程PID(一个正数)。出错返回 -1。

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。

int main()
{
	pid_t pid;
	printf("Before: pid is %d\n", getpid());
	if ( (pid=fork()) == -1 )
    {
        perror("fork()");
    	exit(1);
    }
	printf("After:pid is %d, fork return %d\n", getpid(), pid);
	sleep(1);
	return 0;
}
//运行结果:
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

这里看到了三行输出,一行Before,两行After。进程43676先打印Before消息,然后打印After。另一个After消息是进程43677打印的。注意到进程43677没有打印before,为什么呢?

进程调用fork,当控制转移到内核中的fork代码后,内核会:

分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

3. 写时拷贝

通常,父子进程代码共享,父子进程再不写入时,数据也是共享的,当任意一方试图写入数据时,便以写时拷贝的方式各自一份副
本。具体见下图:

4. fork函数具体实例

下面我用几个例子帮助大家更好的理解fork调用的细节。

例1

int main()
{
    fork();//fork1
    fork();//fork2
    printf("hello world\n");
    return 0;
}

我们规定,main进程编号为父1

第一次fork,创建新的进程子1,和原本父1。

第二次fork,父1和子1分别创建新进程子2和子3,原来存在着父1和子1。

经历两次fork后,一共四个进程,打印四次hello。

例2

int main()
{
    for(int i = 0;i < 2;i++)
    {
        fork();
        printf("A\n");//遇到\n会自动刷新缓冲区
    }
    exit(0);
}

for循环执行2次,i=0时,第一次fork,父1创建新进程子1父1子1各自执行完毕代码,打印两个A。

父1和子1在打印过后,两个进程都i++,进入i=1的循环中,fork之后父1创建进程子2子1创建进程子3,四个进程,打印4个A。

例3

int main()
{
    for(int i = 0;i < 2;i++)
    {
        fork();
        printf("-");//不会刷新缓冲区
    }
    exit(0);
}

这题的关键点在于printf函数中没有’\n’,所以不会在执行完printf函数后立即刷新缓冲区,还会将要打印的字符放在缓冲区中,直到程序结束,才一次性打印到屏幕上。

注意:父子进程代码共享,当任意一方试图写入数据时,便以写时拷贝的方式各自一份副本。所以父子进程拥有同样的缓冲区。

这个程序一共有4个进程,执行了6次printf,前两次的printf没有打印到显示器,只是存在了缓冲区中,后四次的printf在原先的缓冲区中追加了“-”,也就是说后四次printf每一次打印“–”,一共是8个“-”。

例4

int main()
{
	fork() || fork();
	printf("A\n");
	exit(0);
}

首先我们要知道 || 是如何执行的,举个例子:

A || B ,从左到右,如果A为真,不执行B,直接返回。如果A为假,执行B,根据B的真假返回。

在本题中,第一次fork后出现了两种情况,父进程自己返回一个 >0 的数,可以看做为真,所以不执行第二个fork,直接printf打印,但fork出来的子进程返回 ==0 还需要执行第二个fork才可以判断,所以子进程又创建了一个子进程。

例5

int main()
{
	fork() && fork();
	printf("A\n");
	exit(0);
}

与上一题类似,A && B; 执行时,如果A为真,继续执行B。如果A为假,不执行B。

第一次fork后出现了两种情况,第一次fork出来的子进程返回 ==0 ,不执行第二个fork,直接printf打印。父进程自己返回一个 >0 的数,可以看做为真,执行第二个fork,父进程又创建了一个子进程,分别打印。

例6

int main()
{
   fork();
   fork() && fork() || fork();
   fork();
}

第一个fork之后,新建一个子进程,因为进程过多,且这两个父子进程完全相同,所以我们只分析一边。

第二个fork之后,分为两种情况,父进程 >0 继续执行第三个fork,子进程 ==0不执行第三个fork,执行第四个fork。

第三个fork之后,又分为两种情况,父进程 >0 ,&&运算之后依旧为真,不执行第四个fork。子进程 ==0,&&运算过后为假,执行第四个fork。

第四个,第五个fork均无特殊情况出现。

最后统计父进程下一共有10个进程,子进程同样也有10个,总计20个进程。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐