目录
- 前言
- 一、进程创建
- 1.1 fork函数初识
- 1.2 写时拷贝
- 1.3 fork常规用法
- 1.4 fork调用失败的原因
- 二、进程终止
- 2.1 进程终止时,操作系统做了什么??
- 2.2 进程终止的常见方式有哪些??
- 2.3 如何用代码终止一个进程
- 三、进程等待
- 3.1 进程等待的必要性
- 3.2 进程等待的方法
- wait方法
- waitpid方法
- 补充知识
- 四、waitpid进一步讲解
- 五、进程替换
- 5.1 概念及原理
- 5.2 操作
- 5.2.1 不创建子进程
- execl
- 5.2.2 创建子进程
- execv
- execlp
- execvp
- execle + 执行其他的C二进制程序
- 执行其他语言的程序
- 5.3 execve
- 六、自己实现一个简易的shell程序
- 6.1 函数介绍
- 6.2 myshell.c完整代码
- 总结
前言
前面的文章都是关于进程概念的学习,今天我们就要进入一个新的章节,关于进程控制的解析,这其中我会穿插着前面已经讲过的知识,在以前知识的基础上学习新的知识,如果其中有哪里不懂的地方,大家可以去看一看我前面的文章,下面我们一起来学习新的知识吧。
一、进程创建
1.1 fork函数初识
在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
1.分配新的内存块和内存数据结构给子进程
2.将父进程部分数据结构内容拷贝至子进程
3.添加子进程到系统进程列表中
4.fork返回,开始调度器调度
fork之后,父子进程代码的共享情况
1.2 写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式变为各自一份副本,具体可见下图
写时拷贝的好处: 因为有写时拷贝技术的存在,所以,父子进程得以彻底分离!完成了进程独立性的技术保证!
写时拷贝是一种延时申请技术,可以提高整机内存的使用率
1.3 fork常规用法
1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。
2.一个进程要执行不同的程序
1.4 fork调用失败的原因
1.系统中有太多的进程
2.实际用户的进程数超过了限制(我们平时在写代码的时候,进程数其实是有限的)
二、进程终止
2.1 进程终止时,操作系统做了什么??
当然是释放进程申请的相关内核数据和对应的数据和代码,本质就是释放系统资源。
2.2 进程终止的常见方式有哪些??
a.代码跑完,结果正确
b.代码跑完,结果不正确
c.代码没有跑完,程序崩溃了(这部分涉及到了信号部分,我们先说一点点,后面再详细的说)
这里我们主要分析a、b两条内容,我先提一个问题:
大家平时在写C/C++程序的时候,一定都会写main函数,main函数最后都会有return 0,那么这里的return 0的含义是什么呢?为什么总是0,main函数返回的意义是什么???
在做下面的实验前我再补充一个小的知识点
echo $?:获取最近一个进程,执行完毕的退出码。
现在相信大家已经知道main函数最后的return 0是什么含义了,每一个退出码都有着不同的含义,我们通过下面的这个函数把C语言中的退出码是什么含义打印一下看看。
我们今天学习完退出码的概念,大家以后写代码的时候要可以将其应用起来,大家一起看下面的这个例子
int main()
{
FILE* fp = fopen();
if(fp == NULL)
return 1;
return 0;
}
当我们需要打开一个文件的时候,就可以使用上述的if判断语句来判断我们的这个文件是否被成功打开,当程序的退出码为1的时候,就是不允许操作,即这个文件打开失败,这样就可以更快地排查错误。
a、b两条内容分析完后,我们还要谈一谈c的内容,代码没有跑完,程序崩溃了。
2.3 如何用代码终止一个进程
什么是一个正确的终止???
1.return
main函数内的return语句,就是终止进程的,return 退出码
只有在main函数返回,进程退出
调用一些其他的函数,return则是退出当前的函数并返回一个值。
2.exit,C语言层面的函数
通过上面的情况再与return进行比较我们可以发现:
return只有在main函数中有结束进程的作用,exit在代码的任何位置调用,都表示直接终止进程!!!
3._exit,系统层面的函数
上面图片中的_exit与_Exit的作用类似,我们只用_exit来举例子,让大家看一下与_exit与exit有什么不同之处
从图中大家可以看到_exit()与exit()的区别,在平时写代码的时候还是建议大家使用exit()函数。
三、进程等待
在讲述进程等待前我们先来试想两种场景
1.子进程退出,父进程不管子进程,子进程就要处于僵尸状态,一直处于僵尸状态不释放资源,就会导致内存泄漏
2.父进程创建了子进程,是要让子进程完成一些任务的,那么子进程把任务完成得怎么样?父进程需不需要关心,如果需要,怎么得知子进程的情况,如果不需要,又要怎么处理呢?
子进程把任务完成的情况也是分为三种情况:
a.代码跑完,结果正确
b.代码跑完,结果不正确
c.代码没有爬完,程序崩溃了
3.1 进程等待的必要性
1.子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,即使是kill -9也无能为力,因为谁也没有办法杀去一个已经死去的进程。
2.最后,父进程派给子进程的任务完成的如何,我们其实是需要知道的,比如子进程运行完成后,结果对不对,或者最后是否正常退出,都是我们需要考虑的问题
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 进程等待的方法
我们先来看一下不让父进程去回收子进程的资源的情景,来进行一下对比
在上面的代码中,我们使用fork()函数创建了一个子进程,然后让其循环五次,打印同一行代码,循环结束后,结束进程,而父进程则一直进行循环,这样当子进程结束后,父进程还在死循环,无法收集子进程的资源,那么子进程就将成为僵尸进程。下面我们来看结果
这样就导致了僵尸进程的产生,那么如何解决这个问题呢?
wait方法
主要验证回收僵尸进程的问题
wait接口,我们可以通过man 2 wait来查询关于wait的信息
当然其中也有waitpid的信息。
下面我们先来简单认识一下wait()
下面我们来看一下实验代码以及运行结果
实验一:我们先来看一下如果父进程属于睡眠状态不调用wait函数,子进程是不是还是变为僵尸进程
运行结果分析
实验二:当父进程不处于睡眠状态时,又是一种怎样的情景
运行结果分析
父进程:wait时,父进程处于阻塞式等待,父进程处于阻塞态,等子进程退出。当子进程退出时,操作系统再将父进程唤醒,放到运行队列里,由父进程调用wait函数,再返回。
父进程处于阻塞式的等待意味着: 子进程不退出,父进程也不退出
waitpid方法
进程异常退出或者崩溃,本质是操作系统杀掉了你的进程!!
操作系统如何杀掉的呢?本质是通过发送信号的方式!
下面我们来看一下实验代码以及运行结果
实验一:子进程正常结束
运行结果
实验二:子进程非正常结束(1)
注: 当进程非正常结束时,进程的退出码就没有任何意义,主要观察进程收到的信号编号
运行结果
关于信号的信息我们可以通过kill -l(小写的l)来查看
其中1~31为普通信号,没有0号信号
实验二:子进程非正常结束(2)
运行结果
实验二:子进程非正常结束(3)
程序异常,不光光是内部的代码有问题,也可能是外力杀掉了子进程(子进程代码有没有跑完并不确定)
上面的代码我们让子进程死循环,那么这个进程就会一直运行
此时我们在另一个窗口将该进程杀掉
最后父进程收到的信号编号就是9号信号
总结:
1.如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
2.如果在任意时刻调用wait/waitpid,子进程存在企鹅撑场运行,则进程可能阻塞
3.如果不存在该子进程,则立即出错返回
4.往后我们编写多进程,基本的写法就是fork + wait/waitpid
补充知识
1.父进程通过wait/waitpid可以拿到子进程的退出结果,为什么要用wait/waitpid函数呢?可不可以使用全局变量?
答:不可以
因为我们在前面就说过进程具有独立性,那么数据就要发生写时拷贝,父进程无法拿到子进程内的数据,更何况是信号了
2.既然进程是具有独立性的,那进程退出码也应该是子进程的数据,父进程凭什么能拿到呢,wait/waitpid究竟做了什么事情?
答:wait/waitpid的本质其实是读取子进程的task_struct结构。
3.这又和task_struct结构有啥关系呢??
其实最后进程退出的时候,一个进程的退出码和退出信号会被写到task_struct中(task_struct结构中有exit_pid,exit_signal两个变量),僵尸进程也是一样的,子进程退出,那么他的代码和数据就不会被运行和应用,就可以释放相关的数据,但是至少他会保留该进程的PCB信息,task_struct里面保留了任何进程退出时的退出结果信息, wait/waitpid读取task_struct结构后再将exit_pid,exit_signal(两个字段)进行位操作设置到传入的status变量中,然后再返回出来,就可以得到子进程的信号编号和退出码
4.wait/waitpid有这个权利么?
答:肯定有这个权利
wait/waitpid属于系统调用函数,本质是操作系统去拿取task_struct中的数据,而task_struct是内核数据结构对象,所以操作系统可以对其进行读取也就没有任何问题了。
四、waitpid进一步讲解
options中的WNOHANG选项。
WNOHANG选项,代表父进程非阻塞等待!
非阻塞等待:我们的父进程通过调用waitpid来进行等待,如果子进程没有退出,我们waitpid这个系统调用会立刻返回。
了解了上面的宏定义后,我们先来使用一下,顺便再结合而代码详细介绍一下我们上面所讲的内容
下面我们来看一下waitpid的伪代码
阻塞等待我们已经讲述完毕,后面就是非阻塞等待
非阻塞等待我们还是通过代码来演示
其实非阻塞等待的最大意义在于:
当子进程还在处理自己事情的时候,父进程不需要进入阻塞状态,父进程依然可以去做一些自己的事情,当子进程处理完自己的事情以后,父进程再去回收子进程的资源和数据。
运行结果:
通过上面的运行结果,我们可以看到,当子进程还在运行的时候,父进程可以腾出来时间来处理自己的任务,这就是非阻塞等待的好处。
如果未来编写网络代码的话,大部分都是IO类别,会不断面临阻塞和非阻塞的接口
五、进程替换
5.1 概念及原理
fork()之后,父子代码是共享的,数据写时拷贝,各自一份,父子进程各自执行父进程代码的一部分,但是如果子进程就想执行一个全新的程序呢?
进程的程序替换可以完成这个功能,程序替换是通过特定的接口,加载磁盘上的一个权限的程序(代码和数据),加载到调用进程的地址空间中,阿里让子进程执行其他的程序
可能上面说的内容比较抽象,大家不好理解,那么我们下面就通过画图的方式,来剖析一下进程替换的原理
5.2 操作
关于进程替换的函数有以下六个,看似很复杂,其实还是比较好理解的
下面我们先来进行一下基本的演示
1.不创建子进程
2.创建子进程
从这两个方面介绍,同时只使用最简单的exec函数,先看一看进行进程替换后会发生什么事情
后面我们会详细展开其他函数的用法
5.2.1 不创建子进程
execl
第一个我们要介绍的函数就是execl
其实该函数的可变参数列表可以参考我们平时最常用的命令行参数
下面我们就以平时用的最多的ls命令进行举例,先来看一下ls命令
红框内就是ls的路径,那么我们下面就来用ls命令进行进程替换,先看代码
在代码中,我在main函数的开始和结尾都加上了一句打印,这样就可以看到进程进行的情况,然后中间去调用函数进行进程替换。
代码运行结果
通过代码运行的结果,我们可以看到一些问题
程序刚开始运行的时候,打印了第一句代码,后面进行了程序替换,执行了ls的命令,同时也将其对应的结果打印了出来,但是最后面的一行代码并没有被打印出来,这个就是程序替换的特性
针对代码结果进行分析
1.为什么最后一句代码不打印呢??
execl是程序替换,调用该函数成功之后,会将当前进程的所有代码和数据都进行替换,包括已经执行的和没有执行的
简单来说,就是执行execl之后,execl之前和之后的代码都会被替换掉,只不过之前的代码运气比较好,在替换之前就已经执行完了,所以才有了最开始的打印结果。而后面所有的代码,全都不会执行
2.execl的返回值
我们看手册的介绍可以得知,如果调用失败,会返回-1,但是调用成功,并不会返回值,这又是为什么呢?
为什么调用成功没有返回值呢?
这个其实也很好理解,因为execl根本不需要进行函数返回值判定,如果替换成功,execl会将main函数中包含execl本身的代码全部都替换掉,前面用于接受返回值的数据也都已经被替换了,所以也就不需要有返回值了。
如果想让我们打印的字体也颜色的话,就可以看ls的内容再加一些代码
讲解后面知识之前,我们先来回忆一下之前的内容
加载新程序之前,父子的数据和代码的关系:
代码共享,数据写时拷贝
当子进程加载新程序的时候,就是一种写入,那么这个时候代码要不要进程写时拷贝呢?
一定会进行写时拷贝,父子进程的代码必须要分离
所以当我们调用execl函数的时候,父子进程在代码和数据上就彻底分开了。
5.2.2 创建子进程
为什么要创建子进程呢?
为了不影响父进程,我们想让父进程聚焦在读取数据,分析数据,指派进程执行代码的功能,如果不创建,那么我们替换的进程只能是父进程,如果创建了,替换的进程就是父进程,从而不会影响父进程
在上面我们已经看了execl的使用,所以这里就不多说了,关于exec系列还有几个函数,我们一起来看一下
execv
实验代码及结果
execlp
实验代码及结果
execvp
实验代码及结果
以上的代码基本都没有什么问题,那么我们来探究一个新的问题
1.如何执行其他我自己写的C、C++二进制程序
2.如何执行其他语言的程序
execle + 执行其他的C二进制程序
Makefile操作延伸
在Makefile中替换单词
例如:将Makefile中所有的exec替换为myshell
1.先按Esc
2.输入 :(英文冒号)
3.%s/exec/myshell/g + 回车
exec.c代码
mycmd.c代码
经过我们上面的代码,成功地实现了一个程序调用另一个程序
回顾 + 总结: 环境变量具有全局属性,可以被子进程继承下去,通过execle就可以将父进程的环境变量传给子进程
执行其他语言的程序
我们这里就简单的演示一下执行python语言的程序和执行shell程序
python代码,只输出一下,能够看到我们调用了即可
shell程序代码
运行python: python test.py
运行test.sh: bash test.sh
如果我们使用shmod + x test.py也可以执行python程序,这句话的意思就是给test.py加上可执行权限,就可以将test.py变为可执行程序,那么也就能直接执行该程序,了解了这些以后,下面开始进行一下简单的调用
1.调用python程序
1.调用shell程序
以上就是调用其他程序的全部内容。大家只需要了解就可以了
5.3 execve
其实在程序替换中,操作系统只提供了这一个接口,这个才是真正的系统调用接口。前面讲的函数是系统提供了基础封装(C语言做的封装),从而用来满足上层的不同的调用场景,上面的函数接受的数据经过整合后再传给execve函数
execve没有带p,所以需要带全路径
六、自己实现一个简易的shell程序
我们在自己写shell的时候需要创建子进程去完成命令,为什么呢?
因为我们执行的一些命令可能会出现一些错误,子进程执行的话只会影响子进程的运行,不会影响到父进程。
6.1 函数介绍
fgets函数介绍
strtok函数介绍
chdir函数介绍
6.2 myshell.c完整代码
总结
以上就是进程控制全部的内容了,可以看到其中涉及到的知识点还是非常的多的,有的也不是很好理解,所以我们要多去看,多去总结,同时自己也要将这些函数全都用一下,这样才可以加深印象,如果文章中有错误的话,希望大家评论指出,我后面一定会改正,最后,如果你感觉我的文章对你有用的话,就给波三连吧!!谢谢大家