目錄
前言:
1、進程信號基本概念
1.1、什么是信號?
1.2、信號的作用
2、鍵盤鍵入
2.1、ctrl+c 終止前臺進程
2.1.1、signal 注冊執行動作
3、系統調用
3.1、kill 函數
3.2、模擬實現 myKill
3.3、raise 函數
3.4、abort 函數
4、軟件條件信號產生(發送)的第三種方式:軟件條件
4.1、alarm 設置鬧鐘
5、硬件異常
5.1、除 0 導致異常
5.2、狀態寄存器
5.3、野指針導致異常
6.核心轉儲?
6.2、打開與關閉核心轉儲
6.3、核心轉儲的作用
?總結:
前言:
在?Linux
?中,進程具有獨立性,進程在運行后可能 “放飛自我”,這是不利于管理的,于是需要一種約定俗成的方式來控制進程的運行,這就是?進程信號
,本文將會從什么是進程信號開篇,講述各種進程信號的產生方式及作用
正文:
1、進程信號基本概念
1.1、什么是信號?
信號 是信息傳遞的承載方式,一種信號往往代表著一種執行動作,比如:
- 雞叫 =>?天快亮了
- 鬧鐘 =>?起床、完成任務
- 紅綠燈 =>?紅燈停,綠燈行
當然這些都是生活中的 信號,當產生這些 信號 時,我們會立馬想到對應的 動作 ,這是因為 我們認識并能處理這些信號
我們能進行處理是因為受過教育,學習了執行動作,但對進程來說,它可沒有接受過九年義務教育,也不知道什么時候該干什么事
于是程序員們給操作系統植入了一批 指令,一個指令表示一種特殊動作,而這些指令就是 信號(進程信號)
kill -l
?
這些就是當前系統中的 進程信號,一共 62 個,其中 1~31 號信號為 普通信號(學習目標),用于 分時操作系統;剩下的 34~64 號信號為 實時信號,用于 實時操作系統
- 分時操作系統:根據時間片實行公平調度,適用于個人電腦
- 實時操作系統:高響應,適合任務較少、需要快速處理的平臺,比如汽車車機、火箭發射控制臺
1.2、信號的作用
早在 《Linux進程學習【進程狀態】》 我們就已經使用過?信號?了,比如:
kill -9 pid
?終止進程運行kill -19 pid
?暫停進程運行kill -18 pid
?恢復進程運行
就連常用的?ctrl+c
?和?ctrl+d
?熱鍵本質上也是?信號
這么多信號,其對應功能是什么呢?
- 可以通過?
man 7 signal
?進行查詢
man 7 signal
?
?簡單總結一下,1~31
?號信號對應的功能如下(表格內容引用自?2021dragon
?Linux中的31個普通信號)
?
?注意:?其中的?9
?號 和?19
?號信號是非常特殊的,不能修改其默認動作
1.3、信號的基本認知
進程信號由 信號編號 + 執行動作 構成,一個信號對應一種動作,對于進程來說,動作無非就這幾種:終止進程、暫停進程、恢復進程,3 個信號就夠用了啊,為什么要搞這么多信號?
- 創造信號的目的不只是控制進程,還要便于管理進程,進程的終止原因有很多種,如果一概而論的話,對于問題分析是非常不友好的,所以才會將信號細分化,搞出這么多信號,目的就是為了方便定位、分析、解決問題
- 并且 普通信號 就 31 個,這就是意味著所有普通信號都可以存儲在一個 int 中,表示是否收到該信號(信號的保存)
所以信號被細化了,不同的信號對應不同的執行動作,雖然大部分最終都是終止進程
進程的執行動作是可修改的,默認為系統預設的 默認動作
- 默認動作
- 忽略
- 自定義動作
所以我們可以 更改信號的執行動作(后面會專門講信號處理相關內容)
信號有這么多個,并且多個進程可以同時產生多個信號,操作系統為了管理,先描述、再組織,在 PCB 中增加了 信號相關的數據結構:signal_struct,在這個結構體中,必然存在一個 位圖結構 uint32_t signals 存儲 1~31 號信號的有無信息
?
//信號結構體源碼(部分)
struct signal_struct {atomic_t sigcnt;atomic_t live;int nr_threads;wait_queue_head_t wait_chldexit; /* for wait4() *//* current thread group signal load-balancing target: */struct task_struct *curr_target;/* shared signal handling: */struct sigpending shared_pending;/* thread group exit support */int group_exit_code;/* overloaded:* - notify group_exit_task when ->count is equal to notify_count* - everyone except group_exit_task is stopped during signal delivery* of fatal signals, group_exit_task processes the signal.*/int notify_count;struct task_struct *group_exit_task;/* thread group stop support, overloads group_exit_code too */int group_stop_count;unsigned int flags; /* see SIGNAL_* flags below *//** PR_SET_CHILD_SUBREAPER marks a process, like a service* manager, to re-parent orphan (double-forking) child processes* to this process instead of 'init'. The service manager is* able to receive SIGCHLD signals and is able to investigate* the process until it calls wait(). All children of this* process will inherit a flag if they should look for a* child_subreaper process at exit.*/unsigned int is_child_subreaper:1;unsigned int has_child_subreaper:1;//……
};
1.信號是執行的動作的信息載體,程序員在設計進程的時候,早就已經設計了其對信號的識別能力
2.信號對于進程來說是異步的,隨時可能產生,如果信號產生時,進程在處理優先級更高的事情,那么信號就不能被立即處理,此時進程需要保存信號,后續再處理
3.進程可以將 多個信號 或 還未處理 的信號存儲在 signal_struct 這個結構體中,具體信號編號,存儲在 uint32_t signals 這個位圖結構中
4.所謂的 “發送” 信號,其實就是寫入信號,修改進程中位圖結構中對應的比特位,由 0 置為 1,表示該信號產生了
5.signal_struct 屬于內核數據結構,只能由 操作系統 進行同一修改,無論信號是如何產生的,最終都需要借助 操作系統 進行發送
6.信號并不是立即處理的,它會在合適的時間段進行統一處理
?
本文講解的就是?信號產生?部分相關知識,下面正式開始學習?信號產生?
2、鍵盤鍵入
信號產生(發送)的第一種方式:鍵盤鍵入
通俗來說就是命令行操作
2.1、ctrl+c 終止前臺進程
系統卡死遇到過吧?程序死循環遇到過吧?這些都是比較常見的問題,當發生這些問題時,我們可以通過?鍵盤鍵入?ctrl + c
?發出?2
號信號終止前臺進程的運行
?下面是一段死循環代碼:
#include <iostream>
#include <unistd.h>
using namespace std;int main()
{while(true){cout << "我是一個進程,我正在運行…… PID: " << getpid() << endl;sleep(1);}return 0;
}
運行程序后,會一直循環打印,此時如果想要終止進程,可以直接按?ctrl + c
?發出?2
?號信號,終止前臺進程?
此時發出了一個 2 號信號 SIGINT 終止了該進程的運行
如何證明呢?如何證明按 ctrl + c 發出的是 2 號信號呢?
證明自有方法,前面說過,一個信號配有一個執行動作,并且執行動作是可以修改的,需要用到 signal 函數(屬于 信號處理 部分的內容,這里需要提前用一下)
2.1.1、signal 注冊執行動作
signal
?函數可以用來?修改信號的執行動作,也叫注冊自定義執行動作
signal 調用成功返回上一個執行方法的值(其實就是下標,后面介紹),失敗則返回 SIG_ERR,并設置錯誤碼
返回值可以不用關注,重點在于 signal 的參數
參數1 待操作信號的編號
參數2 待注冊的新方法
參數1 就是信號編號,為 int,單純地傳遞 信號名也是可以的,因為信號名其實就是信號編號的宏定義
?
參數2?是一個函數指針,意味著需要傳遞一個?參數為?int
,返回值為空的函數對象?
- 參數?
int
?是執行動作的信號編號
void handler(int) //其中的函數名可以自定義
顯然,signal
?函數是一個?回調函數,當信號發出時,會去調用相應的函數,也就是執行相應的動作
我們先對?2
?號信號注冊新動作,在嘗試按下?ctrl + c
,看看它發出的究竟是不是?2
?號信號
?
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;void handler(int signo)
{cout << "當前 " << signo << " 號信號正在嘗試執行相應的動作" << endl;
}int main()
{//給 2 號信號注冊新方法signal(2, handler);while(true){cout << "我是一個進程,我正在運行…… PID: " << getpid() << endl;sleep(1);}return 0;
}
?
當我們修改 2 號信號的執行動作后,再次按下 ctrl + c 嘗試終止前臺進程,結果失敗了!執行動作變成了我們注冊的新動作
這足以證明 ctrl + c 就是在給前臺進程發出 2 號信號,ctrl + c 失效后,可以通過 ctrl + \ 終止進程,發出的是 3 號信號(3 號信號在發出后,會生成 核心轉儲 文件)
普通信號只有 31 個,如果把所有普通信號的執行動作都改了,會發生什么呢?難道會得到一個有著 金剛不壞 之身的進程嗎?
?
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;void handler(int signo)
{cout << "當前 " << signo << " 號信號正在嘗試執行相應的動作" << endl;
}int main()
{//給所有普通信號注冊新方法for(int i = 1; i < 32; i++)signal(i, handler);while(true){cout << "我是一個進程,我正在運行…… PID: " << getpid() << endl;sleep(1);}return 0;
}
?
大部分信號的執行動作都被修改了,但 9 號信號沒有,因為 9 號信號是 SIGKILL,專門用于殺死進程,只要是進程,他都能干掉
19 號信號 SIGSTOP 也無法修改執行動作,所以前面說過,9 號 SIGKILL 和 19 號 SIGSTOP 信號是很特殊的,經過特殊設計,不能修改其執行動作!
?
2.2、硬件中斷
當我們從鍵盤按下 ctrl + c 時,發生了這些事:CPU 獲取到鍵盤 “按下” 的信號,調用鍵盤相應的 “方法” ,從鍵盤中讀取數據,讀取數據后解析,然后發出 3 號信號
其中 CPU 捕獲鍵盤 “按下” 信號的操作稱為 硬件中斷
CPU 中有很多的針腳,不同的硬件對應著不同的針腳,每一個針腳都有自己的編號,硬件與針腳一對一相連,并通過 中斷控制器(比如 8259)進行控制,當我們按下鍵盤后
中斷控制器首先給 CPU 發送信息,包括鍵盤對應的針腳號
然后 CPU 將獲取到的針腳號(中斷號)寫入 寄存器 中
最后根據 寄存器 里的 中斷號,去 中斷向量表 中查表,找到對應硬件的方法,執行它的讀取方法就行了
這樣 CPU 就知道是 鍵盤 發出的信號,然后就會去調用 鍵盤 的執行方法,通過鍵盤的讀取方法,讀取到 ctrl + c 這個信息,轉化后,就是 2 號信號,執行終止前臺進程的動作
鍵盤被按下 和 鍵盤哪些位置被按下 是不一樣的
首先鍵盤先按下,CPU 確定對應的讀取方法
其次才是通過 讀取方法 從鍵盤中讀取數據
注:鍵盤讀取方法如何進行讀取,這是驅動的事,我們不用關心
硬件中斷 的流程與 進程信號 的流程雷同,同樣是 先檢測到信號,然后再去執行相應的動作,不過此時發送的是 中斷信號,執行的是 調用相應方法罷了
信號 與 動作 的設計方式很實用,操作系統只需要關注是否有信號發出,發出后去中斷向量表中調用相應的方法即可,不用管硬件是什么樣、如何變化,做到了 操作系統 與 硬件 間的解耦
3、系統調用
除了可以通過?鍵盤鍵入?發送信號外,還可以通過直接調用?系統接口?發送信號,畢竟?bash
?也是一個進程,本質上就是在進行程序替換而已
3.1、kill 函數
信號的發送主要是通過?kill
?函數進行發送
返回值:成功返回?0
,失敗返回?-1
?并設置錯誤碼
參數1:待操作進程的?PID
參數2:待發送的信號
下面來簡單用一下(程序運行?5
?秒后,自己把自己殺死)
?
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;int main()
{int n = 1;while (true){cout << "我是一個進程,已經運行了 " << n << " 秒 PID: " << getpid() << endl;sleep(1);n++;if (n > 5)kill(getpid(), SIGKILL);}return 0;
}
kill
?函數當然也可以發送其他信號,這里就不一一展示了,其實命令行中的?kill
?命令就是對?kill
?函數的封裝,kill -信號編號 -PID
?其中的參數2、3不正是?kill
?函數所需要的參數嗎?所以我們可以嘗試自己搞一個?myKill
?命令
3.2、模擬實現 myKill
這里就直接利用?命令行參數?簡單實現了
#include <iostream>
#include <string>
#include <signal.h>using namespace std;void Usage(string proc)
{// 打印使用信息cout << "\tUsage: \n\t";cout << proc << " 信號編號 目標進程" << endl;exit(2);
}int main(int argc, char *argv[])
{// 參數個數要嚴格限制if (argc != 3){Usage(argv[0]);}//獲取兩個參數int signo = atoi(argv[1]);int pid = atoi(argv[2]);//執行信號發送kill(pid, signo);return 0;
}
下面隨便跑一個進程,然后用自己寫的?myKill
?命令給進程發信號
我們可以把這個程序改造下,改成進程替換的方式,讓后將自己寫的命令進行安裝,就能像?kill
?一樣直接使用了
3.3、raise 函數
發送信號的還有一個?raise
?函數,這個函數比較奇怪,只能?自己給自己發信號
返回值:成功返回?0
,失敗返回?非0
就只有一個參數:待發送的信號
可以這樣理解:raise
?是對?kill
?函數的封裝,每次傳遞的都是自己的?PID
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;int main()
{int n = 1;while (true){cout << "我是一個進程,已經運行了 " << n << " 秒 PID: " << getpid() << endl;sleep(1);n++;if (n > 5)raise(SIGKILL); //自己殺死自己 }return 0;
}
3.4、abort 函數
abort
?是?C
?語言提供的一個函數,它的作用是?給自己發送?6
?號?SIGABRT
?信號
沒有返回值,也沒有參數
值得一提的是,abort
?函數即使在修改執行動作后,最后仍然會發送?6
?號信號
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;void handler(int signo)
{cout << "收到了 " << signo << " 號信號,已執行新動作" << endl;
}int main()
{signal(6, handler);// signal(SIGABRT, handler); //這種寫法也是可以的int n = 1;while (true){cout << "我是一個進程,已經運行了 " << n << " 秒 PID: " << getpid() << endl;sleep(1);n++;if (n > 5)abort();}return 0;
}
即使執行了我們新注冊的方法,abort 最后仍然會發出 6 號信號終止進程
同樣是終止進程,C語言 還提供了一個更好用的函數:exit(),所以 abort 用的比較少,了解即可
總的來說,系統調用中舉例的這三個函數關系是:kill 包含 raise,raise 包含 abort,作用范圍是在逐漸縮小的
4、軟件條件信號產生(發送)的第三種方式:軟件條件
?
其實這種方式我們之前就接觸過了:管道讀寫時,如果讀端關閉,那么操作系統會發送信號終止寫端,這個就是 軟件條件 引發的信號發送,發出的是 13 號 SIGPIPE 信號
4.1、alarm 設置鬧鐘
系統為我們提供了 鬧鐘(報警):alarm,這個 鬧鐘 可不是用來起床的,而是用來 定時 的
返回值:如果上一個鬧鐘還有剩余時間,則返回剩余時間,否則返回?0
參數:想要設定的時間,單位是秒
當時間到達鬧鐘中的預設時間時,鬧鐘會響,并且發送?14
?號?SIGALRM
?信號
比如這樣:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;int main()
{alarm(5); //設定一個五秒后的鬧鐘int n = 1;while (true){cout << "我是一個進程,已經運行了 " << n << " 秒 PID: " << getpid() << endl;sleep(1);n++;}return 0;
}
?
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;void handler(int signo)
{cout << "收到了 " << signo << " 號信號,已執行新動作" << endl;int n = alarm(10);cout << "上一個鬧鐘剩余時間: " << n << endl;
}int main()
{signal(SIGALRM, handler);alarm(10); //設定一個十秒后的鬧鐘while(true){cout << "我是一個進程,我正在運行…… PID: " << getpid() << endl;sleep(1);};return 0;
}
系統中不止一個鬧鐘,所以?OS
?需要?先描述,再組織,將這些鬧鐘管理起來
可以借助鬧鐘,簡單測試一下當前服務器的算力
?
5、硬件異常
最后一種產生(發送)信號的方式是:硬件異常
所謂?硬件異常?其實就是我們在寫程序最常遇到的各種報錯,比如?除 0、野指針
5.1、除 0 導致異常
先來看一段簡單的錯誤代碼
#include <iostream>
using namespace std;int main()
{int n = 10;n /= 0;return 0;
}
顯然是會報錯的是,畢竟?0
?不能作為常數?
?
根據報錯信息,可以推測出此時發送的是?8
?號?SIGFPE
?信號(浮點異常)
讓我們通過?signal
?更改?8
?號信號的執行動作,嘗試逆天改命,讓?除 0 合法?
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;void handler(int signo)
{cout << "雖然除 0 了,但我不終止進程" << endl;
}int main()
{signal(SIGFPE, handler);int n = 10;n /= 0;return 0;
}
結果:一直在死循環似的發送信號,明明只發生了一次 除 0 行為
想要明白背后的原理,需要先認識一下?狀態寄存器
5.2、狀態寄存器
在 CPU 中,存在很多 寄存器,其中大部分主要用來存儲數據信息,用于運算,除此之外,還存在一種特殊的 寄存器 =》 狀態寄存器,這個 寄存器 專門用來檢測當前進程是否出現錯誤行為,如果有,就會把 狀態寄存器(位圖結構)中對應的比特位置 1,意味著出現了 異常
當操作系統檢測到 狀態寄存器 出現異常時,會根據其中的值,向出現異常的進程 輪詢式 的發送信號,目的就是讓進程退出
比如上面的 除 0 代碼,發生異常后,CPU 將 狀態寄存器 修改,變成 異常狀態,操作系統檢測到 異常 后會向進程發送 8 號信號,即使我們修改了 8 號信號的執行動作,但 因為狀態寄存器仍然處于異常狀態,所以操作系統才會不斷發送 8 號信號,所以才會死循環式的打印
能讓?狀態寄存器?變為?異常?的都不是小問題,需要立即終止進程,然后尋找、解決問題
畢竟如果讓?除 0?變為合法,那最終的結果是多少呢?所以操作系統才會不斷發送信號,目的就是?終止進程的運行
5.3、野指針導致異常
除了?除 0?異常外,還有一個?臭名昭著?的異常:野指針問題
?
#include <iostream>
using namespace std;int main()
{int* ptr = nullptr;*ptr = 10;return 0;
}
?
Segmentation fault
?段錯誤?這是每個?C/C++
?程序猿都會遇到的問題,因為太容易觸發了,出現段錯誤問題時,操作系統會發送?11
號?SIGSEGV
?信號終止進程,可以通過修改執行動作驗證,這里不再演示
那么?野指針?問題是如何引發的呢?
借用一下?共享內存?中的圖~
野指針問題主要分為兩類:
- 指向不該指向的空間
- 權限不匹配,比如只讀的區域,偏要去寫
共識:在執行 *ptr = 10 這句代碼時,首先會進行 虛擬地址 -> 真實(物理)地址 之間的轉換
指向不該指向的空間:這很好理解,就是頁表沒有將 這塊虛擬地址空間 與 真實(物理)地址空間 建立映射關系,此時進行訪問時 MMU 識別到異常,于是 MMU 直接報錯,操作系統識別到 MMU 異常后,向對應的進程發出終止信號
權限不匹配:頁表中除了保存映射關系外,還會保存該區域的權限情況,比如 是否命中 / RW 等權限,當發生操作與權限不匹配時,比如 nullptr 只允許讀取,并不允許其他行為,此時解引用就會觸發 MMU 異常,操作系統識別到后,同樣會對對應的進程發出終止信號
頁表中的屬性
- 是否命中
- RW 權限
- UK 權限(不必關心
?
注:MMU 是內存管理單元,主要負責 虛擬地址 與 物理地址 間的轉換工作,同時還會識別各種異常行為
一旦引發硬件層面的問題,操作系統會直接發信號,立即終止進程
到目前為止,我們學習了很多信號,分別對應著不同的情況,其中有些信號還反映了異常信息,所以將信號進行細分,還是很有必要的
6.核心轉儲?
?對于某些信號來說,當終止進程后,需要進行?core dump
,產生核心轉儲文件
?比如:3號 SIGQUIT
、4號 SIGILL
、5號 SIGTRAP
、6號 SIGABRT
、7號 SIGBUS
、8號 SIGFPE
、11號 SIGSEGV
、24號 SIGXCPU
、25號 SIGXFSZ
、31號 SIGSYS
?都是可以產生核心轉儲文件的
不同信號的動作(
Action
)
Trem
?-> 單純終止進程Core
?-> 先發生核心轉儲,生成核心轉儲文件(前提是此功能已打開),再終止進程
但在前面的學習中,我們用過?3
、6
、8
、11
?號信號,都沒有發現?核心轉儲?文件啊
難道是我們的環境有問題嗎?
確實,當前環境確實有問題,因為它是?云服務器,而?云服務器?中默認是關閉核心轉儲功能的
6.2、打開與關閉核心轉儲
通過指令?ulimit -a
?查看當前系統中的資源限制情況
ulimit -a
可以看到,當前系統中的核心轉儲文件大小為?0
,即不生成核心轉儲文件
通過指令手動設置核心轉儲文件大小
ulimit -c 1024
?
現在可以生成核心轉儲文件了
就拿之前的?野指針?代碼測試,因為它發送的是?11
?號信號,會產生?core dump
?文件
?
?核心轉儲文件是很大的,而有很多信號都會產生核心轉儲文件,所以云服務器一般默認是關閉的
云服務器上是可以部署服務的,一般程序發生錯誤后,會立即重啟
如果打開了核心轉儲,一旦程序 不斷掛掉、又不斷重啟,那么必然會產生大量的核心轉儲文件,當文件足夠多時,磁盤被擠滿,導致系統 IO 異常,最終會導致整個服務器掛掉的
還有一個重要問題是 core 文件中可能包含用戶密碼等敏感信息,不安全
關閉核心轉儲很簡單,設置為?0
?就好了
ulimit -c 0
6.3、核心轉儲的作用
如此大的核心轉儲文件有什么用呢?
答案是?調試
沒錯,核心轉儲文件可以調試,并且直接從出錯的地方開始調試
這種調試方式叫做?事后調試
調試方法:
gcc / g++
?編譯時加上?-g
?生成可調試文件- 運行程序,生成?
core-dump
?文件 gdb 程序
?進入調試模式core-file core.file
?利用核心轉儲文件,快速定位至出錯的地方
?
之前在?進程創建、控制、等待?中,我們談到了?當進程異常退出時(被信號終止),不再設置退出碼,而是設置?core dump
?位 及 終止信號
也就是說,父進程可以借此判斷子進程是否產生了?核心轉儲?文件
?
?總結:
信號產生部分就到此,下一篇信號保存