本篇文章,继续和大家分享与Linux相关的只是。本篇文章主要会涉及到进程的状态和进程的优先级。
上一篇文章,我们讲了操作系统理论中进程状态。接下来,我们来了解Linux中有那些进程状态。
进程状态
实际上,在Linux中有如下七种状态,并没有所谓的新建状态和就绪状态。这告诉我们理论和实现是不完全相同的。
R状态
R状态,可以理解为运行态。我可以编写一个简单的程序来看看R状态。
我们一边运行程序,一边运行脚本来查看进程信息。可我们发现一直运行的进程所处的状态并不是R,而是S呀!这是为什么?这是因为CPU的处理速度是纳秒级的,外设的处理速度是毫秒级别的。我们运行的程序的大部分时间都用在了等待请求外设上。所以,我们查询的时候,有99%的概率查到,程序处于等待状态,也就是阻塞状态,在Linux中用S表示。
如果我们将while循环中的语句都去掉,就能查到程序是处于R状态了。注释代码,我们可以使用vim的批量注释功能。第一步:ctrl+v 第二步:使用h、j、k、l这四个进行移动确定范围 第三步:按大写i,也就是shift+i ,进入插入模式。第四步:输入两个斜杠,按Esc就完成注释了。哪如何取消注释呢?和注释一样先确定范围。确定好后,按d即可。
监视结果,和我们的的预期一样。
R状态,就是R状态,为什么R后面还有一个加号?
R后面的加号,表示程序在前台运行,没有加号表示后台运行。如果我们想让程序后台运行,只需要在需要执行的程序后面加一个取地址符号即可。
此时,程序在后台运行,我们没法使用ctrl+c来终止程序。我们可以使用如下指令来终止程序。
[common_108@iZf8zaj27gxmvq7veqrekfZ test]$ kill -9 进程的
使用的kill指令,后面加的是进程的PID,而不是进程的名字。因为操作系统只认识数字,并不认识字符串。我们平时用的用户名,OS也不认识。哪它怎么辨识谁是谁,每一个用户都会有对应的身份证号,也就是查看进程时的属性UID。
我们可以通过ls指令,加上n选项来查看,我们的UID。简单一对比,就知道我此时用的用户的UID是1000。
s状态
S状态,也称之为浅度睡眠。可以理解为阻塞状态。
如果你的程序,有使用到外设,程序大部分时间都会用在外设的访问等待中,也就是程序处于S状态。
我们来看一个更明显的例子。
我们运行程序,一直不给它输入,它就会处于阻塞状态,一直等待键盘资源的就绪。
有一个与S状态相似的状态,叫D状态。
D状态
D状态,也称为深度睡眠。它也是一种阻塞,不过它接受操作系统的任何控制,也就是处于该状态的进程不能被杀死。为什么会存在这样一个状态呢?讲个小故事,你就理解了。有数据,需要写入磁盘。可磁盘一直没有就绪,进程只能处于S状态,在那等着。这时,OS路过,说你,你,你,怎么在干站着,什么也不做。刚好内存满了,这里容不下你这种闲人了。于是,OS就把该进程杀掉了。过几天,经理查数据,发现数据丢了。这是一批很重要的数据,记录了10000个用户的贷款金额。于是,就把OS,进程,磁盘三个人告上了法庭。法庭上,磁盘说,我压根就没接到进程的数据,这和我有什么关系?进程一听,他急了,赶忙说,你说你还没好,我就在那等,结果OS直接把我赶走了,这不怪我。法官,把目光看向OS。OS连忙解释道,我的这个权力不是他们继续的吗?我只是按流程办事而已,我有什么错?法官一听,它们好像都没有问题。难道是我有问题?当大家都没有问题的时候,说明机制存在问题。于是,Linux引入了D状态。在进程进行磁盘写入的时候,谁也不能杀死该进程。经此过后,OS再路过进程身边,进程直接亮出免死金牌。OS,好吧!你们不让我管,那我也不管了。
T状态
处于T状态的程序,表示该程序被暂停了。我可以使用kill -l指令来查看kill指令的使用选项。
[common_108@iZf8zaj27gxmvq7veqrekfZ test]$ kill -l
我们好用到的选项有18和19,19号选项可以让进程暂停,而18号选项则是让程序继续运行。
我们将程序的代码改成如下内容:
我们启动程序,通过ps指令查到它的PID,然后,使用kill -19指令
[common_108@iZf8zaj27gxmvq7veqrekfZ ~]$ kill -19 19066
程序就被暂停了,再使用ps指令查询。此时,程序就处于T状态。
使用kill -18指令,可以让程序继续执行。
t状态
小t状态和T状态差不多,这不作区分。
我们在编译程序的时候,加上-g选项,以debug版本发布。
我们使用调试器gdb打开可执行程序(gdb+可执行程序名),进行调试。打开后,我们使用指令l查看代码内容,然后,使用指令b在第6行打断点。打上断点后,使用指令info b确认断点无误后,使用指令r运行程序。
此时,使用ps指令查询proc可执行程序的状态,就处于小t状态。
Z状态
什么Z状态?我们通过一个小故事来理解。有一天,路边上走着一个老爷爷,他走着走着,就躺下了,这一躺就在也没有起来。刚好看,这一幕的你,赶紧拨打110和119。最先赶到的是110,警察迅速把现场保护起来,疏散人群。不久,119也赶到了,医生上前看了看,眼神失落的站起身来,摇了摇头,说没救了。警察,他们也有自己的法医,法医赶紧来确认死者的死因,是他杀,还是自然死亡。最后,警察汇总完结果,电话告知家属后,才开始对尸体做处理。在还没得出死因结果告知家属前,警察都会一直保护现场。同理,在子进程结束后,如果父进程一直没来关心它,获取它的运行结果。那么,子进程会被一直保护着,无法被回收。此时,子进程处于的状态,就叫Z状态。
处于Z状态的进程,我们称之为僵尸进程。如果僵尸进程一直的不到关心,它就一直无法被回收,一直占用着内存。这是什么问题?不就是内存泄漏吗?也就是内存泄漏方式,除了C语言时使用malloc外,僵尸进程也会造成内存泄漏问题。
我们可以简单验证一下,下面这个程序是我们利用if....else让父进程一直运行,而子进程先结束,exit用于终止进程,在man手册第3章。
我们根据子进程的pid查找子进程的状态,子进程确实处于Z状态。
上图中的defunct的意思,就是僵尸进程。
刚刚,我们说了,如果运行结束的进程,如果一直得不到父进程的关心,它就会一直处于僵尸,处于Z状态,可现在,在父进程结束后,我们ps却查不到这个进程的状态,这是为什么?
我们把程序改一下,让父进程先结束,子进程一直运行,你就明白了。
在父进程退出后,子进程的父进程变成1。我们把父进程先退出,而子进程没有退出,这样的子进程称之为孤儿进程。它会被1号进程领养。前面,我们子进程先退出,父进程后退出。在父进程退出后,我们之所以查不到子进程的信息,是因为父进程退出的一瞬间,子进程被1号进程领养并且回收了。
1号进程可以简单理解,就是操作系统。这里的1好进程是systemd,也有的是init。这个具体操作系统有关。
X状态
X状态,就是一个程序运行结束或终止了。
Linux中进程的状态转换图如下:
说了这么久,我们一直都在说父进程,那有没母进程呢?没有。也就是说,一个进程可以有多个子进程,但只有一个父进程。这就注定了进程之间是一个多叉树的结构。好像有点问题,我们之前,不是说OS采用双链表来组织进程吗?我确实有说过,但谁说过一个进程或者节点只能存在于一个数据结构里。一个节点可以既属于双链表,也可以同时属于多叉树。这在实现上只是多几个指针的事情。
在我们学习数据结构的时候,会把数据内容放到node节点中。但在Linux中,节点node只用来存放指针信息。数据信息等属性都放在进程的PCB中,节点node也是进程的属性之一。通过进程的node,我们就把一个一个的进程链接成了一个双链表。通过node的信息就可以找到任意一个进程的信息,再通过下图中的公式,就可以找到每个进程的PCB的起始地址。
下图中的公式怎么理解?首先,强转0的类型变成task_struct*,再通过这个访问PCB中的link。接着,对这个整体取地址,我们就得到了node在PCB中的偏移量。再接着,用节点的起始地址start减去偏移量就可以得到PCB的起始地址了,这需要分别对它们两做一个强转才能进行相减。假设我们现在操作的是other这个PCB,强转完类型,我们就可以访问other进程的PCB了。
进程的优先级
什么是优先级
查看进程的优先级是ps -l+进程pid指令。
修改优先级
修改优先级的方式,我们这里介绍两个方式,第一个方式使用nice和renice指令。第二个方式是使用top指令。
我们这里只演示top指令。
第一步:使用top指令
[common_108@iZf8zaj27gxmvq7veqrekfZ ~]$ top
第二步:按r,会显示下图中的提示
第三步:输入要修改优先级的PID,按下回车会出现下图的提示
第四步:输入n(nice)值,也就是NI的值。我们这里输入10。此时,优先级就变成了90。优先级的计算公式为:PRI(new)=PRI(old)+NI。简答来说:一个进程的优先级的值等于旧的优先级的值加上nice的值。需要注意的是,旧的优先级的值在Linux中固定为80,可以理解为PRI=80+NI。OS并不想让我们过分的修改优先级,给nice值限定了一个范围[-20,20)。通过公式换算也就得到了优先级得取值范围[60,90),一共40个级别。
OS是如何根据优先级进行调度
位图的概念,不知道大家有没有听说过。
位图利用一个bit来标记对应的数字存不存在,一个char类型,占一个字节,一个字节有8个bit。我们用下图中的结构体bitmap来表示位图。现在,查找一个数N是否存在位图中,我们用N除以sizeof(char)*8,就可以知道N在数组中的那个位置,就是下标几。在用N对sizeof(char)*8取模,就可以知道表示N的bit为是第几位。对应的比特位为0表示不存在,为1表示存在。
在Linux中,有一个类似下图的结构体runqueue,里面有两个二级指针run和wait,分别指向running和waiting这两个指针数组,这两个指针数组大小都为140。[0,99]的位置用于存放其他类型的进程,[100,139]是不是刚好40个级别,就是我们调整优先级的范围。如果来了一个优先级为60的进程,就把它的PCB链入到数组下标60的位置,如还有就跟在它后面。来61的,就链接在数组小标61的位置。CPU只需要从头遍历一边数组,就完成了按优先级调度进程。数组中有一些位置为空,CPU访问空位置会浪费时间。怎么办?还记得我们刚刚讲的位图吗?可以用位图来表示,数组的某一个位置是否为空。那怎么判断那个位置为一呢?我们可以采用二进制数最右边的1,快速定位需要访问的下一个数组位置。当CPU正在遍历running数组时,新来的进程,需要插入到waiting数组,避免干扰CPU的调度。当running数组遍历完了,只需要交换两个二级指针run和wait的内容,就可以进行下一轮的调度运行了。
好了,到这里,我们本次的分享就到此结束了,不知道我有没有说明白,给予你一点点收获。关于更多和Linux相关的知识,会在后面的文章更新。如果你有所收获,别忘了给我点个赞,这是对我最好的回馈,当然你也可以在评论发表一下你的收获和心得,亦或者指出我的不足之处。如果喜欢我的分享,别忘了给我点关注噢。