1. 程式人生 > >linux signal 核心處理

linux signal 核心處理

linux signal 處理

說明:

本文主要翻譯自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 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 的成員列在下表中:

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 thert_sigtimedwait( ) system call) ( 執行緒私有)

structsigpending

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 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 中與訊號處理有關的成員:

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 await4( ) system call

struct task_struct *

curr_target

Descriptor of the last process in the thread group that received a signal

structsigpending

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 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_tstructure

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_tstructure

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_tstructure

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_tstructure

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, 那麼這個地址就是&current->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 = &current->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.

注意: 訊號有三種處理方式, 只有使用自定義處理函式才需要這樣麻煩啊.

接下來我們需要仔細瞧瞧這一切怎麼發生的.

Setting up 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 U