• 【Linux】深入理解进程的优先级(Linux 2.6版本O(1)调度算法)


    【前置知识】

    首先我们要了解什么是进程?知道什么是进程的PCB,可以查看我之前的文章什么是进程?

    一、进程的优先级

    (一)为什么要有优先级?

    我们思考一下?为什么要有优先级?
    本质:系统资源的不足,所有就必须要有优先级的出现解决这个问题。

    就比如:我们去学校的食堂买饭,食堂肯定是有一个个的窗口,那么当我们学生的数量过多的时候,就需要到一个窗口去进行排队,出现这种现象的原因很简单,就是学校的食堂不够大,窗口不够多。
    所以同样的道理,操作系统在调度进程的时候,由于CPU的资源有限,所以就决定了进程在被调度的时候,进程要进行排队,而排队就一定会有先后顺序,所以这个先后顺序就是进程的优先级。

    (二)进程的优先级的范围

    进程优先级范围:

    • 在Linux中,进程的优先级可以通过nice值进行调整,范围为[-20, 19],而不是[-19, 20]。
    • nice值越低,优先级越高,-20代表最高优先级,19代表最低优先级。

    进程优先级是可以被修改的, PRI(new) = PRI(原)+ nice值,注意:这里的PRI(原)它都是默认按80为标准来进行计算的。

    我们可以验证一下:

    #include 
    #include 
    
    using namespace std;
    
    int main()
    {
        while(1){
            cout << "I am a process!" << endl;
            sleep(1);
        }
        return 0;
    }
    

    我们在XShell下运行这段代码,然后通过ps -al来查看这个进程的信息
    在这里插入图片描述
    PRI就是当前的优先级,NI是nice值
    我们可以看到a.out就是我们的代码的可执行程序,它的PRI为80,然后我们通过top命令对它进行修改,详细步骤为

    top,然后按r,然后输入进程的PID,然后输入nice值,这里我输入5
    在这里插入图片描述
    PRI被修改成5了,然后我再次修改,这次我修改nice值为10,按理来说应该是85 +10 = 95,但结果是90
    在这里插入图片描述
    这里就说明了,修改PRI都是以80为基础来进行修改的。

    二、操作系统是如何实现进程的优先级?(Linux内核2.6版本O(1)调度算法)

    首先我们知道,根据冯诺依曼体系结构决定,CPU要拿数据,就要从内存中拿数据,所以一个进程要被CPU调度,就要将进程的代码数据和它的属性(PCB)加载进内存里,然后CPU再进行调度。

    而一个CPU通常一次只能调度一个进程,而操作系统里是有很多的进程需要被调度的,所以这就意味着,这些等待被调度的进程就要进行排队,而排队就要有先后的顺序,也就是进程的优先级,那么想到这里?我们就要思考它是如何排队的?

    我们在前面也看到了,进程的优先级范围的调整是[-20, 19], PRI的数值越小,优先级就越高,而进程间会有相同优先级的进程,在什么是进程?那一篇博客里,我也说过了,进程在组织的时候,是按照双链表的结构进行组织起来的,所以我们可以大致的想象

    每个PRI会对应着数组的某个下标位置,有一种映射的关系,那么每个数组下标的位置存放的会是一个个双链表,这种结构不就是我们数据结构里的哈希桶吗?
    在这里插入图片描述
    然后操作系统就依次从下标小的位置去拿出一个运行的队列出来,然后依次运行,这样就实现了一个进程优先级的现象。

    那么实际上,操作系统是怎么做的?

    调度队列:

    • Linux的调度器采用运行队列(runqueue)的概念。每个CPU有自己的runqueue,其中包含两个优先级数组:一个活动队列(active queue)和一个过期队列(expired queue)。
    • 运行队列中的每个优先级数组是一个链表数组,每个链表对应一个特定的优先级。调度器使用这些链表来管理具有相同优先级的进程。

    实际上就是在CPU中,它管理着一个结构体runqueue,该结构体里有两个成员。

    • 一个活动队列,也就是当前CPU正在运行调度的队列。
    • 一个是过期队列,这个队列是把要新加入的进程就先放在这个队列里
      > 而这两个其实都是数组,数组大小是140个空间,其中前100个空间是操作系统要用的,有特殊用途,我们只能使用后40个数据空间,这就是为什么优先级只有40个范围

    为什么要有这个过期队列呢?直接加入进活动队列里不就行了吗?

    1. 首先,一个CPU它如果既要运行我们对应的进程,如果没有过期队列,它是不是就还需要花费一些资源,去维护新加入进来的进程,这样不仅加大了管理的成本,也浪费CPU的资源,CPU资源是非常宝贵的。
    2. 其次,我们试想一下,在这种调度结构里,总是先把优先级高的执行了之后,再依次向下执行,那么如果有一些进程,它的优先级很低,但是对于操作系统而言,可能随时都会有新的进程到来,如果这些进程的优先级都比较高,那么低优先级的进程将会迟迟得不到被调度,造成饥饿问题。
    3. 如果我的CPU当前已经把该数组位置的进程链表都执行完了,执行到下一个优先级了,但是此时,又突然来一个刚才执行完的进程,需要我去执行,那么这时候我是又返回去执行,然后再去执行下一个进程的位置吗?那么这样子设计的成本是非常高的。

    我们的操作系统是非常优雅的,它不会去浪费这些资源和精力去做这种事情。实际上CPU的runqueue,它里面有两个二级指针,分别指向活动队列数组和过期队列数组。
    在这里插入图片描述
    然后CPU运行活动队列,不需要管新加入的进程,统一将新加入的进程放到过期队列中,过期队列其实就相当于是活动队列的一个镜像,新加入的进程就根据优先级映射到对应的数组下标,然后添加进对应位置的哈希桶里面,当CPU把运行队列里的内容都执行完了。
    > 这时候运行队列就是空的,然后只需要把那个二级指针,将它们交换一下所指向的内容,这样CPU立马就又得到了一个有数据的活动队列,然后此时又有了一个空的过期队列

    这种处理方式是不是非常的优雅!
    那么操作系统该怎么知道活动队列的内容为空呢?
    有人说,判空还不简单吗?直接遍历一遍,看看哪里有数据不就好了吗?
    但是这样线性遍历的时间复杂度是一个O(N)级别的,我们操作系统响应是非常快的,这显然不合适。

    1. 操作系统使用了位图的数据结构,来记录活动队列的每个下标的位置,低比特位就代表高优先级,也对应着低下标位置。
    2. 然后对应的比特位映射的数组下标位置如果有内容,就将它置为1,没有内容了就置为0,然后操作系统只需要通过位运算,就可以很快速的定位到哪些位置有内容。
    3. 从低比特位开始拿,拿最近的一个比特位为1的位置,这样也有了优先级的出现。
    4. 最后只需要判断这个位图的值是不是0,就能够知道活动队列是不是为空了。

    综上所述,这种进程优先级的调度算法是Linux内核2.6版本引入的O(1)调度算法。Linux 2.4 及之前版本: 使用的是 O(n) 调度算法,调度器的时间复杂度与系统中进程的数量成线性关系。在进程数量较多时,调度效率较低。这种算法极大的提高了调度效率和系统的扩展性。

  • 相关阅读:
    安装vite报错:Cannot use import statement outside a module
    程序员工作5年,还是个中级程序员,如何再快速晋升?
    [附源码]计算机毕业设计JAVAjsp旅游景点管理系统
    华为OD机试 - 计算面积 - 逻辑分析(Java 2023 B卷 100分)
    猿创征文|JAVA 实现《连连看》游戏
    Debian中执行脚本 提示没有那个文件或目录
    C# 关于sendtoback()和bringtofront() 的特点说明
    ThinkPHP5文档——路由
    CSDN21天学习挑战赛之折半查找
    【软考-中级】系统集成项目管理工程师 【15 信息 (文档) 和配置管理】
  • 原文地址:https://blog.csdn.net/weixin_74127402/article/details/139419961