嵌入式Linux-对子进程的监控

news2024/12/23 19:18:25

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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 <sys/types.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
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 <sys/types.h>
#include <sys/wait.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
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()函数是完全等价的。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/147386.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode 1658. 将 x 减到 0 的最小操作数[python3 双指针实现与思路整理]

题目 给你一个整数数组 nums 和一个整数 x 。每一次操作时&#xff0c;你应当移除数组 nums 最左边或最右边的元素&#xff0c;然后从 x 中减去该元素的值。请注意&#xff0c;需要 修改 数组以供接下来的操作使用。 如果可以将 x 恰好 减到 0 &#xff0c;返回 最小操作数 &a…

HTML与CSS基础(四)—— CSS基础(选择器进阶、Emmet语法、背景属性、元素显示模式、三大特性)

一、选择器进阶目标&#xff1a;能够理解 复合选择器 的规则&#xff0c;并使用 复合选择器 在 HTML 中选择元素1. 复合选择器1.1 后代选择器&#xff1a;空格作用&#xff1a;根据 HTML 标签的嵌套关系&#xff0c;选择父元素 后代中 满足条件的元素 选择器语法&#xff1a;选…

第二章JavaWeb基础学习路线

文章目录什么是Java WebJava Web基础的技术栈关于我们的客户端与服务端&#xff08;BS&#xff09;我们客户端的形式**PC端网页****移动端**服务端应用程序关于请求&#xff08;request&#xff09;和响应(response)类比生活中的请求和响应服务器中的请求和响应项目的逻辑构成架…

CSS权威指南(六)文字属性

1.缩进和行内对齐 &#xff08;1&#xff09;缩进文本&#xff08;text-indent&#xff09; text-indent属性把元素的第一行文本缩进指定的长度&#xff0c;缩进的长度可以可以是负值。这个属性通常用于缩进段落的第一行。text-indent作用于块级元素之上&#xff0c;缩进将沿着…

config:配置中心

Spring Cloud Config 为分布式系统中的外部配置提供服务器端和客户端支持。使用 Config Server&#xff0c;您可以集中管理所有环境中应用程序的外部配置。 Spring Cloud Config就是一个配置中心&#xff0c;所有的服务都可以从配置中心取出配置&#xff0c;而配置中心又可以从…

mmap(内存映射)、sendfile() 与零拷贝技术

内存映射&#xff08;Memory-mapped I/O&#xff09;是将磁盘文件的数据映射到内存&#xff0c;用户通过修改内存就能修改磁盘文件。 RocketMQ为什么快&#xff1f;kafka为什么快&#xff1f;什么是mmap&#xff1f;这些问题都逃不过一个点&#xff0c;就是零拷贝。 虽然还有其…

电脑不能开机的几个常见原因

现在手机已经将电脑取代了&#xff0c;用电脑的越来越少&#xff0c;因为一些原因上网课的多了起来&#xff0c;大家都将放置几年的电脑搬了出来&#xff0c;开不开机的大有人在&#xff0c;由于机器闲置很久大多都出现了各种各样的故障和问题&#xff0c;在这里总结了电脑台式…

C语言:浮点型存储方式

浮点型存储方式 任意一个二进制浮点数V可以表示成下面的形式 (-1)^S *M *2^E 1&#xff08;S符号位&#xff09; 8&#xff08;E阶码&#xff09; 23&#xff08;M尾码&#xff09;省略首位1 S&#xff1a;表示正负 只有0/1两个值 M&#xff1a;由浮点数转化成二进制数表示 在…

4.7、IPv4 数据报的首部格式

固定部分&#xff1a;每个 IP 数据报首部都必须包含的内容 某些 IP 数据报的首部除了包含 202020 字节的固定部分外&#xff0c;还包含一些可选的字段来增加 IP 数据报的功能 IP 数据报的首部常以 323232 个比特为单位进行描述 图中的每一行都由 323232 个比特&#xff08;也…

小波分析——4.使用小波对信号成分进行分析

文章目录首先创建一个包含多频率成分的信号然后我们用数学实现一个墨西哥草帽小波然后我们开始对原始信号进行处理吧接下来可以把信号成分进行绘制在前面的章节里已经介绍过小波的理论、公式等知识点&#xff0c;现在我们来看看如何用小波来实现对复杂信号的成分分析。 在我们…

性能优化系列之『HTML5 离线化:主流的技术实现方案有哪些?』

文章の目录一、离线包类型二、离线包架构三、离线包下载四、离线包运行模式五、大厂离线包方案写在最后一、离线包类型 全局离线包&#xff1a;包含公共的资源&#xff0c;可供多个应用共同使用私有离线包&#xff1a;只可以被某个应用单独使用 二、离线包架构 三、离线包下载…

Fairness in Recommendation: A Survey 阅读笔记

论文链接 搁置了许久的毕设&#xff0c;又要开始重新启航。 2022年的最后一段时间过得真是很崎岖&#xff0c;2023希望大家平安喜乐。 课设还未结束&#xff0c;但是毕设不能再拖&#xff0c;开工啦&#xff01;这又是一篇综述&#xff0c;有关推荐系统中的公平性&#xff0c;…

5.Isaac教程--创建Isaac应用

创建Isaac应用 本教程将指导您完成使用 Isaac SDK 创建机器人应用程序的过程&#xff0c;以视频输入的 OpenCV 边缘检测处理为例。 文章目录创建Isaac应用预安装显示相机源创建应用程序文件启用节点间通信配置组件创建 Bazel 构建文件运行应用程序查看相机源处理相机源添加边缘…

1.6日报

以redis为基础完成 addCacheData getCacheDataByUniqueId delCacheDataByUniqueId 并且测试通过。 升级getQRcodeAndScene接口&#xff0c;添加版本参数&#xff0c;实现“不同环境取不同小程序版本“功能 并添加扫码跳转页面功能。 遇到的问题及解决 加深对RequestBody…

性能优化系列之『HTTP-2 :升级HTTP-2的好处有哪些?如何升级?』

文章の目录一、HTTP/2 概念二、HTTP/2 优点三、HTTP/2 站点的优势四、在 Nginx 上启用 HTTP/21、升级 OpenSSL2、重新编译3、验证 HTTP/24、浏览器请求截图写在最后一、HTTP/2 概念 HTTP/2&#xff08;超文本传输协议第2版&#xff0c;最初命名为 HTTP 2.0&#xff09;&#x…

【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解

文章目录前言AOP与Spring AOPAspect简单案例快速入门一、Pointcutannotation二、五种通知Advice1. Before前置通知2. After后置通知3. AfterRunning返回通知4. AfterThrowing异常通知5. Around环绕通知总结前言 在微服务流行的当下&#xff0c;在使用SpringCloud/Springboot框…

分布式基础篇3——前端开发基础知识

前端技术对比一、ES61、简介2、什么是 JavaScript3、ES6新特性3.1 let3.2 const3.3 解构表达式3.4 字符串扩展3.5 函数优化3.6 对象优化3.7 map 和 reduce3.8 Promise3.9 模块化二、Vue1、MVVM 思想2、Vue 简介3、Vue 入门案例4、Vue 指令插值表达式v-text、v-htmlv-bindv-mode…

景区地图最短路径快速实现

1 前言以前粗略学习了一下在地图中实现最短路径&#xff0c;并在切图工具中实现了自动处理生成导航相关数据。https://blog.csdn.net/bq_cui/article/details/86795213最近发现工具实现的结果&#xff0c;错误一大堆。这次再详细捋一捋整个步骤&#xff0c;感兴趣的同学可以试一…

ctemplate 的安装和使用

ctemplate 用于linux下的web开发&#xff0c;可以动态生成一个html网页&#xff0c;这里的 “ 动态 ” 指的是网页的数据不是固定的&#xff0c;可以使用变量来填充网页内容。 目录 1、下载ctemplate 2、安装 ctemplate 3、使用ctemplate库 1、下载ctemplate 可以在gite…

前端对接微信公众号网页开发流程,授权对接

前面讲到 前端对接微信公众号网页开发流程&#xff0c;前期配置&#xff0c;本篇文章主要详细介绍关于公众号的授权对接。 一、引入微信js-sdk 在需要调用 JS 接口的页面引入如下 JS 文件 http://res.wx.qq.com/open/js/jweixin-1.6.0.js如需进一步提升服务稳定性&#xff0…