前言:本篇进入新章节——进程控制。 本章节和上一章节同样都是讲解进程, 但是内容上却比上一章内容好理解的多。上一章内容都是进程的概念性相关, 那个时候我们对于进程的理解还处于小白状态, 所以很多东西很抽象, 不好懂。 但是度过了上一章后, 我们对于linux进程这一方面来说, 就能拥有一些知识储备, 并且用来理解一些计算机现象了。所以, 本篇内容有了上一章的基础会容易理解很多。
ps:本篇内容适合已经理解了linux进程概念的友友们进行观看
目录
进程创建
进程创建的函数就是fork, for在上一章的PCB板块中就讲到过, 并且讲的很深入——我们是从四方面讲解的:在PCB小节分别讲了三个问题——为什么fork给父进程返回子进程pid, 子进程返回0、fork函数究竟干了什么、fork函数如何做到的返回两次;在虚拟地址空间小节又谈了为什么fork明明是一个变量却又两个不同的内容。
同时也讲解了fork的用法, 但是本节讲重新说一下fork的用法。
创建单个子进程
fork函数的后面都会被创建成为子进程
下面是运行结果:
可以看到, 里面创建了一个进程pid7778,这其实就是子进程。
创建多个子进程
我们写一段代码如下:
这串一共创建了五个进程, 当每一个子进程跑完之后都会终止结束。
为了观察, 我们先启动监控脚本:
然后运行程序, 5s内, 运行的结果如下:
当5s结束后,如下图:
由上面的结果我们可以发现, 当我们子进程创建的时候, 父进程和子进程哪一个先运行呢? ——当我们创建多进程的时候, 到底谁先运行, 完全由调度器决定, 也就是谁先被放到队列中, 谁就先被运行。——也就是说, 对于一批子进程来说, 谁先被运行, 并不一定。
重谈写时拷贝
写实拷贝博主之前的章节也有说过。 忘记的友友们可以自行复习。
写实拷贝其实就是——子进程赋值父进程的PCB以及进程地址空间以及页表。 然后页表的映射物理内存是相同一份数据。 但是当子进程想要修改的时候, 由于进程的独立性, 子进程修改不能影响父进程的数据。 就要发生写时拷贝——重新映射。
具体的过程就是下面这样:
意思就是说, 一开始,子进程拷贝父进程的PCB。 虚拟地址空间, 页表。 所以父子进程中地址空间中的数据段和代码段映射到物理内存也是一样的。 但是这个时候页表会让他们都为只读状态, 如果后续的过程中子进程或者父进程都是只读, 那么就没有问题。 但是只要其中一个发生修改, 那么这个进程对应的页表就会重新映射空间。 这就是写实拷贝的原理。——延时申请、按需申请。
退出
程序运行完毕后有两种状态, 一种是正常退出, 一种是异常终止。 这里将分别讨论一下两种状态:
正常退出
正常退出又分为两个状态, 一个是运行结果正确, 一个是运行结果不正确。
而运行结果也就是return某个数字或者exit某个数字, 这里的某个数字就是运行结果——也被成为表征码,退出码。用来表示运行结果正确与否:
运行后:
我们可以使用环境变量? 打印上一次退出码的结果:
这些运行码的表征结果最终会返回给bash进程。为什么会这样?——这里要思考一个问题:对于进程来说, 谁会关心我运行的情况呢? 一般而言是我们的进程的父进程需要关心。所以会将结果返回给bash进程。
哪儿买, 父进程接收退出码, 需要关心什么? 父进程关心子进程, 更多的是关心子进程退出时, 代码跑完后为什么结果是不正确的。
而代码的成功只有0, 只要是非0那么就是不正确。 而不正确的非零数字, 就会代表不同的推出原因——退出码。
对于?来说, 为什么echo $?能狗打印退出码——因为? 是一个环境变量, ? 中保存着上一个执行的进程的退出码, 并且这个只能保留一个。
就比如这个程序, 执行后打印退出码, 可以得到:
再执行一次可以得到:
进程运行后环境变量如下:
但是多运行几次后, 就会出现下图:
这是因为对于?来说, 第一次打印process的退出码。 但是因为打印要运行echo程序, 这个时候的?里面保存的就是echo的退出码, 正确为0, 所以之后的打印都是0.
那么, 为什么要有表征码呢? ——对于现阶段来说, 我们打印数据都是打印在硬件显示器上面。 但是未来我们可能并不只是在显示器上面打印, 可能还在网卡网络上面打印。
但是对于上面的数字来说,我们并不好确认某个数字对应的错误信息。 所以对于用户来说, 我们是不是应该接收到对应的一串字符串信息, 就能更好地接收到进程地错误信息。 ——其实, 系统真的提供了这么遗传信息。 这串信息就是一个接口——strerror, 我们使用man手册可以查看到:
仙逝后我们就可以看到一个叫做strerror地接口。 这个接口可以将错误码转化为错误的描述信息。 并且打印出来, 我们就可以使用下面的程序进行验证:
打印的结果就是如下。 可以看到返回0就是成功, 返回1就是操作被限制。 返回2就是没有这个文件。 现在我们来看几条指令, 以及这些指令的结果。
看下面这个指令, 我本想查看一个新文件地信息, 但是这个新文件并不存在, 那么就会打印下面这种情况:
使用echo后, 我们也会发现, 打印的数字和报错信息地对应关系是一样的:
我们也可以自己实现一个错误码, 这个错误码如下:
想要哪个错误码, 返回相应的下标即可。
errno可以返回程序最新的错误码, 也就是说, 如果有多次错误, 它就会返回最新一次的, 使用方式如下:
运行的结果如下:
异常终止
运行异常,对于运行异常来说, 程序的退出码基本可以不看。因为异常就代表没有正常执行到退出程序, 这个时候退出码没有太大意义。
现在看一下下图:
这个程序就是对于零号地址进行解引用, 但是系统层面不允许我们这么做。 这就会给我们发出一个信号, 如下图:
这上面的就是程序的结果, 对应的意思就是对野指针的解引用。
事实上日常中我们也是这样, 如果一个程序发生了异常终止。 ——本质上就是进程收到了对应的信号。
其实, 如果一般的异常, 一定会触发一些硬件级别的错误。 比如说cpu除零, 那么cpu就会出现一些溢出性错误。 又比如野指针, 野指针就是这个地址在页表中没有建立对应的映射关系。 或者建立了映射关系, 但是权限只是只读, 最终也会转化为硬件问题。
这些硬件问题会被操作系统识别, 进而操作系统会向目标进程发信号。
当我们进程出问题的时候kill -9直接杀掉进程:
现在来观察一下下面这个进程:
如下图:
现在使用kill -9杀掉进程, 没有问题:
但是, 如果我们给这个程序发一下某个异常信号:
当我们没有发信号的时候, 这个程序正在正常的跑。——如果不出意外, 这个进程是能够无限循环的, 但是如果我们给进程发一个信号, 那么这个进程就会被检测为异常, 进而被操作系统杀掉。 由此可以看出——进程的本质就是接收到了对应的信号。
exit
exit用来终止进程, exit里面也有退出码:
比如这个进程, 跑不到return就会遇到exit直接终止, 如下图:
现在我们来看这么一串代码, 看看如图是如何进行的:
请问上面的程序的退出码是什么? ——答案是13, 因为对于exit来说, 无论exit在什么位置, 只要exit出现, 就会终止掉调用该函数的进程。
exit和_exit区别
要解决这个问题, 先看下面的两串代码:
运行结果分别是:
没有什么区别, 但是现在是有\n, 会刷新缓冲区, 我们去掉\n后就会出现问题:
首先来看一下_exit()
可以观察到的情况就是:printf打印了一段语句。 但是程序并没有将这段语句打印出来:
按照我们正常的理解, 这个语句是可以打印出来的, 就如同下面的exit:
为什么会这样就是因为_exit是系统调用, exit是上层封装, 就如同下图:
_exit是一个系统调用接口, 正常程序直接调用操作系统接口_exit, 直接在系统层面终止进程。 而exit是先调用这些刷新缓冲区的函数等等, 然后最后再调用_exit来终止进程。
我们printf显示数据时, 一定是先把数据写入缓冲区, 然后在合适的时候, 再进行刷新!
缓冲区问题
现在我们思考一个问题, 缓冲区, 绝对不在哪里呢?
现在我们来看一下下面的图:
其实这里面上面的就是用户层, 而下面的是内核空间。
那么假如缓冲区在内核空间, 那么_exit也在内核中, 那么当_exit终止进程的时候就能直接刷新缓冲区。 就起到了缓冲的作用, 这和事实矛盾。 所以得到的结论就绝对不在缓冲区。
以上就是本篇文章全部内容——下面是本节笔记