Linux进程控制

发布时间:2023年12月18日

一.进程创建(fork函数)

关于fork函数,我们之前已经介绍过了,这里就不再赘述了
(因为我们本文的重点是进程等待和进程程序替换)
详细的大家可以去看Linux进程理解(冯诺依曼体系结构,操作系统,进程概念和基本操作)这篇博客,里面介绍了fork函数
这里再补充几点fork函数的知识点:
在这里插入图片描述

二.进程终止

我们知道,当一个进程退出时,我们肯定是要拿到它的退出信息的
那么我们如何拿到这个进程的退出信息呢?

回想一下:
我们写main函数的时候都会在最后加上return 0;
而对于一个可执行程序而言,执行完main函数之后不就代表这个进程运行完毕了吗?

因此:
我们可以通过main函数的返回值来拿到进程的退出信息

其实main函数的返回值就被叫做进程的退出码

不过一个进程的退出信息不单单只有退出码,还有一个进程退出时的信号编号
下面我们一一来介绍
先来介绍退出码

1.退出码的概念

对于main函数而言
我们平常都会写return 0;
这个0就是退出码

0代表进程正常退出
非0代表进程不正常退出(又称为错误码)
每一个错误码都对应于一种错误信息
下面我们来查看一下错误码

2.查看错误码

bash会记录最近一次进程执行结束时的进程码
echo $?  查看最近一次的进程的退出码
当然退出码也包含错误码

因为我上一个进程正常退出,退出码为0,因此显示0
在这里插入图片描述
下面我们写一个hello world,返回值为2
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此时运行mycmd,然后发现此时退出码的确为2
那么每一个错误码对应的错误信息是什么呢?

3.查看错误码对应的错误信息

1.strerror

strerror是C语言的库函数
在这里插入图片描述
然后我们把code.c改一下:
在这里插入图片描述
打印一下0~200所对应的错误码来看一下
在这里插入图片描述
这是main函数的错误码
那么普通函数有没有错误码呢?
对于C语言的库函数来说,是有错误码的
但是我们自定义的函数是没有错误码的
不过我们可以自定义错误码来模拟一下

2.函数退出时的错误码

我们要清楚一点:
main函数退出时的返回值:代表该进程退出时的退出码
而其他函数退出时的返回值,仅仅表示该函数调用完毕,返回我们想要得到的值而已

那么我怎么知道某个函数退出时的错误码呢?
比方说fopen这个函数
在这里插入图片描述
我们修改一下code.c
此时我们当前目录下没有test.txt这个文件
下面我用r方式打开这个文件
那么就会打开失败,返回NULL,并且设置errno
在这里插入图片描述
在这里插入图片描述
此时提示我们:
错误码:2 没有这个文件

注意:errno只有C语言的库函数才能用
自定义函数是用不了的,不过我们可以自定义错误码

2.自定义错误码

我们也可以自己设置一些错误码
就像这样:
在这里插入图片描述
假设work函数发生IO错误,返回了1这个错误码
然后我们在调用work函数的这个main函数中接收这个信息
然后打印它的退出信息
在这里插入图片描述
正和我们的意思

4.进程异常

我们介绍完退出码之后,
下面我们来介绍进程退出时的信号编号

我们在平常C语言的学习过程中
都知道如果发生对空指针进行解引用操作时,程序运行时会直接崩溃
其实这个运行时崩溃就是进程异常的一种表现

进程异常:就是进程在运行过程中收到了异常信号

我们可以
使用kill -l来查看进程信号
使用 kill -进程信号编号 进程PID
来给某个特定进程发送进程信号
其实我们之前学的kill -9 进程PID 用来杀死进程就是给进程这个信号让进程退出

在这里插入图片描述
下面我们来演示一下
在这里插入图片描述
mycmd这个进程一直死循环执行
然后我分别

kill -8 PID
kill -11 PID
来让这个进程出现除0异常和段错误

在这里插入图片描述

5.exit终止进程

我们在C语言学习的时候,可能见过这个函数:exit
它是用来终止一个进程的
在这里插入图片描述

其实我们完全可以这样理解:
执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数

换言之也可以这么理解:exit(n)就是直接执行main函数的return n;

因此,我们来看这个代码
在这里插入图片描述
在这里插入图片描述
此时这个进程运行到work之后执行到exit(0)时就会终止掉
并不会执行下面的printf

6.总结

在这里插入图片描述

三.进程等待

1.为什么要有进程等待

在这里插入图片描述
那么父进程该如何等待子进程呢?
我们介绍两个函数
1.wait
2.waitpid

2.wait

首先,我们介绍一下wait函数
在这里插入图片描述
下面我们来演示一下使用wait函数回收子进程的过程
在这里插入图片描述
这个代码的意思是:
0~5s内父进程和子进程都在运行
5s时子进程退出,变为僵尸进程
6~10s内子进程一直处于僵尸状态
等到10s时父进程回收子进程后,子进程完全退出,然后父进程又休眠3s后退出
在这里插入图片描述

3.waitpid

1.函数介绍

在这里插入图片描述
最后一个参数options是0的话代表是以阻塞等待的方式等待子进程退出以便回收子进程的退出状态
关于阻塞等待和非阻塞等待我们下面会单独介绍的
下面我们来验证一下

2.演示

在这里插入图片描述
这个代码的意思是:
前5s子进程运行,父进程等待子进程退出,
子进程退出后,父进程立即回收子进程,然后退出

rid>0:等待成功

status:获取退出状态(反馈子进程退出情况的)
第三个参数设置为0:默认采取阻塞等待
在这里插入图片描述

3.利用位运算分别取出退出码和退出信号编号

那么我们能不能分别取出退出码和退出信号编号来呢?
当然是可以的
利用位运算的方法即可:
在这里插入图片描述
总结:

exit_signal(退出信号) == status & 0x7f
exit_code(退出码) == status & 0xff

下面我们来验证一下
在这里插入图片描述
如果正确的话:

exit_signal==0
exit_code==1

在这里插入图片描述
这样好是好,也很巧妙
可是总感觉有些麻烦,还得用位运算才能看到,有没有更直接的方式呢?
当然有啦,就是下面要介绍的两个宏

4.两个宏

WIFEXITED(status): 如果exit_signal==0,那么它的值就是true,否则就是false
WEXITSTATUS(status): 它的值就是exit_code

大家可以这么来记:
1.WIFEXITED:
W:wait等待
IF:if 判断
EXITED:exited:退出信号
判断退出信号是否为0

2.WEXITSTATUS
W:wait等待
EXIT:exit:退出
STATUS:status:状态
它的值就是退出码

下面我们用一下这两个宏
在这里插入图片描述
在这里插入图片描述
其实他这么设计也是很有道理的
当一个进程运行后,我们最关心的是:它的exit_signal是否为0
因为如果退出信号不是0,就代表进程异常退出,此时退出码是什么对我们来说没有任何意义
因此WIFEXITED这个宏就是用来判断退出信号是否为0的
而WEXITSTATUS这个宏,它的值就是退出码,因为退出码对我们而言还是很有价值的

5.非阻塞等待和轮询访问

在这里插入图片描述
因为大多数情况下,执行非阻塞等待很少能够恰好遇到子进程运行完毕后的状态
因此非阻塞等待一般配合循环一起使用
而且我们知道:

操作系统是要保证运行效率和资源利用率的
因此如果父进程一直去阻塞等待子进程运行完毕的话,那么这段时间内你父进程不就什么都干不了了吗?
这样的话运行效率不就不够好了吗

因此非阻塞等待可以让父进程在这段空挡内依然可以运行程序,执行它的任务
提高了运行效率和资源利用率

下面我们来演示一下
在这里插入图片描述
在这里插入图片描述
这个专业术语叫做:基于非阻塞的轮询访问
因为单次查询时不一定马上就能收到子进程运行完毕的回复
因此常常使用循环+单次调用非阻塞等待一起配合使用

这样做有一个优点:在轮询期间可以让父进程做做其他事情
阻塞调用:等待的时候父进程没法做其他事情

四.进程程序替换

1.原理

在这里插入图片描述

2.小程序

首先我们要先明确一点:
Linux中我们平常使用的指令,它本身也是一个程序
因此下面我们实现一个小程序:
用我们的程序执行系统的指令(就是相当于把Linux的系统指令封装一下)

1.execl

在这里插入图片描述
在这里插入图片描述
关于可变参数列表,我们可以以printf来理解一下
这是printf这个函数的原型int printf(const char* format,…)
也正是因此printf可以打印相当相当多的数据
无论你设置多少个格式占位符%d %s %p %f等等等等…

2.小程序

下面我们就来实现一下
在这里插入图片描述
在这里插入图片描述
成功调用了ls命令
可是有几点细节需要说明
在这里插入图片描述
也就是说只有当程序替换执行失败之后,才会执行exec*后续的代码
但是此时因为程序替换执行失败了.所以此时退出状态需要设置为异常的
因此我们可以这样来写,顺便演示一下程序替换失败后的情形
在这里插入图片描述
在这里插入图片描述

3.更改为多进程版本

下面我们创建一个子进程,让子进程去替换为ls这个指令
在这里插入图片描述
在这里插入图片描述
我们发现:
子进程替换成功,不过为什么父进程还能正常运行呢?
因为:进程之间具有独立性,在替换的时候会发生写时拷贝
(注意:我们今天通过这个现象知道了一点:不仅仅数据可以发生写时拷贝,代码也可以!!)

4.细节性学习各种exec接口

小程序已经实现完毕
下面我们就来细节性的学习一下其他的那些接口

1.execlp

在这里插入图片描述
因此我们只需要小小地修改一下我们的代码
在这里插入图片描述
在这里插入图片描述
正常运行

2.execv和execvp

在这里插入图片描述
因此我们可以修改一下我们的代码
依然要注意:最后以NULL结尾
在这里插入图片描述
在这里插入图片描述
正常运行

其实main函数中的命令行参数就是从这里传过来的
shell通过进程替换创建子进程时,将命令行参数传给子进程的main函数时,使用的就是这种方式

在这里插入图片描述
下面我们只需要小小的修改即可
在这里插入图片描述
在这里插入图片描述
我们已经介绍完4个接口了,还差3个接口
其实我们介绍完那3个接口中的1个接口,剩下的两个接口其实大家也就会了
在介绍最后剩下的那一个接口之前,我们先补充一个知识点
能够让我们更好地理解剩下的一个接口和环境变量

6.补充

在这里插入图片描述
因此,我们来写一份C++的代码
在这里插入图片描述
这里补充一点:
因为

makefile从上往下扫描时默认只会形成一个可执行程序

所以为了让makefile能够生成多个可执行程序,我们可以这样做

makefile编译生成多个可执行程序

在这里插入图片描述
.PHONY设置伪目标all
all总是被创建
all依赖于mycmd,cppcmd
没有依赖方法,但是想要生成all,就必需要生成mycmd和cppcmd
然后就可以分别去生成mycmd和cppcmd了
在这里插入图片描述
下面我们在mycmd当中调用cppcmd
在这里插入图片描述
在这里插入图片描述
成功调用

7.execle

1.execle函数使用

在这里插入图片描述
下面我们来演示一下
在这里插入图片描述
将旧程序(C语言程序)中的环境变量交给新程序(C++程序)
在这里插入图片描述
在这里插入图片描述

2.execle补充:

在这里插入图片描述
下面先给大家演示一下
在mycmd当中新增环境变量,在程序替换的时候让cppcmd这个新程序也能拿到该环境变量
在这里插入图片描述
在这里插入图片描述
下面我们来演示一下
借助execle这个函数交给子进程全新的环境变量
在这里插入图片描述
在这里插入图片描述
子进程的确是全新的环境变量

8.shell运行原理

在这里插入图片描述

以上就是Linux进程控制的全部内容,希望能对大家有所帮助!

文章来源:https://blog.csdn.net/Wzs040810/article/details/134766296
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。