【Linux】一篇文章徹底搞定信號!【轉】

轉自:https://blog.csdn.net/w903414/article/details/109802539

信號
1.信號是什麼?
2.信號的種類
3.信號的產生
3.1硬件產生
3.2軟件產生
4.信號的註冊
4.1非可靠信號的註冊
4.2可靠信號的註冊
5.信號的註銷
5.1非可靠信號的註銷
5.2可靠信號的註銷
6.信號阻塞
6.1信號是怎樣阻塞的?
6.2sigprocmask
7.信號未決
7.1未決概念
7.2sigpending
8.信號的處理方式
8.1signal函數
8.2sigaction函數
8.3自定義信號處理的流程
9.信號的捕捉
9.1信號捕捉的條件
9.2信號捕捉流程
10.常用信號集操作函數
11.SIGCHLD信號
1.信號是什麼?
信號其實就是一個軟件中斷。

例:

輸入命令,在Shell下啓動一個前臺進程。
用戶按下Ctrl-C,鍵盤輸入產生一個硬件中斷。
如果CPU當前正在執行這個進程的代碼,則該進程的用戶空間代碼暫停執行, CPU從用戶
態切換到內核態處理硬件中斷。
終端驅動程序將Ctrl-C解釋成一個SIGINT信號,記在該進程的PCB中(也可以說發送了一
個SIGINT信號給該進程)。
當某個時刻要從內核返回到該進程的用戶空間代碼繼續執行之前,首先處理PCB中記錄的
信號,發現有一個SIGINT信號待處理,而這個信號的默認處理動作是終止進程,所以直接
終止進程而不再返回它的用戶空間代碼執行。
在這個例子中,由ctrl+c產生的硬件中斷就是一個信號。Ctrl+C產生的信號只能發送給前臺進程,命令後加&就可放到後臺運行。
Shell可同時運行一個前臺進程和任意多個後臺進程,只有前臺進程才能接受到像CTRL+C這種控制鍵產生的信號。

2.信號的種類
使用命令查看:kill -l

非可靠信號:1~31號信號,信號可能會丟失
可靠信號:34~64號信號,信號不可能丟失

SIGHUP:1號信號,Hangup detected on controlling terminal or death of controlling process(在控制終端上掛起信號,或讓進程結束),ation:term

SIGINT:2號信號,Interrupt from keyboard(鍵盤輸入中斷,ctrl + c ),action:term

SIGQUIT:3號信號,Quit from keyboard(鍵盤輸入退出,ctrl+ | ),action:core,產生core dump文件

SIGABRT:6號信號,Abort signal from abort(3)(非正常終止,double free),action:core

SIGKILL:9號信號,Kill signal(殺死進程信號),action:term,該信號不能被阻塞、忽略、自定義處理

SIGSEGV:11號信號,Invalid memory reference(無效的內存引用,解引用空指針、內存越界訪問),action:core

SIGPIPE:13號信號,Broken pipe: write to pipe with no readers(管道中止: 寫入無人讀取的管道,會導致管道破裂),action:term

SIGCHLD:17號信號,Child stopped or terminated(子進程發送給父進程的信號,但該信號爲忽略處理的)

SIGSTOP:19號信號,Stop process(停止進程),action:stop

SIGTSTP:20號信號,Stop typed at terminal(終端上發出的停止信號,ctrl + z),action:stop

具體的信號採取的動作和詳細信息可查看:man 7 signal

3.信號的產生
3.1硬件產生
硬件產生即通過終端按鍵產生的信號:

ctrl + c:SIGINT(2),發送給前臺進程,& 進程放到後臺運行,fg 把剛剛放到後臺的進程,再放到前臺來運行
ctrl + z:SIGTSTP(20),一般不用,除非有特定場景
ctrl + | :SIGQUIT(3),產生core dump文件
產生core dump文件的條件:

當前OS一定不要限制core dump文件的大小,ulimit -a
磁盤空間要足夠
如何產生:
3.1 解引用空指針,收到11號信號,產生core dump文件
3.2 內存訪問越界,程序一旦崩潰,就會收到11號信號,也就會產生core dump文件
3.3 double free,收到6號信號,併產生core dump。
3.4 free(NULL),不會崩潰
3.2軟件產生
軟件產生即調用系統函數向進程發信號

kill函數
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
參數解釋:
pid:進程號
sig:要發送的信號值
返回值:成功返回0,失敗返回-1,並設置錯誤

kill命令:kill -[信號] pid,
abort:void abort(void);,收到6號信號,誰調用該函數,誰就收到信號
alarm:unsigned int alarm(unsigned int seconds);,收到14號信號,告訴內核在seconds秒後給進程發送SIGALRM信號,該信號默認處理動作爲終止當前進程。
4.信號的註冊
信號註冊又分爲可靠信號的註冊和非可靠信號的註冊。
信號註冊實際上是一個位圖和一個sigqueue隊列。


4.1非可靠信號的註冊
當進程收到非可靠信號時:

將非可靠信號對應的比特位置爲1
添加sigqueue節點到sigqueue隊列當中,但是,在添加sigqueue節點的時候,隊列當中已然有了該信號的sigqueue節點,則不添加
4.2可靠信號的註冊
當進程所受到可靠信號時:

在sig位圖中更改信號對應的比特位爲1
不論之前sigqueue隊列中是否存在該信號的sigqueue節點,都再次添加sigqueue節點到sigqueue隊列當中去
5.信號的註銷
5.1非可靠信號的註銷
信號對應的比特位從1置爲0
將該信號的sigqueue節點從sigqueue隊列當中進行出隊操作
5.2可靠信號的註銷
將該信號的sigqueue節點從sigqueue隊列當中進行出隊操作
需要判斷sigqueue隊列當中是否還有相同的sigqueue節點:
①沒有了:信號比特位從1置爲0
②還有:不會更改sig位圖中的比特位
6.信號阻塞
6.1信號是怎樣阻塞的?


信號的阻塞,並不會干擾信號的註冊。信號能註冊,但不能被立即處理,
將block位圖中對應的信號比特位置爲1,表示阻塞該信號
進程收到該信號,還是一如既往的註冊
當進程進入到內核空間,準備返回用戶空間的時候,調用do_signal函數,就不會立即去處理該信號了
當該信號不被阻塞後,就可以進行處理了
6.2sigprocmask
函數原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
參數解釋:

how,該做什麼樣的操作
SIG_BLOCK:設置信號爲阻塞
SIG_UNBLOCK:解除信號阻塞
SIG_SETMASK:替換阻塞位圖
set:用來設置阻塞位圖
SIG_BLOCK:設置某個信號爲阻塞,block(new) = block(old) | set
SIG_UNBLOCK:解除某個信號阻塞,block(new)= block(old) & (~set)
SIG_SETMASK:替換阻塞位圖,block(new)= set
oldset:原來的阻塞位圖
例:下述例子,信號全部被阻塞,採用kill -9,將該進程結束掉

 

結果: 此時發送信號是不會有作用的,採用kill -9強殺掉

 

代碼:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>


void signcallback(int signumber)
{
printf("change the signal %d\n",signumber);
}

int main()
{
sigset_t set;
sigset_t oldset;
sigfillset(&set);//所有比特位全置爲1,則信號全部會被阻塞
sigprocmask(SIG_BLOCK,&set,&oldset);
while(1)
{
sleep(1);
}

return 0;
}

7.信號未決
7.1未決概念
實際執行信號的處理動作稱爲信號遞達(Delivery),信號從產生到遞達之間的狀態,稱爲信號未決(Pending)。
進程可以選擇阻塞(Block)某個信號。被阻塞的信號產生時將保持在未決狀態,直到進程解除對此信號的阻塞,才執行遞達的動作。注意,阻塞和忽略是不同的,只要信號被阻塞就不會遞達,而忽略是、在遞達之後可選的一種處理動作。

7.2sigpending
函數原型:int sigpending(sigset_t *set);
讀取當前進程的未決信號集,通過set參數傳出。調用成功返回0,出錯返回-1.

例:

 

結果:

 

代碼:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void signalcallback(int signumber)
{
printf("chang signumber %d\n",signumber);
}
void printsigset(sigset_t *set)
{
int i = 0;
for(;i < 32;i++)
{
if(sigismember(set,i))
{
putchar('1');
}
else{
putchar('0');
}
}
}

int main()
{
signal(2,signalcallback);
signal(10,signalcallback);
sigset_t set;
sigset_t oldset;
sigset_t pending;
sigfillset(&set);//所有比特位全部置爲1,則信號會全部被阻塞
sigprocmask(SIG_BLOCK,&set,&oldset);
while(1)
{
sigpending(&pending);
printsigset(&pending);
sleep(1);
}

return 0;
}

8.信號的處理方式


每個信號都有兩個標誌位分別表示阻塞和未決,還有一個函數指針表示處理動作。
1
在上述例子中:

SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作。
SIGINT信號產生過,但正在被阻塞,所以暫時不能遞達。雖然它的處理動作是忽略,但在沒有解除阻塞之前不能忽略這個信號,因爲進程仍有機會改變處理動作之後再解除阻塞。
SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義函數sighandler。
8.1signal函數
該函數可以更改信號的處理動作。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
參數解釋:

signum:更改的信號值
handler:函數指針,要更改的動作是什麼
實際上,該函數內部也調用了sigaction函數。

8.2sigaction函數
讀取和修改與指定信號相關聯的處理動作。

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
參數解釋:

signum:待更改的信號值
struct sigaction結構體:

void (*sa_handler)(int);//函數指針,保存了內核對信號的處理方式
void (*sa_sigaction)(int, siginfo_t *, void *);//
sigset_t sa_mask;//保存的是當進程在處理信號的時候,收到的信號
int sa_flags;//SA_SIGINFO,OS在處理信號的時候,調用的就是sa_sigaction函數指針當中
//保存的值0,在處理信號的時候,調用sa_handler保存的函數
void (*sa_restorer)(void);

例:

 

結果:

 

代碼:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void signcallback(int signumber)
{
printf("change signumber %d\n",signumber);
}


int main()
{
struct sigaction act;//act爲入參
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = signcallback;

struct sigaction oldact;//oldact爲出參
sigaction(3,&act,&oldact);
while(1)
{
sleep(1);
}
return 0;
}


8.3自定義信號處理的流程


task_struct結構體中有一個struct sighand_struct結構體。
struct sighand_struct結構體有一個struct k_sigaction action[_NSIG]結構體數組。
該數組中,其中的_sighandler_t sa_handler保存的是信號的處理方式,通過改變其指向,可以實現我們對自定義信號的處理。
9.信號的捕捉
9.1信號捕捉的條件
如果信號的處理動作是用戶自定義函數,在信號遞達時就調用這個函數,這就稱爲信號捕捉。

9.2信號捕捉流程


內核態返回用戶態會調用do_signal函數,兩種情況:

無信號:sys_return函數,返回用戶態
有信號:先處理信號,信號返回,再調用do_signal函數
例:

程序註冊了SIGQUIT信號的處理函數sighandler。
當前正在執行main函數,這時發生中斷或異常切換到內核態。
在中斷處理完畢後要返回用戶態的main函數之前檢查到有信號SIGQUIT遞達。
內核決定返回用戶態後不是恢復main函數的上下文繼續執行,而是執行sighandler函數, sighandler和main函數使用不同的堆棧空間,它們之間不存在調用和被調用的關係,是兩個獨立的控制流程。
sighandler函數返回後自動執行特殊的系統調用sigreturn再次進入內核態。
如果沒有新的信號要遞達,這次再返回用戶態就是恢復main函數的上下文繼續執行了。
10.常用信號集操作函數
int sigemptyset(sigset_t *set);://將比特位圖全置爲0

int sigfillset(sigset_t *set);//將比特位圖全置爲1

int sigaddset(sigset_t *set, int signum);//將該set位圖,多少號信號置爲1

int sigdelset(sigset_t *set, int signum);//將該set位圖,多少號信號置爲0

int sigismember(const sigset_t *set, int signum);//信號signum是否是set位圖中的信號

11.SIGCHLD信號
該信號是子進程在結束是發送給父進程的信號,但是該信號的處理方式是默認處理的。
父進程對子進程發送過來的SIGCHLD信號進行了忽略處理,就會導致子進程成爲殭屍進程。

可以自定義該信號的處理方式:

指令查看後臺:ps aux | grep ./fork


代碼:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>

void signcallback(int signumber)
{
printf("change signal %d\n",signumber);
wait(NULL);
}

int main()
{
signal(17,signcallback);
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
return -1;
}
else if(pid == 0)
{
printf("I am child\n");
sleep(1);
exit(12);
}
else{
while(1)
{
sleep(1);
}
}
return 0;
}


碼字不易,點個贊是對博主最大的支持!!

轉載請註明出處!
————————————————
版權聲明:本文爲CSDN博主「903419」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/w903414/article/details/109802539

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章