Schedule()调度函数源码解析与总结


提示

本篇博客是参考调度程序schedule()注释,结合我个人查询到的其他知识以及个人理解完成的。由于本人知识储备有限,其中的注释及理解是否正确我自己也不知道,只能作为参考,有问题的地方欢迎大家指出。

一、schedule()内核源码及注释分析

// 通过schedule函数,完成对进程的调度

asmlinkage void schedule(void)
{
    //声明调度数据结构体变量:sched_data
    struct schedule_data * sched_data; 
    // 声明进程控制块PCB结构体变量:上一个PCB prev,下一个PCB next,指针p
    struct task_struct *prev, *next, *p; 
    //声明表头结构体:tmp
    struct list_head *tmp; 
    // 声明两个整型:当前CPU this_cpu,时间片 c
    int this_cpu, c; 

    // =============== 处理使用完cpu的当前进程current ===============
    
    // #### 锁运行队列,关中断 ####
    // ① 运行队列:即就绪队列,Linux中称为run queue;
    // ② 锁运行队列:将运行的队列上锁,此期间只能由某一个程序执行操作,
    //   避免多个程序同时对队列进行操作,造成数据污染产生差错;
    // ③ 关中断:关闭中断信号产生的程序,主要用于在此期间执行
    //   一些不能被中断信号打断的程序(开中断,即打开产生中断信号的程序)。
    spin_lock_prefetch(&runqueue_lock);                                   

    // #### 将当前运行的进程调度出去 ####
    // 正在运行的而进程采用current宏表示
    // ① 如果active_mm没有借用当前进程的mm,则执行报错函数bug()
    //  (内核线程没有mm空间,但其active_mm会借用当前进程的mm,保持与用户线程处理的统一性)
    if (!current->active_mm) BUG();        
    // 将当前正在运行的进程调度出去,current变为prev,变为上一个刚刚用完CPU的PCB(即成为过去式)
    prev = current;                  
    //再获取上次运行进程所用的cpu
    this_cpu = prev->processor;  
    
    // #### 如果内核处于执行中断处理程序中(可能性小),则报错 ####
    // ① 中断中,即内核执行中断处理程序的期间,若允许进程调度,那么当中断处理程序运行时,进行了内核抢占,
    //   那么处理器就会去执行抢占的进程,这个进程的具体信息处理器能够通过进程控制块去找到;
    // ② unlikely()这种设计的原因:注意:unlikely用于gcc>=2.96之后的编译优化,表示if内代码运行的可能性比较低,
    //   这样编译器就可以将else里面的代码提前,cpu在进行指令预取方面有性能提高,反之,likely则是if内代码运行可能性高。
    if (unlikely(in_interrupt())) {   
        // 提示
        printk("Scheduling in interrupt\n");
        //报错,提示出问题
        BUG();
    }

    // #### 释放上次运行的进程prev所占用的资源 ####
    // ① 如果之前运行的进程prev占用了全局内核锁,释放;
    // ② 如果当前cpu占用了全局中断锁,释放;开当前cpu中断线
    release_kernel_lock(prev, this_cpu);                                
                                                                        

    // #### 对sched_data进行事实保护,每个CPU只能运行一个进程 ####
    sched_data = &aligned_data[this_cpu].schedule_data;
    
    // #### 锁运行队列,关中断 ####
    spin_lock_irq(&runqueue_lock);                                   

    // #### 将耗尽时间片的上次运行进程prev移动到运行队列最后 ####
    //如果是实时进程(小概率事件)
    if (unlikely(prev->policy == SCHED_RR))     
        //如果时间片已经用完
        if (!prev->counter) {    
            //将nice转换为时间片
            //【nice为UNIX时期沿用的负向优,向优先级,取值-20~19,值越大越谦让,值越小,优先级越高】
            prev->counter = NICE_TO_TICKS(prev->nice);
            //将其移动到运行队列尾部
            move_last_runqueue(prev);   
            
        }

    // #### 根据上次进程prev的状态,给予对应操作 ####
    //prev->state:获取进程状态
    switch (prev->state) {     
    // 若进程状态处于TASK_INTERRUPTIBLE浅度睡眠,则执行下方代码(浅度睡眠,即可中断的睡眠)
    case TASK_INTERRUPTIBLE: 
        //如果该进程接收到唤醒信号
        if (signal_pending(prev)) { 
            //让该进程状态置为TASK_RUNNING(就绪态)
            prev->state = TASK_RUNNING;
            break;
        }
        
    // 如果该进程处于其他状态,如:TASK_STOPED(暂停),TASK_ZOMBE(死亡但用户未注销),
    // TASK_UNINNTERRUPTIBLE(深度睡眠,不可中断的睡眠)状态,就执行如下代码。
    default:
        del_from_runqueue(prev);//比如调用exit(),wait4()等
        
    //如果进程处于就绪态,则不做任何操作,让其等待执行即可
    case TASK_RUNNING:;
    }
    
    // #### 清空need_resched ####
    // ① need_resched条件:如果为真, 内核会重新进程一次调度
    prev->need_resched = 0;                                                

    
    // =============== 调度一个进程来使用CPU ===============

    repeat_schedule:
    
        // #### 选择即将使用CPU的进程next ####
        //获得空闲进程
        next = idle_task(this_cpu);           
        //找最大值的常用初始化
        c = -1000;            
        //遍历运行队列
        list_for_each(tmp, &runqueue_head) {                                
            p = list_entry(tmp, struct task_struct, run_list);
            //如果程序可以在cpu上跑,并且允许在这颗cpu上跑
            if (can_schedule(p, this_cpu)) {
                //获取调度权重
                int weight = goodness(p, this_cpu, prev->active_mm);
                //更新最大权重与选中进程
                if (weight > c)
                    c = weight, next = p;                                    
            }
        }
    
        // #### 如果c==0,说明所有进程时间片用完了,可能性很小 ####
        // ① 个人理解:c==0时,!c为1即为真;unlikely是一种对发生可能性的描述,unlikely(!c),
        //   表示!c为真即c==0的概率是unlikely的,就是很小概率,几乎不可能的,但是如果真出现这种可能了,则执行if里的语句。
        if (unlikely(!c)) {                                                    
            struct task_struct *p;
            //开运行队列锁,开中断
            spin_unlock_irq(&runqueue_lock);            
            //锁住进程双向链表
            read_lock(&tasklist_lock);  
            //更新每个进程的时间片
            for_each_task(p)
                p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);    
            //开进程双向链表
            read_unlock(&tasklist_lock);                                   
            //锁运行队列,关中断
            spin_lock_irq(&runqueue_lock);                                    
            //再次寻找最值得调度的进程
            goto repeat_schedule;                                            
        }
    
        // #### 将选择好的进程切换来使用CPU ####
        //将当前运行的进程指向为我们选择好的next进程
        sched_data->curr = next;                                            
        //将正在使用CPU的进程以及CPU内核更新为所选择的进程next和this_cpu
        task_set_cpu(next, this_cpu);                                        
        //解锁运行队列,开中断
        spin_unlock_irq(&runqueue_lock);  
        
        // #### 如果选择到的进程仍为之前的进程(认为可能性很小) ####
        if (unlikely(prev == next)) {                                        
            prev->policy &= ~SCHED_YIELD;                   
            // 执行“处理相同进程”的方法
            goto same_process;
        }
    
    // =============== 维护每个流程的“last schedule”值 ===============
    
    //(即使我们重新调度到同一进程,也必须重新计算)目前,这仅用于SMP,而且是近似值,因此我们不必在保持运行队列自旋锁的同时维护它。
    #ifdef CONFIG_SMP
        
        //更新调度进程时的时钟,用于smp中另外一个cpu调度参考
        sched_data->last_schedule = get_cycles();                            
    
        /*
        * 我们提前删除了调度程序锁(它是一个全局自旋锁),因此我们必须在switch_to()期间锁定前一个进程,以避免重新调度。
        */
    
    #endif /* CONFIG_SMP */
    
        //记录调度次数
        // 有3个进程受上下文切换的影响:prev == .... ==> (last => next)
        // 下一个堆栈上的是“更多以前的”“prev”,但通过switch_to()将prev设置为(刚才运行的)“last”进程。这听起来可能有点令人困惑,但很有道理。
        kstat.context_swtch++;       
        
    
        prepare_to_switch();
        {
            //新进程的运行空间
            struct mm_struct *mm = next->mm;                                
            //原进程的运行空间
            struct mm_struct *oldmm = prev->active_mm;                      
            //如果新进程没有运行空间,则是内核进程
            if (!mm) {                                                        
                //内核进程在调度出去的时候会释放其借用的运行空间,如果此处仍然存在,则有问题
                if (next->active_mm) BUG();                                    
                //借用原进程的运行空间                                                            
                next->active_mm = oldmm;                                    
                //原进程运行空间计数加1,用于内存交换信息
                atomic_inc(&oldmm->mm_count);                                
                //tlb采用lazy刷新方式
                enter_lazy_tlb(oldmm, next, this_cpu);                        
            }
            //反之,如果是用户进程
            else {                 
                //用户进程的两个运行空间应该相同,如果不同则报错
                if (next->active_mm != mm) BUG();                      
                //切换用户空间
                switch_mm(oldmm, mm, next, this_cpu);                        
            }
    
            //如果原进程是内核进程
            if (!prev->mm) {                                                
                 //释放其引用的运行空间
                prev->active_mm = NULL;                                       
                //运行空间计数-1
                mmdrop(oldmm);                                                
            }
        }
        
        //切换寄存器状态与堆栈
        switch_to(prev, next, prev);                                        
        //原进程放入运行队列尾部
        __schedule_tail(prev);      
        
    
    // =============== 处理调度后仍为相同进程 ===============
    
    same_process:
        //针对smp,要将当前进程的内核深度清0
        reacquire_kernel_lock(current);     
        //再次调度 
        if (current->need_resched)                                           
            goto need_resched_back;
        return;
}

二、Schedule()调度函数整理分析结论

参考连接:
Linux进程调度时机Schedule函数解析
linux调度子系统8 - schedule函数

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核中的进程调度器被称为“调度器”,其源代码位于`kernel/sched/`目录下。调度器是内核的一部分,它控制着在多个进程之间分配CPU时间片的方式。在这个过程中,调度器需要考虑多个因素,例如进程优先级、进程所需的资源、进程等待的时间以及系统负载等。 下面是对Linux内核进程调度器的源代码进行简要的解析: 1. 调度器初始化 调度器初始化函数为`sched_init()`,它负责初始化调度器的各种数据结构和变量。在这里,调度器将创建一个名为`init_task`的内核线程,该线程是系统中的第一个进程。调度器还会初始化各种调度相关的数据结构和变量,例如CPU负载平衡器、进程优先级队列等。 2. 进程调度 进程调度函数为`sched_schedule()`,它被调用以选择下一个要运行的进程。调度器使用了一种多级反馈队列调度算法,其中进程根据它们的优先级被分配到不同的队列中。调度器将首先从优先级最高的队列中选择下一个要运行的进程。如果该队列为空,则调度器将继续选择下一个队列,直到找到非空队列为止。如果所有队列都为空,则调度器将选择一个空闲的CPU,并将当前进程移到该CPU上。 3. 进程优先级 进程的优先级是一个重要的因素,它决定了进程在被分配CPU时间片时的顺序。Linux内核中,进程的优先级范围从0到139,其中0是最高优先级,139是最低优先级。优先级为0的进程是实时进程,可以通过`sched_setscheduler()`函数设置。其他进程的优先级由调度器动态调整,通常根据进程的历史运行时间、进程类型、进程等待时间等因素进行计算。 4. CPU负载平衡 当系统中有多个CPU时,调度器还需要考虑如何平衡CPU的负载。为此,调度器将在不同的CPU之间移动进程,以确保每个CPU的负载尽可能相等。调度器会定期扫描系统中的所有CPU,查找负载最轻的CPU,并将某些进程从其他CPU移动到该CPU上。 5. 调度策略 调度器还支持不同的调度策略,例如完全公平调度(CFS)、实时调度(RT)、轮转调度(RR)等。不同的调度策略适用于不同的场景,例如CFS适用于普通的任务,而RT适用于实时任务。调度策略可以通过`sched_setscheduler()`函数进行设置。 以上是对Linux内核进程调度器的简要介绍和源代码解析。由于调度器的复杂性,这里仅介绍了一些基本概念和代码实现。如果您对此感兴趣,建议仔细阅读相关文档和源代码。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值