程序是作業系統中的
執行上下文
。說白了,執行上下文
包括了要執行的程式碼與其相關的資源。要執行的程式碼比較容易理解,就是我們編寫的程式程式碼,例如: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 的利用率達到最優。當程序等待的資源變為可用時,核心主動喚醒等待中的程序,程序便可以繼續執行。