schedule()函數(重點)

好了,前面的準備工作都做完了,我們就進入進程調度的主體程序——schedule()函數。

函數schedule()實現調度程序。它的任務是從運行隊列的鏈表rq中找到一個進程,並隨後將CPU分配給這個進程。schedule()可以由幾個內核控制路徑調用,可以採取直接調用或延遲調用(可延遲的)的方式。下面,我們就來詳細介紹。

1 直接調用

 

如果current進程因不能獲得必須的資源而要立刻被阻塞,就直接調用調度程序。在這種情況下,如何阻塞進程該進程的內核路徑呢?按下述步驟執行:

1.把current進程current插入適當的等待隊列,參見《非運行狀態進程的組織 》博文。

2.把current進程的狀態改爲TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。

3.調用schedule()。

4.檢查資源是否可用,如果不可用就轉到第2步。

5.一但資源可用就從等待隊列中刪除當前進程current。

內核路徑反覆檢查進程需要的資源是否可用,如果不可用,就調用schedule( )把CPU分配給其它進程。稍後,當調度程序再次允許把CPU分配給這個進程時,要重新檢查資源的可用性。這些步驟與wait_event( )所執行的步驟很相似,也與《非運行狀態進程的組織》博文的函數很相似。

許多反覆執行長任務的設備驅動程序也直接調用調度程序。每次反覆循環時,驅動程序都檢查TIF_NEED_RESCHED標誌,如果需要就調用schedule()自動放棄CPU。

 

2 延遲調用


延遲調用的方法是,把TIF_NEED_RESCHED標誌設置爲1(thread_info),在以後的某個時段調用調度程序schedule()。由於總是在恢復用戶態進程的執行之前檢查這個標誌的值,所以schedule()將在不久之後的某個時間被明確地調用。

延遲調用調度程序的典型例子,也是最重要的三個進程調度實務:
- 當 current 進程用完了它的CPU 時間片時,由scheduler_tick( )函數做延遲調用,前面的博文已經講得很清楚了。
- 當一個被喚醒進程的優先權比當前進程的優先權高時,由try_to_wake_up( )函數做延遲調用,前面的博文也已經講得很清楚了。
- 當發出系統調用sched_setscheduler( )時,有興趣的同志可以玩玩這個系統調用對應的函數庫。

下面,我們就來分析schedule函數到底做了些什麼工作。咱先把代碼擺出來,來自Linux-2.6.18/kernel/Sched.c:

 

asmlinkage void __sched schedule(void)
{
    struct task_struct *prev, *next;
    struct prio_array *array;
    struct list_head *queue;
    unsigned long long now;
    unsigned long run_time;
    int cpu, idx, new_prio;
    long *switch_count;
    struct rq *rq;

    if (unlikely(in_atomic() && !current->exit_state)) {
        printk(KERN_ERR "BUG: scheduling while atomic: "
            "%s/0x%08x/%d/n",
            current->comm, preempt_count(), current->pid);
        dump_stack();
    }
    profile_hit(SCHED_PROFILING, __builtin_return_address(0));

need_resched:
    preempt_disable();
    prev = current;
    release_kernel_lock(prev);
need_resched_nonpreemptible:
    rq = this_rq();

    if (unlikely(prev == rq->idle) && prev->state != TASK_RUNNING) {
        printk(KERN_ERR "bad: scheduling from the idle thread!/n");
        dump_stack();
    }

    schedstat_inc(rq, sched_cnt);
    spin_lock_irq(&rq->lock);
    now = sched_clock();
    if (likely((long long)(now - prev->timestamp) < NS_MAX_SLEEP_AVG)) {
        run_time = now - prev->timestamp;
        if (unlikely((long long)(now - prev->timestamp) < 0))
            run_time = 0;
    } else
        run_time = NS_MAX_SLEEP_AVG;

    run_time /= (CURRENT_BONUS(prev) ? : 1);

    if (unlikely(prev->flags & PF_DEAD))
        prev->state = EXIT_DEAD;

    switch_count = &prev->nivcsw;
    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
        switch_count = &prev->nvcsw;
        if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
                unlikely(signal_pending(prev))))
            prev->state = TASK_RUNNING;
        else {
            if (prev->state == TASK_UNINTERRUPTIBLE)
                rq->nr_uninterruptible++;
            deactivate_task(prev, rq);
        }
    }

    update_cpu_clock(prev, rq, now);

    cpu = smp_processor_id();
    if (unlikely(!rq->nr_running)) {
        idle_balance(cpu, rq);
        if (!rq->nr_running) {
            next = rq->idle;
            rq->expired_timestamp = 0;
            wake_sleeping_dependent(cpu);
            goto switch_tasks;
        }
    }

    array = rq->active;
    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;
    }

    idx = sched_find_first_bit(array->bitmap);
    queue = array->queue + idx;
    next = list_entry(queue->next, struct task_struct, run_list);

    if (!rt_task(next) && interactive_sleep(next->sleep_type)) {
        unsigned long long delta = now - next->timestamp;
        if (unlikely((long long)(now - next->timestamp) < 0))
            delta = 0;

        if (next->sleep_type == SLEEP_INTERACTIVE)
            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);
        }
    }
    next->sleep_type = SLEEP_NORMAL;
    if (dependent_sleeper(cpu, rq, next))
        next = rq->idle;
switch_tasks:
    if (next == rq->idle)
        schedstat_inc(rq, sched_goidle);
    prefetch(next);
    prefetch_stack(next);
    clear_tsk_need_resched(prev);
    rcu_qsctr_inc(task_cpu(prev));

    prev->sleep_avg -= run_time;
    if ((long)prev->sleep_avg <= 0)
        prev->sleep_avg = 0;
    prev->timestamp = prev->last_ran = now;

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

        prepare_task_switch(rq, prev, next);
        prev = context_switch(rq, prev, next);
        barrier();

        finish_task_switch(this_rq(), prev);
    } else
        spin_unlock_irq(&rq->lock);

    prev = current;
    if (unlikely(reacquire_kernel_lock(prev) < 0))
        goto need_resched_nonpreemptible;
    preempt_enable_no_resched();
    if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
        goto need_resched;
}

 

3 進程切換之前schedule()所做的工作


schedule()函數的任務之一是用另外一個進程來替換當前正在執行的進程。因此,該函數的關鍵結果是設置一個叫做next的變量,使它指向被選中的進程,該進程將取代當前進程。如果系統中沒有優先權高於當前進程的可運行進程,最終next與current相等,不發生任何進程切換。

schedule( )函數在一開始,先禁用內核搶佔並初始化一些局部變量:
need_resched:
    preempt_disable();
    prev = current;
    release_kernel_lock(prev);
need_resched_nonpreemptible:
    rq = this_rq();

正如你所見,把current返回的指針賦給prev,並把與本地CPU相對應的運行隊列數據結構的地址賦給rq。

下一步,schedule( )要保證prev不佔用大內核鎖(我們會在同步和互斥專題中詳細講解):
    if (prev->lock_depth >= 0)
        up(&kernel_sem);
注意,schedule( )不改變lock_depth 字段的值,當prev恢復執行的時候,如果該字段的值不等於負數,prev重新獲得kernel_flag 自旋鎖。因此,通過進程切換,會自動釋放和重新獲取大內核鎖。

繼續走,調用sched_clock( )函數以讀取TSC,並將它的值轉換成納秒,所獲得的時間戳存放在局部變量now中。然後,schedule( )計算prev所用的時間片長度:
    now = sched_clock( );
    run_time = now - prev->timestamp;
    if (run_time > 1000000000)
        run_time = 1000000000;
通常使用限制在1秒(要轉換成納秒)的時間。run_time的值用來限制進程對CPU的使用。不過,鼓勵進程有較長的平均睡眠時間:run_time /= (CURRENT_BONUS(prev) ? : 1);記住,CURRENT_BONUS返回0到10之間的值,它與進程的平均睡眠時間是成比例的。

在開始尋找可運行進程之前,schedule( )必須關掉本地中斷,並獲得所要保護的運行隊列的自旋鎖:
    spin_lock_irq(&rq->lock);

prev可能是一個正在被終止的進程。爲了確認這個事實,schedule( )檢查PF_DEAD標誌:
    if (prev->flags & PF_DEAD)
        prev->state = EXIT_DEAD;

接下來,schedule()檢查prev的狀態,如果不是可運行狀態,而且它沒有在內核態被搶佔,就應該從運行隊列刪除prev進程。不過,如果它是非阻塞掛起信號,而且狀態爲TASK_INTERRUPTIBLE,函數就把該進程的狀態設置爲TASK_RUNNING,並將它插入運行隊列。這個操作與把處理器分配給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( )從運行隊列中刪除該進程:
    rq->nr_running--;
    dequeue_task(p, p->array);
    p->array = NULL;

現在,schedule( )檢查運行隊列中剩餘的可運行進程數。如果有可運行的進程,schedule()就調用dependent_sleeper( )函數,在絕大多數情況下,該函數立即返回0。但是,如果內核支持超線程技術(見本後面“多處理器系統中運行隊列的平衡”博文),函數檢查要被選中執行的進程,其優先權是否比已經在相同物理CPU的某個邏輯CPU上運行的兄弟進程的優先權低,在這種特殊的情況下,schedule()拒絕選擇低優先權的進程,而去執行swapper進程。
    if (rq->nr_running) {
        if (dependent_sleeper(smp_processor_id( ), rq)) {
            next = rq->idle;
            goto switch_tasks;
        }
    }

如果運行隊列中沒有可運行的進程存在,函數就調用idle_balance( ),從另外一個運行隊列遷移一些可運行進程到本地運行隊列中,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( ) 沒有成功地把進程遷移到本地運行隊列中,schedule( )就調用wake_sleeping_dependent( )重新調度空閒CPU(即每個運行swapper進程的CPU)中的可運行進程。就象前面討論dependent_sleeper( )函數時所說明的,通常在內核支持超線程技術的時候可能會出現這種情況。然而,在單處理機系統中,或者當把進程遷移到本地運行隊列的種種努力都失敗的情況下,函數就選擇swapper進程作爲next進程並繼續進行下一步驟。

我們假設schedule( )函數已經肯定運行隊列中有一些可運行的進程,現在它必須檢查這些可運行進程中是否至少有一個進程是活動的,如果沒有,函數就交換運行隊列數據結構的active和expired字段的內容,因此,所有的過期進程變爲活動進程,而空集合準備接納將要過期的進程。
    array = rq->active;
    if (!array->nr_active) {
        rq->active = rq->expired;
        rq->expired = array;
        array = rq->active;
        rq->expired_timestamp = 0;
        rq->best_expired_prio = 140;
    }

現在可以在活動的prio_array_t數據結構中搜索一個可運行進程了。首先,schedule()搜索活動進程集合位掩碼的第一個非0位。回憶一下,當對應的優先權鏈表不爲空時,就把位掩碼的相應位置1。因此,第一個非0位的下標對應包含最佳運行進程的鏈表,隨後,返回該鏈表的第一個進程描述符:
    idx = sched_find_first_bit(array->bitmap);
    next = list_entry(array->queue[idx].next, task_t, run_list);

函數sched_find_first_bit( )是基於bsfl 彙編語言指令的,它返回32位字中被設置爲1的最低位的位下標。局部變量next現在存放將取代prev的進程描述符。schedule( )函數檢查next->activated字段,該字段的編碼值表示進程在被喚醒時的狀態,如表所示:

  值

說明

0

進程處於TASK_RUNNING 狀態。

1

進程處於TASK_INTERRUPTIBLE 或TASK_STOPPED 狀態,而且正在被系統調用服務例程或內核線程喚醒。

2

進程處於TASK_INTERRUPTIBLE 或TASK_STOPPED 狀態,而且正在被中斷處理程序或可延遲函數喚醒。

-1

進程處於TASK_UNINTERRUPTIBLE 狀態而且正在被喚醒。

 

如果next是一個普通進程而且它正在從TASK_INTERRUPTIBLE 或 TASK_STOPPED狀態被喚醒,調度程序就把自從進程插入運行隊列開始所經過的納秒數加到進程的平均睡眠時間中。換而言之,進程的睡眠時間被增加了,以包含進程在運行隊列中等待CPU所消耗的時間。
    if (next->prio >= 100 && next->activated > 0) {
        unsigned long long delta = now - next->timestamp;
        if (next->activated == 1)
            delta = (delta * 38) / 128;
        array = next->array;
        dequeue_task(next, array);
        recalc_task_prio(next, next->timestamp + delta);
        enqueue_task(next, array);
    }
    next->activated=0;

要說明的是,調度程序把被中斷處理程序和可延遲函數所喚醒的進程與被系統調用服務例程和內核線程所喚醒的進程區分開來,在前一種情況下,調度程序增加全部運行隊列等待時間。而在後一種情況下,它只增加等待時間的一部分。這是因爲交互式進程更可能被異步事件(考慮用戶在鍵盤上的按鍵操作)而不是同步事件喚醒。

 

4 schedule( )完成進程切換時所執行的操作


現在schedule( )函數已經要讓next 進程投入運行。內核將立刻訪問next 進程的thread_info數據結構,它的地址存放在next進程描述符的接近頂部的位置。
switch_tasks:
    prefetch(next);

prefetch 宏提示CPU控制單元把next進程描述符的第一部分字段的內容裝入硬件高速緩存,正是這一點改善了schedule()的性能,因爲對於後續指令的執行(不影響next),數據是並行移動的。

在替代prev之前,調度程序應該完成一些管理的工作:
    clear_tsk_need_resched(prev);
    rcu_qsctr_inc(prev->thread_info->cpu);
以防(萬一)以延遲方式調用schedule( ), clear_tsk_need_resched( )函數清除prev的TIF_NEED_RESCHED標誌。然後,函數記錄CPU正在經歷靜止狀態。

schedule( )函數還必須減少prev的平均睡眠時間,並把它補充給進程所使用的CPU時間片:
    prev->sleep_avg -= run_time;
    if ((long)prev->sleep_avg <= 0)
        prev->sleep_avg = 0;
    prev->timestamp = prev->last_ran = now;
隨後更新進程的時間戳。

prev 和next很可能是同一個進程:在當前運行隊列中沒有優先權較高或相等的其他活動進程時,會發生這種情況。在這種情況下,函數不做進程切換:
    if (prev == next) {
        spin_unlock_irq(&rq->lock);
        goto finish_schedule;
    }

之後,prev和next肯定是不同的進程了,那麼進程切換確實地發生了:
    next->timestamp = now;
    rq->nr_switches++;
    rq->curr = next;
    prev = context_switch(rq, prev, next);

context_switch( )函數建立next的地址空間:

static inline struct task_struct *context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
    struct mm_struct *mm = next->mm;
    struct mm_struct *oldmm = prev->active_mm;

    trace_sched_switch(rq, prev, next);

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

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

#ifndef __ARCH_WANT_UNLOCKED_CTXSW
    spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif

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

    return prev;
}

進程描述符的active_mm字段指向進程所使用的內存描述符,而mm字段指向進程所擁有的內存描述符。對於一般的進程,這兩個字段有相同的地址,但是,內核線程沒有它自己的地址空間,因而它的mm字段總是被設置爲NULL。context_switch( )函數保證:如果next是一個內核線程,它使用prev所使用的地址空間:
    if (!next->mm) {
        next->active_mm = prev->active_mm;
        atomic_inc(&prev->active_mm->mm_count);
        enter_lazy_tlb(prev->active_mm, next);
    }

一直到Linux 2.2 版,內核線程都有自己的地址空間。那種設計選擇不是最理想的,因爲不管什麼時候當調度程序選擇一個新進程(即使是一個內核線程)運行時,都必須改變頁表;因爲內核線程都運行在內核態,它僅使用線性地址空間的第4個GB,其映射對系統的所有進程都是相同的。甚至最壞情況下,寫cr3寄存器會使所有的TLB表項無效,這將導致極大的性能損失。現在的Linux具有更高的效率,因爲如果next是內核線程,就根本不觸及頁表。作爲進一步的優化,如果next是內核線程, schedule( )函數把進程設置爲懶惰TLB模式。

相反,如果next是一個普通進程,schedule( )函數用next的地址空間替換prev的地址空間:
    if (next->mm)
        switch_mm(prev->active_mm, next->mm, next);

如果prev是內核線程或正在退出的進程,context_switch( )函數就把指向prev內存描述符的指針保存到運行隊列的prev_mm 字段中,然後重新設置prev->active_mm:
    if (!prev->mm) {
        rq->prev_mm = prev->active_mm;
        prev->active_mm = NULL;
    }

現在,context_switch( )終於可以調用switch_to( )執行prev 和next之間的進程切換了(參見前面博文“執行進程間切換 ”):
    switch_to(prev, next, prev);
    return prev;

 

5 進程切換後schedule( )所執行的操作


schedule( )函數中在switch_to宏之後緊接着的指令並不由next進程立即執行,而是稍後當調度程序選擇prev又執行時由prev執行。然而,在那個時刻,prev局部變量並不指向我們開始描述schedule( )時所替換出去的原來那個進程,而是指向prev被調度時由prev替換出的原來那個進程。(如果你被搞糊塗,請回到“執行進程間切換 ”博文)。

進程切換後的第一部分指令是:
    barrier( );
    finish_task_switch(prev);

在schedule( )中,緊接着context_switch( )函數調用之後,宏barrier( )產生一個代碼優化屏障(以後博文會討論,這裏略過)。然後,執行finish_task_switch( )函數:
    mm = this_rq( )->prev_mm;
    this_rq( )->prev_mm = NULL;
    prev_task_flags = prev->flags;
    spin_unlock_irq(&this_rq( )->lock);
    if (mm)
        mmdrop(mm);
    if (prev_task_flags & PF_DEAD)
        put_task_struct(prev);

如果prev是一個內核線程,運行隊列的prev_mm 字段存放借給prev的內存描述符的地址。mmdrop( )減少內存描述符的使用計數器,如果該計數器等於0了,函數還要釋放與頁表相關的所有描述符和虛擬存儲區。

finish_task_switch( )函數還要釋放運行隊列的自旋鎖並打開本地中斷。然後,檢查prev 是否是一個正在從系統中被刪除的僵死任務, 如果是,就調用put_task_struct( )以釋放進程描述符引用計數器,並撤消所有其餘對該進程的引用。

schedule( )函數的最後一部分指令是:
//finish_schedule:
    prev = current;
    if (prev->lock_depth >= 0)
        __reacquire_kernel_lock( );
    preempt_enable_no_resched();
    if (test_bit(TIF_NEED_RESCHED, &current_thread_info( )->flags)
        goto need_resched;
    return;

如你所見,schedule( )在需要的時候重新獲得大內核鎖、重新啓用內核搶佔、並檢查是否一些其他的進程已經設置了當前進程的TIF_NEED_RESCHED標誌,如果是,整個schedule( )函數重新開始執行,否則,函數結束。

發佈了23 篇原創文章 · 獲贊 2 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章