進程調(diào)度跟蹤分析

此文僅用于MOOCLinux內(nèi)核分析作業(yè)

張依依+原創(chuàng)作品轉(zhuǎn)載請注明出處 + 《Linux內(nèi)核分析》**
MOOC課程**http://mooc.study.163.com/course/USTC-1000029000


進程調(diào)度

Linux的調(diào)度程序是一個叫schedule()的函數(shù),這個函數(shù)被調(diào)用的頻率很高,由它來決定是否要進行進程的切換,如果要切換的話,切換到哪個進程等等。

Linux調(diào)度時機主要有:

  1. 中斷處理過程(包括時鐘中斷、I/O中斷、系統(tǒng)調(diào)用和異常)中,直接調(diào)用schedule(),或者返回用戶態(tài)時根據(jù)need_resched標(biāo)記調(diào)用schedule()
  2. 內(nèi)核線程可以直接調(diào)用schedule()進行進程切換,也可以在中斷處理過程中進行調(diào)度,也就是說內(nèi)核線程作為一類的特殊的進程可以主動調(diào)度,也可以被動調(diào)度;
  3. 用戶態(tài)進程無法實現(xiàn)主動調(diào)度,僅能通過陷入內(nèi)核態(tài)后的某個時機點進行調(diào)度,即在中斷處理過程中進行調(diào)度。

代碼分析

關(guān)鍵函數(shù)的調(diào)用關(guān)系:

schedule() --> context_switch() --> switch_to --> __switch_to()
  • schedule()

這里調(diào)用__schedule(),tsk為當(dāng)前進程.

asmlinkage __visible void __sched schedule(void)
{
    struct task_struct *tsk = current;

    sched_submit_work(tsk);
    __schedule();
}
  • __schedule();

該函數(shù)包含了一些:

  1. 針對搶占的處理
  2. 自旋鎖(raw_spin_lock_irq(&rq->lock);)
  3. 檢查prev的狀態(tài),并且重設(shè)state的狀態(tài)
  4. 進程調(diào)度算法(next = pick_next_task(rq, prev);)
  5. 更新就緒隊列的時鐘
  6. 進程上下文切換(context_switch(rq, prev, next);)
static void __sched __schedule(void)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq *rq;
    int cpu;

...
//調(diào)度算法
    next = pick_next_task(rq, prev);
    clear_tsk_need_resched(prev);
    clear_preempt_need_resched();
    rq->skip_clock_update = 0;

    if (likely(prev != next)) {
        rq->nr_switches++;
        rq->curr = next;
        ++*switch_count;

//進程上下文切換
        context_switch(rq, prev, next);
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
    } else
        raw_spin_unlock_irq(&rq->lock);

    post_schedule(rq);

    sched_preempt_enable_no_resched();
    if (need_resched())
        goto need_resched;
}

  • context_switch

在挑選得到了下一個即將被調(diào)度進來的進程之后,如果被選中的進程不是當(dāng)前正在運行的進程,那么需要進行上下文切換以執(zhí)行被選中的進程即context_switch.

context_switch中包含了:

  1. 判斷是否為內(nèi)核線程,即是否需要上下文切換(mm)
    • 如果next是一個普通進程,schedule( )函數(shù)用next的地址空間替換prev的地址空間
    • 如果prev是內(nèi)核線程或正在退出的進程,context_switch()函數(shù)就把指向prev內(nèi)存描述符的指針保存到運行隊列的prev_mm字段中,然后重新設(shè)置prev->active_mm
  2. 切換堆棧和寄存器(switch_to(prev, next, prev);)

ps:宏switch_to用來進行關(guān)鍵上下文切換

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
    struct mm_struct *mm, *oldmm;

    prepare_task_switch(rq, prev, next);

    mm = next->mm;
    oldmm = prev->active_mm;

    arch_start_context_switch(prev);

    if (!mm) {
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);
    } else
        switch_mm(oldmm, mm, next);

    if (!prev->mm) {
        prev->active_mm = NULL;
        rq->prev_mm = oldmm;
    }

    spin_release(&rq->lock.dep_map, 1, _THIS_IP_);

    context_tracking_task_switch(prev, next);
    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev);

    barrier();

    finish_task_switch(this_rq(), prev);
}

  • 宏switch_to

#define switch_to(prev, next, last)
do {

    unsigned long ebx, ecx, edx, esi, edi;

    asm volatile("pushfl\n\t"       /* save    flags */
             "pushl %%ebp\n\t"      /* save    EBP   */
             "movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */
             "movl %[next_sp],%%esp\n\t"    /* restore ESP   */
             "movl $1f,%[prev_ip]\n\t"  /* save    EIP   */
             "pushl %[next_ip]\n\t" /* restore EIP   */
             __switch_canary
             "jmp __switch_to\n"    /* regparm call  */
             "1:\t"
             "popl %%ebp\n\t"       /* restore EBP   */
             "popfl\n"          /* restore flags */

             /* output parameters */
             : [prev_sp] "=m" (prev->thread.sp),
               [prev_ip] "=m" (prev->thread.ip),
               "=a" (last),

               /* clobbered output registers: */
               "=b" (ebx), "=c" (ecx), "=d" (edx),
               "=S" (esi), "=D" (edi)

               __switch_canary_oparam

               /* input parameters: */
             : [next_sp]  "m" (next->thread.sp),
               [next_ip]  "m" (next->thread.ip),

               /* regparm parameters for __switch_to(): */  
               [prev]     "a" (prev),
               [next]     "d" (next)

               __switch_canary_iparam

             : /* reloaded segment registers */
            "memory");
} while (0)

這個宏實現(xiàn)了進程之間的真正切換:

  • 首先在當(dāng)前進程prev的內(nèi)核棧中保存esi,edi及ebp寄存器的內(nèi)容。
  • 然后將prev的內(nèi)核堆棧指針ebp存入prev->thread.esp中。
  • 把將要運行進程next的內(nèi)核棧指針next->thread.esp置入esp寄存器中
  • 將popl指令所在的地址保存在prev->thread.eip中,這個地址就是prev下一次被調(diào)度
  • 通過jmp指令(而不是call指令)轉(zhuǎn)入一個函數(shù)__switch_to()
  • 恢復(fù)next上次被調(diào)離時推進堆棧的內(nèi)容。從現(xiàn)在開始,next進程就成為當(dāng)前進程而真正開始執(zhí)行。

內(nèi)核堆棧情況:

stack1.png
stack2.png
stack3.png

  • __switch_to函數(shù)

在宏switch_to中,用jmp跳轉(zhuǎn)到該函數(shù)運行.

該函數(shù)主要進行一些針對TSS的操作,不再贅述

__visible __notrace_funcgraph struct task_struct *
__switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
    struct thread_struct *prev = &prev_p->thread,
                 *next = &next_p->thread;
    int cpu = smp_processor_id();
    struct tss_struct *tss = &per_cpu(init_tss, cpu);
    fpu_switch_t fpu;


    fpu = switch_fpu_prepare(prev_p, next_p, cpu);


    load_sp0(tss, next);


    lazy_save_gs(prev->gs);


    load_TLS(next, cpu);


    if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
        set_iopl_mask(next->iopl);


    task_thread_info(prev_p)->saved_preempt_count = this_cpu_read(__preempt_count);
    this_cpu_write(__preempt_count, task_thread_info(next_p)->saved_preempt_count);


    if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
             task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
        __switch_to_xtra(prev_p, next_p, tss);


    arch_end_context_switch(next_p);

    this_cpu_write(kernel_stack,
          (unsigned long)task_stack_page(next_p) +
          THREAD_SIZE - KERNEL_STACK_OFFSET);


    if (prev->gs | next->gs)
        lazy_load_gs(next->gs);

    switch_fpu_finish(next_p, fpu);

    this_cpu_write(current_task, next_p);

    return prev_p;
}

GDB調(diào)試

使用MenuOS進行調(diào)試,并設(shè)置合適的斷點.

  • 首先在schedule處停下來:
process1.png
  • 查看當(dāng)前進程tsk,觀察到該進程pid=1,stack=0xC7858000
process2.png
  • 繼續(xù)執(zhí)行,到__schedule中的關(guān)鍵函數(shù)pick_next_task停下
process3.png
  • 查看隊列rq
process4.png
  • context_switch
process5.png
  • switch_to宏&__switch_to函數(shù)
process6.png
  • 在這里查看切換的進程prev&next,prev就是最開始tsk
process7.png
process8.png

總結(jié)

  1. Linux的調(diào)度程序是一個叫schedule()的函數(shù),這個函數(shù)被調(diào)用的頻率很高,由它來決定是否要進行進程的切換,如果要切換的話,切換到哪個進程等等。
  2. Linux系統(tǒng)的一般執(zhí)行過程主要在進程X切換到進程Y
    • 正在運行的用戶態(tài)進程X
    • 發(fā)生中斷
    • SAVE_ALL
    • 中斷處理過程中或中斷返回前調(diào)用了schedule()
    • 開始運行用戶態(tài)進程Y
    • restore_all
    • iret
    • 繼續(xù)運行用戶態(tài)進程Y
  3. 內(nèi)核線程主動調(diào)用schedule(),只有進程上下文的切換
  4. switch_to實現(xiàn)了進程之間的真正切換

參考

  1. 進程管理之schedule->context_switch()
  2. 深入分析Linux內(nèi)核源碼
  3. Context switch in Linux
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容