Linux内核调度器以及CFS调度器

1.调度:就是按照某种调度的算法设计,从进程的就绪队列当中选取进程分配CPU,主要是协调对CPU等等相关资源使用。进程调度的目的:最大限度使用CPU。

2.如果调度器支持就绪状态切换到执行态,同时支持执行态切换到就绪态,就成该调度器为抢占式调度器。

3.调度类sched_class结构体:

重要字段讲解

1. const struct sched_class *next
  • 功能:该字段为指针,用于指向链表中的下一个调度类。操作系统里存在多个调度类,这些调度类按优先级形成链表,借助 next 指针能够遍历整个链表。
  • 示例:若有调度类 A、B、C,它们按优先级从高到低排列,那么 A 的 next 指针指向 B,B 的 next 指针指向 C。
2. void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags)
  • 功能:这是一个函数指针,指向的函数会把进程添加到执行队列中。具体而言,会把调度实体存于红黑树,同时让 nr_running 变量自动加 1。
  • 示例:当一个新进程被创建时,操作系统会调用此函数将该进程加入执行队列,以便后续调度。
3. struct task_struct *(*pick_next_task)(struct rq *rq)
  • 功能:此为函数指针,指向的函数用于选择下一个要运行的任务。操作系统会调用该函数,从执行队列里挑选合适的进程来运行。
  • 示例:在时间片轮转调度算法中,该函数可能会按顺序选择队列中的下一个进程。
4. void (*check_preempt_curr)(struct rq *rq, struct task_struct *p, int flags)
  • 功能:这是函数指针,指向的函数专门用于检查当前任务是否能够被抢占。当有新进程进入执行队列时,操作系统会调用此函数来判断是否要抢占当前正在运行的进程。
  • 示例:若新进程的优先级比当前进程高,此函数可能会返回允许抢占的结果。
5. void (*task_tick)(struct rq *rq, struct task_struct *p, int queued)
  • 功能:这是函数指针,指向的函数会在时钟中断时被调用,用于更新当前任务的状态。例如,更新任务的时间片、优先级等。
  • 示例:在每个时钟周期结束时,操作系统会调用该函数,若当前任务的时间片用完,就会将其从执行队列中移除。

4.调度类可以分为:stop_sched_class、dl_sched_class、rt_sched_class、fair_sched_class以及idle_sched_class。

        这 5 种调度类的优先级从高到低依次为:停机调度类、限期调度类、实时调度类、公平调度类、空闲调度类。 

一、停机调度类

功能与特点:优先级最高,用于调度系统中优先级最高的 “停机进程”。这类进程拥有绝对调度权,可抢占所有其他进程,且自身不会被任何进程抢占,主要处理系统最关键、需立即执行的任务。
举例:当系统内核出现严重错误需紧急修复时,会触发停机调度类的进程。例如,内核在检测到文件系统元数据损坏时,启动的修复进程会通过停机调度类调度,此时该进程会立即抢占用户正在运行的视频播放、文档编辑等进程,确保系统核心功能恢复。

二、限期调度类

功能与特点:采用最早截止期限优先(EDF)算法,利用红黑树将进程按 “绝对截止期限” 从小到大排序,每次调度选择截止期限最小的进程,确保任务在规定时间内完成,适合对时间敏感的实时任务。
举例:自动驾驶系统中,传感器数据处理任务有严格的截止期限。如雷达数据处理进程要求每 10ms 内完成一次数据解析,限期调度类会将该进程与其他任务(如摄像头图像分析)按截止期限排序。若雷达数据处理的截止期限更近,系统会优先调度它,保证车辆及时响应障碍物检测。

三、实时调度类

功能与特点:为每个调度优先级维护独立队列,高优先级队列的进程优先调度,同一优先级内按先进先出(FIFO)或轮转(RR)方式执行,适用于对响应时间要求极高的实时场景。
举例:工业自动化产线中,机械臂的运动控制任务被赋予高优先级。实时调度类会将机械臂的控制进程放入高优先级队列,而产线状态监控等低优先级任务放入低优先级队列。当机械臂需要执行精准抓取动作时,系统优先从高优先级队列调度控制进程,确保动作实时性,避免生产事故。

四、公平调度类

功能与特点:基于完全公平调度算法(CFS),通过 “虚拟运行时间” 衡量进程对 CPU 的占用公平性。虚拟运行时间 = 实际运行时间 ×(nice0 权重 / 进程权重),权重越低的进程(如高优先级进程)虚拟运行时间增长越慢,从而获得更多 CPU 时间,实现公平分配。
举例:多用户共享的服务器场景中,多个用户同时运行办公软件(如文档编辑、表格处理)。公平调度类会根据每个进程的权重分配 CPU 时间。例如,普通用户的办公进程权重较低,虚拟运行时间增长慢,能更频繁地获取 CPU 资源,避免某个高权重进程(如后台备份任务)长期占用 CPU,保证用户操作的流畅性。

五、空闲调度类

功能与特点:优先级最低,每个 CPU 对应一个空闲线程(0 号线程)。仅当系统中没有其他可调度进程时,才会执行空闲调度类的任务,主要用于处理低优先级的后台工作。
举例:当电脑处于空闲状态(无用户操作、无应用程序运行)时,空闲调度类会启动系统维护任务,如磁盘碎片整理、系统日志压缩等。这些任务在其他进程运行时不会被调度,只有在 CPU 无负载时,才通过空闲线程执行,避免影响正常业务流程。

        也就是说对于这五个类内部的组织方式,首先是停机类,本身并不依赖复杂的数据结构来组织进程。每出现一个停机进程立刻调度执行,可以把它理解为一种 “紧急中断” 机制,只要有停机进程请求,就会优先处理; 

        限期进程内部使用红黑树,直接将截止时间作为键存储在红黑树上,到时间就执行;

        实时调度类为每个调度优先级维护独立队列,内部维护了 100个优先级队列。这些队列对应实时优先级的范围是0到99,其中数值越大表示优先级越高。高优先级队列的进程优先调度,同一优先级内按先进先出(FIFO)或轮转(RR)方式执行,前者在没有更高优先级进程的情况下不允许抢占知道主动放弃CPU为止,后者在时间片耗尽后允许被抢占;

        而公平调度类基于CFS将虚拟运行时间vruntime作为键把进程组织成红黑树,在前三种调度类中都没有进程的时候,会从这个红黑树中选取vruntime最小的进程运行;

        最后没有其他进程的时候会调度空闲调度类。

5.进程优先级

         实时进程(Real-Time Process):优先级高、需要立即被执行的进程。
        普通进程(Normal Process):优先级低、更长执行时间的进程。
        进程的优先级是一个 0--139 的整数直接来表示。数字越小,优先级越高,其中优先级 0-99 留给实时进程,100-139 留给普通进程。

 

6.内核调度策略:Linux内核提供一些调度策略供用户应用程序来选择调度器。Linux内核调度策略源码如下:

 SCHED_NORMAL:普通进程调度策略,使 task 选择 CFS 调度器来调度运行;
SCHED_FIFO:实时进程调度策略,先进先出调度,没有时间片,在没有更高优先级进程的状态下,需等待当前进程主动让出 CPU;
SCHED_RR:实时进程调度策略,采用时间片轮转,进程使用完时间片后加入对应优先级运行队列尾部,将 CPU 让给同等优先级的其他进程;
SCHED_BATCH:普通进程调度策略,用于批量处理,使 task 选择 CFS 调度器来调度运行。侧重于提升 CPU 密集型后台任务的处理效率。对于不需要与用户频繁交互的批量处理任务,减少不必要的抢占,让任务能较长时间连续运行,充分利用 CPU 资源和缓存,提高整体处理性能 ;
SCHED_IDLE:普通进程调度策略,使 task 以最低优先级选择 CFS 调度器来调度运行。目标是在系统空闲时才执行任务,防止低优先级任务在系统忙碌时抢占资源,干扰正常业务。利用系统空闲时间执行对实时性要求极低的任务,不影响其他高优先级任务的响应和执行 ;
SCHED_DEADLINE:限期进程调度策略,使 task 选择 Deadline 调度器来调度运行。

以下是一个简单示例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sched.h>

// 线程执行函数
void* thread_function(void* arg) {
    int policy = *(int*)arg;
    struct sched_param param;
    pthread_getschedparam(pthread_self(), &policy, &param);

    switch (policy) {
        case SCHED_NORMAL:
            printf("Thread is using SCHED_NORMAL policy with priority %d\n", param.sched_priority);
            break;
        case SCHED_FIFO:
            printf("Thread is using SCHED_FIFO policy with priority %d\n", param.sched_priority);
            break;
        case SCHED_RR:
            printf("Thread is using SCHED_RR policy with priority %d\n", param.sched_priority);
            break;
        case SCHED_BATCH:
            printf("Thread is using SCHED_BATCH policy with priority %d\n", param.sched_priority);
            break;
        case SCHED_IDLE:
            printf("Thread is using SCHED_IDLE policy with priority %d\n", param.sched_priority);
            break;
        case SCHED_DEADLINE:
            printf("Thread is using SCHED_DEADLINE policy with priority %d\n", param.sched_priority);
            break;
        default:
            printf("Unknown scheduling policy\n");
    }

    // 模拟一些工作
    for (int i = 0; i < 100000000; i++);

    return NULL;
}

int main() {
    pthread_t threads[6];
    int policies[6] = {SCHED_NORMAL, SCHED_FIFO, SCHED_RR, SCHED_BATCH, SCHED_IDLE, SCHED_DEADLINE};
    struct sched_param param;

    for (int i = 0; i < 6; i++) {
        // 设置调度策略和优先级
        param.sched_priority = sched_get_priority_max(policies[i]);

        pthread_attr_t attr;
        pthread_attr_init(&attr);
        pthread_attr_setschedpolicy(&attr, policies[i]);
        pthread_attr_setschedparam(&attr, &param);
        pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

        // 创建线程
        if (pthread_create(&threads[i], &attr, thread_function, &policies[i]) != 0) {
            perror("pthread_create");
            return 1;
        }

        pthread_attr_destroy(&attr);
    }

    // 等待所有线程结束
    for (int i = 0; i < 6; i++) {
        pthread_join(threads[i], NULL);
    }

    return 0;
}    

7.实际运行时间

        CFS 是 Completely Fair Scheduler 简称,完全公平调度器。在实际当中必须会有进程优先级高或者进程优先级低,CFS 调度器引入权重,使用权重代表进程的优先级,各个进程按照权重比例分配 CPU 时间。

        假设有 2 个进程 X 和 Y,X 权重为 1024,Y 权重为 2048。
                X 获得 CPU 时间比例为:1024/(1024+2048)=33% 左右
                Y 获得 CPU 时间比例为:2048/(1024+2048)=66% 左右

        在引入权重之后分配给进程的时间计算公式如下:
                实际运行时间 = 调度周期 * 进程权重 / 所有进程权重之和。

8.虚拟运行时间

        虚拟运行时间 = 实际运行时间 * NICE_0_LOAD / 进程权重 =(调度周期进程权重 / 所有进程权重之和)NICE_0_LOAD / 进程权重 = 调度周期 *NICE_0_LOAD / 所有进程总权重。
        在一个调度周期里面,所有进程的虚拟运行时间是相同的,所以在进程调度时,只需要找到虚拟运行时间最小的进程调度运行即可。

公式讲解

  • 核心逻辑:公式通过推导,将虚拟运行时间简化为 调度周期 ×NICE_0_LOAD / 所有进程总权重。其中,NICE_0_LOAD 是基准负载值(如默认设为 1024),最终结果表明:同一调度周期内,所有进程的虚拟运行时间理论上相等,以此保证调度公平性。若因计算精度等问题出现细微差异,调度器会选择虚拟运行时间最小的进程执行,弥补偏差。

数字举例

假设:

  • 调度周期为 T,
  • NICE_0_LOAD=1024,
  • 进程 X 权重 1024,进程 Y 权重 2048,总权重 1024+2048=3072。

计算虚拟运行时间:

  • X 的虚拟运行时间:3072T×1024​。
  • Y 的虚拟运行时间:3072T×1024​。

        可见,两者结果完全相同,验证了 “一个调度周期内所有进程虚拟运行时间相同” 的结论。实际调度中,若因计算误差导致微小差异,调度器会优先选择虚拟运行时间更小的进程,确保资源分配的公平性。

         也就是说在Linux系统的CFS策略中,实际决定进程是否被调度运行的依据是虚拟运行时间vruntime,在红黑树中每次调度会选择vruntime最小的进程运行。

        每次选取最小的虚拟运行时间最小就可以保证公平,而由于nice值小、优先级高的进程权重大,因此在实际运行时间相同的情况下虚拟运行时间增加的慢,这也保证了高优先级的进程得到了更多的运行时间。

        但是当进程数量非常多的时候,按权重分给每个进程的时间会趋于0,会导致频繁切换增加切换开销。因此CFS引入最小粒度1毫秒,即每个任务最少运行时间是1毫秒,而由于实际进程数量不会多到趋于无穷,因此CFS可以尽可能保证公平。

9.调度子系统各个组件模块

        主调度器:通过调用 schedule() 函数来完成进程的选择和切换。
        周期性调度器:根据频率自动调用 scheduler_tick 函数,作用根据进程运行时间触发调度。
        上下文切换:主要做两个事情(切换地址空间、切换寄存器和栈空间)。

10.CFS调度器类

        CFS调度类源码如下:

        enqueue_task_fair:当任务进入可运行状态时,用此函数将调度实体存放到红黑树,完成入队操作
        dequeue_task_fair:当任务退出可运行状态时,用此函数将调度实体从红黑树中移除,完成出队操作 

https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值