1. 程式人生 > >深入理解 Linux 核心---訊號

深入理解 Linux 核心---訊號

訊號的作用

訊號是很短的訊息,可以被髮送到一個程序或一組程序。
傳送給程序的唯一資訊通常是一個數,來標識訊號。

字首為 SIG 的一組巨集標識訊號。
如,當一個程序引用無效的記憶體時,SIGSEGV 巨集產生髮送給程序的訊號識別符號。

使用訊號的兩個目的:

  • 讓程序知道已經發生了一個特定的事件。
  • 強迫程序執行自己程式碼中的訊號處理程式。

除了一些常規訊號,POSIX 標準還引入了實時訊號,編碼範圍為 32~64。
不同於常規訊號,它們必須排隊,以便傳送的多個訊號都能被接收到。
而同種型別的常規訊號並不排隊:如果一個常規訊號被連續傳送多次,則只有其中一個傳送到接收程序。
Linux 核心不使用實時訊號,但通過幾個特定的系統呼叫實現了 POSIX 標準。

訊號的一個重要特點是它們可以隨時被髮送給狀態經常不可預知的程序。
傳送給非執行程序的訊號必須由核心儲存,直到程序恢復執行。
阻塞一個訊號會拖延訊號的傳遞,直到阻塞解除。

因此,核心區分訊號傳遞的兩個不同階段:

  • 訊號產生。核心更新目標程序的資料結構,以表示一個新訊號已經被髮送。
  • 訊號傳遞。核心強迫目標程序通過以下方式對訊號做出反應:或改變目標程序的執行狀態,或開始執行一個特定的訊號處理程式,或兩者都是。

每個產生的訊號之多被傳遞一次。
訊號是可消費資源:一旦已經傳遞出去,程序描述符中有關該訊號的所有資訊都被取消。

已經產生但還沒有傳遞的訊號被稱為掛起訊號。
任何時候,一個程序僅儲存特定型別的一個掛起訊號,同一程序同種型別的其他訊號不被排隊,只被簡單地丟棄。
但對於實時訊號,同種型別的掛起訊號可以有好幾個。

一般,訊號可以保留不可預知的掛起時間,必須考慮下列因素:

  • 訊號通常只被當前正在執行的程序(current)傳遞。
  • 給定型別的訊號可以由程序選擇性地阻塞。此時,在取消阻塞前程序將不接受該訊號。
  • 當程序執行一個訊號處理程式的函式時,通常“遮蔽”相應的訊號,即自動阻塞該訊號直到處理程式結束。
    因此,所處理的訊號另一次出現不能中斷訊號處理程式,所以訊號處理函式不必是可重入的。

訊號的核心實現比較複雜,核心必須:

  • 記住每個程序阻塞哪些訊號。
  • 當從核心態切換到使用者態時,對任何一個程序都要檢查是否有一個訊號已經到達。這幾乎在每個定時中斷時都發生。
  • 確定是否可忽略該訊號。發生在下列條件都滿足時:
    • 目標程序沒有被另一個程序跟蹤(程序描述符中 ptrace 欄位的 PT_PTRACED 的標誌等於 0)。
    • 訊號沒有被目標程序阻塞。
    • 訊號被目標程序忽略。
  • 處理這一訊號,即訊號可能在程序執行期的任意時刻請求把程序切換到一個訊號處理函式,並在這個函式返回後恢復原來執行的上下文。

此外,還需考慮相容性。

傳遞訊號之前所執行的操作

程序以三種方式對一個訊號做出應答:

  1. 顯式地忽略訊號。
  2. 執行與訊號相關的預設操作。由核心預定義的預設操作取決於訊號的型別:
  • Terminate,程序被終止
  • Dump,程序被終止,如果可能,建立包含程序執行上下文的核心轉儲檔案,該檔案可用於除錯。
  • Ignor,訊號被忽略。
  • Stop,程序被停止,即把程序設定為 TASK_STOPPED 狀態。
  • Continue,如果程序被停止,就把它設定為 TASK_RUNNING 狀態。
  1. 通過呼叫相應的訊號處理函式捕獲訊號。

對一個訊號的阻塞和忽略是不同的:
只要訊號被阻塞,就不被傳遞;只有在訊號解除阻塞後才傳遞。
而一個被忽略的訊號總是被傳遞,只是沒有進一步的操作。

SIGKILL 和 SIGSTOP 訊號不可被顯示忽略、捕獲或阻塞,因此,通常必須執行它們的預設操作。
因此,SIGKILL 和 SIGSTOP 分別允許具有適當特權的使用者終止、停止任何程序,不管程式執行時採取怎樣的防禦措施。

如果某個訊號的傳遞導致核心殺死一個程序,那麼該訊號對程序就是致命的。
致命的訊號包括:

  • SIGKILL 訊號
  • 預設操作為 Terminate 的每個訊號
  • 不被程序捕獲的訊號對於該程序是致命的

如果一個被程序捕獲的訊號,對應的訊號處理函式終止了該程序,那麼該訊號就不是致命的,因為程序自己選擇了終止,而不是被核心殺死。

POSIX 訊號和多執行緒應用

POSXI 1003.1 標準對多執行緒應用的訊號處理有一些嚴格的要求:

  • 訊號處理程式必須在多執行緒應用的所有執行緒之間共享;不過,每個執行緒必須有自己的掛起訊號掩碼和阻塞訊號掩碼。
  • POSIX 庫函式 kill() 和 sigqueue() 必須向所有的多執行緒應用而不是某個特殊的執行緒傳送訊號。所有由核心產生的訊號同樣如此。
  • 每個傳送給多執行緒應用的訊號僅傳送給一個執行緒,這個執行緒是由核心在從不阻塞該訊號的執行緒中隨意選擇出來的。
  • 如果向多執行緒應用傳送了一個致命的訊號,那麼核心將被殺死該應用的所有執行緒,而不僅僅是殺死接收訊號的那個執行緒。

為遵循 POSIX 標準,Linux 核心把多執行緒應用實現為一組屬於同一個執行緒組的輕量級程序。

如果一個掛起訊號被髮送給了某個特定程序,那麼該訊號是私有的;如果被髮送給了整個執行緒組,它就是共享的。

與訊號相關的資料結構

在這裡插入圖片描述

程序描述符中的 blocked 欄位存放程序當前所遮蔽的訊號。
它是一個 sigset_t 位陣列,每種訊號型別對應一個元素:

typedef struct
{
	unsigned long sig[2];  // 每個無符號長整數由 32 位組成
}sigset_t;

訊號的編號 = sigset_t 位陣列中相應位的下標 + 1。
1 ~ 31 之間的編號對應於常規訊號,32 ~ 64之間的編號對應於實時訊號。

訊號描述符和訊號處理程式描述符

程序描述符的 signal 欄位指向訊號描述符—一個 signal_struct 型別的結構,用來跟蹤共享掛起訊號。
訊號描述符還包括與訊號處理關係不密切的一些欄位,如

  • rlim,每程序的資源限制陣列
  • pgrp,程序的組領頭程序 PID
  • session,程序的會話領頭程序 PID
    在這裡插入圖片描述

訊號描述符被屬於同一執行緒組的所有程序共享,即被呼叫 clone() 系統呼叫(設定 CLONE_SIGHAND 標誌)建立的所有程序共享,因此,對屬於同一執行緒組的每個程序而言,訊號描述符中的欄位必須都是相同的。

每個程序還有訊號處理程式描述符,是一個 sighand_struct 型別的結構,用來描述每個訊號必須如何被執行緒組處理。

在這裡插入圖片描述

呼叫 clone() 時設定 CLONE_SIGHAND 標誌,訊號處理程式描述符就可以被幾個程序共享。

描述符的 count 欄位表示共享該結構的程序個數。
在一個 POSIX 的多執行緒應用中,執行緒組中的所有輕量級程序都應該用相同的訊號描述符和訊號處理程式描述符。

sigaction 資料結構

欄位:

  • sa_handler,指定執行操作的型別。它的值可以是指向訊號處理程式的一個指標,SIG_EFL,或 SIG_IGN。
  • sa_flags,標誌集,指定必須怎樣處理訊號。
  • sa_mask,型別為 sigset_t 的變數,指定當執行訊號處理程式時要遮蔽的訊號。

掛起訊號佇列

為了跟蹤當前的掛起訊號是什麼,核心把兩個掛起訊號佇列與每個程序關聯:

  • 共享掛起訊號佇列,位於訊號描述符的 shared_pending 欄位,存放這個執行緒組的掛起訊號。
  • 私有掛起訊號佇列,位於程序描述符的 pending 欄位,存放特定程序的掛起訊號。

掛起訊號佇列由 sigpending 資料結構組成,定義如下:

structural singpengding 
{
	struct list_head list; // 包含 sigqueue 資料結構的雙向連結串列的頭
	sigset_t signal;  // 指定掛起訊號的位掩碼
}

siginfo 是一個 128 位元組的資料結構,存放有關出現特定訊號的資訊,包含下列欄位:

  • si_signo,訊號編號
  • si_errno,引起訊號產生的指令的出錯碼,沒有錯誤則為 0
  • si_code,傳送訊號者的程式碼,如:SI_USER、SI_KERNEL、SI_QUEUE、SI_TIMER 等
  • _sifields,依賴於訊號型別的資訊的聯合體。

在訊號資料結構上的操作

下面的 set 是指向 sigset_t 型別變數的一個指標,nsig 是訊號的編號,mask 是無符號長整數的位掩碼。

  • sigemptyset(set) 和 sigfillset(set):把 set 中的位分別置為 0 或 1。

  • sigaddset(set, nsig) 和 sigdelset(set, nsig):把 nsig 訊號在 set 中對應的位分別置為 1 或 0。

    • sigaddset() 簡化為:
set->sig[(nsig-1) / 32] |= 1UL << ((nsig - 1) % 32);
    • sigdelset() 簡化為:
set->sig[(nsig-1) / 32] |= ~(1UL << ((nsig - 1) % 32));
  • sigaddsetmask(set, mask) 和 sigdelsetmask(set, mask):把 mask 中的位在 set 中對應的所有位分別設定為 1 或 0。僅用於編號為 1~32 之間的訊號,可分別簡化為:
set->sig[0] |= mask;
set->sig[0] |= ~mask;
  • sigismember(set, nsig):返回 nsig 訊號在 set 中對應的值。可簡化為:
return  1 & (set->sig[(nsig - 1) / 32] >> ((nsig - 1) % 32));
  • sigmask(nsig):產生 nsig 訊號的位索引。如果核心需要設定、清除或測試一個特定訊號在 sigset_t 型別變數中對應的位,可通過該巨集得到合適的位。

  • sigandsets(d, s1, s2)、sigoresets(d, s1, s2) 和 signandsets(d, s1, s2):
    在 sigset_t 型別的 s1 和 s2 變數之間分別執行邏輯“與”、邏輯“或”即邏輯“與非”。
    結果儲存在 d 指向的 sigset_t 型別的變數中。

  • sigtestsetmask(set, mask):如果 mask 在 set 中對應的任意一位被設定,就返回 1;否則返回 0,只用於編號為 1 ~ 31。

  • siginitset(set, mask):把 mask 中的位初始化為 1 ~ 32 之間的訊號在 set 中對應的低位,並把 33 ~ 63 之間訊號的對應位清 0。

  • siginitsetinv(set, mask):用 mask 中位的補碼初始化 1 ~ 32 間的訊號在 sigset_t 型別的變數中對應的低位,並把 33 ~ 63 之間訊號的對應位置位。

  • signal_pending§如果 *p 程序描述符所表示的程序有非阻塞的掛起訊號,就返回 1,否則返回 0。通過檢查程序的 TIF_SIGPENDING 標誌實現。

  • recalc_sigpending_tsk(t) 和 recalc_sigpending(): 第一個函式檢查是 *t 程序描述符表示的程序有掛起訊號(t->pending->signa),還是程序所屬的執行緒組有掛起的訊號(t->signal->shared_pending->signal),然後把 t->thread_info->flags 的 TIF_SIGPENDING 標誌置位。第二個函式等價於 recalc_sigpending_tsk(current)。

  • rm_from_queue(mask, q):從掛起訊號佇列 q 中刪除與 mask 位掩碼相對應的掛起訊號。

  • flush_sigqueue(q):從掛起訊號佇列 q 中刪除所有的掛起訊號。

  • flush_signals(t):刪除傳送給 *t 程序描述符所表示的程序的所有訊號。
    通過清除 t->thread_info->flags 中的 TIF_SIGPENDING 標誌,並在 t->pending 和 t->signal->shared_pending 佇列上兩次呼叫 flush_sigqueue() 實現。

產生訊號

當傳送給程序或整個執行緒組一個訊號時,該訊號可能來自核心,也可能來自另一個程序。

傳送給程序的訊號的函式在結束時會呼叫 specific_send_sig_info()。

傳送給整個執行緒組的訊號的函式在結束時會呼叫 group_send_sig_info()。

specific_send_sig_info()

向指定程序傳送訊號。

引數:

  • sig,訊號編號。
  • info,或者是 siginfo_t 表的地址,或者是三個特殊值中的一個:
    • 0:訊號由使用者態程序傳送。
    • 1:訊號由核心傳送。
    • 2:由核心傳送的 SIGSTOP 或 SIGKILL 訊號。
  • t:指向目標程序描述符的指標。

必須在關本地中斷和已經獲得 t->sighand->siglock 自旋鎖的情況下呼叫該函式,執行下列步驟:

  1. 檢查程序是否忽略訊號,如果是就返回 0(不產生訊號)。
    以下三個條件都滿足時,訊號被忽略:
  • 程序沒有被跟蹤(t->ptrace 中的 PT_PTRACED 標誌被清 0)
  • 訊號沒有被阻塞(sigismember(&t->blocked, sig) 返回 0)
  • 或者顯示地忽略訊號(t->sighand->action[sig-1].sa_handler == SIG_IGN),或者隱含地忽略訊號(sa_handler == SIGDFL,且訊號是 SIGCONT、SIGCHLD、SIGWINCH 或 SIGURG)
  1. 如果訊號是非實時的(sig < 32),且在程序的私有掛起訊號佇列上已經有另外一個相同的掛起訊號(sigismember(&t->pending.signal, sig) 返回 1),什麼都不需要做,返回0.
  2. send_signal(sig, info, t, &t->pending) 把訊號新增到程序的掛起訊號集合中。
  3. 如果 send_signal() 成功結束,且訊號不被阻塞(sigismember(&t->blocked, sig) 返回 0),signal_wake_up() 通知程序有新的掛起訊號,隨後,該函式執行下述步驟:
    a. 把 t->thread_info->flags 中的 TIF_SIGPENDING 標誌置位。
    b. 如果程序處於 TASK_INTERRUPTILE 或 TASK_STOPPED 狀態,且訊號是 SIGKILL,try_to_wake_up() 喚醒程序。
    c. 如果 try_to_wake_up() 返回 0,說明進已經是可執行的:檢查程序是否已經在另外一個 CPU 上執行,如果是就像那個 CPU 傳送一個處理器間中斷,以強制當前程序的重新排程。
    因為從排程函式返回時,每個程序都檢查是否存在掛起訊號,因此,處理器間中斷保證了目標程序能很快注意到新的掛起訊號。
  4. 返回 1(成功產生訊號)。

send_signal()

在掛起訊號佇列中插入一個新元素。

引數:

  • 訊號編號 sig
  • siginfo_t 資料結構的地址 info
  • 目標程序描述符的地址 t
  • 掛起訊號佇列的地址 signals

執行下列步驟:

  1. 如果 info == 2,該訊號就是 SIGKILL 或 SIGSTOP,且已經由核心通過 force_sig_specific() 產生:跳到第 9 步,核心立即強制執行與這些訊號相關的操作,因此函式不用把訊號新增到掛起訊號佇列中。
  2. 如果程序擁有者的掛起訊號的數量(t->user->sigpending)小於當前程序的資源限制(t->signal->rlim[RLIMT_SIGPENDING].rlim_cur),就為新出現的訊號分配 sigqueue 資料結構:
q = kmeme_cache_alloc(sigqueue_cachep, GFP_ATOMIC);
  1. 如果程序擁有者的掛起訊號的數量太多,或上一步的記憶體分配失敗,就跳轉到到第 9 步。
  2. 遞增擁有者掛起訊號的數量(t->user->sigpending)和 t->user 所指向的每使用者資料結構的引用計數器。
  3. 在掛起訊號佇列 signals 中增加 sigqueue 資料機構:
list_add_tail(&q->list, &signals->list);
  1. 在 sigqueue 資料結構中填充表 siginfo_t:
if((unsigned long)info == 0)
{
	q->info.si_signo = sig;
	q->info.si_errno = 0;
	q->info.si_code = SI_USER;
	q->info._sifields._kill._pid = current->pid;
	q->info._sifields._kill._uid = current->uid;
}
else if((unsigned long)info == 1)
{
	q->info.si_signo = sig;
	q->info.si_errno = 0;
	q->info.si_code = SI_KERNEL;
	q->info._sifields._kill._pid = 0;
	q->info._sifiields._kill._uid = 0;
}
ese
	copy_siginfo(&q->info, info);  // 複製由呼叫者傳遞的 siginfo_t 表
  1. 把佇列位掩碼中與訊號相應的位置 1:
sigaddset(&signals->signal, sig);
  1. 返回 0:說明訊號已經被成功追加到掛起訊號佇列中。
  2. 此時,不再向訊號掛起佇列中增加元素,因為已經有太多的掛起訊號,或已經沒有可以分給 sigqueue 資料結構的空閒空間,或者訊號已經由核心強制立即傳送。
    如果訊號是實時的,並已經通過核心函式傳送給佇列排隊,則 send_signal() 返回錯誤程式碼 -EAGIN:
if(sig >= 32 && info && (unsigned long)info != 1 && info->si_code != SI_USER)
	return -EAGIN;
  1. 設定佇列的位掩碼中與訊號相關的位:
sigaddset(&signals->signal, sig);
  1. 返回 0:即使訊號沒有被追加到佇列中,掛起訊號掩碼中相應的位也被設定。

即使在掛起佇列中沒有空間存放相應的掛起訊號,讓目標程序能接收訊號也很重要。
假設一個程序正在消耗過多記憶體,核心必須保證即使沒有空閒記憶體,kill() 也能成功執行。

group_send_sig_info()

向整個執行緒組傳送訊號。

引數:

  • 訊號編號 sig
  • siginfo_t 表的地址 info
  • 程序描述符的地址 p

執行下列步驟:

  1. 檢查 sig 是否正確
if(sig < 0 || sig > 64)
	return -EINVAL;
  1. 如果訊號是由使用者態程序傳送的,則確定是否允許該操作。如果不允許使用者態程序傳送訊號,返回 -EPERM。
    下列條件至少有有一個成立,訊號才可被傳遞:
  • 傳送程序的擁有者擁有適當的許可權(通常意味著通過系統管理員釋出訊號)。
  • 訊號為 SIGCONT 且目標程序與傳送程序處於同一個註冊會話中。
  • 兩個程序屬於同一個使用者。
  1. 如果引數 sig == 0,不產生任何訊號,立即返回:
if(!sig || !p->sighand)
	return 0;
  • 0 是無效的訊號編碼,說明發送程序沒有向目標執行緒組傳送訊號的特權。如果目標程序正在被殺死(通過檢查它的訊號處理程式描述符是否被釋放得知),那麼函式也返回。
  1. 獲取 p->sighand->siglock 自旋鎖並關閉本地中斷。
  2. handle_stop_signal() 檢查訊號的某些型別,這些型別可能使目標執行緒組的其他掛起訊號無效。
    a. 如果執行緒組正在被殺死(訊號描述符的 flags 欄位的 SIGNAL_GROUP_EXIT 標誌被設定),則返回。
    b. 如果 sig 是 SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU 訊號,rm_from_queue() 從共享掛起訊號佇列 p->signal->shared_pending 和執行緒組所有成員的私有訊號佇列中刪除 SIGCONT 訊號。
    c. 如果 sig 是 SIGCONT 訊號,rm_from_queue() 從共享掛起訊號佇列 p->signal->shared_pending 中刪除所有的 SIGSTOP、SIGTSTP、SIGTTIN 和 SIGTTOU 訊號,然後從屬於執行緒組的程序的私有掛起訊號佇列中刪除上述訊號,並喚醒程序:
// 掩碼 0x003c0000 選擇以上四種停止訊號
rm_from_queue(0x003c0000, &p->signal->shared_pending);
t = p;
do
{
	rm_from_queue(0x003c0000, &t->pending);
	try_to_wake_up(t, TASK_STOPPED, 0);
	t = next_thread(t);   // 返回執行緒組中不同輕量級程序的描述符地址
}while(t != p);
  1. 檢查執行緒組是否忽略訊號,如果是就返回 0 值(成功)。如果前一節“訊號的作用”中提到的忽略訊號的三個條件都滿足,就忽略訊號。
  2. 如果訊號是非實時的,並且線上程組的共享掛起訊號佇列中已經有另外一個相同的訊號,就什麼都不做,返回 0 值(成功)。
if(sit < 32 && sigismember(&p->signal->shared_pending.signal, sig))
	return 0;
  1. send_signal() 把訊號新增到共享掛起訊號佇列中。如果返回非 0 的錯誤碼,終止並返回相同值。
  2. __group_complete_signal() 喚醒執行緒組中的一個輕量級程序。
  3. 釋放 p->sighand->siglock 自旋鎖並開啟本地中斷。
  4. 返回 0(成功)。

__group_complete_signal() 掃描執行緒組中的程序,查詢能接收新訊號的程序。滿足下述所有條件的程序可能被選中:

  • 程序不阻塞訊號。
  • 程序的狀態不是 EXIT_ZOMBIE、EXIT_DEAD、TASK_TRACED 或 TASK_STOPPED。
  • 程序沒有正在被殺死,即它的 PF_EXITING 標誌沒有置位。
  • 程序或者當前正在 CPU 上執行,或者它的 TIF_SIGPENDING 標誌還沒有設定。

一個執行緒組可能有很多滿足上述條件的程序,函式按照下面的規則選中其中一個程序:

  • 如果 p 標識的程序(group_send_sig_info() 的引數傳遞的描述符地址)滿足所有的優先準則,函式就選擇該程序。
  • 否則,函式通過掃描執行緒組的成員搜尋一個適當的程序,搜尋從接收執行緒組最後一個訊號的程序(p->siganl->curr_target)開始。

如果 __group_complete_signal() 成功找到一個適當的程序,就開始向被選中的程序傳遞訊號。
首先檢查訊號是否是致命的,如果是,通過向執行緒組中的所有輕量級程序傳送 SIGKILL 訊號殺死整個執行緒組。
否則,呼叫 signal_wake_up() 通知被選中的程序:有新的掛起訊號。

傳遞訊號

如何確保程序的掛起訊號得到處理核心所執行的操作。

在執行程序恢復使用者態下的執行前,核心會檢查程序 TIF_SIGPENDING 標誌的值。
每當核心處理完一箇中斷或異常時,就檢查是否存在掛起訊號。

為了處理非阻塞的掛起訊號,核心呼叫 do_signal()。引數:

  • regs,棧區的地址,當前程序在使用者態下暫存器的內容存放在這個棧中。
  • oldset,變數的地址,假設函式把阻塞訊號的位掩碼陣列存放在這個變數中。不需要儲存位掩碼陣列時,置為 NULL。

通常只在 CPU 要返回到使用者態時才呼叫 do_signal()。
因此,如果中斷處理程式呼叫 do_signal(),該函式立即返回。

if((regs->xcs & 3) != 3)
	return 1;

如果 oldset 引數為 NULL,就用 current->blocked 欄位的地址對它初始化:

if(!oldset)
	oldset = &current->blocked;

do_signal() 的核心是重複呼叫 dequeue_signal(),直到私有掛起訊號佇列和共享掛起訊號佇列中都沒有非阻塞的掛起訊號為止。

dequeue_signal() 的返回碼存放在 signr 區域性變數中,值為:

  • 0,所有掛起的訊號已全部被處理,且 do_signal() 可以結束。
  • 非 0,掛起的訊號正等待被處理,且 do_signal() 處理了當前訊號後又呼叫了 dequeue_signal()。

dequeue_signal() :

  • 首先考慮私有訊號佇列中的所有訊號,並從最低編號的掛起訊號開始。
  • 然後考慮共享佇列中的訊號。
  • 它更新資料結構以標識訊號不再是掛起的,並返回它的編號。
    這就涉及清 current->pending.signal 或 current->signal->shared_pending.signal 中對應的位,並呼叫 recalc_sigpending() 更新 TIF_SIGPEDING 標誌的值。

do_signal() 處理每個掛起的訊號,並將其編號通過 dequeue_signal() 返回:

  • 首先,檢查 current 接收程序是否正受其他一些程序的監控;
    如果是,呼叫 do_notify_parent_cldtop() 和 schedule() 讓監控程序知道程序的訊號處理。
  • 然後,把要處理訊號的 k_sigaction 資料結構的地址賦給區域性變數 ka:
ka = &current->sig->action[signr-1];
  • 根據 ka 的內容可以執行三種操作:忽略訊號、執行預設操作或執行訊號處理程式。
    如果顯式忽略被傳遞的訊號,do_signal() 僅僅繼續執行迴圈,接著考慮另一個掛起訊號:
if(ka->sa.sa_handler == SIG_IGN)
	continue;

接下來說明如何執行預設操作和訊號處理程式。

執行訊號的預設操作

如果 ka->sa.sa_handler == SIG_DFL,do_signal() 就必須執行訊號的預設操作。
但當接收程序是 init 時,該訊號被丟棄:

if(current->pid == 1)
	continue;

如果接收程序是其他程序,對預設操作是 Ignore 的訊號進行簡單處理:

if(signr == SIGCONT || signr == SIGCHLD || signr == SIGWINCH || signr == SIGURG)
	continue;

預設操作是 Stop 的訊號可能停止執行緒組中的所有程序。
因此,do_singal() 把程序的狀態都設定為 TASK_STOPPED,並隨後呼叫 schedule():

if(signr == SIGTOP || signr == SIGTSTP || signr == SIGTTIN || signr = SIGTTOU)
{
	// SIGSTOP 與其他訊號的差異:SIGSTOP 總是停止執行緒組
	// 而其他訊號只停止不在“孤兒程序組”中的執行緒組。
	// POSIX 標準規定,只要程序組中有一個程序有父程序,
	// 即便父程序處於不同的程序組中,但在同一個會話中,
	// 那麼該程序組不是孤兒程序組
	// 因此,如果父程序死亡,但啟動該程序的使用者仍登入線上,
	// 那麼該程序組就不是一個孤兒程序組
	if(signr != SIGSTOP && is_orphaned_pgrp(current->signal->pgrp))
		continue;

	// 檢查 current 是否是執行緒組中第一個被停止的程序,如果是,啟用“組停止”:
	// 本質上,將訊號描述符中的 group_stop_count 欄位設為正值
	// 並喚醒執行緒組中的所有程序
	// 組中的所有程序都都檢查該欄位以確認正在進行”組停止“
	// 然後把程序的狀態設定為 TASK_STOPPED,並呼叫 schedule()
	// 如果執行緒組領頭程序的父程序沒有設定 SIGCHLD 的 SA_NOCLDSTOP 標誌
	// 還需要向它傳送 SIGCHLD 訊號
	do_signal_stop(signr);
}

預設操作位 Dump 的訊號可以在程序的工作目錄中建立一個”轉儲“檔案,該關檔案列出程序地址空間和 CPU 暫存器的全部內容。
do_signal() 建立了轉儲檔案後,就殺死該執行緒組。

剩餘 18 個訊號的預設操作時 Terminate,僅僅殺死執行緒組。
為了殺死整個執行緒組,呼叫 do_group_exit() 執行徹底的”組退出“過程。

捕獲訊號

如果訊號有一個專門的處理程式,do_signal() 就執行它。
通過呼叫 handle_signal() 進行:

handle_signal(signr, &info, &ka, oldset, regs);

// 如果所接收訊號的 SA_ONESHOT 標誌被置位
// 就必須重新設定它的預設操作
// 以便同一訊號的再次出現不會再次觸發訊號處理程式的執行
if(ka->sa.sa_flags & SA_ONESHOT)
	ka->sa.sa_handler = SIG_DFL;  
	
// 處理了一個單獨的訊號後返回
// 直到下一次呼叫 do_signal() 時才考慮其他掛起的訊號
// 確保了實時訊號將以適當的順序得到處理
return 1;

執行一個訊號處理程式複雜性一:在使用者態和核心態之間切換時,需要謹慎地處理棧中的內容。

handle_signal() 執行在核心態,而訊號處理程式執行在使用者態,當前程序恢復”正常“執行前,必須首先執行使用者態的訊號處理程式。
此外,當核心打算恢復程序的正常執行時,核心態堆疊不再包含被中斷程式的硬體上下文,因為每當從核心態向用戶態轉換時,核心態堆疊都被清空。

執行一個訊號處理程式複雜性二:可以呼叫系統呼叫。這種情況下,執行了系統呼叫的服務例程後,控制權必須返回到訊號處理程式,而不是被中斷程式的正常程式碼流。

Linux 所採用的解決方法是,把儲存在核心態堆疊中的硬體上下文拷貝到當前程序的使用者態堆疊中。
使用者態堆疊也以同樣方式修改:即當訊號處理程式終止時,自動呼叫 sigreturn() 把這個硬體上下文拷貝回核心態堆疊中,並恢復使用者態堆疊中原來的內容。
在這裡插入圖片描述

圖 11-2 說明了有關捕獲一個訊號的函式的執行流:

  1. 一個非阻塞的訊號傳送一個程序。
  2. 中斷或異常發生時,程序切換到核心態。
  3. 核心執行 do_signal(),該函式依次處理訊號(handle_signal())和建立使用者態堆疊(setup_frame() 或 setup_rt_frame())。
  4. 程序返回到使用者態,因為訊號處理程式的起始地址被強制放程序序計數器,因此開始執行訊號處理程式。
  5. 處理程式終止時,setup_frame() 或 setup_rt_frame() 放在使用者態堆疊中的返回程式碼被執行。該程式碼呼叫 sig_return() 或 rt_sigreturn() 系統呼叫,相應的服務例程把正常程式的使用者態堆疊硬體上下文拷貝到核心態堆疊,並把使用者態堆疊恢復到它原來的樣子(restore_sigcontext())。
  6. 普通程序恢復執行。

下面詳細討論該種方案。

建立幀

為建立程序的使用者態堆疊,handle_signal() 呼叫 setup_frame() 或 setup_rt_frame()。
為了在這兩個函式之間進行選擇,核心檢查與訊號相關的 sigaction 表 sa_flags 欄位的 SA_SIGINFO 標誌。

setup_frame() 引數:

  • sig,訊號編號
  • ka,與訊號相關的 k_sigaction 表的地址
  • oldset,阻塞訊號的位掩碼陣列的地址
  • regs,使用者態暫存器的內容儲存在核心態堆疊區的地址

setup_frame() 把幀推入使用者態堆疊中,該幀含有處理訊號所需的資訊,並確保正確返回到 handle_signal()。

一個幀就是包含下列欄位的 sigframe 表:
在這裡插入圖片描述
setup_frame() 執行步驟:

  1. 呼叫 get_sigframe() 計算幀的第一個記憶體單元,通常在使用者態堆疊中:
// 因為棧朝低地址方向延伸,所以通過把當前棧頂的地址減去它的大小
// 使其結果與 8 的倍數對齊,就獲得了幀的起始地址
(rets->esp - sizeof(struct sigframe)) & 0xfffffff8
  1. 用 access_ok 巨集對返回地址進行驗證。
    如果地址有效,反覆呼叫 __put_user() 填充幀的所有欄位。
    幀的 pretcode = &__kernel_sigreturn,一些粘合程式碼的地址存放在 vsyscall 頁中。

  2. 修改核心態堆疊的 regs 區,保證了當 current 恢復在使用者態的執行時,控制權將傳遞給訊號處理程式。

regs->esp = (unsigned long)frame;  // 而 esp 指向已推進使用者態堆疊頂的幀的第一個記憶體單元
regs->eip = (unsigned long)ka->sa.sa_handler; //  eip 暫存器執行訊號處理程式的第一條指令
regs->eax = (unsigned long)sig;
regs->edx = regs->ecx = 0;

// 把儲存在核心態堆疊的段暫存器內容重新設定成它們的預設值
regs->xds = regs->xes = regs->xss = __USER_DS;
regs->xcs = __USER_CS;

現在,訊號處理程式所需的資訊就在使用者態堆疊的頂部。

setup_rt_frame() 與 setup_frame() 非常相似,但它把使用者態堆疊存放在一個擴充套件幀中(rt_sigframe 資料結構中),該幀包含了與訊號相關的 siginfo_t 表的內容。
此外,該函式設定 pretcode 欄位以使它執行 vsyscall 頁中的 __kernel_rt_sigreturn 程式碼。

檢查訊號標誌

建立使用者態堆疊後,handle_signal() 檢查與訊號相關的標誌值。

// 如果訊號沒有設定 SA_NODEFER 標誌
if(!(ka->sa.sa_flags & SA_NODEFER))
{
	spin_lock_irq(&current->sighand->siglock);

	// 在 sigaction 表中 sa_mask 欄位對應的訊號就必須在訊號處理程式執行期間被阻塞
	sigorsets(&current->blocked, &current->blocked, &ka->sa.sa_mask);
	
	sigaddset(&current->blocked, sig);  // sig 為訊號編號
	
	// 檢查程序是否有非阻塞的掛起訊號,並因此設定它的 TIF_SIGPENDING 標誌
	recalc_sigpending(curent);
	
	spin_unlock_irq(&current->sighand->siglock);
}

然後,返回到 do_signal(),do_signal() 也立即返回。

開始執行訊號處理程式

do_signal() 返回時,當前程序恢復它在使用者態的執行。
由於 setup_frame() 的準備,eip 暫存器執行訊號處理程式的第一條指令,而 esp 指向已推進使用者態堆疊頂的幀的第一個記憶體單元。
因此,訊號處理程式被執行。

終止訊號處理程式

訊號處理程式結束時,返回棧頂地址,該地址指向幀的 pretcode 欄位所引用的 vsyscall 頁中的程式碼:

__kernel_sigreturn:
	popl %eax
	movl $__NR_sigreturn, %eax
	int $0x80

訊號編號(即幀的 sig 欄位)被從棧中丟棄,然後呼叫 sigreturn() 。

sys_sigreturn() :

  1. 計算型別為 pt_regs 的 regs 的地址,pt_regs 包含使用者態程序的硬體上下文。
  2. 根據存放在 esp 欄位中的地址,匯出並檢查幀在使用者態堆疊內的地址:
frame = (struct sigframe *)(regs.esp - 8);
if(verify_area(VERIFY_READ, frame, sizeof(*frame))
{
	force_sig(SIGSEGV, current);
	return 0;
}
  1. 把呼叫訊號處理程式前所阻塞的訊號的位陣列從幀的 sc 欄位拷貝到 current 的 blocked 欄位。結果,為訊號處理函式的執行而遮蔽的所有訊號解除阻塞。
  2. 呼叫 recalc_sigpending() 。
  3. 把來自幀的 sc 欄位的程序硬體上下文拷貝到核心態堆疊中,並從使用者態堆疊中刪除幀,這兩個任務通過呼叫 restore_sigcontext() 完成。

rt_sigqueueinfo() 需要與訊號相關的 siginfo_t 表。
擴充套件幀的 pretcode 指向 vsyscall 頁中的 __kernel_rt_sigturn 程式碼,它呼叫 rt_sigreturn(),相應的 sys_rt_sigreturn() 服務例程把來自擴充套件幀的程序硬體上下文拷貝到核心態堆疊,並通過從使用者態堆疊刪除擴充套件幀以恢復使用者態堆疊原來的內容。

系統呼叫的重新執行

核心不總是能立即滿足系統呼叫發出的請求,這時,把發出系統呼叫的程序置為 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 狀態。

如果程序處於 TASK_INTERRUPTIBLE 狀態,並且某個程序向它傳送了一個訊號,則核心不完成系統呼叫就把程序置成 TASK_RUNNING 狀態。
當切換回使用者態時訊號被傳遞給程序。
這時,系統呼叫服務例程沒有完成,但返回 EINTR、ERESTARTNOHAND、ERESTART_RESTARTBLOCK、ERESTARTSYS 或 ERESTARTNOINTR 錯誤碼。

實際上,使用者態程序獲得的唯一錯誤碼是 EINTR,表示系統呼叫還沒有執行完。
核心內部使用剩餘的錯誤碼來指定訊號處理程式結束後是否自動重新執行系統呼叫。

在這裡插入圖片描述

  • Terminate,不會自動重新執行系統呼叫:程序在 int $0x80 或 sysenter 指令緊接著的那條指令將恢復它在使用者態的執行,這時 eax 暫存器包含的值為 -EINTR。
  • Reexecute,核心強迫使用者態程序把系統呼叫號重新裝入 eax 暫存器,並重新執行 int $0x80 或 sysenter 指令。程序意識不到這種重新執行,出錯碼也不傳遞給程序。
  • Depends,只有被傳遞訊號的 SA_RESTART 標誌被設定,才重新執行系統呼叫;否則,系統呼叫以 -EINTR 出錯碼結束。

傳遞訊號時,核心在試圖重新執行一個系統呼叫前,必須確定程序確實發出過該系統呼叫。
regs 硬體上下文的 orig_eax 欄位起該作用。
中斷或異常處理程式開始時初始化該欄位:

  • 中斷,與中斷相關的 IRQ 號減去 256
  • 0x80 或 sysenter,系統呼叫號
  • 其他異常,-1

因此,orig_eax 欄位中的非負數意味著訊號已經喚醒了在系統呼叫上睡眠的 TASK_INTERRUPTIBLE 程序。
服務例程認識到系統呼叫曾被中斷,並返回前面提到的某個錯誤碼。

重新執行被未捕獲訊號中斷的系統呼叫

如果訊號被顯式忽略,或者它的預設操作被強制執行,do_signal() 就分析系統呼叫的出錯碼,並確定是否重新自動執行未完成的系統呼叫。
如果必須重新開始執行系統呼叫,do_signal() 就修改 regs 硬體上下文,以便在程序返回使用者態時,eip 指向 int $0x80 或 sysenter 指令,且 eax 包含系統呼叫號:

if(regs->orig_eax >= 0)
{
	if(regs->eax == -ERESTARTNOHAND || regs->eax == -ERESTARTSYS || regs->eax == -ERESTARTNOINTR)
	{
		regs->eax = regs->orig_eax;   // 系統呼叫服務路測的返回碼賦給 regs->eax
		regs->eip -= 2;  // int $0x80 和 sysreturn 的長度都是兩個位元組,eip 減 2 後,指向引起系統呼叫的指令
	}

	// 因為 eax 暫存器存放了 restart_syscall() 的系統呼叫號
	// 因此,使用者態程序不會重新指向被訊號中斷的同一系統呼叫
	// 該錯誤碼僅用於與時間相關的系統呼叫,重新指向這些系統呼叫時
	// 應該調整它們的使用者態引數
	if(regs->eax == -ERESTART_RESTARTBLOCK)  
	{
		regs->eax = __NR_restart_syscall;
		regs->eip -= 2;
	}
}

為所捕獲的訊號重新執行系統呼叫

如果訊號被捕獲,那麼 handle_signal() 可能分析出錯碼,也可能分析 sigaction 表的 SA_RESTART 標誌,來決定是否必須重新執行未完成的系統呼叫。

如果系統呼叫必須被重新開始執行,handle_signal() 就與 do_signal() 完全一樣繼續執行;否則,向用戶態程序返回一個出錯碼 -EINTR。

if(regs->orig_eax >= 0)
{
	switch(regs->eax)
	{
	case -ERESTART_RESTARTBLOCK:
	case -ERESTARTNOHAND:
		regs->eax = -EINTR;
		break;
	case -ERESTARTSYS:
		if(!(ka->sa.sa_flags & SA_RESTART))
		{
			regs->eax = -EINTR;
			brea;
		}
	case -ERESTARTNOINTR:
		regs->eax = regs->orig_eax;
		regs->eip -= 2;
	}
}

與訊號處理相關的系統呼叫

kill()

kill(pid, sig) 向普通程序或多執行緒應用傳送訊號,其服務例程是 sys_kill()。pid 引數的含義取決於它的值:

  • pid > 0,把 sig 訊號傳送到 PID 等於 pid 的程序所屬的執行緒組。
  • pid = 0,把 sig 訊號傳送到與呼叫程序同組的程序的所有執行緒組。
  • pid = -1,把訊號傳送到所有程序,除了 swapper(PID = 0),init(PID = 1)和 current。
  • pid < -1,把訊號傳送到程序組 -pid 中程序的所有執行緒組。

sys_kill() 為訊號建立最小的 siginfo_t 表,然後呼叫 kill_something_info():

info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info._sifields._kill._pid = current->tgid;
info._sifields._kill._uid = current->uid;

// 或呼叫 kill_proc_info()(通過 group_send_sig_info() 向一個單獨的執行緒組傳送訊號)  
// 或呼叫 kill_pg_info()(掃描目標程序組的所有程序,併為目標程序組中的所有程序呼叫 send_sig_info())
// 或為系統中的所有程序反覆呼叫 group_send_sig_info()(如果 pid  等於 -1)
return kill_something_info(sig, &info, pid);  

kill() 能傳送任何訊號,包括 32 ~ 64 間的實時訊號。
但不能確保一個新的元素加入到目標程序的掛起訊號佇列,因此,掛起訊號的多個例項可能被丟失。
實時訊號應當通過 rt_sigqueueinfo() 程序傳送。

tkill() 和 tgkill()

向執行緒組中的指定程序傳送訊號。

tkill() 的兩個引數:

  • PID,訊號接收程序的 pid
  • sig,訊號編號

sys_tkill() 服務例程為 siginfo 表賦值、獲取程序描述符地址、進行許可性檢查,並呼叫 specific_send_sig_info() 傳送訊號。

tgkill() 還需要第三個引數:

  • tgid,訊號接收程序組所線上程組的執行緒組 ID

sys_tgkill() 服務例程執行的操作與 sys_tkill() 一樣,但還需要檢查訊號接收程序是否確實屬於執行緒組 tgid。
該附加的檢查解決了向一個正在被殺死的程序傳送訊息時出現的競爭條件的問題:
如果另外一個多執行緒應用正以足夠快的速度建立輕量級級程序,訊號就可能被傳遞給一個錯誤的程序。
因為執行緒組 ID 在多執行緒應用的整個生存期中是不會改變的。

改變訊號的操作

sigaction(sig, act, oact) 允許使用者為訊號指定一個操作。
如果沒有自定義的訊號操作,則執行與傳遞的訊號相關的預設操作。

sys_sigaction() 服務例程作用於兩個引數:

  • sig,訊號編號
  • act,型別為 old_sigaction 的 act 表(表示新的操作)
  • oact,可選的輸出引數,獲得與訊號相關的以前的操作。
  1. 檢查 act 地址的有效性。
  2. 用 *act 的欄位填充型別為 k_sigaction 的 new_ka 區域性變數的 sa_handler、sa_flags 和 sa_mask 欄位:
__get_user(new_ka.sa.sa_handler, &act->sa_handler);
__get_user(new_ka.sa.sa_flags, &act->sa_flags);
__get_user(mask, &act->sa_mask);
siginitset(&new_ka.sa.sa_mask, mask);
  1. 呼叫 do_sigaction() 把新的 new_ka 表拷貝到 current->sig->action 的 sig-1 位置的表項中(沒有 0 訊號):
k = &current->sig->action[sig-1];
if(act)
{
	*k = *act;
	
	// 訊號處理程式從不遮蔽 SIGKILL 和 SIGSTOP 
	sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SITSTOP));

	// POSIX 標準規定,當預設操作是“Ignore”時
	// 把訊號操作設定為 SIG_IGN 或 SIG_DFL 將引起同類型的任意掛起訊號被丟棄
	if(k->sa.sa_handler == SIG_IGN || (k->sa.sa_handler == SIG_DFL && 
		(sig == SIGCONT || sig == SIGCHLD || sig == SIGWINCH || sig == SIGURG)))
	{
		rm_from_queue(sigmask(sig), &current->signal->shared_pendig);
		t = current;
		do
		{
			rm_from_queue(sigmask(sig), &current->pending);
			recalc_sigpending_tsk(t);
			t = next_thread(t);
		}while(t != current);
	}
}

sigaction() 還允許使用者初始化表 sigaction 的 sa_flags 欄位。

原來的 System V Unix 變體提供了 signal() 系統呼叫,Linux 提供了 sys_signal() 服務例程:

new_sa.sa_handler = handler;
new_sa.sa_flags = SA_ONESHOT | SA_NOMASL.
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;

檢查掛起的阻塞訊號

sigpending() 允許程序檢查訊號被阻塞時已經產生的那些訊號。
服務例程 sys_sigpending() 只作用於一個引數 set,即使用者變數的地址,必須將位陣列拷貝到該變數中:

sigorsets(&pending, &current->pending.signal, &current->signal->shared_pending.signal);
sigandsets(&pending, &current->blocked, &pending);
copy_to_user(set, &pending, 1);

修改阻塞訊號的集合

sigprocmask() 只應用於常規訊號(非實時訊號)。
sys_sigprocmask() 服務例程作用於三個引數:

  • oset,程序地址空間的一個指標,執行存放以前位掩碼的一個位數組。
  • set,程序地址空間的一個指標,執行包含新位掩碼的位陣列。
  • how,一個標誌,可採取如下值:
    • SIG_BLOCK,*set 位掩碼陣列,指定必須加到阻塞訊號的位掩碼陣列中的訊號。
    • SIG_UNBLOCK,*set 位掩碼陣列,指定必須從阻塞訊號的位掩碼陣列中刪除的訊號。
    • SIG_SETMASK,*set 位掩碼陣列,指定阻塞訊號新的位掩碼陣列。

sys_sigprocmask():

// 呼叫 copy_from_user() 把 set 引數拷貝到區域性變數 new_set 中
if(copy_from_user(&new_set, set, sizeof(*set)))
	return -EFAULT;
	
new_set &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));

// 把 current 標準阻塞訊號的位掩碼陣列拷貝到 old_set 區域性變數中
old_set = current->blocked.sig[0];

// 根據 how 標誌進行相應操作
if(how == SIG_BLOCK)
	sigaddsetmask(&current->blocked, new_set);
else if(how == SIG_UNBLOCK)
	sigdelsetmask(&current->blocked, new_set);
else if(how == SIG_SETMASK)
	current->blocked.sig[0] = new_set;
else
	return -EINVAL;
	
recalc_sigpending(current);
if(oset && copy_to_user(oset, &old_set, sizeof(*oset)))
	return -EFAULT;
return 0;

掛起程序

sigsuspend() 把程序置為 TASK_ITERRUPTIBLE 狀態,這發生在把 mask 引數指向的位掩碼陣列所指定的標準訊號阻塞後。
只有當一個非忽略、非阻塞的訊號傳送到程序後,程序才被喚醒:

sys_sigsuspend() 服務例程:

mask&= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
saveset = current->blocked;
siginitset(&current->blocked, mask);
recalc_sigpending(current);
regs->eax = -EINTR;
while(1)
{
	current->state = TASK_INTERRUPTIBLE;

	schedule();  // 選擇另一個程序執行

	// 當發出 sigsuspend() 的程序又開始執行時
	// do_signal() 傳遞喚醒了該程序的訊號,返回值為 1 時,不忽略該訊號
	// 因此返回 -EINTR 出錯碼後終止
	if(do_signal(regs, &saveset))
		return -EINTR;
}

實時訊號的系統呼叫

實時訊號的幾個系統呼叫(rt_sigaction()、rt_sigpending()、rt_sigprocmask() 即 rt_sigsuspend()) 與前面的描述類似。

rt_sigqueueinfo():傳送一個實時訊號以便把它加入到目標程序的共享訊號佇列中。
一般通過標準庫函式 sigqueue() 呼叫 rt_sigqueueinfo()。

rt_sigtimedwait():把阻塞的掛起訊號從佇列中刪除而不傳遞它,並向呼叫者返回訊號編號;
如果沒有阻塞的訊號掛起,就把當前程序掛起一個固定的時間間隔。
一般通過標準庫函式 sigwaitinfo() 和 sigtimedwait() 呼叫 rt_sigtimedwait()。