Linux多进程开发2 - 孤儿、僵尸进程

时间:2024-03-30 19:19:19

参考学习彻底搞懂孤儿/僵尸/守护进程

一、孤儿进程(Orphan Process)

  • 父进程运行结束,但子进程还在运行,这样的子进程就称为孤儿进程
  • 每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init (即养父)
  • Init 会等待被收养的子进程终止
  • 孤儿进程的 getppid() 返回的是 init 的 Pid ,该值一般为1
  • 孤儿进程并不会造成什么危害

实例

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

int main() {

    // 创建子进程
    __pid_t pid = fork();

    if(pid > 0) {
        printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
    } 
    else if(pid == 0) {

        // 先让子进程休眠1s,此过程中父进程已运行结束
        sleep(1);
        printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
    }

    // for循环
    for(int i = 0; i < 3; i++) {
        printf("i : %d , pid : %d\n", i , getpid());
    }
    return 0;
} 

运行结果:孤儿进程被 init 收养了

二、僵尸进程(Zombie Process)

        一个进程在调用 exit 命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构

        注意系统调用 exit,它的作用是使进程退出,但也仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。
        在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,它的残留资源(PCB)仍然存放于内核中。除此之外,僵尸进程不再占有任何内存空间,它需要它的父进程来为它收尸,如果他的父进程没安装 SIGCHLD 信号处理函数调用 wait() 或 waitpid() 等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态。

        如果这时父进程结束了, 那么 init 进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果父进程是一个循环,那么子进程就会一直保持僵尸状态,从而占用大量的进程号导致系统不能产生新的进程,危害极大,需要避免。*僵尸进程将会导致资源浪费,而孤儿则不会。


实例

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

int main() {
    __pid_t pid = fork();

    if(pid > 0) {
        while(1){
            printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
            sleep(1);
        }
        
    } else if(pid == 0) {
        printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
    }
    
    for(int i = 0; i < 3; i++) {
        printf("i : %d , pid : %d\n", i , getpid());
    }
    return 0;
} 

运行结果:让父进程一直循环打印一句话,永不结束,而子进程退出,变为僵尸进程。可以看到有标记为 的进程就是僵尸进程

        ID 为 24597 的进程就是已经变成僵尸进程的子进程了,而在这种情况下可以使用简单的 kill -9 ID 指令来杀死父进程,从而让 init 进程接手,init 始终会负责清理掉僵尸进程。

        但是在真实的开发环境中,当程序已经跑起来时,我们不可能使用 kill 指令来杀死父进程,因此需要引入wait 和 waitpid 函数。

三、进程回收

        在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块 PCB 的信息,包括进程号、退出状态、运行时间等

        父进程可以通过调用 wait 或 waitpid 得到子进程的退出状态同时彻底清除掉它

       wait() 和 waitpid() 函数的功能一样,区别在于 wait()函数会阻塞,waitpid()可以设置不阻塞,waitpid() 还可以指定等待哪个子进程结束。

        注意:1 次 wait 或 waitpid 调用只能清理一个子进程,清理多个子进程应使用循环。

1、wait() 函数

pid_t wait(int *wstatus)

  • 功能:等待任意一个子进程结束,如果任意一个子进程结束了,回收子进程的资源
  • 参数int *wstatus

            进程退出时的状态信息,传入的是一个 int 类型的地址,传出参数。

  • 返回值

            - 成功:返回被回收的子进程的 id

            - 失败:-1 (所有的子进程都已经结束了,调用函数失败)

        调用 wait 函数的父进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(继续往下执行)

        如果没有子进程了,函数立刻返回,返回-1         

        如果子进程都已经结束了,也会立即返回,返回-1


实例

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


int main() {

    // 有一个父进程,创建5个子进程(兄弟关系)
    pid_t pid;

    // 创建5个子进程
    for(int i = 0; i < 5; i++) {
        pid = fork();
        if(pid == 0) {
            // 如果是子进程,为避免它创建自己的子进程,在此 break 跳出循环
            break;
        }
    }

    if(pid > 0) {
        // 父进程
        while(1) {
            printf("parent, pid = %d\n", getpid());

            int st;
            int ret = wait(&st);  // 传入st的地址

            if(ret == -1) {
                break;
            }

            if(WIFEXITED(st)) {
                // 是不是正常退出
                printf("退出的状态码:%d\n", WEXITSTATUS(st));
            }
            if(WIFSIGNALED(st)) {
                // 是不是异常终止
                printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
            }

            printf("child die, pid = %d\n", ret);

            sleep(1);
        }

    } else if (pid == 0){
            printf("child, pid = %d\n",getpid());    
            sleep(1);       
        exit(0);
    }

    return 0;
}

        *使用 fork 创建子线程时,比如:

pid = fork();

pid = fork();

        当前父进程连续创建2个子线程时,现在系统中有几个线程?3个吗?

        其实当第一个子线程被创建时,再执行一次 fork 操作,这个子线程也会创建自己的子线程,父线程超级加辈变成了祖父进程(勇者辛梅尔 ~ 老年辛梅尔),放任不管的话,会产生无穷无尽的进程,所以应当使用 break 阻止这种情况发生。

运行结果:

使用 kill -9 指令杀死子线程时,会触发 WIFSIGNALED 异常终止情况并返回被信号 9 干掉的信息

2、waitpid()函数

pid_t waitpid(pid_t pid, int *wstatus, int options)

  • 功能:回收指定进程号的子进程,可以设置是否阻塞。
  • 参数:- pid(参考详解wait、waitpid)

                pid > 0 : 只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去

                pid = 0 : 等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬

                pid = -1 : 等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样(最常用)

                pid < -1 : 等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值

                 - options:设置阻塞或者非阻塞

                0 : 阻塞

                WNOHANG : 非阻塞

  • 返回值

                > 0 : 返回子进程的id

                = 0 : options=WNOHANG, 表示还有子进程活着

                = -1 :错误,或者没有子进程了

        从本质上讲,系统调用 waitpid 和 wait 的作用是完全相同的,但waitpid多出了两个可由用户控制的参数 pidoptions,从而为我们编程提供了另一种更灵活的方式。

        目前在Linux中只支持 WNOHANGWUNTRACED 两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:

ret=waitpid(-1,NULL,WNOHANG | WUNTRACED)

        如果我们不想使用它们,也可以把options设为0,如:

ret=waitpid(-1,NULL,0);

        如果使用了WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,父进程可以去做自己的事情,不会像在 wait 情况下那样永远等下去。


实例

        与 wait 函数的代码相似,为 waitpid 设置参数 WNOHANG ,表示设置调用 waitpid函数 的父进程为非阻塞状态,父子进程都有工作要做,使用 kill -9 ID 指令杀死子进程,而父进程察看到子进程退出后就收集它的状态信息。

while(1) {
            printf("parent, pid = %d\n", getpid());
            sleep(1);

            int st;
            // int ret = waitpid(-1, &st, 0);
            int ret = waitpid(-1, &st, WNOHANG);

            if(ret == -1) {
                break;
            } else if(ret == 0) {
                // 说明还有子进程存在
                continue;
            } else if(ret > 0) {

                if(WIFEXITED(st)) {
                    // 是不是正常退出
                    printf("退出的状态码:%d\n", WEXITSTATUS(st));
                }
                if(WIFSIGNALED(st)) {
                    // 是不是异常终止
                    printf("被哪个信号干掉了:%d\n", WTERMSIG(st));
                }

                printf("child die, pid = %d\n", ret);
            }
        }

运行结果:  可以看出父进程并没有被阻塞

​​​​​​​