当前位置: 首页 > news >正文

【Linux篇】:进程抢占式调度的量子纠缠--状态,优先级与上下文切换的三角博弈

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.进程状态
    • 1.一般的操作系统学科进程状态
    • 2.Linux中的进程状态
    • 3.Linux中的两种特殊进程
      • 僵尸进程
      • 孤儿进程
  • 二.进程优先级
    • 1.什么是优先级
    • 2.为什么要存在优先级
    • 3.优先级如何实现的
  • 三.进程的调度和切换
    • 进程调度
    • 进程切换

一.进程状态

1.一般的操作系统学科进程状态

1.运行状态

一个进程在被CPU运行之前,需要在CPU的运行队列中排队等待,一个CPU绑定一个运行队列,竞争访问CPU资源,根据队列中的顺序,先后运行队列中的进程。

//运行队列:
struct runqueue{
    //运行队列的队头进程
    struct task_struct *head;
    //运行队列的正在被CPU运行的进程
    struct task_struct *tail;
    .....
}

只要在运行队列中的进程,当前进程状态就是运行状态(R),正在被运行的进程也是运行状态。运行状态表示当前该进程已经准备好了,可以随时被调度器调度运行。

但是一个进程只要把自己放到CPU上开始运行了,是不是一直要执行完毕,才能把该进程放下来?

当然不是,比如说死循环时,我们就会手动结束当前程序的运行。

此外,每个进程都有一个叫做时间片的东西,时间片大概为10ms,在一个时间段内,运行队列中的所有进程的代码都会被执行,当一个进程的时间片用完之后,就会运行队列中的下一个!这就是并发执行

所以存在大量的把进程从CPU上放上去,拿下来的动作,这就是进程切换

运行队列的存在本质是对调度做管理

2.阻塞状态

假设当前有一段代码,代码中有一条语句需要我们输入当前变量的值,在运行这段代码时,执行到该语句会停下来,等待我们从键盘中输入。如果我们一直不输入,当前程序就会一直停留在这里,等待我们输入。输入的时候从键盘输入,键盘就是输入设备,根据冯诺依曼体系结构,只有内存能和输入设备交互,所以等待我们输入的当前程序还在内存中,只有当我们输入完后,才会被CPU运行。

进程因等待某种资源准备就绪时(如I/O完成)暂停运行,驻留在内存中而不占用CPU资源,当前进程的状态就是阻塞状态

因为可能存在多个进程等待同一个设备的资源,所以对于每个设备来说,都有一个等待队列。但是设备也存在多个,所以还是要对设备进行管理:

//以键盘为例
struct dev{
    //设备类型
    int type
    //设备状态
    int status;
    //等待队列的队头进程
    struct task_struct *head;
    //指向下一个设备的指针
    struct dev *next;
    ....
};

当某个设备等待队列中的进程获取到当前设备的资源时,就会完成准备工作,从新放到运行队列中等待CPU运行,所谓唤醒的本质就是将阻塞状态变为运行状态,放到运行队列中

所以不同的状态其实就是把PCB放到不同的队列中

3.挂起状态

如果操作系统内部的资源严重不足了,为了保证正常的情况下,操作系统就会生出来内存资源,如何省出?

对于阻塞状态的进程来说,需要等待某种设备资源的准备就绪而在等待队列中排队等待,对于在等待队列中的进程,排队等待的是进程的PCB,而当前进程对应的代码和数据并不使用,所以操作系统会将等待队列中的PCB保留,而代码和数据则是放到外设中,这个过程叫做换出;当该进程准备就绪时,操作系统会再从外设中获取到该进程的代码和数据,这个过程叫做换入

等待队列中的进程的代码和数据并不在内存中,当前状态就是挂起状态

通常阻塞和挂起状态都是相伴存在。

如果所有的等待的进程都按照这种规则,就可以省出一大块内存空间。

2.Linux中的进程状态

1.运行状态R(running)

已经完成资源的准备,在运行队列中的进程和正在被运行的进程,当前状态都是运行状态

在这里插入图片描述

在这里插入图片描述

R+表示前台运行,如果要后台运行,在输入执行指令时,需要加上&,后台运行为R,需要手动终止。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.浅度睡眠S(sleeping)

睡眠状态对应的就是阻塞状态,只不过在Linux中还要分为浅度和深度睡眠。

浅度睡眠状态,等待某种资源的准备就绪,可以被唤醒(比如,直接终止)

在这里插入图片描述

bash命令行解释器,等待指令输入,其实就是一种阻塞状态,对应的浅度睡眠。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.深度睡眠D(disk sleep)

假设当前有一个进程需要向磁盘中写入1GB的数据,该进程会先向磁盘发送信号表示要进行写入,磁盘在接收到信号后会根据内容大小找到一块合适的空间写入,如果磁盘空间不足时,磁盘会向该进程发送信号,如果该进程依然要强行写入,磁盘就会花费一些时间进行处理然后写入,再写入的这段时间,进程是需要等待的。该进程在等待时,等待某种资源的准备就绪,也就是阻塞状态。

但是当系统压力足够大,内存资源紧张时,操作系统会将强行写入的正在等待的进程直接干掉,干掉后当磁盘写入完后,会发现原来的进程已经不存在了,就可能会导致原本的数据丢失。

为了避免这种情况发生,需要让进程在写入磁盘的时候,不被任何人干掉,就可以避免数据丢失。

让进程在等待某种资源准备就绪时,不被唤醒(终止掉),当前状态就是深度睡眠状态。

深度睡眠状态很难观察到,因为如果存在过多深度睡眠状态的程序,说明当前系统压力过大,容易造成系统卡死。

4.暂停状态T(stopped)/t(tracing stop)

可以理解为阻塞的一种,和睡眠状态的区别是,睡眠一定是在等待某种资源的准备就绪,暂停可能是在等待某种资源的就绪,也可能是由其他进程控制。

kill指令的相关信号,19为暂停信号,18为继续信号:

在这里插入图片描述

使用kill指令的暂停信号时,正在运行中的程序就会暂停:

在这里插入图片描述

t722%5CFileStorage%5CTemp%5Ce0355597aab1098abe641121350eafd.png&pos_id=img-aHvtm94m-1742202360405)

暂停前为前台运行,暂停后再次运行就是后台运行:

在这里插入图片描述

在这里插入图片描述

注意转变为后台运行后只能使用kill -9 [目标进程]指令终止掉。

此外暂停还有一个典型的例子就是使用gdb调试器调试代码。

5.死亡状态X(dead)

这个状态只是一个返回状态,表示该进程结束,很难被观察到,因为结束是一瞬间发生的。

6.僵死状态Z(zombie)

进程一般退出的时候,如果父进程没有主动回收子进程信息(使用wait()系统调用,后面会讲),子进程会一直让自己处于僵死状态,等待父进程读取退出状态代码,所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就一直处于僵死状态。进程的相关信息尤其是task_struct结构体会一直不被释放!

3.Linux中的两种特殊进程

僵尸进程

子进程先结束,父进程还在运行,子进程一直处于僵死状态就是僵尸进程

维护退出状态本身就是用数据维护,也属于进程的基本信息保存在task_struct中,所以僵死状态一直不退出,task_struct就要一直维护。

如果task_struct一直不回收,不光会造成内存资源的浪费,时间长了还有造成内存泄漏问题!

如何避免僵尸进程,后面会讲到。

创建一个僵尸进程:

在这里插入图片描述

在这里插入图片描述

孤儿进程

如果父进程先退出,子进程的父进程就会被改成pid为1的进程,也就是操作系统本身。父进程是1号进程的,当前进程就是孤儿进程

孤儿进程表示该进程被系统领养,为什么要被领养?

因为孤儿进程之后退出时,需要被父进程释放掉。

创建一个孤儿进程:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

二.进程优先级

1.什么是优先级

优先级表示当前进程已经获取CPU资源访问的权限,只是对于资源的访问,谁先访问,谁后访问的问题。

2.为什么要存在优先级

因为CPU资源是有限的,而进程存在多个,注定了进程之间是竞争关系!这就是竞争性

而操作系统必须要保证进程之间是良性竞争,因此需要借助优先级,明确进程之间,谁先访问,
谁后访问。

如果进程长时间得不到CPU资源的访问,该进程的代码长时间无法得到推进,就会导致该进程的饥饿问题

3.优先级如何实现的

查看目标进程的优先级:

PRI表示的就是该进程的优先级,PRI值越小,优先级就越高,优先级越高,就越早被CPU运行。

NI表示的nice值,可以用来修改进程的PRI值。

PRI(new)=PRI(old)+nice

在上面这个表达式中,每次修改,PRI(old)都默认是80

既然优先级可以被调整,那么是不是可以大量的大大的更改nice值,然后大大的提高进程的优先级?通过控制优先级,是不是可以让目标进程一直在被调度?

理论上来说,确实是可以实现,但是Linux操作系统不想过多的让用户参与优先级的调整,所以对于用户来说,只能在对应范围内进行调整。

nice的取值范围:[-20,19],一共40个级别

注意:nice值不是进程的优先级,它和PRI不是一个概念,但是进程的nice值会影响进程的优先级变化。可以理解为nice值是进程优先级的修正数据
在这里插入图片描述

三.进程的调度和切换

进程调度

操作系统是如何根据优先级,开展进程的调度?

在Linux内核2.5到2.6.23版本中采用的都是O(1)调度算法,通过进程调度队列来实现。

根据前面的内容,现在可以知道进程在被CPU执行之前要在运行队列中排队等待,调度器根据优先级来调度执行。

而在运行队列中包含两个了两个数组:

  • 活跃数组:存放当前可以准备运行的进程
  • 过期数组:存放时间片用完的进程
struct running{
    ...
    //两个指针数组,数组里面存放的是指向每个进程PCB的指针
    task_struct *active_array[140];
    task_struct *expired_array[140];
    ...
};

每个数组的大小为140,也就是有140个队列,下标对应[0,139]个优先级,其中下标[0,99]表示实时进程优先级(一般不用),下标[100,139]表示普通进程优先级,对应由nice值决定的优先级范围[60,99],正好四十个优先级

根据优先级映射到对应的数组下标位置,相同优先级存放到数组同一位置的队列中(本质其实就是一种开散列的哈希做法)。

优先级对应的数值越低,下标越小,访问数组时,越先被访问,因此CPU运行进程,其实就是遍历数组获取对应位置上的进程,直到当前数组为空,就是运行了所有的进程。

注意运行进程并不是一直运行当前的进程直到进程结束才执行下一个(前面讲解过),而是每个进程都有一个时间片,当该进程的时间片用完后,就要结束运行,开始运行队列中的下一个,时间片用完的进程就要从新排队等待下次被CPU运行(CPU运行的非常快,所以平时就会察觉不到)。

如果当前时间片用完后,就要根据进程的优先级从先放到数组中的对应位置,如果当前进程优先级非常高,运行结束后,又该轮到该进程运行,一直抢占后面本应该要运行的进程,就会导致后面的进程长时间得不到CPU资源的访问,后面进程的代码长时间无法得到推进,就会导致后面进程的饥饿问题

为了解决该问题,就需要借助另一个过期数组,当进程的时间片用完后,根据优先级从新插入到过期数组的位置上,而不是活跃数组中,同理刚准备好的,新来的进程也要插入到过期数组中。这样就能公平的让所有进程访问CPU资源。

运行队列中除了包含上面的两个数组,还包含两个指向该数组的指针:

struct running{
    ...
    //两个指向对应数组的指针
    task_struct **active;
    task_struct **expired;
    
    //两个指针数组,数组里面存放的是指向每个进程PCB的指针
    task_struct *active_array[140];
    task_struct *expired_array[140];
    ...
};

当活跃数组中所有队列的进程全部运行完后,只要交换活跃数组和过期数组(本质就是交换两个指针的指向:swap(&active,&wait),让空的活跃数组成为新的过期数组等待下一次调度运行,原来的非空过期数组成为新的活跃数组开始调度运行。

这里又有一个问题,如何判断数组为空?

如果是直接遍历数组,数组中有140个位置,每个位置是一个队列,如果某个队列过长,就会导致时间复杂度过高,影响运行效率,所以为了能过快速实现判断数组是否为空,就要借助位图来实现。

位图也是用到哈希的思想。两个数组都有一个对应的位图,假如位图是一个大小为5的数组:

int bits[5];

一个元素含有八个比特位,五个元素一共有四十个比特位对应普通优先级的四十个队列;每个比特位由二进制表示,只有0和1,0表示当前数组中对应的队列为空,1表示对应的队列非空,还有进程没有运行。

只要当前位图中含有非零元素,就说明当前数组中还有进程没有被运行,全部为零就说明当前数组中的进程全部运行完,数组为空。因为位图的大小非常小,遍历一次为常数级别,所以时间复杂度相当于O(1)。

注意:O(1)调度算法是Linux内核2.5到2.6.23版本中使用的调度算法,现在已经被CFS取代,CFS使用红黑树按照vruntime排序,确保完全公平,而非基于优先级的队列

进程切换

根据前面讲的现在可以知道,多个进程在一个CPU上交替运行,通过时间片分配和调度器切换,从而在用户宏观上形成了多个进程同时运行的现象,但在微观上仍然是顺序运行。

多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发

前面讲解的进程调度是所有进程整个的调度过程,接下来需要讲解单个进程在时间片用完之后,调度器切换下一个进程是如何切换的?

先补充一个关于进程上下文的概念:

CPU中包含非常多的寄存器,不同的寄存器有不同的功能。

比如为什么函数的返回值会被外部拿到?

这是因为函数在返回的时候会将返回值存放到某个寄存器中,比如eax,当需要用到该函数的返回值时就会直接从该寄存器中获取。

另一个在运行我们自己写的代码时,系统如何得知我们的进程当前执行到哪行代码了?

这是因为存在程序计数器(pc,eip),也是寄存器的一种,它会记录当前进程正在执行指令的下一行指令的地址。

除了上面的这两种,CPU中还包含很多寄存器:

通用寄存器:eax  ebx   ecx   edx
栈帧:   edp   esp  eip
状态寄存器: status

CPU中为什么存在这么多寄存器?

因为寄存器也具有对数据的临时保存能力,将CPU运行进程时的高频数据放入到寄存器中,方便CPU快速的寻址找到对应的数据,提高运行的效率。所以CPU内的寄存器里面保存的都是进程相关的数据,用来进行访问或者修改。

当一个进程被CPU执行的时候,相关的数据就会存放到寄存器中,但是进程的时间片用完后,要被调度器切换为下一个进程,所以保存的这些数据是临时的。

进程的上下文就是指CPU寄存器里面保存的进程的临时数据

当进程的时间片结束后,该进程就要从CPU上离开,离开的时候,要将自己的上下文数据保存好,甚至带走,如果不保存,下一个进程也要把自己对应的上下文放到寄存器中,就会覆盖掉上一个进程的上下文,当再次轮到该进程执行时,就要从新获取上下文数据,效率就会降低。

所以在进程切换的时候,保存对应的上下文数据的目的,都是为了方便再次运行时恢复数据。

进程在被切换的时候,需要完成核心两个工作

  • 保存当前进程的上下文
  • 恢复下一个进程的上下文

进程的并发是操作系统多任务功能的核心体现,通过调度器的调度和切换,实现了资源的合理分配和任务的高效执行。

进程的上下文就是指CPU寄存器里面保存的进程的临时数据

当进程的时间片结束后,该进程就要从CPU上离开,离开的时候,要将自己的上下文数据保存好,甚至带走,如果不保存,下一个进程也要把自己对应的上下文放到寄存器中,就会覆盖掉上一个进程的上下文,当再次轮到该进程执行时,就要从新获取上下文数据,效率就会降低。

所以在进程切换的时候,保存对应的上下文数据的目的,都是为了方便再次运行时恢复数据。

进程在被切换的时候,需要完成核心两个工作

  • 保存当前进程的上下文
  • 恢复下一个进程的上下文

进程的并发是操作系统多任务功能的核心体现,通过调度器的调度和切换,实现了资源的合理分配和任务的高效执行。

以上就是关于进程的第二部分,进程状态,进程优先级,进程调度和切换的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
在这里插入图片描述

相关文章:

  • python基础8 单元测试
  • 【算法】一维差分
  • 【Linux】Makefile秘籍
  • 深度解读 | AI驱动下的新型金融对冲策略:稀疏奖励强化学习的应用
  • 1.angular介绍
  • 第九步:web-js
  • Go基础语法阶段核心内容(5天)
  • ESP32(4)TCP通信
  • 免费实用工具,wps/office/永中通吃!
  • Matlab 高效编程:用矩阵运算替代循环
  • 淘宝商品详情商品评论数据爬取的技术之旅
  • 数据结构 -- 树和二叉树
  • 系统架构图
  • tongweb7控制台无法访问
  • 第59天:Web攻防-XSS跨站反射型存储型DOM型接受输出JS执行标签操作SRC复盘
  • Linux|静态库 共享库
  • redis十大应用数据类型具体使用及其应用
  • Go语言不定长参数使用详解
  • 【蓝桥杯】第十三届C++B组省赛
  • 删除排序链表中的重复元素(js实现,LeetCode:83)
  • 移动互联网未成年人模式正式发布
  • 大型长读长RNA测序数据集发布,有助制定精准诊疗策略
  • 兴业银行一季度净赚超237亿降逾2%,营收降逾3%
  • 国家统计局:一季度全国规模以上文化及相关产业企业营业收入增长6.2%
  • 新经济与法|如何治理网购刷单与控评?数据合规管理是关键
  • 美军空袭也门拘留中心,已致68人死亡