👦個人主頁:Weraphael
?🏻作者簡介:目前正在學習c++和算法
??專欄:Linux
🐋 希望大家多多支持,咱一起進步!😁
如果文章有啥瑕疵,希望大佬指點一二
如果文章對你有幫助的話
歡迎 評論💬 點贊👍🏻 收藏 📂 加關注😍
目錄
- 一、通過終端按鍵產生信號
- 1.1 Ctrl + /
- 1.2 Ctrl + z
- 二、通過kill命令產生信號
- 三、通過系統調用接口產生信號
- 3.1 kill
- 3.2 raise
- 3.3 abort
- 四、硬件異常產生信號
- 4.1 除 0 錯誤導致異常
- 4.2 狀態寄存器
- 4.3 野指針異常
- 五、軟件條件產生信號
- 5.1 alarm 函數
- 六、核心轉儲 core dump
- 6.1 知識回顧
- 6.2 打開與關閉核心轉儲
- 6.3 核心轉儲的作用
一、通過終端按鍵產生信號
1.1 Ctrl + /
在上篇博客中(點擊跳轉),我們已經驗證了Ctrl + c
是向前臺進程發送2
號信號SIGINT
來中斷程序。
這篇再介紹一個組合鍵:Ctrl + \
。它其實是向前臺進程發送3
號信號SIGQUIT
來終止程序,同時會產生一個core
文件。我們可以使用捕捉信號函數signal
來驗證。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void myhandler(int signum)
{// 修改的行為:打印信號編號后退出cout << "信號編號為:" << signum << endl;exit(1);
}int main()
{// 捕捉3號信號signal(SIGQUIT, myhandler);while (true){cout << "我是一個進程,我在做死循環操作" << endl;sleep(1);}return 0;
}
【程序結果】
其原理再簡單重復一遍:當按下ctrl + \
,操作系統識別到鍵盤上有數據,觸發了硬件中斷,CPU
收到中斷請求后,會暫停當前執行的程序,保存當前狀態,并根據中斷號來調用對應硬件的方法。由于操作系統識別到是特殊控制字符,就將其轉化為3
號信號發送給前臺進程
1.2 Ctrl + z
Ctrl + z
會向前臺進程發送19
號信號SIGSTOP
。當進程收到此信號時,它會立即停止運行。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;int main()
{while (true){cout << "我是一個進程,我在做死循環操作" << endl;sleep(1);}return 0;
}
【程序結果】
需要注意的是:19
號信號SIGSTOP
不能使用signal
函數捕捉。類似地,9
號信號SIGKILL
用于立即終止一個進程,并且也不能被signal
函數捕捉。
二、通過kill命令產生信號
kill -n <pid>
# n是信號編號
以以下代碼為例:
#include <iostream>
#include <unistd.h>
using namespace std;int main()
{while (true){cout << "進程:" << getpid() << ", 我在做死循環啦~" << endl;sleep(1);}return 0;
}
【程序結果】
三、通過系統調用接口產生信號
3.1 kill
系統調用kill
函數是用于向進程發送某種信號,其函數原型如下:
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
參數解釋:
pid
:要發送信號的進程pid
sig
:要發送的信號編號,可以是標準信號如SIGINT
、SIGKILL
,也可以是用戶自定義的信號。- 返回值:成功時,返回
0
;失敗時,返回-1
,并設置errno
來指示錯誤的原因。
我們可以使用這個kill
系統調用接口來模擬實現一個kill
命令。首先kill
命令必須是如下形式:
kill -信號編號 進程pid
那么這里就可以巧用main
函數的參數。main
函數的第一個參數argc
:表示字符指針數組當中的有效元素個數。argv
:是一個字符指針數組(向量表),數組以NULL指針結尾,注意argv
的第一個值原因是程序名。
因此,代碼如下:
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <signal.h>
#include <sys/types.h>using namespace std;void Usage(string proc)
{cout << "格式:\n\t" << proc << " signum pid\n\n";
}// argv:程序名argv[0]、信號編號argv[1]、進程pid argv[2]
int main(int argc, char *argv[])
{// 如果是三個參數,可以殺掉用戶指定進程if (argc == 3){int signum = stoi(argv[1]);pid_t pid = stoi(argv[2]);int n = kill(pid, signum);if (n != 0){perror("kill");exit(2);}}// 如果不是輸入三個參數,就提醒用戶輸入格式else{Usage(argv[0]);exit(1);}return 0;
}
接下來我再寫一個死循環代碼程序如下:
#include <unistd.h>
#include <iostream>
using namespace std;int main()
{while (true){cout << "my pid is " << getpid() << endl;sleep(1);}return 0;
}
【程序結果】
3.2 raise
raise
函數用于向當前進程發送一個信號。它的基本形式如下:
#include <signal.h>int raise(int sig);
sig
:要發送的信號編號,可以是標準信號(如SIGINT
、SIGTERM
等),也可以是用戶自定義的信號。- 返回值:成功時,返回
0
;失敗時,返回非0
值。
這個函數可以這么理解:相當于一顆地雷,當程序“踩到”它,就會產生信號并執行。
例如:程序是死循環打印,當打印完3條消息后,就立馬殺掉進程。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>using namespace std;int main()
{int cnt = 3;while (true){cout << "My pid is " << getpid() << endl;sleep(1);cnt--;if (cnt == 0){// 9號信號raise(SIGKILL);}}return 0;
}
【程序結果】
這個函數通常搭配signal
函數使用。因為有些信號終止程序不會像9號信號一樣給killed
提示,比方說2
號信號SIGINT
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>using namespace std;int main()
{int cnt = 3;while (true){cout << "My pid is " << getpid() << endl;sleep(1);cnt--;if (cnt == 0){// 2號信號raise(SIGINT);}}return 0;
}
【程序結果】
當加了signal
函數后,就直觀很多了。代碼如下:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>using namespace std;void handler(int signum)
{cout << "我得到了2號信號SIGINT" << endl;exit(1);
}int main()
{signal(2, handler);int cnt = 3;while (true){cout << "My pid is " << getpid() << endl;sleep(1);cnt--;if (cnt == 0){// 2號信號raise(SIGINT);}}return 0;
}
【程序結果】
這個函數其實封裝了系統調用接口kill
,例如以上代碼等價于kill(getpid(), 2);
3.3 abort
abort
函數用于向自己發送一個6
號信號SIGABRT
,異常終止當前進程的執行。
函數原型如下:
#include <stdlib.h>void abort(void);
【代碼樣例】
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>using namespace std;void handler(int signum)
{cout << "我得到了6號信號SIGABRT" << endl;exit(1);
}int main()
{signal(6, handler);int cnt = 3;while (true){cout << "My pid is " << getpid() << endl;sleep(1);cnt--;if (cnt == 0){abort();}}return 0;
}
【程序結果】
值得一提的是,abort
函數即使在修改執行動作后(沒有exit(int)
),最后仍然會發送6
號信號來退出進程。
因此,abort
函數也是被封裝的,相當于kill(getpid(), 6);
總的來說,系統調用中舉例的這三個函數關系是:kill
包含raise
,raise
包含 abort
,作用范圍是在逐漸縮小的
四、硬件異常產生信號
4.1 除 0 錯誤導致異常
程序異常本質是進程收到了某種信號。
比方說,我們可以使用signal
函數來捕捉除0
異常的進程收到了幾號信號。
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>using namespace std;void handler(int signum)
{cout << "我收到了" << signum << "號信號" << endl;
}int main()
{for (int i = 1; i <= 31; i++){signal(i, handler);}printf("1 / 0 = %d\n", 1 / 0);return 0;
}
【程序結果】
從上我們發現:
- 操作系統發送
8
號信號SIGFPE
- 只發生了一次 除
0
異常,進程非但沒退出,還在死循環打印。
首先大部分進程對信號的處理行為都是終止程序,這里沒有終止程序是因為我們修改了系統默認的行為,在handler
函數最后一行補上exit
函數即可正常終止。
但非常奇怪的是,我們代碼中只有一次除0
異常啊,正常來說只需要打印一次就行了,即使進程沒有退出,將進程阻塞不就好了。為什么會一直死循環打印呢?想要明白背后的原理,需要先認識一下狀態寄存器。
4.2 狀態寄存器
代碼中除
0
和野指針行為,操作系統是怎么知道的呢?
在CPU
中,存在很多寄存器,其中大部分主要用來存儲數據信息,用于運算。除此之外,還存在一種特殊的 寄存器:狀態寄存器。這個寄存器是一個位圖結構。專門用來檢測當前進程是否出現錯誤行為。如果有,就會把位圖結構中對應的比特位置設置成1
,意味著出現了異常。
當檢測到狀態寄存器中某個異常標志位被設置為1
時,硬件(寄存器)會向CPU
發送一個硬件中斷,CPU
收到來自硬件的中斷信號后,又因為操作系統是這些硬件的“管理者”,它會暫停當前正在執行的進程。然后檢查當前進程的上下文異常標志位的具體含義,以確定發生了什么樣的異常。根據異常類型調用相應的異常處理程序。這些程序通常是預先定義好的。
所以現在就可以解釋為什么進程收到來自操作系統的異常信號后,雖然我們修改了默認處理動作(不退出),但是會死循環打印的情況。這是因為操作系統一直檢測當前進程的狀態寄存器仍然處于異常狀態,再加上不退出進程,此時進程一直在被調度運行,所以操作系統才會不斷發送8
號信號,才會死循環式的打印。
因此,異常信號被捕捉不是為了解決什么問題,而是讓用戶清除的知道程序是因為什么而掛掉的。
4.3 野指針異常
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <string>
#include <signal.h>
using namespace std;int main()
{int *p = nullptr;*p = 10;return 0;
}
【程序結果】
Segmentation fault
是一個段錯誤,這是每個C/C++
程序猿都會遇到的問題,因為太容易觸發了,出現段錯誤問題時,操作系統會發送11
號SIGSEGV
信號終止進程,這里不再證明演示。
那么 野指針問題是如何引發的呢?本質上就是:虛擬地址轉化為物理地址失敗。比方說:虛擬地址映射到不屬于它的物理空間(越界訪問)、對該物理空間沒有寫的權限等。具體解釋如下:
-
當代的
MMU
已經被集成在CPU
內部,除此之外CPU
還有一個寄存器,稱為BadVAddr
(Bad Virtual Address
,壞虛擬地址)。這個寄存器的作用是在虛擬地址轉換失敗時,將引起異常的虛擬地址存儲起來。 -
所以,硬件(寄存器)會向
CPU
發送一個硬件中斷,CPU
收到來自硬件的中斷信號后,又因為操作系統是這些硬件的“管理者”,它會暫停當前正在執行的進程。然后檢查當前進程的上下文異常標志位的具體含義,以確定發生了什么樣的異常。根據異常類型調用相應的異常處理程序。這些程序通常是預先定義好的。
五、軟件條件產生信號
異常不僅由硬件產生,還可以由軟件產生!
比如我們之前的管道,如果一開始讀寫端都打開,但是關閉了讀端,那么寫端進程就會收到一個13
號信號SIGPIPE
。這就是一種軟件異常。【Linux】進程間通信之匿名管道
5.1 alarm 函數
alarm
函數是一個系統調用接口,用來設置一個定時器。其功能是:在指定的秒數后發送14
號信號SIGALRM
給調用進程。該信號的默認處理動作是終止當前進程。
原型如下:
#include <unistd.h>unsigned int alarm(unsigned int seconds);
seconds
:指定的秒數,即多少秒后發送SIGALRM
信號。如果參數為0
,則任何已設置的定時器都會被取消,但不會產生信號。- 返回值:返回調用之前的剩余定時器時間。如果之前沒有設置定時器,則返回
0
。 aralm
函數的原理:我們知道每個進程都可以使用alarm
設置鬧鐘,所以操作系統中一定有大量的鬧鐘。所以操作系統要管理鬧鐘,所以鬧鐘就會用struct
結構體描述,一定有進程pid
(表示哪個進程設置的鬧鐘)等字段,然后用鏈表等數據結構管理起來。這樣所謂的鬧鐘管理就變成了對鏈表等的增刪查改。操作系統底層中alarm
的底層所用的時間用的是時間戳,只要系統的當前時間大于等于里面設置的時間,就會發送信號。我們遍歷鏈表的時候是比較浪費時間的。所以用一個小堆是最簡單的。
【代碼樣例】
#include <iostream>
#include <unistd.h>
#include <stdlib.h>using namespace std;int main()
{// 設定一個5s的定時器alarm(5);int n = 1;while (true){cout << "我是一個進程,已經運行了 " << n++ << " 秒 PID: " << getpid() << endl;sleep(1);}return 0;
}
【程序結果】
那我們就可以在執行主要任務的同時,去定時完成其他任務了。例如:
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>using namespace std;void work()
{cout << "我在完成其他任務" << endl;
}void handler(int signum)
{// 異常捕捉時,再設置一個定時器完成其他任務alarm(5);work();
}int main()
{// 設定一個5s的定時器alarm(5);// 信號捕捉signal(14, handler);int n = 1;while (true){// 主要任務cout << "我是一個進程,已經運行了 " << n++ << " 秒 PID: " << getpid() << endl;sleep(1);}return 0;
}
【程序結果】
六、核心轉儲 core dump
6.1 知識回顧
我們可以使用以下命令來看信號的詳細信息手冊
man 7 signal
我們可以注意到,常見的信號中大部分是終止Term
(terminate
)信號的。還有一些是暫停(Stop
),繼續(Cont
),忽略(Ign
)。
那么這個核心Core
是什么東西呢?
在我們當時學習進程控制的時候,還有一個這個字段core dump
標志我們還沒提到過(【Linux】進程控制 )
當一個進程異常退出時(參看上面Action
為Core
信號),該core dump
標志會被設置成1
,并且會生成一個core.pid
二進制文件,然后不再設置退出碼(為0
,因為這種情況下程序沒有正常的執行路徑結束)
我們可以寫代碼來驗證一下:
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main()
{pid_t pid = fork();if (pid == 0) // 子進程{int cnt = 500;while (cnt--){cout << "I am child process, pid:" << getpid() << ", cnt:" << cnt << endl;sleep(1);}// 打印5句話后子進程退出exit(1);}else // 父進程{int status = 0;pid_t rid = waitpid(pid, &status, 0);if (rid == pid){cout << "child quit info, rid:" << rid << ", exit code:" << ((status >> 8) & 0xff)<< ", exit signal:" << (status & 0x7f) << ", core dump:" << ((status >> 7) & 1)<< endl;}}return 0;
}
【程序結果】
為什么沒有生成核心轉儲文件啊?難道是我們的環境有問題嗎?
確實,當前環境確實有問題,因為它是云服務器,而云服務器中默認是關閉核心轉儲功能的。
6.2 打開與關閉核心轉儲
通過以下指令可以查看當前系統中的資源限制情況
ulimit -a
可以看到,當前系統中的核心轉儲文件大小為0
,即不生成核心轉儲文件。
通過以下指令手動設置核心轉儲文件大小(打開核心轉儲文件)
ulimit -c <大小自己定>
# 關閉大小設置為0即可
# 重啟xhell自動關閉
我們可以再來測試一下,是否dump core
是否還為1
:
我們觀察到:核心轉儲文件是很大的。
6.3 核心轉儲的作用
當一個進程在出現異常的時候,操作系統會將進程在內存中的運行信息進行核心轉儲,轉儲到當前進程的運行目錄下(磁盤),形成core.pid
這樣的二進制大文件(核心轉儲文件)。
核心轉儲的作用是什么呢?
答案是:調試(事后調試),并且直接從出錯的地方開始調試。
以下是調試樣例代碼
#include <iostream>using namespace std;int main()
{int a = 1;int b = 0;a = a / b;cout << "a = " << a << endl;return 0;
}
調試方法如下(四個步驟):
- 首先
gcc/g++
在編譯時加-g
生成可調試文件
- 運行程序,生成 core-dump 文件
gdb 可執行文件
進入調試模式
- 在
gdb
命令行輸入:core-file <核心轉儲文件>
。即利用核心轉儲文件,快速定位至出錯的地方。
最后一個問題:既然核心轉儲文件這么好用,為什么大多數的云服務器默認是將它關閉的呢?
現在許多大型IT公司以及許多中小型企業,都傾向于使用云服務器來托管其后端服務和應用程序。如果打開的話,當應用程序如果收到某個信號,核心轉儲文件會在進程的當前目錄下創建一個大文件。而有很多進程都可能會產生核心轉儲文件,當文件足夠多時,磁盤被擠滿,導致系統IO
異常,最終會導致整個服務器掛掉的,所以云服務器一般默認是關閉的。