1. 程式人生 > >schedule()函式(重點)

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( )函式重新開始執行,否則,函式結束。

最核心的schedule( )勝利結束!感謝黨,感謝人民!