对于fork()用法的初步探讨

前言

进程和线程是两种CPU资源以及调度的描述。
这句话可能有点拗口,所以简单展开说明一下。

现代CPU太快了,所有的资源(RAM, device, bus..)都无法填满它。所以有多个任务时,CPU就轮流着来处理。得到CPU资源时,其他总线,显卡,RAM等等都要准备好,这样就构成了我们程序执行的上下文。等执行完或CPU分配给它的时间用完了,它就要被切换出去,等待CPU的下一次临幸。当然切换出去之前会保存上下文。
所以从CPU的角度看它整天就在: 加载A的上下文,执行A,保存A的上下文,调入B的上下文,执行B,保存B的上下文。。。

进程就是包括上下文切换在内的程序执行时间总和 = cpu加载上下文+CPU执行+CPU保存上下文

进程颗粒度太大,每次都有上下文的调入调出开销。而一段程序一定有多个块,可以把它分成A, B, C多个块,然后这样执行:
CPU加载程序上下文,执行A, 执行B,执行C, 执行A, 执行B,执行C。。。, CPU保存程序上下文。
也就是线程共享了进程的上下文。

Fork基础

一个进程包括代码,数据和分配给进程的资源。fork函数调用会使内核创建一个与原来进程几乎完全相同的进程。内核会给新进程分配新的资源,然后把原来进程的值拷贝到新进程中,只有少数如fork()返回值不同。
一般我们称原进程为父进程,fork()出来的新进程为子进程。
注意: 父进程和子进程的执行顺序是不定的
所以运行下面的例子,可能输出结果会有差异,关键是理解fork()的机制原理。

在fork.c里主要的函数包括
《对于fork()用法的初步探讨》

  • verify_area

void verify_area(void * addr,int size) // addr 是虚拟地址 ,size是需要写入的字节大小
  • copy_mem

int copy_mem(int nr,struct task_struct * p) //拷贝内存页表,把进程p的数据段copy到nr*TASK的线性地址处
  • copy_process

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
        long ebx,long ecx,long edx, long orig_eax,
        long fs,long es,long ds,
        long eip,long cs,long eflags,long esp,long ss)
        //拷贝系统进程信息,数据段,并设置必要的registers,
  • find_empty_process

int find_empty_process(void) //为新进程获取不重复的pid

在Linux中,对fork进行了优化,调用时采用写时复制 (COW,copy on write)的方式,在系统调用fork生成子进程的时候,不马上为子进程复制父进程的资源,而是在遇到“写入”(对资源进行修改)操作时才复制资源。

语法

#include <sys/types.h>  //define pid_t
#include <unistd.h>
pid_t fork(void)

返回值

0: 返回给子进程,子进程的ID返回给父进程
-1: 出错, 返回给父进程,错误原因返回到errno
>0: 子进程的ID返回给父进程

EAGAIN: 进程ID号达到最大可使用值
ENOMEM: 内存不足,无法配置核心所需的数据结构

例子

初级示例

//
// Created by         : Harris Zhu
// Filename           : test.c
// Author             : Harris Zhu
// Created On         : 2017-08-19 17:36
// Last Modified      :
// Update Count       : 2017-08-19 17:36
// Tags               :
// Description        :
// Conclusion         :
//
//=======================================================================

#include <stdio.h>
#include <unistd.h>

int main (int argc, char**argv)
{
    pid_t pid;
    int count=0;
    pid=fork();
    if (pid < 0)
        perror("fork error!");
    else if (pid == 0) {
        printf("the pid of son is %d\n",getpid());
        count++;
    }
    else {
        printf("the pid of parent is %d\n",getpid());
        count++;
    }
    printf("count = %d\n",count);
    return 0;
}

输出:

the pid of parent is 20081
count = 1
the pid of son is 20082
count = 1

fork函数执行结束后,如果创新成功,则出现两个进程,父进程(原来进程),子进程。
在子进程中,fork()返回0, 在父进程中返回子进程的ID。
fork后的两个进程没有固定的先后顺序。
可以通过getpid()函数来获得自己的进程ID,可以通过getppid()函数来获得自己父进程ID

中级示例

代码

//
// Created by         : Harris Zhu
// Filename           : test.c
// Author             : Harris Zhu
// Created On         : 2017-08-19 17:55
// Last Modified      :
// Update Count       : 2017-08-19 17:55
// Tags               :
// Description        :
// Conclusion         :
//
//=======================================================================

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char** argv)
{
   int i=0;
   printf("i\tson/father\tppid\tpid\trpid\n");
   for(i=0;i<2;i++){
       pid_t rpid=fork();
       if(rpid==0)
           printf("%d\tson\t\t%4d\t%4d\t%4d\n",i,getppid(),getpid(),rpid);
       else
           printf("%d\tfather\t\t%4d\t%4d\t%4d\n",i,getppid(),getpid(),rpid);
   }
   sleep(1);
   return 0;
}

注意 程序中我sleep 1s是为了防止子进程还未结束,父进程先结束,这时ppid会显示1

输出

为了阅读方便,我这里用表格

ison/fatherppidpidrpid
0father204252042620427
0son20426204270
1father204252042620428
1father204262042720429
1son20426204280
1son20427204290

关系图

《对于fork()用法的初步探讨》

规律总结

for循环次数为N,
执行print次数: $2*(1+2+4+…2^{N-1})$
生成的新进程为: $(1+2+4+…+2^{N-1})$

统计进程

可以用printf("%d\n", getpid())printf("+\n")来判断

中高级示例

代码

//
// Created by         : Harris Zhu
// Filename           : test.c
// Author             : Harris Zhu
// Created On         : 2017-08-19 21:30
// Last Modified      :
// Update Count       : 2017-08-19 21:30
// Tags               :
// Description        :
// Conclusion         :
//
//=======================================================================

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char ** argv)
{
    pid_t pid;
    static int a=0;
    printf("a=%d current pid=%d\n\n", a, getpid());
    pid = fork();
    if(pid <0)
        perror("fork error!");
    printf("a=%d pid=%d, ppid=%d\n", a, pid, getppid());
    sleep(0.1);

    a++;

    pid = fork();
    if(pid <0)
        perror("fork error!");
    printf("a=%d pid=%d, ppid=%d\n", a, pid, getppid());
    sleep(0.1);

    a++;

    pid = fork();
    if(pid <0)
        perror("fork error!");
    printf("a=%d pid=%d, ppid=%d\n", a, pid, getppid());
    sleep(0.1);


    printf("getpid = %d getppid= %d \n\n", getpid(), getppid());

    sleep(1);

    return 0;
}

输出

a=0 current pid=24369

a=0 pid=24370, ppid=24368
a=0 pid=0, ppid=24369
a=1 pid=24371, ppid=24368
a=1 pid=24372, ppid=24369
a=2 pid=24373, ppid=24368
getpid = 24369 getppid= 24368

a=2 pid=0, ppid=24369
getpid = 24373 getppid= 24369

a=2 pid=24374, ppid=24369
getpid = 24370 getppid= 24369

a=1 pid=0, ppid=24369
a=2 pid=0, ppid=24370
getpid = 24374 getppid= 24370

a=2 pid=24375, ppid=24369
a=1 pid=0, ppid=24370
getpid = 24371 getppid= 24369

a=2 pid=0, ppid=24371
getpid = 24375 getppid= 24371

a=2 pid=24376, ppid=24370
getpid = 24372 getppid= 24370

a=2 pid=0, ppid=24372
getpid = 24376 getppid= 24372

分析图

对照输出,可以画出下面的继承关系图,以及执行顺序
《对于fork()用法的初步探讨》
我不展开讲解这个运行结果如何来的,作为学习,如果能自己画个框图搞清楚执行的顺序和逻辑,那么就算是学懂fork()了

高级示例

代码

这是网上非常著名的一个示例。理解这个例子感觉就像在解初中的数学道,特别有趣。

//
// Created by         : Harris Zhu
// Filename           : test.c
// Author             : Harris Zhu
// Created On         : 2017-08-19 20:08
// Last Modified      :
// Update Count       : 2017-08-19 20:08
// Tags               :
// Description        :
// Conclusion         :
//
//=======================================================================

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char** argv)
{
    fork();
    fork() && fork() || fork();
    fork();
    printf("+\n");
    sleep(1);  //if no this lien, it prints out 19 +
    return 0
}

分析

第一个和最后一个是肯定执行的
下面分析一下中间3个fork()的执行
《对于fork()用法的初步探讨》
A&&B: 如果A为0, 则忽略B的执行
A||B: 如果A为1, 则忽略B的执行
上图中一个5个进程, 总共2*5*2=20个进程
完整分支图如下,空间原因fork #0未展开,它和fork #1是对称的
《对于fork()用法的初步探讨》

后序

内核对于进程的创建,调度和清理等细节就不展开,有兴趣的可以自行搜索。如果个人有兴趣讨论,可以发信给我邮箱
本文主要参考这里

    原文作者:harriszh
    原文地址: https://segmentfault.com/a/1190000010748809
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞