C语言fork函数解析

C语言fork函数解析
C语言fork函数解析

首先看下fork的基本知识:

函数原型:pid_t fork( void);

返回值:若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1

一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值

而父进程中返回子进程ID。

注意要点:

1、子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。

此处先简要介绍下COW(Copy-on-write)机制,大致原理如下:

在复制一个对象的时候并不是真正的把原先的对象复制到内存的另外一个位置上,而是在新对象的内存映射表中设置一个指针,指向源对象的位置,并把那块内存的Copy-On-Write位设置为1.这样,在对新的对象执行读操作的时候,内存数据不发生任何变动,直接执行读操作;

而在对新的对象执行写操作时,将真正的对象复制到新的内存地址中,并修改新对象的内存映射表指向这个新的位置,并在新的内存位置上执行写操作。

linux内核下fork使用COW机制工作原理:

进程0(父进程)创建进程1(子进程)后,进程0和进程1同时使用着共享代码区内相同的代码和数据内存页面, 只是执行代码不在一处,因此他们也同时使用着相同的用户堆栈区。在为进程1(子进程)复制其父进程(进程0)的页目录和页表项时,进程0的640KB页表项的属性没有改动过(仍然可读写),但是进程1的640KB对应的页表项却被设置成只读。因此当进程1(子进程)开始执行时,对用户堆栈的入栈操作将导致页面写保护异常,从而使得内核的内存管理程序为进程1在主内存区中分配一内存页面,并把进程0中的页面内容复制到新的页面上。从此时开始,进程1开始有自己独立的内存页面,

由于此时的内存页面在主内存区,因此进程1中继续创建新的子进程时也可以采用COW技术。

内核调度进程运行时次序是随机的,进程0创建进程1后,可能先于进程1修改共享区,进程0是可读写的,在未分开前,进程1是只读的,由于两个进程共享内存空间,

为了不出现冲突问题,就必须要求进程0在进程1执行堆栈操作(进程1的堆栈操作会导致页面保护异常,从而使得进程1在主内存区得到新的用户页面区,此时进程1和进程0才算是真正独立,

如前面所述)之前禁止使用用户堆栈区。所以进程0在执行了fork(创建了进程1)之后的pause使用内嵌的方式,保证进程0(主进程)不会弄乱堆栈。

fork()后立即执行exec(),地址空间就无需被复制了,一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,

对系统而言,还是同一个进程,不过已经是另一个程序了。

fork()的实际开销就是复制父进程的页表以及给子进程创建一个进程描述符。在一般情况下,进程创建后都为马上运行一个可执行的文件,采用COW这种优化,可以避免拷贝大量根本就不会被使用的数据(地址空间里常常包含数十兆的数据)。由于Unix强调进程快速执行的能力,所以这个优化是很重要的。

2、在linux中存在缓冲区的问题。当写printf函数,但是没有换行的话,它是不会输出的,而是先将要输出的内容存放再缓冲区中,当碰到换行时,将缓冲区中的内容再一起输出。

所以fork后子进程会复制父进程的缓冲区,因此也将待输出内容复制到自己“与父进程独立的缓冲区中”,因此当子进程执行遇到换行,就将缓冲区中的内容全都输出来。

代码1:

#include < unistd.h >

#include < stdio.h >

int main(int argc, char *argv[])

{

int i;

pid_t pid;

for(i=0; i<2; i++)

{

printf("-");

pid = fork();

}

return 0;

}

输出:--------

8个中划线

分析:

原因分析:如果不清楚fork的深拷贝机制,估计看到上述代码的第一感觉是:死循环,永远fork下去。一定要仔细理解上面的注意要点1。

在整个执行周期内,一共会产生4个进程,由于是遍历所以主进程会产生两个子进程(假设i=0时fork出的子进程记为:first,i=1时fork出的子进程记为:second),

first进程在遍历时会再产生一个自己的子进程,记录为:third,而second进程,i的值已经为1,不会再继续执行下去,其它进程同理,所以此处不会形成死循环

主进程会输出2个中划线(总计:2个)

first进程会输出1个自己的中划线,考虑到注意要点2,还会输出从主进程缓冲区拷贝过来的1个中划线(总计:2个)

second进程没有自己的中划线输出,考虑到注意要点2,会输出从主进程缓冲区拷贝过来的2个中划线(总计:2个)

third进程没有自己的中划线输出,考虑到注意要点2,会输出从first进程缓冲区拷贝过

来的2个中划线(总计:2个)

所以一共会输出8个,怎么确定会产生4个进程呢,只要把进程的id号给打印出来,就非常清晰明了了,具体代码参考代码5

代码2:

#include < unistd.h >

#include < stdio.h >

int main(int argc, char *argv[])

{

int i;

pid_t pid;

for(i=0; i<2; i++)

{

pid = fork();

printf("-");

}

return 0;

}

输出:--------

8个中划线

解析:虽然同代码1一样,也是输出8个中划线,但是内部运行机制相差很远,具体原因分析参考代码1原因分析

主进程会输出2个中划线(总计:2个)

first进程会输出2个自己的中划线,无主进程缓冲区拷贝输出(总计:2个)

second进程会输出1个自己的中划线,考虑到注意要点2,会输出从主进程缓冲区拷贝过来的1个中划线(总计:2个)

third进程会输出1个自己的中划线,考虑到注意要点2,会输出从first进程缓冲区拷贝过来的1个中划线(总计:2个)

代码3:

#include < unistd.h >

#include < stdio.h >

int main(int argc, char *argv[])

{

int i;

pid_t pid;

for(i=0; i<2; i++)

{

printf("-\n");

pid = fork();

}

return 0;

}

输出:

-

-

-

3个中划线

解析:具体原因分析参考代码1原因分析

主进程会输出2个中划线(总计:2个)

first进程会输出1个自己的中划线,无主进程程缓冲区拷贝输出(总计:1个)second进程没有自己的中划线输出,无主进程缓冲区拷贝输出(总计:0个)

third进程会没有自己的中划线输出,无first进程缓冲区拷贝输出(总计:0个)

代码4:

#include < unistd.h >

#include < stdio.h >

int main(int argc, char *argv[])

{

int i;

pid_t pid;

for(i=0; i<2; i++)

{

pid = fork();

printf("-\n");

}

return 0;

}

输出:

-

-

-

-

-

-

6个中划线

解析:具体原因分析参考代码1原因分析

主进程会输出2个中划线(总计:2个)

first进程会输出2个自己的中划线,无主进程缓冲区拷贝输出(总计:2个)second进程会输出1个自己的中划线,无主进程缓冲区拷贝输出(总计:1个)third进程会输出1个自己的中划线,无first进程缓冲区拷贝输出(总计:1个)

代码5:

#include < unistd.h >

#include < stdio.h >

int main(int argc, char *argv[])

{

int i;

pid_t pid;

for(i=0; i<2; i++)

{

pid = fork();

if(pid == 0)

printf("child process:%d", getpid());

else if(pid > 0)

printf("parent process:%d", getpid());

else

printf("error");

}

printf("\n");

return 0;

}

输出:

parent process:22881parent process:22881

parent process:22881child process:22883

child process:22882parent process:22882

child process:22882child process:22884

相关主题
相关文档
最新文档