嵌入式Linux-对子进程的监控
创始人
2024-05-08 13:31:02
0

1. 进程的诞生与终止

1.1 进程的诞生

一个进程可以通过 fork()或 vfork()等系统调用创建一个子进程,一个新的进程就此诞生!事实上,Linux系统下的所有进程都是由其父进程创建而来,譬如在 shell 终端通过命令的方式执行一个程序./app,那么 app进程就是由 shell 终端进程创建出来的,shell 终端就是该进程的父进程。

既然所有进程都是由其父进程创建出来的,那么总有一个最原始的父进程吧,否则其它进程是怎么创建出来的呢?确实如此,在 Ubuntu 系统下使用"ps -aux"命令可以查看到系统下所有进程信息,如下:
在这里插入图片描述
上图中进程号为 1 的进程便是所有进程的父进程,通常称为 init 进程,它是 Linux 系统启动之后运行的第一个进程,它管理着系统上所有其它进程,init 进程是由内核启动,因此理论上说它没有父进程。init 进程的 PID 总是为 1,它是所有子进程的父进程,一切从 1 开始、一切从 init 进程开始!
一个进程的生命周期便是从创建开始直至其终止。

1.2 进程的终止

通常,进程有两种终止方式:异常终止和正常终止。

正常终止main 函数中使用 return 返回、调用 exit()函数结束进程、调用_exit()或_Exit()函数结束进程等
异常终止在程序当中调用 abort()函数异常终止进程、当进程接收到某些信号导致异常终止等

_exit()函数和 exit()函数的 status 参数定义了进程的终止状态(termination status),父进程可以调用 wait()函数以获取该状态。虽然参数 status 定义为 int 类型但仅有低 8 位表示它的终止状态,一般来说,终止状态为 0 表示进程成功终止,而非 0 值则表示进程在执行过程中出现了一些错误而终止,譬如文件打开失败、读写失败等等,对非 0 返回值的解析并无定例。

在我们的程序当中,一般使用 exit()库函数而非_exit()系统调用,原因在于 exit()最终也会通过_exit()终止进程,但在此之前,它将会完成一些其它的工作,exit()函数会执行的动作如下:

  1. 如果程序中注册了进程终止处理函数,那么会调用终止处理函数。
  2. 刷新 stdio 流缓冲区。
  3. 执行_exit()系统调用。

所以,由此可知,exit()函数会比_exit()会多做一些事情,包括执行终止处理函数、刷新 stdio 流缓冲以及调用_exit(),在前面曾提到过,在我们的程序当中,父、子进程不应都使用 exit()终止,只能有一个进程使用 exit()、而另一个则使用_exit()退出,当然一般推荐的是子进程使用_exit()退出、而父进程则使用 exit()退出。其原因就在于调用 exit()函数终止进程时会刷新进程的 stdio 缓冲区。接下来我们便通过一个示例代码进行说明:

#include 
#include 
#include 
int main(void)
{printf("Hello World!\n");switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */exit(0);default:/* 父进程 */exit(0);}}

在这里插入图片描述
打印结果确实如我们所料,接下来将代码进行简单地修改,把 printf()打印的字符串最后面的换行符\n去掉,如下所示:

#include 
#include 
#include 
int main(void)
{printf("Hello World!");switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */exit(0);default:/* 父进程 */exit(0);}
}

在这里插入图片描述
从打印结果可知,"Hello World!"被打印了两次,这是怎么回事呢?在程序当中明明只使用了 printf 打印了一次字符串。要解释这个问题,首先要知道,进程的用户空间内存中维护了 stdio 缓冲区,通过 fork()创建子进程时会复制这些缓冲区。标准输出设备默认使用的是行缓冲,当检测到换行符\n 时会立即显示函数 printf()输出的字符串,在第一次实例代码中 printf 输出的字符串中包含了换行符所以会立即读走缓冲区中的数据并显示,读走之后此时缓冲区就空了,子进程虽然拷贝了父进程的缓冲区,但是空的,虽然父、子进程使用 exit()退出时会刷新各自的缓冲区,但对于空缓冲区自然无数据可读

但在第二次代码中,printf()并没有添加换行符\n,当调用 printf()时并不会立即读取缓冲区中的数据进行显示,由此 fork()之后创建的子进程也自然拷贝了缓冲区的数据,当它们调用 exit()函数时,都会刷新各自的缓冲区、显示字符串,所以就会看到打印出了两次相同的字符串。

tips:这个结果,可以当一个结论吧,这也许可能会影响到后续编程时,出现的一些看不出问题的因素。
所以,可以采用以下任一方法来避免重复的输出结果:

  1. 对于行缓冲设备,可以加上对应换行符,譬如 printf 打印输出字符串时在字符串后面添加\n 换行符,对于 puts()函数来说,本身会自动添加换行符;
  2. 在调用 fork()之前,使用函数 fflush()来刷新 stdio 缓冲区,当然,作为另一种选择,也可以使用setvbuf()和 setbuf()来关闭 stdio 流的缓冲功能;
  3. 子进程调用_exit()退出进程、而非使用 exit(),调用_exit()在退出时便不会刷新 stdio 缓冲区,这也解释前面为什么我们要在子进程中使用_exit()退出这样做的一个原因。

2. 监视子进程

在很多应用程序的设计中,父进程需要知道子进程于何时被终止,并且需要知道子进程的终止状态信息,是正常终止、还是异常终止亦或者被信号终止等,意味着父进程会对子进程进行监视。

所以本小结,多来探讨一下通过前面提到的wait()【监视子进程状态的函数】,来观察进程的变化。

2.1 wait()函数

对于许多需要创建子进程的进程来说,有时设计需要监视子进程的终止时间以及终止时的一些状态信息,在某些设计需求下这是很有必要的。系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,其函数原型如下所示:

#include 
#include pid_t wait(int *status);

函数参数和返回值含义如下:
status:参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程终止时的状态信息。

返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1。系统调用 wait()将执行如下动作:

  1. 调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终止;
  2. 如果进程调用 wait(),但是该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
  3. 如果进程调用 wait()之前,它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait()也不会阻塞。wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程“收尸”,关于这个问题后面再给大家进行介绍。所以在调用 wait()函数之前,已经有子进程终止了,意味着正等待着父进程为其“收尸”,所以调用 wait()将不会阻塞,而是会立即替该子进程“收尸”、处理它的“后事”,然后返回到正常的程序流程中,一次 wait()调用只能处理一次。

参数 status 不为 NULL 的情况下,则 wait()会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 status信号参数:

  1. WIFEXITED(status):如果子进程正常终止,则返回 true;
  2. WEXITSTATUS(status):返回子进程退出状态,是一个数值,其实就是子进程调用_exit()或 exit()时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()或 exit()时指定的状态,可通过WEXITSTATUS 宏转换;
  3. WIFSIGNALED(status):如果子进程被信号终止,则返回 true;
  4. WTERMSIG(status):返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号;
  5. WCOREDUMP(status):如果子进程终止时产生了核心转储文件,则返回 true;
#include 
#include 
#include 
#include 
#include 
#include 
int main(void)
{int status;int ret;int i;/* 循环创建 3 个子进程 */for (i = 1; i <= 3; i++) {switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */printf("子进程<%d>被创建\n", getpid());sleep(i);_exit(i);default:/* 父进程 */break;}}sleep(1);printf("~~~~~~~~~~~~~~\n");for (i = 1; i <= 3; i++) {ret = wait(&status);if (-1 == ret) {if (ECHILD == errno) {printf("没有需要等待回收的子进程\n");exit(0);}else {perror("wait error");exit(-1);}}printf("回收子进程<%d>, 终止状态<%d>\n", ret,WEXITSTATUS(status));}exit(0);
}

示例代码中,通过 for 循环创建了 3 个子进程,父进程中循环调用 wait()函数等待回收子进程,并将本次回收的子进程进程号以及终止状态打印出来,编译测试结果如下:
在这里插入图片描述

2.2 waitpid()函数

使用 wait()系统调用存在着一些限制,这些限制包括如下:

  1. 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
  2. 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
  3. 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了。

为了突破这些限制,在linux中,设计了waitpid()函数;

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

函数参数和返回值含义如下:
pid:参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:

  1. 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
  2. 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
  3. 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
  4. 如果 pid 等于-1,则等待任意子进程。wait(&status)与 waitpid(-1, &status, 0)等价。

status:与 wait()函数的 status 参数意义相同。
options:下面介绍。

返回值:返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况下,返回值会出现 0,稍后介绍。

options:一个位掩码,可以包括 0 个或多个如下标志:

  1. WNOHANG:如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
  2. WUNTRACED:除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
  3. WCONTINUED:返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。从以上的介绍可知,waitpid()在功能上要强于 wait()函数,它弥补了 wait()函数所带来的一些限制,具体在实际的编程使用当中,可根据自己的需求进行选择。

将上述wait()的代码改成waitpid()的代码:

#include 
#include 
#include 
#include 
#include 
#include 
int main(void)
{int status;int ret;int i;/* 循环创建 3 个子进程 */for (i = 1; i <= 3; i++) {switch (fork()) {case -1:perror("fork error");exit(-1);case 0:/* 子进程 */printf("子进程<%d>被创建\n", getpid());sleep(i);_exit(i);default:/* 父进程 */break;}}sleep(1);printf("~~~~~~~~~~~~~~\n");for (i = 1; i <= 3; i++) {ret = waitpid(-1, &status, 0);if (-1 == ret) {if (ECHILD == errno) {printf("没有需要等待回收的子进程\n");exit(0);}else {perror("wait error");exit(-1);}}printf("回收子进程<%d>, 终止状态<%d>\n", ret,WEXITSTATUS(status));}exit(0);
}

将 wait(&status)替换成了 waitpid(-1, &status, 0),通过上面的介绍可知,waitpid()函数的这种参数配置情况与 wait()函数是完全等价的。
在这里插入图片描述

相关内容

热门资讯

【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
ASM贪吃蛇游戏-解决错误的问... 要解决ASM贪吃蛇游戏中的错误问题,你可以按照以下步骤进行:首先,确定错误的具体表现和问题所在。在贪...
AsusVivobook无法开... 首先,我们可以尝试重置BIOS(Basic Input/Output System)来解决这个问题。...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...