整體程序

信號的入門
1、生活中的例子
1、 你在網上買了很多件商品
(多種信號),再等待不同商品快遞的到來,但即便快遞沒有到來,也就是你能“識別快遞”(識別多種信號),(信號達到之前)
2、當快遞員到了你樓下,你收到了通知,但是你正在打游戲,需5min之后才能去取快遞,那么在這5min之內,你并沒有去取快遞,但是你是知道有快遞到來了,也就是取快遞的行為并不是一定要立即執行,可以理解成“在合適的時候去取”,(作業系統收到了信號并不立即處理,這是可能還有比處理信號更重要的事要做,)
3、當你覺得時間合適了,拿到快遞之后,就要處理快遞了,而處理快遞(處理信號的三種方式)一般方式有三種:(1)、執行默認動作(幸福的打開快遞,使用商品),(2)、 執行自定義動作(快遞是零食,你要送給你你的女朋友),(3)、 忽略快遞(快遞拿上來之后,扔掉床頭,繼續開一把游戲),
4、快遞到來的整個程序,對你來講是異步的,你不能準確斷定快遞員什么時候給你打電話,
2、技術應用角度的信號
實驗代碼:
#include<stdio.h>
#include<unistd.h>
int main(){
while(1){
printf("I am running....\n");
sleep(2);
}
return 0;
}
運行截圖:

當我在控制臺輸入命令啟動一個行程,當按下
ctrl+C,這個鍵盤輸入產生一個硬體中斷,被OS所截獲,解釋成信號,發送目標前臺行程一個SIGINT信號,前臺行程收到一個信號,進而引起行程退出,
3、注意
1、Ctrl+C產生的信號只能發給前臺行程,系統中只允許有一個前臺行程,一個命令后面加&可以把這個行程放在后臺運行,這樣Shell就不必等待行程結束就可以接受新的命令,啟動新的行程,
2、Shell可以同時運行一個前臺行程和任意多個后臺行程,只有前臺行程才能接到像Ctrl+C這種控制鍵產生的信號,
3、前臺行程在運行程序中用戶隨時可能按下Ctrl+C而產生信號,也就是說該行程的用戶空間代碼執行到任何地方都有可能收到SIGINT信號而終止,所以信號相對于行程的控制流來說是異步的,
4、信號的概念
信號是行程之間事件異步通知的一種方式,屬于軟中斷,它通知行程系統中發生了一個某種型別的事件,
5、用kill -l命令可以察看系統定義的信號串列
每個信號都有一個編號和一個宏定義名稱,這些宏定義可以在signal.h中找到,編號34–64的信號實時信號,我們只討論34以下的信號,
注意:其中9號信號是不能不自定義捕捉的,

6.、信號處理常見方式概覽
處理動作有以下三種:
1、忽略此信號,
2、執行該信號的默認處理動作,
3、提供一個信號處理函式,用戶自定義函式,這種方式稱為捕捉,
產生信號
1、呼叫系統函式向行程發送信號
kill函式
//作用:行程通過呼叫kill函式發送信號給其他行程(包括它們自己)
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
//成功回傳0,錯誤回傳-1
實體:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
int main(int argc,char* argv[]){
if(argc==3){
kill(atoi(argv[1]),atoi(argv[2]));
}
return 0;
}
//通過運行一個后臺行程,然后通過呼叫這個程式來殺死那個后臺行程
運行結果:

raise函式
//作用:呼叫raise函式可以自己給自己發信號
#include <signal.h>
int raise(int sig);
//成功回傳0,錯誤回傳-1
實體:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
void handler(int sno){ //自定義一個信號
printf("I catch a signal..\n");
}
int main(int argc,char* argv[]){
signal(2,handler); //注冊一個信號
while(1){
sleep(1);
raise(2); //自己給自己發送2號信號
}
return 0;
}
運行結果:

abort函式
//作用:abort函式使當前行程接收到一個6信號而例外終止
#include <stdlib.h>
void abort(void);
實體:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
void handler(int sno){ //自定義捕捉一個信號
printf("I catch a signal : %d\n",sno);
}
int main(){
signal(6,handler); //注冊一個信號
while(1){
sleep(1);
abort();
}
return 0;
}
運行結果:

2、由軟體條件產生信號
呼叫alarm函式可以設定一個鬧鐘,也就是告訴內核在seconds秒之后給當前行程發送14號信號(SIGALRM信號),該信號的默認處理動作是終止該行程,
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//回傳:前一次鬧鐘剩余的秒數,若以前沒有設定鬧鐘,則為0
實體:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
int main(){
alarm(1);
int count=0;
while(1){
printf("%d\n",count++);
}
return 0;
}
運行結果:

3、 硬體例外產生信號
硬體例外是指:例外被硬體檢測到并通知OS,內核向當前行程發送適當的信號,例如:當前行程執行了除0的指令,CPU的運算單元會產生例外,內核將這個例外解釋 為SIGFPE信號發送給行程,
再例如:當前行程訪問了非法的記憶體地址,MMU會產生例外,內核將這個例外解釋為SIGSEGV信號發送給行程,
模擬野指標例外
模擬代碼:
#include<stdio.h>
int main(){
int *p=NULL;
*p=100;
while(1);
return 0;
}
運行結果:

證明收到了SIGSEGV信號:
#include<stdio.h>
#include<signal.h>
void handler(int sno){
printf("I catch a signal..\n");
}
int main(){
signal(SIGSEGV,handler);
int *p=NULL;
*p=100;
while(1);
return 0;
}
運行結果:

我們自定義了捕捉SIGSEGV信號,一直會列印這個結果那條訊息,因為我們捕捉了并沒有處理這個結果,所以一直會存在段錯誤那個例外,
模擬除0例外
模擬代碼:
#include<stdio.h>
int main(){
int t=1/0;
return 0;
}
運行結果:

證明收到了SIGFPE信號:
#include<stdio.h>
#include<signal.h>
void handler(int sno){
printf("I catch a signal..\n");
}
int main(){
signal(SIGFPE,handler);
int i=1/0;
return 0;
}
運行結果:

我們自定義了捕捉SIGFPE信號,一直會列印這個結果那條訊息,因為我們捕捉了并沒有處理這個結果,所以一直會存在除零那個例外,
由此可以確認,我們在C/C++當中的除零,記憶體越界等例外,在系統層面上,是被當成信號處理的,
4、 通過鍵盤發送信號
在鍵盤上出入Ctrl+C會導致內核發送一個SIGINT信號到前臺行程組中的每個行程,默認情況下,結果是終止前臺作業,類似地,輸入Ctrl+Z會發送一個SIGTSTP信號到前臺行程中的每個行程,默認情況下,結果是停止(掛起)前臺行程,
思考總結
上面所說的所有信號產生,最終都要有OS來進行執行的,為什么?
OS是行程的管理者,
信號的處理是不是立即處理的?是合適的時候被處理,
信號如果不是被立即處理,那么信號是否需要暫時被行程記錄下來?記錄在哪里最合適呢?是要被記錄下來的,被記錄在行程的資料結構(task_struct)中的,
一個行程在沒有收到信號的時候,能否知道自己應對合法信號作何處理?是知道的,如果沒有自定義捕捉信號那么有自己默認的處理方式,
信號的操作
信號其他相關常見概念
實際執行信號的處理動作稱為
信號遞達,
信號從產生到遞達之間的狀態稱為信號未決,
行程可以選擇阻塞某個信號,
被阻塞的信號產生時將保持在未決狀態,直到行程解除對此信號的阻塞,才執行遞達的動作,
注意:阻塞和忽略時不同的,信號阻塞狀態是信號未決,而信號忽略狀態是信號遞達,
在內核表示大意圖
信號的行程中的保存示意圖:

block、pending位圖的意義:

1、每個信號都有兩個標志位分別表示阻塞(Block)和未決(pending),還有一個函式指標表示處理動作,其中標志位我們用位圖表示34號以下的信號狀態,
其中位元位的位置:是哪個信號,位元位的內容:是否產生信號,信號產生時,內核在行程控制塊中設定該信號的未決狀態,直到該信號遞達才清除該標志位,
2、例如:對于上圖:SIGHUP信號未阻塞也未產生過,當它遞達時執行默認處理動作,SIGINT信號產生過,但正在被阻塞,所以暫時不能被遞達,雖然它的處理動作是忽略的,但在沒有接觸阻塞之前不能忽略這個信號,行程有機會在改變處理動作之后再接觸阻塞,SIGQUIT信號未產生過,一旦產生SIGQUIT信號將被阻塞,它的處理動作是用戶自定義sighandler函式,
3、如果在解除阻塞之前產生過多次相同的信號,Linux系統對于1-31號的信號只記錄一次,但是對于34-64號的實時信號Linux會將這個信號放在一個佇列中進行管理,
sigset_t
對于阻塞標志和未決標志,每個信號只用一個bit位標志,非0即1,不記錄該信號產生多少次,因此阻塞和未決用同一種型別sigset_t來存盤,
sigset_t稱為信號集,在阻塞信號集中“有效“和”無效“,表示信號是否被阻塞狀態,在未決信號集中”有效“和“無效”表示信號是否處于未決狀態,
信號集操作函式
我們可以通過以下函式對信號集sigset_t操作,對于系統怎么存盤這些bit則依賴于具體的系統,我們只需要使用以下函式操作信號集就行,
#include <signal.h>
int sigemptyset(sigset_t *set);
//sigemptyset初始化set所指向的信號集,使其中所有bit位清零
int sigfillset(sigset_t *set);
//sigfillset初始化set所指向的信號集,使其中所有bit位設為有效
int sigaddset (sigset_t *set, int signo);
//sigaddset將信號集中的某個信號bit位設為有效
int sigdelset(sigset_t *set, int signo);
//sigdelset將信號集中的某個信號bit位設為無效
//對于上面4個函式的回傳值:成功回傳0,出錯則為-1
int sigismember(const sigset_t *set, int signum);
//sigismember是一個布爾函式,用于判斷一個信號集的有效信號中是否包含某種信號,若包含則回傳1,不包含則回傳0,出錯回傳-1
sigprocmask函式
呼叫sigprocmask函式可以讀取或更改行程的阻塞信號集(Block位圖表),
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//引數:
how和set信號阻塞集一起只是如何更改行程的阻塞信號集
oset若是非空指標,讀取當前行程的阻塞信號集通過oset傳出,輸出型引數,
set若是非空指標,更改行程的阻塞信號集
//回傳值:若成功則為0,若出錯則為-1
how引數值可選:
| 可選引數 | 含義 |
|---|---|
| SIG_BLOCK | set包含了我們希望添加到當前阻塞信號集的信號,相當于mask=mask|set |
| SIG_UNBLOCK | set包含了我們希望從當前阻塞信號集中解除的信號,相當于mask=mask&~set |
| SIG_SETMASK | 設定當前阻塞信號集為set所指向的值,相當于mask=set |
sigpending函式
//功能:讀取當前行程的未決信號集,輸出型引數
#include <signal.h>
int sigpending(sigset_t *set);
//回傳值:成功回傳0,出錯失敗回傳-1
通過上面函式寫一個小程式
我們一直讀取當前行程的pending信號表,首先在程式的最初阻塞2號信號,然后通過按Ctrl+C向當前行程發送2號行程,這個程序持續15秒,然后解除阻塞,
實驗代碼:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void show_pending(sigset_t *pending){//顯示當前的pending信號集
int i;
for(i=1;i<=31;++i){
if(sigismember(pending,i)){
printf("1");
}else{
printf("0");
}
}
printf("\n");
}
int main(){
sigset_t pending,block,oblock;
sigemptyset(&pending);
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block,2);//阻塞2號信號集
sigprocmask(SIG_SETMASK,&block,&oblock);//將行程的阻塞信號集設為block
int count=0;
while(1){
sigemptyset(&pending);
sigpending(&pending);
show_pending(&pending);//顯示當前行程pending位圖
sleep(1);
count++;
if(count==15){
printf("recover sig mask\n");
sigprocmask(SIG_SETMASK,&oblock,NULL);//當恢復以前的阻塞信號集時,解除阻塞,然后信號遞達,信號終止
}
}
return 0;
}
實驗截圖:

信號的捕捉
信號捕捉大致程序

內核如何實作信號捕捉
信號捕捉:如果信號的處理動作是用戶自定義函式,信號遞達時就呼叫這個函式,
其中內核實作捕捉信號就是上圖中的大致程序以及狀態轉換,為了更好的理解舉例說明:用戶注冊了SIGQUIT信號的處理函式sighandler,當前正在執行main函式,這時發生中斷或例外呼叫切換到內核,在中斷處理完畢后要回傳用戶態的main函式之前檢查到有信號SIGQUIT遞達,內核決定回傳用戶態執行處理函式sighandler而不是main函式執行,由于main函式和sighandler函式使用不同的堆疊空間,是兩個獨立的執行流,sighandler函式回傳后自動執行特殊的系統呼叫sigreturn再次進入內核態,如果沒有新的信號要遞達,這次再回傳用戶態就是恢復main函式的背景關系繼續執行了,
問題
為什么要回傳用戶態執行用戶自定義處理函式為什么不在內核態處理?
我們知道內核態的權限更大,一定是可以執行用戶態的自定義函式,但是為什么要回傳用戶態執行用戶態的函式?因為內核態的權限更大,在內核態下運行自定義函式,如果這個自定義函式不是一個正常的函式是一個非法的動作,那么這時在內核態下執行這個函式,很有可能破壞作業系統,其他行程和作業系統還不能終止這個行程,
volatile關鍵字
volatile關鍵字的作用:
保持記憶體的可見性,告訴編譯器,被該關鍵字修飾的變數,不允許被優化,對該變數的任何操作,都必須在真實的記憶體中進行操作,
站在信號的角度理解volatile關鍵字
實驗代碼:
//mybin.c
#include<stdio.h>
#include<signal.h>
int quit=0;
void handler(int son){
quit=1;
printf("change quit 0 to 1..!\n");
}
int main(){
signal(2,handler);
while(!quit);
printf("end of process....!!\n");
return 0;
}
//Makefile:
mybin:mybin.c
gcc mybin.c -o mybin
.PHONY:clean
clean:
rm -f mybin
運行截圖:

標準情況下,鍵入 CTRL+C ,2號信號被捕捉,執行自定義動作,修改 quit=1 , while 條件不滿足,退出回圈,行程退出,
當編譯的時候對程式進行優化:
#Makefile
mybin:mybin.c
gcc mybin.c -o mybin -O2
.PHONY:clean
clean:
rm -f mybin
運行截圖:

優化的情況下,鍵入Ctrl+C,2號信號被捕捉,執行自定義動作,修改quit=1,但是while條件依舊滿足,繼續執行行程運行!編譯器將程式做了一定的優化,當發現quit在main函式中不會對quit做任何修改的動作,所以
將quit優化到了CPU暫存器中,但是修改quit=1,實際是修改的記憶體中的quit,并沒有修改暫存器中的quit,所以while條件依舊滿足程式不會退出,所以我們可以用volatile關鍵字避免這個問題,
添加volatile關鍵字:
//mybin.c
#include<stdio.h>
#include<signal.h>
volatile int quit=0;
void handler(int son){
quit=1;
printf("change quit 0 to 1..!\n");
}
int main(){
signal(2,handler);
while(!quit);
printf("end of process....!!\n");
return 0;
}
//Makefile:
mybin:mybin.c
gcc mybin.c -o mybin -O2
.PHONY:clean
clean:
rm -f mybin
運行截圖:

SIGCHLD關鍵字
子行程在終止時會給父行程發送SIGCHLD信號,該信號的默認處理動作是忽略的,父行程可以自定義SIGCHLD信號處理函式,這樣父行程只需要專心處理自己的作業,不必關心子行程了,子行程終止時會通知父行程,父行程在信號處理函式中呼叫wait清理子行程即可,
代碼驗證:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
void handler(int sno){
printf("catch a signum : %d ,i pid is : %d\n",sno,getpid());
}
int main(){
signal(SIGCHLD,handler);
if(fork()==0){
printf("I am child,I am running.I pid is %d, I ppid is %d\n",getgid(),getppid());
sleep(5);
printf("I will exit...\n");
exit(1);
}
while(1);
return 0;
}
運行結果:

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275877.html
標籤:其他
上一篇:7道騰訊演算法編程真題,你能做對幾道?包含一道趣味題
下一篇:瑾小的春招總結
