2020年3月13日 星期五

linux/unix signal 議題

以簡馭繁,以變為宗。
signal 是一個很複雜的東西, 如果和 fork, thread 搞在一起, 複雜到令人害怕, 關於 signal 和 thread 的其中一個複雜議題, 可以參考 - thread 和 signal

如果用 c++ 再搭配 c++ exception handling, 那更是複雜到爆炸, 隨便混在一起, 你清楚程式的執行路徑嗎?

在我剛開始接觸 linux programming 之時, 每本相關書籍都會提到 siganl, 但我總是懵懵懂懂, 好像知道了, 又好像完全不認識 signal, 在利用 signal + setjmp/longjmp 寫出 user mode thread 時, 我又重新學習了 signal。

signal 可以想成是中斷。一般 cpu 會有中斷控制器可以接受中斷, signal 則是 unix 用來模擬中斷的行為。

如果想練習中斷控制器的程式, 但沒有適當的硬體或是不想面對硬體的暫存器設定, 可以用 signal 程式來練習, 和寫中斷程式的觀念都一樣, 本篇就是要討論這些問題。

當然硬體的中斷不能佔用太多 cpu 時間來處理, signal 則沒有這樣的限制, 你要在 signal handler while(1) 也不會影響整個系統。但其餘要注意的可重入性都是和中斷一樣的。

而 signal 一開始的設計不完美, 衍生一些漏洞, 而之後改善的版本是以另外一組 signal 來提供, 之前不完美的 signal 就因為相容性保留下來。

「Linux 内核源代码情景分析」6.4 一節, 說明 linux 怎麼實作 signal, 我從這裡得知為什麼 pause() 可以被 signal 打斷, 而 system call 為什麼又會因為 signal 而有被打斷而沒有執行完整的問題。

傳統 signal (不完美版本)
Signal     Value     Action   Comment
───────────────────────────────────────────
SIGHUP        1       Term    Hangup detected on controlling terminal
                              or death of controlling process
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGILL        4       Core    Illegal Instruction
SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating point exception
SIGKILL       9       Term    Kill signal
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe: write to pipe with no
                              readers
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
SIGCHLD   20,17,18    Ign     Child stopped or terminated
SIGCONT   19,18,25    Cont    Continue if stopped
SIGSTOP   17,19,23    Stop    Stop process
SIGTSTP   18,20,24    Stop    Stop typed at terminal
SIGTTIN   21,21,26    Stop    Terminal input for background process
SIGTTOU   22,22,27    Stop    Terminal output for background process

改善的版本:
SIGRTMIN ~ SIGRTMAX

這篇文章要看幾個 signal 特性:
q11: 當處理 SIGUSR1 signal handler 發動時, 這時候再收到一個 SIGUSR1, 同一個 SIGUSR1 signal handler 會被中斷, 然後去執行這次收到的 SIGUSR1 引發的 SIGUSR1 signal handler 嗎?

注意: 這裡指再次收到同樣的 SIGUSR1 signal, 如果是處理 SIGUSR1 signal handler, 然後收到 SIGUSR2, 那是不同的情形。

答案是「會」, 也「不會」。

如果是「不會」的話, 等到第一次的 SIGUSR1 signal handler 執行完之後, 會再次執行一次 SIGUSR1 signal handler 嗎?

答案是「會」, 也「不會」。

如果是「會」的話, 再問一個問題:
q22: 當處理 SIGUSR1 signal handler 發動時, 這時候再收到「兩個」SIGUSR1, 同一個 SIGUSR1 signal handler 執行結束之後, 這個 SIGUSR1 signal handler 會再執行 2 次嗎?

在 q11 的前提上, 答案是不會, 一樣只執行一次。

但是如果 SIGUSR1 換成 SIGRTMIN, 同一個 signal handler 會執行 2 次, 很奇怪, 為什麼要設計成不一樣的行為? 這就是新舊 2 種 signal 的分別。

test_signal.c 用了 2 個 signal 測試, 一次是 SIGUSR1, 另外一次是 SIGRTMIN。我刻意讓 signal handler sigalrm_fn 做個 delay, 以便在還沒結束時, 可以再收到一個或是兩個 SIGUSR1/SIGRTMIN。

test_signal.c
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <sys/time.h>
 5 #include <signal.h>
 7 
 8 void sigalrm_fn(int sig)
 9 {
10   static int cnt;
11   printf("got sig: %d, cnt: %d\n", sig, cnt);
12   for (int i=0 ; i < 10000 ; ++i)
13     for (int i=0 ; i < 1000 ; ++i)
14       for (int i=0 ; i < 500 ; ++i)
15         ;
16   printf("end USR1!\n", cnt);
17   ++cnt;
18 }
19 
20 int main(int argc, char *argv[])
21 {
22   printf("SIGRTMIN: %d\n", SIGRTMIN);
23   printf("SIGRTMAX: %d\n", SIGRTMAX);
24   //signal(SIGUSR1, sigalrm_fn);
25   signal(SIGRTMIN, sigalrm_fn);
26 
29   while (1) 
30     pause();
31 
32   return 0;
33 }

對於 SIGUSR1 來說, 如果在 sigalrm_fn 執行期間送出 2 個 SIGUSR1, sigalrm_fn 只會執行一次; 對於 SIGRTMIN 來說, 如果在 sigalrm_fn 執行期間送出 2 個 SIGRTMIN, sigalrm_fn 會執行 2 次, 一般來說都會覺得 SIGRTMIN 的行為比較正確, 這就是改善後的效果。

對於傳統的 signal, 2 次的 signal 會被合併成一個, 對於 rt 系列的 signal, 會被 queue 起來。

在我的系統上, SIGRTMIN 是 34, 所以用以下的指令測試。

killall -34 test_signal
killall -s SIGUSR1 simple_thread

在 end USR1! 訊息還沒有印出的時候, 再送出 2 次的 SIGRTMIN 或是 SIGUSR1, 就可以看到這個現象。

如果在你的平台不是這樣的結果可能是 signal 的預設行為不同, 那 ... 就算了。

如果這樣還沒有難倒你, 那再來看一個。

sysv_signal() 是古早不可靠時代的版本, 把 signal(SIGUSR1, sigalrm_fn) 換成 sysv_signal(SIGUSR1, sigalrm_fn) 之後, 會發現在 sigalrm_fn 執行期間, 送出 SIGUSR1 之後, 程式會突然結束。

這是因為古早的版本除了不會 queue 住訊號之外, 在執行的途中如果又來一個 SIGUSR1, 正在執行的 signal handler 會被中斷, 而在執行 SIGUSR1 signal handler 期間, 還會把設定好的 SIGUSR1 signal handler sigalrm_fn 換回預設行為, 而 SIGUSR1 的預設行為就是中斷程式, 所以程式就這麼結束了。

所以得在 SIGUSR1 signal handler 裡頭再次呼叫 signal, 讓 signal 不要把 SIGUSR1 signal handler 改回預設, 很奇怪的設計吧!

那我使用的 signal 行為到底是那一種呢? 很遺憾, 沒有固定的版本, glibc 不同版本的實作可能也不同, 所以還是使用 sigaction 會有比較好的可攜性, 不過 sigaction 用法就複雜多了。

現在你知道為什麼 sigaction 這麼複雜, 因為要知道這麼多東西, 才能把 sigaction 的那些參數設定好。

上述的 signal 相當於以下的 sigaction。
struct sigaction s1, old_s1;

sigemptyset(&s1.sa_mask);

s1.sa_flags = 0;

s1.sa_handler = sigalrm_fn;
sigaction(SIGUSR1, &s1, &old_s1);

如果想要在執行 signal handler 時, 可以中斷目前的 signal handler 執行, 使用以下的 flags。
s1.sa_flags = SA_NODEFER;

看起來只多了幾行, 但要理解背後的設計並不簡單, tlpi 花了 3 章在說明 signal, 但我想第一次看這 3 章的人應該不會一次就能理解。

本篇對於 signal 的敘述沒有很精確, 請參考相關專業書籍, tlpi (The Linux Programming Interface), 經典的 apue (Advanced Programming in the UNIX Environment), 最主要是想提提一些觀念, 讓大家知道 signal 不是好惹的。

我還閱讀了「Linux 内核源代码情景分析」6.4 一節, 理解 linux 怎麼實作 signal, 慢慢串起這些觀念。

windows message 機制我不熟, 但應該和 signal 中斷的方式是不一樣的。

signal 的設計太厲害了, 這麼複雜的東西到底是誰想到加入 unix 的。

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。