信號分紅兩種:
regular signal( 非實時信號 ), 對應的編碼值爲 [1,31]
real time signal 對應的編碼值爲 [32,64]linux
編碼爲 0 的信號 不是有效信號,只用於檢查是當前進程否有發送信號的 權限 ,並不真正發送。程序員
線程會有本身的懸掛信號隊列 , 而且線程組也有一個信號懸掛隊列 .數組
信號懸掛隊列保存 task 實例接收到的信號 , 只有當該信號被處理後它纔會從懸掛隊列中卸下 .緩存
信號懸掛隊列還有一個對應的阻塞信號集合 , 當一個信號在阻塞信號集合中時 ,task 不會處理該被阻塞的信號 ( 可是該信號依舊在懸掛隊列中 ). 當阻塞取消時 , 它會被處理 .session
對一個信號 , 要三種處理方式 :數據結構
忽略該信號 ;多線程
採用默認方式處理 ( 調用系統指定的信號處理函數 );app
使用用戶指定的方式處理 ( 調用用戶指定的信號處理函數 ).socket
對於某些信號只能採用默認的方式處理 (eg:SIGKILL,SIGSTOP).xss
信號處理能夠分紅兩個階段 : 信號產生並通知到接收方 (generation), 接收方進行處理 (deliver)
.........
簡介
Unix 爲了容許用戶態進程之間的通訊而引入signal. 此外, 內核使用signal 給進程通知系統事件. 近30 年來, signal 只有很小的變化 .
如下咱們先介紹linux kernel 如何處理signal, 而後討論容許進程間 exchange 信號的系統調用 .
The Role of Signals
signal 是一種能夠發送給一個進程或一組進程的短消息( 或者說是信號 , 可是這麼容易和信號量混淆). 這種消息一般只是一個整數 , 而不包含額外的參數 .
linux 提供了不少種signal, 這些signal 經過宏來標識( 這個宏做爲這個信號的名字). 而且這些宏的名字的開頭是SIG.eg:
宏SIGCHLD , 它對應的整數值爲17, 用來表示子進程結束時給父進程發送的消息 ( 即當子進程結束時應該向父進程發送標識符爲17 的signal/ 消息/ 信號)
. 宏SIGSEGV, 它對應的整數值爲11, 當進程引用一個無效的物理地址時( 內核) 會向進程發送標識符爲11 的signal/ 消息/ 信號 ( 參考linux 內存管理的頁錯誤異常處理程序, 以及linux 中斷處理).
信號有兩個目的:
1. 使一個進程意識到一個特殊事件發生了( 不一樣的事件用不一樣的signal 標識)
2. 並使目標進程進行相應處理(eg: 執行的信號處理函數 , signal handler). 相應的處理也能夠是忽略它 .
固然 , 這兩個目的不是互斥的 , 由於一般一個進程意識到一個事件發生後就會執行該事件相應的處理函數 .
下表是linux2.6 在80x86 上的前31 個signals 及其相關說明 . 這些信號中有些是體系結構相關的(eg:SIGCHLD,SIGSTOP), 有些則專門了某些體系結構才存在的(eg:SIGSTKFLT) ( 能夠參考中斷處理 , 裏面也列出了一些異常對應的signal).
The first 31 signals in Linux/i386 |
||||
# |
Signal name |
Default action |
Comment |
POSIX |
1 |
SIGHUP |
Terminate |
Hang up controlling terminal or process |
Yes |
2 |
SIGINT |
Terminate |
Interrupt from keyboard |
Yes |
3 |
SIGQUIT |
Dump |
Quit from keyboard |
Yes |
4 |
SIGILL |
Dump |
Illegal instruction |
Yes |
5 |
SIGTRAP |
Dump |
Breakpoint for debugging |
No |
6 |
SIGABRT |
Dump |
Abnormal termination |
Yes |
6 |
SIGIOT |
Dump |
Equivalent to SIGABRT |
No |
7 |
SIGBUS |
Dump |
Bus error |
No |
8 |
SIGFPE |
Dump |
Floating-point exception |
Yes |
9 |
SIGKILL |
Terminate |
Forced-process termination |
Yes |
10 |
SIGUSR1 |
Terminate |
Available to processes |
Yes |
11 |
SIGSEGV |
Dump |
Invalid memory reference |
Yes |
12 |
SIGUSR2 |
Terminate |
Available to processes |
Yes |
13 |
SIGPIPE |
Terminate |
Write to pipe with no readers |
Yes |
14 |
SIGALRM |
Terminate |
Real-timerclock |
Yes |
15 |
SIGTERM |
Terminate |
Process termination |
Yes |
16 |
SIGSTKFLT |
Terminate |
Coprocessor stack error |
No |
17 |
SIGCHLD |
Ignore |
Child process stopped or terminated, or got signal if traced |
Yes |
18 |
SIGCONT |
Continue |
Resume execution, if stopped |
Yes |
19 |
SIGSTOP |
Stop |
Stop process execution |
Yes |
20 |
SIGTSTP |
Stop |
Stop process issued from tty |
Yes |
21 |
SIGTTIN |
Stop |
Background process requires input |
Yes |
22 |
SIGTTOU |
Stop |
Background process requires output |
Yes |
23 |
SIGURG |
Ignore |
Urgent condition on socket |
No |
24 |
SIGXCPU |
Dump |
CPU time limit exceeded |
No |
25 |
SIGXFSZ |
Dump |
File size limit exceeded |
No |
26 |
SIGVTALRM |
Terminate |
Virtual timer clock |
No |
27 |
SIGPROF |
Terminate |
Profile timer clock |
No |
28 |
SIGWINCH |
Ignore |
Window resizing |
No |
29 |
SIGIO |
Terminate |
I/O now possible |
No |
29 |
SIGPOLL |
Terminate |
Equivalent to SIGIO |
No |
30 |
SIGPWR |
Terminate |
Power supply failure |
No |
31 |
SIGSYS |
Dump |
Bad system call |
No |
31 |
SIGUNUSED |
Dump |
Equivalent to SIGSYS |
No |
上述signal 稱爲regular signal . 除此以外, POSIX 還引入了另一類singal 即real-time signal
. real time signal 的標識符的值從32 到64. 它們與reagular signal 的區別在於每一次發送的real time signal 都會被加入懸掛信號隊列,因此屢次發送的real time signal 會被緩存起來( 而不會致使後面的被忽略掉) . 而同一種( 即標識符同樣) regular signal 不會被緩存, 即若是同一個signal 被髮送屢次 , 它們只有一個會被放入接受進程的懸掛隊列 .
雖然linux kernel 並無使用real time signal. 可是它也( 經過特殊的系統調用) 支持posix 定義的real time signal.
有不少系統調用能夠給進程發送singal, 也有不少系統調能夠指定進程在接收某一個signal 時應該如何響應( 即實行哪個函數). 下表給出了這類系統調用: ( 關於這些系統調用的更多信息參考下文)
System call |
Description |
kill( ) |
Send a signal to a thread group |
tkill( ) |
Send a signal to a process |
tgkill( ) |
Send a signal to a process in a specific thread group |
sigaction( ) |
Change the action associated with a signal |
signal( ) |
Similar to sigaction( ) |
sigpending( ) |
Check whether there are pending signals |
sigprocmask( ) |
Modify the set of blocked signals |
sigsuspend( ) |
Wait for a signal |
rt_sigaction( ) |
Change the action associated with a real-time signal |
rt_sigpending( ) |
Check whether there are pending real-time signals |
rt_sigprocmask( ) |
Modify the set of blocked real-time signals |
rt_sigqueueinfo( ) |
Send a real-time signal to a thread group |
rt_sigsuspend( ) |
Wait for a real-time signal |
rt_sigtimedwait( ) |
Similar to rt_sigsuspend( ) |
signal 可能在任意時候被髮送給一個狀態未知的進程 . 當信號被髮送給一個當前並不正在執行的進程時, 內核必須把先把該信號保存直到該進程恢復執行. (to do ???????)
被阻塞的信號儘管會被加入進程的懸掛信號隊列 , 可是在其被解除阻塞以前不會被處理(deliver),Blocking a signal (described later) requires that delivery of the signal be held off until it is later unblocked, which
acer s the problem of signals being raised before they can be delivered.
內核把信號傳送分紅兩個階段:
signal generation: 內核更新信號的目的進程的相關數據結構 , 這樣該進程就能知道它接收到了一個信號. 以爲稱爲收到信號階段更恰當. 這個generation 翻譯成目的進程接收也不錯 .
signal delivery(): 內核強制目的進程處理接收到的信號,這主要是經過修改進程的執行狀態或者在目的進程中執行信號處理函數來實現的 . 以爲稱爲處理收到的信號階段更恰當 . diliver 這裏翻譯成處理更恰當 .
deliver 的翻譯: 有不少個 , 估計翻譯成in computing 比較合理
一個genearated signal 最多隻能deliver 一次( 即一個信號最多隻會被處理一次) . signal 是可消耗資源 , 一旦一個signal 被deliver, 那麼全部進程對它的引用都會被取消 .
已經產生可是還未被處理(deliver) 的信號稱爲pending signal ( 懸掛信號). 對於regular signal, 在某一個時刻 , 一種signal 在一個進程中只能有一個實例( 由於進程沒有用隊列緩存其收到的signal) . 由於有31 種regualar signal , 因此一個進程某一個時刻能夠有31 個各種signal 的實例. 此外由於linux 進程對real time signal 採用不一樣的處理方式, 它會保存接收到的real
time signal 的實例 , 因此能夠同時有不少同種signal 的實例 .
問題: 不一樣種類的信號的優先級( 從值較小的開始處理) .
通常而言 , 一個信號可能會被懸掛很長是時間( 即一個進程收到一個信號後 , 該信號有可能在該進程裏好久 , 由於進程沒空來處理它), 主要有以下因素:
1. 信號一般被當前進程處理 . Signals are usually delivered only to the currently running process (that is, to the current process).
2. 某種類型的信號可能被本進程阻塞. 只有當其被取消阻塞好纔會被處理 .
3. 當一個進程執行某一種信號的處理函數時 , 通常會自動阻塞這種信號 , 等處理完畢後纔會取消阻塞 . 這意味着一個信號處理函數不會被同種信號阻塞 .
儘管信號在概念上很直觀 , 可是內核的實現卻至關複雜. 內核必須:
1. 記錄一個進程阻塞了哪些信號
2. 當從核心態切換到用戶態時 , 檢查進程是否接受到了signal.( 幾乎每一次時鐘中斷都要幹這樣的事 , 費時嗎?).
3. 檢查信號是否能夠被忽略. 當以下條件均知足時則可被忽略:
1). 目標進程未被其它進程traced( 即PT_PTRACED==0). 但一個被traced 的進程收到一個信號時 , 內核中止目標線程 , 而且給tracing 進程發送信號SIGCHLD. tracing 進程可能會經過SIGCONT 來恢復traced 進程的執行
2). 目標進程未阻塞該信號 .
3). 信號正被目標進程忽略( 或者因爲忽略是顯式指定的或者因爲忽略是默認操做).
4. 處理信號 . 這可能須要切換到信號處理函數
此外, linux 還須要處理BSD, System V 中signal 語義的差別性 . 另外 , 還須要遵照POSIX 的定義 .
處理信號的方式 (Actions Performed upon Delivering a Signal)
一個進程能夠採用三中方式來響應它接收到的信號:
1.(ignore) 顯示忽略該信號
2.(default) 調用默認的函數來響應該信號( 這些默認的函數由內核定義) , 通常這些默認的函數都分紅以下幾種( 採用哪種取決於信號的類型 , 參考前面的表格):
Terminate: The process is terminated (killed)
Dump: The process is terminated (killed) and a core file containing its execution context is created, if possible; this file may be used for debug purposes.
Ignore:The signal is ignored.
Stop:The process is stopped, i.e., put in the TASK_STOPPED state.
Continue:If the process was stopped (TASK_STOPPED), it is put into the TASK_RUNNING state.
3.(catch) 調用相應的信號處理函數 ( 這個信號處理函數一般是程序員在運行時指定的). 這意味着進程須要在執行時顯式地指明它須要catch 哪種信號. 而且指明其處理函數 . catch 是一種主動處理的措施 .
注意上述的三個處理方式被標識爲:ignore, default, catch. 這三個處理方式之後會經過這三個標識符引用 .
注意阻塞一個信號和忽略一個信號是不一樣 , 一個信號被阻塞是就當前不會被處理 , 即一個信號只有在解除阻塞後纔會被處理 . 忽略一個信號是指採用忽略的方式來處理該信號( 即對該信號的處理方式就是什麼也不作) .
SIGKILL 和SIGSTOP 這兩個信號不能忽略 , 不能阻塞 , 不能使用用戶定義的函數(caught) . 因此老是執行它們的默認行爲 . 因此 , 它們容許具備恰當特權級的用戶殺死別的進程, 而沒必要在乎被殺進程的防禦措施 ( 這樣就容許高特權級用戶殺死低特權級的用戶佔用大量cpu 的時間) .
注: 有兩個特殊狀況. 第一 , 任意進程都不能給進程0( 即swapper 進程) 發信號 . 第二 , 發給進程1 的信號都會被丟棄(discarded), 除非它們被catch. 因此進程 0 不會死亡, 進程1 僅在int 程序結束時死亡 .
一個信號對一個進程而言是致命的(fatal) , 當前僅當該信號致使內核殺死該進程 . 因此,SIGKILL 老是致命的. 此外 , 若是一個進程對一個信號的默認行爲是terminate 而且該進程沒有catch 該信號 , 那麼該信號對這個進程而言也是致命的 . 注意 , 在catch 狀況下 , 若是一個進程的信號處理函數本身殺死了該進程 , 那麼該信號對這個進程而言不是致命的 , 由於不是內核殺死該進程而是進程的信號處理函數本身殺死了該進程.
POSIX 信號以及多線程程序
POSIX 1003.1 標準對多線程程序的信號處理有更加嚴格的要求:
( 因爲linux 採用輕量級進程來實現線程 , 因此對linux 的實現也會有影響)
1. 多線程程序的全部線程應該共享信號處理函數 , 可是每個線程必須有本身的mask of pending and blocked signals
2. POSIX 接口kill( ), sigqueue( ) 必須把信號發給線程組 , 而不是指定線程. 另外內核產生的SIGCHLD, SIGINT, or SIGQUIT 也必須發給線程組 .
3. 線程組中只有有一個線程來處理(deliver) 的共享的信號就能夠了 . 下問介紹如何選擇這個線程 .
4. 若是線程組收到一個致命的信號 , 內核要殺死線程組的全部線程, 而不是僅僅處理該信號的線程 .
爲了聽從POSIX 標準, linux2.6 使用輕量級進程實現線程組.
下文中 , 線程組表示OS 概念中的進程, 而線程表示linux 的輕量級進程. 進程也( 更多地時候) 表示linux 的輕量級進程 . 另外每個線程有一個私有的懸掛信號列表 , 線程組共享一個懸掛信號列表 .
與信號有關的數據結構
注:pending/ 懸掛信號, 表示進程收到信號 , 可是尚未來得及處理 , 或者正在處理可是尚未處理完成 .
對於每個進程, 內核必須知道它當前懸掛(pending) 着哪些信號或者屏蔽(mask) 着哪些信號 . 還要知道線程組如何處理信號. 爲此內核使用了幾個重要的數據結構( 它們可經過task 實例訪問), 以下圖:
The most significant data structures related to signal handling
( 注意task 中的一些關於signal 的成員在上圖中沒有表現出來)
task 中關於signal 的成員列在下表中:
blocked 成員 保存進程masked out 的signal . 其類型爲sigset_t
, 定義以下:
typedef struct {
unsigned long sig[2];
} sigset_t;
sizeof(long)==32, sigset_t 被當成了bit array 使用. 正如前文提到的,linux 有64 種信號 , [1,31] 爲regular signal, [32,64] 爲real time signal. 每一種對應sigset_t 中一個bit.
信號描述符& 信號處理函數描述符
task 的signal, sighand 成員分別是信號描述符與信號處理函數描述符 .
signal 成員 是一個指針 , 它指向結構體signal_struct 的實例 , 該實例保存了線程組懸掛着的信號 . 也就是說線程組中的全部進程( 這裏稱爲task 更合理) 共用同一個signal_struct 實例. signal_struct 中的shared_pending 成員保存了全部懸掛的信號( 以雙向鏈表組織) . 此外signal_struct 中還保存了許多其它的信息(eg: 進程資源限制信息, pgrp, session 信息) .
下表列出了signal_struct 中與信號處理有關的成員:
除了signal 成員外 , 還有一個sighand 成員 用來指明相應的信號處理函數.
sighand 成員是一個指針 , 指向一個sighand_struct 變量 , 該變量爲線程組共享 . 它描述了一個信號對應的信號處理函數.
sighand_struct 成員以下:
The fields of the signal handler descriptor |
||
Type |
Name |
Description |
atomic_t |
count |
Usage counter of the signal handler descriptor |
struct k_sigaction [64] |
action |
Array of structures specifying the actions to be performed upon delivering the signals |
spinlock_t |
siglock |
Spin lock protecting both the signal descriptor and the signal handler descriptor |
sighand_struct 中的重要成員是action, 它是一個數組 , 描述了每一種信號對應的信號處理函數 .
sigaction 數據結構
某一些平臺上, 會賦予一個signal 一些只能內核纔可見的屬性. 這些屬性與sigaction( 它在用戶態也可見) 構成告終構體k_sigaction. 在x86 上,k_sigaction 就是sigaction.
注: 用戶使用的sigaction 和內核使用的sigaction 結構體有些不一樣可是 , 它們存儲了相同的信息( 本身參考一下用戶態使用的sigaction 結構體吧).
內核的sigaction 的結構體的成員以下:
1)sa_handler: 類型爲 void (*)(int):
這個字段指示如何處理信號 . 它能夠是指向處理函數的指針 , 也能夠是SIG_DFL(==0) 表示使用默認的處理函數 , 還能夠是SIG_IGN(==1) 表示忽略該信號
2)sa_flags: 類型爲unsigned long:
指定信號如何被處理的標誌 , 參考下表 ( 指定信號如何處理的標誌) .
3)sa_mask: 類型爲sigset_t:
指定當該信號處理函數執行時,sa_mask 中指定的信號必須屏蔽 .
指定信號如何處理的標誌
注: 因爲歷史的緣由 , 這些標誌的前綴爲SA_, 這和irqaction 的flag 相似 , 但其實它們沒有關係 .
Flags specifying how to handle a signal |
|
Flag Name |
Description |
SA_NOCLDSTOP |
Applies only to SIGCHLD ; do not send SIGCHLD to the parent when the process is stopped |
SA_NOCLDWAIT |
Applies only to SIGCHLD ; do not create a zombie when the process terminates |
SA_SIGINFO |
Provide additional information to the signal handler |
SA_ONSTACK |
Use an alternative stack for the signal handler |
SA_RESTART |
Interrupted system calls are automatically restarted |
SA_NODEFER, SA_NOMASK |
Do not mask the signal while executing the signal handler |
SA_RESETHAND, SA_ONESHOT |
Reset to default action after executing the signal handler |
懸掛的信號隊列 (sigpending)
經過前文咱們知道有些系統調用可以給線程組發信號(eg:kill, rt_sigqueueinfo), 有些操做給指定的進程發信號(eg:tkill, tgkill) .
爲了區分這兩類, task 中其實有兩種懸掛信號列表:
1.task 的 pending 字段表示了本task 上私有的懸掛信號( 列表)
2.task 的signal 字段中的shared_pending 字段則保存了線程組共享的懸掛信號( 列表).
懸掛信號 列表用數據結構sigpending 表示 , 其定義以下:
struct sigpending {
struct list_head list;
sigset_t signal;
}
其signal 成員指明當前懸掛隊列懸掛了哪些信號 .
其list 字段實際上是一個雙向鏈表的頭 , 鏈表的元素的類型是sigqueue. sigqueue 的成員以下:
The fields of the sigqueue data structure |
||
Type |
Name |
Description |
struct list_head |
list |
Links for the pending signal queue's list |
spinlock_t * |
lock |
Pointer to the siglock field in the signal handler descriptor corresponding to the pending signal |
Int |
flags |
Flags of the sigqueue data structure |
siginfo_t |
info |
Describes the event that raised the signal |
struct user_struct * |
user |
Pointer to the per-user data structure of the process's owner |
( 注:sigqueue 的名字有queue, 但它其實只是懸掛隊列的一個元素 . 它會記錄一個被懸掛的信號的信息)
siginfo_t 是一個包含128 byte 的數據結構 , 用來描述一個指定信號的發生,其成員以下:
si_signo: 信號ID
si_errno: 致使這個信號被髮出的錯誤碼. 0 表示不是由於錯誤才發出信號的 .
The most significant signal sender codes |
|
Code Name |
Sender |
SI_USER |
kill( ) and raise( ) |
SI_KERNEL |
Generic kernel function |
SI_QUEUE |
sigqueue( ) |
SI_TIMER |
Timer expiration |
SI_ASYNCIO |
Asynchronous I/O completion |
SI_TKILL |
tkill() and tgkill() |
_sifields: 這個字段是一個union, 它有很多成員 , 哪個成員有效取決於信號 . 好比對於SIGKILL, 則它會記錄信號發送者的PID,UID; 對於SIGSEGV, 它會存儲致使訪問出錯的內存地址 .
操做信號數據結構的函數
一些宏和函數會使用信號數據結構 . 在下文的解說中, set 表示指向sigset_t 變量的指針, nsig 表示信號的標識符( 信號的整數值).mask 是一個unsign long bit mask.
sigemptyset (set) and sigfillset (set)
把set 全部bit 設置爲 0 或者1 .
sigaddset (set,nsig) and sigdelset (set,nsig)
把set 中對應與nsig 的bit 設置爲1 或者 0. In practice, sigaddset( ) reduces to:
set->sig[(nsig - 1) / 32] |= 1UL << ((nsig - 1) % 32);
and sigdelset( ) to:
set->sig[(nsig - 1) / 32] &= ~(1UL << ((nsig - 1) % 32));
sigaddsetmask (set,mask) and sigdelsetmask (set,mask)
根據mask 的值設置set. 僅能設置1-32 個signal. The corresponding functions reduce to:
set->sig[0] |= mask;
and to:
set->sig[0] &= ~mask;
sigismember (set,nsig)
返回set 中對應nsig 的bit 的值. In practice, this function reduces to:
return 1 & (set->sig[(nsig-1) / 32] >> ((nsig-1) % 32));
sigmask (nsig)
根據信號標誌碼nsig 等到它的在sigset_t 中的bit 位的index.
sigandsets (d,s1,s2), sigorsets (d,s1,s2), and
signandsets (d,s1,s2)
僞代碼以下:d=s1 & s2; d=s1|s2, d=s1 & (~s2)
sigtestsetmask (set,mask)
若是mask 中的爲1 的位在set 中的相應位也爲1, 那麼返回1. 不然返回0. 只適用於1-32 個信號.
siginitset (set,mask)
用mask 設置set 的1-32 個信號, 並把set 的33-63 個信號清空.
siginitsetinv (set,mask)
用(!mask) 設置set 的1-32 個信號, 並把set 的33-63 個信號設置爲1.
signal_pending (p)
檢查p 的 t->thread_info->flags 是否爲 TIF_SIGPENDING. 即檢查p 是否有 懸掛的非阻塞信號.
recalc_sigpending_tsk (t) and recalc_sigpending ( )
第一個函數檢查 t->pending->signal 或者 t->signal->shared_pending->signal 上是否有懸掛的非阻塞信號. 如有設置 t->thread_info->flags 爲 TIF_SIGPENDING.
recalc_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 標誌, 而且調用flush_sigqueue 把t->pending 和 t->signal->shared_pending 清掉 .
Generating a Signal
不少內核函數會產生signal, 它完成處理處理的第一個階段(generate a signal) , 即更新信號的目標進程的相應字段 . 可是它們並不直接完成信號處理的第二階段(deliver the signal), 可是它們會根據目標進程的狀態或者喚醒目標進程或者強制目標進程receive the signal .
注:generating a signal 這個階段是從源進程發起一個信號 , 而後源進程在內核態下修改目標進程的相應狀態, 而後可能源進程還會喚醒目的進程 .
不管一個信號從內核仍是從另一個進程被髮送給另外一個線程( 目標進程) , 內核都會執行以下的函數之一來發送信號:
Kernel functions that generate a signal for a process |
|
Name |
Description |
send_sig( ) |
Sends a signal to a single process |
send_sig_info( ) |
Like send_sig( ) , with extended information in a siginfo_t structure |
force_sig( ) |
Sends a signal that cannot be explicitly ignored or blocked by the process |
force_sig_info( ) |
Like force_sig( ) , with extended information in a siginfo_t structure |
force_sig_specific( ) |
Like force_sig( ) , but optimized for SIGSTOP and SIGKILL signals |
sys_tkill( ) |
System call handler of tkill( ) |
sys_tgkill( ) |
全部這些函數最終都會調用 specific_send_sig_info ( ) .
不管一個信號從內核仍是從另一個進程被髮送給另外一個線程組( 目標進程), 內核都會執行以下的函數之一來發送信號:
Kernel functions that generate a signal for a thread group |
|
Name |
Description |
send_group_sig_info( ) |
Sends a signal to a single thread group identified by the process descriptor of one of its members |
kill_pg( ) |
Sends a signal to all thread groups in a process group |
kill_pg_info( ) |
Like kill_pg( ) , with extended information in a siginfo_t structure |
kill_proc( ) |
Sends a signal to a single thread group identified by the PID of one of its members |
kill_proc_info( ) |
Like kill_proc( ) , with extended information in a siginfo_t structure |
sys_kill( ) |
System call handler of kill( ) |
sys_rt_sigqueueinfo( ) |
System call handler of rt_sigqueueinfo( ) |
這些函數最終都調用 group_send_sig_info ( ) .
specific_send_sig_info 函數說明
這個函數給指定的目標線程( 目標進程) 發送一個信號 . 它有三個參數:
參數sig: 信號( 即某一個信號) .
參數info: 或者是 siginfo_t 變量地址或者以下三個特殊值:
0 : 表示信號由用戶態進程發送;
1 : 表示信號由核心態( 進程) 發送;
2 : 表示信號由核心態( 進程) 發送, 而且信號是SIGKILL 或者SIGSTOP.
參數t: 目標進程的task 實例指針
specific_send_sig_info 調用時必須禁止本cpu 的中斷 , 而且得到t->sighand->siglock spin lock. 它會執行以下操做:
1. 檢查目標線程是否忽略該信號, 如果返回0. 當以下三個條件均知足時則可認爲忽略該信號:
1). 目標線程未被traced( 即t->ptrace 不含PT_PTRACED 標誌).
2). 該信號未被目標線程阻塞( 即sigismember(&t->blocked, sig) == 0).
3). 該信號被目標線程顯式地忽略( 即t->sighand->action[sig-1].sa_handler == SIG_IGN) 或者隱式忽略( 即handler==SIG_DFT 而且信號爲SIGCONT, SIGCHLD, SIGWINCH, or SIGURG.).
2. 檢查信號是不是非實時信號(sig<32) 而且一樣的信號是否已經在線程的私有懸掛信號隊列中了, 如果則返回0.
3. 調用send_signal(sig, info, t, &t->pending) 把信號加入目標線程的私有懸掛信號隊列中. 下文會詳述.
4. 若是send_signal 成功而且信號未被目標線程阻塞, 則調用signal_wake_up ( ) 來通知目標進程有新的信號達到. 這個函數執行以下步驟:
1). 把標誌TIF_SIGPENDING 加到t->tHRead_info->flags 中
2). 調用try_to_wake_up(). 若是目標線程處於TASK_INTERRUPTIBLE 或者TASK_STOPPED 而且信號是SIGKILL 則喚醒目標線程.
3). 若是try_to_wake_up 返回0, 則目標線程處於runnable 狀態, 以後檢查目標線程是否在別的CPU 上執行, 若是是則向該CPU 發送處理器中斷以強制該cpu 重調度目標線程( 注: 目前咱們並未考慮多處理器的狀況). 由於每個線程在從schedule() 返回時都會檢查是否存在懸掛的信號, 因此這個處理器中斷將會使目標線程很快就看到這個新的懸掛信號.
5. 返回1( 表示信號已經成功generated.)
send_signal 函數
這個函數接受四個參數:sig, info, t, signals. 其中sig, info,t 在specific_send_sig_info
中已經介紹過了. signals 則是t 的pending queue 的首地址 . 它的執行流程如:
1. 若info==2, 那麼這個信號是SIGKILL 或是SIGSTOP, 而且由kernel 經過force_sig_specific 產生. 此時直接跳到9. 由於這種狀況下, 內核會當即執行信號處理, 因此不用把該信號加入信號懸掛隊列中.
2. 若是目標進程的用戶當前的懸掛信號數目(t->user->sigpending) 小於目標進程的最大懸掛信號數目(t->signal->rlim[RLIMIT_SIGPENDING].rlim_cur), 則爲當前信號分配一個sigqueue 變量, 標識爲q
3. 若是目標進程的用戶當前的懸掛信號數目太大, 或者上一步中分配sigqueue 變量失敗, 則跳到9.
4. 增長目標進程的用戶當前的懸掛信號數目(t->user->sigpending) 以及t-user 的引用數.
5. 把信號q 加入目標線程的懸掛隊列:
list_add_tail(&q->list, &signals->list);
6. 填充q, 以下
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._sifields._kill._uid = 0;
} else
copy_siginfo(&q->info, info);
函數copy_siginfo 用caller 傳進來的info 填充q->info
7. 設置懸掛信號隊列中的mask 成員的與sig 相應的位( 以表示該信號在懸掛信號隊列中)
sigaddset(&signals->signal, sig);
7. 返回0 以表示信號被成功加入懸掛信號隊列.
9. 若是執行這一步, 則該信號不會被加入信號懸掛隊列, 緣由有以下三個:1) 有太多的懸掛信號了, 或者2) 沒有空閒的空間來分配sigqueue 變量了, 或者3) 該信號的處理由內核當即執行. 若是信號是實時信號而且經過內核函數發送而且顯式要求加入隊列, 那麼返回錯誤代碼-EAGAIN( 代碼相似以下):
if (sig>=32 && info && (unsigned long) info != 1 &&
info->si_code != SI_USER)
return -EAGAIN;
10. 設置懸掛信號隊列中的mask 成員的與sig 相應的位( 以表示該信號在懸掛信號隊列中)
sigaddset(&signals->signal, sig);
11. 返回0. 儘管該信號沒有放到懸掛信號隊列中, 可是相應的signals->signal 中已經設置了
即便沒有空間爲信號分配sigqueue 變量,也應該讓目標信號知道相應的信號已經發生, 這一點很重要. 考慮以下情形: 目標進程使用了不少內存以至於沒法再分配sigqueue 變量了, 可是內核必須保證對目標進程依的kill 依然可以成功, 不然管理員就沒有機會殺死目標進程了.
group_send_sig_info 函數
函數 group_send_sig_info 把一個信號發給一個線程組 . 這個函數有三個參數:sig, info, p . ( 和specific_send_sig_info 相似).
這個函數的執行流程以下 :
1. 檢查參數sig 的正確性:
if (sig < 0 || sig > 64)
return -EINVAL;
2. 若是信號的發送進程處於用戶態, 則檢查這個發送操做是否容許. 僅當知足以下條件之一( 才視爲容許):
1). 發送者進程有恰當的權限( 一般發送者進程應該是system administrator).
2). 信號爲SIGCONT, 而且目標進程和發送者進程在同一個login session.
3). 目標進程和發送者進程屬於同一個用戶
3. 若是用戶態的進程不能發送此信號, 則返回-EPERM. 若是sig==0, 則當即返回.( 由於0 是無效的信號). 若是sighand==0, 也當即返回, 由於此時目標進程正在被殺死, 從而sighand 被釋放.
if (!sig || !p->sighand)
return 0;
4. 得到鎖 p->sighand->siglock, 而且關閉本cpu 中斷.
5. 調用handle_stop_signal 函數, 這個函數檢查sig 是否會和現有的懸掛的信號衝突, 會的話解決衝突. 這個函數的步驟以下:
1). 若是線程組正在被殺死(SIGNAL_GROUP_EXIT) ,則返回.
2). 若是sig 是IGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 中的一種, 則調用rm_from_queue, 把線程組中全部懸掛的SIGCONT 刪除. 注意: 包含線程組共享的懸掛信號隊列中的(p->signal->shared_pending) 以及每個線程私有懸掛隊列中的.
3). 若是sig 是SIGCONT, 則調用rm_from_queue, 把線程組中全部懸掛的SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 刪除. 注意: 包含線程組共享的懸掛信號隊列中的(p->signal->shared_pending) 以及每個線程私有懸掛隊列中的. 以後爲每個線程調用try_to_wake_up.
6. 檢查線程組是否忽略該信號, 若是忽略返回0.
7. 若是是非實時信號, 而且該線程組已經有這種懸掛的信號了, 那麼返回0:
if (sig<32 && sigismember(&p->signal->shared_pending.signal,sig))
return 0;
8. 調用send_signal( ) 把信號加到線程組的共享懸掛信號隊列中, 若是send_signal 返回非0 值, 則group_send_sig_info 退出並把該非零值返回.
9. 調用_ _group_complete_signal( ) 來喚醒線程組中的一個輕量級進程. 參考下文.
10. 釋放p->sighand->siglock 而且打開本地中斷.
11. 返回 0 (success).
函數 _ _group_complete_signal ( ) 掃描目標線程組 , 而且返回一個可以處理 (receive) 該新信號的進程 . 這樣的進程必須同時具有以下的條件 :
1) 該進程不阻塞新信號.
2) 進程的狀態不是EXIT_ZOMBIE, EXIT_DEAD, TASK_TRACED, or TASK_STOPPED. 可是當信號是SIGKILL 是, 進程的狀態容許是TASK_TRACED or TASK_STOPPED.
3) 進程不處於正在被殺死的狀態, 即狀態不是PF_EXITING.
4) 或者進程正在某一個cpu 上執行, 或者進程的TIF_SIGPENDING 的標誌未被設置.
一個線程組中知足上訴條件的線程( 進程) 可能不少, 根據以下原則選擇一個:
1) 若是group_send_sig_info 中的參數p 指定的進程知足上述條件, 則選擇p.
2) 不然從最後一個接收線程組信號的線程(p->signal->curr_target) 開始查找知足上述條件的線程, 找到爲止.
( 若是線程組中沒有一個線程知足上述條件怎麼辦?)
如_ _group_complete_signal( ) 成功找到一個進程( 表示爲selected_p), 那麼:
1. 檢查該信號是不是致命的, 如果, 經過給線程組中的每個線程發送SIGKILL 來殺死線程組
2. 若不是, 調用signal_wake_up 來喚醒selected_p 並告知它有新的懸掛信號,
Delivering a Signal
經過上面的介紹, 內核經過修改目標進程的狀態, 告知目標進程有新的信號到達. 可是目標進程對到達的新信號的處理(deliver signal) 咱們尚未介紹. 下面介紹目標進程如何在內核的幫助下處理達到的新信號.
注意當內核( 代碼) 要把進程從核心態恢復成用戶態時( 當進程從異常/ 中斷處理返回時), 內核會檢查該進程的 TIF_SIGPENDING 標識 , 若是存在懸掛的信號 , 那麼將先處理該信號 .
這裏須要介紹一下背景: 當進程在用戶態( 用U1 表示) 下因爲中斷/ 異常而進入核心態, 那麼須要把U1 的上下文記錄到該進程的內核堆棧中.
爲了處理非阻塞的信號 , 內核調用do_signal 函數 . 這個函數接受兩個參數:
regs: 指向U1 上下文在內核堆棧的首地址 ( 參考進程管理).
oldest: 保存了一個變量的地址, 該變量保存了被阻塞的信號的信息( 集合). 若是該參數爲NULL, 那麼這個地址就是¤t->blocked ( 以下文). 注意當自定義信號處理函數結束後, 會把oldest 設置爲當前task 的阻塞信號集合.( 參考源代碼, 以及rt_frame 函數).
咱們這裏描述的do_signal 流程將會關注信號delivery( 處理), 而忽略不少細節, eg: 競爭條件 , 產生core dump, 中止和殺死線程組等等 .
通常,do_signal 通常僅在進程即將返回用戶態時執行 . 所以 , 若是一箇中斷處理函數調用do_signal, 那麼do_signal 只要按以下方式放回:
if ((regs->xcs & 3) != 3)
return 1;
若是oldest 爲NULL, 那麼 do_signal 會把它設置爲當前進程阻塞的信號:
if (!oldset)
oldset = ¤t->blocked;
do_signal 的核心是一個循環 , 該循環調用dequeue_signal 從進程的私有懸掛信號隊列和共享懸掛隊列獲取未被阻塞的信號. 若是成功得到這樣的信號, 則經過handle_signal 調用相應的信號處理函數, 不然退出do_signal .
( 這個循環不是用C 的循環語句來實現, 而是經過修改核心棧的regs 來實現. 大概的流程能夠認爲以下: 當由核心態時切換向用戶態時, 檢查是否有非阻塞的懸掛信號, 有則處理( 包含: 準備信號處理函數的幀, 切換到用戶態以執行信號處理函數, 信號處理函數返回又進入核心態), 無則返回原始的用戶態上下文)
dequeue_signal 先從私有懸掛信號列表中按照信號值從小到大取信號,取完後再從共享懸掛信號列表中取 . ( 注意取後要更新相應的信息)
接着咱們考慮, do_signal 如何處理得到的信號( 假設用signr 表示) .
首先 , 它會檢查是否有別的進程在監控(monitoring) 本進程 , 若是有 , 調用do_notify_parent_cldstop 和schedule 來讓監控進程意識到本進程開始信號處理了.
接着,do_signal 得到相應的信號處理描述符( 經過current->sig->action[signr-1]) , 從而得到信號處理方式的信息 . 總共有三種處理方式: 忽略 , 默認處理 , 使用用戶定義的處理函數
.
若是是忽略 , 那麼什麼也不作 :
if (ka->sa.sa_handler == SIG_IGN)
continue;
執行默認的信號處理函數
若是指定的是默認的處理方式. 那麼do_signal 使用默認的處理方式來處理信號 . ( 進程 0 不會涉及 , 參考前文)
對於init 進程除外 , 則它要丟棄信號:
if (current->pid == 1)
continue;
對於其它進程, 默認的處理方式取決於信號 .
第一類: 這類信號的默認處理方式就是不處理
if (signr==SIGCONT || signr==SIGCHLD ||
signr==SIGWINCH || signr==SIGURG)
continue;//
第二類: 這類信號的默認處理方式以下:
if (signr==SIGSTOP || signr==SIGTSTP ||
signr==SIGTTIN || signr==SIGTTOU) {
if (signr != SIGSTOP &&
is_orphaned_pgrp(current->signal->pgrp))
continue;
do_signal_stop(signr);
}
這裏, SIGSTOP 與其餘的信號有些微的區別.
SIGSTOP 中止整個線程組. 而其它信號只會中止不在孤兒進程組中的進程( 線程組).
孤兒進程組(orphand process group).
非孤兒進程組 指若是進程組A 中有一個進程有父親, 而且該父進程在另一個進程組B 中, 而且這兩個進程組A,B 都在用一個會話(session) 中, 那麼進程組A 就是非孤兒進程組. 所以若是父進程死了, 可是啓動在進程的session 依舊在, 那麼進程組A 都不是孤兒.
注: 這兩個概念讓我迷糊.
do_signal_stop 檢查當前進程是不是線程組中的第一個正在被中止的進程, 若是是, 它就激活一個組停(group stop) 。本質上, 它會把信號描述符的 group_stop_count 字段設置爲正值, 而且喚醒線程組中的每個進程。每個進程都會查看這個字段從而認識到正在中止整個線程組, 並把本身的狀態改成 TASK_STOPPED, 而後調用schedule. do_signal_stop 也會給線程組的父進程發送SIGCHLD, 除非父進程已經被設置爲SA_NOCLDSTOP
flag of SIGCHLD.
默認行爲是dump 的信號處理可能會進程工做目錄下建立一個core 文件. 這個文件列出了進程的地址空間和cpu 寄存器的值. do_signal 建立這個文件後, 就會殺死整個線程組. 剩下18 個信號的默認處理是terminate, 這僅僅是簡單地殺死整個線程組. 爲此,do_signal 調用了do_group_exit 。
使用指定的函數來處理信號(catching the signal)
若是程序爲信號設置了處理函數 , 那麼do_signal 將會經過調用handle_signal 來強制該信號函數被執行:
handle_signal(signr, &info, &ka, oldset, regs);
if (ka->sa.sa_flags & SA_ONESHOT)
ka->sa.sa_handler = SIG_DFL;
return 1;
若是用戶在爲信號設置信號處理函數時指定了 SA_ONESHOT , 那麼當該信號處理函數第一次執行後 , 其將會被reset. 即之後來的這樣的信號將會使用默認的處理函數 .
Notice how do_signal( ) returns after having handled a single signal. Other pending signals won't be considered until the next invocation of do_signal( ) . This approach ensures that real-time signals will be dealt with in the proper order.
執行一個信號處理函數至關複雜 , 由於須要內核當心處理用戶信號處理函數的調用棧, 而後把控制權交給用戶處理函數( 注意這裏涉及內核態到用戶態的轉換) .
用戶的信號處理函數定義在用戶態中而且包含在用戶代碼段中,它須要在用戶態(U2) 下執行. hande_signal 函數在覈心態下執行. 此外, 因爲當前的核心態是在前一個用戶態(U1) 轉過來, 這意味着當信號處理函數(U2) 結束, 回到內核態, 而後內核態還須要回到U1, 而當從U2 進入核心態後, 內核棧存放的已經再也不是U1 的上下文了( 而是U2), 此外通常信號處理函數中還會發生系統調用( 用戶態到核心態的轉換), 而系統調用結束後要回到信號處理函數.
注意: 每個內核態切換到用戶態, 進程的內核堆棧都會被清空.
那麼handle_signal 如何調用信號處理函數呢??
Linux 採用的方法以下: 每次調用信號處理函數以前, 把U1 的上下文拷貝到信號處理函數的棧中( 通常信號處理函數的棧也是當前進程的用戶態的棧, 可是程序員也能夠在設置信號處理函數時指定一個本身定義的棧, 可是這裏不影響這個方法, 因此咱們只描述信號處理函數使用進程用戶態的棧的狀況). 而後再執行信號處理函數. 而當信號處理函數結束以後, 會調用sigreturn() 從U2 的棧中把U1 的上下文拷貝到內核棧中.
下圖描述了信號處理函數的執行流程. 一個非阻塞的信號發給目標進程. 當一箇中斷或異常發生後, 目標進程從用戶態(U1) 進入核心態. 在它切換回用戶態(U1) 以前, 內核調用do_signal. 這個函數逐一處理懸掛的非阻塞信號. 而若是目標進程設置了對信號的處理函數, 那麼它會調用handle_signal 來調用自定義的信號處理函數( 這期間須要使用 setup_frame 或setup_rt_frame 來爲信號處理函數設置棧 ), 此時當切換到用戶態時, 目標進程執行的是信號處理函數而不是U1.
當信號處理函數結束後, 位於 setup_frame 或setup_rt_frame 棧之上的返回代碼 ( return code) 被執行, 這返回代碼會執行sigreturn 或者rt_sigreturn 從而把U1 的上下文從setup_frame 或setup_rt_frame 棧中拷貝到核心棧. 而這結束後, 內核能夠切換回U1.
注意: 信號有三種處理方式, 只有使用自定義處理函數才須要這樣麻煩啊.
接下來咱們須要仔細瞧瞧這一切怎麼發生的.
爲了能恰當地爲信號處理函數設置棧,handle_signal 調用setup_frame( 當信號沒有相應的siginfo_t 時) 或者setup_rt_frame(
當信號有相應的siginfo_t 時). 爲了判斷採用哪種, 須要參考 sigaction 中的sa_flag 是否包含SA_SIGINO.
setup_frame 接受四個參數, 以下:
sig: 信號標識
ka: 與信號相關的 k_sigaction 實例
oldest: 進程阻塞的信號
regs: U1 上下爲在覈心棧的地址.
setup_frame 函數會在用戶棧中分配一個sigframe 變量, 該變量包含了可以正確調用信號處理函數的信息( 這些信息會被 sys_sigreturn 使用 ). sigframe 的成員以下( 其示意圖以下):
pretcode : 信號處理函數的返回地址. 其指向標記爲 kernel_sigreturn 的代碼
sig : 信號標識.
sc : sigcontext 變量. 它包含了U1 的上下文信息, 以及被進程阻塞的非實時信號的信息.
fpstate : _fpstate 實例, 用來存放U1 的浮點運算有關的寄存器.
extramask : 被進程阻塞的實時信號的信息 .
retcode :8 字節的返回代碼, 用於發射 sigreturn 系統調用. 早期版本的linux 用於信號處理函數返回後的善後處理.linux2.6 則用於特徵標誌, 因此調試器可以知道這是一個信號處理函數的棧.
setup_frame 函數首先得到sigframe 變量的地址, 以下:
frame =(regs->esp - sizeof(struct sigframe)) & 0xfffffff8
注意: 默認地信號處理函數使用獲得棧是進程在用戶態下的棧, 可是用戶在設置信號處理函數時能夠指定. 這裏只討論默認狀況. 對於用戶指定其實也同樣.
另外因爲棧從大地址到小地址增加, 因此上面的代碼要看明白了. 此外還須要8 字節對齊.
以後使用 access_ok 來驗證 frame 是否可用, 以後用__put_user 來填充frame 各個成員. 填充好以後, 須要修改核心棧, 這樣從核心態切換到用戶態時就能執行信號處理函數了, 以下:
regs->esp = (unsigned long) frame;
regs->eip = (unsigned long) ka->sa.sa_handler;
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 的實例, rt_sigframe 除了sigframe 外還包含了siginfo_t( 它描述了信號的信息). 另外它使用 _ _kernel_rt_sigreturn.
設置好棧後,handle_signal 檢查和信號有關的flags. 若是沒有設置 SA_NODEFER , 那麼在執行信號處理函數時, 就要阻塞sigaction.sa_mask
中指定的全部信號以及sig 自己. 以下:
if (!(ka->sa.sa_flags & SA_NODEFER)) {
spin_lock_irq(¤t->sighand->siglock);
sigorsets(¤t->blocked, ¤t->blocked, &ka->sa.sa_mask);
sigaddset(¤t->blocked, sig);
recalc_sigpending(current);
spin_unlock_irq(¤t->sighand->siglock);
}
如前文所述,recalc_sigpending 會從新檢查進程是否還有未被阻塞的懸掛信號, 並依此設置進程的 TIF_SIGPENDING 標誌.
注意: sigorsets(¤t->blocked, ¤t->blocked, &ka->sa.sa_mask) 等價於current->blocked |= ka->sa.sa_mask. 而current->blocked 原來的值已經存放在frame 中了.
handle_signal 返回到do_signal 後,do_signal 也當即返回.
do_signal 返回後, 進程由核心態切換到用戶態, 因而執行了信號處理函數.
Terminating the signal handler
信號處理函數結束後, 由於其返回值的地址( pretcode 指定的 ) 是_ _kernel_sigreturn 指向的代碼段, 因此就會執行_ _kernel_sigreturn 指向的代碼. 以下:
_ _kernel_sigreturn:
popl %eax
movl $_ _NR_sigreturn, %eax
int $0x80
這會致使 sigreturn 被執行 ( 會致使從用戶態切換到核心態).
sys_sigreturn 函數能夠計算獲得sigframe 的地址. 以下:
frame = (struct sigframe *)(regs.esp - 8);
if (verify_area(VERIFY_READ, frame, sizeof(*frame)) {
force_sig(SIGSEGV, current);
return 0;
}
接着, 它要從frame 中把進程真正阻塞的信號信息拷貝到current->blocked 中. 結果那些在sigaction 中懸掛的信號解除了阻塞. 以後調用 recalc_sigpending.
接着 sys_sigreturn 須要調用restore_sigcontext 把frame 的sc( 即U1 的上下文) 拷貝到內核棧中並把frame 從用戶棧中刪除.
_ _kernel_sigreturn 的處理與這相似.
從新執行系統調用( 被信號處理掐斷的系統調用 )
注: 當用核心態轉向用戶態時, 該核心態多是系統調用的核心態.
小小總結 : 當內核使用用戶指定的處理方式時 , 由於是從用戶態轉向內核態再轉向用戶態 , 因此其處理比較複雜 . 以下描述 : 當從用戶態 (U1) 轉入內核態後 , 在內核態試圖回到 U1 時 , 會先判斷是否有非阻塞的懸掛信號 , 若是有就會先調用用戶的處理函數 ( 即進入用戶態 , 這裏是用戶態 2), 處理完後 , 再回到內核態 , 而後再回到 U1. 注意在 U2 中也有可能發生系統調用從而再次進入內核態 . ( 注意在 U2 過程當中 , 系統處於關中斷狀態 , 因此信號處理應該儘量地快
), 咱們知道當用戶態進入核心態時會把用戶態的信息保存在覈心態的棧中 , 爲了不在從 U2 因系統調用再進入核心態是破壞 U1 在覈心態中的信息 , 在進入 U2 以前 , 要不 U1 在覈心棧中的信息拷貝到 U1 的棧中 , 並在 U2 返回後 , 再把 U2 棧中保存 U1 的信息拷貝會核心棧 .
注 :U2 使用的棧能夠和 U1 是同一個棧 , 也能夠是用戶在設置信號處理函數時指定的一段內存 .
當一個進程調用某些並不能立刻知足的系統調用(eg: 寫文件) 時, 內核會把該進程的狀態設置爲 TASK_INTERRUPTIBLE 或者TASK_UNINTERRUPTIBLE.
當一個進程( 表示爲wp) 處於TASK_INTERRUPTIBLE 狀態, 而另一個進程又給它發信號, 那麼內核會把wp 的狀態的進程設置爲TASK_RUNNING( 可是此時wp 的系統調用仍未完成). 而當wp 切換會用戶態時, 這個信號會被deliver. 若是這種狀況真的發生了, 則系統調用服務例程並無成功完成任務, 可是會返回錯誤碼EINTR , ERESTARTNOHAND , ERESTART_RESTARTBLOCK , ERESTARTSYS , 或 ERESTARTNOINTR.
( 參考中斷處理的從中斷返回部分).
從實踐上看, 用戶得到的錯誤代碼是是EINTR, 這意味着系統調用沒有成功完成. 程序員能夠決定是否再次發起該系統調用. 其他的錯誤代碼由內核使用來判斷是否在信號處理以後自動從新執行該系統調用.
下表列出了這些錯誤代碼在每一種可能的中斷行爲下對未完成系統調用的影響. 表中用的詞定義以下:
Terminate: 該系統調用不會被內核自動從新執行. 而用戶獲得的該系統調用的返回值是-EINTER. 對程序員而言該系統調用失敗.
Reexecute: 內核會強制進程在用戶態下自動從新執行該系統調用( 經過把中斷號放到eax, 執行int 0x80 或者sysenter 指令). 可是這對程序員透明.
Depends: 若是當被deliver 的信號設置了 SA_RESTART 標誌, 那麼自動從新執行該系統調用. 不然停止系統調用並返回-EINTER.
Error codes and their impact on system call execution |
||||
Signal Action |
EINTR |
ERESTARTSYS |
ERESTARTNOHAND ERESTART_RESTARTBLOCK |
ERESTARTNOINTR |
Default |
Terminate |
Reexecute |
Reexecute |
Reexecute |
Ignore |
Terminate |
Reexecute |
Reexecute |
Reexecute |
Catch |
Terminate |
Depends |
Terminate |
Reexecute |
注: ERESTARTNOHAND , ERESTART_RESTARTBLOCK 使用不一樣的機制來從新自動執行系統調用( 參下文 ).
當 delivering 一個信號時, 內核必須確信進程正在執行系統調用中,這樣它才能reexecute 該系統調用, 而 regs 中的成員orig_eax 就是幹這個事情的. 回想一下這個成員在中斷/ 異常時如何被初始化的:
Interrupt: 它等於 IRQ 數值 - 256.
0x80 exception ( 或者 sysenter): 它等於系統調用的編號.
Other exceptions: 它等於-1.
因此若是該值>=0, 那麼可肯定進程是在處於系統調用中被信號處理喚醒的( 即信號處理喚醒一個等待系統調用完成( 狀態爲 TASK_INTERRUPTIBLE ) 的進程). 因此內核在delivering 信號時, 可以返回上述的錯誤代碼, 並做出恰當的挽救.
重啓被非自定義信號處理函數中斷的系統調用
注:上面語句的中斷不是OS 中的中斷, 而是平常生活中的中斷的含義.
若是系統調用由於信號的默認處理函數或者信號的忽略處理而中斷( 即由系統調用把task 的狀態改成可中斷狀態, 可是卻被信號的默認處理函數或者忽略信號操做把該task 的狀態改成running, 如前文所述), 那麼do_signal 函數須要分析系統調用的錯誤碼來決定是否自動從新執行被中止的系統調用. 若是須要重啓該系統調用, 那麼必須修改regs 中的內容, 從而在切換到用戶態後, 在用戶態下再次執行該系統調用( 即再次在用戶態下讓eax 存放系統調用的編號, 而後執行int 0x80 或者sysenter).
以下代碼:
if (regs->orig_eax >= 0) {
if (regs->eax == -ERESTARTNOHAND || regs->eax == -ERESTARTSYS ||
regs->eax == -ERESTARTNOINTR) {
regs->eax = regs->orig_eax;
regs->eip -= 2;
}
if (regs->eax == -ERESTART_RESTARTBLOCK) {
regs->eax = __NR_restart_syscall;
regs->eip -= 2;
}
}
regs->eax 存放系統調用的編號 . 此外,int 0x80 或者sysreturn 均爲2 字節. 因此regs->eip -=2 等價於切換到用戶態後從新執行int 0x80 或者sysretrun 指令.
對於錯誤碼 ERESTART_RESTARTBLOCK, 它須要使用restart_syscall 系統調用, 而不是使用原來的系統調用. 這個錯誤碼只用在與時間有關的系統調用. 一個典型的例子是 nanosleep( )
: 想象一下, 一個進程調用這個函數來暫停20ms, 10ms 後因爲一個信號處理髮生( 從而激活這個進程), 若是這信號處理後從新啓動這個系統調用, 那麼它在重啓的時候不能直接再次調用nanosleep, 不然將會致使該進程睡覺30ms. 事實上, nanosleep 會在當前進程的thread_info 的restart_block 中填寫下若是須要重啓nanosleep, 那麼須要調用哪個函數, 而若是其被信號處理中斷, 那麼它會返回-ERESTART_RESTARTBLOCK,
而在重啓該系統調用時,sys_restart_syscall 會根據restart_block 中的信息調用相應的函數. 一般這個函數會計算出首次調用與再次調用的時間間距, 而後再次暫停剩餘的時間段.
在這種狀況下,handle_signal 會分析錯誤碼以及 sigaction 中的標誌是否包含了SA_RESTART, 從而決定是否重啓未完成的系統調用. 代碼以下:
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;
break;
}
/* fallthrough */
case -ERESTARTNOINTR:
regs->eax = regs->orig_eax;
regs->eip -= 2;
}
}
若是須要重啓系統調用, 其處理與do_signal 相似. 不然向用戶態返回 -EINTR.
問題 :
在信號處理函數中能夠發生中斷嗎 , 能夠再 發出系統調用嗎,能夠發出異常嗎 ?
若是不行 會有什麼影響 ??
與信號處理相關的系統調用
由於當進程在用戶態時, 容許發送和接受信號. 這意味着必須定義一些系統調用來容許這類操做.
不幸的是, 因爲歷史的緣由這些操做的語義有可能會重合, 也意味着某些系統調用可能不多被用到. 好比,sys_sigaction, sys_rt_sigaction 幾乎相同, 因此C 的接口sigaction 只調用了sys_rt_siaction. 咱們將會描述一些重要的系統調用.
進程組 : Shell 上的一條命令行造成一個進程組 . 注意一條命令其實能夠啓動多個程序 . 進程組的 ID 爲其領頭進程的 ID.
原型爲: int kill(pid_t pid, int sig)
其用來給一個線程組( 傳統意義上的進程) 發信息. 其對應的系統服務例程(service routine) 是sys_kill. sig 參數表示待發送的信號,pid 根據其值有不一樣的含義, 以下:
pid > 0: 表示信號sig 發送到由pid 標識的線程組( 即線程組的PID==pid).
pid = 0: 表示信號sig 發送到發送進程所在的進程組中的全部線程組.
pid = -1: 表示信號sig 發送到除進程0, 進程1, 當前進程外的全部進程
pid < -1: 表示信號sig 發送到進程組-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;
return kill_something_info(sig, &info, pid);
kill_something_info 會調用kill_proc_info( 這個函數調用 group_send_sig_info 把信號發給線程組 ) 或者 kill_pg_info( 這個會掃描目標進程組而後逐一調用send_sig_info ) 或者爲系統中的每個進程調用group_send_sig_info( 當pid=-1 時).
系統調用kill 能夠發送任意信號, 然而它不保證該信號被加到目標進程的懸掛信號隊列中. ( 這個是指對於非實時信號 它也有可能會丟棄該信號嗎????
) 對於實時信號, 可使用 rt_sigqueueinfo.
System V and BSD Unix 還有killpg 系統調用, 它能夠給一組進程發信號. 在linux 中, 它經過kill 來實現. 另外還有一個raise 系統調用, 它能夠給當前進程發信號. 在linux 中,killpg, raise 均以庫函數提供.
這兩個函數給指定線程發信號.
pthread_kill 使用它們之一來實現. 函數原型爲:
int tkill(int tid, int sig);
long sys_tgkill (int tgid, int pid, int sig);
tkill 對應的服務例程是sys_tkill, 它也會填充一個siginfo_t 變量, 進程權限檢查, 而後掉用specific_send_sig_info.
tgkill 與tkill 的差異在於它多了一個tgid 的參數, 它要求pid 必須是tgid 中的線程. 其對應的服務例程是sys_tgkill, 它作的事情和sys_tkill 相似, 但它還檢查了pid 是否在tgid 中. 這種檢查在某些狀況下能夠避免 race condition. 好比: 一個信號被髮給了線程組A 中的一個正在被殺死的線程(killing_id), 若是另一個線程組B 很快地建立一個新的線程而且其PID= killing_id, 那麼信號有可能會發送到線程組B 中的新建的線程.
tgkill 能夠避免這種狀況, 由於線程組A,B 的ID 不同.
程序員能夠經過系統調用sigaction (sig,act,oact) 來爲信號sig 設置用戶本身的信號處理函數act. 固然若是用戶沒有設置, 那麼系統會使用默認的信號處理函數. 其函數原型爲:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
oldact 用來保存信號signum 的舊的信號處理函數( 由於signum 的新的信號處理函數是act, 保存舊的是但願可以恢復使用舊的信號處理函數).
其對應的服務例程是sys_sigaction, 它首先檢查act 地址的有效性, 而後act 的內容拷貝到一個類型爲 k_sigaction 的 本地變量new_ka ,以下:
_ _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);
接着調用 do_sigaction 把new_ka 拷貝到current->sig->action[sig-1] 中的. 相似以下:
k = ¤t->sig->action[sig-1];
if (act) {
*k = *act;
sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
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), ¤t->signal->shared_pending);
t = current;
do {
rm_from_queue(sigmask(sig), ¤t->pending);
recalc_sigpending_tsk(t);
t = next_thread(t);
} while (t != current);
}
}
POSIX 規定當默認行爲是忽略時, 把信號處理函數設置爲SIG_IGN 或者SIG_DFT 會致使懸掛的信號被丟棄. 此外, SIKKILL 和SIGSTOP 永遠不會被屏蔽 ( 參考上述代碼).
此外, sigaction 系統調用還容許程序員初始化sigaction 中的sa_flags.
System V 也提供signal 系統調用. C 庫的signal 使用rt_sigaction 來實現. 可是linux 仍然有相應的服務例程sys_signal. 以下:
new_sa.sa.sa_handler = handler;
new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
得到被阻塞的懸掛信號
系統調用 sigpending () 容許 用戶得到當前線程被阻塞的懸掛信號. 函數原型爲:
int sigpending(sigset_t *set);
set 用來接收被阻塞的懸掛信號的信息.
其對應的服務例程是sys_sigpending, 其實現代碼以下:
sigorsets(&pending, ¤t->pending.signal,
¤t->signal->shared_pending.signal);
sigandsets(&pending, ¤t->blocked, &pending);
copy_to_user(set, &pending, 4);
修改被阻塞的信號的集合
系統函數sigprocmask 能夠用來修改當前線程的阻塞信號集合. 可是它僅適用於非實時信號. 函數原型爲:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
假設在執行這個函數以前線程的阻塞信號的集合爲bs. 執行這個函數以後線程的阻塞信號的集合爲nbs.
oldsett: 用於返回( 返回) 線程當前阻塞的信號的集合(*oldest=bs)
set: 用於存儲信號集合. 怎麼用它還取決於how 參數.
how: 執行線程的新的阻塞信號集合若是經過set 參數得到. 其可能的值及其含義以下:
SIG_BLOCK: nbs=bs|set
SIG_UNBLOCK:nbs=bs-set
SIG_SETMASK:nbs=set
其對應的服務例程是 sys_sigprocmask( ) . 它調用copy_from_user 把set 值拷貝到本地變量new_set, 並把bs 拷貝到oldset 中. 其執行的代碼相似以下:
if (copy_from_user(&new_set, set, sizeof(*set)))
return -EFAULT;
new_set &= ~(sigmask(SIGKILL)|sigmask(SIGSTOP));
old_set = current->blocked.sig[0];
if (how == SIG_BLOCK)
sigaddsetmask(¤t->blocked, new_set);
else if (how == SIG_UNBLOCK)
sigdelsetmask(¤t->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;
懸掛( 暫停) 進程
int sigsuspend(const sigset_t *mask);
其含義是: 把本線程的阻塞信號設置爲mask 並把線程狀態設置爲 TASK_INTERRUPTIBLE. 而且只有當一個 nonignored, nonblocked 的信號發到本線程後纔會把本線程喚醒(deliver 該信號, 系統調用返回).
其相應的服務例程爲sys_sigsuspend, 執行的代碼爲:
mask &= ~(sigmask(SIGKILL) | sigmask(
SIGSTOP ));
saveset = current->blocked;// saveset 本地局部變量
siginitset(¤t->blocked, mask);
recalc_sigpending(current);
regs->eax = -EINTR;
while (1) {
current->state = TASK_INTERRUPTIBLE;
schedule( );
if (do_signal(regs, &saveset))// 把阻塞信號集合恢復爲saveset
return -EINTR;
}
( 注意, 本系統調用自己指望它被信號處理函數中斷.)
函數schedule 會致使執行別的進程( 線程), 而當本進程再次執行時( 即上面的schedule 返回了), 它會調用do_signal 來處理其未被阻塞的懸掛的信號, 而後恢復線程的阻塞信號集合(saveset). 若是do_signal 返回非0(do_signal 中調用用戶自定義信號處理函數或者殺死本線程時返回非0), 那麼該系統調用返回.
即只有當本線程處理完不被阻塞的信號( ==(!mask)| SIGKILL| SIGSTOP) 後, 它纔會返回.
實時信號的系統調用
前面所述的系統調用僅適用於非實時信號,linux 還引入了支持實時信號的系統調用.
一些實時系統調用( 如: rt_sigaction, rt_sigpending, rt_sigprocmask, rt_sigsuspend) 與它們的非實時的版本相似( 只是在名字加了rt_). 下面僅簡單描述兩個實時信號的系統調用.
rt_sigqueueinfo( ): 把一個實時信號發給線程組( 放到線程組的共享懸掛信號列表中). 庫函數sigqueue 利用這個系統調用來實現.
rt_sigtimedwait( ): 把阻塞的懸掛信號從懸掛信號隊列中刪除, 若是在調用這個系統調用時尚未相應的阻塞懸掛信號, 那麼它會把本進程(task) 阻塞一段時間. 庫函數sigwaitinfo,sigtimedwait 經過這個系統調用實現.