一、信號入門
什么是信號:信號就是一條消息,它用來通知進程系統(tǒng)中發(fā)生了一個某種類型的事件。
信號是多種多樣的,并且一個信號對應一個事件,這樣才能知道收到一個信號后,到底是一個什么事件,應該如何處理這個信號。
1、信號的一些特性
- 進程在沒有收到信號時就已經知道了一個信號應該怎么被處理了,這說明進程能夠識別并處理信號。
- 信號對于進程來說是隨時都有可能產生的,因此進程與信號是異步的!
- 由于進程與信號是異步的,當信號產生時,進程可能正在執(zhí)行優(yōu)先級更高的事情,這時進程并不能立即處理信號,需要在合適的時候再進行處理,因此在這個空窗期內信號要能夠被保存起來,這說明進程具有記錄信號的能力!
- 進程記錄的信號可能有很多個,因此進程需要用一種數據結構去管理所有的信號,在Linux下對于信號的管理采用的是位圖結構,比特位的位置代表信號的編號。
- 所以所謂的發(fā)送信號本質就是:直接修改特定進程的信號位圖中的特定的比特位。(由
0
->1
) - 進程信號的位圖結構本質還是屬于
task_struct
里面的數據,因此對于進程信號的位圖結構里面的數據的修改,只能有操作系統(tǒng)來完成,即無論有多少種信號產生的方式,最終都必須讓OS來完成最后的發(fā)送過程!
2、信號的處理方式
- 執(zhí)行默認動作(即操作系統(tǒng)給信號設定的默認動作)
- 忽略信號
- 執(zhí)行自定義動作(用戶修改了操作系統(tǒng)設定的默認動作,改成了自己想要的動作),操作系統(tǒng)為我們提供一個信號處理函數
signal
,可以要求內核在處理該信號時切換到用戶態(tài)執(zhí)行這個處理函數,這種方式稱為捕捉(Catch) 一個信號。
信號捕捉初識
信號捕捉主要是使用signal
函數,該函數內部使用了回調函數。
該函數的作用就是將指定的信號的默認行為更改為執(zhí)行第二個參數對應的函數,這個函數要求必須是返回值為void
參數是int
的函數。
-
參數:
- 信號的編號。
- 回調函數的函數指針。
-
返回值: 返回先前的信號處理函數指針,如果有錯誤則返回
SIG_ERR(-1)
。
實例代碼:
我們在鍵盤下按的 Ctrl + C 其實就是2號信號,下面我們嘗試對2
號信號進行捕捉。
#include <iostream>
#include <signal.h>
#include <unistd.h>
void hander(int sig)
{
std::cout << "get a signal " << sig << std::endl;
}
int main()
{
signal(2, hander);
while (true)
{
std::cout << "我正在運行...,我的PID是: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
運行結果:
可以看到我們使用 Ctrl + C 已將無法終止進程了,變成了我們自定義的動作了!
3、Linux下的信號
在Linux下我們可以使用kill -l
命令列出所有的信號。
仔細觀察我們發(fā)現,這里面是沒有32 ,33
號信號的!其中從1~31
號信號是普通信號,34~64
是實時信號。(這里我們主要討論普通信號)
- 每個信號都有一個編號和一個宏定義名稱,這些宏定義可以在
/usr/include/bits/signum.h
中找到。 - 對于普通信號默認的處理動作是什么,在
man 7 signal
中都有詳細說明。
二、信號的產生
在Linux下進程信號的產生是有多種方式的,下面我們就來一起了解一下吧!
1、通過終端按鍵產生信號
在Linux下輸入命令可以在Shell下啟動一個前臺進程,當我們想要終止一個前臺進程時,我們可以按下 Ctrl + C 來進行終止這個前臺進程,其實這個 Ctrl + C 也是一個信號,它對應的信號的2
號信號SIGINT
,這個信號對應的默認處理動作就是終止當前的前臺進程。
-
用戶按下 Ctrl-C ,這個鍵盤輸入產生一個硬件中斷 ,被OS獲取,解釋成信號,發(fā)送給目標前臺進程,前臺進程因為收到信號,進而引起進程退出
-
Ctrl-C 產生的信號只能發(fā)給前臺進程。一個命令后面加個
&
可以放到后臺運行,這樣Shell不必等待進程結束就可以接受新的命令,啟動新的進程,同樣這樣的后臺進程也無法使用Ctrl-C 來進行殺死。 -
Shell可以同時運行一個前臺進程和任意多個后臺進程,只有前臺進程才能接到像 Ctrl-C 這種控制鍵產生的信號。
關于硬件中斷:
- 硬件中斷是由硬件設備觸發(fā)的中斷,當硬件設備有數據或事件需要處理時,會向CPU發(fā)送一個中斷請求,CPU在收到中斷請求后,會立即暫停當前正在執(zhí)行的任務,進入中斷處理程序中處理中斷請求。
關于軟中斷
- 信號是進程之間事件異步通知的一種方式,屬于軟中斷。
2、調用系統(tǒng)函數向進程發(fā)信號
a、kill函數
kill
函數是操作系統(tǒng)給我們提供的一個系統(tǒng)調用,通過它我們能夠給指定的進程發(fā)送指定的信號。
-
參數:
- 目標進程的
pid
。 - 要發(fā)送的信號
signal
。
- 目標進程的
-
返回值:調用成功就返回
0
,調用失敗就返回-1
。
kill
命令其是就是調用kill
函數實現的,下面我們也來模擬實現一下kill
命令。
實例代碼:
#include <iostream>
#include <string>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include <signal.h>
#include <sys/types.h>
void Usage(const std::string proc)
{
std::cout << "Usage:" << std::endl;
std::cout << " " << proc << " 信號編號 目標進程" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(-1);
}
pid_t pid = atoi(argv[2]);
int signo = atoi(argv[1]);
int return_val = kill(pid, signo);
if (return_val == -1)
{
std::cout << "錯誤碼:" << errno << " 錯誤信息:" << strerror(errno) << std::endl;
}
return 0;
}
運行結果:
b、raise函數
此函數會向當前進程發(fā)送指定的信號
-
參數: 要發(fā)送的信號
sig
。 -
返回值:調用成功就返回
0
,調用失敗就返回非0
。
實例代碼:
我們用raise
函數給當前進程發(fā)送暫停信號19
SIGSTOP
,暫停以后我們可以在命令行中給進程發(fā)送繼續(xù)運行18
號SIGCONT
信號
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
sleep(1);
std::cout << "我要被暫停了,我的PID是:" << getpid() << std::endl;
raise(19);
std::cout << "我要繼續(xù)運行了,我的PID是:" << getpid() << std::endl;
return 0;
}
c、abort函數
abort
函數使當前進程接收到信號而異常終止,abort
函數其實是向進程發(fā)送6
號信號SIGABRT
,就像exit
函數一樣,abort
函數總是會成功的,所以沒有返回值,值得注意的是就算6
號信號被捕捉了,調用abort
函數還是會退出進程。
實例代碼:
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
int main()
{
std::cout << "begin" << std::endl;
abort();
std::cout << "end" << std::endl;
return 0;
}
這三個函數只有kill
是系統(tǒng)調用,另外兩個都是C庫函數,它們的功能對比如下:
3. 由軟件條件產生信號
SIGPIPE
是一種由軟件條件產生的信號,在“管道”中已經介紹過了。這里主要介紹alarm
函數和SIGALRM
信號。
調用alarm
函數可以設定一個鬧鐘,也就是告訴內核在seconds
秒之后給當前進程發(fā)14
號信號SIGALRM
信號, 該信號的默認處理動作是終止當前進程。
- 參數:鬧鐘的秒數。
- 返回值:這個函數的返回值有一點特殊,它是是上一次設定的鬧鐘時間還余下的秒數或者是0(0代表上一次的鬧鐘沒有收到干擾,正確的執(zhí)行完了)
實例代碼:
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
alarm(1);
int count = 0;
while (true)
{
std::cout << count++ << std::endl;
}
return 0;
}
4、硬件異常產生信號
硬件異常產生信號是指硬件產生了錯誤并以某種方式被硬件檢測到并通知內核,然后內核向當前進程發(fā)送適當的信號。
例如當前進程執(zhí)行了除以0
的指令,CPU的運算單元會產生異常,內核將這個異常解釋為SIGFPE
信號發(fā)送給進程。再比如當前進程訪問了非法內存地址,MMU會產生異常,內核將這個異常解釋為SIGSEGV
信號發(fā)送給進程。
例如下面的代碼我們進行除0
操作:
#include <iostream>
int main()
{
int a = 10;
a /= 0;
std::cout << a << std::endl;
return 0;
}
在編譯的時候我們收到了一個警告(除0
問題),然后我們不管接著運行我們的代碼,然后我們的程序就崩潰了,系統(tǒng)提示是浮點異常問題,其實這個浮點異常問題對應的就是我們的硬件異常,它對應的信號是8
號信號SIGFPE
大致原理:在計算機內部是有一個狀態(tài)寄存器的,該寄存器內部是一個位圖結構,如果對應的比特位為1
就表示本次計算有數據溢出的情況,說明本次計算結果不正確,CPU執(zhí)行有誤,而操作系統(tǒng)每次調度進程時都會去檢查狀態(tài)寄存器的狀態(tài),確保進程的執(zhí)行的正確性。
當讓CPU執(zhí)行除0
操作就會引發(fā)數據溢出的問題,然后狀態(tài)寄存器里面對應的比特位被置為1
,我們操作系統(tǒng)檢測到了狀態(tài)寄存器中有比特位被置為1
,就會向對應的進程發(fā)送SIGFPE
信號終止掉該進程,于是除0
就會導致程序崩潰。
下面我們可以用信號捕捉去驗證我們上面的原理和結論。
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
std::cout << "我是收到 " << sig <<"信號才崩潰了"<< std::endl;
}
int main()
{
signal(SIGFPE, handler);
int a = 10;
a /= 0;
std::cout << a << std::endl;
return 0;
}
運行結果:
可以看到我們的程序出現了死循環(huán)的打印,這是因為我們捕捉了8
號信號,將原來的默認動作終止進程修改成了打印動作,當我們的進程處理完信號時,操作系統(tǒng)再次調用該進程時,由于上一次的狀態(tài)寄存器里面的比特位沒有被置0
,所以操作系統(tǒng)再次調用該進程時,看到的狀態(tài)寄存器的對應比特位為還是1
,于是又向該進程發(fā)送8
號信號,而我們的自定義動作始終沒有去處理狀態(tài)寄存器,于是就陷入了死循環(huán)當中。
所以我們一般都是捕捉完該信號以后讓該進程直接退出。
下面我們來看野指針引起的硬件異常:
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
int* p = nullptr;
*p = 10;
std::cout << "野指針問題" << std::endl;
return 0;
}
運行結果:
系統(tǒng)提示我們發(fā)生了段錯誤,對于野指針問題,其實也是我們進程收到了操作系統(tǒng)發(fā)送的信號而崩潰的,這個信號是11
號信號SIGSEGV
,而這一次硬件異常的是MMU單元(內存管理單元)。
大致原理:由于我們進程使用的地址都是虛擬地址,當我們進程的代碼實際被執(zhí)行時,需要進行虛擬地址到物理地址的轉換,而這個轉換就要借助MMU這個硬件來進行轉換,當我們的MMU在進行地址轉換時,MMU單元在頁表中尋找地址的映射關系并比較讀寫權限是否一致,如果在頁表中找不到映射或者找到了映射但是進行的操作與讀寫權限不一致,就會導致轉換失敗,進而告知操作系統(tǒng),操作系統(tǒng)識別以后就會向對應的進程發(fā)送SIGSEGV
信號,從而終止掉該進程。(注意這里對于這個轉換異常,操作系統(tǒng)并沒有修復,如果用戶捕捉了這個信號,也不修復也不退出,也會導致操作系統(tǒng)一直給該進程發(fā)送此信號)
對于0
地址可能操作系統(tǒng)根本沒有給0
地址建立映射關系,或者建立了映射關系但是操作系統(tǒng)不會允許0
地址處發(fā)生寫入!而當我們進行*p = 10
時,是需要進行寫入的,MMU在地址轉換時發(fā)現權限不一致,進而引發(fā)給異常,報告給了操作系統(tǒng),然后操作系統(tǒng)向我們的的進場發(fā)送SIGSEGV
信號。
結語
本章講述的是進程信號的產生,但是只知道這些還是不夠的,下一章我們繼續(xù)深入理解進程信號的保存,提升我們對于信號的理解。文章來源:http://www.zghlxwxcb.cn/news/detail-541476.html
當然如果本篇文章有錯誤或不足的地方,歡迎評論或私信討論!那么我們下期見,byebye!文章來源地址http://www.zghlxwxcb.cn/news/detail-541476.html
到了這里,關于【Linux】進程信號之信號的產生的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!