Linux下IPC方式之信號2
3. 信號集操作函數
內核通過讀取未決信號集來判斷信號是否應被處理。信號屏蔽字mask可以影響未決信號集。而我們可以在應用程序中自定義set來改變mask。已達到屏蔽指定信號的目的。
3.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
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);——用來設置或者解除阻塞信號集
int sigpending(sigset_t *set);——獲取未決信號集
sigset_t
類型的本質是位圖。但不應該直接使用位操作,而應該使用上述函數,保證跨系統操作有效。
對比認知select
函數。
3.2 sigprocmask函數
用來屏蔽信號、解除屏蔽也使用該函數。其本質,讀取或修改進程的信號屏蔽字(PCB中)
嚴格注意,屏蔽信號:只是將信號處理延後執行(延至解除屏蔽);而忽略表示將信號丟處理。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
成功:0;失敗:-1,設置errno
參數:
set: 傳入參數,是一個位圖,set中哪位置1,就表示當前進程屏蔽哪個信號。
oldset: 傳出參數,保存舊的信號屏蔽集。
how參數取值: 假設當前的信號屏蔽字爲mask
1.SIG_BLOCK: 設置阻塞。 當how設置爲此值,set表示需要屏蔽的信號。相當於 mask = mask|set
2.SIG_UNBLOCK: 解除阻塞。 當how設置爲此,set表示需要解除屏蔽的信號。相當於 mask = mask & ~set
3.SIG_SETMASK: 設置set爲新的阻塞信號集。 當how設置爲此,set表示用於替代原始屏蔽集的新屏蔽集。相當於 mask = set,調用sigprocmask解除了對當前若干個信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達。
3.3 sigpending函數
讀取當前進程的未決信號集
int sigpending(sigset_t *set);
set傳出參數。
返回值:成功:0;失敗:-1,設置errno
3.4 把所有常規信號的未決信號集打到屏幕上
將2號信號(ctrl+c)放在阻塞信號集上,這樣該信號在未決信號集上會進行保留
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int main(){
sigset_t pend, sigproc;
//設置阻塞信號,等待按鍵產生信號
//先清空
sigemptyset(&sigproc);
//將2號信號(ctrl+c)放在集合裏
sigaddset(&sigproc, SIGINT);
//設置阻塞信號集
sigprocmask(SIG_BLOCK, &sigproc, NULL);
//循環取未決信號集中的信號
while(1){
//循環讀取未決信號,打印
sigpending(&pend);
for(int i=0; i<32; i++){
//存在信號集中
if(sigismember(&pend, i)==1){
printf("1");
}else{
printf("0");
}
}
printf("\n");
sleep(1);
}
return 0;
}
4. 信號捕捉(重要)
作用主要是爲了防止進程意外死亡
4.1 signal函數
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum 要捕捉的信號
handler 要執行的捕捉函數指針,函數應該聲明 void func(int);//函數名可變
4.2 sigaction函數
修改信號處理動作(通常在Linux用其來註冊一個信號的捕捉函數)
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
成功:0;失敗:-1,設置errno
signum: 捕捉的信號
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
,表使用默認屬性。
捕捉我們定時給自己發送的14號自殺信號
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/time.h>
//定義捕捉函數
void catch_sig(int num){
printf("catch %d sig\n", num);
}
int main(){
//註冊捕捉函數
struct sigaction act;
//說明爲你使用的是sigaction結構體中的第一個捕捉函數
act.sa_flags=0;
//那個捕捉函數的函數指針指向我們上面自己寫的捕捉函數catch_sig
act.sa_handler=catch_sig;
//清空信號集
sigemptyset(&act.sa_mask);
sigaction(SIGALRM, &act, NULL);
//setitimer 5秒之後每隔3秒來一次信號
struct itimerval myit={{3,0},{5,0}};
setitimer(ITIMER_REAL, &myit, NULL);
while(1){
printf("Who can kill me!\n");
sleep(1);
}
return 0;
}
運行結果:
4.3 信號捕捉特性
- 進程正常運行時,默認PCB中有一個信號屏蔽字,假定爲☆,它決定了進程自動屏蔽哪些信號。當註冊了某個信號捕捉函數,捕捉到該信號以後,要調用該函數。而該函數有可能執行很長時間,在這期間所屏蔽的信號不由☆來指定。而是用sa_mask來指定。調用完信號處理函數,再恢復爲☆。
- XXX信號捕捉函數執行期間,XXX信號自動被屏蔽。
- 阻塞的常規信號不支持排隊,產生多次只記錄一次。(後32個實時信號支持排隊)
4.4 內核實現信號捕捉過程
5. SIGCHLD信號(重要)
5.1 SIGCHLD的產生條件
- 子進程終止時
- 子進程接收到SIGSTOP信號停止時
- 子進程處在停止態,接受到SIGCONT後喚醒時
5.2 藉助SIGCHLD信號回收子進程
子進程暫停或者結束運行,其父進程會收到SIGCHLD
信號。該信號的默認處理動作是忽略。可以捕捉該信號,在捕捉函數中完成子進程狀態的回收。
我們可以通過捕捉SIGCHLD
信號來回收子進程。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
void catch_sig(int num){
pid_t pid=waitpid(-1, NULL, WNOHANG);
if(pid>0){
printf("wait child %d ok\n", pid);
}
}
int main(){
int i=0;
pid_t pid;
for(i=0; i<10; i++){
pid=fork();
if(pid==0){
break;
}
}
if(i==10){
//father
struct sigaction act;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
act.sa_handler=catch_sig;
sigaction(SIGCHLD, &act, NULL);
while(1){
sleep(1);
}
}else if(i<10){
printf("I am %d child, pid=%d\n", i, getpid());
sleep(i);
}
return 0;
}
上面的程序是有問題的,如果十個子進程不是依次死去,而是一起死,就會出現殭屍進程,因爲信號不排隊的。
把上面程序sleep(i);
註釋掉
else if(i<10){
printf("I am %d child, pid=%d\n", i, getpid());
//sleep(i);
}
運行結果:
改進一下上面的代碼,讓他不出殭屍進程。我們即將信號收集函數catch_sig
裏面用一個while循環來循環處理pid收集到的各個SIGCHLD
信號。
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
void catch_sig(int num){
pid_t pid;
//此時,如果多個子進程一起死,pid對獲得多個信號
while((pid=waitpid(-1, NULL, WNOHANG))>0) {
if(pid>0){
printf("wait child %d ok\n", pid);
}
}
}
int main(){
int i=0;
pid_t pid;
for(i=0; i<10; i++){
pid=fork();
if(pid==0){
break;
}
}
if(i==10){
//father
struct sigaction act;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
act.sa_handler=catch_sig;
sigaction(SIGCHLD, &act, NULL);
while(1){
sleep(1);
}
}else if(i<10){
printf("I am %d child, pid=%d\n", i, getpid());
}
return 0;
}
如果註冊sigaction
之前,子進程就死了,那麼還是會產生殭屍進程。如何避免?
子進程死之前,如果main
函數還沒註冊sigaction
,就把SIGCHLD
信號屏蔽即可。屏蔽信號的工作,應該在創建子進程之前就開始去做,如此可以避免極端情況下出現差錯。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void catch_sig(int num)
{
pid_t wpid ;
while( (wpid = waitpid(-1,NULL,WNOHANG)) > 0 ){
printf("wait child %d ok\n",wpid);
}
}
int main()
{
int i =0;
pid_t pid ;
//在創建子進程之前屏蔽SIGCHLD信號
sigset_t myset,oldset;
sigemptyset(&myset);
sigaddset(&myset,SIGCHLD);
//oldset 保留現場,設置了SIGCHLD到阻塞信號集
sigprocmask(SIG_BLOCK,&myset,&oldset);
for(i = 0 ; i < 10 ; i ++){
pid =fork();
if(pid == 0 ){
break;
}
}
if(i == 10 ){
//parent
//模擬註冊晚於子進程死亡
sleep(2);
struct sigaction act ;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = catch_sig;
sigaction(SIGCHLD,&act,NULL);
//解除屏蔽現場
sigprocmask(SIG_SETMASK,&oldset,NULL);
while(1){
sleep(1);
}
}else if(i < 10){
printf("I am %d child,pid = %d\n",i,getpid());
}
return 0;
}