linux中fork小结

fork函数在linux中非常重要,因为进程大多是通过它来创建的,比如linux系统在启动时首先创建了进程0,之后的很多进程借助do_fork得 到创建.这两天在看匿名管道时了解了下fork,其应用毕竟广,这里只说些我才学到的吧.
首先来看例1.

#include "stdio.h" 

#include "unistd.h"
#include "stdlib.h"
 
int main( ) {
int i;
printf ( "hello world %d/n " , getpid( ) ) ;
i= 3 ;
fork( ) ;
printf ( "var %d in %d/n " , i, getpid( ) ) ;
return 0 ;
}

输出是什么呢?
这是在我的机器上一次执行的结果:

hello world 8168
var 3 in 8169
var 3 in 8168

为什么会有两次输出var 3 一行呢?看似不可思议吧…要解释原因,就牵涉到了我们要讨论的fork,它到底做了什么?

fork英文是叉的意思.在这里的意思是进程从这里开始分叉,分成了两个进程,一个是父进程,一个子进程.子进程拷贝了父进程的绝大部分.栈阿,缓冲区阿 等等.系统为子进程创建一个新的进程表项,其中进程id与父进程是不相同的,这也就是说父子进程是两个独立的进程,虽然父子进程共享代码空间.但是在牵涉 到写数据时子进程有自己的数据空间,这是因为copy on write机制,在有数据修改时,系统会为子进程申请新的页面.

再来复习下进程的有关知识.系统通过进程控制块PCB来管理进程.进程的执行,可以看作是在它的上下文中执行.一个进程的上下文(context)由三部 分组成:用户级上下文,寄存器上下文和系统级上下文.用户级上下文中有正文,数据,用户栈和共享存储区;寄存器上下文中有个非常重要的程序计数器(传说中 的)PC,还有栈指针和通用寄存器等;系统级上下文分静态和动态,PCB中进程表项,U区,还有本进程的表项,页表,系统区表项等都属于静态部分,而核心 栈等则属于动态部分.

回到fork上来.fork在内核中对应的是do_fork函数,本来想自己写下函数说明的,发现已经有了.详见:内核 do_fork 函数源代码浅析 . 上面已经提到,fork后,子进程拷贝了父进程的进程表项,还有栈阿,缓冲区,U区等等.当然在这之前会去检查系统有没有可用的资源,取一个空闲的进程表 项和唯一的PID号等工作.(后面的例子会体现子进程到底拷贝了父进程的哪些东西.)需要指出的是,这里所说的拷贝,并不是说子进程再申请页面,将父进程 中的全部拷贝过来.而是,他们共享一个空间,子进程只是作一层映射而已,这个时候进程页面标记为只读.在有数据修改时,才会申请新的页面,拷贝过来,并标 记为可写.

fork执行后,对父进程和子进程不同的地方还有,对父进程返回子进程的pid号,对子进程返回的是0.大致的算法描述为:

if (当前正在执行的是父进程){
将子进程的状态设置为”就绪状态”;
return (子进程的pid号);
}else{ /*正在执行的是子进程*/
初始化U区等工作;
return 0;
}

现在来看例1,是不是已经清晰了很多? 在执行了fork之后,父子进程分别都执行了下一步printf语句.由于fork拷贝走了pc,所以在子进程中不会再从main入口重新执行,而是执行 fork后的下一条指令.而i是保存在进程栈空间中的,所以子进程中也存在.
有了前面的基础,再看下面一个例2:

#include <stdio.h>

#include <unistd.h>
int main( )
{
int i= 0 ;
pid_t fork_result;
printf ( "pid : %d --> main begin()/n " , getpid( ) ) ;
 
fork_result = fork( ) ;
 
if ( fork_result < 0 ) {
printf ( "Fork Failure/n " ) ;
return 0 ;
}
for ( i= 0 ; i< 3 ; i++ ) {
if ( fork_result == 0 ) { //在子进程中.
printf ( "child process : %d/n " , i) ;
} else {
printf ( "Father process : %d/n " , i) ;
}
}
return 0 ;
}

这次输出可以更明确的显示出子进程到底拷贝了些什么.我机器上的两次执行结果:

boluor@boluor-laptop:~/programs/pipe/fork$ ./a.out
pid : 16567 –> main begin()
child process : 0
child process : 1
child process : 2
Father process : 0
Father process : 1
Father process : 2
boluor@boluor-laptop:~/programs/pipe/fork$ ./a.out
pid : 16569 –> main begin()
Father process : 0
Father process : 1
Father process : 2
child process : 0
child process : 1
child process : 2

同时也可以说明,父子进程到底哪个先执行,是跟cpu调度有关系的.如果想固定顺序,那么就要用wait或vfork函数.
继续看例3:

#include "stdio.h"

#include "unistd.h"
#include "stdlib.h"
 
int main( )
{
printf ( "hello world %d" , getpid( ) ) ;
//fflush(0);
fork( ) ;
return 0 ;
}

执行上面的程序,可以发现输出了两遍hello world.而且两次的pid号都是一样的.这是为什么呢? 这其实是因为printf的行缓冲的问题,printf语句执行后,系统将字符串放在了缓冲区内,并没有输出到stdout.不明白的话看下面的例子:

#include "stdio.h"

int main( ) {
printf ( "hello world" ) ;
while ( 1 ) ;
return 0 ;
}

执行上面的程序你会发现,程序陷入死循环,并没有输出”hello world”.这就是因为把”hello world”放入了缓冲区.我们平常加’/n’的话,就会刷新缓冲区,那样就会直接输出到stdout了.

因为子进程将这些缓冲也拷贝走了,所以子进程也打印了一遍.父进程直到最后才输出.他们的输出是一样的,输出的pid是一致的,因为子进程拷贝走的是 printf语句执行后的结果.如果利用setbuf设置下,或者在printf语句后调用fflush(0);强制刷新缓冲区,就不会有这个问题了.这 个例子从侧面显示出子进程也拷贝了父进程的缓冲区.

关于fork的应用还很多很多,在实际项目中需要了再去深入研究.关于fork和exec的区别,exec是将本进程的映像给替换掉了,跟fork差别还 是很大的,其实fork创建子进程后,大部分情况下,子进程会调用exec去执行不同的程序的.

先说到这里了.如果需要更多fork的知识就google一下^.^.

你可能感兴趣的:(算法,linux,工作,Google,存储)