一、进程创建
这一块我们在前篇都已经讲过,第一部分就简单带大家回顾一下之前的知识。
1.1 fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include<unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程,看如下程序:
运行结果为:
这里看到了三行输出,一行before,两行after。进程15151先打印before消息,然后它有打印after。另一个after 消息有15152打印的。注意到进程15152 没有打印before,为什么呢?如下图所示 :
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1.2 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.3 fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
1.4 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
那为什么子进程要写入时,会写时拷贝一份副本,是因为父子进程之间是独立的,不能相互影响,并且写时拷贝避免了空间的浪费,做到了按需分配。
二、进程终止
2.1 终止是在做什么
我们都知道,进程是内核数据结构加本身的代码和数据,在创建一个进程之前,操作系统会先将这个进程的内核数据结构创建好,但是页表并不会与代码和数据建立映射关系,等到将代码和数据放到内存里后再建立映射关系。
既然我们创建进程需要创建内核数据结构和在内存中为代码和数据开辟对应的空间,那么,我们进程终止的时候要做的就是:
- 释放曾经代码和数据所占用的空间
- 释放内核数据结构
退出一个进程,首先我们已经确认它不会再被调度,所以可以直接将它的代码和数据所占用的空间释放掉,但是它的内核数据结构需要先维护起来,等待父进程的回收,此时等待父进程回收时该进程的状态就是Z:僵尸进程
2.2 退出码的介绍
我们先来理一个关系,我们在写C/C++程序时,我们的主函数main经常要返回一个值:0,这也叫做退出码。那为什么要返会0呢?返回100可以吗?
上述一个这么简单的代码,我们返回值是0,打印出来的结果也是没问题的。
ppid是我们的bash进程,我们之前已经讲过,从命令行启动的进程的父进程都是bash.那我们把返回值改成100会不会有问题呢?
返回值依然是没问题的
所以这又能说明什么呢?
我们直接看一个命令:echo $? 查看父进程bash获取到的最近一个进程的退出码
这怎么有我们刚刚设定的返回值100。
echo命令我们都知道,它是一个内建命令,打印的都是bash内部的变量数据,那意思是,我们的返回值是返回给bash了。那为什么要返回给bash?
就好比,班主任派我去完成一件事,我完成之后肯定需要报告结果给班主任,所以,返回给bash,就是要告诉父进程bash,这个子进程事情完成的怎么样了。
在退出码中:
- 0:表示成功
- !0:表示失败
成功不必多言,失败了肯定有失败的原因,你做一件事多次不成功,就有可能有多种原因,所以,!0的退出码一方面表示失败,另一方面也表示失败的原因。我们接下来看看不同的退出码对应的失败的原因。
2.3 退出码对应的失败原因
我们使用strerror函数打印每个退出码对应的失败原因
strerror函数就是将退出码转化为失败描述:
打印结果:
下面还有很多,我这里只是截了一部分。
每个退出码都对应一种失败原因,那么,bash知道进程的退出码就是为了知道失败原因?这里更重要的是,bash要为用户负责,因为用户想知道运行的进程为什么失败,bash要告诉用户原因!
2.4 进程终止的三种情况
经过2.3与2.4的学习,我们来进一步学习进程终止的三种情况:
我们先说前两种,在上述学习部分也已经显露出来:
- 代码跑完,结果正确
- 代码跑完,结果失败
正确或者不正确,我们都可以通过退出码观察出,结果失败的时候,我们也可以通过退出码去知道失败的原因。
第三种情况,大家可能想到了:代码出现了异常,提前退出
就比如说,我们在vs变编程运行的时候,崩溃了,是因为操作系统发现你的进程做了不该做的事情,杀死了进程。
我们先来看一种异常情况---野指针问题
此时出现非法访问的情况,异常退出,这个错误其实就是我们平常做题时会出现的段错误。
当一个进程因为异常退出时,已经跟退出码没有什么关系了,因为该进程已经提前退出了。
那我们为什么会出现异常呢?首先是操作系统发现异常,进程出现异常退出,本质上就是进程收到了操作系统发给进程的信号。就比如上面到的“Segmentation fault”异常信号
既然进程会被提前杀死,那我们之前讲过,我们想要退出进程有两种方式,一种是ctrl+c,另一种是命令kill.
kill命令又有很多子命令:
其中11就是与我们上面对应的异常信号,我们接下来将代码里的野指针问题去掉,我们在进程运行的时候,发送kiil -11 命令,就是相当于给这个进程发送了一个异常信号。
可以看到,我们给进程发送了异常信号后进程退出。
换句话说,当我们进程因为异常退出时,我们看退出信号是多少,就可以判断进程为什么异常了。
综上,衡量一个进程退出,我们只需要两个数字
- 退出码
- 进程信号
有了这两个中的任意一个,我们就可以判断出进程出现了什么错误,因为什么异常退出。
2.5 进程常见退出方法
1>正常退出
1、 return返回
- 从main返回,就是进程终止
- 从函数返回,表示函数结束
2. 代码调用exit函数,在代码的任意位置调用这个函数,都表示进程终止。
2>异常退出
ctrl+c,信号终止
三、进程等待
3.1 什么是等待
任何子进程,再退出的情况下,一般必须被父进程进程进行等待,进程在退出的时候,如果父进程不管不顾,退出进程,那么子进程就会变成僵尸进程,造成内存泄露。
所以,父进程需要通过等待,解决子进程退出的僵尸问题,回收系统资源,这个一定要考虑。另外,可以会获得子进程的退出信息,使其知道子进程是因为什么退出的。
3.2 如何等待
1、wait方法
wait方法是等待任意进程,我们可以使用该方法来防止僵尸进程的产生。
先看举例代码:
在上述代码中,子进程退出之前,父进程一直在等待子进程,等到子进程退出,父进程受到子继承的退出信息,则等待成功。我们看一下运行结果 :
当子进程退出时,父进程获取信息后也会立马退出。
那我们让父进程也睡上十秒,也就是说,当子进程5秒退出后,父进程还要等5秒才能来回收它,那么在这5秒等待时,子进程就会出现僵尸状态。等待完毕后,父进程再等待并收到子进程的信息将它回收,僵尸状态消失。
可以看到,子进程从僵尸状态退出。
2、waitpid方法
如果将第一个参数pid设置成-1,那么waitpid方法表达的也会是等待任意进程,如果我们想等待某个特定的的进程,那么就需要将进程的pid当作第一个参数
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
option参数设置成0,若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进 程的ID。