进程状态是PCB中定义的一个字段,具体到LInux操作系统,就是task_struct结构体中的一个变量,所谓的状态变化,本质就是修改整型变量。例如:
#define NEW 1
#define RUNNING 2
#define BLOCK 3
……
int status = 2;//运行状态
以上是在网上找的图,这些图基本都是教材上的,然而你会发现,不同教材进程的划分方式不一样,到底该听谁的呢?
其实不同的操作系统的状态分类是不一样的,但是原理都是大差不差,所以我们要做的就是学号原理。接下来我说的三种状态就是操作系统的原理层面,讲解完后我会再结合具体的LInux操作系统来分析,最终你会发现L虽然Inux操作系统的状态有那么多种,但都是从这三种上面衍生过来的。
在操作系统内部,每个CPU都对应着一个运行队列,CPU和这个运行队列相关联,CPU要调度进程时直接到运行队列中获取
只要在运行队列中的进程 ,状态都是运行状态。也就是说运行状态不一定是真正意义上的运行,而是说这个进程已经准备好了,可以随时被调度。
我们写的代码中,一定会或多或少地访问系统中的某些资源,比如磁盘,键盘,显示器等硬件设备。
加入代码中有一段使用scanf从键盘读取数据,代码跑到这就停了下来,等待用户输入数据。但我就是不输入,键盘上的数据没有就绪,不具备访问条件,进程的代码就无法向后执行。
进程要访问的资源没有就绪,操作系统要不要知道呢?当然要知道,不只要知道,所谓的不具备访问条件,就是操作系统告诉你的。操作系统不仅仅要进行进程管理,还有驱动管理来管理硬件。
和进程管理类似,要想对硬件进行管理,就得先描述再组织,所以每一个硬件设备也有对应的结构体来描述它的属性。所以硬件的资源有没有准备好,操作系统是一清二楚的。
操作系统是这样做的:当硬件资源没有就绪时,将进程的PCB从运行队列中移除,放到硬件设备的等待队列中,并将进程状态设为阻塞状态。当硬件资源就绪时,操作系统也是最先知道的,这时再将进程的PCB从等待队列移到运行队列,进程状态更改为运行状态,随时准备被调度。
没错,硬件也有自己的队列,事实上,操作系统中存在非常多的队列:CPU的运行队列,等待硬件设备的等待队列
这里我们得出一个重要结论:进程状态变化的本质就是:
当一个进程阻塞了,我们看到了什么现象?为什么?
我们会看到进程卡住了,因为PCB不在运行队列中,进程不是运行状态,不会被CPU调度。
生活中,当我们执行多个下载任务时就会很卡,这是因为很多个进程都要向网卡获取数据,网卡忙到停不下来,很多进程大多数时候都处于阻塞状态。
补充说明:一个进程的PCB可同时在多个链表中
我们写的链表,next结点只有一个,因为结构体里的next指针只有一个,但是PCB在实现时,将prev,next这些链接字段又用了一层结构体封装了,所以一个PCB里可以有多个prev,next指针。
当一个进程阻塞了,注定了这个进程在它所等待的资源就绪之前,该进程是无法被调度的。如果此时,操作系统内的内存资源已经严重不足了,怎么办呢?操作系统是如何解决的?
当操作系统内存资源严重不足时,操作系统会将某些处于阻塞状态的进程的代码和数据置换到磁盘中,以腾出更多空间来使用,这时进程所处的状态就叫做阻塞挂起。当进程被调度,曾经被置换出去的进程代码和数据,又要被重新加载进来
问题一:这样不会变慢吗?
肯定是会变慢的,但这是操作系统生死存亡的时候啊,慢些又算得了什么呢?
问题二:数据换到哪里了?
换到了磁盘上的swap分区。建议不要把swap分区设置得太大,因为空间太大了,操作系统会更加依赖这种挂起进程获得空间的方式,频繁地和磁盘交换数据会降低效率。swap分区设置成和内存大小一样即可。
以下内容来自Linux某个版本的源代码:
- static const char * const task_state_array[] =
- {
- "R (running)",
- "S (sleeping)",
- "D (disk sleep)",
- "T (stopped)",
- "t (tracing stop)",
- "X (dead)",
- "Z (zombie)",
- };
写一段平平无奇的helloworld,运行后打开另一个窗口,用ps命令查看一下
- 1 #include
- 2
- 3 int main()
- 4 {
- 5 while (1)
- 6 {
- 7 printf("hello world\n");
- 8 }
- 9 return 0;
- 10 }
while :; do ps ajx | head -1 && ps ajx | grep code |grep -v "vim" | grep -v "grep" ; sleep 1; echo "###########################################"; done
用这样一段Shell脚本来监控进程,也就是一秒钟打印一下信息。
然后发生了戏剧性的一幕,左边code进程在疯狂刷屏,右边进程的stat怎么不是R呢?这个S+是个什么鬼?
S是休眠状态,就是一种特殊的阻塞状态。因为printf操作是在访问显示器外设,在执行IO操作,IO操作和CPU处理处理速度相比是很慢的,所以这个进程大多数时间是在阻塞状态等待显示器就绪的。而监控脚本一秒钟打印一次,所以检测到运行状态的概率很小。
- 1 #include
- 2
- 3 int main()
- 4 {
- 5 while (1)
- 6 {
- 7 }
- 8 return 0;
- 9 }
当把printf去掉,再次运行代码,监控进程,得到的结果就是这样的了:
为什么是S理解了,但后面加号是什么呢?+表示这个进程是前台进程。这种进程一旦启动就会导致命令行解释器无法接收命令,并且可以Ctrl + C终止的,就叫做前台进程。要想一个进程在后台运行,只需启动时加上&。后台进程运行时,我们仍然可以在命令行中输入命令,并且这种进程不能用Ctrl + C终止,但是可以用kill命令杀死
./code &
前面讲的阻塞状态是一种统称,具体到Linux系统,S睡眠状态就是所说的阻塞状态。S状态是一种浅度睡眠,会对外部信号做出反应,可以被kill掉。与浅度睡眠相对的还有深度睡眠
D休眠状态也是阻塞状态,它是一种深度睡眠,是专门针对磁盘来设计的。D状态是为了防止进程向磁盘写入关键数据时,该进程被操作系统杀掉而导致数据丢失。深度睡眠的进程不可被杀掉,即使是操作系统也不行。只有这个进程完成了写入工作自动醒来,除此之外没有任何办法,关机都关不了。
一般而言,D状态我们是查不到的。如果你查到了D状态,说明你的磁盘已经非常忙了,计算机快要挂掉了。
kill不仅可以杀死一个进程,也可以把一个进程暂停,使其进入T停止状态。
kill -l
如图可以使用kill指令给进程发送各种信号,前面是编号,后面是名称。这些信号其实是操作系统中的宏定义。其中19号信号就能使进程stop。
kill -SIGSTOP [pid]
kill -19 [pid]
以上两个指令都能暂停一个进程,但这个进程并没有被杀死,如果它是前台进程,就会将它移到后台。
kill -SIGCON [pid]
这个指令可以解除进程的暂停状态,使其继续运行。
S状态存在的意义是什么呢?
在进程访问软件资源的时候,操作系统可能暂时不让进程访问,就将进程设为T停止状态
T状态和D状态一样,用户基本是见不到的。
T和t可以基本看成是同一个状态,T不常见,当t比较常见。
例如用gdb来debug程序时,追踪程序,遇到断点,进程就暂停了,此时就处于t状态。
其实T/t状态也是一种阻塞状态,进程debug时停下来,其实是在等待gdb给它下一步的指令,是在等待gdb这个软件(进程)的资源。当我们输入n给gdb,gdb就会给进程发送kill SIGCON指令,软件资源就绪,进程又会继续运行。
所以说等待队列不只是CPU和各种硬件设备拥有,进程PCB也是可以有自己的等待队列的!进程也可以等待另一个进程
当一个进程结束时就处于X状态,这是一个瞬时状态,用户很难查到
我们创建一个进程的目的是什么?是为了完成某项任务。那么一个进程在退出时,是不是理应告诉我任务完成的情况如何呢?是这样的。
当一个进程退出后,它的退出信息会被操作系统写入到进程PCB中,进程的代码和数据会被立即释放,但是PCB仍然被操作系统维护。只有父进程读取了退出信息,得知进程的退出原因,PCB才会被释放。像这种进程已经退出,但是退出信息未被父进程读取,PCB一直存在的进程就叫做僵尸进程,所处的状态就做僵尸状态。当退出信息被读取之后,操作系统会先将PCB设为X状态,然后释放PCB。
僵尸进程的危害是什么?
一个进程已经退出,但是它的父进程就是不读取退出信息,PCB就会一直存在,占用内存空间,造成内存泄漏。
当一个进程的父进程退出时,他就变成了孤儿进程。孤儿进程必须要被领养,否则他的退出信息无法被读取,会一直处于僵尸状态,造成内存泄漏。孤儿继承统一被一号进程systemd领养,1号进程就是操作系统