前言
本篇文章是接着上一篇【linux】:进程概念的后续,对于有基础的同学可以直接看这篇文章,对于初学者来说强烈建议大家从上一篇的概念开始看起,上一篇主要解释了冯诺依曼体系以及操作系统的概念还有在linux系统中进程是什么样的,如何去查看一个进程,如何给一个进程多开一个子进程以及为什么fork()函数可以有两个返回值的问题。
一、进程状态
为了能更深刻的理解linux中的进程状态,我们把linux中的内核源代码拿出来看一下:
/* * The task state array is a strange "bitmap" of * reasons to sleep. Thus "running" is zero, and * you can test for combinations of others with * simple bit tests. */ static const char * const task_state_array[] = { "R (running)", /* 0 */ "S (sleeping)", /* 1 */ "D (disk sleep)", /* 2 */ "T (stopped)", /* 4 */ "t (tracing stop)", /* 8 */ "X (dead)", /* 16 */ "Z (zombie)", /* 32 */ };
task_struct是一个结构体,内部会包含各种属性,其中就包括状态,如下图:
我们先来讲解阻塞和挂起这两个重要的概念,阻塞就是进程因为等待某种资源就绪,而导致的一种不推进的状态。我们经常可以看到不管是手机还是电脑当打开的软件很多的时候,就有出现应用卡顿的情况,这是因为当我们打开很多的软件的时候进程也变多了,操作系统调度不过来了,这个时候卡的那个进程就是阻塞了。再比如说我们下载一个软件,下载了一半没网了,这个时候下载进度就不动了,这个时候这个进程就变成了阻塞状态,只有当网络好了能继续下载了CPU才会继续调度这个进程,所以这个进程卡住了是在等待某种资源就绪,当资源就绪了就会被CPU调度取消阻塞状态。所以进程要通过等待的方式,等具体的资源被别人用完之后,再被自己使用。那么进程等待某种资源就绪的过程中,资源指什么呢?这里的资源指软硬件资源,比如:磁盘,显卡,网卡等各种外设。下面我们用图解释一下阻塞的过程:
我们前面讲过操作系统对于软硬件的管理是先描述在组织,所以对于网卡磁盘等也是通过struct来描述的,当CPU正在跑一个进程的时候,这个进程突然没网了这个时候就将这个进程先变成阻塞状态,然后看下图:
这个时候因为网络的问题进程需要等待网络恢复才能继续在CPU上运行所以这个进程就会链接在网卡的尾部等待网卡资源就绪也就是网络恢复才可以正常运行。所以PCB是可以被维护在不同的队列中的。
阻塞:阻塞就是不被CPU调度。一定是因为当前进程需要等待某种资源就绪。一定是进程task_struct结构体需要在某种被OS管理的资源下排队。
下面我们解释一下挂起的概念:
上图是一个进程正在被CPU调度,然后突然没网了,看下图:
这个时候进程进入阻塞状态等待网卡设备就绪,由于内存中空间有限所以对于阻塞状态的进程的代码和数据来说无疑是浪费空间的,所以操作系统会先将阻塞状态的进程的代码和数据放入磁盘中,将内存中的代码和数据释放掉。
等过了一段时间,网卡设备就绪了,这个时候进程会继续被CPU调度,在这之前需要把磁盘中的代码和数据继续放入内存中
以上的将代码和数据先放入磁盘等待网卡设备就绪然后就绪后再将磁盘中的代码和数据放入内存的过程就叫做挂起。
在这里问一个问题,进程是R状态一定是在CPU上运行吗?答案是不一定。进程一般是什么状态,要看这个进程在哪里排队,看下图:
首先我们创建一个.c文件然后写一个死循环代码,然后创建Makefile文件:
接下来我们直接运行并且查看当前进程状态:
我们先用ps axj | head -n1指令调出进程属性,然后后面加上grep mytest过滤出mytest可执行程序的进程,后面加的grep -v grep是过滤掉grep自己本身的进程,最后成功的显示出这个进程,我们可以很清楚的看到这个正在运行的程序的状态并不是R而是S+,S代表休眠状态,接下来我们将打印注释掉试一下是什么状态:
我们可以看到当将打印代码注释掉后这个进程的状态变成了R,这个时候为什么是运行状态了呢?因为printf打印需要打印到屏幕上,而屏幕就是一种外设,频繁的往屏幕打印进程会等待屏幕就绪才可以打印,而CPU的运算非常快外设的速度却很慢,所以CPU早就跑完了代码接下来将进程状态设为阻塞状态让这个进程去屏幕后面等待屏幕就绪当屏幕就绪后又会重新被CPU调度然后执行重复的操作。
所以进程是R状态不代表进程在运行,而代表该进程在运行队列中排队。
S状态是休眠状态,可中断休眠。而S状态就是一种阻塞状态。
接下里我们演示一下,首先修改一下代码:
这个时候我们去看进程状态。
为什么是S状态呢?因为进程会等待键盘资源就绪,也就是说只有键盘输入了才叫键盘资源就绪这个时候才会被CPU调度
当我们终止程序后程序就结束了这个进程也结束了,对应了S状态是可中断休眠状态。
D状态也是一种休眠状态,D状态是不可休眠状态。D状态在生活中我们基本不会遇到,就是磁盘基本快满了还在往磁盘存数据,这个时候你就会发现你的电脑非常卡而且不能强制进行任何关闭操作,如果关电源会导致资料丢失所以不可中断只能等磁盘自己恢复。
T状态叫暂停状态,下面我们来演示一下T状态:
我们先讲代码修改一下方便演示:
运行后我们可以看到这个时候的进程状态为S,这个时候我们使用一个暂停命令:
19号这个命令sigstop为暂停然后我们使用一下:
这个时候我们能很清楚的看到进程变为T状态了并且程序确实暂停了,这个时候我们如何恢复运行呢?
这里的18号代码为sigle continue的简写就是继续的意思
这个时候程序就又可以继续了。但是我们发现这个时候我们用ctrl+c关不掉了,并且进程状态也变了。
从原来的S+变成了S为什么就不能终止程序了呢,因为状态后面带+号是代表程序在前台运行,在前台运行的程序可以ctrl + c结束,没有+就变成了在后台运行,这个时候我们只能通过kill命令杀掉这个进程。
这个时候程序就结束了,不管是前台或后台我们都可以用kill杀掉。
X状态被称为死亡状态,Z状态称为僵尸状态,下面我们解释一下这两个状态:
为什么我们要创建进程呢?因为我们需要进程为我们做事,我们写过C语言代码,知道每个main函数没必须返回0,这是因为我们要知道函数的结果,如果没有返回值我们就无法确定一个函数是否允许,所以我们需要进程的返回值来确定进程的状态。那么如果一个进程退出了立马变成X状态,作为这个进程的父进程,有没有机会拿到结果呢?linux当进程退出的时候,一般进程不会立即彻底退出,而是要维持一个状态叫Z,也叫作僵尸状态,方便后续父进程(OS)读取该子进程退出的退出结果。那么如何看到僵尸状态呢?子进程退出,但是不要回收子进程。下面我们演示一下:
这个时候两个进程都处于S状态,根据我们刚刚所说的只要让子进程先退出就能看到僵尸状态。
我们可以清楚的看到,当我们将子进程杀掉后子进程的状态变成了Z也就是僵尸状态,在mytest后面的单词<defunct>这个单词就有死人的意思,那么僵尸状态在这会占用资源吗?答案是会的,维持僵尸状态会占用资源,僵尸状态必须释放,如果这样的僵尸状态很多那么机器就很容易卡死。而维护僵尸状态的意义就是让父进程能够读取子进程退出的信息。
僵尸状态的危害:
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护。那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间! 内存泄漏?是的
刚刚我们看到子进程先退就变成僵尸了,下面我们看看父进程先退会怎么样。
我们将代码修改一下方便观察,下面我们用命令观察一下:
我们先用shell编程每隔1秒监测一下进程。然后将进程跑起来:
'
我们可以看到一开始有两个进程,当父进程结束后只剩下了子进程,这个时候我们发现子进程的状态从S+变成S了,也就是说从前台变成后台了。
我们可以看到这个时候已经不能用ctrl+c终止程序了,只能用kill杀掉子进程。那么为什么父进程先退出的时候没有变成僵尸呢?这是因为这个先退出的父进程被他的父进程回收了,他的父进程就是bash。怎么证明呢?在上图中我们发现pid为15529的进程的父进程一开始为15528,当15528退出的时候,15529这个子进程又重新给自己找了个爹pid为1,pid为1的进程我们都知道,这就是操作系统,也就是说,父进程退出,子进程会被OS自动领养(通过让1号进程成为新的父进程),那么这个被领养的进程就被称为孤儿进程。那么为什么我们上面演示子进程退出的时候子进程变成僵尸状态了呢?因为上面我们为了演示出僵尸状态故意没有将代码写完,因为没有等待,所以子进程变成了僵尸状态。那么为什么子进程会被自动领养呢?因为如果不领养就导致没人能找到子进程,一旦子进程退出就没人回收这个进程了,那么这个子进程就是一种游离状态,这样就会造成资源浪费,也就是内存泄漏。
进程的优先级
cpu资源分配的先后顺序,就是指进程的优先权。优先权高的进程有优先执行权力,配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
优先级和权限有什么区别呢?答案是权限代表了能不能的问题,而优先级是你执行的先后顺序,优先级已经确定了你可以干某件事只是取决于先后问题。那么为什么会有优先级呢?因为CPU的资源有限。
我们可以用ps -l命令查看进程中的优先级,如下:
在上面的图片中,PRI代表程序的优先级,NI代表进程优先级的修正数据。PRI的值越小进程的优先级越高,而NI值可以理解为是改变PRI的值从而修改进程的优先值。PRI(新) = PRI(旧)+NI
而在linux系统中,旧的PRI值一定为80。下面来演示一下:
我们先随便写一个死循环程序,然后运行起来。
可以看到程序已经跑起来了,这个时候用top命令去修改优先级。top进入后输入R,R就是renice的意思,然后输入pid
接下里让我们输入nice值,我们就调整为-20
我们可以看到确实成功修改了这个时候我们再修改为100
为什么是不是180呢?因为我们优先级的调整范围是-20到19这个级别,也就是说最小是-20,最大是19。 进程的优先级在我们平时使用中都不会调整,一般都会使用默认的优先级,大家知道该怎么修改就可以了。