父子进程

什么是父子进程?

所谓父子进程,就是在一个进程的基础上创建出另一条完全独立的进程,这个就是子进程,相当于父进程的副本。

在Linux中,程序员可以通过pid_t fork()函数即可为当前进程创建出一个子进程:
在这里插入图片描述
【注意】:fork()函数如果成功返回,其返回值有两个,但两个返回值并不可能返回给一个进程,而是分别返回给父子进程:父进程收到的返回值 > 0、子进程收到的返回值 = 0


例:为一个进程创建子进程并让父子进程执行不同的任务(通过fork函数的返回值区分父子进程)[请在Linux环境测试,windows不支持fork函数]

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

// 测试创建子进程函数 pid_t fork();
int main()
{
    pid_t pid = fork();
    if(-1 == pid)
    {
        //创建子进程失败
        return -1;
    }
    if(0 == pid)
    {
        //子进程
        printf("I am child, my pid:%d, ppid:%d\n",getpid(),getppid());
        sleep(5);
    }
    else
    {
        //父进程        
        printf("I am father, my pid:%d, ppid:%d\n",getpid(),getppid());
        sleep(5);
    }
    return 0;
}

ppid 指当前进程的父进程的PID号,可以看到:child 的 ppid == father 的pid
在这里插入图片描述


父子进程特性

  1. 子进程完全拷贝父进程的PCB,但并不是同一个(后序会讲到写时拷贝

  2. 代码共享,数据独有。

    我们知道进程的PCB中有指向该程序在内存上数据的指针,并且保留了上下文信息,那么子进程修改一个变量是否会影响到地址相同的父进程变量?
    在这里插入图片描述

    我们在子进程中修改变量a的值,并将变量a的值打印处理,看到子进程对变量a的修改竟然并不影响父进程相同地址的变量值 !
    在这里插入图片描述
    这样的结果也说明了父子进程虽然代码共享,但数据是各自独有的。这是因为OS中的虚拟内存机制(详见后续博客),这样的机制保证了父子进程独立运行互不干扰

  3. 子进程从代码的fork之后才汇编执行

    子进程拷贝父进程PCB,而父进程执行时在PCB存储了程序计数器上下文信息,因此虽然父子进程代码共享,但子进程并不会从代码起始从新运行

    这种机制也避免了子进程创建后再次执行fork()函数,从而无限创建子进程

  4. 用户能够在命令行中执行的进程,其父进程都是:bash(centos的shell)


为什么有父子进程?

也就是说,创建子进程有什么作用?

通过父子进程特性可以看到,子进程与父进程有着很强的关联,但其运行过程并不影响父进程;

因此子进程也被称为父进程的守护进程:当一个进程需要做一些可能发生阻塞或中断的任务,父进程可通过子进程去做,来避免自己进程的崩溃。


僵尸进程(defunct进程)

什么是僵尸进程?

直白来说,就是子进程先于父进程退出,子进程就会变成僵尸进程

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

// 制作僵尸进程
// 子进程在父进程运行时退出
// 子进程的退出状态信息父进程就无法回收
int main()
{
    // 创建子进程
    int ret = fork();
    if(ret < 0)
    {
        return -1;
    }
    else if(ret == 0)
    {
        // 子进程
        printf("this is child!\n");
    }
    else
    {
        // 父进程
        // 一直死循环运行
        while(1)
        {
            printf("this is father!\n");
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述

僵尸进程的底层原因

一个进程在退出的时,会关闭所有的文件描述符,释放在用户空间中分配的内存,但是该进程的 PCB 仍会暂时保留,因为里面还存放着进程的退出状态以及统计信息等,这些PCB的信息均需该进程的父进程接收

Linux下任何进程都有父进程,即每个进程的PCB都需由其父进程回收(除了0号进程)

Linux下有3个特殊的进程,idle进程(PID=0)init进程(PID=1)kthreadd进程(PID=2)

idle进程(0号进程)是系统所有进程的先祖,内核静态创建的,运行在内核态;这也是唯一一个没有通过fork或者kernel_thread产生的进程;

init进程(1号进程) 是系统中所有其它用户进程的祖先进程,由0进程创建,完成系统的初始化;

Linux中的所有进程都是有 init进程 创建并运行的,这个流程大概是:首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统进程启动完成后,init将变为守护进程监视系统其他进程。

若用户形成一个孤儿进程,该孤儿进程会转由1号进程回收其PCB信息;(后续详谈)

kthreadd进程(2号进程)由0进程创建,始终运行在内核空间, 负责所有内核线程的调度和管理

父进程如何回收子进程的PCB信息:

  • 子进程退出时,会为父进程发送SIGCHLD信号(进程信号详谈)
  • 父进程需要主动捕捉这个信号才能回收子进程的PCB信息

但是当父进程正处于运行或睡眠状态,是无法接收子进程的退出信号, 子进程的退出状态信息无法被回收,其PCB将一直存在于内存(也就是变成所谓的僵尸态),久而久之便会造成内存泄漏

僵尸进程的危害与解决方法

危害
子进程在内存上的PCB无法释放,会造成内存泄漏!
且一旦造成僵尸进程,通过kill -9强杀指令也无法终止该指令;因此在编码时要坚决避免僵尸进程!
在这里插入图片描述

解决方法

  1. (不推荐)重启OS,僵尸进程自动释放;
  2. (不推荐)杀死父进程,子进程由僵尸进程转为孤儿进程,PCB由1号进程回收;
  3. 进行进程等待(父进程一旦创建子进程,一定要配套增减进程等待机制,以此避免僵尸进程);
    (进程等待详见下篇博客)

孤儿进程

什么是孤儿进程?

直白来说,就是父进程先于子进程退出,子进程就会变成孤儿进程;
注意:没有孤儿状态!

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

// 测试孤儿进程
// 让父进程先于子进程退出

int main()
{
    int ret = fork();
    if(ret < 0)
    {
        return -1;
    }
    if(ret == 0)
    {
        // 子进程
        while(1)  //让子进程不退出观察其状态
        {
            printf("this is child!\n");
            printf("my pid:%d, my ppid:%d\n",getpid(),getppid());
            sleep(3);
        }
    }
    else
    {
        // 父进程
        printf("this is father!\n");
        printf("my pid:%d, my ppid:%d\n",getpid(),getppid());
        sleep(10);
    }
    return 0;
}

在这里插入图片描述

孤儿进程的后果

孤儿进程的出现流程:

  • 父进程先于子进程退出后,回收子进程的父进程就不在了,会使子进程变成孤儿;
  • 随即该孤儿进程会马上被操作系统的1号进程领养
  • 该进程的PCB回收也由1号进程完成;

孤儿进程的危害:孤儿进程由系统回收,没有危害;


参考:

  1. 进程概念——进程本质与PCB(进程控制块)
  2. Linux下1号进程的前世(kernel_init)今生(init进程)----Linux进程的管理与调度(六)
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐