1. 程式人生 > >Linux喚醒搶佔----Linux程序的管理與排程(二十三)

Linux喚醒搶佔----Linux程序的管理與排程(二十三)

1. 喚醒搶佔

當在try_to_wake_up/wake_up_process和wake_up_new_task中喚醒程序時, 核心使用全域性check_preempt_curr看看是否程序可以搶佔當前程序可以搶佔當前執行的程序. 請注意該過程不涉及核心排程器.

每個排程器類都因應該實現一個check_preempt_curr函式, 在全域性check_preempt_curr中會呼叫程序其所屬排程器類check_preempt_curr進行搶佔檢查, 對於完全公平排程器CFS處理的程序, 則對應由check_preempt_wakeup函式執行該策略.

新喚醒的程序不必一定由完全公平排程器處理, 如果新程序是一個實時程序, 則會立即請求排程, 因為實時程序優先極高, 實時程序總會搶佔CFS程序.

2 Linux程序的睡眠

在Linux中,僅等待CPU時間的程序稱為就緒程序,它們被放置在一個執行佇列中,一個就緒程序的狀 態標誌位為TASK_RUNNING. 一旦一個執行中的程序時間片用完, Linux 核心的排程器會剝奪這個程序對CPU的控制權, 並且從執行佇列中選擇一個合適的程序投入執行.

當然,一個程序也可以主動釋放CPU的控制權. 函式schedule()是一個排程函式, 它可以被一個程序主動呼叫, 從而排程其它程序佔用CPU. 一旦這個主動放棄CPU的程序被重新排程佔用CPU, 那麼它將從上次停止執行的位置開始執行, 也就是說它將從呼叫schedule()的下一行程式碼處開始執行.

有時候,程序需要等待直到某個特定的事件發生,例如裝置初始化完成、I/O 操作完成或定時器到時等. 在這種情況下, 程序則必須從執行佇列移出, 加入到一個等待佇列中, 這個時候程序就進入了睡眠狀態.

Linux 中的程序睡眠狀態有兩種

  • 一種是可中斷的睡眠狀態,其狀態標誌位TASK_INTERRUPTIBLE.

可中斷的睡眠狀態的程序會睡眠直到某個條件變為真, 比如說產生一個硬體中斷、釋放程序正在等待的系統資源或是傳遞一個訊號都可以是喚醒程序的條件.

  • 另一種是不可中斷的睡眠狀態,其狀態標誌位為TASK_UNINTERRUPTIBLE.

不可中斷睡眠狀態與可中斷睡眠狀態類似, 但是它有一個例外, 那就是把訊號傳遞到這種睡眠 狀態的程序不能改變它的狀態, 也就是說它不響應訊號的喚醒. 不可中斷睡眠狀態一般較少用到, 但在一些特定情況下這種狀態還是很有用的, 比如說: 程序必須等待, 不能被中斷, 直到某個特定的事件發生.

在現代的Linux作業系統中, 程序一般都是用呼叫schedule的方法進入睡眠狀態的, 下面的程式碼演示瞭如何讓正在執行的程序進入睡眠狀態。

sleeping_task = current;
set_current_state(TASK_INTERRUPTIBLE);
schedule();
func1();
/* Rest of the code ... */

3 linux程序的喚醒

當在try_to_wake_up/wake_up_processwake_up_new_task中喚醒程序時, 核心使用全域性check_preempt_curr看看是否程序可以搶佔當前程序可以搶佔當前執行的程序. 請注意該過程不涉及核心排程器.

3.1 wake_up_process

我們可以使用wake_up_process將剛才那個進入睡眠的程序喚醒, 該函式定義在kernel/sched/core.c, line 2043.

int wake_up_process(struct task_struct *p)
{
    return try_to_wake_up(p, TASK_NORMAL, 0);
}

在呼叫了wake_up_process以後, 這個睡眠程序的狀態會被設定為TASK_RUNNING,而且排程器會把它加入到執行佇列中去. 當然, 這個程序只有在下次被排程器排程到的時候才能真正地投入執行.

3.2 try_to_wake_up

try_to_wake_up函式通過把程序狀態設定為TASK_RUNNING, 並把該程序插入本地CPU執行佇列rq來達到喚醒睡眠和停止的程序的目的.

例如: 呼叫該函式喚醒等待佇列中的程序, 或恢復執行等待訊號的程序.

static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)

該函式接受的引數有: 被喚醒程序的描述符指標(p), 可以被喚醒的程序狀態掩碼(state), 一個標誌wake_flags,用來禁止被喚醒的程序搶佔本地CPU上正在執行的程序.

try_to_wake_up函式定義在kernel/sched/core.c, line 1906

3.3 wake_up_new_task

void wake_up_new_task(struct task_struct *p)

該函式定義在kernel/sched/core.c, line 2421

之前進入睡眠狀態的可以通過try_to_wake_up和wake_up_process完成喚醒, 而我們fork新建立的程序在完成自己的建立工作後, 可以通過wake_up_new_task完成喚醒工作, 參見Linux下程序的建立過程分析(_do_fork/do_fork詳解)–Linux程序的管理與排程(八)

使用fork建立程序的時候, 核心會呼叫_do_fork(早期核心對應do_fork)函式完成核心的建立, 其中在程序的資訊建立完畢後, 就可以使用wake_up_new_task將程序喚醒並新增到就緒佇列中等待排程. 程式碼參見kernel/fork.c, line 1755

3.4 check_preempt_curr

wake_up_new_task中喚醒程序時, 核心使用全域性check_preempt_curr看看是否程序可以搶佔當前程序可以搶佔當前執行的程序.

    check_preempt_curr(rq, p, WF_FORK);

函式定義在kernel/sched/core.c, line 905

void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
    const struct sched_class *class;

    if (p->sched_class == rq->curr->sched_class)
    {
        rq->curr->sched_class->check_preempt_curr(rq, p, flags);
    }
    else
    {
        for_each_class(class) {
            if (class == rq->curr->sched_class)
                break;
            if (class == p->sched_class) {
                resched_curr(rq);
                break;
            }
        }
    }

    /*
     * A queue event has occurred, and we're going to schedule.  In
     * this case, we can save a useless back to back clock update.
     */
    if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))
        rq_clock_skip_update(rq, true);
}

4 無效喚醒

4.1 無效喚醒的概念

幾乎在所有的情況下, 程序都會在檢查了某些條件之後, 發現條件不滿足才進入睡眠. 可是有的時候程序卻會在判定條件為真後開始睡眠, 如果這樣的話程序就會無限期地休眠下去, 這就是所謂的無效喚醒問題.

在作業系統中, 當多個程序都企圖對共享資料進行某種處理, 而最後的結果又取決於程序執行的順序時, 就會發生競爭條件, 這是作業系統中一個典型的問題, 無效喚醒恰恰就是由於競爭條件導致的.

設想有兩個程序A 和B, A 程序正在處理一個連結串列, 它需要檢查這個連結串列是否為空, 如果不空就對連結串列裡面的資料進行一些操作, 同時B程序也在往這個連結串列新增節點. 當這個連結串列是空的時候, 由於無資料可操作, 這時A程序就進入睡眠, 當B程序向連結串列裡面添加了節點之後它就喚醒A程序, 其程式碼如下:

A程序:

spin_lock(&list_lock);
if(list_empty(&list_head))
{
    spin_unlock(&list_lock);
    set_current_state(TASK_INTERRUPTIBLE);
    schedule();
    spin_lock(&list_lock);
}
/* Rest of the code ... */
spin_unlock(&list_lock);
}

B程序:

spin_lock(&list_lock);
list_add_tail(&list_head, new_node);
spin_unlock(&list_lock);
wake_up_process(A);

在這之後, A程序繼續執行, 它會錯誤地認為這個時候連結串列仍然是空的, 於是將自己的狀態設定為TASK_INTERRUPTIBLE然後呼叫schedule()進入睡眠. 由於錯過了B程序喚醒, 它將會無限期的睡眠下去, 這就是無效喚醒問題, 因為即使連結串列中有資料需要處理, A程序也還是睡眠了.

4.2 無效喚醒的原因

如何避免無效喚醒問題呢?

我們發現無效喚醒主要發生在檢查條件之後和程序狀態被設定為睡眠狀態之前, 本來B程序的wake_up_process提供了一次將A程序狀態置為TASK_RUNNING的機會,可惜這個時候A程序的狀態仍然是TASK_RUNNING,所以wake_up_process將A程序狀態從睡眠狀態轉變為執行狀態的努力沒有起到預期的作用.

4.3 避免無效搶佔

要解決這個問題, 必須使用一種保障機制使得判斷連結串列為空和設定程序狀態為睡眠狀態成為一個不可分割的步驟才行, 也就是必須消除競爭條件產生的根源, 這樣在這之後出現的wake_up_process就可以起到喚醒狀態是睡眠狀態的程序的作用了.

找到了原因後, 重新設計一下A程序的程式碼結構, 就可以避免上面例子中的無效喚醒問題了.

A程序

set_current_state(TASK_INTERRUPTIBLE);
spin_lock(&list_lock);
if(list_empty(&list_head))
{
    spin_unlock(&list_lock);
    schedule();
    spin_lock(&list_lock);
}
set_current_state(TASK_RUNNING);
/* Rest of the code ... */
spin_unlock(&list_lock);

可以看到,這段程式碼在測試條件之前就將當前執行程序狀態轉設定成TASK_INTERRUPTIBLE了, 並且在連結串列不為空的情況下又將自己置為TASK_RUNNING狀態.

這樣一來如果B程序在A程序程序檢查了連結串列為空以後呼叫wake_up_process, 那麼A程序的狀態就會自動由原來TASK_INTERRUPTIBLE變成TASK_RUNNING, 此後即使程序又呼叫了schedule, 由於它現在的狀態是TASK_RUNNING, 所以仍然不會被從執行佇列中移出, 因而不會錯誤的進入睡眠,當然也就避免了無效喚醒問題.

5 Linux核心的例子

5.1 一個最基本的例子

在Linux作業系統中, 核心的穩定性至關重要, 為了避免在Linux作業系統核心中出現無效喚醒問題, Linux核心在需要程序睡眠的時候應該使用類似如下的操作:

/* ‘q’是我們希望睡眠的等待佇列 */
DECLARE_WAITQUEUE(wait,current);
add_wait_queue(q, &wait);
set_current_state(TASK_INTERRUPTIBLE);

/* 或TASK_INTERRUPTIBLE */
while(!condition) /* ‘condition’ 是等待的條件*/
schedule();
set_current_state(TASK_RUNNING);
remove_wait_queue(q, &wait);

上面的操作, 使得程序通過下面的一系列步驟安全地將自己加入到一個等待佇列中進行睡眠: 首先呼叫DECLARE_WAITQUEUE建立一個等待佇列的項, 然後呼叫add_wait_queue()把自己加入到等待佇列中, 並且將程序的狀態設定為 TASK_INTERRUPTIBLE或者TASK_INTERRUPTIBLE.

然後迴圈檢查條件是否為真: 如果是的話就沒有必要睡眠, 如果條件不為真, 就呼叫schedule

當程序檢查的條件滿足後, 程序又將自己設定為TASK_RUNNING並呼叫remove_wait_queue將自己移出等待佇列.

從上面可以看到, Linux的核心程式碼維護者也是在程序檢查條件之前就設定程序的狀態為睡眠狀態,

然後才迴圈檢查條件. 如果在程序開始睡眠之前條件就已經達成了, 那麼迴圈會退出並用set_current_state將自己的狀態設定為就緒, 這樣同樣保證了程序不會存在錯誤的進入睡眠的傾向, 當然也就不會導致出現無效喚醒問題.

核心中有很多地方使用了避免無效喚醒的時候, 最普遍的地方是核心執行緒的, 因為核心執行緒的主要功能是輔助核心完成一定的工作的, 大多數情況下他們處於睡眠態, 當核心發現有任務要做的時候, 才會喚醒它們.

5.2 2號程序的例子-避免無效搶佔

下面讓我們用linux核心中的例項來看看Linux 核心是如何避免無效睡眠的, 我還記得2號程序吧, 它的主要工作就是接手核心執行緒kthread的建立, 其工作流程函式是kthreadd程式碼在kernel/kthread.c, kthreadd函式, line L514

for (;;) {
    set_current_state(TASK_INTERRUPTIBLE);
    if (list_empty(&kthread_create_list))
        schedule();
    __set_current_state(TASK_RUNNING);

    spin_lock(&kthread_create_lock);
    /*  ==do_something start==  */
    while (!list_empty(&kthread_create_list)) {
        struct kthread_create_info *create;

        create = list_entry(kthread_create_list.next,
                    struct kthread_create_info, list);
        list_del_init(&create->list);
        spin_unlock(&kthread_create_lock);

        create_kthread(create);
        /*  ==do_something end == */

        spin_lock(&kthread_create_lock);
    }
    spin_unlock(&kthread_create_lock);

5.2 kthread_worker_fn

kthread_worker/kthread_work是一種核心工作的更好的管理方式, 可以多個核心執行緒在同一個worker上工作, 共同完成work的工作, 有點像執行緒池的工作方式.

核心提供了kthread_worker_fn函式一般作為 kthread_create或者 kthread_run函式的 threadfn 引數執行, 可以將多個核心執行緒附加的同一個worker上面,即將同一個worker結構傳給kthread_run 或者kthread_create當作threadfn的引數就可以了.

其kthread_worker_fn函式作為worker的主函式框架, 也包含了避免無效喚醒的程式碼, kernel/kthread.c, kthread_worker_fn函式, line573, 如下所示

int kthread_worker_fn(void *worker_ptr)
{
    /* ......*/
    set_current_state(TASK_INTERRUPTIBLE);  /* mb paired w/ kthread_stop */

    if (kthread_should_stop()) {
        __set_current_state(TASK_RUNNING);
        spin_lock_irq(&worker->lock);
    worker->task = NULL;
    spin_unlock_irq(&worker->lock);
    return 0;
    }
    /* ......*/
}

此外核心的__kthread_parkme函式中也包含了類似的程式碼

6 總結

通過上面的討論, 可以發現在Linux 中避免程序的無效喚醒的關鍵是

  • 在程序檢查條件之前就將程序的狀態置為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
  • 並且如果檢查的條件滿足的話就應該將其狀態重新設定為TASK_RUNNING.

這樣無論程序等待的條件是否滿足, 程序都不會因為被移出就緒佇列而錯誤地進入睡眠狀態, 從而避免了無效喚醒問題.

set_current_state(TASK_INTERRUPTIBLE);
spin_lock(&list_lock);
if(list_empty(&list_head))
{
    spin_unlock(&list_lock);
    schedule();
    spin_lock(&list_lock);
}
set_current_state(TASK_RUNNING);
/* Rest of the code ... */
spin_unlock(&list_lock);