3
微信公众号:LinuGo,欢迎关注获取更多详情

前言

进程就是运行起来的一个程序,但是进程并不局限于执行起来的代码,他的作用范围还有很多,如存放数据的内存地址空间,执行线程,打开的文件,挂起的信号,处理器状态等。

进程在创建的时候开始存活,Linux系统会调用fork()方法复制一个现有进程来创建一个全新的进程,新产生的进程为子进程,创建者进程为父进程。当程序结束运行时,通过exit()系统调用退出执行,该进程占用的资源包括内存空间,线程等被释放掉。

进程家族树

进程都是由其他进程创建出来的,每个进程都有自己的PID(进程标识号),在Linux系统的进程之间存在一个继承关系,所有的进程都是init进程的后代。可以通过pstree命令查看到进程的族谱。系统中的每个进程必定会有一个父进程,可以在/proc文件系统中看到进程对应的父进程号,也可以通过ps -ef命令。

Linux进程涉及进程的所有操作都围绕进程描述符展开,就是task_struct结构体。结构体包含好几百个字段,该结构体可以完整的描述一个正在执行的程序,如虚拟地址空间的信息,打开的文件,进程的执行状态和信息,命名空间,身份信息等。

在每个进程的task_struct中,用字段parent表示该进程的父进程,sibling代表兄弟进程链表,children代表子进程链表,理论上可以通过每一个进程获取任何进程。

//结构体中相关字段
struct task_struct{
    ...
    struct task_struct *parent; /* 父进程 */ 
    struct list_head children; /* 子进程链表 */ 
    struct list_head sibling; /* 连接到父进程的子进程链表,兄弟进程 */ 
    ....
}

进程终结

当进程终结时候,系统需要释放他所占有的所有资源。进程通过exit()系统调用结束进程,这个调用可能是来自进程内部的exit(),也可能来自外部的信号。在结束时候,该进程会使用该系统调用释放自己的空间,包括引用的文件,内存描述符,还会给自己的父进程发送信号,给自己的子进程寻找一个父进程等操作。

调用结束后,此时该进程并没有完全从系统上消失,进程的进程描述符依然存在于系统中,存在的唯一目的就是向父进程提供信息。

与自然规律相反,进程的收尾工作总是由该进程的父进程来做的,父进程会通过wait()系统调用来释放该进程最后剩余的进程标识符,slab缓存等,该调用会阻塞当前父进程,直到某个子进程退出。

关于进程退出,可以结合看一下Linux bash怎么做的。

  • 首先ps命令获取bash的进程PID

  • 再开一个bash页面,查看上个bash进程的系统调用
$ sudo strace -p <Pid>

可以看到该进程的系统调用已经被捕获。

  • 输入一条命令回车,观察系统调用情况

这里一条命令相当于一个bash的子进程,这里以tail为例。可以看到阻塞到该系统调用,也就是在等待回收子进程。

使用Ctrl+C结束进程tail时候,返回了tail进程的Pid。

了解了这些后,接下来理解僵尸进程和孤儿进程就很容易了。下面通过案例讲解一下。


僵尸进程

当进程exit()退出之后,他的父进程没有通过wait()系统调用回收他的进程描述符的信息,该进程会继续停留在系统的进程表中,占用内核资源,这样的进程就是僵尸进程。

接下来通过一个demo构造一个僵尸进程。

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

int main ()
{
    /*fpid表示fork函数返回的值,fork会返回两次,
    一次是父进程,返回值是子进程的Pid,在子进程会返回0*/
    pid_t fpid;
    fpid=fork();//fork后会出现两个分支执行下面的代码,一个父进程,一个新的子进程
    if (fpid < 0)
        printf("fork error!");
    else if (fpid == 0) { //
        printf("child id is %dn",getpid());
        sleep(30);//睡眠30s,在父进程之前退出
        printf("child finally...");
    }
    else { //父进程
        printf("parent id is %dn",getpid());
        sleep(60);
        printf("parend finally...");
    }
}
  • 编译并运行这段代码。
$ gcc -o corpse corpse.c
$ ./corpose

  • pstree查看进程树
$ pstree -p 9106

  • 等待子进程退出,查看子进程的状态
$ cat /proc/<Pid>/status

等到父进程退出之后,再来查看系统,该僵尸进程在系统中找不到了。

父进程退出,没有为子进程”收尸”,但是子进程也会一并退出,怎么做到的呢?这就涉及到了孤儿进程。

孤儿进程

当一个进程正在运行时,他的父进程忽然退出,此时该进程就是一个孤儿进程。作为一个进程,需要找到一个父进程,否则这种进程在退出之后没人回收他的进程描述符,空耗内存。此时该进程会找到一个父进程,如果自己所在的进程组没人收养,那就作为init进程的子进程。

构造一个测试代码:

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

int main ()
{
    /*fpid表示fork函数返回的值,fork会返回两次,
    一次是父进程,返回值是子进程的Pid,在子进程会返回0*/
    pid_t fpid;
    fpid=fork();//fork后会出现两个分支执行下面的代码,一个父进程,一个新的子进程
    if (fpid < 0)
        printf("fork error!");
    else if (fpid == 0) { //
        printf("child id is %dn",getpid());
        sleep(100);
    }
    else { //父进程
        printf("parent id is %dn",getpid());
        sleep(30);//睡眠30s,在子进程之前退出
        printf("parend finally...");
    }
}
  • 编译并运行
$gcc -o orphan orphan.c
$./orphan

  • pstree查看进程树

  • 等到父进程退出再看进程信息

可以看到看到该进程的父进程变为1,也就是init进程。

Init进程会为每一个子进程使用wait系统调用,确保不会产生僵尸进程。这里的wait系统调用指的是waitpid(),会传入一个要等待的进程Pid,等待的指定进程,而不阻塞当前进程去等待。

等到该进程退出后,该进程的进程描述符等信息会被init进程回收,不会形成僵尸进程。

回到上一个僵尸进程的案例中,父进程退出掉后,该进程会找到init作为父进程,init进程针对给进程调用wait()系统调用回收了该子进程。这就是查询不到这个进程的原因。

处置方式

孤儿进程会由init进程收养作为子进程,所以不会有什么危害;僵尸进程会占用进程号,以及未回收的文件描述符占用空间,如果产生大量的僵尸进程,将会导致系统无法分配进程号,说明父进程的代码编写有问题。

$ ps -aux|grep Z

在理想情况下,可以通过kill命令将进程杀死该进程的父进程来结束僵尸进程。当然也要结合具体场景来对待。


微信关注LinuGo回复书名即可获取下方书籍pdf版

参考书籍

  • 《Linux内核设计与实现》第三章.进程管理
  • 《深入Linux内核与架构》第二章.进程管理与调度
  • 《后台开发核心技术与应用实现》第十章.进程

郭朝
24 声望7 粉丝