休眠簡介
當一個進程被置入休眠時,它會被標記為一種特殊狀態,並從調度器的運行隊列中移走;直到某些情況下修改了這個狀態,進程才會在任意cpu上調度,即運行該進程;休眠中的進程會被擱置在一邊,等待將來的某個時間發生;
為了將進程以一種安全方式進入休眠,需要牢記下面的規則:
第一條規則,永遠不要在原子上下文中進入休眠;原子上下文是指下面這種狀態:在執行多個步驟時,不能有任何的並發訪問;這意味着,對休眠來講,我們的驅動程序不能再任何擁有自旋鎖,順序鎖或者RCU鎖的時候休眠;如果我們已經禁止了中斷,也不能休眠;在擁有信號量時休眠是合法的,但是必須仔細檢查有用信號量時休眠的代碼,如果代碼在擁有信號量時休眠,任何其他等待該信號量的線程也會休眠,因此任何擁有該信號量而休眠的代碼必須很短,並且還需要確保擁有信號量並不會阻塞最終喚醒我們自己的那個進程;
第二條規則:喚醒之后的狀態不能做任何假定,必須檢查以確保我們等待的條件為真;當從休眠中喚醒時,無法知道休眠了多長時間,以及休眠時發生了什么事情;通常也無法知道是否還有其他進程在同一事件上休眠,這個進程可能會在我們之前唄喚醒並將我們等待的資源拿走;
第三條規則:除非我們知道有其他人會在其他地方喚醒我們,否則進程不能進入休眠;喚醒任務的代碼必須能夠找到我們的進程,這樣才能喚醒休眠的進程;為確保喚醒發生,需要整體理解代碼,並清除的知道對每個休眠而言哪些事件序列會結束休眠;能夠找到休眠的進程意味着,需要維護一個稱謂等待隊列的數據結構;等待隊列就是一個進程鏈表,其中包含了要等待某個特定事件的所有進程;
Linux中,一個等待隊列通過一個“等待隊列頭”來管理,等待隊列頭是一個類型為wait_queue_head_t的結構體,定義在<linux/wait.h>中;
靜態定義和初始化一個等待隊列頭使用下面宏:
1 #define DECLARE_WAITQUEUE(name, tsk)
或者使用動態方法:
1 //wait_queue_head_t q 2 #define init_waitqueue_head(q)
簡單休眠
當進程休眠時,它將期待某個條件會在未來成真;而當一個進程被喚醒時,它必須再次檢查它所等待的條件的確為真;
Linux內核中最簡單的睡眠方法是wait_event宏,在實現休眠的同時,它也檢查進程等待的條件;
1 #define wait_event(wq, condition) 2 #define wait_event_timeout(wq, condition, timeout) 3 #define wait_event_interruptible(wq, condition) 4 #define wait_event_interruptible_timeout(wq, condition, timeout)
對應的喚醒應該使用wake_up宏,帶有interruptible的要配對使用;下面兩個函數會喚醒隊列上的所有非獨占進程,以及單個獨占進程;
1 #define wake_up(x) 2 #define wake_up_interruptible(x)
除了上述列出的方法,內核還定義了一些其他wait_event和wake_up類似的方法,具體在<linux/wait.h>中;其中wake_up相關函數與獨占等待有很多關聯;后面獨占等待部分詳細說明;
高級休眠
簡單休眠函數可以滿足很多驅動程序的休眠要求,但是在某些情況下,我們需要對Linux等待隊列的機制有更加深入的理解;復雜的鎖定以及性能需求會強制驅動程序使用底層的函數來實現休眠;
進程如何休眠-wait_event的內部原理
在<linux/wait.h>中,我們看到wait_queue_head_t類型的數據結構為一個自旋鎖和一個鏈表組成;鏈表中保存的是一個等待隊列的入口,該入口聲明為wait_queue_t類型,這結構中包含了休眠進程的信息以及其期望被喚醒的相關細節信息;
將進程置於休眠的步驟:
第一步,通常是分配並初始化一個wait_queue_t結構,然后將其加入到對應的等待隊列中,完成這些工作之后,不管誰負責喚醒該進程,都能找到正確的進程;
第二步,設置進程的狀態,將其標記為休眠;<linux/sched.h>定義了多個任務狀態,TASK_RUNNING表示進程可運行,盡管進程並不一定在任何給定時間都運行在某個處理器上;有兩個狀態表明進程處於休眠狀態:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE;顯然,它們分別對應於兩種休眠;
通常不需要驅動程序之間操作進程狀態,如果需要可以調用下面宏設置:
1 #define set_current_state(state_value)
通過修改當前狀態,我們只是改變了調度器處理該進程的方式,但尚使進程讓出處理器;
第三步,讓出處理器,放棄處理器是最后的步驟,但在此之前還需要做另外一件事,必須首先檢查休眠等待的條件;如果不做這個檢查,可能引入競態;試想,如果在上述過程中條件變成了真,而其他線程正在試圖喚醒我們,這時會發生什么呢?我們會丟掉被喚醒的機會,從而可能休眠更長時間;因此,深入休眠代碼,我們可以看到下面語句:
1 if (!condition) 2 schedule();
如果在等待的條件在設置進程狀態之前發生,我們會在這個檢查中注意到且不會真正的進入休眠;如果喚醒在其后發生,不管我們是否真正進入休眠,進程都會被置於可運行狀態;
對schedule()的調用將調用調度器,並讓出cpu;無論在什么時候調用這個函數,都將告訴內核重新選擇其他進程運行,並在必要時將控制切換到那個進程;這樣,我們無法知道,在調度返回到我們的代碼之前需要多少時間;
在上述if條件測試以及可能的schedule調用之后,需要完成一些清理工作;因為代碼不在期望休眠,因此必須確保任務狀態被重置為TASK_RUNNING;如果代碼從schedule中返回,則不需要這一步,但是如果因為不需要休眠而跳過了對schedule的調用,那么進程狀態是不正確的;並且需要將進程從等待隊列中移走,佛足額可能會被多次喚醒;
手工休眠
為了設置一些特殊的操作(比如設置獨占),也可以使用手工休眠的方式完成上述所有步驟;
第一步,初始化一個等待隊列入口,使用下面的方式靜態定義:
1 DEFINE_WATI(my_wait)
或者動態定義:
1 wait_queue_t my_wait; 2 init_wait(&my_wait);
第二步,將等待隊列入口添加到隊列中,並且設置進程的狀態:
1 void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
其中,q和wait為等待隊列頭和進程入口,state是進程的新狀態,它應該是TASK_INERRUPTIBLE或者TASK_UNINTERRUPTIBLE;
第三步,調用schedule,當然在這之前,需要確保有必要等待;
1 if (!condition) 2 schedule();
第四步,一旦schedule返回,就到了清理時間,這個工作可以通過下面的函數進行;
1 void finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
第五步,代碼可測試其狀態,並且判斷是否需要重新等待;
獨占等待
當某個進程在等待隊列上調用wake_up時,所有等待在該隊列上的進程都將被置為可運行狀態;假如,我們知道只會有一個被喚醒的進程可以獲得期望的資源,而其他唄喚醒的進程只會再次休眠,這些被喚醒的進程中每一個都要獲得處理器,為資源競爭,然后再次進入休眠;如果等待隊列中的進程數非常龐大,則這種行為將嚴重影響性能;
為了解決這個問題,內核中增加了“獨占等待”選項,一個獨占等待的行為和通常的休眠類似,但有如下兩個重要區別:
1. 等待隊列入口設置了WQ_FLAG_EXCLUSIVE標志,則會被添加到等待隊列的尾部,而沒有這個標志的入口則會被添加到頭部;
2. 在某個等待隊列上調用了wake_up時,它會在喚醒第一個具有WQ_FLAG_EXCLUSIVE標志的進程只會停止喚醒其他進程;(注意,因為非獨占的進程都在隊列前面,所以都會被喚醒)
使用獨占標記需要考慮兩個條件:
1. 對某個資源存在嚴重的競爭;
2. 喚醒單個進程就能完整消耗該資源;
將進程設置成獨占等待狀態可以調用prepare_wait_exclusive來設置入口的獨占標記,並將進程添加到等待隊列的尾部;
1 void prepare_to_wait_exclusive(wait_queue_head_t *q, wait_queue_t *wait, int state)
而wait_event以及其變種的方法無法執行獨占等待;
