进程的状态

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

一 操作系统中的状态介绍

1 运行状态

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

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

2 阻塞状态

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

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

3 挂起状态

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

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

二 linux下的状态介绍

1 运行态R状态

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

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

bash 复制代码
#include<stdio.h>
int main()
{
    printf("begin\n");
    while(1);
    return 0;
}

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

如果换成下面的代码。

bash 复制代码
#include<stdio.h>
int main()
{
    printf("begin\n");
    while(1)
       printf("我是进程");
    return 0;
}

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

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

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

3 D状态(深度睡眠)

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

4 T状态

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

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

5 Z状态

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

bash 复制代码
    #include<stdio.h>
  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;
    }

结果如下图。

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

cpp 复制代码
    #include<stdio.h>
  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无法回收孙子进程,可是这一点我还不好理解,或许等之后我自己写回收子进程的代码时,能体会的更深刻,

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

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

6 X状态

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

三 优先级

1 优先级的意义

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

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

uid:表示用户名

pid:进程代号

ppid:父进程代号

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

2 如何按照优先级调度

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

系统中所有的优先级有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表示在即可。

相关推荐
YRr YRr34 分钟前
解决Ubuntu 20.04上编译OpenCV 3.2时遇到的stdlib.h缺失错误
linux·opencv·ubuntu
认真学习的小雅兰.36 分钟前
如何在Ubuntu上利用Docker和Cpolar实现Excalidraw公网访问高效绘图——“cpolar内网穿透”
linux·ubuntu·docker
zhou周大哥1 小时前
linux 安装 ffmpeg 视频转换
linux·运维·服务器
不想起昵称9291 小时前
Linux SHELL脚本中的变量与运算
linux
the丶only2 小时前
单点登录平台Casdoor搭建与使用,集成gitlab同步创建删除账号
linux·运维·服务器·docker·gitlab
枫叶红花3 小时前
【Linux系统编程】:信号(2)——信号的产生
linux·运维·服务器
_微风轻起3 小时前
linux下网络编程socket&select&epoll的底层实现原理
linux·网络
WANGWUSAN663 小时前
Python高频写法总结!
java·linux·开发语言·数据库·经验分享·python·编程
Stark、4 小时前
【Linux】文件IO--fcntl/lseek/阻塞与非阻塞/文件偏移
linux·运维·服务器·c语言·后端