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

【Linux操作系统】简学深悟启示录:进程控制

文章目录

  • 1.进程创建补充
  • 2.进程终止
    • 2.1 查看进程退出
    • 2.2 exit 和 _exit
  • 3.进程等待
    • 3.1 wait 和 waitpid
    • 3.2 阻塞和非阻塞轮询
  • 4.进程替换
    • 4.1 进程替换本质
    • 4.2 进程替换函数
  • 希望读者们多多三连支持
  • 小编会继续更新
  • 你们的鼓励就是我前进的动力!

1.进程创建补充

关于进程创建及其本质都在前面的文章有详细介绍了,这里就不叙述太多,只进行细节补充

传送门:【Linux操作系统】简学深悟启示录:进程初步
【Linux操作系统】简学深悟启示录:环境变量&&进程地址

在这里插入图片描述

当子进程继承父进程的数据段的时候,无论该部分的权限之前如何,系统会将数据段的权限都设置成只读,当子进程需要修改共享数据时,此时会触发只读权限,系统不会将该修改识别为异常,而是自动修改权限并赋予子进程新的数据空间,实现写时拷贝

2.进程终止

进程退出的三种情况:

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.1 查看进程退出

[zzh_test@hcss-ecs-6aa4 PC]$ echo $?
0

比如最熟悉的 main 函数,最后总要加个 return 0; 表示进程退出状态正常,$? 查看的是最近一次执行进程的退出码

在这里插入图片描述

0 表示正常退出,除此以外用 strerror 解释退出码,可以看到多个退出原因

当进程异常时同样会返回退出码,但此时更重要的是异常的原因,即程序中断原因,应该查看信号,因为进程退出异常的本质就是收到了信号,这个后面会讲

2.2 exit 和 _exit

在这里插入图片描述

通过代码发现,exit 是用于返回进程码的函数,但是 exit 后的代码就再也没有执行了,这也就说明 exit 是进程退出,return 表示的是函数退出,二者不一样

在这里插入图片描述

我们知道对于 printf 来说 \n 的作用不仅是换行,更是起到刷新缓冲区让字符串强制输出的作用

在这里插入图片描述

去掉 \n,进行两种函数的对比,发现两种函数对于返回退出码的作用是一样的,但是一个输出了一个没输出,这是因为 _exit 是系统级别的调用,直接调用系统终止进程;exit 最终也是调用了 _exit 来终止进程的,但是还做了清理函数,刷新缓冲区的工作

3.进程等待

🤔为什么需要进程等待?什么是进程等待?

之前讲过如果子进程在父进程还在运行的时候进行了退出,父进程此时不对子进程进行处理,那么子进程会变成僵尸进程,此时连 kill -9 都无法杀死该进程,因为一个已经死掉的进程无法被杀掉,父进程派给子进程的任务完成的如何,我们需要知道,子进程运行完成,结果对还是不对,或者是否正常退出。因此父进程通过进程等待的方式,调用 wait / waitpid 回收子进程资源,获取子进程退出信息

3.1 wait 和 waitpid

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){int cnt = 5;while(cnt){printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(0);}else if(id > 0){int cnt = 10;while(cnt){printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}pid_t ret = wait(NULL);if(ret == id){printf("wait successful, ret: %d\n", ret);}sleep(5);}else{printf("fork fail\n");}return 0;
}

以上是一个使用 wait 回收子进程的例子

pid_t wait(int *status),该函数 status 用于获取子进程的退出状态,成功返回被等待进程 pid,失败返回 -1

  • 如果不关心子进程的退出状态,可传入 NULL ,就像代码中 pid_t ret = wait(NULL); 这种写法

  • 如果想获取子进程退出状态,可定义一个 int 类型变量,将其地址传入。通过一些宏来检查和解析状态信息,比如 WIFEXITED(status):判断子进程是否正常退出,若正常退出返回非零值,否则返回 0WEXITSTATUS(status):在 WIFEXITED 为真时使用,用于获取子进程通过 exitreturn 返回的退出码

if (WIFEXITED(status)) 
{ // 判断子进程是否正常退出printf("子进程正常退出\n");printf("子进程的退出码是:%d\n", WEXITSTATUS(status)); // 获取子进程的退出码
} 
else 
{printf("子进程异常退出\n");
}

🔥值得注意的是: WIFEXITED 可以记忆为 wait if exited(等待是否退出),WEXITSTATUS 可以记忆为 wait exited status(等待退出状态)

在这里插入图片描述

通过执行代码,可以发现僵尸进程确实是被回收了,再深度思考,我们如何获取子进程的退出状态?比如异常了是被什么信号打断了?正常运行但是结果有错是什么原因造成的,此时处于什么状态?那么这个时候就用到了 waitpid 函数

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if(id == 0){int cnt = 5;while(cnt){printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(1);}else if(id > 0){int cnt = 10;while(cnt){printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}int status;pid_t ret = waitpid(id, &status, 0);if(ret == id)                                                                                                                                                           {printf("wait successful, ret: %d\n, status: %d", ret, status);}else{printf("wait fail\n");}}else{printf("fork fail\n");}return 0;
}

要传入参数 &status,所以需要在外设置变量 status 获取子进程的退出状态(系统会自动捕捉子进程退出状态给到 status

在这里插入图片描述

观察 status 的值,256 不是正常的退出码值,为什么会出现这种情况?我们要知道,父进程等待子进程期望获得哪些信息?

  • 子进程代码是否异常
  • 没有结果,结果异常是为什么

所以 status 一定不是单纯的整数类型而已

在这里插入图片描述

对于 status 我们只看最低的 16 位,因为这里存储的是有效信息

在这里插入图片描述

7 位(第 1 - 7 位)为 0,表示是正常终止而非被信号杀死,第 8 位啥意思现在先不用管,高 8 位(第 8 - 16 位)存储 “退出状态”(即子进程通过 exitreturn 指定的退出码,比如 exit(1) 里的 1)。0000 00001 0000 0000 化为二进制是 0X7F,即 256,刚好就是打印出来的 status

✏️函数解析:

pid_ t waitpid(pid_t pid, int *status, int options)

返回值:

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

参数:

  • pid:
    pid=-1,等待任一个子进程,与 wait等效
    pid>0,等待其进程 IDpid 相等的子进程
  • status:
    WIFEXITED(status):若为正常终止子进程返回的状态,则为真(查看进程是否是正常退出)
    WEXITSTATUS(status):若 WIFEXITED 非零,提取子进程退出码(查看进程的退出码)
  • options:
    WNOHANG:若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0,不予以等待。若正常结束,则返回该子进程的 ID

在这里插入图片描述

3.2 阻塞和非阻塞轮询

如果子进程一直不结束,那么父进程 wait 岂不是要一直进行等待?确实是这样的,这种情况就是阻塞,那我们要如何优化呢?

waitpid 中将 option 参数设置成 WNOHANG 的方式就能避免阻塞的情况,他使用的是非阻塞轮询的方式

在这里插入图片描述
阻塞就是父进程一直等待子进程结束,因此会很耽误父进程自己的效率,非阻塞轮询不同的地方在于他是间歇性的询问子进程是否结束,如果没结束,父进程会继续干自己的事,比如打印日志等,就这样不断重复询问,直到子进程结束父进程就能回收了

4.进程替换

前面遇到的情况都是父子进程共用同一套代码,但是如果子进程想要实行另一套代码呢?那就需要用到进程替换了

4.1 进程替换本质

在这里插入图片描述
子进程进行进程替换时,数据代码父子共享,就有人会问了:那是不是就得创建新的进程来放新代码?还是进行写时拷贝?都不是!我们这里是整体替换,而不是部分修改,所以不涉及写时拷贝,更不涉及新进程创建

真正的方式: 清空子进程之前从父进程继承的内存映射(包括代码段、数据段等),从硬盘读取新程序,然后为子进程建立新的虚拟内存映射,并将文件内容加载到对应物理内存页中,该替换是由 exec 系列函数实现的

🔥值得注意的是:

  • 进程替换成功之后,exec* 后续的代码不会被执行,替换失败才可能执行后续代码,只有失败有返回值(比如 exit(1)),能看到 exit(1) 对应的返回值就表示进程替换失败了
  • 替换程序的时候,会将新程序的表头先加载进去,当真正要使用的时候才全部加载,这也是懒加载的一种表现
  • 环境变量默认不会被替换

4.2 进程替换函数

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t id = fork();if (id == 0){printf("替换前\n");execl("/usr/bin/ls", "-ls", "-a", "-l", NULL);printf("替换后\n");int cnt = 3;while (cnt){printf("I am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}exit(1);}else if (id > 0){int cnt = 5;while (cnt){printf("I am father, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);cnt--;sleep(1);}int status;pid_t ret = waitpid(id, &status, 0);if (ret == id){printf("wait successful, ret: %d, WIFEXITED: %d, WEXITSTATUS: %d\n", ret, WIFEXITED(status), WEXITSTATUS(status));}sleep(5);}else{printf("fork fail\n");}return 0;
}

在子进程进行主体函数运行时,添加一个 ececl 进程替换函数

在这里插入图片描述

根据结果,可以发现子进程程序替换之后的代码都没有执行

在这里插入图片描述

exec 开头的函数,这一系列的都是程序替换的函数,这些函数都封装了 execve 函数(系统调用函数)来间接调用

l(list):表示参数采用列表
v(vector):参数用数组
p(path) :有 p 自动搜索环境变量 PATH
e(env):表示自己维护环境变量

使用示例:

char *argv[] = {"ls", "-a", "-l", NULL};
char *envp[] = {"VAR1=value1", "VAR2=value2", NULL};execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
execlp("ls", "ls", "-a", "-l", NULL);
execle("/usr/bin/ls", "ls", "-a", "-l", NULL, envp);execv("/usr/bin/ls", argv);
execvp("ls", argv);
execve("/usr/bin/ls", argv, envp)

execlexecv 是一组的,第一个参数都是替换程序的绝对或相对路径,后面填的就是可变参数,指令怎么用这里就怎么填,记得要用 NULL 结尾,至于为什么,和命令行参数的道理是一样的,这两不同的就在于一个是直接传入,另一个是把参数放在数组里然后传入

传送门:命令行参数

execlpexecvp 是一组的,唯一的区别就是不用写路径,而是直接写要替换程序的文件名,前提是该文件名要在 PATH 环境变量下。execleexecve 是一组的,唯一的区别就是可以传入自己的环境变量,环境变量采用的是覆盖写入而不是追加

🔥值得注意的是:

在这里插入图片描述

引入 exec 系列函数时,通常会包括 extern char **environ,有人就问了,不是从父进程继承了吗?为什么还要写这条代码,确实你从父进程继承了该环境变量数据,但是你不知道在哪啊,需要外部声明来找到位置。补充一个知识点,除了可以 expot 写入 $PATH,还可以用 putenv()


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

http://www.dtcms.com/a/364386.html

相关文章:

  • unity中的交互控制脚本
  • 如何选择适合企业的海外智能客服系统:6 大核心维度 + 实战选型指南
  • 【STL源码剖析】从源码看 deque :拆解双端队列的底层实现与核心逻辑
  • 用友T3、T6/U8批量作废凭证
  • 从数据生成到不确定性估计:用 LSTM + 贝叶斯优化实现时间序列多步预测
  • 基于SpringBoot的旅游管理系统
  • 【大前端】React 使用 Redux 实现组件通信的 Demo 示例
  • React实现点击按钮复制操作【navigator.clipboard与document.execCommand】
  • 基于单片机PWM信号发生器系统Proteus仿真(含全部资料)
  • 平衡车 - 电机调速
  • 基于单片机车内换气温度检测空气质量检测系统Proteus仿真(含全部资料)
  • 单片机点灯
  • Linux 网络编程中核心函数`recv`。
  • zynq 开发系列 新手入门:GPIO 连接 MIO 控制 LED 闪烁(SDK 端代码编写详解)
  • Spring Boot 实现数据库表变更监听的 Redis 消息队列方案
  • 单片机控制两只直流电机正反转C语言
  • 变频器实习DAY42 VF与IF电机启动方式
  • Excel 电影名匹配图片路径教程:自动查找并写入系统全路径
  • wpf 自定义控件,只能输入小数点,并且能控制小数点位数
  • 机器学习从入门到精通 - Python环境搭建与Jupyter魔法:机器学习起航必备
  • 如何在modelscope上上传自己的MCP服务
  • 【收藏】2025 前端开发者必备 SVG 资源大全
  • 【2025ICCV-持续学习方向】一种用于提示持续学习(Prompt-based Continual Learning, PCL)的新方法
  • 【CouponHub开发记录】SpringAop和分布式锁进行自定义注解实现防止重复提交
  • RAG|| LangChain || LlamaIndex || RAGflow
  • kafka概念之间关系梳理
  • mac idea 配置了Gitlab的远程地址,但是每次pull 或者push 都要输入密码,怎么办
  • 项目中常用的git命令
  • python基础案例-数据可视化
  • Streamlit 数据看板模板:非前端选手快速搭建 Python 数据可视化交互看板的实用工具