Linux基于CentOS学习【进程状态】【进程优先级】【调度与切换】【进程挂起】【进程饥饿】

目录

进程状态

状态决定了什么

进程等待方式------队列

进程状态的表现

挂起状态

基于阻塞的挂起------阻塞挂起

swap分区

进程状态表示

Z僵尸状态

进程的优先级

什么是进程的优先级

为什么会有进程的优先级

进程饥饿

Linux的调度与切换

切换

调度

[queue [ 140 ]:优先级](#queue [ 140 ]:优先级)

[bitmap [ 5 ] :解决效率](#bitmap [ 5 ] :解决效率)

[*active & *expried:解决饥饿问题](#*active & *expried:解决饥饿问题)

阻塞和等待CPU运行


进程状态

状态决定了什么

状态决定了后续动作,如果是运行状态,那么后续就意味着可以随时被调度运行,如果是阻塞状态那么意味着要等待某种资源

阻塞状态举例:在编写代码的时候,我们会经常写scanf 或者是cin 当运行时,计算机就会在那里等待我们的输入,此刻就是阻塞状态的最好体现,它就是在等待一种资源,它将进程卡在那里,不会被CPU执行,主要是当前要等待键盘资源就绪,等待数据输入进去。它会将这个程序的PCB从运行状态改为阻塞状态,并且将其从运行队列中拿下来,放到等待队列中去

为什么CPU不往后执行呢?因为当前代码没有将scanf 或者 cin执行完,程序无法执行下去

进程等待方式------队列

不仅仅是CPU有队列,我们底层对应的网络设备或者一般的IO设备等,都有自己的队列

所以,当我们对这些设备进行访问的时候

1,OS根据描述的硬件数据结构的对象,以及对象的状态,亦或是通过一些数字,我们可以得知底层的设备的一些状态

2, 我们对应的每种硬件状态所对应的描述结构体,可以帮助我们找到该设备等待的进程,方便我们随时唤醒

3,OS作为硬件的管理者,对硬件当前的状态是非常清楚的,一旦硬件准备就绪,OS会将等待该资源的进程重新唤醒,所谓的唤醒,本质上就是将阻塞状态改为运行状态,并且投入到运行队列里。

让进程状态变更,本质上就是把进程放到不同的队列里

进程状态的表现

所谓进程状态,本质就是一个整型变量,就是task_struct中的一个整型变量,它表示状态的值

比如,通过返回不同的整型来反映它是啥状态的

cpp 复制代码
#define New 1
#define ready 2
#define running 3
#define block 4


struct task_struct
{
    ...
    int status;   
    ...
}

一个CPU维护一个运行队列,主要在运行队列里,它的状态都可以被称为运行状态

硬件的就绪状态只有OS最清楚,因为OS是硬件的管理者

当我们的进程在等待软硬件资源的时候,资源如果没有准备就绪,我们的task_struct只能

1,将自己设置为阻塞状态

2,将自己的PCB链入等待队列

状态的变迁引起的是PCB被OS搬到不同的队列里


挂起状态

基于阻塞的挂起------阻塞挂起

把进程的管理转换为对某种数据结构的增删查改,每一个进程都有匹配的代码和数据

如果当前等待的外设在短期之内不会就绪,那么它就会在阻塞状态里进行等待,但当OS压力特别大,整机或者CPU,内存所对应的剩余资源变得越来越少,那么资源现在严重不足了,不能够分配,对于计算机讲,将面临着大量的操作失败,就不能向上提供良好的运行环境,比如打游戏直接卡成PPT

对于OS,要么直接崩溃,要么需要想办法挤压资源,OS需要对整个系统内进行重新安排,阻塞进程就是浪费的其中一部分,阻塞进程一又还没有等到资源,二又站着OS分配的管理资源,本来我就资源紧张了,你等待队列里还占着那么多不干活的task_struct

所以在资源紧张的时候,会将task_struct对于的代码和数据换出,但是不会将task_struct换出,换出了就管理不了了,如果你将task_struct都赶出去了,谁还知道你有这个进程

进程 = 内核PCB对象 + 可执行程序(代码和数据)

swap分区

磁盘有一个固定区域,是专门供给当OS资源紧张的时候,OS能够将一些不是特别紧要的资源进行换入和换出,即swap分区

当OS压力最大的时候,就会将一些文件啊,进程啊,代码数据啊,传到磁盘的swap区,这个工作叫换出

等到顶住了这波压力,且想调度了的时候,我们就将swap区的资源换回来,这个工作就叫做换入

所以当我们的进程它所对应的代码和数据不在内存中的时候,此时就称该进程为挂起状态,这种叫做阻塞挂起

创建进程时,实现创建PCB呢,还是先加载代码和数据到内存呢?

答案是先创建内核数据结构PCB,也就是先把管理字段创建出来,OS才知道你有这个进程,数据代码还可以晚点换入

swap分区一般不会特别大,因为我们整体的换出,本质是个把数据拷贝到外设的过程,换入本质是将数据拷贝到内存里。如果太大的话,会加深内存与磁盘的频率,从而引发IO的频率升高,无效操作就会变多

进程状态表示

cpp 复制代码
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 运行状态( running ) : 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里, 后面的+表示是前台

S状态可称为阻塞状态,也可称为可中断睡眠,浅度睡眠

D状态可称为不可中断睡眠,深度睡眠

操作系统杀不死D状态的进程,但是S状态可以被杀,但是一旦出现系统级别的D状态,也说明系统已经很紧要了

D状态也是阻塞状态

T状态,暂停状态

kill -l 查看信号

其中,9号信号就是平常所使用的杀进程信号

19号信号是让这个进程暂停

再查看一下

一般我们的进程会在什么时候处在暂停状态呢

进程读取某一设备时,但是设备又不允许给你读,OS又不好杀掉,就让其处于暂停状态

恢复暂停状态,18号信号,就是恢复

恢复之后 ,CTRL+c是杀不掉的,因为在暂停的时候,进程就转移到后台了,恢复了也还是后台,只能用信号9来杀掉

t状态,也是属于等待某种资源

暂停状态可不可以也认为是阻塞状态?

可以,因为都是在等待某种资源

Z僵尸状态

我们在执行完一个进程的时候,对于进程里所对应的代码和数据,可以直接被释放,但是进程所对应的PCB不应该立即就被释放掉,这样做是因为为了系统或者其它进程能够读取到子进程或者这个进程的退出数据。

只有将这些数据拿走后,才是真正的死亡状态,才能释放PCB

我们将一个进程执行完毕,但是并没有获取进程退出的相关数据,此时就叫做Z状态

一个父子进程,如果子进程退了,那么父进程就一定要去读取子进程的状态,如果不读取,此时子进程的PCB就不释放,此刻就是Z状态

cpp 复制代码
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main(){
  5   pid_t id = fork();
  6   if(id == 0){
  7     //child
  8     int cnt = 5;
  9     while(cnt){
 10       printf("I am child, pid :%d ,ppid : %d\n",getpid(),getppid());
 11       sleep(1);
 12       cnt--;                                                                             
 13     }
 14     exit(0);    //让子进程直接退出
 15   }
 16 
 17   //father
 18   while(1){
 19     printf("I am father, pid :%d ,ppid : %d\n",getpid(),getppid());
 20     sleep(1);
 21   }
    }

Q:为什么要有Z状态?

A:创建进程是希望这个进程给用户完成工作的,子进程必须要有结果数据,这些在PCB中
Q:什么是Z:

A:进程已经退出,但是当前的进程状态需要自己维持住,供上层读取,就必须处于Z状态
Q:如果我作为父进程不读取呢?

A:就会带来这个僵尸状态的进程一直存在,task_struct对象就要一直存在,这些就要占据内存空间,一直不释放最终导致的问题就是内存泄漏

所以所有的进程在退出的时候,都必须先处于Z状态,被人状态读取完后才会变成X,最后完全退出
Q: 父进程比子进程先挂,子进程变成僵尸的时候会由谁来收?

A: 不会出现这种情况,如果父进程先挂掉的话,其子进程就会被1号进程所领养,1号进程就是操作系统 领养进程就会被称为孤儿进程,但是孤儿进程可以有子进程的

进程的优先级

什么是进程的优先级

进程要访问某种资源,进程进行通过一定的方式,比如排队,来确认享受资源的先后顺序

优先级和权限的区别:权限决定的是能不能做的问题,而优先级决定的是谁先的问题

为什么会有进程的优先级

资源过少带来的,但是资源过少是属于一个相对概念,每一个进程都要有它的运行队列,CPU又只有一个,所以谁先跑要排队

Linux的默认优先级是80,优先级可以修改,优先级的范围是 [60,99],总共40个

数字越小,优先级越高!但不建议调整优先级

Linux系统允许用户调整优先级,但是不能直接让你修改PRI,而是修改nice值,nice值不是优先值,而是进程优先值的修正数据 !

pri = pri(old) + nice;

pri (old)每次都是从80开始,在Linux的内核里,它会做判断,如果你的nice值超过系统给它设置的最大值,比如我突然100上去,它也不会取100,而是取19,从而到达99,反之nice值的最小值是20,不会说你取-100,就给你干到-20去

进程饥饿

为什么Linux的优先级的调整是会受到限制?

不加限制的话,就会将自己的进程优先级调整得非常高,别人的优先级就会调成非常低

优先级较高的进程,优先得到资源,其后又会源源不断地产生新的进程,最后导致的结果就是常规进程很难享受到优先级的资源,从而导致进程饥饿问题

好比你正常排队,却老是有人插队,最终饿的却是正常人

所以操作系统一般都会有分时操作系统,调度上,它能够较为公平的进行调度,且会对调度范围进行严格的限制

Linux的调度与切换

Q:进程在运行的时候,放在CPU上,是直接必须把进程代码跑完才行吗?

A:答案是明显不行的,比如是死循环,操作系统是绝对不允许让死循环一直跑下去的

现代操作系统,都是基于时间片进行轮换执行的,防止恶性竞争

竞争性 : 系统进程数目众多,而CPU 资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级
独立性 : 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行 : 多个进程在多个 CPU 下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发,非常非常非常高频

CPU里面有大量的寄存器,eip寄存器里保存最经典的就是PC指针

当一个进程在CPU上运行时,CPU上的所有寄存器就要围绕着这个进程来进行展开运算,换句话说,这个进程在CPU运行期间,进程会在CPU内部形成大量的临时数据,这些临时数据放在CPU的寄存器中

切换

所有的保存都是为了恢复,所有的恢复都是为了上一次的运行

进程在CPU的各种数据,当时间片到了,需要我们的进程进行切换的时候我,我们首先要做的就是将CPU上运行的数据进行保存,保存的地点就是该进程的PCB里,一般就是存储在PCB里的tss_struct的结构里

CPU内部的所有临时数据,我们叫做进程的硬件上下文

进程进行保护硬件上下文的工作称为保护上下文

当进程被二次调度的时候(这里的二次调度并不是说被调了两次,而是可以指两次,三次,多次),我们的进程被放到CPU上运行的时候,首先要做的就是,要将曾经保存的硬件上下文进行恢复

所以进程进行切换工作的时候,一定会做的两个工作

一个就是保护前者的硬件上下文,第二个就是恢复后者的硬件上下文

CPU内的寄存器只有一套,寄存器内部保存的数据可以有多套

那我们区分一下,寄存器和寄存器内容

虽然寄存器的数据放在了一个共享的CPU设备里面,但是所有的数据其实都是被进程私有的,CPU内的任意时刻的数据都只是属于某一进程

寄存器不等于寄存器的内容!!!

调度

Linux实现进程调度的算法,要考虑优先级,考虑饥饿,考虑效率

queue [ 140 ]:优先级

在runqueue(运行队列)里,我们有两个queue[140],它们都是属于数组指针,为什么是140呢?

它的0~99是不使用的,只用100~139

接下来需要配合的就是Linux的默认优先级是80,优先级可以修改,优先级的范围是 [60,99],总共40个

好比如果你的优先级是60,它会根据Linux的调度算法,进行运算,将你放到 [ 100 ] 位置上,所以在一定程度上,这个队列可以映射优先级的先后顺序

每次运行之前,其会对着140进行遍历,为空则下一个,不为NULL则运行这个队列的优先级队列

我们可以看出,我们运行队列有140个,但真正需要检测的只有40个,难道我们每次都要多检测100个吗,显然不是的,我们会用到runqueue结构中的另一个数组,bitmap[ 5 ]

bitmap [ 5 ] :解决效率

bitmap 是int 类型的,总共有5个单位,共5*32 = 160个比特位,其每个比特位有各自的含义

比特位的位置表示哪一个队列,比特位的内容表示队列是否为空,我有160个比特位,而运行队列只有140个单元,比特位比它还多20个,我那20个就不用了,其中有队列的位置,比特位标1,没有队列的位置,比特位标0

如此,就转换为了检测比特位就可,因为位操作会比遍历操作快得多

检查二进制中有多少个1

调度算法时间复杂度为O(1)

优先级解决了,效率解决了,那我饥饿问题咋办

也就是说,每次我都把我的优先级调成60,并放入到queue[140]队列的最前端,那我后面的进程岂不是要一直饿着?

解决办法就是:优先级数组不只是只有一个,其实还有一个,前面的那个queue [140] 是活跃队列,另一个叫做过期队列,两个数组互不干扰,CPU调度进程调度的是活跃队列,OS存放进程存放的是过期队列,即便来了优先级更高的进程,其也只会在过期队列里等待

活跃队列queue[140] + bitmap[5] + nr_active 构成一个结构体

过期队列queue[140] + bitmap[5] + nr_active 构成一个结构体

cpp 复制代码
struct q {
    nr_active;
    bitmap[5];
    queue[140];
}

两个结构体可以构成结构体数组

struct q array[2];

*active & *expried:解决饥饿问题

这是两个指针,刚开始struct q *active = &array[0] , struct q *expried = &array[1]

当array[0] 中的queue[140] 不断减少,直至为0的时候,且array[1] 里的内容不断增多的时候

就执行swap ( &active , &expried ) ,交换的时候,更改的是指针变量的内容

阻塞和等待CPU运行

这时候就会问,在运行队列中等待被调度的过程算不算等待资源,既然是等待资源的话,会不会就是阻塞状态,这样做不会矛盾吗

运行队列(runqueue)中的进程处于可运行状态,这意味着它们已经准备好被CPU执行,并且正在等待获取CPU资源。这些进程并不是在等待特定的资源,如I/O操作或页面置换等,而是在等待CPU时间。

当进程处于运行状态(TASK_RUNNING)时,它们在运行队列中,这意味着它们已经准备好运行,并且内核调度器将决定哪个进程将获得CPU时间。这些进程不是在等待资源,而是在等待CPU时间来执行。

如果一个进程需要等待某个资源(如磁盘I/O、网络响应或锁),它通常会被放置在等待队列(wait queue)中,而不是运行队列。等待队列是与特定资源或条件相关的队列,当资源变得可用或条件得到满足时,进程会被唤醒并可能被放入运行队列。

因此,运行队列中的进程是在等待CPU资源,而不是在等待其他类型的资源。

以上就是本博文的学习内容,如果有不正确的地方,还望各位大佬指点出来,谢谢阅读!

相关推荐
朝九晚五ฺ1 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream1 小时前
Linux的桌面
linux
xiaozhiwise2 小时前
Makefile 之 自动化变量
linux
猫爪笔记3 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
pq113_63 小时前
ftdi_sio应用学习笔记 3 - GPIO
笔记·学习·ftdi_sio
谭震鸿3 小时前
Zookeeper集群搭建Centos环境下
分布式·zookeeper·centos
澄澈i3 小时前
设计模式学习[8]---原型模式
学习·设计模式·原型模式
意疏4 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu4 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器