1. 程式人生 > >wait_event族函式淺析

wait_event族函式淺析

週末閒暇無事,聊聊核心中的wait_event*類函式的具體實現,等待事件必定涉及到某個條件,而這些函式的區別主要是等待後喚醒的方式……直奔主題,上原始碼

wait_event_interruptible

複製程式碼
#define wait_event_interruptible(wq, condition)                \
({                                    \
    int __ret = 0;                            \
    if (!(condition))                        \
        __wait_event_interruptible(wq, condition, __ret);    \
    __ret;                                \
})
複製程式碼

呼叫該巨集首先會先檢查條件,如果條件已經滿足,則不用等了呀,返回吧……,否則呼叫__wait_event_interruptible

複製程式碼
#define __wait_event_interruptible(wq, condition, ret)            \
do {                                    \
    DEFINE_WAIT(__wait);                        \
                                    \
    for
(;;) { \ prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE); \ if (condition) \ break; \ if (!signal_pending(current)) { \ schedule(); \
continue; \ } \ ret = -ERESTARTSYS; \ break; \ } \ finish_wait(&wq, &__wait); \ } while (0)
複製程式碼

 首先聲明瞭一個關聯當前程序的wait物件,然後進入一個for空迴圈,開始就呼叫prepare_to_wait,該函式程式碼如下,功能就是把wait物件加入到等待佇列並設定當前程序的狀態,注意此刻僅僅是設定了結構體的狀態,並沒有觸發排程。在真正觸發排程之前,需要再次檢查條件是否滿足,如果滿足了,直接break,否則檢查當前程序是否有訊號存在,因為該巨集設定的等待是可以被訊號喚醒的,如果有訊號則同樣break。否則就呼叫schedule進行排程吧!其他種類的額wait函式都是同樣的結構,區別就在於for迴圈內內部的不同,所以後續主要列舉的是for迴圈的程式碼。

複製程式碼
void
prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
    unsigned long flags;

    wait->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&q->lock, flags);
    /*保證只加入一次*/
    if (list_empty(&wait->task_list))
        __add_wait_queue(q, wait);
    set_current_state(state);
    spin_unlock_irqrestore(&q->lock, flags);
}
複製程式碼

wait_event_interruptible_timeout

該函式和前面類似,但是增加了等待時間,還是簡單看下__wait_event_interruptible_timeout的for迴圈

複製程式碼
for (;;) {                            \
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    \
        if (condition)                        \
            break;                        \
        if (!signal_pending(current)) {                \
            ret = schedule_timeout(ret);            \
            if (!ret)                    \
                break;                    \
            continue;                    \
        }                            \
        ret = -ERESTARTSYS;                    \
        break;                            \
    }            
複製程式碼

在沒有訊號的狀態下,是呼叫了schedule_timeout函式,該函式在排程之前會對當前程序設定一個定時器,並設定process_timeout回撥函式用於時間到了就執行該函式,自然該函式就是完成喚醒程序的功能。關於定時器,本文不做介紹。

wait_event_timeout

該函式和上面區別就是沒有了對訊號的判斷,並且設定程序狀態為TASK_UNINTERRUPTIBLE,其__wait_event_timeout中for迴圈如下

複製程式碼
for (;;) {                            \
        prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);    \
        if (condition)                        \
            break;                        \
        ret = schedule_timeout(ret);                \
        if (!ret)                        \
            break;                        \
    }        
複製程式碼

wait_event

該函式是最簡單的,設定狀態TASK_UNINTERRUPTIBLE,進行條件判斷,如果不符合就排程。沒有其他額外的操作。

 核心中的sleep函式分析

static inline void sleep(unsigned sec)
{
    current->state = TASK_INTERRUPTIBLE;
    schedule_timeout(sec * HZ);
}

這裡首先設定當前程序狀態為TASK_INTERRUPTIBLE,然後呼叫了schedule_timeout,引數就是設定的睡眠時間,單位是秒,乘以時鐘頻率HZ(每秒鐘發生時鐘中斷的次數)

複製程式碼
signed long __sched schedule_timeout(signed long timeout)
{
    struct timer_list timer;
    unsigned long expire;

    switch (timeout)
    {
    case MAX_SCHEDULE_TIMEOUT:
        /*
         * These two special cases are useful to be comfortable
         * in the caller. Nothing more. We could take
         * MAX_SCHEDULE_TIMEOUT from one of the negative value
         * but I' d like to return a valid offset (>=0) to allow
         * the caller to do everything it want with the retval.
         */
        schedule();
        goto out;
    default:
        /*
         * Another bit of PARANOID. Note that the retval will be
         * 0 since no piece of kernel is supposed to do a check
         * for a negative retval of schedule_timeout() (since it
         * should never happens anyway). You just have the printk()
         * that will tell you if something is gone wrong and where.
         */
        if (timeout < 0) {
            printk(KERN_ERR "schedule_timeout: wrong timeout "
                "value %lx\n", timeout);
            dump_stack();
            current->state = TASK_RUNNING;
            goto out;
        }
    }
    /*定時器到期時間*/
    expire = timeout + jiffies;
    /*設定定時器*/
    setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
    __mod_timer(&timer, expire, false, TIMER_NOT_PINNED);
    /*排程*/
    schedule();
    /*回來就刪除定時器*/
    del_singleshot_timer_sync(&timer);

    /* Remove the timer from the object tracker */
    destroy_timer_on_stack(&timer);

    timeout = expire - jiffies;
    /*timeout大於0意味這程序被提前喚醒*/
 out:
    return timeout < 0 ? 0 : timeout;
}
複製程式碼

這裡如果設定的睡眠時間足夠長,就不設定定時器,直接排程。否則如果設定的時間小於0,就列印下棧資訊,然後設定狀態會TASK_RUNNING就返回了。正常情況下就設定一個定時器,根據傳入的時間設定到期時間,然後再出發排程,這樣在這段時間過後如果定時器被有被出發就會由時鐘中斷觸發執行,看到定時器的回撥函式是process_timeout,其中呼叫了wake_up_process喚醒了睡眠的程序。建立定時器之後就呼叫__mod_timer啟用定時器。預設情況下是在當前CPU,但是如果當前CPU是空閒的即IDLE狀態,這種情況如果配置了動態時鐘會關閉週期時鐘,這樣會把定時器遷移到一個非空閒的CPU上。因為在動態時鐘下,並不是每個jiffies都會發生中斷,這樣有可能造成延遲。在選定CPU後如果不是當前CPU那麼需要對定時器做一些修改,然後呼叫internal_add_timer把定時器加入到管理向量陣列中。從這裡看沒有考慮高解析度定時器呀!!!

說明:在schedule函式中,如果當前程序非執行態,在允許搶佔的情況下,極有可能會把task從就緒佇列移除,而在喚醒程序的時候再重新加入到就緒佇列。

以馬內利!

參考:linux3.10.1原始碼