【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并不代表程序一定是异常退出的,也有可能成功执行,但结果不符合预期。而异常退出是说程序执行时遇到异常,直接终止了,没有符不符合预期的概念了。

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

bash 复制代码
echo $?

main函数返回

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

_exit()

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

cpp 复制代码
#include <unistd.h>

void _exit(int status);

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

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

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

exit()

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

cpp 复制代码
#include <stdlib.h>

void exit(int status);

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

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

测试

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

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello world\n");
    exit(50);
}

执行程序并打印退出码:

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

四、进程等待

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

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

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

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

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

wait

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

cpp 复制代码
#include <sys/wait.h>

pid_t wait(int *status);

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

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

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

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

waitpid

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

cpp 复制代码
#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系列函数,一共有六个:

cpp 复制代码
#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完全相同。原型如下:

cpp 复制代码
#include <unistd.h>

int execve(const char *filename, char *const argv[], char *const envp[]);

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

cpp 复制代码
#include <unistd.h>

int main()
{
    //假如要将程序替换为/usr/bin路径下的ls指令,并加上选项-l
    //并且对于支持修改环境变量的函数,修改环境变量PATH
    
    char* 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命令行程序。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

相关推荐
吴声子夜歌19 分钟前
Linux运维——Vim技巧三
linux·运维·vim
大块奶酪----1 小时前
某信服EDR3.5.30.ISO安装测试(一)
linux·运维·服务器
猴子请来的逗比4891 小时前
文本三剑客试题
linux·运维·服务器
muxue1781 小时前
yum源配置文件CentOS-Base.repo完整内容
linux·运维·centos
Aaaa小嫒同学2 小时前
在spark中配置历史服务器
服务器·javascript·spark
饭九钦vlog2 小时前
手机的数据楚门世界是如何推送的
服务器·经验分享
sunshineine2 小时前
Linux系统安装PaddleDetection
linux·运维·服务器·人工智能·算法
m0_549314862 小时前
CRS 16 slot 设备硬件架构
运维·网络·硬件架构·cisco·运营商·ios-xr·crs
belldeep3 小时前
WSL 安装 Debian 后,apt get 如何更改到国内镜像网址?
linux·debian·wsl
烦躁的大鼻嘎3 小时前
【Linux】深入理解Linux基础IO:从文件描述符到缓冲区设计
linux·运维·服务器·c++·ubuntu