热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【云原生】Linux进程控制(创建、终止、等待)

✨个人主页: Yohifo 🎉所属专栏: Linux学习之旅 🎊每篇一句: 图片来源 🎃操作环境&#xff1

✨个人主页: Yohifo
🎉所属专栏: Linux学习之旅
🎊每篇一句: 图片来源
🎃操作环境: CentOS 7.6 阿里云远程服务器

  • Good judgment comes from experience, and a lot of that comes from bad judgment.
    • 好的判断力来自经验,其中很多来自糟糕的判断力。

上帝的指纹




文章目录

  • 🌇前言
  • 🏙️正文
    • 1、进程创建
      • 1.1、fork函数
      • 1.2、写时拷贝
    • 2、进程终止
      • 2.1、退出码
      • 2.2、退出方式
    • 3、进程等待
      • 3.1、等待原因
      • 3.2、等待函数
      • 3.3、等待时执行
  • 🌆总结




🌇前言

进程 创建后,需要对其进行合理管理,光靠 OS 是无法满足我们的需求的,此时可以运用 进程 控制相关知识,对 进程 进行手动管理,如创建 进程、终止 进制、等待 进程 等,其中等待 进程 可以有效解决僵尸 进程 问题

汽车中控台

汽车的中控台,可以对汽车进行各种操作




🏙️正文

本文涉及的代码都是以 C语言 实现的

1、进程创建

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

1.1、fork函数

#include //所需头文件
pid_t fork(void); //fork 函数

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

  • 进程具有独立性,即使是父子进程,也是两个完全不同的进程,拥有各自的 PCB
  • 假设 子进程 发生改写行为,会触发写时拷贝机制

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

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

进程创建图解
通过代码理解 进程 创建

#include
#include
#include
#include //进程等待相关函数头文件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 可能创建进程失败

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

1.2、写时拷贝

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

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

#include
#include
#include
#include //进程等待相关函数头文件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 机制寻址不同的空间

写时拷贝机制本质上是一种按需申请资源的策略

图解
注意:

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



2、进程终止

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

终止进程

2.1、退出码

echo $?

main 函数中的最后一条语句 return 0 表示当前程序的退出码,0 表示程序正常退出,可以通过指令 echo $? 查看最近一次子进程运行的 退出码

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

子进程运行情况:

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

退出码
进程退出后,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() 时,输出语句

结果1
使用 _exit() 时,并没有任何语句输出
结果2
原因:

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

图解



3、进程等待

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

僵尸进程

3.1、等待原因

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

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

注意:

  • 进程的退出状态是必要的
  • 进程的执行结果是非必要的

也就是说,父进程必须对子进程负责,确保子进程不会连累 OS,而子进程执行的结果是否正确,需要我们自行判断

3.2、等待函数

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

#include
#include 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 位表示 code dump,低 7 位表示终止信号
  • options 为选项,比如可以选择父进程是否需要阻塞等待子进程退出

需要特别注意 status
status
通过代码演示 waitpid() 的使用

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

不发出终止信号&#xff0c;让程序自然跑完

结果
发出终止信号&#xff0c;强行终止进程

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

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

注意&#xff1a;

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

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

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

3.3、等待时执行

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

父进程并非需要一直等待子进程运行结束(阻塞等待)&#xff0c;可以通过设置 options 参数&#xff0c;进程解除 状态&#xff0c;父进程变成 等待轮询 状态&#xff0c;不断获取子进程状态(是否退出)&#xff0c;如果没退出&#xff0c;就可以干点其他事

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

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

正常运行
当然也可以通过 kill -9 PID 命令使子进程异常终止

异常终止
可以看到程序能分别捕捉到正常和异常的情况

注意&#xff1a; 如果不写进程等待函数&#xff0c;会引发僵尸进程问题



&#x1f306;总结

以上就是关于 Linux进程控制(创建、终止、等待) 的相关知识了&#xff0c;我们学习了 子进程 是如何被创建的&#xff0c;创建后又是如何终止的&#xff0c;以及 子进程 终止 父进程 需要做些什么&#xff0c;有了这些知识后&#xff0c;在对 进程 进行操作时能更加灵活和全面

如果你觉得本文写的还不错的话&#xff0c;期待留下一个小小的赞&#x1f44d;&#xff0c;你的支持是我分享的最大动力&#xff01;

如果本文有不足或错误的地方&#xff0c;随时欢迎指出&#xff0c;我会在第一时间改正


星辰大海

相关文章推荐

Linux进程学习【进程地址】

Linux进程学习【环境变量】

Linux进程学习【进程状态】

Linux进程学习【基本认知】

&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;&#61;

Linux工具学习之【gdb】

Linux工具学习之【git】

Linux工具学习之【gcc/g&#43;&#43;】

Linux工具学习之【vim】

感谢支持


推荐阅读
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了基于c语言的mcs51单片机定时器计数器的应用教程,包括定时器的设置和计数方法,以及中断函数的使用。同时介绍了定时器应用的举例,包括定时器中断函数的编写和频率值的计算方法。主函数中设置了T0模式和T1计数的初值,并开启了T0和T1的中断,最后启动了CPU中断。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 本文介绍了C函数ispunct()的用法及示例代码。ispunct()函数用于检查传递的字符是否是标点符号,如果是标点符号则返回非零值,否则返回零。示例代码演示了如何使用ispunct()函数来判断字符是否为标点符号。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • C++中的三角函数计算及其应用
    本文介绍了C++中的三角函数的计算方法和应用,包括计算余弦、正弦、正切值以及反三角函数求对应的弧度制角度的示例代码。代码中使用了C++的数学库和命名空间,通过赋值和输出语句实现了三角函数的计算和结果显示。通过学习本文,读者可以了解到C++中三角函数的基本用法和应用场景。 ... [详细]
  • 3.223.28周学习总结中的贪心作业收获及困惑
    本文是对3.223.28周学习总结中的贪心作业进行总结,作者在解题过程中参考了他人的代码,但前提是要先理解题目并有解题思路。作者分享了自己在贪心作业中的收获,同时提到了一道让他困惑的题目,即input details部分引发的疑惑。 ... [详细]
author-avatar
手机用户2502857517_939
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有