Linux系統休眠

http://2482696735.bokee.com/503154878.html
html


休眠是一種進程的特殊狀態(即task->state= TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)]
1、休眠的目的
簡單的說,休眠是爲在一個當前進程等待暫時沒法得到的資源或者一個event的到來時(緣由),避免當前進程浪費CPU時間(目的),將本身放入進程等待隊列中,同時讓出CPU給別的進程(工做)。休眠就是爲了更好地利用CPU。
一旦資源可用或event到來,將由內核代碼(多是其餘進程經過系統調用)喚醒某個等待隊列上的部分或所有進程。從這點來講,休眠也是一種進程間的同步機制。
2、休眠的對象
休眠是針對進程,也就是擁有task_struct的獨立個體。
當進程執行某個系統調用的時候,暫時沒法得到的某種資源或必須等待某event的到來,在這個系統調用的底層實現代碼就能夠經過讓系統調度的手段讓出CPU,讓當前進程處於休眠狀態。
進程何時會被休眠?
進程進入休眠狀態,必然是他本身的代碼中調用了某個系統調用,而這個系統調用中存在休眠代碼。這個休眠代碼在某種條件下會被激活,從而讓改變進程狀態,說到底就是以各類方式包含了:
一、條件判斷語句
二、進程狀態改變語句
三、schedule();
3、休眠操做作了什麼
進程被置爲休眠,意味着它被標識爲處於一個特殊的狀態(TASK_UNINTERRUPTIBLE或 TASK_INTERRUPTIBLE),而且從調度器的運行隊列中移走。這個進程將不在任何 CPU 上被調度,即不會被運行。 直到發生某些事情改變了那個狀態(to TASK_WAKING)。這時處理器從新開始執行此進程,此時進程會再次檢查是否須要繼續休眠(資源是否真的可用?),若是不須要就作清理工做,並將本身的狀態調整爲TASK_RUNNING。過程以下圖所示:
4、誰來喚醒休眠進程
進程在休眠後,就再也不被調度器執行,就不可能由本身喚醒本身,也就是說進程不可能「睡覺睡到天然醒」。喚醒工做必然是由其餘進程或者內核自己來完成的。喚醒須要改變進程的task_struct中的狀態等,代碼必然在內核中,因此喚醒必然是在系統調用的實現代碼中(如你驅動中的read、write方法)以及各類形式的中斷代碼(包括軟、硬中斷)中。
若是在系統調用代碼中喚醒,則說明是由其餘的某個進程來調用了這個系統調用喚醒了休眠的進程。
若是是中斷中喚醒,那麼喚醒的任務能夠說是內核完成了。
 
· 如何找到須要喚醒的進程:等待隊列
上面其實已經提到了:休眠代碼的一個工做就是將當前進程信息放入一個等待隊列中。它實際上是一個包含等待某個特定事件的全部進程相關信息的鏈表。一個等待隊列由一個wait_queue_head_t 結構體來管理,其定義在中。
wait_queue_head_t 類型的數據結構以下:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
它包含一個自旋鎖和一個鏈表。這是一個等待隊列鏈表頭,鏈表中的元素被聲明作wait_queue_t。自旋鎖用於包含鏈表操做的原子性。
wait_queue_t包含關於睡眠進程的信息和喚醒函數。
typedef struct __wait_queue wait_queue_t;
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);
int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01         /* 表示等待進程想要被獨佔地喚醒 */
void *private;                 /* 指向等待進程的task_struct結構圖 */
wait_queue_func_t func;         /* 用於喚醒等待進程的處理例程,在其中實現了進程狀態的改變和將本身從等待隊列中刪除的工做 */
struct list_head task_list;         /* 雙向鏈表結構體,用於將wait_queue_t連接到wait_queue_head_t */
};
他們在內存中的結構大體以下圖所示:
等待隊列頭wait_queue_head_t通常是定義在模塊或內核代碼中的全局變量,而其中連接的元素 wait_queue_t的定義被包含在了休眠宏中。
休眠和喚醒的過程以下圖所示:
5、休眠和喚醒的代碼簡要分析
下面咱們簡單分析一下休眠與喚醒的內核原語。
一、休眠:wait_event
/**
* wait_event - 休眠,直到 condition 爲真
* @wq : 所休眠的等待隊列
* @condition : 所等待事件的一個C表達式
*
* 進程被置爲等待狀態 (TASK_UNINTERRUPTIBLE) 直到
* @condition 評估爲真. @condition 在每次等待隊列@wq 被喚醒時
* 都被檢查。
*
* wake_up() 必須在改變任何可能影響等待條件結果
* 的變量以後被調用。
*/
#define wait_event(wq, condition) \
do { \
if (condition) \
break; \
先測試條件,看看是否真的須要休眠
__wait_event(wq, condition); \
} while (0)
 
#define __wait_event(wq, condition) \
do { \
DEFINE_WAIT(__wait); \
定義一個插入到等待隊列中的等待隊列結構體,注意.private = current,(即當前進程)
#define DEFINE_WAIT_FUNC(name, function) \
wait_queue_t name = { \
.private = current, \
.func = function, \
.task_list = LIST_HEAD_INIT((name).task_list), \
}
#define DEFINE_WAIT(name) DEFINE_WAIT_FUNC(name, autoremove_wake_function)
\
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
將上面定義的結構體__wait放入wq等待隊列中,並設置當前進程狀態爲TASK_UNINTERRUPTIBLE
if (condition) \
break; \
測試條件狀態,看看是否真的須要休眠調度
schedule(); \
開始調度,程序停於此處,直到有其餘進程喚醒本進程,就今後處繼續......
} \
finish_wait(&wq, &__wait); \
因爲測試條件狀態爲假,跳出以上循環後執行休眠後的掃尾工做:
設置當前進程狀態爲TASK_RUNNING
將上面定義的__wait從等待隊列鏈表中刪除。
} while (0)
二、喚醒:wake_up
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
/**
* __wake_up - 喚醒阻塞在等待隊列上的線程.
* @q: 等待隊列
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*
* It may be assumed that this function implies a write memory barrier before
* changing the task state if and only if any tasks are woken up.
*/
void __wake_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(__wake_up);
kernel/sched.c
/*
* 核心喚醒函數.非獨佔喚醒(nr_exclusive == 0) 只是
* 喚醒全部進程. If it's an exclusive wakeup (nr_exclusive == small +ve
* number) then we wake all the non-exclusive tasks and one exclusive task.
*
* There are circumstances in which we can try to wake a task which has already
* started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
* zero in this (rare) case, and we handle it by continuing to scan the queue.
*/
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
遍歷指定等待隊列中的wait_queue_t.
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
調用喚醒函數,也就是建立wait_queue_t時的 autoremove_wake_function
}
}
int autoremove_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
int ret = default_wake_function(wait, mode, sync, key);
if (ret)
list_del_init(&wait->task_list);
從等待隊列中刪除這個進程
return ret;
}
EXPORT_SYMBOL(autoremove_wake_function);
 
int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
void *key)
{
return try_to_wake_up(curr->private, mode, wake_flags);
主要是要改變進程狀態爲 TASK_WAKING,讓調度器能夠從新執行此進程。
}
EXPORT_SYMBOL(default_wake_function);
上面分析的休眠函數是最簡單的休眠喚醒函數,其餘相似的函數,如後綴爲_timeout、_interruptible、_interruptible_timeout的函數其實都是在喚醒後的條件判斷上有些不一樣,多判斷一些喚醒條件而已。這裏就再也不贅述了。
6、使用休眠的注意事項
(1) 永遠不要在原子上下文中進入休眠,即當驅動在持有一個自旋鎖、seqlock或者 RCU 鎖時不能睡眠;關閉中斷也不能睡眠,終端例程中也不可休眠。持有一個信號量時休眠是合法的,若是代碼在持有一個信號量時睡眠,任何其餘的等待這個信號量的線程也會休眠。發生在持有信號量時的休眠必須短暫,並且決不能阻塞那個將最終喚醒你的進程。
(2)當進程被喚醒,它並不知道休眠了多長時間以及休眠時發生什麼;也不知道是否另有進程也在休眠等待同一事件,且那個進程可能在它以前醒來並獲取了所等待的資源。因此不能對喚醒後的系統狀態作任何的假設,並必須從新檢查等待條件來確保正確的響應。
(3)除非確信其餘進程會在其餘地方喚醒休眠的進程,不然也不能睡眠。使進程可被找到意味着:須要維護一個等待隊列的數據結構。它是一個進程鏈表,其中包含了等待某個特定事件的全部進程的相關信息。
7、不可在中斷例程中休眠的緣由
若是在某個系統調用中把當前進程休眠,是有明確目標的,這個目標就是過來call這個系統調用的進程(注意這個進程正在running)。
可是中斷和進程是異步的,在中斷上下文中,當前進程大部分時候和中斷代碼可能一點關係都沒有。要是在這裏調用了休眠代碼,把當前進程給休眠了,那就極有可能把無關的進程休眠了。再者,若是中斷不斷到來,會殃及許多無辜的進程。
在中斷中休眠某個特定進程是能夠實現的,經過內核的task_struct鏈表能夠找到的,不管是根據PID仍是name。可是隻要這個進程不是當前進程,休眠它也可能沒有必要。可能這個進程原本就在休眠;或者正在執行隊列中可是還沒執行到,若是執行到他了可能又無須休眠了。
還有一個緣由是中斷也是所謂的原子上下文,有的中斷例程中會禁止全部中斷,有的中斷例程還會使用自旋鎖等機制,在其中使用休眠也是很是危險的。 下面會介紹。
8、不可在持有自旋鎖、seqlock、RCU 鎖或關閉中斷時休眠的緣由
其實自旋鎖、seqlock、RCU 鎖或關閉中斷期間的代碼都稱爲原子上下文,比較有表明性的就是自旋鎖spinlock。
對於UP系統,若是A進程在擁有spinlock時休眠,這個進程在擁有自旋鎖後主動放棄了處理器。其餘的進程就開始使用處理器,只要有一個進程B去獲取同一個自旋鎖,B必然沒法獲取,並作所謂的自旋等待。因爲自旋鎖禁止全部中斷和搶佔,B的自旋等待是不會被打斷的,而且B也永遠得到不了鎖。由於B在CPU中運行,沒有其餘進程能夠運行並喚醒A並釋放鎖。系統就此鎖死,只能復位了。
對於SMP系統,若是A進程在擁有spinlock時休眠,這個進程在擁有自旋鎖後主動放棄了處理器。若是全部處理器都爲了獲取這個鎖而自旋等待,因爲自旋鎖禁止全部中斷和搶佔,,就不會有進程可能去喚醒A了,系統也就鎖死了。
並非所一旦系統得到自旋鎖休眠就會死,而是有這個可能。可是注意了計算機的運行速度之快,只要有億分之一的可能,也是很容易發生。
全部的原子上下文都有這樣的共性:不可在其中休眠,不然系統極有可能鎖死。
若是你對此還有懷疑,眼見爲實。我編寫了一個故意鎖死系統的及其簡單的驅動:
只要對其設備節點作兩次讀寫操做,系統必死。我在X86 的SMP系統,ARMv五、ARMv六、ARMv7中都作了以下的實驗(單核(UP)系統必須配置CONFIG_DEBUG_SPINLOCK,不然自旋鎖是沒有實際效果(起碼不會有「自旋」), 系統能夠屢次獲取自旋鎖,沒有實驗效果。以後博文中有詳細描述)。現象都和上面敘述的死法相同,看了源碼就知道(關鍵在read\write方法)。如下是實驗記錄:
# insmod spin_lock_sleep.ko
spin_lock sleep module loaded!
# cat /proc/devices
Character devices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
81 video4linux
89 i2c
90 mtd
116 alsa
128 ptm
136 pts
252 spin_lock_sleep
253 ttyO
254 rtc
Block devices:
1 ramdisk
259 blkext
7 loop
8 sd
11 sr
31 mtdblock
65 sd
66 sd
67 sd
68 sd
69 sd
70 sd
71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
# mknod spin_lock_sleep c 252 0
# cat spin_lock_sleep
spin_lock_sleep_read:prepare to get spin_lock! PID:1227
spin_lock_sleep_read:have got the spin_lock! PID:1227
spin_lock_sleep_read:prepare to sleep! PID:1227
spin_lock_sleep_write:prepare to get spin_lock! PID:1229
BUG: spinlock cpu recursion on CPU#0, sh/1229
lock: dd511c3c, .magic: dead4ead, .owner: cat/1227, .owner_cpu: 0
Backtrace:
[] (dump_backtrace+0x0/0x118) from [] (dump_stack+0x20/0x24)
r7:00000002 r6:dd511c3c r5:dd511c3c r4:dd7ef000
[] (dump_stack+0x0/0x24) from [] (spin_bug+0x94/0xa8)
[] (spin_bug+0x0/0xa8) from [] (do_raw_spin_lock+0x6c/0x160)
r5:bf04c408 r4:dd75e000
[] (do_raw_spin_lock+0x0/0x160) from [] (_raw_spin_lock+0x18/0x1c)
[] (_raw_spin_lock+0x0/0x1c) from [] (spin_lock_sleep_write+0xb4/0x190 [spin_lock_sleep])
[] (spin_lock_sleep_write+0x0/0x190 [spin_lock_sleep]) from [] (vfs_write+0xb8/0xe0)
r6:dd75ff70 r5:400d7000 r4:dd43bf00
[] (vfs_write+0x0/0xe0) from [] (sys_write+0x4c/0x78)
r7:00000002 r6:dd43bf00 r5:00000000 r4:00000000
[] (sys_write+0x0/0x78) from [] (ret_fast_syscall+0x0/0x48)
r8:c005a5a8 r7:00000004 r6:403295e8 r5:400d7000 r4:00000002
此時在另外一個終端(ssh、telnet等)中執行命令:
echo 'l' > spin_lock_sleep
BUG: spinlock lockup on CPU#0, sh/1229, dd511c3c
Backtrace:
[] (dump_backtrace+0x0/0x118) from [] (dump_stack+0x20/0x24)
r7:dd75e000 r6:dd511c3c r5:00000000 r4:00000000
[] (dump_stack+0x0/0x24) from [] (do_raw_spin_lock+0x120/0x160)
[] (do_raw_spin_lock+0x0/0x160) from [] (_raw_spin_lock+0x18/0x1c)
[] (_raw_spin_lock+0x0/0x1c) from [] (spin_lock_sleep_write+0xb4/0x190 [spin_lock_sleep])
[] (spin_lock_sleep_write+0x0/0x190 [spin_lock_sleep]) from [] (vfs_write+0xb8/0xe0)
r6:dd75ff70 r5:400d7000 r4:dd43bf00
[] (vfs_write+0x0/0xe0) from [] (sys_write+0x4c/0x78)
r7:00000002 r6:dd43bf00 r5:00000000 r4:00000000
[] (sys_write+0x0/0x78) from [] (ret_fast_syscall+0x0/0x48)
r8:c005a5a8 r7:00000004 r6:403295e8 r5:400d7000 r4:00000002
而你在這樣原子環境中休眠調度,內核一旦檢測到(主要是檢測到關閉了搶佔),你可能會看到以下信息,警告你:
# cat spin_lock_sleep
spin_lock_sleep_read:prepare to get spin_lock! PID:540
spin_lock_sleep_read:have got the spin_lock! PID:540
spin_lock_sleep_read:prepare to sleep! PID:540
BUG: scheduling while atomic: cat/540/0x00000002
Modules linked in: spin_lock_sleep
[] (unwind_backtrace+0x0/0xe4) from [] (schedule+0x74/0x36c)
[] (schedule+0x74/0x36c) from [] (spin_lock_sleep_read+0xe8/0x1bc [spin_lock_sleep])
[] (spin_lock_sleep_read+0xe8/0x1bc [spin_lock_sleep]) from [] (vfs_read+0xac/0x154)
[] (vfs_read+0xac/0x154) from [] (sys_read+0x3c/0x68)
[] (sys_read+0x3c/0x68) from [] (ret_fast_syscall+0x0/0x2c)linux

相關文章
相關標籤/搜索