目錄
1.信號的概念
2.信號的產生
3.信號的保存
4.信號的捕捉
信號的其它內容:
SIGCHLD信號
1.信號的概念
在Linux中,信號是一種用于進程之間通信的基本機制。它是一種異步事件通知,用于通知進程發生了某些事件。如下是一些常見的Linux信號類型:
SIGINT (2):中斷進程,通常由終端產生,例如用戶按下Ctrl+C。
SIGKILL (9):立即終止進程,無法被捕獲或忽略。
SIGTERM (15):請求終止進程,可以被捕獲或忽略。
SIGQUIT (3):請求進程退出并生成核心轉儲文件,可以被捕獲或忽略。
SIGSTOP (17):暫停進程的執行,無法被捕獲或忽略。
SIGCONT (19):恢復進程的執行,無法被捕獲或忽略
?這些信號在進程控制、異常處理和進程間通信中扮演著重要角色。請注意,信號只是通知進程發生了什么事件,并不傳遞任何數據。進程對不同信號有不同的處理方式,可以指定處理函數、忽略或保留系統的默認值。信號機制在Linux編程中非常重要,幫助實現進程之間的協作和控制。
2.信號的產生
先舉兩個樣例:
eg1:
首先我們編寫一個死循環代碼,編譯運行后,我們的命令行就不再有用了,現在是前臺程序,只運行當前的程序,當我們編譯時加上&,使他成為后臺程序,此時的命令行也可以繼續使用,
程序在運行的時候,前臺程序只能有一個,后臺程序可以有多個。后臺程序在運行時,我們的鍵盤可以輸入數據,指令可以運行。
一般操作系統會自動根據情況把shell程序提到前臺或者后臺。下面的指令對shell無效。
前后臺程序切換
./可執行 &? 把程序放到后臺
jobs? 查看后臺任務
fg number(任務編號) 把任務放到前臺
ctrl+z 再加 bg number? ?把后臺任務轉到前臺
ctrl+\ 默認終止
ctrl + z 暫停程序,先放到后臺
而這就是信號的產生,除此之外操作系統知曉鍵盤的輸入也是一種信號:
eg2:當鍵盤的某個按鈕被按下的時候,就會產生高電平信號間接給cpu,cpu得知了之后某個按鈕的高電平,發生中斷,就產生對應的數據。
而信號的產生就是用軟件來模擬中斷行為。我們的指令都是發出信號,
例如接口signal
可以發出我們需要的信號。
如下一段代碼:
#include<iostream>
#include<unistd.h>
#include <signal.h>
#include <stdlib.h>void handler(int signo)
{std::cout<<"獲得一個"<<signo<<"信號"<<std::endl;exit(1);
}int main()
{signal(2,handler);while(true){std::cout<<"pid:"<<getpid()<<",i am running......"<<std::endl;sleep(1);}return 0;
}
?再運行的時候,我們ctrl+z,此時退出進程就會獲得一個為2的信號。
因此信號的產生可以通過鍵盤發出,對于我們的linux也是有許多信號的(kill -l):
其中,沒有0號信號,從1-31的信號我們把它叫做普通信號,沒有32,33信號,從34到64的信號,我們把它叫做實時信號。這些信號的本質就是一些函數指針數組,對應的下標就與他們的編號有關。
對于普通信號,進程是否收到了普通信號,操作系統(pcb中)會用一張位圖來表示,利用位圖中的第幾個比特位表示編號,0表示沒收到,1表示收到。
無論信號有多少種,都是只能讓os來寫(寫)信號,因為os是進程的管理者。
了解到了信號的接收,因此我們在編寫程序時就可以直接發送信號,之后自動運行對應handler方法,例如之前我們使用kill -9殺進程,現在我們發送一個為9的信號,此時自定義它的處理方法,例如只是打印一句話,那么我們kill -9的指令就不會再殺掉我們的進程,而是打印一句話。
但實際上并不可以,操作系統對于某些信號是不可以被自定義捕捉的。
除此以外,Linux提供了三種接口供我們產生信號。
方式一:通過鍵盤組合鍵發送產生信號。
方式二:通過函數接口
接口 raise 可以自己給自己發送任意信號
接口 abort? 收到信號后終止運行
方式三,通過異常:
?以我們熟知的除零錯誤為例,首先除零錯誤并不是語言錯誤,而是進程錯誤,再cpu中通過各個寄存器來計算除零,此時cpu中還有表示狀態的寄存器,當發生除零問題后,狀態寄存器就會產生溢出標記位,從而轉化為信號,就是信號8 SIGFPE? 也就是flaot point exception。
當然發出信號也不僅僅可能是因為異常而導致的,也有可能是鬧鐘響了:
方式四:由軟件條件產生信號:
alarm接口可以設置鬧鐘
#include<iostream>
#include<unistd.h>
#include <signal.h>
#include <stdlib.h>
int cnt=0;
void handler(int signo)
{std::cout<<"獲得一個"<<signo<<"信號"<<"alarm is:"<<cnt<<std::endl;exit(1);
}int main()
{std::cout<<"pid:"<<getpid()<<std::endl;//本質上就是修改函數指針數組的位置signal(14,handler);//設置1s鬧鐘,到點了終止進程alarm(1);while(true){//cout<<cnt++<<endl; 可以看出外設是很慢的cnt++;}}
?操作系統的時間:
當我們電腦關機了,程序結束了,再次重新啟動,我們會發現,時間永遠是跟著走的,實際上,即使關機了,在電腦里也會有一個紐扣電池一直給硬件供電,固定時間間隔計數,再將計數器轉換為時間戳給我們的電腦。CMOS周期性的高頻的發送時間中斷。
3.信號的保存
. 信號其他相關常見概念實際執行信號的處理動作稱為信號遞達 (Delivery)信號從產生到遞達之間的狀態 , 稱為信號未決 (Pending) 。進程可以選擇阻塞 (Block ) 某個信號。被阻塞的信號產生時將保持在未決狀態 , 直到進程解除對此信號的阻塞 , 才執行遞達的動作 .注意 , 阻塞和忽略是不同的 , 只要信號被阻塞就不會遞達 , 而忽略是在遞達之后可選的一種處理動作
?遞達就是開始處理信號,當信號被記錄再為途中時就是信號未決狀態,阻塞:被阻塞的信號一直處在未決狀態,只有當阻塞取消時,才進入遞達狀態。
阻塞與忽略是有區別的,忽略本身沒有阻塞而是遞達,處理了信號,效果為忽略,而阻塞是沒有抵達,且沒處理。
了解了以上概念,因此再管理信號的狀態時,os就需要維護這三張位圖表,用來表示阻塞,未決,遞達這三個狀態的信號。
比特位的位置:代表信號的編號
比特位的內容:對特定信號進行阻塞還是屏蔽。?
每個信號都有兩個標志位分別表示block(阻塞)和pending(未決),其次還有一個函數指針表示要處理的方法。
void handler(int signo)
{cout<<"signo is "<<signo<<endl;exit(1);
}
int main()
{//發送2信號signal(2,signo);//把信號的粗粒設置為原來默認的signal(2,SIG_DFL);//當然還可以把信號忽略signal(2,SIG_IGN);std::cout<<"my pid id:"<<getpid()<<endl;while(true){cout<<"i am running....."<<endl;sleep(1);}}
由于有這么多信號集,操作系統還提供了許多信號及操作接口:
#include <signal.h>int sigemptyset(sigset_t *set);? //對指定的位圖進行清零int sigfillset(sigset_t *set);? ?//對指定的位圖進行置1int sigaddset (sigset_t *set, int signo); //對指定信號添加到指定的位圖中int sigdelset(sigset_t *set, int signo);int sigismember ( const sigset_t *set, int signo); //判定一個信號是否在為位圖中
對于block表的修改:
sigprocmask?調用函數sigprocmask可以讀取或更改進程的信號屏蔽字(阻塞信號集)。
如下代碼:
int main()
{//例如對2號信號屏蔽cout<<"my pid is"<<getpid()<<endl;//先定義兩個信號集位圖sigset_t block,oblock;//先對信號集清空sigemptyset(&block);sigemptyset(&oblock);//其次對2號信號添加到信號集sigaddset(&block,2); //當前并沒有讓操作系統2信號屏蔽,只是語言層面的定義sigaddset(&oblock,2);sigprocmask(SIG_BLOCK,&block,&oblock); //真正讓操作系統屏蔽、更改信號while(true){sleep(1);}return 0;
}
?此時我們再發2號信號就沒有作用了,ctrl+c也無法中斷程序。
既然如此,那么我們是否可以將一個程序的所有信號屏蔽,這樣他就有金剛不壞之身,誰也干不掉他,實際上并是不是所有的信號你都能屏蔽,就跟不是所有的信號的處理可以自定義是一樣的。
比如說9號信號就無法被屏蔽。
那么pending表的修改:接口 sigpending
重要的是獲取pending表.
接下來我們用一個整體的實例來認識這些接口:
void printpending(const sigset_t &pending)
{for(int signo=31;signo>0;signo--){if(sigismember(&pending,signo)){cout<<"1";}else{cout<<"0";}}cout<<"\n";
}
//自定義捕捉
void handler(int signo)
{cout<<"已接受到信號"<<signo<<endl;//exit(1);}int main()
{//例如對2號信號屏蔽cout<<"my pid is"<<getpid()<<endl;signal(2,handler);//先定義兩個信號集位圖sigset_t block,oblock;//先對信號集清空sigemptyset(&block);sigemptyset(&oblock);//其次對2號信號添加到信號集sigaddset(&block,2); //當前并沒有讓操作系統2信號屏蔽,只是語言層面的定義sigaddset(&oblock,2);sigprocmask(SIG_BLOCK,&block,&oblock); //真正讓操作系統屏蔽、更改信號//下打印pending表int cnt=0;sigset_t pending;while(true){sigpending(&pending);printpending(pending);sleep(1);cnt++;if(cnt==5){//直到5S,解除2信號的屏蔽cout<<"解除對2號信號的屏蔽,2號準備抵達"<<endl;sigprocmask(SIG_SETMASK,&oblock,nullptr); //設置為舊的信號 }}return 0;
}
?運行結果如圖:
4.信號的捕捉
信號在什么時候去被捕捉處理呢,在合適的時候---從內核態返回到用戶態的時候,進行信號的檢測和信號的處理。
內核態:內核態是操作系統的一種狀態,能夠大量訪問資源
用戶態:用戶態是一種受控的轉臺,能夠訪問的資源是有限
用戶想要訪問操作系統只能通過系統調用的方式訪問。
首先無論進程如何調度,cpu都會找到os,我們的進程的所有代碼的執行,都可以在地址空間中通過跳轉的方式進行調用和返回。
?那么對于系統的信號的捕捉,首先介紹第一個接口sigaction
第三個參數表示把舊的handler表返回給我,達爾戈參數就是新的handler的設置,第一個參數為信號編號,接口的作用是檢測和修改信號動作。
返回類型是sigaction的結構體類型,其中有五個字段。其中我們比較重點關注的是sa_mask字段,
如果在調用信號處理函數時,除了當前信號被屏蔽外,還希望屏蔽些別的信號,此時sa_mask就是需要被額外屏蔽的信號。
以該代碼為例:
#include<signal.h>
#include<unistd.h>
#include<iostream>
using namespace std;
void print(sigset_t &pending);
void handler(int signo)
{cout<<"接收到信號"<<signo<<"......"<<endl;while(true){//獲取當前pending列表sigset_t pending;sigpending(&pending);print(pending);sleep(1);}
}
void print(sigset_t &pending)
{for(int signo=31;signo>0;signo--){if(sigismember(&pending,signo)){cout<<"1";}else{cout<<"0";}}cout<<endl;
}
int main()
{cout<<"my pid is "<<getpid()<<endl;//定義新的與舊的actstruct sigaction act,oact;//設置handler為當前自定義的處理方法act.sa_handler=handler;sigaction(2,&act,&oact);while(true) sleep(1);return 0;
}
用改接口接受2號信號時,和之前一樣,運行程序,第一次我們ctrl+c,發出2信號時接收到2好信號,但自此之后的2好信號都被屏蔽掉了,再次crtl+c時,信號無法被接受處于未決狀態。
例如:當我們要修改信號2時,這里默認會自動屏蔽信號2,如下圖
。
信號的其它內容:
可重入函數
int flag = 0;
void handler(int sig)
{printf("chage flag 0 to 1\n");flag = 1;
}
int main()
{signal(2, handler);while(!flag);printf("process quit normal\n");return 0;
}
SIGCHLD信號
我們?早已經了解到子進程在退出的時候,是要給父進程發送退出信息的,不然父進程還要維護一份沒必要的資源,而子進程是給父進程發送什么樣的信號呢?---SIGCHLD
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{pid_t id;//收到退出信號 等待子進程while( (id = waitpid(-1, NULL, WNOHANG)) > 0){printf("wait child success: %d\n", id);}printf("child is quit! %d\n", getpid());
}
int main()
{signal(SIGCHLD, handler);pid_t cid;if((cid = fork()) == 0){//childprintf("child : %d\n", getpid());sleep(3);exit(1);}while(1){printf("father proc is running\n");sleep(1);}return 0;
}
可以看到子進程退出時,時回給父進程發信號的。
在Linux中支持手動忽略信號SIGCHDL,可以不用wait子進程。退出自動回收。