細說程序為什麼需要睡眠?

程序是作業系統中的 執行上下文。說白了,執行上下文 包括了要執行的程式碼與其相關的資源。要執行的程式碼比較容易理解,就是我們編寫的程式程式碼,例如:
int

 total = 

10

 + 

20

;

上面的程式碼將 10 加 20 的結果賦值給 total 變數,這就是程序要執行的程式碼。
而程序相關的資源包括:使用的記憶體開啟的檔案使用的CPU時間 等等。執行的程式碼與其相關的資源組成了 執行上下文,也稱為 程序。如果將人比作為程序的話,那麼我們的日常行為對應的就是執行程式碼,而我們擁有的各種社會資源(如金錢、房產、車子等)對應的就是程序佔用的資源。

程序為什麼需要睡眠

由於 CPU 是執行程式碼的主體,所以執行程序的程式碼需要佔用 CPU 時間。但有時候程序的執行需要某些資源提供資料來源,而這些資源可能需要從外部獲取。如在網路程式中,伺服器需要等待客戶端發生請求才能進行下一步操作。但伺服器什麼時候接收到客戶端的請求是不確定的,有可能一整天也沒有收到客戶端的請求。
服務端程式可以透過兩種方式等待客戶端的請求:忙等待 和 睡眠與通知

1. 忙等待

忙等待 是指透過死迴圈來不斷檢測資料是否準備好,如下面虛擬碼:
for

 (;;) {

if

 (如果資料準備好) {

        讀取資料;

break

;

    }

}

從上面程式碼可以看出,忙等待需要消耗非常多的 CPU 時間來檢測資料是否準備好。
舉個日常生活中 忙等待 的例子,例如我們在外面吃飯,當餐廳生意非常好的時候,我們可能需要等位。在等位的過程中,我們需要透過不斷詢問服務員來了解空位的狀況。

2. 睡眠與喚醒

可以看出,忙等待是一個非常低效和浪費時間的方式。那麼有沒有更高效的方式呢?答案是肯定的。
我們還是以在餐廳等位作為例子,如果餐廳提供一個通知客人的裝置,當有空位時,服務員可以透過這個裝置通知等待中的客人。那麼,客人就不需要不斷去詢問服務員空位的狀況,可以利用等待這段時間小瞌一會。
類似的,服務端程式在等待客戶端請求到來這段時間,作業系統可以先把服務端程序掛起(睡眠),然後執行其他可執行的程序。當有客戶端請求到來時,作業系統喚醒服務端程序來處理客戶端的請求。
如下圖所示:
當 程序M 在等待系統中某些資源變為就緒狀態時,作業系統會把 程序M 的從 CPU 中切換出去,然後把 程序M 防止到睡眠佇列中。接著作業系統會從可執行佇列中,選擇一個最合適的程序(如圖中的 程序1)排程到 CPU 中執行。
當 程序M 等待的資源變為就緒狀態後,作業系統作業系統便會把 程序M 放置回可執行佇列中。這樣,程序M 就可以在下一個排程週期中爭奪 CPU 的執行時間。如下圖所示:
在上圖中,當客戶端請求到來後,作業系統便會喚醒等待客戶端請求的 程序M,然後把其放回到可執行佇列中。
這個就是作業系統中的睡眠與喚醒機制。

Linux 的睡眠與喚醒機制實現

在 Linux 核心中,很多系統呼叫和核心函式都可能會導致程序睡眠,如 I/O 相關的系統呼叫、sleep 類核心函式、記憶體分配函式等。
下面我們以 sleep 類核心函式來分析 Linux 是如何實現睡眠與喚醒機制的。

1. 睡眠函式的使用

在 Linux 核心中,如果想讓一個程序進入睡眠狀態,可以呼叫 schedule_timeout_interruptible() 核心函式。其原型如下:
signedlong __sched schedule_timeout_interruptible(signedlong timeout)

;

引數 timeout 表示希望程序睡眠多長時間,此函式會讓程序睡眠 timeout 個時鐘節拍(tick)的時間。例如,如果希望程序睡眠 1 秒,可以使用如下程式碼實現:

...

schedule_timeout_interruptible(

1

 * HZ);

// 1秒後進程被喚醒,繼續執行下面程式碼

...

2. 睡眠函式的實現

接下來,我們來分析一下 schedule_timeout_interruptible() 核心函式是如何讓程序進入睡眠狀態的。其程式碼如下所示:
signedlong

 __sched 

schedule_timeout_interruptible(signedlong timeout)

{

    __set_current_state(TASK_INTERRUPTIBLE);

return

 schedule_timeout(timeout);

}

schedule_timeout_interruptible() 核心函式主要做了如下兩件事情:
  • 呼叫 __set_current_state() 函式將程序設定為可 中斷睡眠狀態。需要注意的是,這個步驟只是將程序的狀態設定為可中斷睡眠狀態,但此時程序還沒有被核心排程程式移出 CPU。
  • 呼叫 schedule_timeout() 函式使程序真正進入睡眠狀態(放棄 CPU 的使用許可權)。
從上面程式碼可以看出,schedule_timeout() 函式才是使程序進入睡眠的主體。那麼,我們繼續來分析schedule_timeout() 函式的實現:
signedlong

 __sched 

schedule_timeout(signedlong timeout)

{

structtimer_listtimer;
unsignedlong

 expire;
    ...

    expire = timeout + jiffies;

// 1. 將當前程序新增到定時器中,定時器的超時時間為expire,回撥函式為process_timeout

    setup_timer_on_stack(&timer, process_timeout, (

unsignedlong

)current);

    __mod_timer(&timer, expire, 

false

, TIMER_NOT_PINNED);

// 2. 主動觸發核心進行程序排程

    schedule();

// 3. 將程序從定時器中刪除

    del_singleshot_timer_sync(&timer);

    destroy_timer_on_stack(&timer);
    timeout = expire - jiffies;
 out:

return

 timeout < 

0

 ? 

0

 : timeout;

}

schedule_timeout() 函式的邏輯主要分為以下三個步驟:
  • 將當前程序新增到定時器中,定時器的超時時間設定為 expire,回撥函式為 process_timeout()。那麼當定時器超時時,便會觸發呼叫 process_timeout() 函式。
  • 呼叫 schedule() 函式觸發核心進行程序排程。由於當前程序在 schedule_timeout_interruptible() 函式中被設定為 可中斷睡眠狀態,所以當排程器發現當前程序是 可中斷睡眠狀態 時,將會把當前程序移出可執行佇列,並且讓出 CPU 的使用許可權。
  • 當程序被喚醒後,將會把程序從定時器中刪除。
從上面的分析可知,在呼叫 schedule_timeout() 函式時,核心會為當前程序建立一個定時器,其超時時間被設定為 schedule_timeout() 函式傳入的引數加上當前時間。當定時器到期後,便會觸發呼叫 process_timeout() 函式,而 process_timeout() 函式最終會呼叫 try_to_wake_up() 函式來喚醒程序。
我們接著來分析下 try_to_wake_up() 函式的實現,看看其如何喚醒程序的:
staticint
try_to_wake_up(struct task_struct *p, unsignedint state, int wake_flags)

{

unsignedlong

 flags;

int

 cpu, success = 

0

;
    ...

// 1. 為程序挑選一個最合適的 CPU 執行

    cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags);

    ...

// 2. 把程序新增到 CPU 的可執行佇列中

    ttwu_queue(p, cpu);

    ...

return

 success;

}

在上面的程式碼中,我們只保留了核心的程式碼。可以看出 try_to_wake_up() 函式主要完成 2 件事情:
  • 呼叫 select_task_rq() 函式為程序挑選一個最合適的 CPU 執行。
  • 呼叫 ttwu_queue() 函式把程序新增到 CPU 的可執行佇列中。
被喚醒的程序新增到 CPU 的可執行佇列後,並不會立即被執行。核心會在下一個排程週期中,選擇合適的程序進行排程時,被喚醒的程序才有可能被選中執行。

總結

本文主要介紹了程序為什麼需要有睡眠和喚醒功能,並且分析了程序睡眠與喚醒的實現原理。程序睡眠與喚醒功能主要為了解決程序在等待某些資源變為可用時,需要不斷探測資源的狀態。這種探測既白白浪費了寶貴的 CPU 時間,而且還影響了系統的吞吐量。
而程序睡眠可以在程序等待資源變為可用狀態時,主動放棄 CPU 的使用許可權,這時 CPU 便可執行其他可執行的程序,從而使 CPU 的利用率達到最優。當程序等待的資源變為可用時,核心主動喚醒等待中的程序,程序便可以繼續執行。
推薦閱讀點選標題可跳轉

相關文章