1. 程式人生 > >分析linux程序排程與程序切換

分析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() 可以找到排程函式被呼叫的位置:

       

       

       

       

二、GDB追蹤schedule()函式的執行

          

程序排程時,首先進入schedule()函式,將一個task_struct結構體的指標tsk賦值為當前程序。 然後呼叫sched_submit_work(tsk) 我們進入這個函式,檢視一下做了什麼工作 

在執行到sched_submit_work時,輸入si進入函式。

          

可以看到這個函式時檢測tsk->state是否為0 (runnable)若為執行態時則返回, tsk_is_pi_blocked(tsk),檢測tsk的死鎖檢測器是否為空,若非空的話就return。

           

然後檢測是否需要重新整理plug佇列,用來避免死鎖。 sched_submit_work主要是來避免死鎖。 
然後我們進入__schedule()函式。

             

三、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 彙編程式碼分析:

                        

該巨集的工作步驟大致如下:

  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編號。

     

per_cup巨集展開如下。__switch_to函式中傳入init_tss引數和CPU編號cpu給per_cpu巨集,然後得到該CPU上的TSS指標。

     

load_esp0函式定義如下。這裡從thread_struct結構中載入esp0到tss中,即即將執行程序的esp0。當SEP開啟時,還用wrmsr寫入新的CS段選擇子——即sysenter指令執行後的程式碼段。

                       

  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)。

                      

現在來看__switch_to函式,其定義和註釋如下。

                       

四、總結

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