--- tags: Linux Kernel Internals, 作業系統 --- # Linux 核心設計: Signal ## 簡介 Signal [信號(Signal)](https://en.wikipedia.org/wiki/Signal_(IPC)) 是在 UNIX/類 UNIX(e.g. Linux)系統下經常存在的設計,其主要作為 process 間的一種通信機制。Signal 可以被送給某一 process 以觸發特定行為。當其被送出後,目標 process 會被作業系統中斷,並在保存好執行狀態(暫存器、stack)之後轉跳至信號處理函式(signal handler) 以進行對應的工作。信號處理函式可以由 process 自行定義,否則會根據[系統預設的行為](https://en.wikipedia.org/wiki/Signal_(IPC)#Default_action)處理。 ## Signal 的處理流程 ### 從 `sigaction` 說起 在 Linux 環境下可以透過 [`sigaction`](https://man7.org/linux/man-pages/man2/sigaction.2.html) 系統呼叫(嚴格來說是透過 library wrapper 去進行系統呼叫) 來替換 signal handler。 ```cpp int sigaction(int signum, const struct sigaction *restrict act, struct sigaction *restrict oldact); ``` `signum` 即所要設定的 signal 編號,而 `act` 則是用來描述要更動該 signal 的哪些行為,後者包含要設置的 signal handler 之函式指標、在該 signal handler 中要 block 哪些 signal 等等。`oldact` 可用來取得替換以前的設定以利復原。 ```cpp int __libc_sigaction (int sig, const struct sigaction *act, struct sigaction *oact) { int result; struct kernel_sigaction kact, koact; if (act) { kact.k_sa_handler = act->sa_handler; memcpy (&kact.sa_mask, &act->sa_mask, sizeof (sigset_t)); kact.sa_flags = act->sa_flags; SET_SA_RESTORER (&kact, act); } /* XXX The size argument hopefully will have to be changed to the real size of the user-level sigset_t. */ result = INLINE_SYSCALL_CALL (rt_sigaction, sig, act ? &kact : NULL, oact ? &koact : NULL, STUB (act, __NSIG_BYTES)); if (oact && result >= 0) { oact->sa_handler = koact.k_sa_handler; memcpy (&oact->sa_mask, &koact.sa_mask, sizeof (sigset_t)); oact->sa_flags = koact.sa_flags; RESET_SA_RESTORER (oact, &koact); } return result; } ``` 以 glibc 針對 Linux 的實作為例,sigaction 的核心程式碼可以參考 [`libc_sigaction.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/libc_sigaction.c;h=3cbf241a5fa28296c910fa40a41b09d2b6113b05;hb=HEAD)。可以看到其將 `struct sigaction` 中的設定對應調整到 `kernel_sigaction`,這是因為雖然兩個結構的成員大致相似,都是由 `sa_handler`、`sa_sigaction`、`sa_mask`、`sa_flags`、`sa_restorer` 組成,但函式庫中定義的 `struct sigaction` 與 kernel 中的 `struct sigaction` 定義可能存在區別,包含變數的 layout 和整個 structure 的大小都有機會不同。 ```cpp extern void restore_rt (void) asm ("__restore_rt") attribute_hidden; #define SET_SA_RESTORER(kact, act) \ (kact)->sa_flags = (act)->sa_flags | SA_RESTORER; \ (kact)->sa_restorer = &restore_rt #define RESET_SA_RESTORER(act, kact) \ (act)->sa_restorer = (kact)->sa_restorer ``` 在此值得注意的點是在進入 kernel 前呼叫的 `SET_SA_RESTORER`。在 [`sigaction`](https://man7.org/linux/man-pages/man2/sigaction.2.html) 文件中提到 `sa_restorer` 並不是呼叫 `sigaction` 的使用者該去填入的欄位。這實際上是由函式庫或作業系統所運用。在展開的 `SET_SA_RESTORER` 中我們可以看到其被設置為指向 `__restore_rt` 的函式指標。 該函式指標的作用是甚麼呢? 這就得談到 signal 的特性: 因為 signal 可以在 process 執行任一非 atomic instruction 時中斷其執行,待進入 signal handler 中,將該 signal 處理完後再行返回。因此可以想像在恢復回原本狀態這個目的上,這顯然比起一般的函式呼叫來得複雜許多。此時就需仰賴特殊的一段程式碼,也就是前面提到 `sa_restorer` 底下所設置的函式來達成。這一段程式碼通常被稱為 [signal trampoline](https://en.wikipedia.org/wiki/Trampoline_(computing))。 以 glibc on x86_64 的實作為例,在 [x86_64/libc_sigaction.c](https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/x86_64/libc_sigaction.c) 中我們可以找到這段程式碼: ```cpp #define RESTORE(name, syscall) RESTORE2 (name, syscall) # define RESTORE2(name, syscall) \ asm \ ( \ /* `nop' for debuggers assuming `call' should not disalign the code. */ \ " nop\n" \ ".align 16\n" \ ".LSTART_" #name ":\n" \ " .type __" #name ",@function\n" \ "__" #name ":\n" \ " movq $" #syscall ", %rax\n" \ " syscall\n" \ ... /* The return code for realtime-signals. */ RESTORE (restore_rt, __NR_rt_sigreturn) ``` 展開 `RESTORE (restore_rt, __NR_rt_sigreturn)` 所得到的 `__restore_rt` 這個 symbol 即為 glibc on x86_64 下預設 signal trampoline。`__restore_rt` 中會呼叫 `rt_sigreturn` 這個 system call,並根據進入 signal handler 以前所保存的狀態回復。 ### Signal frame 從 process 的角度來看,當收到 signal,其轉跳至信號處理函式(signal handler) 以進行對應的工作。但若從作業系統的觀點探討,signal 的傳遞實際上是經由 kernel,並由後者分派給目標 process。由於 signal handler 運行於 userspace,kernel 並不能直接調用,而是得設置相應 process 的環境,以在返回 userspace 時間接運行之。考慮到 signal handler 的執行必然需要使用 stack,同時也需要保存 process 在上一次 context switch 後的狀態,使 signal handler 返回後能夠接續執行,因此 kernel 會在用戶態分配一個空間,為這些目的所使用,這個空間被稱作 signal frame。 signal 處理流程的簡易示意圖如下: ![](https://hackmd.io/_uploads/Bk4Fseddn.png) 這邊我們以 x86_64 架構為例做更詳細的說明。首先,當一個處於 userspace 的程式進行系統呼叫或是被 interrupt,CPU 會回到 kernel space 進行週期的工作或者挑選下一個 process 獲得執行權。而 signal 的處理是從 kernel 回到 userspace 之前檢查的,如果發現有 signal pending,就在對應的 process 上建立 signal frame 並讓其運行 signal handler。關鍵的函式是 [`x64_setup_rt_frame`](https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/signal_64.c#L164)。 ```cpp int x64_setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs) { sigset_t *set = sigmask_to_save(); struct rt_sigframe __user *frame; void __user *fp = NULL; unsigned long uc_flags; /* x86-64 should always use SA_RESTORER. */ if (!(ksig->ka.sa.sa_flags & SA_RESTORER)) return -EFAULT; frame = get_sigframe(ksig, regs, sizeof(struct rt_sigframe), &fp); uc_flags = frame_uc_flags(regs); ... ``` 可以看到程式中藉由 [`get_sigframe`](https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/signal.c#L74) 拿到 signal frame,而 `get_sigframe` 從實作能看到 signal frame 的來源即是從原本 process 的 sp 去擴展出來的。 ```cpp struct rt_sigframe { char __user *pretcode; struct ucontext uc; struct siginfo info; /* fp state follows here */ }; ``` x86_64 架構的 signal frame 如上所定義。其中包含從 signal handler 返回後接下來該執行的函式指標 `pretcode`、復原 process 到上一次 context switch 後狀態所需的 ucontext 結構 `uc`、以及攜帶 signal 相關資訊的 siginfo 結構 `info`。 ```cpp! ... if (!user_access_begin(frame, sizeof(*frame))) return -EFAULT; /* Create the ucontext. */ unsafe_put_user(uc_flags, &frame->uc.uc_flags, Efault); unsafe_put_user(0, &frame->uc.uc_link, Efault); unsafe_save_altstack(&frame->uc.uc_stack, regs->sp, Efault); /* Set up to return from userspace. If provided, use a stub already in userspace. */ unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault); unsafe_put_sigcontext(&frame->uc.uc_mcontext, fp, regs, set, Efault); unsafe_put_sigmask(set, frame, Efault); user_access_end(); if (ksig->ka.sa.sa_flags & SA_SIGINFO) { if (copy_siginfo_to_user(&frame->info, &ksig->info)) return -EFAULT; } ... ``` 於是 kernel 在真正進入 signal handler 會將 frame 中需要的資料設定好。其中最關鍵的部分是 `sa_restorer` 的位置被設置到 `pretcode`,因此當 signal handler 要返回時,根據 ABI 會將從 stack pop 出的第一個 64 bits 作為返回的位置,就可以執行到該函式。 ```cpp ... /* Set up registers for signal handler */ regs->di = ksig->sig; /* In case the signal handler was declared without prototypes */ regs->ax = 0; /* This also works for non SA_SIGINFO handlers because they expect the next argument after the signal number on the stack. */ regs->si = (unsigned long)&frame->info; regs->dx = (unsigned long)&frame->uc; regs->ip = (unsigned long) ksig->ka.sa.sa_handler; regs->sp = (unsigned long)frame; ... ``` 在保存目標 process 的 context 到 signal frame 後,就可以準備運行 signal handler 了。可以看到這裡暫存器的 `ip` 設置為 `sa_handler`,而 `di`、`si`、`dx` 根據 ABI 是傳遞給函數的參數,對應到在 `sigaction` 系統呼叫時設置的 `sa_sigaction` 函式的三個參數 `(*sa_sigaction)(int, siginfo_t *, void *)`。此外 signal handler 將 `sp` 設置到 `frame` 的開頭 `sp`,也就是接續在 `pretcode` 之後的空間作為 signal handler 的 stack 使用。 ### sigreturn 於是經過 `setup_rt_frame` 的處理後,signal handler 得以順利執行,並且在返回時執行 `sa_restorer` 底下的 signal trampoline。在 signal trampoline 中其藉由 `sigreturn` (`rt_sigreturn`) 回到 kernel space,並還原 process context 至被 signal 中斷以前的狀態。[sigreturn 文件](https://man7.org/linux/man-pages/man2/sigreturn.2.html) 中更為清楚地描述該系統呼叫的目的。行為的細節可以從 [`rt_sigreturn`](https://elixir.bootlin.com/linux/latest/source/arch/x86/kernel/signal_64.c#L243) 下的實作去探究。 ```cpp SYSCALL_DEFINE0(rt_sigreturn) { struct pt_regs *regs = current_pt_regs(); struct rt_sigframe __user *frame; sigset_t set; unsigned long uc_flags; frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long)); if (!access_ok(frame, sizeof(*frame))) goto badframe; if (__get_user(*(__u64 *)&set, (__u64 __user *)&frame->uc.uc_sigmask)) goto badframe; if (__get_user(uc_flags, &frame->uc.uc_flags)) goto badframe; set_current_blocked(&set); if (!restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags)) goto badframe; if (restore_altstack(&frame->uc.uc_stack)) goto badframe; return regs->ax; badframe: signal_fault(regs, frame, "rt_sigreturn"); return 0; } ``` ## Reference * [Debugger Not In Depth: signal trampoline frame](https://hellogcc.github.io/blog/2011-08-03-debugger-not-in-depth-signal-trampoline-frame/index.html) * [Linux操作系统学习笔记(十六)进程间通信之信号](https://ty-chen.github.io/linux-kernel-signal/) * [Linux Signal 一网打尽](https://zhuanlan.zhihu.com/p/227924915)