Linux系列
文章目錄
- Linux系列
- 前言
- 一、背景知識鋪墊
- 1.1 信號的基本概念
- 1.2 進程對信號的處理
- 二、信號的產生
- 2.1 前臺進程和后臺進程
- 2.2 鍵盤組合鍵
- 2.3 kill 命令
- 2.4 系統調用
- 2.4.1 signal()接口
- 2.4.2 kill()接口
- 2.4.3 raise()接口
- 2.4.4 abort()接口
- 總結
前言
Linux中,信號(Signal)是一種進程間通信(IPC)的機制,它用來通知進程發生了特定的事件。進程接受到信號后會根據信號的類型結合自己的處理方式做出相應的處理。
一、背景知識鋪墊
1.1 信號的基本概念
Linux信號是一種異步通信機制,用于在進程之間傳遞事件或在系統于進程之間進行交互。當發生某個特定事件時,如:用戶輸入特定組合建(Ctrl+c等)、進程異常終止,系統就會向相關進程發送信號。
1.2 進程對信號的處理
進程在被設計時,就內置了識別信號的方法以及默認處理不同信號方式,當進程接收到信號時,并不一定會立即處理,這也就要求進程需要具有保存信號的能力,當等到合適的時候,進程會根據信號的類型結合自己的處理方式法,做出處理。
進程在處理信號的方式:
- 默認處理方式(進程內置的)
- 忽略信號
- 自定義處理方式(捕捉信號后,使用用戶設定的方法)
二、信號的產生
穿插了一部分拓展知識
2.1 前臺進程和后臺進程
首先我們先來看下面這個程序:
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{while(true){cout<<"I'm a crazy process,PID:"<<getpid()<<endl;sleep(1);}return 0;
}
當我們執行該程序后,再輸入ls、pwd
,可以看到指令并不會執行,進程則一直運行,當我們使用Ctrl+c
就可以將進程終止,這樣的進程就是前臺進程。再次執行該程序:
這次我們以后臺進程的形式執行該進程./可執行程序 &
,可以發現,當進程執行后,我們再輸入指令,此時指令是可以成功執行的,當我們使用Ctrl+c
時無法終止進程。這種進程為后臺進程。
Linux中,一次登錄,一個終端,一般配有一個bash
,而每個終端只允許有一個前臺進程,可有多個后臺進程,當我們執行./process
時,前臺進程就由bash
變為了./process
而鍵盤輸入是優先被前臺進程獲取的,所以指令無法被執行,但前臺進程./peocess
接收到Ctrl+c
信號時就會終止。這樣我們再來理解第二個現象,當我們以后臺進程運行./process
是,此時bash
仍被視為前臺進程,當用戶輸入指令是仍可被接收并執行,此時再輸入Ctrl+c
信號./process
進程并沒有接收,所以沒有終止。
前臺進程:會獨占終端,直到進程執行完成或者被掛起,在這期間終端無法接受其他命令輸入,用戶只能與該進程進行交互。
后臺進程:不會占用終端,終端可以繼續接受用戶輸入的其他命令,用戶可以在同一個終端中同時啟動多個后臺進程。
前臺進程:其執行過程會受到用戶操作的直接影響,比如用戶可以通過鍵盤輸入來中斷或暫停進程。如果終端關閉,前臺進程通常會被終止,除非進行了特殊的設置。
后臺進程:通常是長時間運行的,不受終端關閉的影響,除非明確地對其進行停止或重啟操作。它按照自身的邏輯和任務需求在后臺持續運行,不會因為用戶的一些常規操作而中斷
Ctrl+c本質會解釋為2號信號,后面我們會驗證
2.2 鍵盤組合鍵
這是我們在學習Linux過程中,比較常用的一種向進程發送信號的方式,它通過一些特定的鍵盤組合鍵,來發送一些特殊的信號,如Ctrl+c
終止進程。組合有很多種,都比較簡單,這里我們想要介紹的是,Ctrl+c
這類組合鍵是如何被轉換為信號,又是如何被進程接收的?
我們可以確定的意見事是,CPU
不能直接從鍵盤讀取數據(馮諾依曼體系結構),那么這個工作只能交由操作系統來完成,操作系統又是如何得知鍵盤有數據了呢?我們根據下圖來回答:
CPU
上有很多針腳,每個針腳對應一個硬件設備(鍵盤、網卡),當用戶按下Ctrl+c
組合鍵時,鍵盤發生硬件中斷,產生中斷號,通過對于針腳發送(充放電)給CPU
,通知CPU
進行相關處理,操作系統從CPU
讀取到中斷號,通過中斷號在自己的中斷向量表中索引到對應方法地址,執行該方法(讀取鍵盤),操作系統識別如果是數據直接讀取,如果是組合鍵就將他解釋成對應信號,如ctrl+c
解釋為2
號信號,并將它讀入鍵盤緩沖區(一切皆文件),再拷貝至用戶緩沖區,被進程接收,進程執行對應處理方法。我們的信號處理方式(異步通知、事件驅動等)就是模擬的硬件中斷,因此信號又被叫做軟件中斷。
2.3 kill 命令
對于上面的后臺進程,我們可以通過kill
指令的形式,給它發送信號,終止它,可以通過kill -l
查看信號種類:
0~31
為普通信號,34~64
為實時信號(我們不研究),這里有多種信號來都能達到終止,后臺進程的要求,如:2
號終止進程,9號
殺掉進程。
使用格式:kill -信號編號 進程PID
.
常用信號
信號編號 | 信號名稱 | 觸發方式 | 作用 |
---|---|---|---|
2 | SIGINT | Ctrl+C | 終止前臺進程 |
3 | SIGQUIT | Ctrl+\ | 終止進程并生成core文件 |
9 | SIGKILL | kill -9 pid | 強制終止進程,不可被捕獲或忽略 |
15 | SIGTERM | kill -15 pid | 正常終止進程,進程可捕獲并進行清理 |
1 | SIGHUP | 終端連接斷開等 | 讓進程重新初始化或終止 |
18 | SIGCONT | kill -18 pid | 繼續執行暫停的進程 |
19 | SIGSTOP | kill -19 pid | 暫停進程,不可被捕獲或忽略 |
20 | SIGTSTP | Ctrl+Z | 暫停前臺進程,將其放入后臺 |
10 | SIGUSR1 | kill -10 pid | 用戶自定義信號,用于特定程序邏輯 |
12 | SIGUSR2 | kill -12 pid | 用戶自定義信號,用于特定程序邏輯 |
2.4 系統調用
2.4.1 signal()接口
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:捕捉指定信號,執行自定義功能
參數
- signum: 要捕捉信號編號
- handler:函數指針,用互自定義的方法
下面的程序我們使用signal
函數捕捉二號信號,執行我們自定義的方法。
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;void handler(int num)
{cout<<"I captured Signal No."<<num<<endl;return ;
}
int main()
{sighandler_t _handler=signal(2,handler);while(true){cout<<"I'm a crazy process,PID:"<<getpid()<<endl;sleep(1);}return 0;
}
可以看到此時我們再使用Ctrl+c
,信號程序就不會終止,而是執行我們的自定義方法。上面的場景也完美的呈現了,信號的異步性(程序先調用singnal
,但是此時進程沒有收到信號,所以這個函數不會執行,當進程接收到信號后,就會執行對于方法)。
需要注意的是,并不是所有信號,都可以被捕捉的,我們可以通過下面的方式來驗證:
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;void handler(int num)
{cout<<"I captured Signal No."<<num<<endl;return ;
}
int main()
{for(int i=0;i<=64;i++)sighandler_t _handler=signal(i,handler);while(true){cout<<"I'm a crazy process,PID:"<<getpid()<<endl;sleep(1);}return 0;
}
執行這個進程時,通過kill
指令向進程發信號,這里不方便演示,大家可以自己嘗試(9號和19號好像不能被捕捉)。
2.4.2 kill()接口
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:向指定進程發送信號
參數
- pid:接收信號的進程PID
- sig:信號編號
返回值
執行成功返回零,失敗飯回-1
并設置errno
。
示例:
#include<iostream>
#include<signal.h>
#include<sys/types.h>
using namespace std;
int main(int argc,char*argv[])
{if(argc!=3){cout << "Usage:\n\t" << argv[0] << " signum pid\n\n";exit(1);}int signum=stoi(argv[1]);int pid=stoi(argv[2]);int n=kill(pid,signum);if(n==-1){perror("kill");exit(1);}return 0;
}
現在我們就可以使用上面這個進程,來對其他進程發送信號了。
當然你可以讓你的進程直接使用kill
自己給自己發信號。
2.4.3 raise()接口
#include <signal.h>
int raise(int sig);
功能:讓當前進程給自己發送信號
參數
- sig:信號編號
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;void handler(int num)
{cout<<"I captured Signal No."<<num<<endl;return ;
}
int main()
{sighandler_t _handler=signal(2,handler);int cnt=5;while(true){cout<<"I'm a crazy process,PID:"<<getpid()<<endl;cnt--;if(cnt==0)break;//跳出循環sleep(1);}raise(9);cout<<"111111"<<endl;return 0;
循環執行五次后跳出,執行raise()
完成“自殺”,程序終止,我們可以感受到raise()
其實就是分裝的kill()
。
2.4.4 abort()接口
#include <stdlib.h>
void abort(void);
功能:向當前進程發送SIGABRT
(6號)信號,默認情況下,進程收到該信號后會立即終止,即使被用戶捕獲,在執行過用戶提供的方法后依然終止進程。
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;void handler(int num)
{cout<<"I captured Signal No."<<num<<endl;return ;
}
int main()
{sighandler_t _handler=signal(6,handler);while(true){cout<<"I'm a crazy process,PID:"<<getpid()<<endl;abort();}return 0;
}
如果不調用該函數,使用指令發送六號信號,當信號被捕獲后,進程不會終止,
abrot
函數內部做了特殊處理,才會使進程終止,你可以測試看看。
總結
特點:
異步傳遞(隨時可能中斷進程)
- SIGINT(2):由鍵盤輸入Ctrl+C產生,用于中斷正在運行的進程。
- SIGKILL(9):強制終止進程,不能被捕獲和忽略,用于緊急情況下終止進程。
- SIGSTOP(19):暫停進程,不能被捕獲和忽略,可使用SIGCONT信號恢復進程運行
不可捕獲的信號:
SIGKILL
(9)和SIGSTOP
(19)無法被捕獲、阻塞或忽略,用于強制控制進程。
信號發送函數
- kill函數:可以向指定進程發送指定信號,例如 kill(pid, SIGINT) 向進程號為 pid 的進程發送 SIGINT 信號。
- raise函數:用于向當前進程發送信號,如 raise(SIGABRT) 向當前進程發送 SIGABRT 信號。
處理方式:
- 默認處理:系統為每個信號定義了默認的處理行為,如終止進程、產生核心轉儲、忽略信號等。
- 捕獲信號:進程可以通過 signal 函數當接收到指定信號時,執行自定義的處理邏輯。
- 忽略信號:進程可以使用 signal 函數將信號設置為忽略,使進程不響應該信號,但有些信號(如 SIGKILL 和 SIGSTOP )不能被忽略(這點我在下篇介紹)。