今天看了一個關於中斷例程為什麼不能休眠的文章,引發了我的思考。其實這個問題在學習驅動的時候早就應該解決了,但是由於5年前學驅動的時候屬於Linux初學者,能力有限,所以對這個問題就知其然,沒有能力知其所以然。現在回頭看這個問題的時候,感覺應該可以有一個較為清晰的認識了。
首先必須意識到:休眠是一種程式的特殊狀態(即task->state= TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)]
一、休眠的目的
簡單的說,休眠是為在一個當前程式等待暫時無法獲得的資源或者一個event的到來時(原因),避免當前程式浪費CPU時間(目的),將自己放入程式等待佇列中,同時讓出CPU給別的程式(工作)。休眠就是為了更好地利用CPU。
一旦資源可用或event到來,將由核心程式碼(可能是其他程式通過系統呼叫)喚醒某個等待佇列上的部分或全部程式。從這點來說,休眠也是一種程式間的同步機制。
二、休眠的物件
休眠是針對程式,也就是擁有task_struct的獨立個體。
當程式執行某個系統呼叫的時候,暫時無法獲得的某種資源或必須等待某event的到來,在這個系統呼叫的底層實現程式碼就可以通過讓系統排程的手段讓出CPU,讓當前程式處於休眠狀態。
- 程式什麼時候會被休眠?
程式進入休眠狀態,必然是他自己的程式碼中呼叫了某個系統呼叫,而這個系統呼叫中存在休眠程式碼。這個休眠程式碼在某種條件下會被啟用,從而讓改變程式狀態,說到底就是以各種方式包含了:
1、條件判斷語句
2、程式狀態改變語句
3、schedule();
三、休眠操作做了什麼
程式被置為休眠,意味著它被標識為處於一個特殊的狀態(TASK_UNINTERRUPTIBLE或 TASK_INTERRUPTIBLE),並且從排程器的執行佇列中移走。這個程式將不在任何 CPU 上被排程,即不會被執行。 直到發生某些事情改變了那個狀態(to TASK_WAKING)。這時處理器重新開始執行此程式,此時程式會再次檢查是否需要繼續休眠(資源是否真的可用?),如果不需要就做清理工作,並將自己的狀態調整為TASK_RUNNING。
四、誰來喚醒休眠程式
程式在休眠後,就不再被排程器執行,就不可能由自己喚醒自己,也就是說程式不可能“睡覺睡到自然醒”。喚醒工作必然是由其他程式或者核心本身來完成的。喚醒需要改變程式的task_struct中的狀態等,程式碼必然在核心中,所以喚醒必然是在系統呼叫的實現程式碼中(如你驅動中的read、write方法)以及各種形式的中斷程式碼(包括軟、硬中斷)中。
如果在系統呼叫程式碼中喚醒,則說明是由其他的某個程式來呼叫了這個系統呼叫喚醒了休眠的程式。
如果是中斷中喚醒,那麼喚醒的任務可以說是核心完成了。
- 如何找到需要喚醒的程式:等待佇列
上面其實已經提到了:休眠程式碼的一個工作就是將當前程式資訊放入一個等待佇列中。它其實是一個包含等待某個特定事件的所有程式相關資訊的連結串列。一個等待佇列由一個wait_queue_head_t 結構體來管理,其定義在中。
wait_queue_head_t 型別的資料結構如下:
1 2 3 4 5 |
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包含關於睡眠程式的資訊和喚醒函式。
1 2 3 4 5 6 7 8 9 10 11 |
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的定義被包含在了休眠巨集中。
休眠和喚醒的過程如下圖所示:
五、休眠和喚醒的程式碼簡要分析
下面我們簡單分析一下休眠與喚醒的核心原語。
1、休眠:wait_event
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
/** * wait_event - 休眠,直到 condition 為真 * @wq: 所休眠的等待佇列 * @condition: 所等待事件的一個C表示式 * * 程式被置為等待狀態 (TASK_UNINTERRUPTIBLE) 直到 * @condition 評估為真. @condition 在每次等待佇列<a href="http://www.jobbole.com/members/wangchao8098">@wq</a> 被喚醒時 * 都被檢查。 * * 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) |
2、喚醒:wake_up
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#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的函式其實都是在喚醒後的條件判斷上有些不同,多判斷一些喚醒條件而已。這裡就不再贅述了。
六、使用休眠的注意事項
(1) 永遠不要在原子上下文中進入休眠,即當驅動在持有一個自旋鎖、seqlock或者 RCU 鎖時不能睡眠;關閉中斷也不能睡眠,終端例程中也不可休眠。
持有一個訊號量時休眠是合法的,如果程式碼在持有一個訊號量時睡眠,任何其他的等待這個訊號量的執行緒也會休眠。發生在持有訊號量時的休眠必須短暫,而且決不能阻塞那個將最終喚醒你的程式。
(2)當程式被喚醒,它並不知道休眠了多長時間以及休眠時發生什麼;也不知道是否另有程式也在休眠等待同一事件,且那個程式可能在它之前醒來並獲取了所等待的資源。所以不能對喚醒後的系統狀態做任何的假設,並必須重新檢查等待條件來確保正確的響應。
(3)除非確信其他程式會在其他地方喚醒休眠的程式,否則也不能睡眠。使程式可被找到意味著:需要維護一個等待佇列的資料結構。它是一個程式連結串列,其中包含了等待某個特定事件的所有程式的相關資訊。
七、不可在中斷例程中休眠的原因
如果在某個系統呼叫中把當前程式休眠,是有明確目標的,這個目標就是過來call這個系統呼叫的程式(注意這個程式正在running)。
但是中斷和程式是非同步的,在中斷上下文中,當前程式大部分時候和中斷程式碼可能一點關係都沒有。要是在這裡呼叫了休眠程式碼,把當前程式給休眠了,那就極有可能把無關的程式休眠了。再者,如果中斷不斷到來,會殃及許多無辜的程式。
在中斷中休眠某個特定程式是可以實現的,通過核心的task_struct連結串列可以找到的,不論是根據PID還是name。但是隻要這個程式不是當前程式,休眠它也可能沒有必要。可能這個程式本來就在休眠;或者正在執行佇列中但是還沒執行到,如果執行到他了可能又無須休眠了。
還有一個原因是中斷也是所謂的原子上下文,有的中斷例程中會禁止所有中斷,有的中斷例程還會使用自旋鎖等機制,在其中使用休眠也是非常危險的。 下面會介紹。
八、不可在持有自旋鎖、seqlock、RCU 鎖或關閉中斷時休眠的原因
其實自旋鎖、seqlock、RCU 鎖或關閉中斷期間的程式碼都稱為原子上下文,比較有代表性的就是自旋鎖spinlock。
對於UP系統,如果A程式在擁有spinlock時休眠,這個程式在擁有自旋鎖後主動放棄了處理器。其他的程式就開始使用處理器,只要有一個程式B去獲取同一個自旋鎖,B必然無法獲取,並做所謂的自旋等待。由於自旋鎖禁止所有中斷和搶佔,B的自旋等待是不會被打斷的,並且B也永遠獲得不了鎖。因為B在CPU中執行,沒有其他程式可以執行並喚醒A並釋放鎖。系統就此鎖死,只能復位了。
對於SMP系統,如果A程式在擁有spinlock時休眠,這個程式在擁有自旋鎖後主動放棄了處理器。如果所有處理器都為了獲取這個鎖而自旋等待,由於自旋鎖禁止所有中斷和搶佔,,就不會有程式可能去喚醒A了,系統也就鎖死了。
並不是所一旦系統獲得自旋鎖休眠就會死,而是有這個可能。但是注意了計算機的執行速度之快,只要有億分之一的可能,也是很容易發生
所有的原子上下文都有這樣的共性:不可在其中休眠,否則系統極有可能鎖死。
只要對其裝置節點做兩次讀寫操作,系統必死。我在X86 的SMP系統,ARMv5、ARMv6、ARMv7中都做了如下的實驗(單核(UP)系統必須配置CONFIG_DEBUG_SPINLOCK,否則自旋鎖是沒有實際效果(起碼不會有“自旋”), 系統可以多次獲取自旋鎖,沒有實驗效果。之後博文中有詳細描述)。現象都和上面敘述的死法相同,看了原始碼就知道(關鍵在readwrite方法)。以下是實驗記錄:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# 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) |