熟悉ucos,或者讀過Jean.J.Labrosse寫過的ucos書籍的人,一定會知道ucos中著名的臨界去管理宏:OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()。
同樣是通過關中斷來保護臨界區,OS_ENTER_CRITICAL/OS_EXIT_CRITICAL一共實現了三種實現方式,如下所示:
#if OS_CRITICAL_METHOD == 1#define OS_ENTER_CRITICAL() __asm__("cli")#define OS_EXIT_CRITICAL() __asm__("sti")#endif#if OS_CRITICAL_METHOD == 2#define OS_ENTER_CRITICAL() __asm__("pushf \n\t cli")#define OS_EXIT_CRITICAL() __asm__("popf")#endif#if OS_CRITICAL_METHOD == 3#define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR())#define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr))#endif
第一種方式,OS_ENTER_CRITICAL()簡單地關中斷,OS_EXIT_CRITICAL()簡單地開中斷。這種方式雖然簡單高效,但無法滿足嵌套的情況。如果有兩層臨界區保護,在退出內層臨界區時就會開中斷,使外層的臨界區也失去保護。雖然ucos的核心寫的足夠好,沒有明顯嵌套臨界區的情況,但誰也無法保證一定沒有,無法保證今後沒有,無法保證在附加的驅動或什麼位置沒有,所以基本上第一種方法是沒有人用的。
第二種方式,OS_ENTER_CRITICAL()會在關中斷前儲存之前的標誌寄存器內容到堆棧中,OS_EXIT_CRITICAL()從堆棧中恢複之前儲存的狀態。這樣就允許了臨界區嵌套的情況。但現在看來,這種方法還存在很大的問題,甚至會出現致命的漏洞。
在OS_CRITICAL_METHOD=2的情況下,假設有如下代碼:
function_a(){ int a=(1<<31); OS_ENTER_CRITICAL(); function_b(a); OS_EXIT_CRITICAL(); }
會出現什麼情況?在我的實驗中,OS_EXIT_CRITICAL()之後,會出現處理器異常。為什麼會出現處理起異常,讓我來類比一下它的彙編代碼。之所以是類比,並非是我虛構資料,而是因為我實際碰到問題的函數複雜一些,理解起來就需要更多的代碼。而這個問題是有普遍意義的,所以請允許我來淺顯地揭示這個隱藏的bug。
function_a: push ebp mov ebp, esp sub esp, 8 mov 4(esp), 0x80000000 pushfd cli mov edi, 4(esp) mov (esp), edi call function_b popfd mov esp, ebp ret
這是參照了gcc編譯結果的彙編類比,無論是否加最佳化選項這一問題都存在。這個問題的起因很簡單,gcc想聰明一點,一次把堆棧降個夠,然後它就可以在棧上隨意放參數去調用其他函數。尤其是在調用函數較多的時候,這種做法就更有意義。而且,gcc這種聰明與最佳化選項O好像沒有太大關係,好像沒有什麼能禁止它這麼做。但問題是,gcc不知道我們的OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()是操作了堆棧的,我嘗試過使用__asm__ __volatile__("pushfd
\n\tcli":::"memory")來通知gcc記憶體資料改變了,但顯然gcc不認為堆棧也改變了。於是,OS_ENTER_CRITICAL()儲存在棧上的狀態就被衝掉了,比如被這裡調用參數a的值。在恢複時,是否會引發異常,會引發什麼異常,這個就要靠運氣了。但我相信一個人的運氣不會總是那麼好的,所以最後別使用OS_CRITICAL_METHOD=2。
第三種,在關中斷前,使用局部變數儲存中斷狀態。這也是幾乎所有即時作業系統共有的選擇。但ucos是一朵奇葩,為了相容前兩種方式,OS_ENTER_CRITICAL()/ OS_EXIT_CRITICAL()宏定義並沒有提供傳遞狀態參數的功能。所以它的臨界去必須這麼用:
function_a(){#if OS_CRITICAL_METHOD == 3 int cpu_sr;#endif int a = 1<<31; OS_ENTER_CRITICAL(); function_b(a); OS_EXIT_CRITICAL();}
這種代碼怎麼看怎麼彆扭,可能是因為在函數體內加了宏定義吧。然後,第三種方法對同一個函數體內的嵌套臨界區無法支援,這在一些很長大的函數中使用時或許會造成一定困擾。
好吧,如果有了問題,就要有解決方案,畢竟我不是為了讓大家對ucos失去信心的。我們可以參考下一般的即時作業系統是如何?關中斷臨界區的,就是以顯式的方式用局部變數儲存中斷狀態。
int int_lock(){ int cpu_sr; __asm__ __volatile__("pushfd \n\t pop %0\n\t cli":"=r"(cpu_sr)); return cpu_sr;}void int_unlock(int cpu_sr){ __asm__ __volatile__("push %0\n\t popfd"::"r"(cpu_sr));}function_a(){ int a, cpu_sr; a=1<<31; cpu_sr = int_lock(); function_b(a); int_unlock(cpu_sr);}
int_lock()和int_unlock()的可以用彙編更高效地實現,也可以選擇只恢複中斷標誌的狀態。這種方法讓我們顯示地管理狀態儲存的情況,我覺得至少要比宏定義清楚多了。