signal處理的一些補充
signal是一類比較特殊的存在,需要kernel和userspace通力合作才能將signal處理流程走通。
signal處理流程
簡單的流程描述如下:
userspace:
- 利用系統呼叫sigaction(…)或signal(…)設定感興趣的signal的屬性,包括處理函式、flags等。
- 已經不鼓勵使用signal系統呼叫,在libc中被封裝成sigaction的呼叫。
kernel:
在處理sigaction時,把userspace的signal處理函式地址以及flags儲存在當前程序的控制塊中:
task_struct->sighand->action[]
當有signal觸發後,會將相應signal number在如下成員中置位,並置目標程序的狀態為TIF_SIGPENDING,然後喚醒目標程序;如果目標程序處於running狀態且運行於其他CPU,則傳送reschedule IPI中斷以便目標程序可以進入kernel並在退出kernel時處理signal;如果目標程序就是當前程序,則在退出kernel時處理signal;如果目標程序從睡眠狀態被喚醒,檢測到有signal pending,也會退出睡眠狀態,從而返回user space。
task_struct->pending task_struct->signal->shared_pending
在程序被喚醒並返回userspace前夕, 發現TIF_SIGPENDING置位,從而執行訊號處理函式do_signal(…)。
do_signal: 根據signal number,從 task_struct->sighand->action[]中得到sigaction後,在程序的user stack或user專門的處理訊號stack上建立 struct rt_sigframe棧幀。當前kernel stack中的資料會複製到struct rt_sigframe的成員uc.uc_mcontext中。以signal的user處理函式為返回地址,然後返回使用者空間。
SA_RESTORER:為了保證執行完user signal handler後能夠立刻返回kernel,返回地址指向一個特殊的user函式__restore_rt,實現在libc中。這個地址是在sigaction時傳遞給kernel的。__restore_rt如下(以x86_64為例)。可見就是系統呼叫rt_sigreturn,直接進入kernel。
ENTRY_PRIVATE(__restore_rt) .L__restore_rt_START: mov $__NR_rt_sigreturn, %rax syscall .L__restore_rt_END: END(__restore_rt)
userpsace:
- 進入userspace時,直接進入signal handler函式。執行完畢後,函式返回。根據上面所述,返回的地址就是__restore_rt,從而進入kernel。
kernel:
- 進入stub_rt_sigreturn -> sys_rt_sigreturn,恢復訊號處理之前的現場,包括恢復uc.uc_mcontext中的資料至kernel stack中、signal handler stack等,以便下次返回userspace時可進入正常流程。
以上就是signal處理的簡化流程。
棧的問題
這裡再說明一下處理signal時所用的棧的問題。可以用程序的使用者棧或用專用於處理signal的棧,在Android上使用的是後者。
在Bionic的檔案pthread_create.cpp中,有如下的函式實現:
pthread_create
-> __pthread_start
->__init_alternate_signal_stack
線上程的入口函式__pthread_start中,會分配一段user空間作為訊號處理棧空間,並利用系統呼叫sigaltstack將此棧的地址和長度告訴kernel,kernel會將此地址和長度儲存在如下地方:
task_struct -> sas_ss_sp
task_struct -> sas_ss_size
後期在處理此執行緒的signal時,將此棧作為signal處理函式的棧。
signal的作用域
signal的作用域是程序,跨程序是無效的。在實現signal時,充分利用此限制,從而簡單化了處理流程。
從上述的流程分析可見,kernel會儲存user signal handler的地址,待處理signal時直接跳轉至此地址。由於各個程序的userspace是不一樣的,此程序的handler地址在其他程序內是無效或不是期望的函式地址。
那麼作用域是執行緒還是程序的執行緒組暱?如果signal是給執行緒的,則pending在 task_struct->pending上;如果是給執行緒組的,則pending在 task_struct->signal->shared_pending。 由fork時的copy_signal(…)可見,當建立執行緒時所有的執行緒共享同一個物件task_struct->signal。
可參見kill, tgkill。kill是傳送signal給程序,而tgkill則是傳送signal給執行緒。