进程的状态

  进程的状态是多种多样的,所以单独一篇博客来说明,本文有些新概念都在先前的博客中提及,在此不再展开论述。

一 操作系统中的状态介绍

 1 运行状态

        当调度器找到进程的pcb数据结构,可能就把地址传给了cpu,cpu通过pcb数据结构找到代码和数据去执行,这么看起来好像是要把一个进程的代码执行完毕才能到下一个进程,可是事实上如果我在电脑打着游戏,挂着聊天软件,好像打游戏的同时还能接受短信,奇怪,难道多个进程在同时跑?  不卖关子了,这就得重新提一个概念,时间片了,一个进程占用cpu资源的时间是由时间片决定的,当时间片归零了,就到下一个进程了,这个时间片可能是毫秒级别的,所以在一秒,这个进程可能已经被轮转十几次了,但是时间片如何耗尽,难道是个整型自己--,整型--例如10毫秒--,也算一个运算,这个运算被减为零的时间如何保证是十毫秒呢,这个点我也不好解释,反正和接来下的知识无关这个进程在被停止执行到下次执行也还是毫秒级别,我们几乎感觉不到,所以看起来像是在同时执行,其实是分别执行,再次回顾一个概念,这种一秒内多个进程都在跑的称之为并发。

进程的状态_第1张图片

        内存分为用户使用的和操作系统内核使用的两部分,上述的数据结构应该存在内核数据区,而代码和数据都在用户区,这个用户区分为堆栈,代码区,但是运行队列里是有许多进程的,所以当内存充足的时候,每个进程应该都把自己的数据和代码分别存在各个区之中,那cpu上有进程的什么数据呢,首先我们知道cpu上有许多寄存器,总存储不大,存的都是cpu高频访问的数据,而从先前的讲解中我们还知道cpu执行的进程的时候会把进程的从cpu拿下,放上的操作,这个操作叫进程切换,切换的时候一般不会把数据和代码写到磁盘中去,而是把cpu上的数据打包带走,这个数据称之为上下文数据,例如下一条代码的地址,所以放在离cpu最近的寄存器中,方便使用。 

2 阻塞状态

        例如当我们执行到了scanf语句,就会需要键盘输入资源,这个时候这个程序状态就不能是运行状态,可能当前进程就被设成了阻塞状态,谁设置的呢,可能是cpu吧,然后操作系统识别到你的状态不是运行状态,就把你从cpu的运行队列中拿下来了,因为后面的代码还不能执行,此时你的状态就不能是运行态,那是什么呢,我们称这种在等待特殊设备的软硬件资源的状态为阻塞状态。那去哪等呢,这又得回到操作系统了,我们知道软硬件资源是被系统管理起来的,当进程想要从硬件要数据,还得经过操作系统,而在操作系统内有着描述这些硬件的数据结构,操作系统通过管理这些数据结构来管理硬件,所以只要把进程的pcb数据结构和对应硬件的数据结构相连,如下图。

进程的状态_第2张图片

        pcb结构体和设备结构体的wait指针相连,这样只要操作系统通过dev发现键盘已经准备好了资源,就通过指针把数据写给进程。

 3 挂起状态

        先前提及了阻塞状态,但是系统中可能存着许多的进程都在阻塞状态,这个时候他们的代码和数据都在占用内存,如果此时内存资源紧张,那操作系统就会把等待的进程的代码和数据移到磁盘,此时进程就是一种挂起状态,这种挂起状态用户是无法感知的,就像你把钱存进银行,银行是不会告诉你,你的钱被借出去了,银行说你别管,反正你要用的时候我就帮你弄回来,你管我现在怎么处理你的代码和数据,反正你也不用,还不如给急用内存的进程,这样转辗腾挪之间可以高效地使用内存。

        为什么要有状态呢,或许是不同的状态,操作系统就要把它连入不同的数据结构中,起到对其管理的作用。运行态就到cpu的运行队列,阻塞状态就可能在硬件数据结构的等待队列中运行态,阻塞态操作系统没有具体定义,貌似在多种场景的等待都是阻塞状态,而对应实际的场景,为了区分不同的等待,所以有了新的状态定义,可能大佬一开始不想分的太细,就交给写系统的人自由发挥。

二 linux下的状态介绍

1 运行态R状态

        就和先前提过运行状态一致,只要在运行队列上,那就是R状态。

但是我们几乎很难看到程序的运行状态。

#include
int main()
{
    printf("begin\n");
    while(1);
    return 0;
}

while循环ps aj打印状态栏,诶,不难啊,这不是R起来了吗,还附赠一个加号(+后提)。

进程的状态_第3张图片

如果换成下面的代码。

#include
int main()
{
    printf("begin\n");
    while(1)
       printf("我是进程");
    return 0;
}

        诶,怎么是S呢?刚刚还在跑的啊?我的R状态呢? 其实原理不难,当时了解后我也恍然大悟,原来如此,首先我们就这么几行代码, 对于cpu来说处理速度是很快的,但是这个过程中我要printf,向外设要资源要从运行队列中拿下来,链入到硬件结构体的wait队列,进入阻塞状态,那向外设写数据等返回结果应该也是一种阻塞状态,而写数据到外设的速度比我们执行代码速度慢了太多,也就是说我们的这个程序起来的进程大多时候都在把数据写给外设,只有少部分时间是在cpu的运行队列,所以我们ps aj查进程状态的时候是很难捕捉到的。ps aj 查进程状态,自己肯定在跑了,自己查自己当然是R。

进程的状态_第4张图片

2 S状态(浅度休眠状态)

       当我们执行了scanf代码,我们发现此时进程就是在s状态,说明linux的S状态就是一种阻塞状态,因为此时进程就是在等硬件资源,那这个时候进程按照上文理解就是在硬件结构的等待队列中。

3 D状态(深度睡眠)

        S状态还可以接收外部的信号,例如kill,而D状态则是不会理会外部的任何信号,因为我们先前说休眠是在等软硬件资源,例如在IO的时候,需要数据,或者写入数据到磁盘,返回写入结果,等数据和写数据的时候,进程都是在等,在休眠,但是当内存资源紧张的时候,操作系统不仅会通过挂起来节约内存,甚至还会杀进程,这个时候如果在等IO结果的进程被干掉,此时磁盘读写又失败了,磁盘也不知道把数据返回给谁,那这数据咋办呢,一般是直接丢了,那你和女神的聊天记录在服务器的保存就没了。

4 T状态

        stop状态,什么时候会出现这种状态呢?不知道大家有没有想起gdb调试,gdb+可执行程序名即可开始调试,break+行号设置断点,当跑起来的时候触发断点,此时程序处于t状态,T和t暂时无法区分。

进程的状态_第5张图片

        stop还表示等待,为什么还要有sleep? 按照状态是为了区分不同场景下的进程这一点来看,一定是sleep状态不适合用于某些等待场景,所以增加了stop,就像向磁盘写入数据时是用深度睡眠D而不是sleep一样。

5 Z状态

        说到Z状态就得提到僵尸进程,我们前面说过父子进程,如果子进程退出了,父进程是要对子进程的返回结果进行读取,并且释放资源。如果父进程迟迟没有来读取数据,那这个时候子进程就还不能被清理, 也没有代码要执行,也不是等待某种数据的阻塞状态,就是静静等父进程回收自己,所以就有了一种特殊的状态。

    #include
  5 int main()
  6 {
  7    printf("begin\n");
 13    int ret = fork();
 14    if(ret==0)
 15    {
 16        int cnt = 5;
 17         while(cnt)
 18         {
 19             printf("我是子进程\n");
 20             cnt--;
 21             sleep(1);
 22         }                                                                            
 23    }
 24    else
 25    {
 26        while(1)
 27        {
 28            printf("我是父进程\n");
 29            sleep(1);
           }
       }
       return 0;
    }

结果如下图。

进程的状态_第6张图片

        那如果父进程先退出了,子进程此时直接没有父进程了,那怎么办。

    #include
  5 int main()
  6 {
  7    printf("begin\n");
 13    int ret = fork();
 14    if(ret==0)
 15    {
 16        
 17         while(1)
 18         {
 19             printf("我是子进程\n");
 20             sleep(1);
 21         }                                                                            
 22    }
 23    else
 24    {
           int cnt = 5;
 25        while(cnt)
 26        {               
 27            printf("我是父进程\n");
 28            sleep(1);
               cnt--;
           }
       }
       return 0;
    }

        这个时候我们从下面代码执行结果可以看见,子进程的父亲变了,变成操作系统了,那为什么不是bash呢,许多资料说bash无法回收孙子进程,可是这一点我还不好理解,或许等之后我自己写回收子进程的代码时,能体会的更深刻,进程的状态_第7张图片

  而且此时的状态变成S了,S表示后台程序,S+表示前台程序,当状态为S时,此时bash命令行可以输指令,但ctrl+c都结束不了进程,只能在另一个窗口用kill  -9+进程pid的方式才能结束进程(shell的另外一些使用小技巧,我打算后面实现shell的时候一起介绍)    

进程的状态_第8张图片

         那操作系统是如何释放内存的呢?是一种内核方式我们后面会提到一种东西叫页表,每个进程的页表记录着使用的内存,如果我直接干掉页表,我就能让你使用的内存给其他人用。

6 X状态

  这个不好复现,一般设为x很快进程就没了。

三 优先级

1 优先级的意义

        我们知道cpu的资源时有限的,而进程是可以有多个的,这意味多个进程都要用到cpu的资源,cpu一次只能为一个进程提供服务(具体原因我也不好解释),多个进程要用cpu就必然存在竞争关系,所以要给进程分个优先级,来告诉操作系统谁先执行,谁后执行。

进程的状态_第9张图片

  先来看看进程都有哪些状态参数。

uid:表示用户名

pid:进程代号

ppid:父进程代号

        PRI和NI就是接下来的重点,默认从80开始,PRI越小,优先级越高。而NI是nice值,是用于调整优先级,因为linux是允许进程抢先的,就是通过调整优先级让这个进程更早被执行,nice的范围是-20到19,而PRI每次从80开始,所以进程真正的优先级PRI+NI范围是60-99。

进程的状态_第10张图片

 2 如何按照优先级调度      

        先前说进程的R状态表示在运行队列,调度器遍历运行队列拿进程让cpu执行代码,可是我们好像忽略一点,那就是如何保证优先级高的进程先被调度? 我记得pcb对象内是存了优先级的,难道我是一个个扫描pcb数据结构,然后排序,再访问?那新来了个进程,又要排序?来看看优雅的Linux是如何实现的吧。

        进程的状态_第11张图片

        系统中所有的优先级有140种,而我们写的程序进程优先级一般也就是60-99,只能放对应数组的60-99的下标处,此时调度器只要从上往下遍历这个数组就可以了,这样就能保证优先级高的进程先执行。

        至于为什么会有个镜像的wait队列呢,你想想如果调度器从0遍历到100,来了个优先级是80,难道这个这个时候要放到上面那个run指针指向的数组吗,这难道是想让调度器每次找进程都要从头开始找吗,这是非常低效的以一般都放到wait指向的数组内,这个数组还放着那些时间片耗尽的进程,如果这些高优先级的进程重新放到run数组可能会一直被调用,其它普通进程将享受不到cpu资源。当run数组没有进程了,就用那个二级指针直接swap即可。

 3 大O(1)的调度算法       

        至于如何判断run数组内没有进程了,一种就是从头遍历到尾部了,到140位置说明进程都执行完了,但是这还不够高效。假如,只有140处有进程,那我前面的遍历不是很浪费时间,如何快速判断数组内有没有进程我们可以用位图,一个二进制位只能表示0和1,我们用0表示不在,1表示在,一个整型有32个二进制位,那五个整型就能表示140个下标下是否有进程(int arr[5]即可),那我怎么知道这个二进制位表示的是哪个优先级呢,我举个例子,例如优先级43,int posi=43/32, 结果为1,就说明43是在a[1]这个整型中,43%32说明是整型的哪个比特位,只要把该位置置为1表示在即可。

你可能感兴趣的:(Linux,linux)