linux signal 處理
linuxsignal 處理
說明:
本文主要翻譯自ULK 3rd chapter 11.
主要受 http://blog.csdn.net/yunsongice 影響,故發表在csdn.
另外,本文是最初版本,估計以後會有一個改進版本. 文中還有很多todo的地方.
另外,如果有版權問題,通知我,我馬上刪掉.
總結
訊號分成兩種:
regular signal(非實時訊號),對應的編碼值為[1,31]
real time signal對應的編碼值為[32,64]
編碼為0的訊號不是有效訊號,只用於檢查是當前程序否有傳送訊號的許可權,並不真正傳送。
執行緒會有自己的懸掛訊號佇列, 並且執行緒組也有一個訊號懸掛佇列.
訊號懸掛佇列儲存task例項接收到的訊號,只有當該訊號被處理後它才會從懸掛佇列中卸下.
訊號懸掛佇列還有一個對應的阻塞訊號集合,當一個訊號在阻塞訊號集合中時,task不會處理該被阻塞的訊號(但是該訊號依舊在懸掛佇列中). 當阻塞取消時,它會被處理.
對一個訊號,要三種處理方式:
忽略該訊號;
採用預設方式處理(呼叫系統指定的訊號處理函式);
使用使用者指定的方式處理(呼叫使用者指定的訊號處理函式).
對於某些訊號只能採用預設的方式處理(eg:SIGKILL,SIGSTOP).
訊號處理可以分成兩個階段:訊號產生並通知到接收方(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 asignal (described later) requires that delivery of the signal be held off untilit is later unblocked, which acer s the problem ofsignals being raised before they can be delivered.
核心把訊號傳送分成兩個階段:
signalgeneration: 核心更新訊號的目的程序的相關資料結構,這樣該程序就能知道它接收到了一個訊號. 覺得稱為收到訊號階段更恰當. 這個generation翻譯成目的程序接收也不錯.
signal delivery():核心強制目的程序處理接收到的訊號,這主要是通過修改程序的執行狀態或者在目的程序中執行訊號處理函式來實現的. 覺得稱為處理收到的訊號階段更恰當. diliver這裡翻譯成處理更恰當.
deliver的翻譯:有很多個,估計翻譯成in computing比較合理
一個genearated signal最多隻能deliver一次(即一個訊號最多隻會被處理一次). signal是可消耗資源,一旦一個signal被deliver,那麼所有程序對它的引用都會被取消.
已經產生但是還未被處理(deliver)的訊號稱為pending signal(懸掛訊號).對於regularsignal, 在某一個時刻,一種signal在一個程序中只能有一個例項(因為程序沒有用佇列快取其收到的signal).因為有31種regualar signal,所以一個程序某一個時刻可以有31個各類signal的例項. 此外因為linux程序對real time signal採用不同的處理方式, 它會儲存接收到的real time signal的例項,所以可以同時有很多同種signal的例項.
問題:不同種類的訊號的優先順序(從值較小的開始處理).
一般而言,一個訊號可能會被懸掛很長是時間(即一個程序收到一個訊號後,該訊號有可能在該程序裡很久,因為程序沒空來處理它),主要有如下因素:
1. 訊號通常被當前程序處理.Signals are usually delivered only to the currentlyrunning 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: Theprocess is terminated (killed)
Dump: Theprocess is terminated (killed) and a core file containing its execution contextis created, if possible; this file may be used for debug purposes.
Ignore:Thesignal is ignored.
Stop:Theprocess is stopped, i.e., put in the TASK_STOPPED state.
Continue:Ifthe 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 andblocked signals
2. POSIX介面kill( ), sigqueue( ) 必須把訊號發給執行緒組,而不是指定執行緒. 另外核心產生的SIGCHLD, SIGINT, or SIGQUIT也必須發給執行緒組.
3. 執行緒組中只有有一個執行緒來處理(deliver)的共享的訊號就可以了.下問介紹如何選擇這個執行緒.
4. 如果執行緒組收到一個致命的訊號,核心要殺死執行緒組的所有執行緒, 而不是僅僅處理該訊號的執行緒.
為了遵從POSIX標準, linux2.6使用輕量級程序實現執行緒組.
下文中,執行緒組表示OS概念中的程序, 而執行緒表示linux的輕量級程序. 程序也(更多地時候)表示linux的輕量級程序. 另外每一個執行緒有一個私有的懸掛訊號列表,執行緒組共享一個懸掛訊號列表.
與訊號有關的資料結構
注:pending/懸掛訊號, 表示程序收到訊號,但是還沒有來得及處理,或者正在處理但是還沒有處理完成.
對於每一個程序, 核心必須知道它當前懸掛(pending)著哪些訊號或者遮蔽(mask)著哪些訊號.還要知道執行緒組如何處理訊號. 為此核心使用了幾個重要的資料結構(它們可通過task例項訪問),如下圖:
The mostsignificant data structures related to signal handling
(注意task中的一些關於signal的成員在上圖中沒有表現出來)
task中關於signal的成員列在下表中:
Process descriptor fields related to signal handling |
||
Type |
Name |
Description |
struct signal_struct * |
signal |
Pointer to the process's signal descriptor(執行緒組共用的訊號) |
struct sighand_struct * |
sighand |
Pointer to the process's signal handler descriptor(執行緒組共用) |
sigset_t |
blocked |
Mask of blocked signals(執行緒私有) |
sigset_t |
real_blocked |
Temporary mask of blocked signals (used by the rt_sigtimedwait( ) system call) (執行緒私有) |
struct sigpending |
pending |
Data structure storing the private pending signals |
unsigned long |
sas_ss_sp |
Address of alternative signal handler stack.(可以不提供) |
size_t |
sas_ss_size |
Size of alternative signal handler stack(可以不提供) |
int (*) (void *) |
Notifier |
Pointer to a function used by a device driver to block some signals of the process |
void * |
notifier_data |
Pointer to data that might be used by the notifier function (previous field of table) |
sigset_t * |
notifier_mask |
Bit mask of signals blocked by a device driver through a notifier function |
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 timesignal. 每一種對應sigset_t中一個bit.
訊號描述符&訊號處理函式描述符
task的signal, sighand成員分別是訊號描述符與訊號處理函式描述符.
signal成員是一個指標,它指向結構體signal_struct的例項,該例項儲存了執行緒組懸掛著的訊號. 也就是說執行緒組中的所有程序(這裡稱為task更合理)共用同一個signal_struct例項. signal_struct中的shared_pending成員儲存了所有懸掛的訊號(以雙向連結串列組織).此外signal_struct中還儲存了許多其它的資訊(eg:程序資源限制資訊, pgrp, session 資訊).
下表列出了signal_struct中與訊號處理有關的成員:
The fields of the signal descriptor related to signal handling |
||
Type |
Name |
Description |
atomic_t |
count |
Usage counter of the signal descriptor |
atomic_t |
live |
Number of live processes in the thread group |
wait_queue_head_t |
wait_chldexit |
Wait queue for the processes sleeping in a wait4( ) system call |
struct task_struct * |
curr_target |
Descriptor of the last process in the thread group that received a signal |
struct sigpending |
shared_pending |
Data structure storing the shared pending signals |
int |
group_exit_code |
Process termination code for the thread group |
struct task_struct * |
group_exit_task |
Used when killing a whole thread group |
int |
notify_count |
Used when killing a whole thread group |
int |
group_stop_count |
Used when stopping a whole thread group |
unsigned int |
flags |
Flags used when delivering signals that modify the status of the process |
除了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表示不是因為錯誤才發出訊號的.
si_code:標識誰發出了這個訊號.參考下表:
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 functionsreduce 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( ) |
System call handler of 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 processgroup).
非孤兒程序組指如果程序組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'tbe considered until the next invocation of do_signal( ). This approachensures 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.
注意:訊號有三種處理方式,只有使用自定義處理函式才需要這樣麻煩啊.
接下來我們需要仔細瞧瞧這一切怎麼發生的.
Settingup the frame
為了能恰當地為訊號處理函式設定棧,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則用於特徵標誌,所以偵錯程式能夠知道這是一個訊號處理函式的棧.
Frame on the UserMode stack
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.
Evaluating thesignal flags
設定好棧後,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也立即返回.