目录
一、传统操作系统进程状态
(一)什么是状态
(二)运行状态
(三)阻塞状态
(四)挂起状态
二、Linux进程状态
(一)进程状态
(二)查看进程状态
(1)僵尸进程
(2)孤儿进程
三、进程PCB如何管理链表
四、进程优先级
(一)查看系统进程
五、进程切换
(1)为什么nice值是【-20,19】
(2)为什么bitmap大小是5?
正文
一、传统操作系统进程状态
下面所讲状态虽然标题是传统操作系统进程状态,但整体也会以Linux为例进行讲解
(一)什么是状态
【结论:状态就是一个数】
学操作系统时,相信大家都见过以下图片吧!
上图,操作系统中有那么多状态,所以状态到底是什么?
举个例子:我们也有多种状态,比如:学习状态、游戏状态、吃饭状态....这些状态决定我们当下在干什么。
同理,操作系统状态也决定进程接下来要做什么工作(运行进程为运行状态...)而系统进行状态的切换是通过#define定义如宏1:运行、宏2:结束、宏3.....的整型宏值进行切换,改变宏值就在改变状态。
(二)运行状态
【结论:并不是运行起来的进程才是运行状态,进程只要在CPU调度队列中就是运行状态】
根据冯诺依曼体系结构规定,操作系统资源本质就外设资源(IO密集型)和CPU资源(计算密集型),资源有限需求无限,必定存在资源竞争。
之前文章讲过:进程=PCB+代码数据
当多个进程同时申请资源运行时,其实CPU内部都有一个调度队列,把每个进程的PCB 放到调度队列中排队,随时等待CPU调度执行就叫做运行状态。
(三)阻塞状态
【结论:将进程PCB连入不同队列中,对应更改其状态属性】
众所周知,操作系统是管理软硬件资源的软件,而我们除了访问软件资源外也需要访问硬件资源(麦克风、键盘、磁盘....)当进程要访问硬件资源时同样也有一个硬件等待队列。
当CPU通过调度队列执行到某个进程时,发现它的硬件资源还没准备就绪(比如键盘还未就绪)这是将原本调度队列的PCB放入到等待队列中,等硬件准备好了再放回去调度队列中设置为运行状态这一行为称为阻塞运行状态。
所以进程状态变化本质是:在不同队列进行流动,也就是对链表的增删改。
(四)挂起状态
【内存空间严重不足,将内存中进程的代码和数据置换到磁盘的行为叫挂起状态】
分为阻塞挂起状态和就绪挂起状态。
阻塞挂起状态:(等硬件时内存空间不足数据被置换出去)
每个进程运行时都要把PCB、代码和数据加载到内存中。当同时调用外设,外设没准备好,此时PCB到硬件等待队列中去排队,而进程的代码和数据还在内存中。内存的空间是有限的,当内存空间严重不足时,内存检测到你PCB还在排队而数据和代码在占用空间,就会通过swap out 接口将进程的数据和代码置换到磁盘中一块swap分区中,等硬件准备好了再通过swap in接口把代码数据置换回CPU中运行。
就绪挂起:在运行队列中等待运行,内存空间不足,将对应PCB的数据被置换到磁盘swap分区中
二、Linux进程状态
区分Linux进程状态和传统操作系统进程状态是因为:Linux中的进程状态和操作系统有一点点区别,比如运行状态在Linux中是用大写R表示,某些状态在Linux中有更细的划分或换了一种说法。
Linux中的状态并没有一开始看到的系统状态图中那么多:
下图是Linux源码中的状态表示:
(一)进程状态
(二)查看进程状态
命令:ps ajx |grep 进程名
上面写了一个死循环不断打印,下面查询进程状态时看到的是S+浅度休眠状态(阻塞状态)而不是R状态?
因为频繁的打印造成的,整个进程运行需要1秒,打印则只需要1纳秒。打印要访问硬件,你死循环般的写一定会让你写进去吗?所以就处于阻塞状态,你能在屏幕上看到输出是因为输出的只是一部分而已。外设太慢了,CPU很快就阻塞住了。把printf那句屏蔽就可以看到R状态!!
S+状态:+号表示前台进程,可以被“Ctrl+C”终止
同样可以造成阻塞状态的还有scanf,这阻塞要等我们手动输入。
T状态:在代码中打断点进行调试时进程出现TT状态,也就是暂停状态
(1)僵尸进程
进程=内核数据结构(PCB)+代码数据
Z结束状态:我们调用一个函数,当这个函数运行结束后会有返回值(一般0表示任务完成,其他则异常)进程本身也是一个任务,运行结束后我们必须知道它完成得怎么样,所以当它结束状态后必有返回值。
进程执行任务本质是:父进程通过fork创建子进程执行的,一个进程运行结束不能马上释放所有资源(代码数据释放了,但进程PCB数据还保留着,PCB里面有该进程的退出码——标志进程是否正常结束,异常结束又是什么原因终止的)所以必须由父进程读取退出码信息后再由系统结束掉,因此进程结束前先进入“僵尸状态”Z,方便父进程读取,等父进程读取后才能通知系统结束,若父进程一直不读就一直处于Z状态,最后内存泄漏。
父进程有没有僵尸状态?没有!!我们是看不到的,父进程结束由bash自动回收。
(2)孤儿进程
进程要想结束必须由父进程读取子进程的PCB,有没有可能出现父进程比子进程先结束呢?
答案是:存在的!那子进程怎么办?
父进程先退出,子进程就称之为“孤儿进程” 孤儿进程被1号进程领养确保子进程能正常结束,最后由1号进程回收PCB。
1号进程就是bash!!!
三、进程PCB如何管理链表
操作系统中有多个进程就有多个task_struct(PCB—描述进程的数据结构)构成一个双链表,task_struct是如何用双链表管理起来的呢?
数据结构双链表定义一个节点,如下图struct Node,里面包含各种变量和指向下一个/前一个节点的指针,他们指向的节点类型都是struct Node,且指向每个节点的起始位置。
在Linux中就有所不同。定义的节点类型不再包含其他数据,只有两个指针;task_struct结构体直接拿节点类型创建变量,没有再单独创建next和prev指针变量。而next指针不再指向整个task_struct结构体的起始位置,而是指向下一个node成员的next。
由上图可知,我们可以知道task_struct结构体内部任意一个成员的起始地址和结构体类型,问题:1.如何得到该结构体的起始位置;2.如何访问该结构体变量内部的任意一个属性?
以我们最常用的数组举例,只知道数组中的一个元素我们是如何访问到首元素的呢?其实是当前位置减去首元素到当前位置的偏移量,而数组的地址是从低到高逐渐增加,对其取地址得到的是地址中最小的字节地址,而类型则决定一次访问多少个字节,所以也就可以算出偏移量。
因此,假设我们知道struct test中一个元素c的位置,通过公式“&c - &((struct test*)0 -> c)”就能使指针指向整个结构体开头,这样知道任意一个成员的起始地址和结构体类型就可以访问任意变量。
&((struct test*)0 -> c):计算c变量在结构体中的偏移量。
Linux中链表这么管理的好处是:
(1)我们实现的双链表再与类型无关。也就是不管是不是task_struct结构体,我们可以把节点struct Node连入任何结构体中(像队列、红黑树...)
(2)当task_struct结构体中不止一个Node节点,Node节点可以既属于全局双链表,也可以同时属于其他数据结构(比如队列就可以实现两种数据结构:数组形式和链表形式),回归系统状态,也就是由于进程PCB中有多个节点,PCB可以在全局双链表中存放也可以同时在调度队列中排队,看似进程有两个PCB其实是一个!!而全局双链表中还没连入调度队列的进程PCB在Linux中称为:新建状态(就绪状态)
四、进程优先级
【进程得到某种资源的先后顺序】
操作系统的资源有限,我们的需求无限!在一对多的情况下要得到某种资源需要区分先后顺序。
优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性能。 还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大改善系统整体性能。(优先级不等于权限)
(一)查看系统进程
在linux系统中,⽤ps ‒l命令则会类似输出以下⼏个内容:
我们很容易注意到其中的几个重要信息,有下:
- UID:代表执行者的身份
- PID:代表这个进程的代号
- PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行(默认是80)
- NI:代表这个进程的nice值
1.1 PRI and NI
PRI也是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该进程将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值
nice其取值范围是【-20,19】,一共40个级别。
1.2 PRI vs NI
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正数据
1.3 用top命令更改已存在进程的nice
- top
- 进入top后按“r”->输入进程PID->输入nice值
1.3 补充概念-竞争、独立、并行、并发
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行(在该进程的时间片内运行下一个进程)。
并发: 多个进程在⼀个CPU下采⽤进程切换的方式,在⼀段时间之内让多个进程同时推进,称之为并发。
五、进程切换
CPU上下文切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运⾏任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, ⼊栈工作完成后就把下⼀个将要运行的任务的当前状况从该任务的栈中重新装⼊CPU寄存器,并开始下⼀个任务的运行, 这⼀过程就是context switch。
CPU内寄存器就是一套存储空间,寄存器只有一套,属于CPU本身,但寄存器内的数据可以有很多。当前系统都是分时系统,每个进程都有自己的时间片。
当进程A暂时被切换下来时,需要进程A顺便带走并保存自己的上下文数据!!目的就是为了下次回来的时候能恢复,便能按照之前的逻辑继续向后运行。
六、进程调度
进程调度和状态、优先级有莫大关系!
以下是Linux2.6内核中完整版进程0(1)调度队列:
在接下来的图片描述中,大家看到的调度队列是我依照完整版画的简单版调度队列,只保留会用到的一些成员。
一个CPU,一个运行队列;多个CPU,多个运行队列。
在运行队列中有个queue[140]它指向一个下标从【0-139】的数组。其中【0-99】表示实时操作系统,先不用管;【100-139】这40个下标表示分时操作系统也就是我们现在用的系统。
(1)为什么nice值是【-20,19】
Linux的进程优先级本身就有范围【60,99】,这个范围刚好也是40,难道和队列中【100-139】的40个下标一样是巧合吗?肯定不是巧合!!!
之所以优先级设置范围【60,99】是因为【60+40,99+40】对应的就是【100-139】下标。nice值是【-20,19】也是因为默认进程优先级(PRI):80+(-20)+40=100,80+19+40=139,一一对应数组【100-139】下标,所以通过更改nice来改变进程优先级。
所以CPU调度直接去queue数组中遍历【100-139】下标,从上往下遍历有进程就执行进程,操作系统觉得这样太慢了于是有了bitmap[5]数组,这是一个位图(每个bit位映射到数组下标表示第几个队列,bit位内容0/1表示该队列是否为空)所以当我们要执行进程时通过位图替换遍历数组操作更高效。
(2)为什么bitmap大小是5?
1byte=32bit,数组【0-139】大小140,4*32=128<140(不够),5*32=160>140(可以)
我们可以把下图三个元素看做一个结构体struct q,这样队列中有struct q[2],分别对应图中array[0]和array[1]。而调度队列中还定义了两个指针active(活跃队列指针)和expired(过期队列指针)指向两个结构体。
所以:CPU调度时直接从active指针找整个结构体,拿到queue[140]再通过位图查找去调度进程。
新进程和时间片到了从CPU剥离下来的进程只能重新入队列,入的是过期队列!
通过nice值更改优先级的进程,本次执行按原本优先级执行,执行完后按更改后的优先级连入过期队列。(不存在饥饿问题)
一旦active没有进程了,那么swap(&activ,&expired)交换两个指针内容。那么下轮CPU调度时接着从active指针找整个结构体然后按步骤执行。
完