进程的状态是多种多样的,所以单独一篇博客来说明,本文有些新概念都在先前的博客中提及,在此不再展开论述。
一 操作系统中的状态介绍
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表示在即可。