[Linux]——Linux进程控制

Linux进程控制

笔者在上一篇博客Linux进程基本概念中带领大家认识了什么叫做进程,操作系统是如何管理进程的,以及如何查看杀死一个进程等等。但是想驾驭“进程”这个概念还是远远不够的,今天就带领大家来看一个主题,叫做进程的控制

1.进程的创建

我们的同学都知道,在Linux下当你生成了一个可执行程序后,只要敲击./加上程序名让这个程序跑起来,我们就创建出了一个进程。今天给大家讲解另外一种创建进程的方式:fork函数。

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1

fork函数是一个重要的函数,他可以在已经存在的进程中,创建一个新的进程,原进程为父进程,新进程为子进程
在这里插入图片描述
很简单的代码,如果这里只有一个进程的话if和else语句肯定只会走一个分支,而结果是同时打印了两句话,说明我们创建了一个新的进程
在这里插入图片描述
这里给大家罗列两种fork函数的常规用法:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段(一个if一个else)
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数(后面讲解)

1.1创建子进程操作系统要做什么

使用fork函数创建一个子进程真的是非常的简单,但是这个简单只是表面上的简单,想要明确创建一个子进程后操作系统做了什么你需要先搞懂什么是进程地址空间

  • 我们之前再谈进程的基本概念的时候一定会提到pcb结构体这个东西,也就是Linux中的test_struct结构体,这个结构体中存放了关于进程的所有的信息,你可以把他理解为进程的身份证。
  • 这还不够,我们知道操作系统为了将每个进程都以相同的方式看待每个数据分段,所以每个进程会都创建一个叫进程地址空间的东西,这个东西实际上也是一个结构体,叫做mm_struct,这个结构体中存放是每个数据分段的起始和结束位置标识。
  • 接着,我们在物理内存为子进程进程申请空间,借助了一个叫做页表的东西,这个东西和硬件mmu配合起来将映射关系存放在页表中
  • 最后操作系统将子进程的pcb结构体加载到调度队列队列等待调度器调度

在这里插入图片描述

1.2写时拷贝

写时拷贝这一概念出现在父子进程的数据处理中,我们的问题是:因为子进程这里执行的代码与父进程完全相同,所以通常代码是共享的,但是数据呢?我们父子进程虽然代码执行的相同,但是如果连数据都相同那么子进程将毫无意义。那数据是父子进程各自私有一份么?那如果父进程有很多的数据,子进程又没有使用这些数据呢?

我们的答案是:通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本

写时拷贝用我们的大白话来说就是我使用你的数据的时候我再拷贝一份:
在这里插入图片描述

1.3fork调用失败

fork函数调用失败时会返回-1,fork失败一般是一下的两条原因:

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

2.进程的终止

一个进程既然能被创建,他也就一定可以被终止,在说进程终止之前,我们先来讲一个生活中的例子:你马上就要参加期末考试了,现在你考试的结果可能有三种。

第一种:你努力复习,考试考了100分
第二种:你考试前通宵打游戏了,考试考了0分
第三种:你作弊了,直接取消了考试资格

现在我们来聊一聊关于进程的终止,进程的终止情况和我们上面讲的考试的结果完全相同,结果一:程序执行完毕,结果正确结果二:程序执行完毕,结果错误结果三:程序未执行完毕,异常,这里结果二错误的意思是说,可能你希望得到10+20的结果,但最后返回了10*20的结果,结果三则表示程序可能奔溃或者栈溢出等异常错误。

2.1进程退出方法

进程退出的方法有:

  • 从main函数退出,return。
  • 调用函数exit或者_exit退出
  • 程序异常退出

从main退出我们比较熟悉,这里我们主要来介绍,exit和_exit这两个函数,首先来看看这两个函数的原型:

exit函数

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

_exit函数

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

这两个函数看起来非常相似,且都只有一个函数参数,那么这个函数的参数是什么意思呢?
在这里插入图片描述
执行echo ¥?
在这里插入图片描述
修改代码
在这里插入图片描述
执行echo ¥?
在这里插入图片描述
可以看出,我们以前写代码时都会写return 0,但是我们并不知道为什么要return 0,其实0表示的是程序的退出码,同理10也是我们的退出码,这里回到主题,上面两个函数中的参数就是程序的退出码。这两个函数的和return使用也基本相同,在哪里调用他们,程序就在哪里退出,但是这两个函数难道没有啥区别么?(ps:上面echo ¥?表示查看最近一次程序退出的退出码)

看一段代码:
在这里插入图片描述
这段代码的现象是:在等待一秒之后,屏幕打印出hello!
在这里插入图片描述
我们现在将代码中的exit换成_exit函数。我们居然发现我们要输出的hello不见了。
在这里插入图片描述
现在我们得出结论:exit函数会对当前的进程进行收尾的工作,比如将缓冲区刷新。而_exit表示不进行收尾工作,直接结束进程,该进程的所有的数据直接丢弃。
在这里插入图片描述

3.进程等待

我们现在到了进程的第三个主题:进程等待,而这个主题貌似有一点特殊,单单从名字上你好像并不知道进程等待是什么意思,谁等谁,为什么要等,别急,我们先聊一聊之前讲过的东西。

你可否还记得什么是僵尸进程,僵尸进程就是一个进程的子进程已经退出,但是这个子进程的状态并没有被父进程读取,所以子进程这时候就变成了僵尸进程。而子进程一直不被读取的危害就是这样会造成内存泄漏,最想知道一个子进程最后执行的结果怎么样的当然是他的父进程,所以父进程通过等待的方式,回收子进程资源,获取子进程退出信息

3.1进程等待的方法

这里等待的方式同样有我们的两个函数:

wait函数

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值: 成功返回被等待进程pid,失败返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

我们现在就来使用一下wait这个函数:下面代码前10秒父子进程都在跑,10秒后子进程退出变成僵尸状态,又过了10秒被父进程等待读取,最后5秒只有父进程在运行(这里我们先不关心wait 的参数,所以给null就行)

在这里插入图片描述
另起一个终端输入命令,这是一个搜索进程的监控脚本,每秒刷新一次
在这里插入图片描述
前10秒:
在这里插入图片描述
10秒后
在这里插入图片描述
最后5秒,子进程被回收
在这里插入图片描述
现在更换代码,大家可以看到,这段代码父进程并没有休眠,应该要直接退出,但是事实上呢?
在这里插入图片描述
我们发现,父子进程同时执行,直到父进程等到了子进程,他们才同时退出。
在这里插入图片描述
可是说,wait使用起来确实是一个比较简单的函数了,那么现在再来再看看第二种等待函数:

waitpid函数

看到这个函数原型时笔者也是头皮发麻,如果你不想看函数原型就接着往下看,慢慢你就理解所有的参数意思了。

pid_ t waitpid(pid_t pid, int *status, int options);
返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid: Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。
status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。

我们发现waitpid函数也有一个status参数,那么不如我们先来聊一聊这个参数的作用,首先你需要明确这是一个输出型参数,什么是输出型参数?就是你给他传了一个整型的地址,他会帮你赋值你想要的量

void func(int* p)
{
	*p = 10;
}
int main()
{
	int a;
	func(&a);
}

所以能看出来,我们拿出的status是一个整型,那么这个整型是干么的?不管这个整型的后16位,前7位代表是否被信号所杀,如果前7位为0表示不是被信号所杀,次8位表示的数是状态码,前面说过,return 0的0就是状态码,前7位不理解别着急,接着看
在这里插入图片描述
pid_t pid这个参数表示你想等谁,也就是说你设置某个子进程的pid为参数,那么等的就是他。如果这个参数设置为-1表示等待任何一个子进程。
int options则表示你以什么样的方式进行等待,这里有两种方式:阻塞式等待(设置参数为0)/非阻塞式等待(设置参数为WNOHANG)
这个函数返回值:如果等待某个子进程失败则返回-1,成功则返回子进程的pid

多说无益,我们直接写一份代码来使用一下这个函数:从代码中我们看到,这次我们给waitpid函数传入status参数,等待的id为子进程id,等待方式以阻塞式等待,阻塞式等待就是一直卡在那里等待,直到等到子进程运行结束
在这里插入图片描述
运行结果与上面相同,程序等待成功前5秒两个进程同时运行,后5秒只有父进程在运行。然而我们肯定不想只看到运行的状态,我们现在不妨把前面说过的status这个输出型参数拿出来看看。

我们看到代码的第29,30行拿出了status的低7位(程序的信号码),以及这个子进程的返回状态码,我们看到子进程中的exit中的返回码设置为了13
在这里插入图片描述
运行程序:我们发现取出的次低8位为13,刚好就是我们设置的退出码,而信号码为0,信号码为0则表示程序正常执行完毕,不是被信号杀死的。
在这里插入图片描述
我们现在做一件新的事情让我们对信号位这个数字有新的理解,我们在这个程序跑起来之后使用kill -9 子进程pid命令杀死子进程,注意9就是信号码,我们发现此时的信号码为9说明程序异常退出,被9号信号杀死
在这里插入图片描述
别急,再看一段特别简单的代码:简单到爆炸的死循环
在这里插入图片描述
运行起来后,他变成一个进程,此时他就会一直处于死循环状态,这里在给大家介绍一个8号信号,表示程序进行了除0错误,但是我们上面的这段代码并没有出现除0的错误,如果我们向这个进程发送8号信号呢?

当你输入命令kill -8之后,这个程序居然退出了,并且报错为除0错误。
在这里插入图片描述
现在你应该明白status低7位和次8位表示的意思了把,现在,我们第二个参数讲完了,来看看第三个参数。第三个参数表示等待的方式,上面说过,等待方式有阻塞式等待和非阻塞式等待。阻塞式等待就是父进程一直卡在那里等待子进程运行完毕,而非阻塞式又是什么?

非阻塞式等待:举个例子,你邀请你同学出去吃饭,你同学在上厕所,告诉你他还需要10分钟,所以在这10分钟里你出去到小卖部买了瓶水,10分钟后你问他好了没,他说还需要10分钟,所以这10分钟里你又去干了其他的事情。基于上面的例子,非阻塞式等待的意思是说如果等待的条件不满足,父进程不会卡在那里,而是会立即返回,下次再判断条件是否满足
在这里插入图片描述
ret如果等于-1则直接退出,如果大于0则表示等待成功,而等于0则表示当前等待子进程成功,但是子进程没有执行完毕,所以父进程进行非阻塞式等待。这里要明白,阻塞式的等待其实就是将进行等待时不满足条件的父进程设置为非R状态,并且从运行队列中拿走,放到等待队列,当等待的条件满足,则操作系统将这个进程重新放到运行队列中等待调度

4.进程程序替换

在之前我们fork一个新的子进程的程序中,实际上我们fork出的子进程还是在执行父进程的代码,那么现在如果我希望子进程执行一个自己全新的代码呢?这就要讲到我们的进程程序替换。

4.1替换原原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数(程序替换函数,后面讲)时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
在这里插入图片描述
用大白话来说就是,进程的程序替换并不创建新的进程,而是直接讲要运行程序的代码和数据直接替换原进程。

4.2替换函数

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[])

来看看函数名字中不同的组合的意思:可以发现函数名字中就是用下面的4个不同字母进行组合的

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

再来看看函数的参数吧:

  • path:你所要替换函数的路径
  • arg:命令行参数选项,必须以null结尾
  • file:要执行的程序名字
  • const argv[]:命令行数组,给替换函数传一个命令参数数组
  • envp[]:环境变量数组
  • …:这几个省略号表示你所要执行命令的命令行参数

上面各种参数组合看起来真的是非常的复杂,不如我们这里先来使用一下。因为使用的lp组合的函数,说明只需要传命令名字和命令所需要的参数,值得注意的是,下图中第一个ls表示要执行的命令名称,第二个则是命令行中的ls
在这里插入图片描述
我们将写好的这段代码跑起来,此时就完成了我们的程序替换,相当于把replace这个函数替换为了ls命名函数
在这里插入图片描述
而其他几个函数的使用方法也完全相同,有兴趣的同学可以下去自己查询一下剩余几个函数的用法。

总结

本节主要要让大家了解如何来控制进程,要学会控制进程,上文中讲的函数都是必不可少的法宝。控制进程就讲到这里,如果有什么错误或者不懂的地方大家可以提出来哦。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值