繼承自 C 的優良傳統, C++ 也是一門非常靠近底層的語言, 可是實在是太靠近了, 很多問題語言本身沒有提供解決方案, 可執行程式碼貼近機器, 執行時沒有虛擬機器來反饋錯誤, 跑著跑著就毫無徵兆地崩潰了, 簡直比過山車還刺激.
雖然 C++ 加入了異常機制來處理很多執行時錯誤, 但是異常機制的功效非常受限, 很多錯誤還沒辦法用原生異常手段捕捉, 比如整數除 0 錯誤. 下面這段程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> int main() { try { int x, y; std::cin >> x >> y; std::cout << x / y << std::endl; } catch (...) { std::cerr << "attempt to divide integer by 0." << std::endl; } return 0; } |
輸入 “1 0” 則會導致程式掛掉, 而那對 try-catch 還呆在那裡好像什麼事情都沒發生一樣. 像 Python 一類有虛擬機器環境支援的語言, 都會毫無懸念地捕獲除 0 錯誤.
使用訊號
不過, 底層自然有底層的辦法, 而且有虛擬機器的環境也並非在每個整數除法指令之前都添上一句 if 0 == divisor: raise 之類的挫語句來觸發異常. 這得益於硬體體系中的中斷機制. 簡而言之, 當發生整數除 0 之類的錯誤時, 硬體會觸發中斷, 這時作業系統會根據上下文查出是哪個程式不給力了, 然後給這個程式發出一個訊號. 某些時候也可以手動給程式發訊號, 比如惱怒的使用者發現某個程式卡死的時候果斷 kill 掉這個程式, 這也是訊號的一種.
這次就不是 C 標準了, 而是 POSIX 標準. 它規定了哪些訊號程式不處理也不會有太大問題, 有些訊號程式想處理也是不行的, 還有一些訊號是錯誤中斷, 如果程式處理了它們, 那麼程式能繼續執行, 否則直接殺掉.
不過, 這些錯誤處理預設過程都是不存在的, 需要通過呼叫 signal 函式配置. 方法類似下面這個例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <csignal> #include <cstdlib> #include <iostream> void handle_div_0(int) { std::cerr << "attempt to divide integer by 0." << std::endl; exit(1); } int main() { if (SIG_ERR == signal(SIGFPE, handle_div_0)) { std::cerr << "fail to setup handler." << std::endl; return 1; } int x, y; std::cin >> x >> y; std::cout << x / y << std::endl; return 0; } |
可以看出, signal 接受兩個引數, 分別是訊號編號和訊號處理函式. 成功設定了針對 SIGFPE (吐槽: 為什麼是浮點異常 FPE 呢?) 的處理函式 handle_div_0, 如果再發生整數除 0 的慘劇, handle_div_0 就會被呼叫.
handle_div_0 的引數是訊號碼, 也就是 SIGFPE, 忽略它也行.
底層機制
雖然說 handle_div_0 是異常處理過程, 但畢竟是函式都會有呼叫棧, 能返回. 假如在 handle_div_0 中不呼叫 exit 自尋死路, 而是選擇返回, 那麼程式會怎麼樣呢? 執行一下, 當出現錯誤時, stderr 會死迴圈般地刷屏.
實際上, 當錯誤發生時, 作業系統會在當前錯誤出現處載入訊號處理函式的呼叫棧幀, 並且把它的返回地址設定為出錯的那條指令之前, 這樣看起來就像是出錯之前的瞬間呼叫了訊號處理函式. 當訊號處理函式返回時, 則又會再次執行那條會出錯的指令, 除非訊號處理函式能通過某些特別的技巧修復指令, 否則退出時會重蹈覆轍.
上面提到的 “修復指令” 指的是修復 CPU 級別的指令碼或者運算元. 把除數 y 變成全域性變數, 然後在 handle_div_0 中設定 y 為 1, 這樣做是於事無補的.
使用異常處理機制
修復指令這種事情簡直是天方夜譚, 所以選擇輸出一跳錯誤語句並退出也算是不錯的方法. 在 C 語言時代, 還可以通過 setjmp 和 longjmp 來跳轉程式流程. 不過 setjmp 和 longjmp 操作起來太不方便了, 相比之下 try-catch 要好得多.
剛才說過, 錯誤處理函式的呼叫棧幀直接位於錯誤發生處所在函式棧幀之上, 因此, 丟擲異常能夠被外部設定的 try-catch 捕獲. 現在定義一個異常型別, 然後在 handle_div_0 中丟擲就行.
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 |
#include <csignal> #include <iostream> struct div_0_exception {}; void handle_div_0(int) { throw div_0_exception(); } int main() { if (SIG_ERR == signal(SIGFPE, handle_div_0)) { std::cerr << "fail to setup handler." << std::endl; return 1; } try { int x, y; std::cin >> x >> y; std::cout << x / y << std::endl; } catch (div_0_exception) { std::cerr << "attempt to divide integer by 0." << std::endl; } return 0; } |
更精準的訊號處理
上述方法的缺陷在於, 只要發生 SIGFPE 中斷, 無論是整數除 0 錯誤, 還是其它浮點異常, 處理方式是統一的. 不過, POSIX 還規定了一組更精細的訊號處理介面, 它們是 sigaction.
呃… 對它們都是 sigaction. 這又是一個雷死人的東西. 在 csignal 中定義了兩個同名的東西, 分別是
1 2 3 4 5 |
struct sigaction; int sigaction(int sig , struct sigaction const* restrict act , struct sigaction* restrict old_act); |
前面那個結構體在設定訊號處理函式時用到, 裡面存放了一些標誌位和訊號處理函式指標. 而後面那個函式就是設定訊號處理的入口 (如果函式的第三個引數並非 NULL, 並且之前設定過訊號處理結構體, 那麼會將之前的處理方法寫入第三個引數所指向的結構中, 這一點並不需要, 所以後面的例子中這個引數直接傳入 NULL, 詳情請見 man 3 sigaction).
結構 sigaction 中會有兩個函式入口地址, 它們分別是
1 2 |
void (* sa_handler)(int); void (* sa_sigaction)(int, siginfo_t*, void*); |
sa_handler 也就是之前所演示的輕便型訊號處理函式; 而 sa_sigaction, 從它接受的引數就能看出, 它能獲得更多的上下文資訊 (然而, 一看第三個引數的型別是 void* 就知道沒有好事, 資訊都在第二個引數指向的結構體中).
既然有兩個處理函式, 那麼如何決定使用哪一個呢? 在 struct sigaction 中有一個標誌位成員 sa_flags, 如果為它置上 SA_SIGINFO 位, 那麼就使用 sa_sigaction 作為處理函式.
siginfo_t 型別中有一個叫做 si_code 的成員, 它為訊號型別提供進一步的細分, 比如在 SIGFPE 訊號下, si_code 可能有 FPE_INTOVF (整數溢位), FPE_FLTUND (浮點數下溢), FPE_FLTOVF (浮點數上溢) 等各種相關取值, 當然還有現在最關心的整數除 0 訊號碼 FPE_INTDIV. 如果陷入 SIGFPE 的窘境中, 而 si_code 又恰好是 FPE_INTDIV 那麼就要果斷丟擲 0 異常了.
由於原生的 struct sigaction 居然跟函式重名, 所以下面的例子中會對其包裝一下, 提供合適的初始化過程.
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 |
#include <csignal> #include <cstring> #include <iostream> struct my_sig_action { typedef void (* handler_type)(int, siginfo_t*, void*); explicit my_sig_action(handler_type handler) { memset(&_sa, 0, sizeof(struct sigaction)); _sa.sa_sigaction = handler; _sa.sa_flags = SA_SIGINFO; } operator struct sigaction const*() const { return &_sa; } protected: struct sigaction _sa; }; struct div_0_exception {}; void handle_div_0(int sig, siginfo_t* info, void*) { if (FPE_INTDIV == info->si_code) throw div_0_exception(); } int main() { my_sig_action sa(handle_div_0); if (0 != sigaction(SIGFPE, sa, NULL)) { std::cerr << "fail to setup handler." << std::endl; return 1; } try { int x, y; std::cin >> x >> y; std::cout << x / y << std::endl; } catch (div_0_exception) { std::cerr << "attempt to divide integer by 0." << std::endl; } return 0; } |