非同步信號安全(async-signal-safety)
想像你正整理桌子整理到一半時恰好被打斷——抽屜開著、紙張懸在半空。如果這個打斷要求你再整理同一張桌子,你會弄得一團亂:桌子正處於做了一半的狀態。信號處理常式面對的正是這個問題。它可能在任何一條指令處打斷你的程式,可能正卡在某個讓某些共享結構只更新到一半的函式中間。若處理常式接著呼叫同一類函式,它操作的就是一個壞掉、做了一半的狀態。
具體來說:假設你的主程式碼正在 malloc() 裡面,正改到一半在編輯配置器內部的自由串列,這時一個信號觸發。你的處理常式現在執行,也呼叫了 malloc()。自由串列正處於不一致的中間狀態,所以這第二次 malloc() 可能弄壞它或當機。printf()(用到內部緩衝區與鎖)、大部分的 stdio、以及許多函式庫呼叫都有同樣的危險。一個函式若保證即使從打斷了任何其他程式碼的信號處理常式中被呼叫也能正確運作,就是「非同步信號安全」的。POSIX 定義了一份特定的短名單——write()、read()、_exit()、kill()、signal() 以及另外數十個——只有這些才可以從處理常式裡呼叫。
實用守則不言自明:在信號處理常式裡幾乎什麼都別做。安全、標準的模式是設一個 volatile sig_atomic_t 型別的變數(這型別保證以原子方式更新)就立刻返回,再讓你的正常程式迴圈注意到這個旗標、在安全的脈絡下做真正的工作。若你非得從處理常式回報些什麼,用 write() 寫到一個檔案描述符,絕不要用 printf()。這不是吹毛求疵:信號處理常式的錯誤之所以惡名昭彰,正因為它們只在信號恰好落在某條倒楣指令時才出現,所以罕見、不確定,且極難重現。
危險: void h(int s){ printf("got it\n"); } /* printf 不是非同步信號安全 */ 安全: void h(int s){ const char m[]="got it\n"; write(2, m, 7); } /* write 是安全的 */
在處理常式裡,write() 沒問題,但 printf() 可能死結或弄壞資料。拿不準時,就只設一個旗標。
malloc()、free() 與 printf() 都不是非同步信號安全的——從處理常式裡呼叫它們是個典型的潛伏錯誤,可能撐過測試、只在正式環境偶爾當機。「設旗標就返回」的模式徹底繞開整個問題。