进程调度与切换简单总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程调度与切换简单总结相关的知识,希望对你有一定的参考价值。

一、Linux时钟系统

1.时钟硬件

  绝大多数的PC都有两个时钟源,RTC(实时时钟)和OS(系统时钟)。RTC也叫做CMOS时钟,它是PC主机板上的一块芯片。OS时钟产生于PC主板上的定时/计数芯片,由操作系统控制这个芯片的工作,OS 时钟的基本单位就是该芯片的计数周期。在系统上电启动的时候,会用RTC来初始化OS。OS 时钟只在开机时才有效,而且完全由操作系统控制,所以也被称为软时钟或系统时钟。OS 时钟所用的定时/计数芯片最典型的是 8253/8254 可编程定时/计数芯片。所以当然是以脉冲计数了,输出脉冲的周期叫做一个“时钟滴答”,计算机中的时间是以时钟滴答为单位的,每一次时钟滴答,系统时间就会加 1。操作系统根据当前时钟滴答的数目就可以得到以秒或毫秒等为单位的其他时间格式。

技术分享

2.时钟运行机制

技术分享

二、时钟中断

  Linux的OS时钟的物理产生原因是可编程定时/计数器产生的输出脉冲,这个脉冲送入CPU,就可以引发一个中断请求信号,我们就把它叫做时钟中断。

  每个时钟滴答,时钟中断得到执行。时钟中断执行的频率很高:100次/秒,时钟中断的主要工作是处理和时间有关的所有信息、决定是否执行调度程序。和时间有关的所有信息包括系统时间、进程的时间片、延时、使用 CPU 的时间、各种定时器,进程更新后的时间片为进程调度提供依据,然后在时钟中断返回时决定是否要执行调度程序。

三、Linux的调度程序—Schedule

1.调度原理

  进程运行需要各种各样的系统资源,如内存、文件、打印机和最宝贵的 CPU 等, 所以说,调度的实质就是资源的分配。 

  一个好的调度算法(时间片轮转调度算法、优先权调度算法、多级反馈队列调度、实时调度)应当考虑以下几个方面。
(1)公平:保证每个进程得到合理的 CPU 时间。
(2)高效:使 CPU 保持忙碌状态,即总是有进程在 CPU 上运行。
(3)响应时间:使交互用户的响应时间尽可能短。
(4)周转时间:使批处理用户等待输出的时间尽可能短。
(5)吞吐量:使单位时间内处理的进程数量尽可能多。

很明显,五个不可能同时满足,所以每种调度算法都是满足其中的一种或多种。

2.Linux进程调度时机

(1)进程状态转换的时刻:进程终止、进程睡眠;
(2)当前进程的时间片用完时(current->counter=0);
(3)设备驱动程序;
(4)进程从中断、异常及系统调用返回到用户态时。

3.进程调度的依据

  调度程序运行时,要在所有处于可运行状态的进程之中选择最值得运行的进程投入运行。选择进程的依据是什么呢?在每个进程的 task_struct 结构中有如下 5 项:need_resched、nice、counter、policy 及rt_priority这五项在前面进程概念中介绍过,对于普通进程,选择进程的主要依据为counter 和nice 。对于实时进程,Linux采用了两种调度策略,即FIFO(先来先服务调度)和RR(时间片轮转调度)。因为实时进程具有一定程度的紧迫性,所以衡量一个实时进程是否应该运行,Linux 采用了一个比较固定的标准。实时进程的counter只是用来表示该进程的剩余滴答数,并不作为衡量它是否值得运行的标准,这和普通进程是有区别的。

 与其他操作系统一样,Linux 的时间单位也是“时钟滴答”,只是不同的操作系统对一个时钟滴答的定义不同而已 (Linux 设计者将一个 “时钟滴答” 定义为 10ms)。在这里,我们把 counter 叫做进程的时间片,但实际上它仅仅是时钟滴答的个数。

4.进程可运行程度的衡量

函数goodness()就是用来衡量一个处于可运行状态的进程值得运行的程度。 该函数综合使用了上面我们提到的5项,给每个处于可运行状态的进程赋予一个权值(weight),调度程序以这个权值作为选择进程的唯一依据。

static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm)
{ 
   int weight;
   if (p->;;policy != SCHED_OTHER) {/*如果是实时进程,则*/
   weight = 1000 + p->;;rt_priority;
   goto out;
   }
   /* 将counter的值赋给weight,这就给了进程一个大概的权值,counter中的值表示进程在一
   个时间片内,剩下要运行的时间.*/
   weight = p->;;counter;
   if (!weight) /* weight==0,表示该进程的时间片已经用完,则直接转到标号out*/
   goto out;
   #ifdef __SMP__
   /*在SMP情况下,如果进程将要运行的CPU与进程上次运行的CPU是一样的,则最有利,因此,
   假如进程上次运行的CPU与当前CPU一致的话,权值加上PROC_CHANGE_PENALTY,这个宏定义为
   20。*/
   if (p->;;processor == this_cpu)
   weight += PROC_CHANGE_PENALTY;
   #endif
   if (p->;;mm == this_mm) /*进程p与当前运行进程,是同一个进程的不同线程,或者是
   共享地址空间的不同进程,优先选择,权值加1*/
   weight += 1;
   weight += p->;;priority; /* 权值加上进程的优先级*/
   out:
   return weight; /* 返回值作为进程调度的唯一依据,谁的权值大,就调度谁运行*/
}

*这个是2.4版本的,2.6后就没有goodness,也就不用这种方式进行调度,关于2.6以后再看。

5.进程调度的实现

  调度程序在内核中就是一个函数,schedule()函数的作用是,选择一个合适的进程在CPU上执行,它仅仅根据‘goodness‘来工作。对于SMP情况,除了计算每个进程的加权平均运行时间外,其他与SMP相关的部分主要由goodness()函数来体现。

asmlinkage void schedule(void)
{
    struct task_struct *prev, *next, *p; /* prev表示调度之前的进程,next表示调度
    之后的*/
    
    struct list_head *tmp;
    int this_cpu, c;
    if (!current->active_mm) BUG();/*如果当前进程的 active_mm 为空,出错*/
    need_resched_back:
    prev = current; /*让 prev 成为当前进程 */
    this_cpu = prev->processor;
    if (in_interrupt()){/*如果schedule是在中断服务程序内部执行,就说明发生了错误*/
        printk("Scheduling in interrupt\n");
        BUG();
    }
    release_kernel_lock(prev, this_cpu); /*释放全局内核锁,并开this_cpu的中断*/
    spin_lock_irq(&runqueue_lock); /*锁住运行队列,并且同时关中断*/
    if (prev->policy == SCHED_RR) /*将一个时间片用完的 SCHED_RR 实时
    goto move_rr_last;              进程放到队列的末尾 */
    move_rr_back:
    switch (prev->state) {/*根据 prev 的状态做相应的处理*/
        case TASK_INTERRUPTIBLE: /*此状态表明该进程可以被信号中断*/
        if (signal_pending(prev)) { /*如果该进程有未处理的信号,则让其                                 
        prev->state = TASK_RUNNING;         变为可运行状态*/
        break;
        }
        default: /*如果为可中断的等待状态或僵死状态*/
        del_from_runqueue(prev); /*从运行队列中删除*/
        case TASK_RUNNING:;/*如果为可运行状态,继续处理*/
    }
    prev->need_resched = 0;
    /*下面是调度程序的正文 */
    repeat_schedule: /*真正开始选择值得运行的进程*/
    next = idle_task(this_cpu); /*缺省选择空闲进程*/
    c = -1000;
    if (prev->state == TASK_RUNNING)
        goto still_running;
    still_running_back:
    list_for_each(tmp,&runqueue_head){ /*遍历运行队列*/
        p = list_entry(tmp, struct task_struct, run_list);
        if (can_schedule(p, this_cpu)){ /*单 CPU中,该函数总返回1*/
        int weight = goodness(p, this_cpu, prev->active_mm);
        if (weight > c)
        c = weight, next = p;
        }
    }
    /* 如果c为0,说明运行队列中所有进程的权值都为0,也就是分配给各个进程的时间片都已
    用完,需重新计算各个进程的时间片 */
    if(!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;
    }
    spin_unlock_irq(&runqueue_lock);/*对运行队列解锁,并开中断*/
    if (prev == next) { /*如果选中的进程就是原来的进程*/
        prev->policy &= ~SCHED_YIELD;
        goto same_process;
    }
    /* 下面开始进行进程切换*/
    kstat.context_swtch++; /*统计上下文切换的次数*/
    {
        struct mm_struct *mm = next->mm;
        struct mm_struct *oldmm = prev->active_mm;
        if (!mm) { /*如果是内核线程,则借用 prev 的地址空间*/
            if (next->active_mm) BUG();
            next->active_mm = oldmm;
        } 
        else { /*如果是一般进程,则切换到 next 的用户空间*/
            if (next->active_mm != mm) BUG();
            switch_mm(oldmm, mm, next, this_cpu);
        }
        if (!prev->mm) { /*如果切换出去的是内核线程*/
            prev->active_mm = NULL;/*归还它所借用的地址空间*/
            mmdrop(oldmm); /*mm_struct 中的共享计数减 1*/
        }
    }
    switch_to(prev, next, prev); /*进程的真正切换,即堆栈的切换*/
    __schedule_tail(prev); /*置 prev->policy 的 SCHED_YIELD 为 0 */
    same_process:
    reacquire_kernel_lock(current);/*针对 SMP*/
    if (current->need_resched) /*如果调度标志被置位*/
     goto need_resched_back; /*重新开始调度*/
    return;
}

本文出自 “三天不读书,智商不如猪!” 博客,谢绝转载!

以上是关于进程调度与切换简单总结的主要内容,如果未能解决你的问题,请参考以下文章

进程的切换和系统的一般执行过程

2017-2018-1 20179215《Linux内核原理与分析》第九周作业

五种进程调度算法的总结;

多线程相关知识总结

操作系统王道考研 p13 进程调度的时机切换与过程调度方式

线程学习知识点总结