1. 信號的概念
Linux下的信號機制是一種進程間通信(IPC)的方式,用于在不同進程之間傳遞信息。
信號是一種異步的信息傳遞方式,這意味著發送信號的進程只發送由信號作為載體的命令,而并不關心接收信號的進程如何處置這個命令(做不做、何時做、怎么做、結果如何等)。
這樣,在發送完命令之后,發送命令的進程就可以著手自己的其他任務,而無需等待對方反饋的結果(或者說無需與對方進行同步)。同時也就意味著接收信號的進程對于命令的處置是相對自由的,甚至可以直接忽略。
例如,菜鳥驛站給你發消息告訴你包裹到了(這就是一種信號),你可以去取,也可以不取(富哥不想要了,又懶得退款,或者單純忘了);你可以立即去取,也可以等幾個小時/幾天再取;你可以自己去取,也可以叫好友幫取,甚至叫個跑腿去取。
但有時候我們又希望自己發送的信號是絕對有效的,不可被輕視的。于是,我們可以將信號分為可靠信號和不可靠信號,一共62種(1~64,除開32和33):
不可靠信號:也稱為非實時信號,不支持排隊,信號可能會丟失。比如發送多次相同的信號,進程只能收到一次,信號值取值區間為1~31。
可靠信號:也稱為實時信號,支持排隊,信號不會丟失,發多少次,就可以收到多少次,信號值取值區間為34~64。
使用 [ kill -l ] 指令可以進行查看:?
2. 信號的處理
2.1 默認處理
每個信號都有默認的處理方式,如SIGINT
信號的默認處理是終止進程,SIGTERM
信號的默認處理也是終止進程,SIGSEGV
信號的默認處理是產生核心轉儲并終止進程。
man 7 signal
找到標題:Standard signals
- Core(核心轉儲并終止進程):如SIGABRT、SIGFPE信號,當這些信號發生時,系統會進行核心轉儲并且終止進程。
- Term(終止進程):像SIGALRM、SIGHUP信號,這些信號的默認動作是終止進程。
- Ign(忽略信號):對于SIGCHLD、SIGCLD信號,系統默認會忽略這些信號。
- Cont(繼續執行):對于SIGCONT信號,其默認動作是如果進程處于停止狀態則繼續執行。
- Stop(停止進程):對于SIGSTOP信號,它會使進程停止執行,且不能被捕獲、忽略或阻塞。
可以看到,大多數信號的默認處理方式都是終止進程。
2.2 捕獲信號
進程可以通過設置信號處理函數來捕獲信號并執行自定義的處理邏輯。例如,可以在信號處理函數中記錄日志、進行資源清理等操作。
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
功能
設置信號處理程序:
signal
函數用于設置一個函數來處理特定的信號。當指定的信號發生時,系統會調用相應的信號處理函數。
參數
sig:表示要處理的信號碼。常見的信號常量包括
SIGABRT
(程序異常終止)、SIGFPE
(算術運算出錯)、SIGILL
(非法指令)、SIGINT
(中斷信號,如Ctrl+C)、SIGSEGV
(非法訪問存儲器)、SIGTERM
(終止請求)等。func:是一個指向函數的指針,用于指定信號處理程序。可以是自定義的函數,也可以是以下預定義函數之一:
SIG_DFL:表示使用該信號的默認處理程序。
SIG_IGN:表示忽略該信號。
返回值
成功時:返回信號處理程序之前的值。
出錯時:返回
SIG_ERR
。
例如,我們編寫一個循環打印 hello world 的程序, 并讓他捕獲2號信號,即ctrl + c發出的信號:
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void SigHandler(int sigid)
{std::cout << "獲得信號: " << sigid << std::endl;exit(sigid);
}int main()
{signal(SIGINT, SigHandler);for(int i = 0; true; i++){std::cout << "Hello World[" << i << "]" << ", pid:[" << getpid() << "]" << std::endl;sleep(1);}return 0;
}
當我們在程序運行的過程中按下 ctrl + c 時,程序會顯示獲得信號:
2.3?忽略信號
進程可以通過設置信號處理函數為SIG_IGN
來忽略某個信號。但有些信號是不能被忽略的,如SIGKILL
和SIGSTOP
信號。
3. 信號的產生方式
用戶輸入:用戶在終端輸入特定的快捷鍵組合,如
Ctrl+C
會產生SIGINT
信號,Ctrl+\
會產生SIGQUIT
信號,Ctrl+Z
會產生SIGTSTP
信號。程序執行異常:當程序執行過程中出現錯誤或異常情況時,內核會發送相應的信號。例如,對一個數除0會產生
SIGFPE
信號,非法訪問一段內存會產生SIGBUS
信號,訪問未分配的虛擬內存會產生SIGSEGV
信號。進程間發送信號:一個進程可以通過系統調用向另一個進程發送信號。例如,使用
kill
函數可以向指定進程發送信號,raise
函數可以向本進程發送信號,sigqueue
函數可以向一個進程發送信號并傳遞額外數據。
用戶輸入方式我們在前面已經見識過,就不再多說。
3.1?程序執行異常
我們的程序在遇到運行時錯誤時會報錯并退出,這就是因為程序在發生執行異常時會引發硬件異常中斷,操作系統檢測到之后,就會向引發異常的進程發送一個信號,進而終止進程。
下面的代碼中,我們嘗試捕捉由除零引發的異常和使用野指針引發的異常:
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void SigHandler(int sigid)
{std::cout << "獲得信號: " << sigid << std::endl;exit(sigid);
}int main()
{for(int i = 1; i < 32; i++){signal(i, SigHandler);}// 8) SIGFPEint a = 10;a /= 0;// 11) SIGSEGV// int *p = nullptr;// *p = 100;return 0;
}
除零異常導致程序退出:
使用野指針導致程序退出:
3.2 進程間發送信號
3.2.1 kill命令
kill -[信號編號] [指定進程pid]
?在命令行使用kill指令可像指定進程發送指定信號。
3.2.2 kill函數
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
功能
kill
函數用于向指定的進程或進程組發送信號,以實現進程間的通知或控制。
參數
pid:表示要發送信號的目標進程或進程組的標識符。
pid > 0:將信號發送給進程標識為
pid
的進程。pid = 0:將信號發送給與調用
kill
函數的進程屬于同一進程組的所有進程。pid = -1:將信號發送給除了進程1(
init
進程)和調用者自身以外的所有進程。pid < -1:將信號發送給進程組ID等于pid絕對值的所有進程。
sig:表示要發送的信號。可以是以下常見信號常量之一
返回值
成功時:返回0。
出錯時:返回-1,并設置
errno
以指示錯誤原因。
?3.2.3 raise函數
#include <signal.h>int raise(int sig);
功能:向調用該函數的進程發送一個信號(即向自己發送信號)。
參數:
sig
表示要發送的信號編號。返回值:成功時返回0;失敗時返回非0值。
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void SigHandler(int sigid)
{std::cout << "獲得信號: " << sigid << std::endl;exit(sigid);
}int main()
{for(int i = 1; i < 32; i++){signal(i, SigHandler);}for(int i = 1; i < 32; i++){sleep(1);// 給自己發信號if(i == 9 || i == 19) // 9-SIGKILL 19-SIGSTOP不能被自定義捕獲 continue;raise(i);}return 0;
}
?9號信號SIGKILL和19號信號SIGSTOP無法被自定義捕獲,這是為了防止出現所有信號都被捕獲而無法通過信號使目標進程被強制停止的情況。
3.2.4 abort函數
#include <stdlib.h>void abort(void);
功能:abort
函數用于立即終止當前程序的執行,通常在檢測到不可恢復的錯誤時使用。
特點:
不執行清理工作:
abort
函數不會執行任何atexit
注冊的函數或對象析構函數,也不會刷新流緩沖區或關閉打開的文件等常規清理操作。產生核心轉儲文件:在某些系統上,如果系統配置允許,
abort
函數會產生一個核心轉儲文件,用于調試程序異常終止的原因。發送信號:
abort
函數會向調用進程發送SIGABRT
信號,進程不應忽略此信號。
3.2.5 sigqueue函數
功能:向指定的進程發送特定的信號,并可以傳遞一個額外的數據值,提供了比kill函數更豐富的功能,可用于進程間的高級通信。
暫時不做介紹。
4. 補充:前后臺進程
在大多數用戶交互式操作系統當中,都會把進程分為前臺進程和后臺進程,在Linux中二者的概念如下:
前臺進程:是與用戶直接交互的進程,占有控制終端,可以從終端接收輸入并向終端發送輸出。在任何時刻,只有一個進程組可以在前臺運行。
后臺進程:是不與用戶直接交互的進程,在后臺默默運行,不占用控制終端。
用戶在命令行啟動可執行程序時,該可執行程序默認以前臺方式運行,由于前臺進程只有一個,所以操作系統會將bash切換到后臺運行,待可執行程序運行結束或者被切換到后臺時,再將bash切換回前臺。
如果用戶希望進程被啟動后在后臺運行,可在其后跟上 & :
可以看到,我們讓SigTest運行起來之后,bash和SigTest都在往標準輸出打印,當我們輸入ls指令之后,發現處于前臺的bash獲得了我們的輸入,并運行了ls。
此時,如果我們想要通過[ctrl + c]的方式來結束SigTest是行不通的,因為只有前臺進程能接收到我們的輸入。此時我們有兩種做法:
- 使用kill -9指定終止SigTest。
- 將SigTest放到前臺,再使用[ctrl + c]。
前者不再進行說明了,接下來我們講解一下如何在命令行控制進程的前后臺切換。
在Linux中,與前后臺相關的指令主要用于管理進程的運行狀態,以下是一些常用的指令:
后臺運行指令
- &:在命令末尾添加“&”符號,可以使程序在后臺運行,例如“./matmul &”將運行一個名為matmul的程序并使其在后臺運行,這樣用戶就可以在前臺繼續執行其他命令。
- nohup:nohup命令用于在后臺運行命令,即使終端關閉或用戶退出,命令也會繼續執行。例如,“nohup ping 101.lug.ustc.edu.cn &”將在后臺運行ping命令,并將輸出重定向到nohup.out文件中。
前后臺切換指令
- Ctrl+Z:將當前正在前臺執行的命令或進程暫停,并放入后臺。例如,當一個命令在前臺運行時,按下Ctrl+Z,該命令將被暫停并放入后臺,屏幕上會顯示相關的狀態信息。
- jobs:用于查看當前在后臺運行的進程或任務的列表,以及它們的狀態和作業號。例如,“jobs -l”將顯示詳細的后臺進程信息,包括進程ID、狀態等。
- fg:將后臺中的進程調至前臺繼續運行。例如,“fg %1”將把后臺中作業號為1的進程調至前臺運行。
- bg:將后臺中暫停的進程繼續在后臺運行。例如,“bg %2”將使后臺中作業號為2的進程在后臺繼續運行。
其他相關指令
- ps:用于查看當前系統中正在運行的進程的狀態和信息。例如,“ps aux”將顯示所有用戶的所有進程的詳細信息。
- kill:用于向進程發送信號,通常用于終止進程。例如,“kill -9 PID”將強制終止指定PID的進程。
- top:用于實時查看系統中各個進程的資源占用情況,包括CPU、內存等。
- htop:是top命令的增強版,提供了更直觀和交互性更強的界面,用于查看和管理進程。
?