一、整體大綱
二、信號詳解
1. 信號的概念
信號在我們的生活中隨處可見, 如:古代戰爭中摔杯為號;現代戰爭中的信號彈;體育比賽中使用的信號槍......他們都有共性:
1) 簡單
2)不能攜帶大量信息
3)滿足某個特設條件才發送。
信號是信息的載體,Linux/UNIX 環境下,古老、經典的通信方式, 現下依然是主要的通信手段。
Unix早期版本就提供了信號機制,但不可靠,信號可能丟失。Berkeley 和 AT&T都對信號模型做了更改,增加了可靠信號機制。但彼此不兼容。POSIX.1對可靠信號例程進行了標准化。
2. 信號的機制
A 給 B 發送信號,B收到信號之前執行自己的代碼,收到信號后,不管執行到程序的什么位置,都要暫停運行,去處理信號,處理完畢再繼續執行。與硬件中斷類似——異步模式。但信號是軟件層面上實現的中斷,早期常被稱為“軟中斷”。
信號的特質:由於信號是通過軟件方法實現,其實現手段導致信號有很強的延時性。但對於用戶來說,這個延遲時間非常短,不易察覺。
每個進程收到的所有信號,都是由內核負責發送的,內核處理。
3. 與信號相關的事件和狀態
(1)產生信號
1)按鍵產生,如:Ctrl+c、Ctrl+z、Ctrl+\
2)系統調用產生,如:kill、raise、abort
3)軟件條件產生,如:定時器alarm
4)硬件異常產生,如:非法訪問內存(段錯誤)、除0(浮點數例外)、內存對齊出錯(總線錯誤)
5)命令產生,如:kill命令
遞達:遞送並且到達進程。
未決:產生和遞達之間的狀態。主要由於阻塞(屏蔽)導致該狀態。
如下圖所示,當 ctl+c 2號信號到達,如果設置為阻塞,則在阻塞信號集和未決信號集中狀態都為 1,當解阻塞該信號時,則該信號才會被處理,同時設置未決信號集該信號狀態為0。
4. 信號的處理方式
1)執行默認動作
2)忽略(丟棄)
3)捕捉(調用戶處理函數)
Linux內核的進程控制塊PCB是一個結構體,task_struct, 除了包含進程id,狀態,工作目錄,用戶id,組id,文件描述符表,還包含了信號相關的信息,主要指阻塞信號集和未決信號集。
阻塞信號集(信號屏蔽字): 將某些信號加入集合,對他們設置屏蔽,當屏蔽x信號后,再收到該信號,該信號的處理將推后(解除屏蔽后)
未決信號集:
1)信號產生,未決信號集中描述該信號的位立刻翻轉為1,表示信號處於未決狀態。當信號被處理對應位翻轉回為0。這一時刻往往非常短暫。
2)信號產生后由於某些原因(主要是阻塞)不能抵達。這類信號的集合稱之為未決信號集。在屏蔽解除前,信號一直處於未決狀態。
5. 信號的編號
可以使用kill –l命令查看當前系統可使用的信號有哪些。
[root@centos 09-linux-day07]# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
不存在編號為0的信號。其中1-31號信號稱之為常規信號(也叫普通信號或標准信號),34-64稱之為實時信號,驅動編程與硬件相關。名字上區別不大。而前32個名字各不相同。
6. 信號4要素
與變量三要素類似的,每個信號也有其必備4要素,分別是:
1)編號
2)名稱
3)事件
4)默認處理動作
可通過man 7 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 The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
在標准信號中,有一些信號是有三個“Value”,第一個值通常對alpha和sparc架構有效,中間值針對x86、arm和其他架構,最后一個應用於mips架構。一個‘-’表示在對應架構上尚未定義該信號。
不同的操作系統定義了不同的系統信號。因此有些信號出現在Unix系統內,也出現在Linux中,而有的信號出現在FreeBSD或Mac OS中卻沒有出現在Linux下。這里我們只研究Linux系統中的信號。
默認動作:
Term:終止進程
Ign: 忽略信號 (默認即時對該種信號忽略操作)
Core:終止進程,生成Core文件。(查驗進程死亡原因, 用於gdb調試)
Stop:停止(暫停)進程
Cont:繼續運行進程
注意從man 7 signal幫助文檔中可看到 : The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
特別強調了9) SIGKILL 和 19) SIGSTOP信號,不允許忽略和捕捉,只能執行默認動作。甚至不能將其設置為阻塞。
另外需清楚,只有每個信號所對應的事件發生了,該信號才會被遞送(但不一定遞達),不應亂發信號!!
7. Linux常規信號一覽表
1) SIGHUP: 當用戶退出shell時,由該shell啟動的所有進程將收到這個信號,默認動作為終止進程 2) SIGINT:當用戶按下了<Ctrl+C>組合鍵時,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動 作為終止進程。 3) SIGQUIT:當用戶按下<ctrl+\>組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程序發出些信 號。默認動作為終止進程。 4) SIGILL:CPU檢測到某進程執行了非法指令。默認動作為終止進程並產生core文件 5) SIGTRAP:該信號由斷點指令或其他 trap指令產生。默認動作為終止里程 並產生core文件。 6) SIGABRT: 調用abort函數時產生該信號。默認動作為終止進程並產生core文件。 7) SIGBUS:非法訪問內存地址,包括內存對齊出錯,默認動作為終止進程並產生core文件。 8) SIGFPE:在發生致命的運算錯誤時發出。不僅包括浮點運算錯誤,還包括溢出及除數為0等所有的算法錯誤。默認動作為終止進程並產生core文件。 9) SIGKILL:無條件終止進程。本信號不能被忽略,處理和阻塞。默認動作為終止進程。它向系統管理員提供了可以殺死任何進程的方法。 10) SIGUSE1:用戶定義 的信號。即程序員可以在程序中定義並使用該信號。默認動作為終止進程。 11) SIGSEGV:指示進程進行了無效內存訪問。默認動作為終止進程並產生core文件。 12) SIGUSR2:另外一個用戶自定義信號,程序員可以在程序中定義並使用該信號。默認動作為終止進程。 13) SIGPIPE:Broken pipe向一個沒有讀端的管道寫數據。默認動作為終止進程。 14) SIGALRM: 定時器超時,超時的時間 由系統調用alarm設置。默認動作為終止進程。 15) SIGTERM:程序結束信號,與SIGKILL不同的是,該信號可以被阻塞和終止。通常用來要示程序正常退出。執行shell命令Kill時,缺省產生這個信號。默認動作為終止進程。 16) SIGSTKFLT:Linux早期版本出現的信號,現仍保留向后兼容。默認動作為終止進程。 17) SIGCHLD:子進程結束時,父進程會收到這個信號。默認動作為忽略這個信號。 18) SIGCONT:如果進程已停止,則使其繼續運行。默認動作為繼續/忽略。 19) SIGSTOP:停止進程的執行。信號不能被忽略,處理和阻塞。默認動作為暫停進程。 20) SIGTSTP:停止終端交互進程的運行。按下<ctrl+z>組合鍵時發出這個信號。默認動作為暫停進程。 21) SIGTTIN:后台進程讀終端控制台。默認動作為暫停進程。 22) SIGTTOU: 該信號類似於SIGTTIN,在后台進程要向終端輸出數據時發生。默認動作為暫停進程。 23) SIGURG:套接字上有緊急數據時,向當前正在運行的進程發出些信號,報告有緊急數據到達。如網絡帶外數據到達,默認動作為忽略該信號。 24) SIGXCPU:進程執行時間超過了分配給該進程的CPU時間 ,系統產生該信號並發送給該進程。默認動作為終止進程。 25) SIGXFSZ:超過文件的最大長度設置。默認動作為終止進程。 26) SIGVTALRM:虛擬時鍾超時時產生該信號。類似於SIGALRM,但是該信號只計算該進程占用CPU的使用時間。默認動作為終止進程。 27) SGIPROF:類似於SIGVTALRM,它不公包括該進程占用CPU時間還包括執行系統調用時間。默認動作為終止進程。 28) SIGWINCH:窗口變化大小時發出。默認動作為忽略該信號。 29) SIGIO:此信號向進程指示發出了一個異步IO事件。默認動作為忽略。 30) SIGPWR:關機。默認動作為終止進程。 31) SIGSYS:無效的系統調用。默認動作為終止進程並產生core文件。 34) SIGRTMIN ~ (64) SIGRTMAX:LINUX的實時信號,它們沒有固定的含義(可以由用戶自定義)。所有的實時信號的默認動作都為終止進程。
8. 信號的產生
(1)終端按鍵產生信號
Ctrl + c → 2) SIGINT(終止/中斷) "INT" ----Interrupt Ctrl + z → 20) SIGTSTP(暫停/停止) "T" ----Terminal 終端。 Ctrl + \ → 3) SIGQUIT(退出)
(2)硬件異常產生信號
除0操作 → 8) SIGFPE (浮點數例外) "F" -----float 浮點數 非法訪問內存 → 11) SIGSEGV (段錯誤) 總線錯誤 → 7) SIGBUS
(3)kill命令產生信號
kill命令產生信號:kill -SIGKILL pid
kill函數:給指定進程發送指定信號(不一定殺死)
int kill(pid_t pid, int sig); 成功:0;失敗:-1 (ID非法,信號非法,普通用戶殺init進程等權級問題),設置errno
sig:不推薦直接使用數字,應使用宏名,因為不同操作系統信號編號可能不同,但名稱一致。
pid > 0: 發送信號給指定的進程。
pid = 0: 發送信號給 與調用kill函數進程屬於同一進程組的所有進程。
pid < 0: 取|pid|發給對應進程組。
pid = -1:發送給進程有權限發送的系統中所有進程。
進程組:每個進程都屬於一個進程組,進程組是一個或多個進程集合,他們相互關聯,共同完成一個實體任務,每個進程組都有一個進程組長,默認進程組ID與進程組長ID相同。
權限保護:super用戶(root)可以發送信號給任意用戶,普通用戶是不能向系統用戶發送信號的。 kill -9 (root用戶的pid) 是不可以的。同樣,普通用戶也不能向其他普通用戶發送信號,終止其進程。 只能向自己創建的進程發送信號。普通用戶基本規則是:發送者實際或有效用戶ID == 接收者實際或有效用戶ID。
練習:循環創建5個子進程,任一子進程用kill函數終止其父進程。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/stat.h> 4 #include <signal.h> 5 6 int main() 7 { 8 int i = 0; 9 for (i = 0; i < 5; i++) 10 { 11 pid_t pid = fork(); 12 if (pid == 0) 13 { 14 break; 15 } 16 } 17 if (i == 2) 18 { 19 //son 20 printf("I will kill father after 5s\n"); 21 sleep(5); 22 kill(getppid(), SIGKILL); 23 while(1) 24 { 25 sleep(1); 26 } 27 } 28 else if (i == 5) 29 { 30 //parent 31 while(1) 32 { 33 printf("I am father, I am happy!\n"); 34 sleep(1); 35 } 36 } 37 38 return 0; 39 }
練習:循環創建5個子進程,父進程用kill函數終止第3個子進程。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <signal.h> 5 6 int main() 7 { 8 int i = 0; 9 pid_t pid3, pid; 10 for (i = 0; i < 5; i++) 11 { 12 pid = fork(); 13 if (pid == 0) 14 { 15 break; 16 } 17 //parent 18 if (i == 2) 19 { 20 pid3 = pid; 21 } 22 } 23 if (i < 5) 24 { 25 while(1) 26 { 27 printf("I am child, pid = %d, ppid = %d\n", getpid(), getppid()); 28 sleep(3); 29 } 30 } 31 else if (i == 5) 32 { 33 printf("I am parent, pid = %d, I will kill son pid = %d\n", getpid(), pid3); 34 sleep(5); 35 kill(pid3, SIGKILL); 36 while(1) 37 { 38 sleep(1); 39 } 40 } 41 42 return 0; 43 }
(4)raise 和 abort 函數
raise 函數:給當前進程發送指定信號(自己給自己發) raise(signo) == kill(getpid(), signo);
int raise(int sig); 成功:0,失敗非0值
abort 函數:給自己發送異常終止信號 6) SIGABRT 信號,終止並產生core文件
void abort(void); 該函數無返回
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <signal.h> 4 #include <unistd.h> 5 #include <stdlib.h> 6 7 int main() 8 { 9 printf("I will die!\n"); 10 sleep(2); 11 //raise(SIGKILL); //kill(getpid(), sig); 12 abort(); 13 14 return 0; 15 }
(5)軟件條件產生信號
- alarm函數
設置定時器(鬧鍾)。在指定seconds后,內核會給當前進程發送14)SIGALRM信號。進程收到該信號,默認動作終止。
每個進程都有且只有唯一個定時器。
unsigned int alarm(unsigned int seconds); 返回0或剩余的秒數,無失敗。
常用:取消定時器alarm(0),返回舊鬧鍾余下秒數。
例:alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0)
定時,與進程狀態無關(自然定時法)!就緒、運行、掛起(阻塞、暫停)、終止、僵屍...無論進程處於何種狀態,alarm都計時。
練習:編寫程序,測試你使用的計算機1秒鍾能數多少個數。
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main() 5 { 6 int i = 0; 7 alarm(1); 8 while(1) 9 { 10 printf("%d\n", i++); //1s數數 11 } 12 13 return 0; 14 }
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main() 5 { 6 int ret = 0; 7 //返回距離上一次設置時間的剩余秒數 8 ret = alarm(6); 9 printf("ret = %d\n", ret); //ret = 0 10 sleep(2); 11 ret = alarm(7); //獲取距離上一次鬧鍾值,並設置新鬧鍾 12 printf("ret = %d\n", ret); //ret = 4 13 sleep(2); 14 15 while(1) 16 { 17 printf("I am happy\n"); //這塊打印7次 18 sleep(1); 19 } 20 21 return 0; 22 }
使用time命令查看程序執行的時間。 程序運行的瓶頸在於IO,優化程序,首選優化IO。
實際執行時間 = 系統時間 + 用戶時間 + 等待時間
- setitimer函數
設置定時器(鬧鍾)。 可代替alarm函數。精度微秒us,可以實現周期定時。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); 成功:0;失敗:-1,設置errno
參數:which:指定定時方式
1)自然定時:ITIMER_REAL → 14)SIGLARM 計算自然時間
2)虛擬空間計時(用戶空間):ITIMER_VIRTUAL → 26)SIGVTALRM 只計算進程占用cpu的時間
3)運行時計時(用戶+內核):ITIMER_PROF → 27)SIGPROF 計算占用cpu及執行系統調用的時間
練習: 使用setitimer函數實現alarm函數,重復計算機1秒數數程序。
1 #include <stdio.h> 2 #include <sys/time.h> 3 #include <unistd.h> 4 5 int main() 6 { 7 int num = 0; 8 struct itimerval myit = {{0, 0}, {1, 0}}; 9 setitimer(ITIMER_REAL, &myit, NULL); 10 while(1) 11 { 12 printf("%d\n", num++); 13 } 14 15 return 0; 16 }
拓展練習,結合man page編寫程序,測試it_interval、it_value這兩個參數的作用。
提示: it_interval:用來設定兩次定時任務之間間隔的時間。
it_value:定時的時長
兩個參數都設置為0,即清0操作。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/time.h> 4 5 unsigned int myalarm(unsigned int seconds) 6 { 7 struct itimerval myit = {{0, 0}, {0, 0}}; 8 struct itimerval oldit; 9 myit.it_value.tv_sec = seconds; 10 setitimer(ITIMER_REAL, &myit, &oldit); //seconds 后發送 SIGALRM信號 11 printf("tv_sec = %ld, tv_micsec = %ld\n", oldit.it_value.tv_sec, oldit.it_value.tv_usec); 12 return oldit.it_value.tv_sec; 13 } 14 15 int main() 16 { 17 int ret = 0; 18 ret = myalarm(5); 19 printf("ret = %d\n", ret); 20 sleep(3); 21 ret = myalarm(3); 22 printf("ret = %d\n", ret); 23 24 while(1) 25 { 26 printf("who can kill me!\n"); 27 sleep(1); 28 } 29 30 31 return 0; 32 }
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/time.h> 4 #include <signal.h> 5 6 //typedef void (*sighandler_t)(int); 7 //sighandler_t signal(int signum, sighandler_t handler); 8 9 void catch_sig(int num) 10 { 11 printf("cat %d sig\n", num); 12 } 13 14 int main() 15 { 16 signal(SIGALRM, catch_sig); 17 struct itimerval myit = {{3, 0}, {5, 0}}; //第一等待5s,之后是每隔3s 18 setitimer(ITIMER_REAL, &myit, NULL); 19 20 while(1) 21 { 22 printf("Who can kill me!\n"); 23 sleep(1); 24 } 25 26 27 return 0; 28 }
9. 信號集操作函數
內核通過讀取未決信號集來判斷信號是否應被處理。信號屏蔽字mask可以影響未決信號集。而我們可以在應用程序中自定義set來改變mask。已達到屏蔽指定信號的目的。
(1)信號集的設定
sigset_t set; // typedef unsigned long sigset_t; int sigemptyset(sigset_t *set); 將某個信號集清0 成功:0;失敗:-1 int sigfillset(sigset_t *set); 將某個信號集置1 成功:0;失敗:-1 int sigaddset(sigset_t *set, int signum); 將某個信號加入信號集 成功:0;失敗:-1 int sigdelset(sigset_t *set, int signum); 將某個信號清出信號集 成功:0;失敗:-1 int sigismember(const sigset_t *set, int signum);判斷某個信號是否在信號集中 返回值:在集合:1;不在:0;出錯:-1 sigset_t類型的本質是位圖。但不應該直接使用位操作,而應該使用上述函數,保證跨系統操作有效。 對比認知select 函數。
(2)sigprocmask函數
用來屏蔽信號、解除屏蔽也使用該函數。其本質,讀取或修改進程的信號屏蔽字(PCB中)
嚴格注意,屏蔽信號:只是將信號處理延后執行(延至解除屏蔽);而忽略表示將信號丟處理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 成功:0;失敗:-1,設置errno
參數:
set:傳入參數,是一個位圖,set中哪位置1,就表示當前進程屏蔽哪個信號。
oldset:傳出參數,保存舊的信號屏蔽集。
how參數取值: 假設當前的信號屏蔽字為mask
SIG_BLOCK: 當how設置為此值,set表示需要屏蔽的信號。相當於 mask = mask|set
SIG_UNBLOCK: 當how設置為此,set表示需要解除屏蔽的信號。相當於 mask = mask & ~set
IG_SETMASK: 當how設置為此,set表示用於替代原始屏蔽及的新屏蔽集。相當於 mask = set,若調用sigprocmask解除了對當前若干個信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達。
(3)sigpending函數
讀取當前進程的未決信號集
int sigpending(sigset_t *set); set傳出參數。 返回值:成功:0;失敗:-1,設置errno
練習:編寫程序。把所有常規信號的未決狀態打印至屏幕。
(4)信號捕捉
1)signal函數
注冊一個信號捕捉函數:
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
該函數由ANSI定義,由於歷史原因在不同版本的Unix和不同版本的Linux中可能有不同的行為。因此應該盡量避免使用它,取而代之使用sigaction函數。
void (*signal(int signum, void (*sighandler_t)(int))) (int);
能看出這個函數代表什么意思嗎? 注意多在復雜結構中使用typedef。
2)sigaction函數
修改信號處理動作(通常在Linux用其來注冊一個信號的捕捉函數)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 成功:0;失敗:-1,設置errno
參數:
act:傳入參數,新的處理方式。
oldact:傳出參數,舊的處理方式。
struct sigaction結構體:
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
sa_restorer:該元素是過時的,不應該使用,POSIX.1標准將不指定該元素。(棄用)
sa_sigaction:當sa_flags被指定為SA_SIGINFO標志時,使用該信號處理程序。(很少使用)
重點:
sa_handler:指定信號捕捉后的處理函數名(即注冊函數)。也可賦值為SIG_IGN表忽略 或 SIG_DFL表執行默認動作
sa_mask: 調用信號處理函數時,所要屏蔽的信號集合(信號屏蔽字)。注意:僅在處理函數被調用期間屏蔽生效,是臨時性設置。
sa_flags:通常設置為0,表使用默認屬性。
3)信號捕捉特性
- 進程正常運行時,默認PCB中有一個信號屏蔽字,假定為☆,它決定了進程自動屏蔽哪些信號。當注冊了某個信號捕捉函數,捕捉到該信號以后,要調用該函數。而該函數有可能執行很長時間,在這期間所屏蔽的信號不由☆來指定。而是用sa_mask來指定。調用完信號處理函數,再恢復為☆。
- XXX信號捕捉函數執行期間,XXX信號自動被屏蔽。
- 阻塞的常規信號不支持排隊,產生多次只記錄一次。(后32個實時信號支持排隊)
練習1:為某個信號設置捕捉函數。
1 #include <stdio.h> 2 #include <signal.h> 3 #include <unistd.h> 4 #include <sys/time.h> 5 6 void catch_sig(int num) 7 { 8 printf("catch %d sig\n", num); 9 } 10 11 int main() 12 { 13 //注冊一下捕捉函數 14 struct sigaction act; 15 act.sa_flags = 0; 16 act.sa_handler = catch_sig; 17 sigemptyset(&act.sa_mask); 18 19 sigaction(SIGALRM, &act, NULL); 20 21 //setitimer 22 struct itimerval myit = {{3, 0}, {5, 0}}; 23 setitimer(ITIMER_REAL, &myit, NULL); 24 while(1) 25 { 26 printf("Who can kill me!\n"); 27 sleep(1); 28 } 29 30 return 0; 31 }
練習2: 驗證在信號處理函數執行期間,該信號多次遞送,那么只在處理函數之行結束后,處理一次。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <signal.h> 4 5 void catch_sig(int num) 6 { 7 printf("begin call, catch %d sig\n", num); 8 sleep(5); 9 printf("end call, catch %d sig\n", num); 10 } 11 12 int main() 13 { 14 struct sigaction act; 15 act.sa_flags = 0; 16 sigemptyset(&act.sa_mask); 17 sigaddset(&act.sa_mask, SIGQUIT); //臨時屏蔽 ctl+\ 信號 18 act.sa_handler = catch_sig; 19 20 //注冊捕捉 21 sigaction(SIGINT, &act, NULL); 22 23 while(1) 24 { 25 printf("who can kill me?\n"); 26 sleep(1); 27 } 28 29 return 0; 30 }
練習3:驗證sa_mask在捕捉函數執行期間的屏蔽作用。
示例同上
4)內核實現信號捕捉過程

(5)競態條件(時序競態)
pause函數
調用該函數可以造成進程主動掛起,等待信號喚醒。調用該系統調用的進程將處於阻塞狀態(主動放棄cpu) 直到有信號遞達將其喚醒。
int pause(void); 返回值:-1 並設置errno為EINTR
返回值:
1)如果信號的默認處理動作是終止進程,則進程終止,pause函數么有機會返回。
2)如果信號的默認處理動作是忽略,進程繼續處於掛起狀態,pause函數不返回。
3)如果信號的處理動作是捕捉,則【調用完信號處理函數之后,pause返回-1】,errno設置為EINTR,表示“被信號中斷”。想想我們還有哪個函數只有出錯返回值。
4)pause收到的信號不能被屏蔽,如果被屏蔽,那么pause就不能被喚醒。
練習:使用pause和alarm來實現sleep函數。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <signal.h> 4 #include <errno.h> 5 6 void sig_alarm(int signum) 7 { 8 printf("cat %d\n", signum); 9 return; 10 } 11 12 void mysleep(int seconds) 13 { 14 signal(SIGALRM, sig_alarm); 15 alarm(seconds); 16 printf("------pause out--------\n"); 17 while(pause() == -1 && errno == EINTR) 18 { 19 printf("------pause in--------\n"); 20 return; 21 } 22 } 23 24 int main() 25 { 26 printf("begin test mysleep...\n"); 27 mysleep(5); 28 printf("end test mysleep...\n"); 29 30 return 0; 31 }
(6)時序競態
1)前導例
設想如下場景:
欲睡覺,定鬧鍾10分鍾,希望10分鍾后鬧鈴將自己喚醒。
正常:定時,睡覺,10分鍾后被鬧鍾喚醒。
異常:鬧鍾定好后,被喚走,外出勞動,20分鍾后勞動結束。回來繼續睡覺計划,但勞動期間鬧鍾已經響過,不會再將我喚醒。
2)時序問題分析
回顧,借助pause和alarm實現的mysleep函數。設想如下時序:
a. 注冊SIGALRM信號處理函數 (sigaction...)
b. 調用alarm(1) 函數設定鬧鍾1秒。
c. 函數調用剛結束,開始倒計時1秒。當前進程失去cpu,內核調度優先級高的進程(有多個)取代當前進程。當前進程無法獲得cpu,進入就緒態等待cpu。
d. 1秒后,鬧鍾超時,內核向當前進程發送SIGALRM信號(自然定時法,與進程狀態無關),高優先級進程尚未執行完,當前進程仍處於就緒態,信號無法處理(未決)
e. 優先級高的進程執行完,當前進程獲得cpu資源,內核調度回當前進程執行。SIGALRM信號遞達,信號設置捕捉,執行處理函數sig_alarm。
f. 信號處理函數執行結束,返回當前進程主控流程,pause()被調用掛起等待。(欲等待alarm函數發送的SIGALRM信號將自己喚醒)
g. SIGALRM信號已經處理完畢,pause不會等到。
1 #include <unistd.h> 2 #include <signal.h> 3 #include <stdio.h> 4 5 void handler(int sig) //信號處理函數的實現 6 { 7 printf("SIGINT sig\n"); 8 } 9 10 int main() 11 { 12 sigset_t new, old; 13 struct sigaction act; 14 15 act.sa_handler = handler; //信號處理函數handler 16 sigemptyset(&act.sa_mask); 17 act.sa_flags = 0; 18 sigaction(SIGINT, &act, NULL); //准備捕捉SIGINT信號 19 20 sigemptyset(&new); 21 sigaddset(&new, SIGINT); 22 printf("Blocked before...\n"); 23 sigprocmask(SIG_BLOCK, &new, &old); //將SIGINT信號阻塞,同時保存當前信號集 24 25 printf("Blocked after...\n"); 26 sigprocmask(SIG_SETMASK, &old, NULL); //取消阻塞 27 28 printf("start sleep 10s...\n"); 29 sleep(10); 30 printf("end sleep 10s...\n"); 31 32 pause(); 33 printf("main progress end...\n"); 34 35 return 0; 36 }
執行結果:
[root@centos 09-linux-day07]# ./pause_bug Blocked before... Blocked after... start sleep 10s... ^CSIGINT sig end sleep 10s... ^CSIGINT sig main progress end...
可以看出在第一次ctl+c時,pause還沒有執行,此時信號已處理,執行完pause之后,程序就會一直掛起,再執行一次ctl+c,此時喚醒程序,程序執行結束。
上面實例的問題是:本來期望pause()之后,來SIGINT信號,可以結束程序;可是,如果當“取消阻塞”和“pause”之間,正好來了SIGINT信號,結果程序因為pause的原因會一直掛起。
3)解決時序問題
可以通過設置屏蔽SIGALRM的方法來控制程序執行邏輯,但無論如何設置,程序都有可能在“解除信號屏蔽”與“掛起等待信號”這個兩個操作間隙失去cpu資源。除非將這兩步驟合並成一個“原子操作”。sigsuspend函數具備這個功能。在對時序要求嚴格的場合下都應該使用sigsuspend替換pause。
int sigsuspend(const sigset_t *mask); 掛起等待信號。
sigsuspend函數調用期間,進程信號屏蔽字由其參數mask指定。
可將某個信號(如SIGALRM)從臨時信號屏蔽字mask中刪除,這樣在調用sigsuspend時將解除對該信號的屏蔽,然后掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復為原來的值。如果原來對該信號是屏蔽態,sigsuspend函數返回后仍然屏蔽該信號。
1 #include <unistd.h> 2 #include <signal.h> 3 #include <stdio.h> 4 5 void handler(int sig) //信號處理程序 6 { 7 if(sig == SIGINT) 8 printf("SIGINT sig\n"); 9 else if(sig == SIGQUIT) 10 printf("SIGQUIT sig\n"); 11 else 12 printf("SIGUSR1 sig\n"); 13 } 14 15 int main() 16 { 17 sigset_t new,old,wait; //三個信號集 18 struct sigaction act; 19 act.sa_handler = handler; 20 sigemptyset(&act.sa_mask); 21 act.sa_flags = 0; 22 sigaction(SIGINT, &act, NULL); //可以捕捉以下三個信號:SIGINT/SIGQUIT/SIGUSR1 23 sigaction(SIGQUIT, &act, NULL); 24 sigaction(SIGUSR1, &act, NULL); 25 26 sigemptyset(&new); 27 sigaddset(&new, SIGINT); //SIGINT信號加入到new信號集中 28 29 sigemptyset(&wait); 30 sigaddset(&wait, SIGUSR1); //SIGUSR1信號加入wait 31 32 sigprocmask(SIG_BLOCK, &new, &old); //將SIGINT阻塞,保存當前信號集到old中 33 //臨界區代碼執行 34 if(sigsuspend(&wait) != -1) //程序在此處掛起;用wait信號集替換new信號集。即:過來SIGUSR1信號,阻塞掉,程序繼續掛起;過來其他信號,例如SIGINT,則會喚醒程序。執行 35 { 36 perror("sigsuspend error:"); 37 return -1; 38 } 39 40 printf("After sigsuspend\n"); 41 42 sigprocmask(SIG_SETMASK, &old, NULL); 43 44 return 0; 45 }
執行結果:
窗口1: [root@centos 09-linux-day07]# ./pause_bugfix SIGINT sig SIGUSR1 sig After sigsuspend 窗口2: [root@centos ~]# ps aux | grep pause_bugfix | grep -v grep root 11442 0.0 0.0 4208 356 pts/0 S+ 23:19 0:00 ./pause_bugfix [root@centos ~]# kill -SIGUSR1 11442 [root@centos ~]# kill -SIGINT 11442
可以看到,當我們程序在 sigsuspend(&wait) 處掛起;用wait信號集替換new信號集。即:過來SIGUSR1信號,阻塞掉,程序繼續掛起;過來其他信號,例如SIGINT,則會喚醒程序。執行sigsuspend的原子操作。
注意:如果“sigaddset(&wait, SIGUSR1);”這句沒有,則此處不會阻塞任何信號,即過來任何信號均會喚醒程序。
練習:改進版mysleep
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <signal.h> 4 #include <errno.h> 5 6 void sig_alarm(int signum) 7 { 8 printf("cat %d\n", signum); 9 return; 10 } 11 12 void mysleep(int seconds) 13 { 14 sigset_t st; 15 struct sigaction act; 16 act.sa_handler = sig_alarm; 17 sigemptyset(&act.sa_mask); 18 act.sa_flags = 0; 19 sigaction(SIGALRM, &act, NULL); 20 21 sigdelset(&st, SIGALRM); 22 sigprocmask(SIG_BLOCK, &st, NULL); 23 24 alarm(seconds); 25 26 printf("------sigsuspend before--------\n"); 27 if (sigsuspend(&st) != -1) 28 { 29 perror("sigsuspend error:"); 30 return; 31 } 32 printf("------sigsuspend after--------\n"); 33 } 34 35 int main() 36 { 37 printf("begin test mysleep...\n"); 38 mysleep(5); 39 printf("end test mysleep...\n"); 40 41 return 0; 42 }
總結:
競態條件,跟系統負載有很緊密的關系,體現出信號的不可靠性。系統負載越嚴重,信號不可靠性越強。
不可靠由其實現原理所致。信號是通過軟件方式實現(跟內核調度高度依賴,延時性強),每次系統調用結束后,或中斷處理處理結束后,需通過掃描PCB中的未決信號集,來判斷是否應處理某個信號。當系統負載過重時,會出現時序混亂。
這種意外情況只能在編寫程序過程中,提早預見,主動規避,而無法通過gdb程序調試等其他手段彌補。且由於該錯誤不具規律性,后期捕捉和重現十分困難。
4)全局變量異步I/O
分析如下父子進程交替數數程序。當捕捉函數里面的sleep取消,程序即會出現問題。請分析原因。
1 #include <stdio.h> 2 #include <signal.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 6 int n = 0, flag = 0; 7 8 void sys_err(char *str) 9 { 10 perror(str); 11 exit(1); 12 } 13 14 void do_sig_child(int num) 15 { 16 printf("I am child %d\t%d\n", getpid(), n); 17 n += 2; 18 flag = 1; 19 sleep(1); 20 } 21 22 void do_sig_parent(int num) 23 { 24 printf("I am parent %d\t%d\n", getpid(), n); 25 n += 2; 26 flag = 1; 27 sleep(1); 28 } 29 30 int main(void) 31 { 32 pid_t pid; 33 struct sigaction act; 34 35 if ((pid = fork()) < 0) 36 { 37 sys_err("fork"); 38 } 39 else if (pid > 0) 40 { 41 n = 1; 42 sleep(1); 43 act.sa_handler = do_sig_parent; 44 sigemptyset(&act.sa_mask); 45 act.sa_flags = 0; 46 sigaction(SIGUSR2, &act, NULL); //注冊自己的信號捕捉函數 父使用SIGUSR2信號 47 do_sig_parent(0); 48 while (1) 49 { 50 /* wait for signal */; 51 if (flag == 1) { //父進程數數完成 52 kill(pid, SIGUSR1); 53 flag = 0; //標志已經給子進程發送完信號 54 } 55 } 56 } 57 else if (pid == 0) 58 { 59 n = 2; 60 act.sa_handler = do_sig_child; 61 sigemptyset(&act.sa_mask); 62 act.sa_flags = 0; 63 sigaction(SIGUSR1, &act, NULL); 64 65 while (1) 66 { 67 /* waiting for a signal */; 68 if (flag == 1) { 69 kill(getppid(), SIGUSR2); 70 flag = 0; 71 } 72 } 73 } 74 75 return 0; 76 }
示例中,通過flag變量標記程序實行進度。flag置1表示數數完成。flag置0表示給對方發送信號完成。
問題出現的位置,在父子進程kill函數之后需要緊接着調用 flag,將其置0,標記信號已經發送。但,在這期間很有可能被kernel調度,失去執行權利,而對方獲取了執行時間,通過發送信號回調捕捉函數,從而修改了全局的flag。
如何解決該問題呢?可以使用后續課程講到的“鎖”機制。當操作全局變量的時候,通過加鎖、解鎖來解決該問題。
現階段,我們在編程期間如若使用全局變量,應在主觀上注意全局變量的異步IO可能造成的問題。
1 #include <stdio.h> 2 #include <signal.h> 3 #include <unistd.h> 4 #include <stdlib.h> 5 6 pid_t pid; 7 int n = 0; 8 9 void sys_err(char *str) 10 { 11 perror(str); 12 exit(1); 13 } 14 15 void do_sig_child(int num) 16 { 17 printf("I am child %d\t%d\n", getpid(), n); 18 n += 2; 19 kill(getppid(), SIGUSR2); 20 } 21 22 void do_sig_parent(int num) 23 { 24 printf("I am parent %d\t%d\n", getpid(), n); 25 n += 2; 26 kill(pid, SIGUSR1); 27 } 28 29 int main(void) 30 { 31 if ((pid = fork()) < 0) 32 { 33 sys_err("fork"); 34 } 35 else if (pid > 0) 36 { 37 struct sigaction act; 38 usleep(10); //等待父進程注冊完畢 39 n = 1; 40 act.sa_handler = do_sig_parent; 41 sigemptyset(&act.sa_mask); 42 act.sa_flags = 0; 43 sigaction(SIGUSR2, &act, NULL); //注冊自己的信號捕捉函數 父使用SIGUSR2信號 44 do_sig_parent(0); 45 46 while (1) { 47 } 48 } else if (pid == 0) { 49 struct sigaction act; 50 n = 2; 51 act.sa_handler = do_sig_child; 52 sigemptyset(&act.sa_mask); 53 act.sa_flags = 0; 54 sigaction(SIGUSR1, &act, NULL); 55 56 while (1) { 57 } 58 } 59 60 return 0; 61 }
5)可/不可重入函數
一個函數在被調用執行期間(尚未調用結束),由於某種時序又被重復調用,稱之為“重入”。根據函數實現的方法可分為“可重入函數”和“不可重入函數”兩種。看如下時序。

顯然,insert函數是不可重入函數,重入調用,會導致意外結果呈現。究其原因,是該函數內部實現使用了全局變量。
注意事項:
1)定義可重入函數,函數內不能含有全局變量及static變量,不能使用malloc、free
2)信號捕捉函數應設計為可重入函數
3)信號處理程序可以調用的可重入函數可參閱man 7 signal
4)沒有包含在上述列表中的函數大多是不可重入的,其原因為:
a. 使用靜態數據結構
b. 調用了malloc或free
c. 是標准I/O函數
(7)SIGCHLD函數
1)SIGCHLD產生的條件
- 子進程終止時
- 子進程接收到SIGSTOP信號停止時
- 子進程處在停止態,接受到SIGCONT后喚醒時
2)借助SIGCHLD函數回收子進程
子進程結束運行,其父進程會收到SIGCHLD信號。該信號的默認處理動作是忽略。可以捕捉該信號,在捕捉函數中完成子進程狀態的回收。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/wait.h> 4 #include <signal.h> 5 6 void catch_sig(int num) 7 { 8 pid_t wpid; 9 while((wpid = waitpid(-1, NULL, WNOHANG)) > 0) 10 { 11 printf("wait child %d ok\n", wpid); 12 } 13 } 14 15 int main() 16 { 17 int i = 0; 18 pid_t pid; 19 for (i = 0; i < 10; i++) 20 { 21 pid = fork(); 22 if (pid == 0) 23 { 24 break; 25 } 26 } 27 28 if (i == 10) 29 { 30 //sleep(2); //模擬注冊晚於子進程死亡,此時子進程沒有回收,全部成為僵屍進程 31 struct sigaction act; 32 act.sa_flags = 0; 33 sigemptyset(&act.sa_mask); 34 act.sa_handler = catch_sig; 35 36 sigaction(SIGCHLD, &act, NULL); 37 while(1) 38 { 39 sleep(1); 40 } 41 } 42 else if(pid < 10) 43 { 44 printf("I am %d child, pid = %d\n", i, getpid()); 45 //sleep(i); //不加第30行代碼,需要加上這行代碼,目的就是注冊早於子進程死亡,否則出現僵屍進程 46 } 47 48 return 0; 49 }
分析該例子。結合 17)SIGCHLD 信號默認動作,掌握父使用捕捉函數回收子進程的方式。
如果每創建一個子進程后不使用sleep可以嗎?可不可以將程序中,捕捉函數內部的while替換為if?為什么?
if ((pid = waitpid(0, &status, WNOHANG)) > 0) { ... }
思考:信號不支持排隊,當正在執行SIGCHLD捕捉函數時,再過來一個或多個SIGCHLD信號怎么辦?
3)子進程結束status處理方式
pid_t waitpid(pid_t pid, int *status, int options)
options
WNOHANG
沒有子進程結束,立即返回
WUNTRACED
如果子進程由於被停止產生的SIGCHLD,waitpid則立即返回
WCONTINUED
如果子進程由於被SIGCONT喚醒而產生的SIGCHLD,waitpid則立即返回
獲取status
WIFEXITED(status)
子進程正常exit終止,返回真
WEXITSTATUS(status)返回子進程正常退出值
WIFSIGNALED(status)
子進程被信號終止,返回真
WTERMSIG(status)返回終止子進程的信號值
WIFSTOPPED(status)
子進程被停止,返回真
WSTOPSIG(status)返回停止子進程的信號值
WIFCONTINUED(status)
SIGCHLD信號注意問題:
- 子進程繼承了父進程的信號屏蔽字和信號處理動作,但子進程沒有繼承未決信號集spending。
- 注意注冊信號捕捉函數的位置。
- 應該在fork之前,阻塞SIGCHLD信號。注冊完捕捉函數后解除阻塞。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/wait.h> 4 #include <signal.h> 5 6 void catch_sig(int num) 7 { 8 pid_t wpid; 9 while((wpid = waitpid(-1, NULL, WNOHANG)) > 0) 10 { 11 printf("wait child %d ok\n", wpid); 12 } 13 } 14 15 int main() 16 { 17 int i = 0; 18 pid_t pid; 19 //在創建子進程之前屏蔽SIGCHLD信號 20 sigset_t myset, oldset; 21 sigemptyset(&myset); 22 sigaddset(&myset, SIGCHLD); 23 //oldset 保留現場,設置了SIGCHLD到阻塞信號集 24 sigprocmask(SIG_BLOCK, &myset, &oldset); 25 26 for (i = 0; i < 10; i++) 27 { 28 pid = fork(); 29 if (pid == 0) 30 { 31 break; 32 } 33 } 34 35 if (i == 10) 36 { 37 //sleep(2); //模擬注冊晚於子進程死亡,此時子進程會被全部回收 38 struct sigaction act; 39 act.sa_flags = 0; 40 sigemptyset(&act.sa_mask); 41 act.sa_handler = catch_sig; 42 43 sigaction(SIGCHLD, &act, NULL); 44 45 //解除屏蔽現場 46 sigprocmask(SIG_SETMASK, &oldset, NULL); 47 48 while(1) 49 { 50 sleep(1); 51 } 52 } 53 else if(pid < 10) 54 { 55 printf("I am %d child, pid = %d\n", i, getpid()); 56 //sleep(i); 57 } 58 59 return 0; 60 }
(8)信號傳參
1)發送信號傳參
sigqueue函數對應kill函數,但可在向指定進程發送信號的同時攜帶參數
int sigqueue(pid_t pid, int sig, const union sigval value); 成功:0;失敗:-1,設置errno union sigval { int sival_int; void *sival_ptr; };
向指定進程發送指定信號的同時,攜帶數據。但,如傳地址,需注意,不同進程之間虛擬地址空間各自獨立,將當前進程地址傳遞給另一進程沒有實際意義。
2)捕捉函數傳參
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
當注冊信號捕捉函數,希望獲取更多信號相關信息,不應使用sa_handler而應該使用sa_sigaction。但此時的sa_flags必須指定為SA_SIGINFO。siginfo_t是一個成員十分豐富的結構體類型,可以攜帶各種與信號相關的數據。
(9)中斷系統調用
系統調用可分為兩類:慢速系統調用和其他系統調用。
- 慢速系統調用:可能會使進程永遠阻塞的一類。如果在阻塞期間收到一個信號,該系統調用就被中斷,不再繼續執行(早期);也可以設定系統調用是否重啟。如,read、write、pause、wait...
- 其他系統調用:getpid、getppid、fork...
結合pause,回顧慢速系統調用:
慢速系統調用被中斷的相關行為,實際上就是pause的行為: 如,read
1)想中斷pause,信號不能被屏蔽。
2)信號的處理方式必須是捕捉 (默認、忽略都不可以)
3)中斷后返回-1, 設置errno為EINTR(表“被信號中斷”)
可修改sa_flags參數來設置被信號中斷后系統調用是否重啟。SA_INTERRURT不重啟。 SA_RESTART重啟。
擴展了解:
sa_flags還有很多可選參數,適用於不同情況。如:捕捉到信號后,在執行捕捉函數期間,不希望自動阻塞該信號,可將sa_flags設置為SA_NODEFER,除非sa_mask中包含該信號。
練習:
1. 使用setitimer實現每個一秒打印一次hello world
2. 使用SIGUSR1和SIGUSR2在父子進程之間進行消息傳遞,實現父子進程交替報數(每隔1s)
a. kill(pid, sig)發送信號
b. 父子進程捕捉信號
3. 在父子進程進程管道通信時,如果管道讀端都關閉,會收到SIGPIPE信號,模擬場景,對該信號進行捕捉,並且使用捕捉函數回收子進程。
參考文獻:
- http://blog.sina.com.cn/s/blog_6af9566301013xp4.html
- https://www.cnblogs.com/kex1n/p/8296332.html (Linux的SIGUSR1和SIGUSR2信號)
- https://www.cnblogs.com/nufangrensheng/p/3516427.html (信號之sigsuspend函數)

