訊號

空中北斗星發表於2020-12-27

訊號的概念

訊號在我們的生活中隨處可見, 如:古代戰爭中摔杯為號;現代戰爭中的訊號彈;體育比賽中使用的訊號槍…
他們都有共性: 1. 簡單 2. 不能攜帶大量資訊 3. 滿足某個特設條件才傳送。

訊號是資訊的載體, Linux/UNIX 環境下,古老、經典的通訊方式, 現下依然是主要的通訊手段。

Unix 早期版本就提供了訊號機制,但不可靠,訊號可能丟失。Berkeley 和 AT&T 都對訊號模型做了更改,增加了可靠訊號機制。但彼此不相容。 POSIX.1 對可靠訊號例程進行了標準化

訊號的機制

A 給 B 傳送訊號, B 收到訊號之前執行自己的程式碼,收到訊號後,不管執行到程式的什麼位置,都要暫停執行,去處理訊號,處理完畢再繼續執行。與硬體中斷類似——非同步模式。但訊號是軟體層面上實現的中斷,早期常被稱為“軟中斷”。

訊號的特質:由於訊號是通過軟體方法實現,其實現手段導致訊號有很強的延時性。但對於使用者來說,這個延遲時間非常短,不易察覺。

每個程式收到的所有訊號,都是由核心負責傳送的,核心處理。

與訊號相關的事件和狀態

產生訊號

  1. 按鍵產生,如: Ctrl+c、 Ctrl+z、 Ctrl+\
  2. 系統呼叫產生,如: kill、 raise、 abort
  3. 軟體條件產生,如:定時器 alarm
  4. 硬體異常產生,如:非法訪問記憶體(段錯誤)、除 0(浮點數例外)、記憶體對齊出錯(匯流排錯誤)
  5. 命令產生,如: kill 命令

遞達

遞送並且到達程式。(我們認為當訊號被遞達即算作被處理)

未決

產生和遞達之間的狀態。主要由於阻塞(遮蔽)導致該狀態。

訊號的處理方法

  1. 執行預設動作
  2. 忽略(丟棄)
  3. 捕捉(呼叫戶處理函式)

Linux 核心的程式控制塊 PCB 是一個結構體, task_struct, 除了包含程式 id,狀態,工作目錄,使用者 id,組 id,檔案描述符表,還包含了訊號相關的資訊,主要指阻塞訊號集未決訊號集

阻塞訊號集(訊號遮蔽字)

將某些訊號加入集合,對他們設定遮蔽,當遮蔽 x 訊號後,再收到該訊號,該訊號的處理將推後(解除遮蔽後)

未決訊號集

  1. 訊號產生,未決訊號集中描述該訊號的位立刻翻轉為 1,表訊號處於未決狀態。當訊號被處理對應位翻轉回為 0。這一時刻往往非常短暫。
  2. 訊號產生後由於某些原因(主要是阻塞)不能抵達。這類訊號的集合稱之為未決訊號集。在遮蔽解除前,訊號一直處於未決狀態。

訊號的編號

可以使用 kill –l 命令檢視當前系統可使用的訊號有哪些。
kill-l
不存在編號為 0 的訊號。其中 1-31 號訊號稱之為常規訊號(也叫普通訊號或標準訊號), 34-64 稱之為實時訊號,驅動程式設計與硬體相關。名字上區別不大。而前 32 個名字各不相同。

訊號 4 要素

與變數三要素類似的,每個訊號也有其必備 4 要素,分別是:
1.編號 2. 名稱 3. 事件 4. 預設處理動作

可通過 man 7 signal 檢視幫助文件獲取。
signal

在標準訊號中,有一些訊號是有三個“值”,第一個值通常對 alpha 和 sparc 架構有效,中間值針對 x86、 arm和其他架構,最後一個應用於 mips 架構。一個‘-’表示在對應架構上尚未定義該訊號。

不同的作業系統定義了不同的系統訊號。因此有些訊號出現在 Unix 系統內,也出現在 Linux 中,而有的訊號出現在 FreeBSD 或 Mac OS 中卻沒有出現在 Linux 下。這裡我們只研究 Linux 系統中的訊號。

特別強調: 9) SIGKILL 和 19) SIGSTOP 訊號,不允許忽略和捕捉,只能執行預設動作。甚至不能將其設定為阻塞。

Linux 常規訊號一覽表

編號名稱事件預設動作
1SIGHUP當使用者退出 shell 時,由該 shell 啟動的所有程式將收到這個訊號終止程式
2SIGINT當使用者按下了<Ctrl+C>組合鍵時,使用者終端向正在執行中的由該終端啟動的程式發出此訊號終止程式
3SIGQUIT當使用者按下<ctrl+\>組合鍵時產生該訊號,使用者終端向正在執行中的由該終端啟動的程式發出些訊號終止程式
4SIGILLCPU 檢測到某程式執行了非法指令終止程式,併產生 core 檔案
5SIGTRAP該訊號由斷點指令或其他 trap 指令產生終止程式,併產生core檔案
6SIGABRT呼叫 abort 函式時產生該訊號終止程式,併產生 core 檔案
7SIGBUS非法訪問記憶體地址,包括記憶體對齊出錯終止程式,併產生core檔案
8SIGFPE在發生致命的運算錯誤時發出(不僅包括浮點運算錯誤,還包括溢位及除數為 0 等所有的演算法錯誤)終止程式,併產生core檔案
9SIGKILL無條件終止程式。(本訊號不能被忽略,處理和阻塞終止程式
10SIGUSR1使用者定義 的訊號(即程式設計師可以在程式中定義並使用該訊號)終止程式
11SIGSEGV指示程式進行了無效記憶體訪問終止程式,併產生core檔案
12SIGUSR2使用者自定義訊號(即程式設計師可以在程式中定義並使用該訊號)終止程式
13SIGPIPEBroken pipe 向一個沒有讀端的管道寫資料終止程式
14SIGALRM定時器超時(超時的時間由系統呼叫 alarm 設定)終止程式
15SIGTERM程式結束訊號(與 SIGKILL 不同的是,該訊號可以被阻塞和終止。通常用來要示程式正常退出。執行 shell 命令 Kill 時,預設產生這個訊號)終止程式
16SIGSTKFLTLinux 早期版本出現的訊號,現仍保留向後相容終止程式
17SIGCHLD子程式狀態發生變化時,父程式會收到這個訊號忽略
18SIGCONT如果程式已停止,則使其繼續執行繼續/忽略
19SIGSTOP停止(暫停)程式的執行(訊號不能被忽略,處理和阻塞暫停程式
20SIGTSTP停止終端互動程式的執行。按下<ctrl+z>組合鍵時發出這個訊號暫停程式
21SIGTTIN後臺程式讀終端控制檯暫停程式
22SIGTTOU在後臺程式要向終端輸出資料時發生(該訊號類似於 SIGTTIN)暫停程式
23SIGURG套接字上有緊急資料時,向當前正在執行的程式發出些訊號,報告有緊急資料到達。(如網路帶外資料到達)忽略
24SIGXCPU程式執行時間超過了分配給該程式的 CPU 時間 ,系統產生該訊號併傳送給該程式終止程式
25SIGXFSZ超過檔案的最大長度設定終止程式
26SIGVTALRM虛擬時鐘超時時產生該訊號(類似於 SIGALRM,但是該訊號只計算該程式佔用 CPU 的使用時間)終止程式
27SGIPROF類似於 SIGVTALRM,它不公包括該程式佔用 CPU 時間還包括執行系統呼叫時間終止程式
28SIGWINCH視窗變化大小時發出忽略
29SIGIO此訊號向程式指示發出了一個非同步 IO 事件忽略
30SIGPWR關機忽略
31SIGSYS無效的系統呼叫終止程式,併產生core檔案
34) SIGRTMIN~ (64) SIGRTMAXLINUX 的實時訊號,它們沒有固定的含義(可以由使用者自定義)所有的實時訊號的預設動作都為終止程式

訊號的產生

終端按鍵產生訊號

Ctrl + c → (2) SIGINT(終止/中斷) “INT” ----Interrupt
Ctrl + z → (20) SIGTSTP(暫停/停止) “T” ----Terminal 終端。
Ctrl + \ → (3) SIGQUIT(退出)

硬體異常產生訊號

除 0 操作 → 8) SIGFPE (浮點數例外) “F” -----float 浮點數。
非法訪問記憶體 → 11) SIGSEGV (段錯誤)
匯流排錯誤 → 7) SIGBUS

kill 函式/命令產生訊號

kill 命令產生訊號:

kill -SIGKILL pid
kill -9 pid

kill 函式:給指定程式傳送指定訊號(不一定殺死)

int kill(pid_t pid, int sig); 
/*
成功: 0;
失敗: -1 (ID 非法,訊號非法,普通使用者殺 init 程式等權級問題),設定 errno

引數1:
sig:不推薦直接使用數字,應使用巨集名,因為不同作業系統訊號編號可能不同,但名稱一致。

引數2:
pid > 0: 傳送訊號給指定的程式。
pid = 0: 傳送訊號給 與呼叫 kill 函式程式屬於同一程式組的所有程式。
pid < -1: 取|pid|發給對應程式組。
pid = -1:傳送給程式有許可權傳送的系統中所有程式。
*/

程式組:每個程式都屬於一個程式組,程式組是一個或多個程式集合,他們相互關聯,共同完成一個實體任務,每個程式組都有一個程式組長,預設程式組 ID 與程式組長 ID 相同。

許可權保護: super 使用者(root)可以傳送訊號給任意使用者,普通使用者是不能向系統使用者傳送訊號的。 kill -9 (root 使用者的 pid) 是不可以的。同樣,普通使用者也不能向其他普通使用者傳送訊號,終止其程式。 只能向自己建立的程式傳送訊號。

普通使用者基本規則是:傳送者實際或有效使用者 ID == 接收者實際或有效使用者 ID

軟體條件產生訊號

alarm函式

例子:alarm.cpp

設定定時器(鬧鐘)。在指定seconds(秒)後,核心會給當前程式傳送
14)SIGALRM訊號。程式收到該訊號,預設動作終止。

每個程式都有且只有唯一一個定時器。

unsigned int alarm(unsigned int seconds);
/*
 * 返回0或者剩餘的秒數。無失敗。
 * 
 * 常用:alarm(0):取消定時器,返回舊鬧鐘剩餘秒數。
 */

例如:
alarm(5)[定時5s]->過去3秒->alarm(4)[重新定時4s][返回值為5-3=2]->過去2s->alarm(10)[重新定時10s]->alarm(0)[取消鬧鐘]

定時,與程式狀態無關(自然定時法)!就緒、執行、掛起(阻塞、暫停)、終止、殭屍…無論程式處於何種狀態,alarm 都計時。

time 命令k可以檢視程式執行的時間

time ./a.out

time
實際執行時間 = 系統時間 + 使用者時間 + 等待時間

程式執行的瓶頸在於 IO,優化程式,首選優化 IO。

setitimer 函式

例子: setitimer.cpp

設定定時器(鬧鐘)。 可代替 alarm 函式。精度微秒 us,可以實現週期定時

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value); 
/*
 * 成功: 0;失敗: -1,設定errno
 *  
 * 引數: 1. which:指定定時方式
 *			1) 自然定時: ITIMER_REAL → 14) SIGLARM    計算自然時間
 *			2) 虛擬空間計時(使用者空間): ITIMER_VIRTUAL → 26) SIGVTALRM 只計算程式佔用 cpu 的時間
 *			3) 執行時計時(使用者+核心): ITIMER_PROF → 27) SIGPROF 計算佔用 cpu 及執行系統呼叫的時間
 *		 
 *		 2. new_value
 *			1) it_interval   定時的時長
 *			2) it_value	 用來設定兩次定時任務之間間隔的時間
 *	        即it_value為第一次定時時長,it_interval為第一次之後的每一次定時時長,因此可以實現週期定時,相當於do...while迴圈。
 *			(兩個引數都設定為 0,即清 0 操作,取消定時)
 *	 
 *		 3. old_value
 *			傳出引數,返回舊鬧鐘剩餘時間。
 */

struct itimerval結構體
(在/usr/include下使用grep命令查詢)

struct itimerval
  {
    /* Value to put into `it_value' when the timer expires.  */
    struct timeval it_interval;
    /* Time to the next timer expiration.  */
    struct timeval it_value;
  }; 

struct timeval {
    __kernel_time_t     tv_sec;     /* seconds */
    __kernel_suseconds_t    tv_usec;    /* microseconds */
};

訊號集操作函式

核心通過讀取未決訊號集來判斷訊號是否應被處理。訊號遮蔽字 mask 可以影響未決訊號集。
我們可以在應用程式中通過改變自定義 set 來改變 mask。已達到遮蔽指定訊號的目的

mask

訊號集設定的函式

例子:signalset.cpp

// typedef unsigned long sigset_t
sigset_t set;			// 本質是點陣圖

// 將某個訊號集清0
// 0 成功: 0;失敗: -1
int sigemptyset(sigset_t *set);		// 將自定義訊號遮蔽字集全置0

// 將某個訊號集置1
// 成功: 0;失敗: -1
int sigfillset(sigset_t *set);		// 將自定義訊號遮蔽字集全置1

// 將某個訊號加入訊號集 
// 成功: 0;失敗: -1
int sigaddset(sigset_t *set, int signum);	// 參1:自定義訊號遮蔽字集, 參2:需要遮蔽的訊號(將此訊號所在位置1)

// 將某個訊號清出訊號集 
// 成功: 0;失敗: -1
int sigdelset(sigset_t *set, int signum);	// 參1:自定義訊號遮蔽字集, 參2:需要去除遮蔽的訊號(將此訊號所在位置0)

// 判斷某個訊號是否在訊號集中 
// 返回值:在集合: 1;不在: 0;出錯: -1
int sigismember(const sigset_t *set, int signum);

sigset_t 型別的本質是點陣圖。但不應該直接使用位操作,而應該使用上述函式,保證跨系統操作有效。

sigprocmask 函式

用來遮蔽訊號、 解除遮蔽也使用該函式。其本質,讀取或修改程式的訊號遮蔽字(PCB 中)

注意,遮蔽訊號:只是將訊號處理延後執行(延至解除遮蔽);而忽略表示將訊號丟處理。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 
/*
 * 成功: 0;失敗: -1,設定 errno
 *  
 * 引數:
 * 		參1:how (假設當前的訊號遮蔽字為 mask,自定義訊號遮蔽字集為set)
 * 			1.SIG_BLOC(遮蔽訊號), mask中原有的遮蔽訊號不變,對set中已有的訊號進行遮蔽,相當於 mask = mask | set
 * 			2.SIG_UNBLOCK(解除遮蔽), 對set中已有的訊號取消遮蔽,相當於 mask = mask & ~set
 * 			3.IG_SETMASK(替換), 表示用set 替代原始遮蔽mask,相當於 mask = set
 * 
 * 		參2:set,傳入引數,是一個點陣圖, set 中那一位置1,就表示當前程式遮蔽哪個訊號。
 *  	
 * 		參3:oldset:傳出引數,儲存舊的訊號遮蔽集。(在需要新的訊號遮蔽集結束後,記得將舊的訊號遮蔽集替換回來)
 */

sigpending 函式

取當前程式的未決訊號集

int sigpending(sigset_t *set); 
/*
 * set 傳出引數。 
 *  
 * 返回值:成功: 0;失敗: -1,設定 errno
 */

訊號捕捉

signal函式

例子: signal.cpp

註冊一個訊號捕捉函式,訊號捕捉實際由核心完成。

typedef void (*sighandler_t)(int);	// 返回值為void,只有一個引數int的函式指標

sighandler_t signal(int signum, sighandler_t handler);

該函式由 ANSI 定義,由於歷史原因在不同版本的 Unix 和不同版本的 Linux 中可能有不同的行為。 因此應該儘量避免使用它, 取而代之使用 sigaction 函式。

sigaction函式

例子:sigaction.cpp

修改訊號處理動作(通常在 Linux 用其來註冊一個訊號的捕捉函式)

int sigaction(int signum, const  *act, struct sigaction *oldact); 
/*
 * 成功: 0;失敗: -1,設定 errno
 * 
 * 引數:
 * 		參1:signum,需要捕捉的訊號(建議不直接使用數字,而使用巨集, 例如:2號訊號使用SIGING
 * 
 *		參2:act:傳入引數,新的處理方式。
 *	
 *		參3:oldact:傳出引數,舊的處理方式。
 */

struct sigaction結構體

struct sigaction {
	void     (*sa_handler)(int);
	void     (*sa_sigaction)(int, siginfo_t *, void *);
	sigset_t   sa_mask;
	int        sa_flags;
	void     (*sa_restorer)(void);
	};

/*
 * sa_restorer:該元素是過時的,不應該使用, POSIX.1 標準將不指定該元素。 (棄用)
 * sa_sigaction:當 sa_flags 被指定為 SA_SIGINFO 標誌時,使用該訊號處理程式。 (很少使用)
 *  
 * sa_handler:指定名(即註冊函式)。也可賦值為 SIG_IGN 表忽略 或 SIG_DFL 表執行預設動作
 * sa_mask: 呼叫訊號處理函式時,所要遮蔽的訊號集合(訊號遮蔽字)。注意:僅在處理函式被呼叫期間遮蔽生效,是臨時性設定。(若沒有需要新增的遮蔽訊號,通常使用sigempty(&act.sa_mask))
 * sa_flags:通常設定為 0,表使用預設屬性(捕捉到的訊號,在訊號捕捉後的處理函式中預設遮蔽) (設定預設屬性是為了不讓在處理函式中再次捕捉到此訊號再重新執行處理函式,造成死迴圈)
 */

sigaction函式訊號捕捉特性

  1. 程式正常執行時,預設 PCB 中有一個訊號遮蔽字,假定為☆,它決定了程式自動遮蔽哪些訊號。當註冊了
    某個訊號捕捉函式,捕捉到該訊號以後,要呼叫該函式。而該函式有可能執行很長時間,在這期間所遮蔽
    的訊號不由☆來指定。而是用 sa_mask 來指定。呼叫完訊號處理函式,再恢復為☆。
  2. XXX 訊號捕捉函式執行期間, XXX 訊號自動被遮蔽。(atc.sa_flags = 0, 使用預設屬性)
  3. 阻塞的常規訊號不支援排隊,產生多次只記錄一次。(後 32 個實時訊號支援排隊)

核心實現訊號捕捉過程

核心實現訊號捕捉過程

SIGCHLD訊號

SIGCHLE訊號產生的條件

子程式狀態傳送改變(子程式終止時、子程式接收到 SIGSTOP 訊號停止時、子程式處在停止態,接受到 SIGCONT 後喚醒時)

藉助 SIGCHLD 訊號回收子程式

例子:cathc_child.cpp

子程式結束執行, 其父程式會收到 SIGCHLD 訊號。 該訊號的預設處理動作是忽略。 可以捕捉該訊號, 在捕捉函式中完成子程式狀態的回收。

SIGCHLD 訊號注意問題

  1. 子程式繼承父程式的訊號遮蔽字和訊號處理動作,但子程式沒有繼承未決訊號集 spending。
  2. 注意註冊訊號捕捉函式的位置。
  3. 應該在 fork 之前,阻塞 SIGCHLD 訊號。註冊完捕捉函式後解除阻塞。

中斷系統呼叫

系統呼叫可分為兩類:慢速系統呼叫和其他系統呼叫

  1. 慢速系統呼叫:可能會使程式永遠阻塞的一類。如果在阻塞期間收到一個訊號,該系統呼叫就被中斷,不再繼續執行(早期);也可以設定系統呼叫是否重啟。如, read、 write、 pause(使呼叫程式掛起,直至捕捉到一個訊號)、 wait…
  2. 其他系統呼叫: getpid、 getppid、 fork…

可修改 sa_flags 引數來設定被訊號中斷後系統呼叫是否重啟。 SA_INTERRURT 不重啟。 SA_RESTART 重啟。

擴充套件瞭解

sa_flags 還有很多可選引數, 適用於不同情況。 如:捕捉到訊號後,在執行捕捉函式期間,不希望自動阻塞該訊號,可將 sa_flags 設定為 SA_NODEFER,除非 sa_mask 中包含該訊號。

相關文章