当前位置: 首页 > 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命令使子进程异常终止
在这里插入图片描述
可以看到程序能分别捕捉到正常异常的情况

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

结语:进程的诗意

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

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

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

在这里插入图片描述

http://www.xdnf.cn/news/444889.html

相关文章:

  • Jmeter元件 CSV Data Set Config详解
  • (1-4)Java Object类、Final、注解、设计模式、抽象类、接口、内部类
  • Doris与ClickHouse深度比较
  • 语音合成之十四 文本转语音(TTS)开源数据集
  • 互联网大厂Java求职面试:优惠券服务架构设计与AI增强实践-6
  • 使用IDEA创建Maven版本的web项目以及lombok的使用
  • 玛哈特矫平机:金属板材加工中的“平整大师”
  • 解读RTOS 第七篇 · 驱动框架与中间件集成
  • Milvus 全面解析
  • 非异步信号安全函数
  • The 2022 ICPC Asia Xian Regional Contest(E,L)题解
  • 5 WPF中的application对象介绍
  • DHCP协议
  • 每日算法-250514
  • Untiy基础学习(十四)核心系统—物理系统之碰撞检测代码篇 刚体,碰撞体,材质
  • 网络运维过程中的常用命令
  • idea中编写spark程序
  • 通过迁移学习改进深度学习模型
  • Python Day25 学习
  • MCU裸机程序如何移植到RTOS?
  • MySQL 入门大全:数据类型
  • 【漫话机器学习系列】258.拐点(Inflection Point)
  • C++中如何实现一个单例模式?
  • Spring Cloud:构建云原生微服务架构的最佳工具和实践
  • 机密虚拟机的威胁模型
  • 仓配一体化系统如何选择,ERP、OMS、WMS 功能解析与搭配策略
  • 生成对抗网络(Generative Adversarial Networks ,GAN)
  • 仿生眼机器人(人脸跟踪版)系列之一
  • 2025tg最新免费社工库机器人
  • Kotlin Multiplatform与Flutter、Compose共存:构建高效跨平台应用的完整指南