深入 Linux 进程

问题

进程参数 和 环境变量 对于进程意味着什么?

进程参数和环境变量的意义

一般情况下,子进程的创建是为了解决某个子问题

子进程解决问题需要父进程的 "数据输入" (进程参数 & 环境变量)

设计原则:

  • 子进程启动时必然用到的参数使用进程参数传递
  • 子进程解决问题可能用到的参数使用环境变量传递

思考

子进程如何将结果 "返回" 父进程?

#include <stdio.h>

int main()
{
    printf("Test: Hello World!\n");
    return 33;
}

这个测试程序,执行完 main 函数后 return 33,我们在命令行中运行这个程序,那么这个程序就为命令行的子进程,我们通过 echo $? 命令可以得到这个程序的返回值,这个命令用于得到上一个进程的退出状态码

程序运行结果如下图所示:

在命令行中通过 echo $? 命令成功获取到了 a.out 这个子进程的进程退出状态码,那么我们可以在程序中获取到子进程的退出状态码吗?

在程序中我们可以通过 wait(...) 函数 或者 waitpid(...) 函数 来获取子进程的退出状态码

深入理解父子进程

子进程的创建是为了并行的解决子问题 (问题分解)

父进程需要通过子进程的结果最终解决问题 (并获取结果)

进程等待系统接口

pid_t wait(int* status);

  • 等待一个子进程完成,并返回子进程标识和状态信息
  • 当有多个子进程完成,随机挑选一个子进程返回

pid_t waitpid(pid_t pid, int* status, int options);

  • 可等待特定的子进程或一组子进程
  • 在子进程还未终止时,可通过 options 设置不必等待 (直接返回)

进程退出系统接口

头文件:#include <unistd.h>

void _exit(int status); 

  • 系统调用,终止当前进程

头文件:#include <stdlib.h>

void exit(int status);

  • 库函数,先做资源清理,再通过系统调用终止进程

void abort(void);

  • 异常终止当前进程 (通过产生 SIGABRT 信号终止)

下面的程序运行后会发生什么?

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


int main(int argc, char* argv[])
{   
    pid_t pid = 0;
    int a = 1;
    int b = 0;
    int status = 0;
    
    printf("parent = %d\n", getpid());
    
    if( (pid = fork()) == 0 ) exit(-1);
    
    printf("child = %d\n", pid);
    
    if( (pid = fork()) == 0 ) abort();
    
    printf("child = %d\n", pid);
    
    if( (pid = fork()) == 0 ) a = a / b, exit(1);
    
    printf("child = %d\n", pid);
    
    sleep(3);
    
    while( (pid = wait(&status)) > 0 )
    {
        printf("child: %d, status: %x\n", pid, status);
    }
    
    return 0;
}

该程序创建了三个子进程,子进程被创建出来后,通过不同的方式退出;在父进程中,通过 wait(...) 函数来得到子进程的退出状态

程序运行结果如下图所示:

pid 为 368521 的子进程,通过 exit(-1) 来退出,退出状态码应该为 -1,而 wait(...) 函数中得到该进程的退出状态码却为 0xFF00,这是因为退出状态码由多个部分组成

进程退出状态详解

进程的退出状态码是16位的整型数,bit0 - bit7 用于记录进程被信号终止的状态值;bit8 用于表示是否生成了 coredump,coredump 记录了进程崩溃前的信息,可以用于调试 ;bit9 - bit15 用于记录进程的退出状态值

进程退出状态详解

使用上面的宏来重新获取进程的提出状态

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


int main(int argc, char* argv[])
{   
    pid_t pid = 0;
    int a = 1;
    int b = 0;
    int status = 0;
    
    printf("parent = %d\n", getpid());
    
    if( (pid = fork()) == 0 ) exit(-1);
    
    printf("child = %d\n", pid);
    
    if( (pid = fork()) == 0 ) abort();
    
    printf("child = %d\n", pid);
    
    if( (pid = fork()) == 0 ) a = a / b, exit(1);
    
    printf("child = %d\n", pid);
    
    sleep(3);
    
    while( (pid = wait(&status)) > 0 )
    {
        if( WIFEXITED(status) )
        {
            printf("Normal - child: %d, code: %d\n", pid, (char)WEXITSTATUS(status));
        }
        else if( WIFSIGNALED(status) )
        {
            printf("Signaled - child: %d, code: %d\n", pid, WTERMSIG(status));
        }
        else
        {
            printf("Paused - child: %d, code: %d\n", pid, WSTOPSIG(status));
        }
    }
    
    return 0;
}

我们通过进程退出状态的相关宏,来得知进程是主动退出还是收到信号退出,并打印出对应的退出状态值或退出信号值

程序运行结果如下图所示:

-1 是 pid 为 368845 的子进程的退出状态值;6 和 8 分别是 pid 为 368846 和 368847 的子进程的退出信号值

僵尸进程 (僵死状态)

理论上,进程 退出 / 终止 后应立即释放所有系统资源

然而,为了给父进程提供一些重要信息,子进程 退出 / 终止 所占的部分资源会暂留

当父进程收集这部分信息后 (wait / waitpid),子进程所有资源被释放

  • 父进程调用 wait(),为子进程 "收尸" 处理并释放暂留资源
  • 若父进程退出,init / systemd 为子进程 "收尸" 处理并释放暂留资源

僵尸进程的危害

僵尸进程保留进程的终止状态和资源使用信息

  • 进程为何退出,进程消耗多少 CPU 时间,进程最大内存驻留值,等

如果僵尸进程得不到回收,那么可能影响正常进程的创建

  • 进程创建最重要的资源是内存和进程标识
  • 僵尸进程的存在可看作一种类型的内存泄露
  • 当系统僵尸进程过多,可能导致进程标识不足,无法创建新进程

僵尸进程初探


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


int main(int argc, char* argv[])
{   
    pid_t pid = 0;
    int a = 1;
    int b = 0;
    int status = 0;
    
    printf("parent = %d\n", getpid());
    
    if( (pid = fork()) == 0 ) exit(-1);
    
    printf("child = %d\n", pid);
    
    if( (pid = fork()) == 0 ) abort();
    
    printf("child = %d\n", pid);
    
    if( (pid = fork()) == 0 ) a = a / b, exit(1);
    
    printf("child = %d\n", pid);
    
    sleep(120);
    
    while( (pid = wait(&status)) > 0 )
    {
        if( WIFEXITED(status) )
        {
            printf("Normal - child: %d, code: %d\n", pid, (char)WEXITSTATUS(status));
        }
        else if( WIFSIGNALED(status) )
        {
            printf("Signaled - child: %d, code: %d\n", pid, WTERMSIG(status));
        }
        else
        {
            printf("Paused - child: %d, code: %d\n", pid, WSTOPSIG(status));
        }
    }
    
    return 0;
}


该程序创建了3个子进程,父进程 sleep 120s 后,使用 wait(...) 函数来获取子进程的退出状态

程序运行结果如下图所示:

红框圈出来的是 a.out 程序创建出来的3个子进程

此时,这三个子进程已经运行结束了,父进程还在 sleep 中,用 ps 查看,发现这三个子进程还存在,状态为 Z,处于僵尸态,资源并没有完全释放

父进程 sleep 120s,wait(...) 三个子进程后,再 ps 看下,发现已经没有 a.out 和 它的三个子进程了,此时,子进程的资源被父进程回收了

wait() 的局限性

不能等待指定子进程,如果存在多个子进程,只能逐一等待完成

如果不存在终止的子进程,父进程只能阻塞等待

只针对终止的进程,无法发现暂停的进程

wait() 的升级版 => waitpid

返回值相同,终止子进程标识符

状态值意义相同,记录子进程终止信息

特殊之处:

利用 waitpid(...) 以及 init / systemd 回收子进程


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

static void worker(pid_t pid)
{
    printf("grand-child: %d\n", pid);
    sleep(150);
}

int main(int argc, char* argv[])
{   
    pid_t pid = 0;
    int status = 0;
    
    printf("parent = %d\n", getpid());
    
    pid = fork();
    
    if( pid < 0 )
    {
        printf("fork error\n");
    }
    else if( pid == 0 )
    {
        int i = 0;
        
        for(i=0; i<5; i++)
        {
            if( (pid = fork()) == 0 )
            {
                worker(getpid());
                break;
            }
        }
        
        sleep(60);
        
        printf("child(%d) is over...\n", getpid());
    }
    else
    {
        printf("wait child = %d\n", pid);     
        sleep(120);
        while( waitpid(pid, &status, 0) == pid )
        {
            printf("Parent is over - child: %d, status = %x\n", pid, status);
        }
    }
    
    return 0;
}


第 22 行,创建了一个子进程

第 34 行,子进程创建了 5 个孙进程

第 49 行,父进程通过 waitpid 来等待子进程运行结束,回收子进程的资源

该程序子进程先运行结束,然后是父进程,最后是孙进程

子进程运行结束后,孙进程就变为了孤儿进程,被 init / systemd 进程接管,孙进程运行结束后,资源由 init / systemd 进程来回收,所以父进程就回收一个子进程的资源即可

程序运行结果如下图所示:

第一阶段,父进程、子进程 和 5个孙进程都在运行

 第二阶段,子进程运行结束,父进程并没有回收它的资源。此时,子进程处于僵尸态,5个孙进程成为孤儿进程,父进程变为 systemd ,由 systemd (pid 为 1) 进程回收资源

 

第三阶段,父进程运行结束,并回收子进程的资源,此时还有 5 个孙进程在运行 

第四阶段,所有进程运行结束,并且资源被回收

在程序设计中,我们可以通过子进程不做其他事情,只创建孙进程来完成任务,父进程 waitpid 子进程的方式来有效的解决僵尸进程带来的问题,这样我们就只需要回收子进程的资源,不用主动回收孙进程的资源了,而是通过 init / systemd 进程自动回收孙进程资源,不过这样就获取不到孙进程的退出状态了

僵尸进程避坑指南

通过 wait(...) 返回值来判断是否继续等待子进程

  • while ( (pid = wait(&status)) > 0 ) { ... }

利用 waitpid(...) 以及 init / systemd 回收子进程

  • 通过两次 fork() 创建孙进程解决子问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值