linux線程同步淺析——睡眠與喚醒的祕密

http://blog.csdn.net/baiduforum/archive/2010/04/12/5475284.aspx

一個程序問題 
之前寫過這樣一個C程序:模塊維護一個工作線程、提供一組調用接口(分同步調用和異步調用)。用戶調用模塊提供的接口後,會向工作隊列添加一個任務。然後任務由工作線程來處理。在同步調用情況下,接口調用後調用者被阻塞,等待工作線程處理完成後,將調用者喚醒。僞代碼如下: 
[調用接口] 
add_command(cmd, pid); /* 1 */
raise(SIGSTOP); /* 2 */
get_response(cmd); /* 6 */
 
[工作線程] 
wait_for_command(&cmd, &pid); /* 3 */
do_command(cmd); /* 4 */
kill(pid, SIGCONT); /* 5 */
 
調用接口向工作隊列添加命令以後,向自己發送一個SIGSTOP信號,把自己掛起;工作線程處理命令完成,通過向調用者進程發送SIGCONT信號,將調用者喚醒。 
流程上還是比較清晰的,但是有點想當然了。測試發現,程序的執行流程可能變成下面的情況: 
[調用接口] 
add_command(cmd, pid); /* 1 */
raise(SIGSTOP); /* 5 ... */
get_response(cmd); 

[工作線程] 
wait_for_command(&cmd, &pid); /* 2 */
do_command(cmd); /* 3 */
kill(pid, SIGCONT); /* 4 */
 
調用者在添加命令後,發生調度,工作線程在調用者進入睡眠之前,先處理了命令併發出喚醒信號。之後,調用者再睡眠,就沒辦法被喚醒了。 
解決方法 
直接使用信號來實現睡眠和喚醒看來是不可取的,於是想到了使用pthread的互斥機制。改寫後的程序如下: 
[調用接口] 
add_command(cmd); /* 1 */
pthread_cond_wait(cond); /* 2 */
get_response(cmd); /* 6 */
 
[工作線程] 
wait_for_command(&cmd, &pid); /* 3 */
do_command(cmd); /* 4 */
pthread_cond_signal(cond); /* 5 */
 
測試發現,這樣做就不會出現由於調度而出現"先喚醒、後睡眠"的問題了。 
但是,pthread條件變量是如何避免"先喚醒、後睡眠"的呢?實際上,它依然無法避免調用者在添加命令後,由於調度,造成pthread_cond_signal先於pthread_cond_wait發生的問題。但是條件變量內部記錄了信號是否已發生,如果pthread_cond_signal先於pthread_cond_wait,則pthread_cond_wait將看到條件變量中記錄的"信號已發生",於是放棄睡眠。 
man一下pthread_cond_signal可以看到如下流程: 
[pthread_cond_wait(mutex, cond)] 
value = cond->value; /* 1 */
pthread_mutex_unlock(mutex); /* 2 */
pthread_mutex_lock(cond->mutex); /* 10 */
if (value == cond->value) { /* 11 */
me->next_cond = cond->waiter;
cond->waiter = me;
pthread_mutex_unlock(cond->mutex); /* X */
unable_to_run(me); /* Y */
} else
pthread_mutex_unlock(cond->mutex); /* 12 */
pthread_mutex_lock(mutex); /* 13 */
[pthread_cond_signal(cond)] 
pthread_mutex_lock(cond->mutex); /* 3 */
cond->value++; /* 4 */
if (cond->waiter) { /* 5 */
sleeper = cond->waiter; /* 6 */
cond->waiter = sleeper->next_cond; /* 7 */
able_to_run(sleeper); /* 8 */
}
pthread_mutex_unlock(cond->mutex); /* 9 */
這份僞代碼中的cond->value就是用於記錄"信號已發生"的變量。 
深入一點 
如果你足夠細心,可能已經發現上面的pthread的僞代碼是有問題的。在‘X'處,cond->value已經判斷過了,cond->mutex也已經釋放了,而unable_to_run(將進程掛起)還沒運行機制。那麼此時如果發生調度,pthread_cond_signal先運行了呢?是不是able_to_run(喚醒)又將發生在unable_to_run之前,而導致"先喚醒、後睡眠"呢? 
這就變成了下面的流程: 

[pthread_cond_wait(mutex, cond)] 
value = cond->value; /* 1 */
pthread_mutex_unlock(mutex); /* 2 */
pthread_mutex_lock(cond->mutex); /* 3 */
if (value == cond->value) { /* 4 */
me->next_cond = cond->waiter;
cond->waiter = me;
pthread_mutex_unlock(cond->mutex); /* 5 */
unable_to_run(me); /* 13 ... */
} else
pthread_mutex_unlock(cond->mutex);
pthread_mutex_lock(mutex);
[pthread_cond_signal(cond)] 
pthread_mutex_lock(cond->mutex); /* 6 (注意:5已經釋放鎖了) */
cond->value++; /* 7 */
if (cond->waiter) { /* 8 */
sleeper = cond->waiter; /* 9 */
cond->waiter = sleeper->next_cond; /* 10 */
able_to_run(sleeper); /* 11 */
}
pthread_mutex_unlock(cond->mutex); /* 12 */
 
這個問題實際上和文章最開始的代碼一樣,在"睡眠前的準備"和"進入睡眠"之間可能發生調度,從而存在"先喚醒、後睡眠"的可能性。 
真的會有問題嗎?其實不會,否則pthread提供這麼一個不能做到同步的同步接口,實在沒什麼意義。其實able_to_run和unable_to_run的實現還是有講究的,簡單的睡眠和喚醒顯然不能滿足需要。 
同步的實現 
當時寫程序的時候是在嵌入式linux下,uClibc庫使用的pthread線程庫是linuxthreads(現在主流的線程庫是NPTL)。在linuxthreads中,上面提到的unable_to_run是基於sigsuspend系統調用來實現的。 
在linux中,每個進程(線程)都有一個信號掩碼,如果某個信號被mask掉,那麼收到的這個信號就不會被處理,而是作爲一個未決信號,記錄在進程的控制信息(task_struct結構)中。默認情況下,linuxthreads把SIGUSER1給mask掉了。而sigsuspend的功能就是使用新的mask,並等待一個信號。收到不被mask的信號後,sigsuspend返回,並且信號掩碼被還原。 
這樣一來,如果出現"先喚醒、後睡眠"(able_to_run先於unable_to_run被執行),則: 
1.able_to_run:SIGUSER1信號被髮送到目標進程上,而目標進程的SIGUSER1信號被mask掉了,於是該信號被記錄在目標進程的task_struct結構中,並不被立刻處理 
2.unable_to_run:調用sigsuspend,新的mask不包含SIGUSER1信號,於是記錄在task_struct結構中的SIGUSER1信號被取出,sigsuspend直接返回,並不會進入睡眠 
可見,sigsuspend之所以能夠實現同步,就是因爲它避免了"睡眠前的準備"和"進入睡眠"之間可能發生的調度("睡眠前的準備"中的最後一步----取消mask,和"進入睡眠",都是在這個調用中完成的),把這兩個操作統一成了一個"原子操作"(對於用戶態程序來說是原子的)。 
再深入一點 
那麼,由內核實現的系統調用sigsuspend,它本身也是一個函數呀,它還是得面對"在‘睡眠前的準備'和‘進入睡眠'之間可能發生調度"的問題呀!其實不然,因爲調度其本身是由內核來實現的,內核大不了就在一小段時間內不調度。 
但是,上面只提到由於調度引起的"先喚醒、後睡眠"問題。然而在多處理器條件下,即將睡眠的進程和喚醒進程可能運行在不同的CPU上,即便不發生調度還是可能出現"先喚醒、後睡眠"的問題。 
爲了解決這個問題,內核還必須用到鎖。內核通過鎖來保證"睡眠前的準備"和"進入睡眠"是"原子的"。然而,鎖總是要釋放的,釋放鎖是不是應該放在睡眠以前?是不是該歸爲"睡眠前的準備"?於是乎,是不是又存在"在‘睡眠前的準備'和‘進入睡眠'之間被插入喚醒操作"的問題呢? 
沒錯,如果鎖一定要在睡眠以前釋放,那麼肯定還是存在這樣的問題。但是內核不一定要在進程睡眠以前釋放鎖,內核可以讓這個進程帶着鎖去睡眠。然後,當上下文切換到另一個進程之後(注意,這時還是在內核態),內核還可以爲上一個進程執行一些代碼,做一些切換後的清理工作。鎖的釋放實際上可以放在這裏來做。 
具體到linux內核代碼,我們來看看用於喚醒的try_to_wake_up函數和用於睡眠的schedule函數(實際上該函數用於觸發一次調度,在調度前如果發現當前進程狀態不是RUNNING,則將其移出可執行隊列,於是當前進程就睡眠了)。 
[try_to_wake_up] 
1.鎖住被喚醒進程對應的可執行隊列 
2.將被喚醒進程加入該隊列 
3.將被喚醒進程狀態設爲RUNNING 
4.釋放鎖 
[schedule] 
1.鎖住當前進程對應的可執行隊列 
2.如果進程狀態不爲RUNNING,則將其移出隊列 
3.進行進程切換 
4.釋放鎖 
調用schedule函數之前,當前進程已經被設置爲非RUNNING狀態,很容易通過鎖機制保證這個動作發生在try_to_wake_up函數被調用之前。那麼,可以看到,即使是"先喚醒、後睡眠",睡眠的進程也能被喚醒。因爲"喚醒"動作將進程狀態設爲RUNNING了,而"睡眠"動作發現進程狀態是RUNNING,則並不會真正睡眠(不會將進程移出可執行隊列)。可執行隊列鎖保證了"喚醒"和"睡眠"兩個動作是原子的,不會交叉執行。而在"睡眠"過程中,是在完成了進程切換後才釋放鎖。這個動作可參閱sched.c:context_switch()函數最後部分調用的finish_task_switch()函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章