「前言」文章是關(guān)于Linux進程信號方面的知識,本文的內(nèi)容是Linux進程信號第一講,講解會比較細,下面開始!
「歸屬專欄」Linux系統(tǒng)編程
「主頁鏈接」個人主頁
「筆者」楓葉先生(fy)
?「楓葉先生有點文青病」「每篇一句」?
人生天地間,忽如遠行客。
——《樂府·青青陵上柏》
目錄
一、認識信號
1.1 生活中的信號
1.2 將1.1的概念遷移到進程
1.3?信號概念
1.4 查看系統(tǒng)定義信號列表
1.5?man 7 signal
1.6 解釋1.2的代碼樣例
1.7 信號處理常見方式概覽
二、產(chǎn)生信號
2.1 signal函數(shù)
2.2?通過終端按鍵產(chǎn)生信號
2.3?調(diào)用系統(tǒng)函數(shù)向進程發(fā)信號
2.3.1 kill函數(shù)
2.3.2?raise函數(shù)
2.3.3?abort函數(shù)
2.4 硬件異常產(chǎn)生信號
2.4.1 除0操作產(chǎn)生的異常?
2.4.2 空指針異常
2.5?由軟件條件產(chǎn)生信號
一、認識信號
1.1 生活中的信號
日常生活中,常見的信號有:發(fā)令槍、紅綠燈、消息提醒、電話鈴聲、鬧鐘等等,以快遞為例。
- 你在網(wǎng)上買了很多件商品,再等待不同商品快遞的到來。但即便快遞沒有到來,你也知道快遞來臨時,你該怎么處理快遞。也就是你能“識別快遞”;
- 識別快遞包含了兩個信息:1、認識:你是認識 “快遞” 的? 2、行為產(chǎn)生:“快遞” 到來,你會產(chǎn)生相應(yīng)的行為;
- 當 “快遞” 到來,快遞小哥給你打電話,但是你正在打游戲,需5min之后才能去 “取快遞”。那么在在這5min之內(nèi),你并沒有去取快遞,但是你是知道有快遞到來了。也就是 ”取快遞” 的行為并不是一定要立即執(zhí)行,可以理解成 “在合適的時候去取”;
- 在收到通知,再到你拿到快遞期間,是有一個時間窗口的,在這段時間,你并沒有拿到快遞,但是你知道有一個快遞已經(jīng)來了。本質(zhì)上是你 “記住了有一個快遞要去取”;
- 當你時間合適,順利拿到快遞之后,就要開始處理快遞了。而處理快遞一般方式有三種:1. 執(zhí)行默認動作(打開快遞,使用商品)2. 執(zhí)行自定義動作(快遞是零食,你要送給你的女朋友)3. 忽略快遞(快遞拿上來之后,放在一旁,繼續(xù)開一把游戲);
- 快遞到來的整個過程,對你來講是異步的,你不能準確斷定快遞員什么時候給你打電話。
快遞便可視為信號,對信號的處理大致分以下三步:
- 識別信號:你認識這個信號,信號到來你會產(chǎn)生相應(yīng)的行為;
- 信號到來(即將處理信號):不一定立即處理信號,也可以在某個合適時間進行處理信號,但是必須要把這個信號記住;
- 拿到信號(處理信號):拿到信號后分三種行為 (1、默認動作:拿到信號后使用信號? 2、忽略動作:拿到信號后忽略信號,什么事也不干? 3、自定義動作:拿到信號去干別的事)
解釋異步概念:
以生活例子為例:你在煮著面條。當你在煮面條時,你可以同時做其他事情,例如準備醬料或者切菜。你不需要一直站在爐子旁邊等待面條煮熟,而是可以在面條煮熟之前做其他事情,這就是異步;
而同步則是:你必須等待面條煮熟了,你才能干其他事,這是同步。
以信號為例:假設(shè)你正在等待信號的到來。在同步中,你會等待信號到達后再繼續(xù)下一個任務(wù),這意味著你會阻塞其他任務(wù)直到信號到達。
另一方面,在異步中,你不會等待信號到達,這意味著在等待信號到來的時可以做其他任務(wù)。
1.2 將1.1的概念遷移到進程
首先知道,信號是給進程發(fā)送的,比如我們之前的 kill -9 ,給進程發(fā)送9號信號終止進程。
- 進程是如何識別信號:也是 認識信號 + 行為動作(進程之所以能夠認識信號,是因為程序員將對應(yīng)的信號種類和邏輯已經(jīng)寫好了);
- 進程收到信號時:進程不一定立即處理這個信號,進程可能干著其他更重要的事情;
- 進程立即處理 或 不一定立即處理這個信號的時候,進程本身必須要有對信號的保存能力;
- 進程處理信號的時候,一般有三個動作(默認、忽略、自定義),進程處理信號稱為信號被捕捉。
進程本身必須要有對信號的保存能力,信號保存在哪里?答案是保存在進程的PCB里面,即task_struct。?
信號如何保存?是否收到了指定的信號,是否即兩態(tài):二進制表示非0即1,即用1代表收到了信號,0則代表沒有收到信號
這種結(jié)構(gòu)稱為位圖結(jié)構(gòu),之前有過相關(guān)解釋,C++專欄也有
指定的信號在Linux是:
用 kill -l 命令可以察看系統(tǒng)定義的信號列表
kill -l
其中 [1, 31] 號信號是普通信號,[34, 64] 號信號是實時信號(信號學(xué)習(xí)中,這里我們只學(xué)習(xí)普通信號)?
即普通信號就可以用32個比特位來表示,即四字節(jié)的整型,即在 task_struct?里面一定存在一個字段 unsigned int:
struct task_struct
{
//進程屬性
//.......
unsigned int signal;
//.......
}
這個字段可以表示所有的普通信號:
第一個比特位代表 1號信號,以此類推(位圖結(jié)構(gòu))
如何理解信號的發(fā)送?也就是發(fā)送信號的本質(zhì)
發(fā)送信號的本質(zhì)就是:修改PCB中的信號位圖,即task_struct 的信號位圖,也就是上面圖中所說的位圖,比如發(fā)送1號信號,發(fā)送1號信號就是把信號位圖的第一個比特位由0置1
而?task_struct 是內(nèi)核維護的一種數(shù)據(jù)結(jié)構(gòu)對象,所以?task_struct 的管理者是OS,只有OS才有權(quán)利修改?task_struct 里面的內(nèi)容,所以以此推導(dǎo):無論在未來學(xué)習(xí)多少種發(fā)送信號的方式,本質(zhì)都是通過OS向目標進程發(fā)送信號(誰都沒有權(quán)利修改OS內(nèi)的數(shù)據(jù)結(jié)構(gòu),只有OS自己可以)
所以我們用戶要操作信號,OS必須提供發(fā)送信號、處理信號的相關(guān)系統(tǒng)調(diào)用
?比如之前一直使用的 kill 命令,底層一定調(diào)用了對應(yīng)的系統(tǒng)調(diào)用
以代碼展示信號:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "我是一個進程,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
該程序的運行結(jié)果就是死循環(huán)地進行打印,而對于死循環(huán)來說,常用方式就是使用 Ctrl+C 終止進程
為什么使用 Ctrl+C 后,該進程就終止了??
實際上當用戶按Ctrl+C時,這個鍵盤輸入會產(chǎn)生一個硬中斷,被操作系統(tǒng)獲取并解釋成信號(Ctrl+C被解釋成2號信號),然后操作系統(tǒng)將2號信號發(fā)送給目標前臺進程,當前臺進程收到2號信號后就會退出
2號信號是:SIGINT
這里只是簡單介紹,下面詳細解釋?
1.3?信號概念
信號是進程之間事件異步通知的一種方式,屬于軟中斷
1.4 查看系統(tǒng)定義信號列表
用 kill -l 命令可以察看系統(tǒng)定義的信號列表
kill -l
1.2?有過大概解釋,這不介紹了,我們可以使用信號數(shù)字的編號,也可以直接使用宏定義,比如2號信號,我們可以使用 2 也可以使用?SIGINT
每個信號都有一個編號和一個宏定義名稱,這些宏定義可以在 signum.h 中找到,例如其中有定 義 #define SIGINT 2
查看 signum.h
1.5?man 7 signal
普通信號各自在什么條件下產(chǎn)生,默認的處理動作是什么,在signal(7)中都有詳細說明,直接看文檔即可
man 7 signal 查看:
普通信號的默認動作:?
1.6 解釋1.2的代碼樣例
man 7 signal 查看二號信號的作用:
Interrupt from keyboard:從鍵盤獲取中斷
默認動作:Term
Term: Default action is to terminate the process.
翻譯:默認操作是終止進程
?所以,到這里我們就知道為什么 2號信號可以終止進程
我們可以使用signal函數(shù)對2號信號進行捕捉,證明當我們按 Ctrl+C 時進程確實是收到了2號信號。使用 signal 函數(shù)時,我們需要傳入兩個參數(shù),第一個是需要捕捉的信號編號,第二個是對捕捉信號的處理方法,該處理方法的參數(shù)是int,返回值是void
注:signal 函數(shù)這里是使用,詳細下面才解釋,這里只是演示
下面的代碼中將2號信號進行了捕捉,當該進程運行起來后,若該進程收到了2號信號就會打印出收到信號的信號編號
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "捕捉到一個信號,該信號編號是:" << signo << endl;
}
int main()
{
signal(2, handler);
while(true)
{
cout << "我是一個進程,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
此時當該進程收到2號信號后,就會執(zhí)行我們給出的handler方法,而不會像之前一樣直接退出了,因為此時我們已經(jīng)將2號信號的處理方式由默認改為了自定義了(默認行為:結(jié)束進程? -> 變成 自定義行為:handler方法)
進程是無法 Ctrl+C 結(jié)束進程,直接發(fā)送 9 號信號終止進程,kill -9 進程pid
注意:?
- Ctrl+C 產(chǎn)生的信號只能發(fā)給前臺進程。一個命令后面加個&可以放到后臺運行,這樣Shell不必等待進程結(jié)束就可以接受新的命令,啟動新的進程。
- Shell可以同時運行一個前臺進程和任意多個后臺進程,只有前臺進程才能接到像 Ctrl+C 這種控制鍵產(chǎn)生的信號。
- 前臺進程在運行過程中用戶隨時可能按下 Ctrl+C 而產(chǎn)生一個信號,也就是說該進程的用戶空間代碼執(zhí)行到任何地方都有可能收到 SIGINT 信號而終止,所以信號相對于進程的控制流程來說是異步(Asynchronous)的
1.7 信號處理常見方式概覽
信號處理動作有以下三種,也就是上面概念所提到的
- 默認:執(zhí)行該信號的默認處理動作;
- 忽略:忽略此信號;
- 自定義:提供一個信號處理函數(shù),要求內(nèi)核在處理該信號時切換到用戶態(tài)執(zhí)行這個處理函數(shù),這種方式稱為捕捉(Catch)一個信號
二、產(chǎn)生信號
前面第一大點都是信號預(yù)備知識,這里第二大點講的是信號產(chǎn)生
2.1 signal函數(shù)
對上面使用signal函數(shù)進行補充:signal函數(shù)的作用是用于處理信號(自定義行為),對信號進行捕捉?
man 2 signal 查看一下
signal
頭文件:
#include <signal.h>
函數(shù)原型:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
參數(shù):
第一個參數(shù)signum:需要捕捉的信號編號
第二個參數(shù)handler:對信號自定義的行為,對捕捉信號的處理方法(函數(shù)),handler是一個回調(diào)函數(shù),該處理方法的參數(shù)是 int,返回值是void
sighandler_t是一個函數(shù)指針
2.2?通過終端按鍵產(chǎn)生信號
測試代碼
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "我是一個進程,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
該代碼是一個死循環(huán),前面已經(jīng)說過可以使用 Ctrl+C 來終止進程,發(fā)送的信號是 2號信號 SIGINT
小提示:進程運行了,該進程就變成了前臺進程(命令行在當前進程無效),bash就會變成后臺進程,進程結(jié)束后,bash又會變回前臺進程,命令行生效?
實際上除了按 Ctrl+C 之外,按 Ctrl+\ 也可以終止該進程
按 Ctrl+C 和按 Ctrl+\ 都可以終止進程,但是兩者有什么區(qū)別?
Ctrl+C?發(fā)送的信號是2號信號 SIGINT,Ctrl+\?發(fā)送的信號是3號信號 SIGQUIT
這兩個信號的默認行為(Action)不一樣:2號信號默認行為是 Term,3號信號默認行為是 Core
Term 上面解釋過了,不解釋了
Core?在終止進程的時候會進行一個動作,那就是核心轉(zhuǎn)儲
Default action is to terminate the process and dump core (see core(5)).
dump core:核心轉(zhuǎn)儲
什么是核心轉(zhuǎn)儲?
Term 把進程終止了就不做其他工作了,核心轉(zhuǎn)儲Core 把進程終止后還做其他的工作
注意:在云服務(wù)器中,核心轉(zhuǎn)儲是默認被關(guān)掉的,我們需要打開才能觀察到現(xiàn)象
以通過使用 ulimit -a 命令查看當前資源限制的設(shè)定
其中,第一行顯示core文件的大小為0,即表示核心轉(zhuǎn)儲是被關(guān)閉的
我們可以通過?ulimit -c size
命令來設(shè)置core文件的大小,即打開核心轉(zhuǎn)儲
core文件的大小設(shè)置完畢后,就相當于將核心轉(zhuǎn)儲功能打開了
再次運行上面的程序,Ctrl+\把進程終止(發(fā)送3號信號,默認動作Core),現(xiàn)象就會出現(xiàn):core dumped
并且會在當前路徑下生成一個core文件,該文件以一串數(shù)字為后綴,而這一串數(shù)字實際上就是發(fā)生這一次核心轉(zhuǎn)儲的進程的PID
?核心轉(zhuǎn)儲就是:當進程出現(xiàn)異常的時候,我們將進程在對應(yīng)的時刻,在內(nèi)存中的有效數(shù)據(jù)轉(zhuǎn)儲到磁盤中,也就是上面的文件
那么核心轉(zhuǎn)儲有什么用??
當我們的代碼出錯了,我們最關(guān)心的是我們的代碼是什么原因出錯的。如果我們的代碼運行結(jié)束了,那么我們可以通過退出碼來判斷代碼出錯的原因,而如果一個代碼是在運行過程中出錯的,那么我們也要有辦法判斷代碼是什么原因出錯的
當我們的程序在運行過程中崩潰了,我們一般會通過調(diào)試來進行逐步查找程序崩潰的原因。而在某些特殊情況下,我們會用到核心轉(zhuǎn)儲,核心轉(zhuǎn)儲指的是操作系統(tǒng)在進程收到某些信號而終止運行時,將該進程地址空間的內(nèi)容以及有關(guān)進程狀態(tài)的其他信息轉(zhuǎn)而存儲到一個磁盤文件當中,這個磁盤文件也叫做核心轉(zhuǎn)儲文件(支持調(diào)試)
可以使用gdb進行調(diào)試
為了方便演示,使用 2.4.2?的空指針例子演示,代碼如下:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "我是一個進程,正在運行中,pid: " << getpid() << endl;
sleep(2);
//空指針(野指針)
int *p = nullptr;
*p = 10;
}
return 0;
}
?注:Linux默認是release,調(diào)試需要增加 -g選項
運行結(jié)果
?使用gdb對當前可執(zhí)行程序進行調(diào)試
gdb 可執(zhí)行程序, 進入調(diào)試
然后直接使用 core-file 核心轉(zhuǎn)儲文件 命令加載 core文件,即可判斷出該程序在終止時收到了11號信號,并且定位到了產(chǎn)生該錯誤的具體代碼,錯誤信息也詳細列出
core-file core.11467
?事后用調(diào)試器檢查core文件以查清錯誤原因,這種調(diào)試方式叫做事后調(diào)試
這就是 Term 和 Core 的區(qū)別,Term是正常終止進程?
2.3?調(diào)用系統(tǒng)函數(shù)向進程發(fā)信號
2.3.1 kill函數(shù)
kill函數(shù)是一個系統(tǒng)調(diào)用,kill命令就是通過 kill函數(shù)來實現(xiàn)的
測試 kill命令?
使用kill命令向一個進程發(fā)送信號時,我們可以用 kill -信號編號 進程pid 的形式進行發(fā)送信號
kill函數(shù)的作用是:向目標進程發(fā)送指定信號
man 2 kill 查看:
函數(shù):kill
頭文件
#include <sys/types.h>
#include <signal.h>
函數(shù)原型:
int kill(pid_t pid, int sig);
參數(shù)
第一個參數(shù)pid就是進程的pid
第二個參數(shù)sig是信號的編號
返回值
發(fā)送成功,返回0,否則返回-1
使用 kill函數(shù)模擬 kill命令(mykill):
#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <string>
#include <unistd.h>
using namespace std;
//使用手冊
static void Usage(const string& proc)
{
cout << "\nUsage: " << proc << " pid signo\n" << endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
//把字符串轉(zhuǎn)整型
pid_t id = atoi(argv[1]);
int signo = atoi(argv[2]);
int n = kill(id, signo);
if(n != 0)
{
perror("kill");
}
return 0;
}
另一個測試代碼,死循環(huán),test.cc
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "我是一個進程,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
選運行死循環(huán)測試代碼,再使用 mykill殺掉死循環(huán),這樣就實現(xiàn)了一個 kill命令
2.3.2?raise函數(shù)
raise函數(shù)的作用是:給自己發(fā)送信號
man 3?raise 查看:
函數(shù):raise
頭文件
#include <signal.h>
函數(shù)原型
int raise(int sig);
參數(shù):sig是要發(fā)送的信號編號
返回值
發(fā)送成功,則返回0,否則返回一個非零值
這個函數(shù)也可以通過 kill函數(shù)實現(xiàn):kill(getpid(), sig)?
測試代碼:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
int cnt = 0;
while(true)
{
cout << "我是一個進程,pid: " << getpid() << " cnt: " << cnt++ << endl;
sleep(1);
//cnt>=5,就給自己發(fā)送3號信號
if(cnt >= 5)
raise(3);
}
return 0;
}
運行結(jié)果
2.3.3?abort函數(shù)
abort函數(shù)用于給自己發(fā)送指定信號:6號信號SIGABRT,6號信號的默認動作也是終止進程
man 3 abort 查看
??
函數(shù):abort
頭文件
#include <stdlib.h>
函數(shù)原型
void abort(void);
?這個函數(shù)也可以通過 kill函數(shù)實現(xiàn):kill( getpid(),??SIGABRT?)?
?測試代碼:
#include <iostream>
#include <cstdlib>
#include <unistd.h>
using namespace std;
int main()
{
int cnt = 0;
while(true)
{
cout << "我是一個進程,pid: " << getpid() << " cnt: " << cnt++ << endl;
sleep(1);
//cnt>=5,就給自己發(fā)送指定信號
if(cnt >= 5)
abort();
}
return 0;
}
運行結(jié)果
abort函數(shù)總是會成功的,所以沒有返回值
進程收到的大部分信號,默認動作都是終止進程
2.4 硬件異常產(chǎn)生信號
信號的產(chǎn)生,不一定非得用戶顯示發(fā)送,有些信號會在OS內(nèi)部自動產(chǎn)生,比如硬件異常產(chǎn)生的信號
硬件異常被硬件以某種方式被硬件檢測到并通知內(nèi)核,然后內(nèi)核向當前進程發(fā)送適當?shù)男盘?
2.4.1 除0操作產(chǎn)生的異常?
比如進行進程除0操作,VS會直接報錯終止進程
下面在g++下進行測試,測試代碼如下
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "我在運行中..." << endl;
sleep(1);
//除0操作
int a = 10;
a /= 0;
}
return 0;
}
運行結(jié)果,編譯警告不用理會
?為什么除0會終止進程??
因為進程收到了來自O(shè)S的信號,該信號是SIGFPE,8號信號
該信號的默認動作也是 Core,也是終止進程
Floating point exception:浮點異常
?下面對該信號進行捕捉,捕捉后進行自定義動作
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
//自定義行為
void handler(int signo)
{
cout << "捕捉到一個信號,該信號編號是:" << signo << endl;
}
int main()
{
signal(8, handler);
while(true)
{
cout << "我在運行中..." << endl;
sleep(1);
//除0操作
int a = 10;
a /= 0;
}
return 0;
}
?運行結(jié)果
?運行結(jié)果說明,進程確實收到了8信號,可是為什么一直對該信號一直捕獲??OS為什么一直發(fā)送8號信號???
下面講解對于除0的理解:
?在CPU中有很多的寄存器,例如eax,ebx,eip等等
CPU會將代碼中的變量拿到寄存器中進行運算,如果有需要,運算結(jié)果需要返回
?進行對兩個數(shù)進行算術(shù)運算時,我們是先將這兩個操作數(shù)分別放到兩個寄存器當中,然后進行算術(shù)運算并把結(jié)果寫回寄存器當中
(這里就簡單略過具體的寄存器,方便理解,圖也是簡略化)
CPU當中還有一組寄存器叫做狀態(tài)寄存器,它可以用來標記當前指令執(zhí)行結(jié)果的各種狀態(tài)信息,如有無進位、有無溢出等等
除0操作,對于計算機來說,是除一個無窮小的數(shù),得到的結(jié)果是無窮大的數(shù),寄存器存不下這個數(shù),這時候狀態(tài)寄存器的溢出標志位由0置1,這時CPU就會發(fā)出運算異常的信號,OS就會識別到這個異常,OS就會給指定的進程發(fā)送這個信號,這個信號就是8號信號,CPU報的這個異常歸屬于硬件異常,OS檢測到,由OS主動發(fā)送給目標進程
接下來解釋為什么會一直死循環(huán)捕獲到8號信號???
?代碼在CPU中執(zhí)行的時候,此時CPU內(nèi)的寄存器的內(nèi)容屬于該進程的上下文數(shù)據(jù),因為寄存器只有一份,代碼沒有運行完,當該進程的時間片到了,就會從CPU上切下去,該進程的上下文數(shù)據(jù)也被會保存,包括溢出標志位
進程被來回切換,就有無數(shù)次寄存器的內(nèi)容被保存會恢復(fù)的過程,所以每次恢復(fù)的時候,OS都會識別到CPU內(nèi)部的狀態(tài)寄存器溢出標志位為1,OS就會給該進程發(fā)送8號信號
由于我們把該信號捕獲了,執(zhí)行自定義行為,該信號的默認行為就不會執(zhí)行,每次恢復(fù)的時候,進程還沒有被終止,OS依舊會識別到CPU內(nèi)部的狀態(tài)寄存器溢出標志位為1,就又發(fā)8號信號給該進程,以此往復(fù),就陷入死循環(huán),所以我們會看到8號信號一直被捕獲
2.4.2 空指針異常
空指針(野指針)問題在程序中可能會遇到,空指針VS直接崩潰,終止程序,下面在g++下測試
測試代碼:
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "我是一個進程,正在運行中,pid: " << getpid() << endl;
sleep(2);
//空指針(野指針)
int *p = nullptr;
*p = 10;
}
return 0;
}
運行結(jié)果
結(jié)果發(fā)現(xiàn),空指針操作進程直接被終止了,為什么進程會被終止?
因為進程收到了來自O(shè)S的信號,該信號是11號信號 SIGSEGV
該信號的默認動作也是 Core,也是終止進程
Invalid memory reference:無效內(nèi)存引用
Segmentation fault:段錯誤
?下面對該信號進行捕捉,捕捉后進行自定義動作
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
//自定義行為
void handler(int signo)
{
cout << "捕捉到一個信號,該信號編號是:" << signo << endl;
}
int main()
{
signal(11, handler);
while(true)
{
cout << "我是一個進程,正在運行中,pid: " << getpid() << endl;
sleep(2);
//空指針(野指針)
int *p = nullptr;
*p = 10;
}
return 0;
}
運行結(jié)果?
?OS怎么知道空指針異常??
我們必須知道的是,當我們要訪問一個物理內(nèi)存時,一定要先經(jīng)過頁表的映射,將虛擬地址轉(zhuǎn)換成物理地址,然后才能進行相應(yīng)的訪問操作
在從虛擬地址映射到物理地址的過程中,必須經(jīng)過頁表,頁表上有一個硬件叫做MMU,MMU被集成在CPU里面,MMU用于計算映射關(guān)系,在MMU算出物理內(nèi)存的映射關(guān)系之后,CPU可以直接進行物理內(nèi)存訪問
當我們要訪問不屬于我們的虛擬地址時(訪問空地址,空指針的使用,空地址是不允許訪問的),MMU在進行虛擬地址到物理地址的轉(zhuǎn)換時就會出現(xiàn)錯誤,MMU就會出現(xiàn)異常,OS在這時也會識別這個異常,然后OS就會向目標進程發(fā)送11號信號SIGSEGV,該信號的默認動作就是終止進程
死循環(huán)捕捉信號,解釋與上面除0的類似,不解釋了
2.5?由軟件條件產(chǎn)生信號
SIGPIPE信號實際上就是一種由軟件條件產(chǎn)生的信號,當進程在使用管道進行通信時,讀端進程將讀端關(guān)閉,而寫端進程還在一直向管道寫入數(shù)據(jù),那么此時寫端進程就會收到13號信號SIGPIPE 進而被操作系統(tǒng)終止
該信號的默認行為是 Term,也是終止進程,這個就解釋到這
下面介紹 alarm函數(shù) 和 SIGALRM信號
?alarm 函數(shù)的作用是:設(shè)定一個鬧鐘,也就是告訴內(nèi)核在 seconds秒之后給當前進程發(fā) SIGALRM信號, 該信號的默認處理動作是終止當前進程
man 2 alarm 查看
函數(shù):alarm
頭文件:
#include <unistd.h>
函數(shù)原型:
unsigned int alarm(unsigned int seconds);
參數(shù):傳入一個時間,單位秒
返回值:
若調(diào)用alarm函數(shù)前,進程已經(jīng)設(shè)置了鬧鐘,則返回上一個鬧鐘時間的剩余時間,并且本次鬧鐘的設(shè)置會覆蓋上一次鬧鐘的設(shè)置
如果調(diào)用alarm函數(shù)前,進程沒有設(shè)置鬧鐘,則返回值為0
?測試代碼
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
//1秒后才發(fā)送信號
alarm(1);
int cnt = 0;
while(true)
{
cout << "cnt: " << cnt << endl;
cnt++;
}
return 0;
}
運行結(jié)果
下面進行捕捉該信號
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int cnt = 0;
//自定義行為
void handler(int signo)
{
cout << "捕捉到一個信號,該信號編號是:" << signo << endl;
cout << "cnt: "<< cnt << endl;
exit(1);//自定義行為,退出進程
}
int main()
{
signal(14, handler);
//1秒后才發(fā)送信號
alarm(1);
while(true)
{
cnt++;
}
return 0;
}
?運行結(jié)果
14號信號默認動作是 Term,也是終止進程
兩次cnt實驗結(jié)果數(shù)據(jù)級別相差較大,由此也證明了,與計算機單純的計算相比較,計算機與外設(shè)進行IO時的速度是非常慢的
注意:9號信號不支持捕捉,這是禁止的,這是一個管理員信號
信號產(chǎn)生,完結(jié),下一篇進入信號保存和處理文章來源:http://www.zghlxwxcb.cn/news/detail-425062.html
--------------------- END ----------------------?文章來源地址http://www.zghlxwxcb.cn/news/detail-425062.html
「 作者 」 楓葉先生
「 更新 」 2023.4.4
「 聲明 」 余之才疏學(xué)淺,故所撰文疏漏難免,
或有謬誤或不準確之處,敬請讀者批評指正。
到了這里,關(guān)于【Linux】第八講:Linux進程信號詳解(一)_ 認識信號 | 產(chǎn)生信號的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!