Linux Signal 解說

signal(7)

signal disposition

代表行程接收到 signal 後會產生什麼行為,並且每個行程有各自的 signal disposition ,基本的預設行為有下列幾種

  • Term : 預設行為是終止行程
  • Ign : 預設行為是忽略該 signal
  • Core : 預設行為是終止行程並 dump core
  • Stop : 預設行為是暫停該行程
  • Cont : 預設行為是將暫停的行程繼續運行

行程可以透過 sigaction(2) 改變 signal disposition ,有以下幾種行為可以選擇

  • 維持預設行為
  • 忽略該 signal
  • 透過 signal handler (programmer-defined function) 來處理該 signal

如果利用 signal handler 來處理該 signal ,預設是將該 signal handler 指向的函式的函式資訊框 (function frame,簡稱 fp) 加在原本行程的堆疊當中,也可用 signalstack(2) 使該函式資訊框使用其他的堆疊。

Execution of signal handlers

Linux 核心在每次轉移回使用者模式時,都會檢查是否有正在等待的 signal 並檢查是否有對應的 signal handler,如果有的話會進行以下動作

  • 執行 signal handler 的前置作業
    • 從等待的 signal set 中移除該 signal
    • 若該 signal handler 有指定使用另外的堆疊空間,則建立新的堆疊空間給它
    • 把和該 signal 相關的資訊存入堆疊上的特定的資訊框當中
    • 所有在註冊該 signal handler 時使用 sigprocmask() 來建立在 act->sa_mask 當中的 signal 都會被加到該執行緒的 signal mask 當中。
  • Linux 核心會在堆疊空間上為該 signal handler 建立一個資訊框,並把對應執行緒的 program counter 指向 signal handler function 的第一個指令,而回傳位址則是指向 signal trampoline (詳見 sigreturn(2))
  • signal trampoline 會呼叫 sigreturn() ,恢復執行緒的狀態回到呼叫 signal handler 之前,然後 Linux 核心把控制權轉回給使用者模式,繼續執行 signal handler 被呼叫之前的指令。

如果 signal handler 因為 siglongjmp() 或使用 execve() 啟動新的程式而沒有回傳,則不會回傳 signal trampoline 也不會呼叫 sigreturn() ,此時這是開發者的責任要去恢復 signal mask 的狀態。

以 Linux 核心的角度來看,它不知道 signal handler 跟其他使用者層級的程式碼之間的差別,自然也不知道目前執行的是誰,所有 signal handler 需要的相關資訊都儲存在使用者層級的暫存器和堆疊中,所以巢狀呼叫的 signal handler 數量上限是被使用者層級的堆疊大小所限制。

sigaction(2)

系統呼叫

sigaction() 系統呼叫可以改變行程接收到特定 signal 時的行為。
signum 是用來指定該 signal 的數字(不能指定 SIGKILLSIGSTOP )。

結構體

struct sigaction 定義如下

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

特別注意到 sa_restorer 並非是給使用者使用的,詳見 sigreturn(2)
如上節所說的, sa_handler 是行程接收到 signum 這個 signal 時會採取的行為

  • SIG_DFL 代表採取預設行為
  • SIG_IGN 代表忽略該 signal
  • 若在 sa_flags 中設定 SA_SIGINFO ,則 sa_sigaction 就指向處理 signum 的 signal-hadling function

signal handler 的函式定義如下

void handler(int sig, siginfo_t *info, void *ucontext);

以下解說參數

  • sig : 觸發此 handler function 的 signal number (和 signum 相同)
  • info : 指向 siginfo_t 的指標,該結構體包含 signal 的詳細資訊。詳見 sigaction(2)
  • ucontext : a pointer to a ucontext_t structure, cast to void * 。此指標指向的結構體包含了 Linux 核心 存在 userspace stack 中的 signal context information 。通常此參數不會被使用到,詳見 getcontext(3)

sa_mask 則代表在 signal handler 執行期間會被阻塞著的 signals ,並且觸發 signal handler 的 signal 本身也會被阻塞。

sa_flags 代表改變該 signal 行為的標籤集合,有數種標籤詳見 sigaction(2)

userspace context

泛 Sytem V 環境當中,我們可以透過以下幾種結構體和函式來控制行程當中不同執行緒的 user-level context switch 。

  • mcontext_t : machine-dependent 且不開放給使用者操作。
  • ucontext_t : 定義如下
typedef struct ucontext_t {
    struct ucontext_t *uc_link;
    sigset_t uc_sigmask;
    stack_t uc_stack;
    mcontext_t uc_mcontext;
    ...
} ucontext_t;
  • uc_link : 指向在目前 context 被終止後會接續執行的 context 。
  • uc_sigmask : 被此 context 阻塞的 signals 集合。
  • uc_stack : 此 context 使用的堆疊空間。
  • uc_mcontext : 被儲存的 context 的 machine-specific 表示法,包含 calling thread's machine registers 。

getcontext() 會將 ucp 指向的結構體初始化並指向目前執行的 context 。

setcontext() 會繼續執行 ucp 指向的 user context ,此 context 有三種方式獲取

  • 呼叫 getcontext()
  • 呼叫 makecontext()
  • signal handler 的第三個參數

最早實現 user-level context switch 此機制的方式是透過 setjmp()/longjmp() ,不過這樣的方式沒有定義如何處理 signal context ,之後 sigsetjmp()/siglongjmp() 讓我們可以處理 signal context 。當 signal 觸發某個 signal handler 時,目前的 user context 會被暫存而 Linux 核心 會為 signal handler 建立一個新的 user context 。我們不該使用 longjmp() 函式來離開 signal handler ,這樣的行為是未定義的,取而代之應該使用 siglongjmp()setcontext()

注意到除非我們自己設置額外儲存裝置進行記錄,不然我們無從得知 getcontext() 的回傳行為是從首次呼叫 getcontext() 還是透過 setcontext() 來回傳。因為 register 會被重新恢復,所以利用 register variable 來記錄是無效的。