Linux操作系统-进程(二)
1.进程状态
我们在学习进程状态时经常看到这样一个图:
其实这张图看着复杂,其实一点也不简单,这张图虽然罗列了进程的各种状态,但是对学初学者而言这张图并不容易理解进程的各种状态,所以我们不必死磕这张图,下面我会详细介绍进程的各种状态。
1.1运行状态
要理解运行状态,我们要先知道cpu调度进程是有相应的调度算法的,这里我以FIFO(first in first out)调度算法为例,看到FIFO相信大家都不陌生,没错,就是数据结构中队列增删查改的算法。
所以这个调度算法来调度进程本质就和队列差不多,下面我以图的形式来帮助大家更好地理解:
大概的结构就如上图所示,会有一个runqueue的调度队列,里面的head指针就指向后面的一系列进程,并且只要在这个队列中,进程就处于运行状态。
可能有的课本会说进程只有在cpu运行时才处于运行状态,不过10本课本,也就1本左右会这样说,也就是说大部分课本都认为只要在这个队列中,进程就处于运行状态这种说法。
注意:一个cpu,一个调度队列!!!
1.2阻塞状态
什么叫做阻塞状态呢?下面用一个常见的现象来解释:
我们在C语言阶段都学过scanf函数,它的作用我们也都清楚,而当程序执行到scanf函数时,程序会变成这样:
程序就会停留在第一幅图所在的界面,系统在干什么呢?
它在等待用户从键盘输入数据,只有这样它才能继续执行后面的代码,而在等待用户从键盘输入数据的时候,此时进程就处于阻塞状态。阻塞状态,顾名思义就是停在那儿不动了,就跟堵住了一样,而上面的现象不就停在那儿不动了吗?
那么处于阻塞状态的结构是什么样的呢?下面我用图来演示一下:
上面就列举了我们常见的一些硬件,每种硬件都有其对应的结构体来记录相关的各种属性,在这里面就有一个队列wait_queue,这个队列顾名思义存的就是需要等待的进程,只要在这个队列中进程就处于阻塞状态,那么具体是如何操作的呢?
回到上面的例子中,当程序运行到scanf函数时,此时系统需要用户从键盘输入数据,而在用户输入数据之前,因为此时进程的状态已经不再是运行状态,所以操作系统会将当前这个进程从runqueue队列中拿出来,放入到wait_queue队列中,后面的进程接着执行,同时等待用户输入数据。
而在用户输入数据后,操作系统就会检查相应硬件的wait_queue队列中有没有正在等待的进程,如果有,就会将其从wait_queue队列中拿出来,重新接到runqueue队列的尾部,当再次轮到这个进程时,就会接着执行后面的代码,所以就有了第二张图所显示的内容。 1.3挂起状态
那什么叫做挂起状态呢?下面我用一个场景来帮助大家更好地理解:
现在因为某些问题,OS发现自己的内存空间不够了,那么这个时候有一个问题:内存空间不足,后面的进程还要运行吗?
答案当然要运行,后面的进程有的是系统进程,它们不运行系统怎么顺利运行呢?那么此时该怎么办呢?
内存本身的大小是固定的,就那么多,那么剩下的就只有一种方式:将此时内存中的一些数据清出去,清出去就有空间了,那么该怎么清?难道直接将那些数据直接丢弃吗?
答案当然不是的,从上面的图我们也能看到,在我们的硬盘中有个swap的分区,这个区域的作用就是出现上述情况时,将一些代码和数据存入其中,并将内存中的代码和数据给清理掉,这样就能腾出空间了。那么系统会将哪些数据通过这种方式来进行交换呢?
比如说上面处于阻塞状态的进程,此时的进程没有被运行,那你的代码和数据存在内存中这不就是白白占了一部分空间吗?所以此时如果此时内存空间不足,系统就会将这类进程的代码和数据交换到swap分区中,等到这个进程要运行了,再重新把相应的代码和数据交换回来,也就是上图所示的swap in和swap out两种过程。
通过这种方式就相当于变相的将内存给扩大了,并且此时这种task_struct在内存中,但是代码和数据却在硬盘的swap分区中的进程就处于挂起状态 ,而上面的情况就处于最初那张进程状态图中的阻塞挂起状态。那么这个时候又有人问了:既然swap分区有这个作用,这个swap分区有多大?
swap分区一般就和我们内存的大小是一样的,比如内存是16G,那么swap分区也就是16G,但也有可能是内存的1.5倍或者2倍。
此时又有一个问题:那既然swap分区可以解决内存空间不足的情况,那是不是意味着swap分区越大越好?
答案当然不是的,使用任何功能都要付出相应的代价,我们可以想一下:为什么内存满了,电脑就开始变得卡顿了?
因为上面swap in和swap out的操作需要进行IO流操作,这是需要消耗时间的,也就是说swap分区存在的本质就是用时间来换空间,过度的swap,就会使系统变得卡顿。
如果swap很大,这就和你初入社会一样,此时没钱没资源,但是你一遇到困难,就有人给你钱,时间久了,你是不是就会对其产生依赖?
和这里的道理是一样的,swap分区很大,系统就会很依赖它,就会频繁的进行swap操作,那我们的电脑就会变得卡顿,这是我们想看到的吗?
所以swap分区不能设置的太大,我们要的只是在上面的极端情况下为内存腾出空间来实现进程的正常运行而已。那么又有人问了:要是通过上面的阻塞挂起还是不能解决问题呢?
那么此时就会拿运行状态的进程开刀,虽然进程处于运行状态,但是我们都知道,除去当前在cpu上运行的,其他的进程本质还是在后面等着的,那么就和上面的阻塞状态是一样的,所以同样会把处于运行状态的进程的代码和数据交换到swap分区中来腾出空间,此时的状态就称为运行挂起状态。
那要是还不能解决问题呢?
那么此时系统就会选择性的杀掉一些进程来腾出空间。
2.具体操作系统的状态,Linux
我们上面讲的各种进程状态适用于所有的操作系统,也就是和我们前面讲的PCB是一样的道理,下面以linux操作系统为例,我们来看看具体某个操作系统中都有哪些进程状态:
这是我截取linux源码中关于进程状态的一部分代码,可以看到,在linux中进程状态相较于我们最初展示的进程状态就较大区别,里面除了R(running)是我们所熟知的运行状态,其他的都不一样,并且每种状态后面都有其相对应的整型数据,这点我们在上一篇已经讲过,所谓状态在底层是实现中就是整型数据。
除了上面的R(running)不解释了以外,下面的各种状态我会一一为大家展示,看看在什么情况下会处于相应的状态。
2.1S(sleeping)状态
我们根据这个状态的名字也能猜出来这个状态的含义,就是休眠状态,下面我用一个实例来看看什么情况下会是S状态:
没错,和我们之前介绍阻塞状态的代码一样。此时我们可以看STAT这一列中,myprocess进程的状态就是S,这也间接说明了虽然在linux中没有阻塞状态,但是还是能找到相对应的状态,不过就是换了个名字而已。
上面是常见的S状态,下面我们来看个特殊一点的:
我们上面写了一个循环来获得当前进程的pid,但是我们通过观察当前进程的状态却是S而不是R,上面的代码不是一直在运行吗,为什么进程的状态不是R而是S呢?
相信大家心中都有这样的疑惑,下面我来解释一下:
我们通过循环来输出得到的pid值,这个过程是由cpu来完成的,这个过程是相当快的,但是我们要想知道pid是多少,就需要将输出的内容打印到显示器上,这里面需要IO流操作,是需要时间的,这个时间可比cpu处理我们写的代码要长多了。
这就导致比如cpu已经处理了10000次代码,也就是要在显示屏上显示10000次,但是因为IO操作消耗时间的原因,目前只能写出100行,这就到很多应该显示的数据只能在后面等着,也就是说整个过程大部分都是处于S状态,只有一小部分才处于R状态。
这就和我们在节假日去旅游一样,到了某个景区,人特别多,很多人都在排队等着买票进景区,因为卖票窗口就那么多,并且处理效率有限,这才导致我们需要排队买票,这和上面的道理是一样的,所以这里进程才会处于S状态。
讲到这里相信很多有人注意到了状态后面有一个“ + ”号,那么这代表什么意思呢?
这代表此时的进程属于前台进程,而没有了“ + ”号则处于后台进程,那什么是前台进程,什么又是后台进程呢?
上面我们启动了一个进程,并且可以通过ctrl+c的指令来终止进程,这就是前台程序的特征,当前台程序运行时,独占当前终端,并且可以与用户进行交互,这就是为什么我们可以通过ctrl+c来终止当前进程。
而后台进程与之相反,我们来看:
我们只需要在启动进程的指令后面加上&的符号,就可以将进程从前台切换为后台,此时我们就无法再通过ctrl+c的指令来终止进程,因为此时的进程处于后台,不会占用终端,也就无法与用户进行交互。
简而言之,前后台程序的区别就是前台程序可以接收用户的输入,而后台进程不接收用户的输入。
这点其实在我们日常生活中也很常见,我们的手机上一般都有微信吧,当我们打开微信时,我们就可以与别人聊天,交流。但是当我们退出微信后(不是删后台),此时的微信就处于后台,这点相信大家都能理解,我们就无法与别人聊天了,但是我们还能看到别人给自己发的消息。
这就与上面是一样的,虽然进程在后台,但是它依旧在运行,只是我们无法和其进行交互,所以它依旧可以打印出内容,我们也依旧可以看到别人给我们发的消息。
2.2D(disk sleep)
上面我们讲的是S(sleep)休眠状态,其实也称为可被中断休眠(浅度睡眠),我们上面也看到了可以通过ctrl+c来终止进程。与之相反的就是要讲的D状态,它虽然也是休眠状态,但是它是不可被中断休眠(深度睡眠),这也是linux特有的状态。下面用一个例子来理解为什么不可被中断:
现在内存中的进程要向硬盘中写入500mb的数据,此时需要等待硬盘的数据写完对吧,也就是在硬盘的数据写完之前,进程要等着,等硬盘好了告诉它,然后再进行下一步。那么好,此时内存空间不足了,系统要想办法清理出内存空间对吧,并且现在是非常极端的情况,也就是阻塞挂起和运行挂起都无法解决问题了,只能杀掉某些进程来腾出空间。
当系统扫到这个进程时,发现它什么也没干,于是系统直接就把这个进程给杀掉了。那么此时问题就出来了:等到硬盘写完数据后该怎么告诉进程我写完了呢?
答案是无法告诉的,进程都没了怎么告诉,那么此时磁盘写完数据了,没人告诉它下面该怎么办,得不到回应,然后磁盘就会将这部分数据给丢弃。
可能有人觉得不就是丢了500mb的数据吗?
那如果这部分数据是银行一天的转账记录呢???现在大家此时还觉得是500mb的问题吗?
是不是一下感觉问题就严重了,当然一些无关紧要的数据丢了就丢了,但是那些很重要的数据一旦丢了后果是很严重的,造成的损失是很大的,我们总不能去赌这部分数据不重要吧,并且在这个过程中系统,进程,硬盘都没有做错,做的都是份内工作。
所以只要是相关的进程,也就是和磁盘传输相关的进程是不能被中断的,这类进程被中断的后果上面的例子也能体现。
但是这种进程在这里我并不演示,感兴趣的可以让ai给你生成相关的操作指令来试验,为什么不演示呢?
因为这种行为有风险,一旦操作不当,就很容易直接把系统给整崩溃了,所以如果大家想试验,一定要谨慎操作!!!
2.3T(stopped)和t(tracing stop)
2.3.1T(stopped)
这两个进程的含义分别是暂停和追踪暂停,那么这两种进程状态是什么样的状态呢?我们下面来看一个例子:
当我们启动一个进程时,我们通过kill -19 + pid的指令就可以将一个进程直接暂停,并且暂停后可以看到在进程的下面会出现Stopped的字样,这就意味着当前的进程被暂停了,当进程被暂停了我们观察进程状态就可以看到此时的状态就变为T了。
而与之相对应的kill -18 + pid就可以将进程从暂停状态解除:
此时进程就会接着运行,上述是kill指令的相关操作,值得一提的还有一个kill -9 + pid,作用是杀掉一个进程,剩下的kill指令的相关操作感兴趣的可以去查一查,这里就不过多介绍了。
stop顾名思义就是把进程给暂停,是真的把进程给暂停了,这点我们要和上面的S给区分开,虽然看着而这较为相似,但是意义是不一样的。
2.3.2 t(tracing stop)
看到上面的追踪暂停相信大家都比较疑惑,什么叫追踪暂停呢?我们下面来看一个例子:
上面我通过cgdb指令来调试代码,并且在12行打了一个断点,并通过r指令使程序运行到12后停下,此时我们查看进程的状态俨然就变为了t,那么这到底代表着什么呢?我们接着往下看:
我们有没有想过为什么我们调试的时候,通过逐语句s或者逐过程n程序只会往下走一步,我们输入一次,代码才往下走一行?
这就是答案,t为什么叫做追踪暂停,针对其实就是调试的过程,当我们使用逐过程s或者逐语句n时,在一瞬间进程会取消暂停状态,然后当代码走到下一行时就会瞬间将其暂停住,这才做到了我们执行一下s或n,代码就向下走一行。
正因为有了追踪暂停,我们才能做到调试代码。
2.4X(dead)和Z(zombie)
这两个状态的含义就是死亡状态和僵尸状态,死亡状态好理解,进程被杀掉就是死亡状态,那什么叫做僵尸状态呢?下面我们通过一个例子来更好地理解僵尸状态:
早上天气好,你想着去跑跑步锻炼锻炼,当你跑到你一个岔路口时,一个大爷从你身边路过,走了没多远,突然人就倒下了,你下意识的赶紧打120和110。
随后120和110相机到达,120到后确认大爷已经死亡,那么此时会直接通知大爷的家属来办理后事吗?
不会吧,110首先会封锁现场,然后会请法医来确认大爷的死因,来判断大爷属于自然死亡还是他杀,也就是需要从大爷身上获取信息来判断,之后确定大爷属于自然死亡,然后才会通知大爷的家属来处理后事。
在上面的例子中,大爷死了没?
当然死了,在120到后确认已经死亡,但是死了并没有立即通知大爷的家属来处理后事,为什么?
因为110要从大爷身上提取信息来判断大爷是自然死亡还是他杀。
而在linux中同样也是如此,在杀掉一个进程时,不会立即回收它,而是提取进程的信息后,再由其父进程来进行回收。
但是正常的进程父进程都是bash,它会在很短的时间内提取完信息后进而将其回收,那么此时我们要想看到这个现象就要通过上一篇讲的fork函数来进行操作:
此时我们通过fork函数来产生父子进程,并且我们通过kill指令来杀掉子进程,但是并不让父进程来回收,那么此时我们查看父子进程的状态就会发现子进程就处于Z状态。
说了这么多,当一个进程处于Z状态会发生什么呢?
上面我们通过kill指令杀掉了子进程,但是上面我们也讲了,子进程虽然被杀掉了,但是父进程还需要从子进程身上提取信息,那从哪里提取呢?
当然是进程的task_struct,所以处于Z状态的进程,它本身的代码和数据已经没了,因为进程已经被杀掉了,只留下task_struct来让父进程来提取信息,和上面的挂起状态类似。
那么父进程要提取什么信息呢?
答案是退出信息,当父进程从子进程的task_struct提取到退出信息后,子进程才会被回收,进程状态也会变为X。
那么退出信息是什么呢?
我们学过C语言都知道main函数我们最后都要进行return 0的操作,而返回的0就是退出信息,而在进程中也会有一个信号值来表示退出信息,当父进程提取到这个信号值后,就会回收子进程。
而X状态我们是看不到的,因为当父进程回收子进程只有一瞬间,只有这个瞬间子进程的状态会变为X,然后就消失了,所以我们知道这个状态代表什么含义即可。
最后我们思考一个问题:如果父进程一直不回收子进程会怎么样?
那么子进程就永远处于Z状态,其task_struct就会一直存在于内存中,最终会造成什么后果相信大家都清楚:内存泄漏!!!
所以在实践中子进程必须要回收,这点我们都要注意。
以上就是进程(二)的全部内容。