赞
踩
前言:博主在之前的文章已经讲解了PCB里面的pid——主要讲解了父子进程PID, 以及fork的相关内容。 本节进入PCB的下一个成员——状态, 状态是用来表示一个进程在内存中的状态的, 进程在内存中肯能处于各种状态, 比如运行, 阻塞等等。本节将从一般的操作系统的传统概念开始讲起, 然后再讲解linux是如何维护状态的。
ps:本节内容不好理解——linux的知识点都不太好理解, 建议了解进程的PCB的友友们进行观看。
对于进程的状态, 我们首先要知道为什么要有进程的状态。就比如我们运行一个程序, 这个程序里面有一个scanf, 需要运行输入, 但是如果当需要输入到时候我们没有输入, 一直不管这个程序。 那么这个程序是不是就会一直在那里停着。 ——这个时候程序在运行吗?答案是一定没有运行。 程序这个时候没有被cpu计算, 所以就不算是运行状态, 而是其他等待或者闲置的状态。
也就是说, 一个进程就要有各种各样的状态!
比如说在操作系统当中, 系统刚把我们的进程创建出来, 也就是PCB, 代码和数据创建出来。 这个状态就叫做新建状态。 不用的时候, 这个时候代码和数据已经执行完了, 这个就叫做终止状态。而正在被调度, 运行的程序就叫做运行状态。
现在看下面一张图:
我们知道, 操作系统对于进程的管理其实就是对于数据结构的管理, 只要拿到双链表里面的head。 那么就能对任何一个进程进行相应的增删查改运算。
但是, 我们要知道, 对于一个进程来说, 想要运行, 那么就需要加载到cpu中, 那么就要占用cpu的空间。而cpu说白了, 空间大小的量级是寄存器级别, 并且进程是非常多, 那么这些进程之间就势必是竞争的关系。
而为什么这些进程能够稳定有序的运行, 是因为cpu维护着一个runqueue,这个runqueue就叫运行队列。
- struct runqueue
- {
- //运行队列
- struct task_struct* head;
- struct task_struct* tail;
- ……;
- }
运行队列里面有着各种各样的属性, 但是最重要的就是上面两个维护链表首尾的指针。
这两个指针的目的就是为了将task_struct链入队列之中, 更加方便维护我们的链表!所以, 这个队列维护的不是进程的代码和数据, 而是task_struct, 这个就是进程在cpu里面排队, 这个队列就叫做运行队列。
而调度器就是拿运行队列里面的进程, 进行相应的调度。
所有处于这个运行队列的进程就叫做运行态, 简称R状态。为什么说在运行队列里面的就是运行态?因为在运行队列里面的进程, 随时可以被cpu调度, 这些进程就相当于说——我已经准备好了!可以随时被调用!
一个运行中的程序, 如果被cpu执行了, 是不是必须要执行完毕才能把自己从cpu上放下来?——不是!
就比如我现在正在使用写这篇博客, 但是我可以直接将页面叉掉。 这个就相当于没有执行完就把程序从cpu上放下来了!
而cpu之所以能够让每一个进程不必执行完才下来, 就有了一个时间片的概念——所谓的时间片, 其实就是int time_space之类的一个整形的数据。 这里面保存了一个时间, 比如说10ms, 那么对于一个进程来说, 他每一次被调度到cpu中执行, 都只会运行10ms, 然后就会放到运行队列的尾部等待下一次的调度。 也就是说, 因为时间片很短, 所以对于1s来说, cpu就可以调度很多次进程。 给我们的感觉就是每一个进程都在被调用!!!就比如我们有5个进程, 然后1ms, 就可以每个进程都被调用20次, 所以就给了我们这5个进程都在被执行的假象。
上面的1s其实就是一个时间段, 所以我们通常将这种在一个时间段内, 所有进程都会被执行的现象叫做并发执行!
上面的操作本质上就是把进程从cpu上拿下来, 或者把进程从运行队列拿到cpu上面去。 这种操作, 就叫做进程切换。
什么是阻塞, 首先, 我们回顾一下操作系统, 下面是一个操作系统。
上面是一个进程正在等待的情况,具体情况读图 。未来我们可能有各种各样的数据, 这些数据可能不止关系到键盘, 而是各种硬件设备, 那么这个时候的情况就是这些硬件的结构体都拿出一个指针连接到这些PCB的上面,然后这些PCB再相互连接。 也就是说, 管理的硬件数据结构里面的head其实就是一个个waitqueue, 当一个设备没有就绪的时候就等待。 就绪了然后就被调度放到cpu中执行。
并且, 我们把这种在等待队列中等待设备就绪的过程——这个过程就是假如设备里没有数据。 那么这个进程就在等待,而当设备里有数据, 那么我们就可以直接把进程放到运行队列里的cpu中执行, 再读取设备中的数据——而其中等待的过程就称之为该进程处于阻塞状态!!
假如这个时候有两个设备, 一个是键盘, 一个是磁盘。 这个时候我们有一个进程正在处于阻塞状态!——键盘等待队列等待键盘输入。 但是这个时候如果再来了几个进程, 而这个时候操作系统的内存严重不足了!这个时候, 操作系统就要在保持正常的情况下, 省出来内存资源。 而我们知道一个进程如果在阻塞状态, 那么它的代码和数据就是处于一个空闲的状态。 这个时候操作系统就会将进程的PCB保留, 而进程的代码和数据就会被放到外设当中!
当下次资源就绪的时候,进程就会被重新唤醒。 那么这个时候代码和数据再次从磁盘放到内存中!而这个过程就叫做换出和换入。 而换出后, 也就是代码和数据在磁盘时, 就叫做挂起状态!!!(这种挂起称为阻塞挂起)
linux下的状态的结构体是这样的:
- 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 */
- };
接下来, 将一个一个对结构体里面的状态进行解释。
首先, 我们想要查看R状态, 要如何查看呢?
我在这里先定义一个函数:
上面的这个程序运行起来后, 会一直无限打印printf。 但是当我们使用ps加过滤的时候, 看到的进程状态其实是S, 而不是R, S代表睡眠, 相当于阻塞状态。 为什么会这样呢? 因为运行一个程序, 当我们打印的时候, 我们是向外设打印, 而外设是需要等待资源的。 所以我们的程序就需要很长的等待时间, 也就是下面的S, 睡眠状态。
但是, 如果我们将printf去掉, 那么就不用打印到外设上, 就不需要等待。 我们再查看进程状态, 进程的状态就是R运行了。
这里的运行状态R之后有一个加号, 这个加号代表前台运行, 也就是在命令行解释器运行。 这个时候我们如果再运行其他指令, 那么也是运行不了的。
就像如图这样, 无论是什么指令都是没有办法运行的。 想要退出程序就是cral + C, 如果去掉加号就是R, 这个意思是后台运行。 这个运行不影响bash命令行。 用法就是./程序 &
想要将这个程序退出就只能使用kill -9杀掉程序。
我们都知道S状态就相当于阻塞状态, 那么如何理解, S状态相当于阻塞状态呢?
首先我们知道, 阻塞状态其实就是我们的进程等待外设获取资源的时候, 这个过程进程是闲置的状态。 这个过程是阻塞。 那么请问, 我们将数据输入到外设的时候, 算不算外设等待输入的时候。 要知道, cpu的时间和外设的时间差距是非常大的。 cpu的时间量级是纳秒级别, 而外设的时间量级是毫秒级别, 两者相差了10的三次方。 所以当输入的时候, 消耗的时间对于CPU来说也是非常大的。 所以这个时间也算是等待输入, 那么回到我们上面的那个打印printf的程序中, 为什么程序明明在运行, 状态却是S呢? 本质的原因就是cpu计算太快了, 而打印太慢了, 导致这个我们查看程序状态的时候一直是S也就是阻塞状态。
所以, 归根结底, 向外设打印信息, 从外部读取数据, 归根结底没有区别, 对于CPU来说开销都很大, 都会使程序变成S状态。
D状态, 即disk Sleep, 磁盘睡眠也叫做深度睡眠。 也是一种阻塞状态。 那么, 既然它叫做深度睡眠, 是不是还有浅度睡眠呢? ——是的, S就叫做浅度睡眠。 那么我们如何理解深度睡眠呢? ——深度睡眠其实就是一种不响应操作系统任何请求的状态。 一般的阻塞状态, 当进程停止等待外设后就会进入运行状态, 但是深度睡眠只有当完成了特定的任务, 否则不会响应操作系统的任何命令, 即便系统关机了, 这个进程仍然会自己跑。
D状态状态的产生是因为向磁盘中写入数据:
加入这个进程是一个有着很重要数据的进程, 某一天, 这个数据想要将1G数据写到磁盘中。
但是对于磁盘来说, 磁盘写入数据是有可能失败的。 虽然几率不大, 但是有可能使其他原因, 或者磁盘满了的原因导致写入失败。 所以对于进程来说, 他写入数据就不能将数据交给磁盘后不管了, 他要等待磁盘写入的结果。 如果没有写入成功, 可以再写入一次, 或者写入不成功后想其他办法处理数据。
那么进程就将数据交给磁盘了。 要知道, 内存的时间量级和磁盘的时间量级是相差了10的三次方, 差距很大。 所以, 对于进程来说, 这个等待时间是非常长的, 那么这个时候内存满了, 操作系统管理内存, 就势必要杀掉一部分进程——这个过程是必须等, 事实上, 我们使用电脑或者手机都遇到过打开一个程序, 然后闪退的情况, 这个的本质就是被进程给杀掉了。 这个时候这个进程因为等待磁盘, 会处于闲置的状态, 那么就容易被操作系统杀掉。 而被操作系统杀掉后, 磁盘就不会找不到进程, 那么他就不会在进程写入数据了。 这些数据丢了, 而如果数据很重要, 那么丢掉数据就会出现非常严重的后果。 所以, 为了避免这种情况, 就出现了深度睡眠——D状态。
进程在交给磁盘写入数据时是不受操作系统管理的, 就是进入了深度睡眠, D状态。 相当于有一个免死金牌, 操作系统想要杀掉进程的时候, 这个进程就可以亮出免死金牌。 就可以不受操作系统的约束, 即便操作系统关机, 只要电源存在, 就可以一直进行写入。
但是一般深度睡眠状态, 也就是进程向磁盘写入数据都是很短暂的。 如果时间很长, 以至于用户可以体会到的话, 这个时候操作系统基本就挂掉了。
对于T和t状态, 都叫做暂停状态。 我们可以使用kill -l查看暂停的指令。
如上图:19是暂停业务, 18是继续业务。 使用如下:
19是暂停进程, 如下图:
此时的运行状态时T, 如下, 就代表暂停(加号代表前台)
18是继续如下图:
此时的进程状态是S, 睡眠:
dead状态就是一般意义上的死亡状态。 对于dead来说,dead状态就相当于死掉了。 然后它的资源就会被回收。
其实, 在一个进程被销毁之前, 它要先经历一个只占用资源, 不产生价值的状态, 这个状态就叫做僵尸状态。 那么, 我们为什么要有僵尸状态呢?
那么我们可以利用一个故事来理解, 假如有一个员工,这个员工还是一个程序员, 他维护着一个对于公司来说很重要业务。但是呢, 有几天, 这个程序员非常心不在焉, 有可能是失恋了, 或者其他别的原因。 就导致这个业务, 他这几天疏忽了, 结果呢, 恰好这个业务这几天出问题了, 但是他没有发现。虽然现在这个业务造成的损失还没有计算出来, 但是这个时候他的上司知道了, 它的上司就把他叫到办公室, 说了他一顿, 最后让他回去, 说看一下业务造成的损失, 来定他的处置。 这个时候, 是不是这个员工就是处于一个被他的上司检查的状态, 如果这个业务损失的大, 那么他大概率是会被裁掉的, 反之就能继续工作下去。——这个被检测的过程呢, 就叫做僵尸进程
那么, 后来呢, 结果下来了, 损失很大, 这个员工最后被裁掉了。——这个结果呢, 是不是说明over了? 这个就叫做dead进程。
以上这个故事就是说, 对于一个进程来说, 它在退出的时候, 一定要维持一段时间的僵尸进程, 而只有当父进程或则其他关系进程子进程的资源将这个进程的情况读取到了, 才会去释放这个子进程的资源。 也就是说, 对于僵尸进程来说, 如果父进程一直不去释放它, 那么它就会一直占用着资源。 也就导致了内存泄漏!
其实, 知道了上面的知识点之后, 我们就来看下面这几张图片, 我们定义下面这个程序:
然后运行后, 如下图:
看到这张图, 我们可以看一下左边的图就可以发现, 当父进程结束后, 子进程并不会结束, 并且它的父进程的ppid变成了1, 也就是说, 父进程结束后, 子进程的父进程变了。
我们现在看一下, 谁的pid是1:
使用top命令就可以发现,1号进程其实就是操作系统。
这里就有一个结论, 对于一对父子进程来说, 父进程如果被终止, 子进程仍然会运行。 但是子进程的父进程会变成操作系统本身。 ——这个过程就叫做托孤。 而这个子进程或者说被托孤给操作系统的这些进程, 就叫做孤儿进程。 而之所以要进行托孤操作, 是因为对于一个进程来说, 他想要被释放, 就需要父进程。 而子进程本身是一个进程,它也需要被释放, 否则就会发生内存泄漏, 所以就需要被托孤。
下面是本节内容的笔记:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。