源地址:http://blog.csdn.net/zhuixundelang/article/details/5979465
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的成員列在下表中:
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中與信號處理有關的成員:
除了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 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( ) |
所有這些函數最終都會調用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.
注意:信號有三種處理方式,只有使用自定義處理函數才需要這樣麻煩啊.
接下來我們需要仔細瞧瞧這一切怎麼發生的.
爲了能恰當地爲信號處理函數設置棧,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返回後, 進程由核心態切換到用戶態,於是執行了信號處理函數.
信號處理函數結束後,因爲其返回值的地址(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)
其用來給一個線程組(傳統意義上的進程)發信息.其對應的系統服務例程(serviceroutine)是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 BSDUnix還有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(intsignum, 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 : (unsignedlong)old_sa.sa.sa_handler;
獲得被阻塞的懸掛信號
系統調用sigpending()允許用戶獲得當前線程被阻塞的懸掛信號.函數原型爲:
intsigpending(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可以用來修改當前線程的阻塞信號集合.但是它僅適用於非實時信號.函數原型爲:
intsigprocmask(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;
懸掛(暫停)進程
intsigsuspend(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通過這個系統調用實現.
todo
...............