最近在读Robert love的《Linux Kernel Development》这本书,这本书对于整个linux kernel各个核心子系统进行整体的介绍,第三章提到了linux fork函数的原理和处理过程,但是没有进行详细的阐述和分析。后来在网上查看了不少的帖子和文章对于整体有了一个完整的了解,好记性不如烂笔头,为了加深自己的记忆和方便以后查找,就将自己的一些理解整理出来。
一、进程(process)的概念
首先我们需要了解linu进程(process)的定义,在 The Linux Programming Interface 中定义为 A process is an instance of an executing program.进程是正在执行的程序。但是进程不单单是我们在编辑工具中敲进去的代码,常常包含在运行期间其他的资源,包括执行期间相关联的数据(变量,内存空间,缓冲区间)以及进程执行的上下文(process context)。
其中所谓进程上下文就是进程执行的环境,包括当一个进程执行时CPU所有寄存器中的值,进程的状态以及堆栈中的内容。
二、linux操作系统对于进程的管理
现代操作系统举例,由于虚拟内存和虚拟处理器的机制,会给用户产生同时可以执行多个任务,也就是执行多个进程,那就存在进程的管理和调度。linux中通过Sched.h中定义的task_struct进行管理的。每个进程都有一个自己的进程表项,每个进程表都详细描述了该进程的状态(代码段地址,堆栈地址,文件句柄等等),kernel负责维护整个系统的列表task_list。
由于多任务的操作、中断处理往往需要进行进程的调度,其中一些进程需要让出CPU资源,而另外一些进程将占用CPU资源进行工作。当CPU需要切换到另外一个进程的时候,就需要保存当前执行进程的所有状态,也就是进程的上下文,这样就方便当下一次执行该进程的时候CPU知道从何处或者何种状态开始。我们称为上下文的切换。类似与函数的调用,我们需要保存函数调用的现场,以便函数返回时恢复现场。
三、linux fork的工作原理
在linux系统中,用户通过调用系统方法fork创建新的进程,其中调用fork的进程成为父进程,新产生的进程为子进程。
1、fork函数原型:
#include<unistd.h>
pid_d fork(void); //返回值:如果成功调用该函数返回两个值,子进程返回0,父进程返回子进程的PID;如果调用失败则返回-1;
fork函数调用一次分别给父进程和子进程返回两次值。这个和我们常见的调用一次返回一次的函数调用不同,在刚开始接触linux的时候对于这个概念的理解仅仅是在于对函数功能,并不了解fork的整个过程和工作原理。
2、fork的工作原理:
刚才提到linux中每一个进程都有一个自己的进程表项task_struct,其中记录的该进程的一些状态和数据。那调用fork之后子进程的进程表项中的数据项到底是如何赋值的。有兴趣的可以下载kernel的source code看一下/kernel/目录中fork.c文件中copy_process函数。
调用fork的时候linux系统会创建一个新的进程,并且会为这个进程创建一个新的进程表项,该新子进程的和原父进程表项中大部分数据项是相同的,因为子进程copy了父进程的大部分上下文,特别是程序计数器Program counter的值是相同的的,用来指出计算机正在执行的指令。因此父进程和子进程的上下文中都记录了当前执行的位置,也就是执行fork之后就返回。由于子进程和父进程并没有同时占有CPU,当前CPU中的PC寄存器中保存的是父进程进程执行的值,而子进程的PC的值作为上下文保存在进程表项中。父进程继续执行,返回刚刚创建的新的子进程的PID。
而在之后的某个时间由于进程的调度,刚创建的子进程被调度,子进程的上下文别切换到CPU中。其中的PC只出了当前进程执行的位置,并且子进程和父进程的PC一致,在执行完fork之后返回了0。
其实调用fork之后的两次返回,就是独立的两个进程执行了同一段code,返回了两个不同的数据。这也就是为什么fork被称之为fork的原因,从同一地方开始,分叉为两个不同的进程。
四、实例分析
从一个简单的例子来看一下fork的过程:
#include<unistd.h>
#include<stdio.h>
int main(void)
{
int number = 12;
pid_t pid = 0;
printf("This is a test code\n");
printf("start test code");// 没有换行符
pid = fork();
if(pid < 0)
printf("creat error\n");
else if(pid == 0)
{
printf(" child process, pid=%d\n",getpid());
number ++;
}
else if( pid > 0)
{
printf(" parent process,pid=%d\n",getpid());
}
printf("The number is %d\n",number);
return 0;
}
This is a test code
start test code parent process,pid=5723
The number is 12
start test code child process, pid=5724
The number is 13
从执行的结果来看成功创建了子进程,其中有两点需要进一步的分析:
1、缓冲区中的内容:
上面code中“start test code”这句打印分别在父进程和子进程中都输出了。由于print函数带有缓冲机制,它会将需要输出的内容放到标准输出缓冲中,只有当遇到"\n"或者缓冲区满的时候,系统才会将其中的内容输出。由于“start test code”没有"\n"没有即时输出,当遇到后续执行printf的时候因为有"\n"所以才输出。为什么子进程也会输出呢?调用fork函数创建子进程的时候,子进程copy了父进程大部分的进程表项,其中包括了缓冲区。所以在子进程中也输出了“start test code”。
2、变量的内容:
从输出的log信息中,我们可以看到父子进程的中number变量的数值是不相同的,虽然子进程在创建是copy了父进程的上下文信息,但他们却拥有不同的内存空间,是两个相互独立的进程。