分析linux进程调度与进程切换

慕课18原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000


一、Linux进程调度时机主要有

(1)主动调度:

  • 进程的运行状态发生变化时,例如等待某些事件而进入睡眠态;
  • 设备驱动程序

       主动调度随时都可以进行,一个进程可以调用schedule() 启动一次调度。从应用的角度来看,用户空间放弃运行是可见的,而在内核空间放弃运行是不可见的,它隐藏在其他可能受阻的系统调用中。几乎所有设计外设的的系统调用都可能受阻,如read(),write()等。

       进程要调用sleep()或exit()等函数进行状态转换,这些函数会主动调用调度程序进行进程调度

当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。

(2)被动调度:

  • 当前进程的时间片用完(会发生一个时钟中断)
  • 进程从中断、异常及系统调用返回到用户态时

       不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调用调度程序。在进入内核处理的时候,可能发生嵌套,这段时间可能使一些在睡眠态中等待的进程进入就绪态,如果这些进程的优先级比当前进程的优先级高,那么在从内核态回到用户态的时候自然就需要让出CPU,让高优先级的任务运行。

       每个时钟中断发生时,由三个函数协同工作,共同完成进程的选择和切换,它们是:schedule()、do_timer()及ret_form_sys_call()。我们先来解释一下这三个函数:

schedule():进程调度函数,由它来完成进程的选择

do_timer():启动定时器,在时钟中断服务程序中被调用,是时钟中断服务程序的主要组成部分,该函数被调用的频率就是时钟中断的频率即每秒钟100次(简称100赫兹或100Hz);

ret_from_sys_call():系统调用返回函数。当一个系统调用或中断完成时,该函数被调用,用于处理一些收尾工作,例如信号处理、核心任务等等。

ret_from_sys_call()函数中有如下几行:

cmpl $0, _need_resched

jne reschedule

……

restore_all:

RESTORE_ALL


reschedule:

call SYMBOL_NAME(schedule)

jmp ret_from_sys_call

在内核代码中搜索schedule() 可以找到调度函数被调用的位置:

       分析linux进程调度与进程切换_第1张图片

       分析linux进程调度与进程切换_第2张图片

       分析linux进程调度与进程切换_第3张图片

       

二、GDB追踪schedule()函数的执行

          分析linux进程调度与进程切换_第4张图片

进程调度时,首先进入schedule()函数,将一个task_struct结构体的指针tsk赋值为当前进程。 然后调用sched_submit_work(tsk) 我们进入这个函数,查看一下做了什么工作 

在执行到sched_submit_work时,输入si进入函数。

           分析linux进程调度与进程切换_第5张图片

可以看到这个函数时检测tsk->state是否为0 (runnable)若为运行态时则返回, tsk_is_pi_blocked(tsk),检测tsk的死锁检测器是否为空,若非空的话就return。

           

然后检测是否需要刷新plug队列,用来避免死锁。 sched_submit_work主要是来避免死锁。 
然后我们进入__schedule()函数。

              分析linux进程调度与进程切换_第6张图片


三、schedule函数分析:

1、在进程却换前,scheduler做的事情是用某一个进程替换当前进程。
(1)关闭内核抢占,初始化一些局部变量。

need_resched:

preempt_disable( );

prev = current;

rq = this_rq( );

当前进程current被保存在prev,和当前CPU相关的runqueue的地址保存在rq中。
(2)检查prev没有持有big kernel lock.
   if (prev->lock_depth >= 0)
    up(&kernel_sem);
   Schedule没有改变lock_depth的值,在prev唤醒自己执行的情况下,假如lock_depth的值不是负的,prev需要重新获取kernel_flag自旋锁。所以大内核锁在进程却换过程中是自动释放的和自动获取的。
(3)调用sched_clock( ),读取TSC,并且将TSC转换成纳秒,得到的timestamp保存在now中,然后Schedule计算prev使用的时间片。

now = sched_clock( );

run_time = now - prev->timestamp;

if (run_time > 1000000000)

    run_time = 1000000000;

(4)在察看可运行进程的时候,schedule必须关闭当前CPU中断,并且获取自旋锁保护runqueue.
            spin_lock_irq(&rq->lock);
(5)为了识别当前进程是否已终止,schedule检查PF_DEAD标志。
            if (prev->flags & PF_DEAD)    prev->state = EXIT_DEAD;
(6)Schedule检查prev的状态,假如他是不可运行的,并且在内核态没有被抢占,那么从runqueue删除他。但是,假如prev有非阻塞等待信号 并且他的状态是TASK_INTERRUPTBLE,配置其状态为TASK_RUNNING,并且把他留在runqueue中。该动作和分配CPU给 prev不相同,只是给prev一个重新选择执行的机会。

if (prev->state != TASK_RUNNING &&

    !(preempt_count() & PREEMPT_ACTIVE)) {

    if (prev->state == TASK_INTERRUPTIBLE && signal_pending(prev))

        prev->state = TASK_RUNNING;

    else {

        if (prev->state == TASK_UNINTERRUPTIBLE)

            rq->nr_uninterruptible++;

        deactivate_task(prev, rq);

    }

}

deactivate_task( )是从runqueue移除进程:

rq->nr_running--;

dequeue_task(p, p->array);

p->array = NULL;

(7)检查runqueue中进程数,
   A: 假如有多个可运行进程,调用dependent_sleeper( )函数。一般情况下,该函数立即返回0,但是假如内核支持超线程技术,该函数检查将被运行的进程是否有比已运行在同一个物理CPU上一个逻辑CPU上的兄 弟进程的优先级低。假如是,schedule拒绝选择低优先级进程,而是执行swapper进程。

if (rq->nr_running) {    

if (dependent_sleeper(smp_processor_id( ), rq)) 

{        next = rq->idle;        

         goto switch_tasks;    

}

}

   B:假如没有可运行进程,调用idle_balance( ),从其他runqueue队列中移动一些进程到当前runqueue,idle_balance( )和load_balance( )相似。

if (!rq->nr_running) {    idle_balance(smp_processor_id( ), rq);    

if (!rq->nr_running) {        next = rq->idle;       

 rq->expired_timestamp = 0;        

wake_sleeping_dependent(smp_processor_id( ), rq);        

if (!rq->nr_running)            goto switch_tasks;    

}

}

   假如idle_balance( )移动一些进程到当前runqueue失败,schedule( )调用wake_sleeping_dependent( )重新唤醒空闲CPU的可运行进程。
   假设schedule( )已决定runqueue中有可运行进程,那么他必须检查可运行进程中至少有一个进程是激活的。假如没有,交换runqueue中active 和expired域的内容,任何expired进程变成激活的,空数组准备接受以后expire的进程。

if (unlikely(!array->nr_active)) {

       / * Switch the active and expired arrays.  */

       schedstat_inc(rq, sched_switch);

       rq->active = rq->expired;

       rq->expired = array;

       array = rq->active;

       rq->expired_timestamp = 0;

       rq->best_expired_prio = MAX_PRIO;

    }

(8)查找在active prio_array_t数组中的可运行进程。Schedule在active数组的位掩码中查找第一个非0位。当优先级列表不为0的时候,相应的位掩码 北配置,所以第一个不为0的位标示一个有最合适进程运行的列表。然后列表中第一个进程描述符被获取。

idx = sched_find_first_bit(array->bitmap);

    queue = array->queue + idx;

    next = list_entry(queue->next, task_t, run_list);

   现在next指向将替换prev的进程描述符。
(9)检查next->activated,他标示唤醒进程的状态。
(10)假如next是个普通进程,并且是从TASK_INTERRUPTIBLE 或TASK_STOPPED状态唤醒。Scheduler在进程的平均睡眠时间上加从进程加入到runqueue开始的等待时间。

if (!rt_task(next) && next->activated > 0) {

        unsigned long long delta = now - next->timestamp;

        if (unlikely((long long)(now - next->timestamp) 

            delta = 0;

        if (next->activated == 1)

            delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;

        array = next->array;

        new_prio = recalc_task_prio(next, next->timestamp + delta);

        if (unlikely(next->prio != new_prio)) {

            dequeue_task(next, array);

            next->prio = new_prio;

            enqueue_task(next, array);

        } else

            requeue_task(next, array);

    }

    next->activated = 0;

   Scheduler区分被中断或被延迟函数唤醒的进程和被系统调用服务程式或内核线程唤醒的进程。前者,Scheduler加整个runqueue等待时间,后者只加一部分时间。
2、进程却换时,Scheduler做的事情:
现在,Scheduler已确定要运行的进程。
(1)访问next的thread_info,他的地址保存在next进程描述符的顶部。

switch_tasks:

  if (next == rq->idle)

     schedstat_inc(rq, sched_goidle);

  prefetch(next)

(2)在替换prev前,执行一些管理工作

clear_tsk_need_resched(prev);

rcu_qsctr_inc(task_cpu(prev));

   clear_tsk_need_resched清除prev的TIF_NEED_RESCHED,该动作只发生在Scheduler是被间接调用的情况。
(3)减少prev的平均睡眠时间到进程使用的cpu时间片。

    prev->sleep_avg -= run_time;

    if ((long)prev->sleep_avg 

        prev->sleep_avg = 0;

    prev->timestamp = prev->last_ran = now;

(4)检查是否prev和next是同一个进程,假如为真,放弃进程却换,否则,执行(5)

   if (prev == next) {

     spin_unlock_irq(&rq->lock);

     goto finish_schedule;

   }

(5) 真正的进程却换
       next->timestamp = now;
       rq->nr_switches++;
       rq->curr = next;
       ++*switch_count;
       prepare_task_switch(rq, next);
       prev = context_switch(rq, prev, next);
   context_switch 建立了next的地址空间,进程描述符的active_mm指向进程使用的地址空间描述符,而mm指向进程拥有的地址空间描述符,通常二者是相同的。但是 内核线程没有自己的地址空间,mm一直为NULL。假如next为内核线程,context_switch确保next使用prev的地址空间。假如 next是个正常的进程,context_switch使用next的替换prev的地址空间。

    struct mm_struct *mm = next->mm;

    struct mm_struct *oldmm = prev->active_mm;

    if (unlikely(!mm)) {

       next->active_mm = oldmm;

       atomic_inc(&oldmm->mm_count);

       enter_lazy_tlb(oldmm, next);

    } else

    switch_mm(oldmm, mm, next);

  假如prev是个内核线程或正在退出的进程,context_switch在runqueue的prev_mm中保存prev使用的内存空间。

    if (unlikely(!prev->mm)) {

       prev->active_mm = NULL;

       WARN_ON(rq->prev_mm);

       rq->prev_mm = oldmm;

    }

        调用switch_to(prev, next, prev)进行prev和next的转换。

 3、进程转换后的工作

(1)finish_task_switch():

struct mm_struct *mm = rq->prev_mm;         

unsigned long prev_task_flags;          

rq->prev_mm = NULL;         

 prev_task_flags = prev->flags;         

finish_arch_switch(prev);         

finish_lock_switch(rq, prev);         

if (mm)                

      mmdrop(mm);        

 if (unlikely(prev_task_flags & PF_DEAD))                 

     put_task_struct(prev)

   假如prev是内核线程,runqueue的prev_mm保存prev的内存空间描述符。 Mmdrop减少内存空间的使用数,假如该数为0,该函数释放内存空间描述符,连同和之相关的页表和虚拟内存空间。 finish_task_switch()还释放runqueue的自选锁,开中断。

(2)最后 

prev = current;         

if (unlikely(reacquire_kernel_lock(prev)                  

      goto need_resched_nonpreemptible;         

preempt_enable_no_resched();         

if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))                 

    goto need_resched;

   schedule获取大内核块,重新使内核能够抢占,并且检查是否其他进程配置了当前进程的TIF_NEED_RESCHED,假如真,重新执行schedule,否则该程式结束


三、switch_to 汇编代码分析:

                        分析linux进程调度与进程切换_第7张图片

该宏的工作步骤大致如下:

  1. prev的值送入eax,next的值送入edx(这里我从代码中没有看出来,原著上如是写,可能是从调用switch_to宏的switch_context或schedule函数中处理的)。
  2. 保护prev进程的eflags和ebp寄存器内容,这些内容保存在prev进程的内核堆栈中。
  3. 将prev的esp寄存器中的数据保存在prev->thread.esp中,即将prev进程的内核堆栈保存起来。
  4. 将next->thread.esp中的数据存入esp寄存器中,这是加载next进程的内核堆栈。
  5. 将数值1保存到prev->thread.eip中,该数值1其实就是代码中"1:\t"这行中的1。为了恢复prev进程执行时用。
  6. 将next->thread.eip压入next进程的内核堆栈中。这个值往往是数值1。
  7. 跳转到__switch_to函数处执行。
  8. 执行到这里,prev进程重新获得CPU,恢复prev进程的ebp和eflags内容。
  9. 将eax的内容存入last参数(这里我也没看出来,原著上如是写,只是在__switch_to函数中返回prev,该值是放在eax中的)。

__switch_to函数

  __switch_to函数采用FASTCALL调用模式,利用eax和edx传入两个参数的值。由于__switch_to中用了很多其他函数,这里首先介绍相关函数和宏,然后再讨论__switch_to函数。

   smp_process_id宏展开如下。该宏得到当前代码运行在哪个CPU上,返回CPU编号。current_thread_info函数返回当前运行着的进程的thread_info结构地址,该函数中让esp的值与上(THREAD_SIZE - 1)的逆,实际上,THREAD_SIZE - 1 = 8192 - 1 = 8191 = 0x1FFF,取反就是0xE000,就是让esp低13位清零,这样就通过内核堆栈得到thread_info结构地址。然后从该结构中得到cpu编号。

     分析linux进程调度与进程切换_第8张图片

per_cup宏展开如下。__switch_to函数中传入init_tss参数和CPU编号cpu给per_cpu宏,然后得到该CPU上的TSS指针。

     分析linux进程调度与进程切换_第9张图片

load_esp0函数定义如下。这里从thread_struct结构中加载esp0到tss中,即即将执行进程的esp0。当SEP打开时,还用wrmsr写入新的CS段选择子——即sysenter指令执行后的代码段。

                       分析linux进程调度与进程切换_第10张图片

  Load_TLS函数定义如下。该函数中使用宏C(i)和per_cpu,先通过per_cpu得到CPU的GDT所在内存的地址,然后将3个thread_struct结构中的tls_array加载到GDT的TLS段中。其中GDT_ENTRY_TLS_MIN=6,这正是3个TLS在GDT中的索引,因此Load_TLS就加载了GDT中3个线程局部段(TLS)。

                      分析linux进程调度与进程切换_第11张图片

现在来看__switch_to函数,其定义和注释如下。

                       分析linux进程调度与进程切换_第12张图片


四、总结

1、Linux 调度器将进程分为三类:

交互式进程

  此类进程有大量的人机交互,因此进程不断地处于睡眠状态,等待用户输入。典型的应用比如编辑器 vi。此类进程对系统响应时间要求比较高,否则用户会感觉系统反应迟缓。

批处理进程

  此类进程不需要人机交互,在后台运行,需要占用大量的系统资源。但是能够忍受响应延迟。比如编译器。

实时进程

  实时对调度延迟的要求最高,这些进程往往执行非常重要的操作,要求立即响应并执行。比如视频播放软件或飞机飞行控制系统,很明显这类程序不能容忍长时间的调度延迟,轻则影响电影放映效果,重则机毁人亡。

  根据进程的不同分类 Linux 采用不同的调度策略。对于实时进程,采用 FIFO 或者 Round Robin 的调度策略。对于普通进程,则需要区分交互式和批处理式的不同。传统 Linux 调度器提高交互式应用的优先级,使得它们能更快地被调度。而 CFS  RSDL 等新的调度器的核心思想是“完全公平”。这个设计理念不仅大大简化了调度器的代码复杂度,还对各种调度需求的提供了更完美的支持。


2、调度的发生主要有两种方式:

主动式调度(自愿调度)

  在内核中主动直接调用进程调度函数schedule(),当进程需要等待资源而暂时停止运行时,会把状态置于挂起(睡眠),并主动请求调度,让出cpu

被动式调度(抢占式调度、强制调度)

  用户抢占(在中断返回等过程中,从内核空间回到用户空间的时候)

  内核抢占(在内核进程中发生抢占)


3、schedule() --> context_switch() --> switch_to --> __switch_to()


4、、jmp和call 的区别

  call会把他的下一条指令的地址压入堆栈,然后跳转到他调用的开始处,同时ret会自动弹出返回地址。 

  JMP只是简单的跳转

  call的本质相当于push+jmp  ret的本质相当于pop+jmp



你可能感兴趣的:(linux内核分析,liunx相关)