『Linux从入门到精通』第 ⑰ 期 - 那年我手执『wait』桃木剑,轻松解决僵尸进程~

2023-11-21 21:20

本文主要是介绍『Linux从入门到精通』第 ⑰ 期 - 那年我手执『wait』桃木剑,轻松解决僵尸进程~,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

文章目录

  • 💐专栏导读
  • 💐文章导读
  • 🐧进程退出
    • 🐦进程常见的退出方法
      • 🐔正常终止
        • 🔔return 退出
        • 🔔exit 退出
        • 🔔_exit 退出
      • 🐔异常终止
  • 🐧进程等待
      • 🐦必要性
      • 🐦是什么
      • 🐦如何等待
        • 🔔解决子进程僵尸问题
        • 🔔如何获取子进程status
    • 🦋阻塞等待
    • 🦋非阻塞等待
      • 🐦完整代码

💐专栏导读

🌸作者简介:花想云 ,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。

🌸专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。

🌸相关专栏推荐:C语言初阶系列C语言进阶系列C++系列数据结构与算法

💐文章导读

前几章我们讲了关于如何创建进程与进程状态。那么本章我们就来看看进程在退出时又有哪些花样吧~ 为了解决之前所讲的僵尸进程问题,我们必须要让父进程得到子进程的退出状态,这就是本章的另一个话题——进程等待~

在这里插入图片描述

🐧进程退出

思考一下,我们创建一个进程的目的是什么?当然是想让进程帮助我们完成某件事情。例如:①将一个文件拷贝到某个目录下、②判断某个某个文件是否为空…

有些情况下,我们只需要进程去做某件事即可,并不需要关心它做的是否合格(这显然是不可取的行为);有时候不仅需要进程去做某件事,还需要关心它是否成功了,即关心一个进程的结果。

那么当一个进程结束时,一共可分为3中情况:

  1. 代码运行完毕,且结果正确;
  2. 代码运完毕,结果错误;
  3. 代码异常终止;

🐦进程常见的退出方法

🐔正常终止

🔔return 退出

在C语言中,我们编写程序时都会在main函数的最后加上一条语句——return 0; main函数中的return并不等同于其他函数的returnmain函数返回的其实是进程退出码

在Linux中,我们可以使用指令来查看一个进程的退出码:

$ echo $?
  • 只会保存最近一次进程退出的退出码。

示例

#include <stdio.h>
int main()
{int a = 10;int b = 30;int ret = a + b; // 代码1//int ret = a + b / 2; // 代码2// 结果正确返回0,错误返回1 if(ret == 40)return 0;else return 1;printf("mytest......\n");
}

代码1
在这里插入图片描述
代码2

在这里插入图片描述

🔔exit 退出

我们可能对这个函数并不陌生,它的作用就是终止进程。exit的参数就是退出进程时需要返回的退出码。

我们故意写一段错误的代码来看看exit返回的退出码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>void Sort(int* array,int n)
{if(array == NULL){perror("Array Fail");exit(111);}// 排序略...
}
int main()
{int * prt = NULL;Sort(prt,10);return 0}

在这里插入图片描述

🔔_exit 退出

_exit系统调用,并不像exit是C语言的库函数_exitexit使用方法完全相同,但是两者某些行为却有差别:

  • exit在退出进程之前会刷新缓冲区
  • _exit直接退出进程;

示例

exit 的表现

#include <stdio.h>
#include <stdlib.h>void Sort(int* array,int n)
{if(array == NULL){//per1ror("Array Fail");printf("函数发生错误");sleep(1);exit(111);}// 排序略...
}
int main()
{   int * prt = NULL;Sort(prt,10);return 0;
}

在这里插入图片描述

_exit 的表现

#include <stdio.h>
#include <stdlib.h>void Sort(int* array,int n)
{if(array == NULL){//per1ror("Array Fail");printf("函数发生错误");sleep(1);_exit(111);}// 排序略...
}
int main()
{   int * prt = NULL;Sort(prt,10);return 0;
}

在这里插入图片描述

很显然,在_exit的表现中,“函数发生错误”还在缓冲区中未刷新,进程就已经退出了。

  • exit最后其实也会调用_exit, 但在调用_exit之前,还做了其他工作:
    1. 执行用户通过 atexiton_exit定义的清理函数;
    2. 关闭所有打开的流,所有的缓存数据均被写入;
    3. 调用_exit;

在这里插入图片描述

🐔异常终止

除了程序自己发现异常而终止,一个程序还可能因为外力因素而提前终止。例如我们之前学习过的指令:kill -9 ,它的作用就是从外部“杀”掉进程、或者我们经常使用的Ctrl+c

$ kill -9 进程ID

🐧进程等待

🐦必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏

  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程;

  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如:子进程运行完成,结果对还是不对,或者是否正常退出;

  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息;

🐦是什么

我们谈进程等待,那么我们究竟要等待什么呢?

  • 进程等待,就是通过系统调用,获取子进程的退出码或者退出信号的方式,顺便解决内存释放的问题。

🐦如何等待

这里就要介绍两个系统调用接口了:

  • wait
pid_t wait(int *status);
  • 返回值:成功返回被等待进程pid,失败返回-1

  • 参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

🔔解决子进程僵尸问题

之前讲到过,由于父进程为对子进程进行等待,子进程就会进入僵尸状态,且危害极大。那么意味着,要避免僵尸问题,我们必须对子进程进行等待。代码如下:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{   pid_t id = fork();int cnt = 50;if(id == 0){// 子进程while(cnt){printf("我是子进程,我还在运行,我还有%d...\n",cnt--);sleep(1);}exit(111);}// 父进程pid_t ret_id = wait(NULL);printf("%d %d\n",id,ret_id);sleep(10);
}

在这里插入图片描述

如图所示,在kill -9 终止子进程后,子进程并没有进入僵尸状态。而且可以清晰的看到,wait的返回值就是子进程的PID

🔔如何获取子进程status

在这里插入图片描述

  • waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

  • 如果传递NULL,表示不关心子进程的退出状态信息。

  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

  • status不能简单的当作整型来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

在这里插入图片描述

做个形象的比喻,如果你正在考试,如果你以正确的方式参加了考试,那么你的成绩必定有好有坏,就看你如何看待自己考试的结果了;但是,如果你考试作弊了,考试还未结束,你就被监考老师叉出去了,提前结束了考试,此时,你的卷子尽管可能有得分,这个得分有没有参考价值呢?当然没有。

所以,如果一个进程正常终止,我们可以拿到它的退出状态即进程退出码;如果一个进程被信号(监考老师)终止,此时退出状态是没有意义的,但是我们可以查看终止信号,(至少看看是什么原因导致的考试异常结束)。

示例

  • 进程正常终止;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{   pid_t id = fork();int cnt = 5;if(id == 0){// 子进程while(cnt){printf("我是子进程,我还在运行,我还有%d...\n",cnt--);sleep(1);}exit(111);}// 父进程int status = 0;pid_t ret_id = wait(&status);printf("child exit code:%d,child exit singl:%d\n",(status >> 8) & 0xFF,status & 0x7F);
}

在这里插入图片描述

  • kill -9 终止进程(把计时改为20,因为我手速没那么快…);

在这里插入图片描述


  • waitpid;
pid_ t waitpid(pid_t pid, int *status, int options);
  • 返回值:

    • 当正常返回的时候waitpid返回收集到的子进程的PID
    • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
    • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
  • 参数:

    • pid
      • Pid = -1,等待任意一个子进程,与wait等效。
      • Pid > 0,等待其进程IDpid相等的子进程。
    • status
      • WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
      • WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
  • options:

    • WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID

waitpidwait的用法大致是类似的,这里就不做专门的演示了。

🦋阻塞等待

仔细观察上文中示例中的结果,父进程的输出总在子进程结束之后,如图:

在这里插入图片描述

在子进程运行期间,父进程一直在等待,并没有做其他事情,直到等待子进程成功。我们把这种情况称为父进程在进行阻塞等待

在这里插入图片描述

如果父进程不想干干地等待子进程结束,而是想在等待的期间做点其他有意义的事情该如何处理呢?

🦋非阻塞等待

  • 首先我们先预设一批任务,在父进程等待期间执行这些任务;
void sync_disk()
{printf("这是一个刷新数据的任务\n");
}void sync_log()
{printf("这是一个同步日志的任务\n");
}void net_send()
{printf("这是一个网络发送的任务\n");
}
  • 设置任务加载函数、任务运行函数、任务初始化函数等;
#define TASK_NUM 10 // 任务的数量typedef void (*FUNC_PTR)(); // 函数指针类型重定义FUNC_PTR other_task[TASK_NUM]={NULL}; // 定义一个函数指针数组int LoadTask(FUNC_PTR func)
{int i = 1;for(; i < TASK_NUM; ++i){if(other_task[i]==NULL) break;}if(i == TASK_NUM) return -1;else other_task[i] = func;return 0;
}void InitTask()
{int i = 0;for(; i < TASK_NUM; ++i) other_task[i] == NULL;LoadTask(sync_disk);LoadTask(sync_log);LoadTask(net_send);
}void RunTask()
{for(int i = 0; i < TASK_NUM; i++){if(other_task[i] == NULL) continue;other_task[i]();}
}
  • main函数设计;
int main()
{   pid_t id = fork();int cnt = 15;if(id == 0){// 子进程while(cnt){printf("我是子进程,我还在运行,我还有%d...\n",cnt--);sleep(1);}exit(111);}InitTask();// 父进程while(1){int status = 0;pid_t ret_id = waitpid(id,&status,WNOHANG);if(ret_id < 0){printf("等待出错\n");exit(1);}else if(ret_id == 0) // 子进程还未结束,做做其他事情{RunTask();sleep(1);continue;}else // 已等待成功 {if(WIFEXITED(status)) // 子进程正常退出{printf("等待成功,子进程pid:%d,子进程退出码:%d\n",id,WEXITSTATUS(status));}else // 子进程异常退出{printf("等待成功,子进程pid:%d,子进程退出信号:%d\n",id,status&0x7F);}break;}}
}

效果如下

在这里插入图片描述

🐦完整代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>#define TASK_NUM 10 // 任务的数量void sync_disk()
{printf("这是一个刷新数据的任务\n");
}void sync_log()
{printf("这是一个同步日志的任务\n");
}void net_send()
{printf("这是一个网络发送的任务\n");
}typedef void (*FUNC_PTR)(); // 函数指针类型重定义FUNC_PTR other_task[TASK_NUM]={NULL};int LoadTask(FUNC_PTR func)
{int i = 1;for(; i < TASK_NUM; ++i){if(other_task[i]==NULL) break;}if(i == TASK_NUM) return -1;else other_task[i] = func;return 0;
}void InitTask()
{int i = 0;for(; i < TASK_NUM; ++i) other_task[i] == NULL;LoadTask(sync_disk);LoadTask(sync_log);LoadTask(net_send);
}void RunTask()
{for(int i = 0; i < TASK_NUM; i++){if(other_task[i] == NULL) continue;other_task[i]();}
}int main()
{   pid_t id = fork();int cnt = 15;if(id == 0){// 子进程while(cnt){printf("我是子进程,我还在运行,我还有%d...\n",cnt--);sleep(1);}exit(111);}InitTask();// 父进程while(1){int status = 0;pid_t ret_id = waitpid(id,&status,WNOHANG);if(ret_id < 0){printf("等待出错\n");exit(1);}else if(ret_id == 0) // 子进程还未结束,做做其他事情{RunTask();sleep(1);continue;}else // 已等待成功 {if(WIFEXITED(status)) // 子进程正常退出{printf("等待成功,子进程pid:%d,子进程退出码:%d\n",id,WEXITSTATUS(status));}else // 子进程异常退出{printf("等待成功,子进程pid:%d,子进程退出信号:%d\n",id,status&0x7F);}break;}}
}

本章的内容就到这里了,觉得对你有帮助的话就支持一下博主吧~

在这里插入图片描述

点击下方个人名片,交流会更方便哦~
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

这篇关于『Linux从入门到精通』第 ⑰ 期 - 那年我手执『wait』桃木剑,轻松解决僵尸进程~的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/405190

相关文章

闲置电脑也能活出第二春?鲁大师AiNAS让你动动手指就能轻松部署

对于大多数人而言,在这个“数据爆炸”的时代或多或少都遇到过存储告急的情况,这使得“存储焦虑”不再是个别现象,而将会是随着软件的不断臃肿而越来越普遍的情况。从不少手机厂商都开始将存储上限提升至1TB可以见得,我们似乎正处在互联网信息飞速增长的阶段,对于存储的需求也将会不断扩大。对于苹果用户而言,这一问题愈发严峻,毕竟512GB和1TB版本的iPhone可不是人人都消费得起的,因此成熟的外置存储方案开

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

如何解决线上平台抽佣高 线下门店客流少的痛点!

目前,许多传统零售店铺正遭遇客源下降的难题。尽管广告推广能带来一定的客流,但其费用昂贵。鉴于此,众多零售商纷纷选择加入像美团、饿了么和抖音这样的大型在线平台,但这些平台的高佣金率导致了利润的大幅缩水。在这样的市场环境下,商家之间的合作网络逐渐成为一种有效的解决方案,通过资源和客户基础的共享,实现共同的利益增长。 以最近在上海兴起的一个跨行业合作平台为例,该平台融合了环保消费积分系统,在短

数论入门整理(updating)

一、gcd lcm 基础中的基础,一般用来处理计算第一步什么的,分数化简之类。 LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } <pre name="code" class="cpp">LL lcm(LL a, LL b){LL c = gcd(a, b);return a / c * b;} 例题:

Java 创建图形用户界面(GUI)入门指南(Swing库 JFrame 类)概述

概述 基本概念 Java Swing 的架构 Java Swing 是一个为 Java 设计的 GUI 工具包,是 JAVA 基础类的一部分,基于 Java AWT 构建,提供了一系列轻量级、可定制的图形用户界面(GUI)组件。 与 AWT 相比,Swing 提供了许多比 AWT 更好的屏幕显示元素,更加灵活和可定制,具有更好的跨平台性能。 组件和容器 Java Swing 提供了许多