当前位置: 首页 > news >正文

生命之舞:创建,终止与等待,Linux进程控制的交响乐章

文章目录

  • 引言:进程的诞生与消亡
  • 一、进程创建
    • 1.1、fork函数
    • 1.2、写时拷贝
  • 二、进程终止
    • 2.1、退出码
    • 2.2、退出方式
  • 三、进程等待
    • 3.1、等待原因
  • 3.2、等待函数
    • 3.3、等待时执行
  • 结语:进程的诗意

在这里插入图片描述

引言:进程的诞生与消亡

在操作系统的广袤宇宙中,进程如繁星般闪烁。它们诞生、闪耀、相互交织,最终化为虚无,将自己的成果留给这个数字世界。Linux系统,作为这片宇宙中最为璀璨的星系之一,以其独特的机制编排着这场生命的交响乐。

本文,我们将探索Linux进程控制的三重奏:创建终止等待,深入理解这场生命之舞的优雅节奏与内在规律。

一、进程创建

在学习 进程控制 相关知识前,先要对回顾如何创建 进程,涉及一个重要的函数 fork

1.1、fork函数

#include <unistd.h>	//所需头文件
pid_t fork(void);	//fork 函数

fork 函数的作用是在当前 进程 下,创建一个 子进程子进程 创建后,会为其分配新的内存块和内核数据结构(PCB),将 父进程 中的数据结构内容拷贝给 子进程,同时还会继承 父进程 中的环境变量表

  • 进程具有独立性,父子进程具有不同的pid
  • 如果子进程发生对数据的改写行为,会触发写时拷贝机制

fork 函数返回类型为 pid_t,相当于 typedef int,不过是专门用于进程的,同时它拥有两个返回值

  • 如果进程创建失败,返回 -1
  • 进程创建成功后,给子进程返回 0,给父进程返回子进程PID

在这里插入图片描述

代码示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> //进程等待相关函数头文件
#include<stdlib.h>int main()
{//创建两个子进程pid_t id1 = fork();if(id1 == 0){//子进程创建成功,创建孙子进程pid_t id2 = fork();if(id2 == 0){printf("我是孙子进程,PID:%d   PPID:%d\n", getpid(), getppid());exit(1); //孙子进程运行结束后,退出}wait(0);  //等待孙子进程运行结束printf("我是子进程,PID:%d   PPID:%d\n", getpid(), getppid());exit(1);  //子进程运行结束后,退出}wait(0);  //等待子进程运行结束printf("我是父进程,PID:%d   PPID:%d\n", getpid(), getppid());return 0; //父进程运行结束后,退出
}

在这里插入图片描述
观察结果不难发现,两个子进程已经成功创建,但最晚创建的进程,总是最先运行,这是因为fork创建进程后,先执行哪个进程取决于调度器

得到子进程后,此时可以在一个程序中同时执行两个进程!(父进程非阻塞的情况下)

注意:fork 可能创建进程失败

原因如下:

  • 系统中的进程过多时
  • 实际用户的进程数超过了限制

总结:

  • fork之前父进程独立执行,fork之后,父子两个执行流分别执行
  • 注意,fork之后,谁先执行完全由调度器决定。

1.2、写时拷贝

【进程地址空间】一文中,谈到了写时拷贝机制,实现原理就是通过 页表+MMU 机制,对不同的进程进行空间寻址,达到出现改写行为时,父子进程使用不同真实空间的效果

写时拷贝本质上是一种为了优化所做的赌博行为:

  • 系统假设你创建的子进程并没有对数据进行修改,此时子进程中的数据就是父进程中的数据,无须再存储一份,优化了时间和空间
  • 当发生改写时,再进行拷贝赋值,避免污染父进程中的数据

验证写时拷贝现象很简单,创建子进程后,使其对生命周期长的变量作出修改,再观察父子进程的结果即可

代码示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> //进程等待相关函数头文件
#include<stdlib.h>,const char* ps = "This is an Apple";  //全局属性int main()
{pid_t id = fork();if(id == 0){ps = "This is a Banana"; //改写printf("我是子进程,我认为:%s\n", ps);exit(0);  //子进程退出}wait(0);  //等待子进程退出printf("我是父进程,我认为:%s\n", ps);return 0;
}

在这里插入图片描述
不难发现,子进程对指针 ps 指向内容做出改变时,父进程并不受影响,这就是写时拷贝机制

  • 通过地址打印,发现父子进程中的 ps 地址一致,因为此时是虚拟地址
  • 虚拟地址相同的情况下,真实地址是不同的,得益于 页表+MMU 机制寻址不同的空间

写时拷贝机制本质上是一种按需申请资源的策略
在这里插入图片描述

注意:

  • 写时拷贝不止可以发生在常规栈区、堆区,还能发生在只读的数据段代码段
  • 写时拷贝后,生成的是本,不会对原数据造成影响

二、进程终止

  • 假设某个进程陷入了死循环状态,可以通过特定方法终止此程序,如在命令行中莫名其妙输入了一个指令,导致出现非正常情况,可以通过ctrl + c终止当前进程;
  • 对于自己写的程序,有多种终止方法,程序退出时,还会有一个退出码,供 父进程 接收

进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

在这里插入图片描述

2.1、退出码

echo $?

echo $? 打印一个最近程序退出时的退出码

main函数的返回值,代表程序的执行情况,返回值返回给寄存器,进行存储判断

退出码是给父进程看的,可以判断子进程是否成功运行

子进程运行情况:

  • 运行失败或异常终止,此时出现终止信号,无退出码
  • 运行成功,返回退出码,可能出现结果错误的情况

在这里插入图片描述
进程退出后,OS 会释放对应的 内核数据结构+代码和数据

main 函数退出,表示整个程序退出,而程序中的函数退出,仅表示该函数运行结束

2.2、退出方式

对一个正在运行中的进程,存在两种终止方式:外部终止内部终止.

外部终止时,通过kill -9 PID指令,强行终止正在运行中的程序,或者通过ctrl + c终止前台运行中的程序

内部终止是通过函数exit()_exit() 实现的

  • 之前在程序编写时,发生错误行为时,可以通过 exit(-1) 的方式结束程序运行.
  • 代码中任意地方调用此函数,都可以提前终止程序
void exit(int status);void _exit(int status);

这两个退出函数,从本质上来说,没有区别,都是退出进程,但在实际使用时,还是存在一些区别,推荐使用 exit()

比如在下面这段程序中,分别使用exit() _exit() 观察运行结果

int main()
{printf("You can see me");//exit(-1); //退出程序//_exit(-1);  //第二个函数return 0;
}
  • 使用exit()时,输出语句
  • 使用 _exit() 时,并没有任何语句输出

原因:

  • exit() 是对 _exit() 做的封装实现
  • _exit() 就只是单纯的退出程序
  • exit()在退出之前还会做一些事,比如冲刷缓冲区,再调用 _exit()
  • 程序中输出语句位于输出缓冲区,不冲刷的话,是不会输出内容的

在这里插入图片描述
Linux Shell 中的主要退出码:
在这里插入图片描述

三、进程等待

僵尸进程 是一个比较麻烦的问题,如果不对其做出处理,僵尸进程 就会越来越多,导致 内存泄漏 标识符 占用问题

3.1、等待原因

子进程运行结束后,父进程没有等待并接收其退出码和退出状态,OS 无法释放对应的 内核数据结构+代码和数据,出现 僵尸进程

为了避免这种情况的出现,父进程可以通过函数等待子进程运行结束,此时父进程属于阻塞状态

注意:

  • 进程的退出状态是必要的
  • 进程的执行结果是非必要的
  • 也就是说,父进程必须对子进程负责,确保子进程能够被正常回收,而子进程执行的结果是否正确,需要我们自行判断

3.2、等待函数

系统提供的父进程等待函数有两个 wait() waitpid(),后者比较常用

#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int* status);pid_t waitpid(pid_t pid, int* status, int options);
  • wait() 函数前面已经演示过了,这里着重介绍 waitpid() 返回值及其参数
  • wait() 中的返回值和参数,包含在 waitpid()

返回值:

  • 等待成功时,返回 >0 的值
  • 等待失败时,返回 -1
  • 等待中,返回 0

参数列表:

  • pid 表示所等子进程的 PID
  • status 表示状态,为整型,其中高 16 位不管,低 16 位中,次低 8 位表示退出码,第 7 位表示 core dump,低 7 位表示终止信号
  • options 为选项,比如可以选择父进程是否需要阻塞等待子进程退出,如果不关心退出码,则可以直接传入null或0

需要特别注意 status

在这里插入图片描述
代码示例:

int main()
{//演示 waitpid()pid_t id = fork();  //创建子进程if(id == 0){int time = 5;int n = 0;while(n < time){printf("我是子进程,我已经运行了:%d秒 PID:%d   PPID:%d\n", n + 1, getpid(), getppid());sleep(1);n++;}exit(244);  //子进程退出}int status = 0; //状态pid_t ret = waitpid(id, &status, 0); //参数3 为0,为默认选项if(ret == -1){printf("进程等待失败!进程不存在!\n");}else if(ret == 0){printf("子进程还在运行中!\n");}else{printf("进程等待成功,子进程已被回收\n");}printf("我是父进程, PID:%d   PPID:%d\n", getpid(), getppid());//通过 status 判断子进程运行情况if((status & 0x7F)){printf("子进程异常退出,core dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));}else{printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);}return 0;
}

此时未发出终止信号,进程自然结束
在这里插入图片描述

  • waitpid() 的返回值可以帮助我们判断此时进程属于什么状态(在下一份测试代码中表现更明显)
  • status 的不同部分,可以帮助我们判断子进程因何而终止,并获取 退出码(终止信号)

在进程的 PCB 中,包含了 int _exit_codeint _exit_signal 这两个信息,可以通过对 status 的位操作间接获取其中的值

注意:

  • status 的位操作需要多画图理解
  • 正常退出时,终止信号为0;异常终止时,退出码没有,两者是互斥的
  • code dump 现阶段用不到,但它是伴随着终止信号出现的

如果觉得 (status >> 8) & 0xFF(status & 0x7F)这两个位运算难记,系统还提供了两个宏来简化代码

  • WIFEXITED(status) 判断进程退出情况,当宏为时,表示进程正常退出
  • WEXITSTATUS(status) 相当于 (status >> 8) & 0xFF,直接获取退出码

3.3、等待时执行

//options 参数
WNOHANG//比如
waitpid(id, &status, WNOHANG);

父进程并非需要一直等待子进程运行结束(阻塞等待),可以通过设置 options 参数,进程解除状态,父进程变成 等待轮询 状态,不断获取子进程状态(是否退出),如果没退出,就可以在等待期间做别的事情

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> //进程等待相关函数头文件int main()
{//演示 waitpid()pid_t id = fork();  //创建子进程if(id == 0){int time = 9;int n = 0;while(n < time){printf("我是子进程,我已经运行了:%d秒 PID:%d   PPID:%d\n", n + 1, getpid(), getppid());sleep(1);n++;}exit(244);  //子进程退出}int status = 0; //状态pid_t ret = 0;while(1){ret = waitpid(id, &status, WNOHANG); //参数3 设置为非阻塞状态if(ret == -1){printf("进程等待失败!进程不存在!\n");break;}else if(ret == 0){ printf("子进程还在运行中!\n");printf("我可以干一些其他任务\n");sleep(3);}else{printf("进程等待成功,子进程已被回收\n");//通过 status 判断子进程运行情况if(WIFEXITED(status)){printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));break;}else{printf("子进程异常退出,code dump:%d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));break;}}}return 0;
}

程序正常运行,父进程通过 等待轮询 的方式,在子进程执行的同时,执行其他任务

在这里插入图片描述
当然也可以通过kill -9 PID命令使子进程异常终止
在这里插入图片描述
可以看到程序能分别捕捉到正常异常的情况

注意: 如果不写进程等待函数,会引发僵尸进程问题

结语:进程的诗意

在这片数字化的森林中,每一个进程都是一个独特的生命体,它们相互连接、相互影响,共同编织出操作系统运行的壮丽诗篇。而我们,作为这个系统的创造者与观察者,得以在代码与逻辑的缝隙间,窥见生命与技术交融的奇妙景象。

进程的创建如同生命的萌发,充满希望与可能;进程的终止如同生命的谢幕,或从容或仓促;进程的等待则如同生命间的守候与牵挂,表达着系统设计中对协作与秩序的追求。

本篇关于进程创建,终止与等待的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!

在这里插入图片描述

相关文章:

  • C++矩阵操作:正交矩阵(旋转矩阵)
  • RPA vs. 传统浏览器自动化:效率与灵活性的终极较量
  • 电商平台自动化
  • list 容器常见用法及实现
  • Java知识框架
  • 【JVS更新日志】企业文档AI助手上线、低代码、智能BI、智能APS、AI助手5.14更新说明!
  • 机器学习 Day17 朴素贝叶斯算法-----概率论知识
  • 【vue】生命周期钩子使用
  • DataX从Mysql导数据到Hive分区表案例
  • 反向传播算法——矩阵形式递推公式——ReLU传递函数
  • HVV蓝队实战面试题
  • Flink实时统计任务CPU异常排查与解决方案
  • 技术选型不当,如何避免影响项目进展
  • 1. Go 语言环境安装
  • 计算机图形学编程(使用OpenGL和C++)(第2版)学习笔记 10.增强表面细节(一)过程式凹凸贴图
  • 初探 Skynet:轻量级分布式游戏服务器框架实战
  • gitlab提交测试分支的命令和流程
  • 【ESP32-S3】Guru Meditation Error 崩溃分析实战:使用 addr2line 工具 + bat 脚本自动解析 Backtrace
  • Kali Linux 桌面环境安装与配置指南
  • Python操作MySQL 连接加入缓存层完整方案
  • 商务部新闻发言人就出口管制管控名单答记者问
  • 125%→10%、24%税率暂停90天,对美关税开始调整
  • 国内首家破产的5A景区游客爆满,洛阳龙潭大峡谷:破产并非因景观不好
  • 竞彩湃|德甲欧冠资格竞争白热化,伯恩茅斯主场迎恶战
  • 成就彼此,照亮世界:“中欧建交50周年论坛”在沪成功举行
  • 我驻苏丹使馆建议在苏中国公民尽快撤离