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

【Linux】进程控制

🌟🌟作者主页:ephemerals__

🌟🌟所属专栏:Linux

目录

前言

一、什么是进程控制

二、进程创建

三、进程终止(进程退出)

退出码

main函数返回

_exit()

exit()

测试

四、进程等待 

wait

waitpid

注意事项

wait和waitpid的输出型参数

五、进程程序替换

进程替换相关接口

总结


前言

        在现代计算中,操作系统负责管理硬件资源并为应用程序提供一个稳定的运行环境。Linux作为一种广泛使用的类Unix操作系统,它的进程管理功能至关重要。Linux提供了强大的进程控制机制和接口,帮助我们管理进程的生命周期、资源分配和调度。在这篇博客中,我们将深入探讨Linux进程控制的基础知识以及从四个方面(进程创建、进程终止、进程等待、进程程序替换)介绍常用的进程控制相关API接口,帮助大家更好地理解和掌握Linux系统中的进程控制技术。

一、什么是进程控制

        进程控制是操作系统中管理和调度进程的关键功能,涉及到进程的创建、终止、等待、程序替换以及进程间通信等多个方面,简而言之,进程控制确保操作系统能够有效地管理并协调不同程序的执行。对于我们而言,要对进程进行相关操作,就要深入理解操作系统对进程的控制机制,并熟悉Linux下进程控制的相关接口

二、进程创建

        在Linux下,我们通常使用fork函数完成进程的创建。创建后的新进程称之为子进程,而当前进程叫做父进程。博主已经在以下两篇文章中,对进程创建的底层原理、fork的使用方法、注意事项以及写时拷贝的原理进行了详细的讲解,大家参阅这两篇文章即可,这里就不再复述。

 【Linux】进程概念和进程状态-CSDN博客

【Linux】深入理解程序地址空间-CSDN博客 

三、进程终止(进程退出)

        一个进程在做完相应工作后,需要执行“终止”操作,其本质是释放进程的task_struct及其对应的代码和数据。 进程终止的常见情况有四种:

1. main函数返回

2. 调用_exit()退出

3. 调用exit()退出

4. 给进程发送信号(如kill()、ctrl + c)

前三种情况都用于当前正在执行的进程退出,属于进程的正常退出,而最后一种专门用于杀死其他进程,属于异常退出。本文博主会详细介绍前三种退出方法的使用及其原理,至于最后一种,博主后续会结合信号一起讲解。

退出码

         进程退出时,都会返回一个“退出码”,它本质是一个整数,用于表示程序的执行情况。例如,我们在VSCode终端输入指令时,可以看出它是否执行成功,本质就是通过识别对应程序的退出码来完成的:

不同的退出码,所表示的退出状态也不尽相同。只有退出码为0时,表示成功执行,且执行的结果正确,其他退出码均表示执行失败或结果不符合预期,原因各异

以下是常见的退出码所表示的执行情况:

注意:要将“程序成功执行”和“程序执行的结果正确”区分开,退出码非0并不代表程序一定是异常退出的,也有可能成功执行,但结果不符合预期。而异常退出是说程序执行时遇到异常,直接终止了,没有符不符合预期的概念了。

如下指令可以打印最近执行结束的程序的退出码,便于我们判断退出状况:

echo $?

main函数返回

        main函数return返回是最常见的进程退出方法,再程序执行完毕后,使用return返回即可。main函数的返回值即是程序的退出码

_exit()

        _exit() 是一个系统调用,当程序执行到调用处时,会直接退出。它的函数原型如下:

#include <unistd.h>void _exit(int status);

参数status表示程序的退出码,当出现错误,我们可以使用该接口配合退出码退出程序。

_exit是一种低级的退出操作,退出时不会进行任何其他处理,只会返回退出码,所以一般是用于多进程编程时的子进程错误退出。

注:传入的退出码虽然类型为int,但退出码的取值范围是0~255,所以只会取int的最低8位。

exit()

        exit() 是c标准库的函数,包含在<stdlib.h>头文件中,相比_exit更加常用。其函数原型:

#include <stdlib.h>void exit(int status);

参数status也表示程序退出码。

实际上,exit的底层调用了_exit,不过在退出之前做了一系列清理工作,例如刷新 I/O 缓冲区,确保所有的输出数据都被写入到相应的文件或终端、关闭文件描述符、间接释放动态分配的内存等。

测试

        接下来我们做一段测试,写一个简单的程序,调用exit退出,并将50作为退出码,然后在命令行打印退出码:

#include <stdio.h>
#include <stdlib.h>int main()
{printf("hello world\n");exit(50);
}

执行程序并打印退出码:

可以看到,程序打印后退出,并且返回了退出码50。

四、进程等待 

        在之前的文章中提到,一个子进程在退出后,需要父进程回收资源,如果父进程一直不回收,那么子进程的PCB就会一直存在,导致出现僵尸进程。另外进程退出时返回的退出码需要被父进程获取,才能判断子进程的执行情况。

因此,“进程等待”的作用是:

1. 回收子进程,释放子进程的资源(必须完成)

2. 获取子进程的退出信息(可选)

在Linux下,我们通常使用waitwaitpid进行进程等待。

wait

        wait是一个函数,在父进程中使用,作用是等待子进程退出后,回收子进程资源,并获取子进程的退出码与其他退出状态。它的函数原型如下:

#include <sys/wait.h>pid_t wait(int *status);

参数status:是一个输出型参数,表示子进程的退出信息。具体细节在waitpid之后统一介绍。

返回值:如果回收成功,返回子进程的PID。如果失败,返回 -1。

如果父进程有多个子进程,那么wait就会等待任意一个子进程。

注意:调用wait等待子进程时,如果此时子进程还在执行自己的程序,并未退出,那么父进程就会一直阻塞在wait的调用处,直到子进程退出,再执行后续代码。

waitpid

        相比wait,函数waitpid的功能更加丰富,因此也更加常用。 它的函数原型如下:

#include <sys/wait.h>pid_t waitpid(pid_t pid, int *stat_loc, int options);

参数pid:如果传入-1,会等待任意子进程(有多个子进程的情况下);如果传入一个大于0的数,表示专门等待子进程PID为pid的子进程。

参数stat_loc:输出型参数,表示子进程的退出信息。具体细节稍后介绍。

参数options:通常传入0,表示阻塞等待(即子进程退出之前阻塞)。如果传入宏WNOHANG,则表示:若子进程还未退出,不等待,直接返回0(可以配合循环进行多次的非阻塞轮询,非阻塞轮询期间的空闲时间可以被利用,让父进程完成自己的任务

返回值:如果回收成功,返回子进程的PID。如果失败(例如没有对应的pid),返回 -1。如果第三个参数传WNOHANG并且子进程还未退出,返回0。

注意事项

1. 调用wait/waitpid时,如果子进程已经退出而没有被回收,那么函数会立即返回,释放子进程资源,获取退出信息。

2. 调用wait/waitpid时,如果子进程已经被回收,那么就会等待失败,立即返回 -1。

3. 如果父进程不调用wait或waitpid,子进程在退出后会变成僵尸进程。

wait和waitpid的输出型参数

        wait和waitpid都有一个输出型参数,都用于表示子进程的退出信息。这个信息并不只是单纯的退出码,其中还包含了收到信号导致退出时的信号编号退出方式等信息。当然,这些东西具体是什么,我们无需关心,后续在讲解信号时博主会进行分析。现在我们终点关注的是:输出型参数只是一个整数,却包含了多种信息,那势必要对其进行划分,我们了解了划分方式,才能从其中提取出退出信息

        它的划分方式如下:

1. 如果子进程正常退出,那么输出型参数的0~7位全为0,8~15位是退出码;

2. 如果子进程异常退出,那么输出型参数0~6位表示信号编号,第7位表示异常退出方式,8~15位的数据没有使用价值。

获取到输出型参数后,我们可以进行相应的位运算处理,得到退出码或者其他信息。当然,有几个宏可以帮助我们直接求出退出码等信息(传入输出型参数的值即可):

WIFEXITED() 用于判断子进程是否是正常退出。

WEXITSTATUS() 可以求出退出码。

WTERMSIG() 可以求出异常退出时的信号编号。

当然,如果我们不关心子进程的退出状况,可以在输出型参数位置传NULL。

父进程回收子进程,并获取子进程退出信息的过程:子进程退出时,其退出信息会存入其PCB中,当被父进程回收时,在子进程PCB中读取,然后释放子进程的PCB。

五、进程程序替换

         进程程序替换指的是一个进程在运行过程中,用新的程序代码替换当前执行的代码,然后执行新程序的代码。如果我们创建了一个进程,想要这个进程执行一个全新的程序,就可以使用进程程序替换。

        发生进程替换后,PCB的内容不变,也没有创建新的进程,而是当前进程对应物理内存中的代码、数据、堆区栈区全部被目标代码和数据覆盖

进程替换相关接口

        Linux下,实现进程替换最常用的是exec系列函数,一共有六个:

#include <unistd.h>int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

这六个函数都是用于替换当前程序的代码和数据,只是传参方式有所不同,功能略有差别。

首先介绍一下它们的返回值:

如果替换成功,直接执行新代码,后续的原代码已经被替换,不再执行,因此没有返回值。

如果替换失败,返回 -1。

所以对于exec系列函数,无需检查其返回值,因为只要返回就失败。

再来谈谈它们的参数:

1. 上述函数中,叫做path的参数,一律传入的是一个字符串,表示目标程序的路径+程序名

2. 叫做file的参数,一律传入目标程序的程序名(路径会自动到环境变量PATH中查找)。

3. 叫做arg的参数,是一个可变参数,一个一个地传入目标程序的命令行参数字符串(注意程序名算第一个命令行参数;最后一个命令行参数须是NULL)。当然,如果不需要使用其他的命令行参数,只需传一个程序名和一个NULL即可。

4. 叫做argv[]的参数,将指向所有命令行参数字符串的指针数组的数组名传入(注意程序名算第一个命令行参数;指针数组的最后一个元素须是NULL),也就是传命令行参数的另一种方式。

5. 带有参数envp[]的函数,表明替换的进程要使用该函数传入的全新的环境变量。因此,该参数处要传入指向所有环境变量字符串的指针数组的数组名(注意环境变量格式是"环境变量=值";指针数组的最后一个元素须是NULL)。

以上七个函数的名字十分相似,容易混淆,但其实它们也有规律,博主给大家列出来方便记忆:

1. 函数名带"l"的,表示命令行参数要一个个地传入;而带"v"的表示命令行参数要用指针数组的方式传入。

2. 函数名带"p"的表示目标程序不用带路径,会自动在PATH中查找。

3. 函数名带"e"的表示可以给目标程序设置新的环境变量。 

当然,除了这六个函数,还有一个系统调用execve,使用方法和execvpe完全相同。原型如下:

#include <unistd.h>int execve(const char *filename, char *const argv[], char *const envp[]);

写一个简单的调用示例,看看这些函数的调用区别:

#include <unistd.h>int main()
{//假如要将程序替换为/usr/bin路径下的ls指令,并加上选项-l//并且对于支持修改环境变量的函数,修改环境变量PATHchar* const argv[] = {"ls", "-l", NULL};char* const envp[] = {"PATH=/usr/bin", NULL};execl("/usr/bin/ls", "ls", "-l", NULL); // 带l要一个个传入命令行参数execlp("ls", "ls", "-l", "NULL"); // 带p不用传路径execle("usr/bin/ls", "ls", "-l", NULL, envp); // 带e要传环境变量表execv("/usr/bin/ls", argv); // 带v要用指针数组传入命令行参数execvp("ls", argv); // 带p不用传路径;带v用指针数组传命令行参数execvpe("ls", argv, envp); // 带p不用传路径;带v用指针数组传命令行参数;带e要传环境变量表execve("ls", argv, envp);return 0;
}

实际上,exec系列接口中,六个标准库函数底层都封装了系统调用execve,只是产生了多种形式使得传参和功能更加多样化。

总结

        本篇文章,我们学习了进程控制的概念及进程创建、进程终止、进程等待和进程程序替换的相关Linux常用接口,学习了这些接口,想必大家能够更好地理解和掌握Linux系统中的进程控制技术。当然,进程控制还包括进程间通信,但这部分的知识涉及文件IO相关的内容,后续博主会给大家详细讲解。接下来博主会和大家一起,结合这些接口的使用方法及进程控制思想,写一个简单的Shell命令行程序。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

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

相关文章:

  • linux inotify 资源详解
  • 数据结构之二叉树(4)
  • 一款基于 .NET 开源的多功能的 B 站视频下载工具
  • vulkanscenegraph显示倾斜模型(6.5)-vsg::DatabasePager
  • 网络安全自动化:精准把握自动化边界,筑牢企业安全防
  • 拷贝多个Excel单元格区域为图片并粘贴到Word
  • 谷歌最新推出的Gemini 2.5 Flash人工智能模型因其安全性能相较前代产品出现下滑
  • nginx面试题
  • 物联网之对接MQTT最佳实践
  • CPT204 Advanced Obejct-Oriented Programming 高级面向对象编程 Pt.10 二叉搜索树
  • 【将你的IDAPython插件迁移到IDA 9.x:核心API变更与升级指南】
  • WSL 安装 Debian 后,apt get 如何更改到国内镜像网址?
  • C++笔记之委托
  • 利用迁移学习实现食物分类:基于PyTorch与ResNet18的实战案例
  • 【蓝牙协议栈】【BR/EDR】【AVCTP】精讲音视频控制传输协议
  • 分享一个Android中文汉字手写输入法并带有形近字联想功能
  • Baklib驱动企业知识管理AI升级
  • day15 python 复习日
  • 复杂网络系列:第 5 部分 — 社区检测和子图
  • 在写setup时遇到的问题与思考
  • Circular Plot系列(一): 环形热图绘制
  • 《马小帅的Java闯关记》
  • 模型部署与提供服务
  • QpushButton 扩展InteractiveButtonBase
  • k230摄像头初始化配置函数解析
  • nproc命令查看可用核心数量详解
  • [Windows] 智绘教 v20250403a 屏幕批注工具
  • day 12 三种启发式算法:遗传算法、粒子群算法、退火算法
  • 用卷积神经网络 (CNN) 实现 MNIST 手写数字识别
  • Python函数完全指南:从零基础到灵活运用