问题
进程参数 和 环境变量 对于进程意味着什么?
进程参数和环境变量的意义
一般情况下,子进程的创建是为了解决某个子问题
子进程解决问题需要父进程的 "数据输入" (进程参数 & 环境变量)
设计原则:
- 子进程启动时必然用到的参数使用进程参数传递
- 子进程解决问题可能用到的参数使用环境变量传递
思考
子进程如何将结果 "返回" 父进程?
#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() 创建孙进程解决子问题