Linux Signal 一網(wǎng)打盡
前言
Linux Signal想畢很多人都用過步做,比如在命令行下想要結(jié)束某個進(jìn)程法焰,我們會使用kill pid
或者kill -9 pid
缰雇,其實(shí)就是通過給對應(yīng)的進(jìn)程發(fā)送信號來完成。
Linux signal 實(shí)際上可以看作是一種進(jìn)程間通訊的異步方式荆姆,進(jìn)程通過對接收到的信號作相應(yīng)的系統(tǒng)默認(rèn)處理或者用戶自定義處理來實(shí)現(xiàn)某種功能蔼囊,這聽起來焚志,信號處理在行為上與中斷有幾分相似。
下面我們就來進(jìn)入到Linux Signal的世界吧~~~
信號的使用
我們先通過一段代碼實(shí)例來看一下信號量的使用吧畏鼓。
void IntHandler(int signum) {
std::cout << time(NULL) << " Got a int signal" << signum << std::endl;
std::this_thread::sleep_for(5s);
std::cout << time(NULL) << " Fininsh int signal" << signum << std::endl;
}
int main(int argc, char* argv[]) {
signal(SIGINT, IntHandler);
while(true) {
std::this_thread::sleep_for(10s);
std::cout << "." << std::endl;
}
std::cout << std::endl;
return 0;
}
上面這段代碼酱酬,我們通過signal(SIGINT, IntHandler);
自定義了SIGINT
信號量的處理。程序運(yùn)行起來后云矫,當(dāng)按下ctrl + c
時膳沽,IntHandler
信號處理函數(shù)被觸發(fā)。
我們再來看一個例子:
void AlarmHandler(int signum) {
std::cout << "Got a alarm signal" << std::endl;
}
void TestAlarm() {
struct sigaction sig;
sig.sa_handler = AlarmHandler;
sig.sa_flags = 0;
sigemptyset(&sig.sa_mask);
struct sigaction old;
sigaction(SIGALRM, &sig, &old);
std::cout << "A SIGALRM handler has registered" << std::endl;
alarm(3);
pause();
std::cout << "Raise another alarm signal, in 2 second later" << std::endl;
alarm(2);
std::cout << "Star sleep 10s ..." << std::endl;
sleep(10);
std::cout << "Exit sleep 10s ..." << std::endl;
std::cout << "Exit..." << std::endl;
sigaction(SIGALRM, &old, NULL);
}
int main(int argc, char* argv[]) {
TestAlarm();
return 0;
}
上面的代碼我們首先使用sigaction(SIGALRM, &sig, &old);
安裝了SIGALRM
的用戶自定義處理函數(shù)AlarmHandler
, 然后我們調(diào)用alarm(3)
让禀,設(shè)置了一個3s后的鬧鐘挑社,再調(diào)用pause()
讓主程序暫停,2s后鬧鐘到時巡揍,AlarmHandler
函數(shù)被觸發(fā)痛阻,pause()
調(diào)用被中斷,主程序繼續(xù)往下運(yùn)行腮敌。
接下來又設(shè)置了一個2s的鬧鐘后阱当,進(jìn)入sleep(10)
的10s睡眠,2s后鬧鐘到時糜工,AlarmHandler
函數(shù)被觸發(fā)弊添,sleep()
調(diào)用被中斷,主程序繼續(xù)往下運(yùn)行捌木。
這里給讀者留個小問題油坝,如果把上面代碼里的sleep
換成std::this_thread::sleep_for(10s);
,會是什么樣的結(jié)果呢?
信號的發(fā)送
信號的發(fā)送免钻,有人說那還不簡單,只要知道一個進(jìn)程的pid, 那就發(fā)唄~~~
之前寫過一篇文章Linux PID 一網(wǎng)打盡, 里面介紹了在Linux系統(tǒng)里面崔拥,有進(jìn)程极舔,線程,線程組链瓦,進(jìn)程組
這幾個概念拆魏。相應(yīng)地,信號的發(fā)送對象也是多種多樣慈俯。
- raise: 發(fā)送signal到當(dāng)前的調(diào)用進(jìn)程或線程渤刃。如果是單線程程序,就是發(fā)送給這個進(jìn)程贴膘,也就是發(fā)送到主線程卖子;對于多線程程序,發(fā)送給當(dāng)前調(diào)用raise的這個線程刑峡;
-
kill : 發(fā)送signal到任意的進(jìn)程組或進(jìn)程洋闽,它需要一個pid參數(shù)。
- 如果pid是大于0的突梦,這個signal發(fā)送到這個pid相應(yīng)的進(jìn)程诫舅,一個進(jìn)程里可能有多個線程,這個signal可以被其中任意一個線程接收并處理(前提是這個線程沒有屏蔽掉這個信號)宫患;
- 如果pid等于0刊懈,這個signal發(fā)送到當(dāng)前調(diào)用這個kill的進(jìn)程所在的進(jìn)程組里的每一個進(jìn)程;
- 如果pid等于-1娃闲,這個signal發(fā)送到系統(tǒng)中除了pid=1的進(jìn)程以外的其他所有當(dāng)前進(jìn)程有權(quán)發(fā)送signal的所有進(jìn)程虚汛;
- 如果pid小于-1龙巨, 這個signal發(fā)送到進(jìn)程組ID為
-pid
的進(jìn)程組中的每一個進(jìn)程每强。
- tgkill : 可以直接將signal發(fā)送到一個線程組里的某一個線程奶赠。
信號的同步異步處理
對信號的處理可以分為同步和異步兩種方式
- 異步方式圃郊,是大家最熟悉也是最常用的宗收,就像我們上面的例子一樣港庄,使用
signal
或sigaction
設(shè)置處理信號的handler欢搜; - 同步方式牛曹,大家用得少捌年,顧名思義就是在代碼里阻塞住瓢娜,直接有signal到來,然后在這一點(diǎn)kernel將到來的signal的相關(guān)信息返回給程序礼预,一般有兩種方法:
- 調(diào)用
sigwaitinfo
,sigtimedwait
和sigwait
: 阻塞住代碼運(yùn)行眠砾,直接有signal到來; -
signalfd
: 這完全貫徹了Linux下一切皆文件的方針,通過這個接口你可以將對signal的接收抽象成通過的對這個fd的讀取操作托酸,因?yàn)橐呀?jīng)是fd了褒颈,它就是變通的文件的fd沒什么異同了柒巫,你可以通過read阻塞到來,也可以使用select
,epoll
等來多路復(fù)用谷丸。
- 調(diào)用
信號的狀態(tài)
- pending狀態(tài):從信號產(chǎn)生到信號到達(dá)進(jìn)程這段時間是處于pending狀態(tài)堡掏,所有的信號都在是系統(tǒng)調(diào)用完成返回用戶空間前被處理掉,如果當(dāng)前的進(jìn)程沒有觸發(fā)任可的系統(tǒng)調(diào)用刨疼,那到達(dá)它的所有信號都不會被處理泉唁;
- blocked狀態(tài):可以理解成信號被阻塞,它不能到達(dá)當(dāng)前進(jìn)程揩慕,直到當(dāng)前進(jìn)程解除了對此signal的阻塞亭畜,那之前已經(jīng)被阻塞的signal又會到來被處理。進(jìn)程中的每一個線程都有自己獨(dú)立的signal mask, 這個signal mask可以被用于設(shè)置當(dāng)前線程要阻塞哪些signal迎卤。
信號的分類
Linux支持POSIX的標(biāo)準(zhǔn)信號和POSIX的real-time實(shí)時信號拴鸵。
標(biāo)準(zhǔn)信號
標(biāo)誰信號基本上是從Unix繼承而來,包含下列表格中的這批信號:
有以下幾點(diǎn)需要注意:
-
SIGKILL
和SIGSTOP
這兩個信號不能被捕獲蜗搔,不能阻塞宝踪,也不能被忽略,完全由Linux系統(tǒng)自身來處理碍扔; - 不支持排隊(duì)瘩燥,如果在某個signal被阻塞時,產(chǎn)生了多個這個signal, 那當(dāng)這個signal變成非阻塞時不同,只有最先產(chǎn)生的那一個signal會到達(dá)并被處理厉膀;
- 發(fā)送到進(jìn)程的多個signal, 它們到達(dá)進(jìn)程并被處理的順序是不確定的。
實(shí)時信號
從Linux2.2版本開始二拐,支持了real-time信號服鹅,這些real-time信號量被定義在宏SIGRTMIN
和 SIGRTMAX
之間,Linux系統(tǒng)沒有給它們預(yù)先定義含義百新,它們可以被應(yīng)用程序自由定義企软。
我們先看個例子:
void MinPlus2Handler(int signum) {
std::cout << time(NULL) << " Got a SIGRTMIN + 2 signal" << signum << std::endl;
}
int main(int argc, char* argv[]) {
signal(SIGRTMIN+2, MinPlus2Handler);
while(true) {
std::this_thread::sleep_for(10s);
std::cout << "." << std::endl;
}
std::cout << std::endl;
return 0;
}
上面代碼我們通過signal(SIGRTMIN+2, MinPlus2Handler);
為實(shí)時信號SIGRTMIN+2
設(shè)置了handler。
在我的Linux系統(tǒng)上SIGRTMIN+2
是36, 運(yùn)行上面的程序饭望,然后kill -36 pid
仗哨,則MinPlus2Handler
會被觸發(fā)。
實(shí)時信號有以下這幾個特點(diǎn):
- 如果某一個實(shí)時信號當(dāng)前是被阻塞的铅辞,那么如果產(chǎn)生了多個這個實(shí)時信號厌漂,它們會被排隊(duì),等阻塞被取消后斟珊,所有實(shí)例均可到達(dá)進(jìn)程并被處理苇倡;
- 可以使用
sigqueue
來發(fā)送,同時攜帶用戶自定義數(shù)據(jù),這樣在用戶自定義的handler被回調(diào)時旨椒,可以獲取到這個用戶自定義數(shù)據(jù)晓褪。另外,這個sigqueue
也可以用于發(fā)送上面講過的標(biāo)準(zhǔn)信號综慎,但是此時針對同一個標(biāo)準(zhǔn)信號涣仿,依然不支持排隊(duì)操作; - 實(shí)時信號與標(biāo)誰信號不同寥粹,多個實(shí)時信號到達(dá)的順序和它們被發(fā)送的順序是一樣的变过。
信號的內(nèi)核實(shí)現(xiàn)
信號的發(fā)送
我們最常用的向一個進(jìn)程發(fā)送信號的方法就是kill
, 它其實(shí)對應(yīng)著一個系統(tǒng)調(diào)用也是kill
,定義在kernel/signal.c
中:
/**
* sys_kill - send a signal to a process
* @pid: the PID of the process
* @sig: signal to be sent
*/
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info);
return kill_something_info(sig, &info, pid);
}
此系統(tǒng)調(diào)用需要兩個參數(shù)埃元,在用戶容間看到的進(jìn)程pid(pid_t在內(nèi)核代碼被定義為int)涝涤,另一個參數(shù)就是要發(fā)送的signal mumber。
我們接著看一下kill_something_info
函數(shù)的實(shí)現(xiàn)岛杀,主要的邏輯都在這里:
- 先來看
pid > 0
里的處理, 前面我們講過如果pid是大于0的阔拳,這個signal發(fā)送到這個pid相應(yīng)的進(jìn)程:
if (pid > 0) {
? rcu_read_lock();
? ret = kill_pid_info(sig, info, find_vpid(pid));
? rcu_read_unlock();
? return ret;
}
我們來講下find_vpid
,在用戶態(tài)調(diào)用kill
時只需要知道進(jìn)程的pid, 這個pid就是整數(shù)型值类嗤。kernel則需要根據(jù)這個pid
查找到其對應(yīng)的struct pid
, 這就是find_vpid
所作的事:
find_pid_ns(nr, task_active_pid_ns(current));
| |
| |
| V
| ns_of_pid(task_pid(current))
| | |
| | |
| | V
| | current->thread_pid (獲取到current task的struct pid)
| |
| |
| V
| pid->numbers[pid->level].ns (從current task的pid中獲取到 pid namespace) |
V
idr_find(&ns->idr, nr) (pid_t在分配時是通過這個pid對應(yīng)的pid namespace的idr來統(tǒng)一分配糊肠,反之查找時也是通過相應(yīng)pid namespace 的idr)
我們再來看一下kill_pid_info
的實(shí)現(xiàn):
int kill_pid_info(int sig, struct kernel_siginfo *info, struct pid *pid) {
...
rcu_read_lock();
//先根據(jù)pid獲取到對應(yīng)的task_struct
p = pid_task(pid, PIDTYPE_PID);
if (p)
? error = group_send_sig_info(sig, info, p, PIDTYPE_TGID);
rcu_read_unlock();
if (likely(!p || error != -ESRCH))
? return error;
}
先根據(jù)pid獲取到對應(yīng)的task_struct, 然后調(diào)用group_send_sig_info
-> do_send_sig_info
-> send_signal
->__send_signal
, 我們重點(diǎn)來看一下最后這個函數(shù),我把重點(diǎn)的流程處理提取出來遗锣,配上相應(yīng)的注釋:
//先放一個比較重要的struct sigpeding
struct sigpending {
? struct list_head list;
? sigset_t signal;
};
static int __send_signal(int sig, struct kernel_siginfo *info, struct task_struct *t,
enum pid_type type, bool force)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
...
//先根據(jù)type的不同獲取到對應(yīng)的sigpending
// 可以是線程組共享隊(duì)列t->signal->shared_pending, 也可以是task私有隊(duì)列t->pending
// 當(dāng)前應(yīng)該是PIDTYPE_TGID货裹,信號是發(fā)送到當(dāng)前進(jìn)程的,也即TGID, 使用task_struct的signal->pending來接收
pending = (type != PIDTYPE_PID) ? &t->signal->shared_pending : &t->pending;
...
result = TRACE_SIGNAL_ALREADY_PENDING;
//使用legacy_queue來判斷如果sig是屬于上面介紹過的標(biāo)誰信號精偿,且已經(jīng)在pending隊(duì)列中弧圆,則不再添加,
//這也就是上面說的標(biāo)準(zhǔn)信號不支持排隊(duì)
if (legacy_queue(pending, sig))
goto ret;
...
//對于SIGKILL信號笔咽,或者是發(fā)送給內(nèi)核線程的信號搔预,不掛載到pending隊(duì)列,
//直接跳轉(zhuǎn)到out_set,優(yōu)先處理
if ((sig == SIGKILL) || (t->flags & PF_KTHREAD))
goto out_set;
...
//分配新的sigqueue結(jié)構(gòu)體
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
if (q) {
//添加到上面獲取到的pending鏈接里
list_add_tail(&q->list, &pending->list);
}
...
// 再把sig原子性地添加到pending->signal中
sigaddset(&pending->signal, sig);
...
//挑選出合適的task來用于具體處理這個signal, 必要時喚醒這個task
complete_signal(sig, t, type);
...
}
我們再來看下 complete_signal
的實(shí)現(xiàn):
static void complete_signal(int sig, struct task_struct *p, enum pid_type t)
{
struct signal_struct *signal = p->signal;
struct task_struct *t;
//先判斷當(dāng)前task(主線程)是不可以處理這個signal
if (wants_signal(sig, p))
t = p;
else if ((type == PIDTYPE_PID) || thread_group_empty(p))
如果signal是發(fā)送給某個線程或者當(dāng)前線程組為空叶组,則直接返回
return;
else {
//遍歷當(dāng)前線程組所有線程拯田,找到合適的線程
t = signal->curr_target;
while (!wants_signal(sig, t)) {
t = next_thread(t);
if (t == signal->curr_target)
return;
}
signal->curr_target = t;
}
...
//喚醒被中的task
signal_wake_up(t, sig == SIGKILL);
return;
}
接下來看一下一個比較重要的函數(shù)signal_wake_up
:
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
// 設(shè)置TIF_SIGPENDING的task的flag,
// 我們上面說過signal的處理時機(jī)是在系統(tǒng)調(diào)用完成后從內(nèi)核態(tài)返回用戶態(tài)時,
// 此時要檢查這個TIF_SIGPENDING是否被設(shè)置
set_tsk_thread_flag(t, TIF_SIGPENDING);
/*
* TASK_WAKEKILL also means wake it up in the stopped/traced/killable
* case. We don't check t->state here because there is a race with it
* executing another processor and just now entering stopped state.
* By using wake_up_state, we ensure the process will wake up and
* handle its death signal.
*/
// 喚醒對應(yīng)的task, 這部分具體實(shí)現(xiàn)我們留到后面講task調(diào)度時再詳細(xì)說明
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
kick_process(t);
}
-
我們來看一下
pid = -1
時kill_something_info
的處理甩十,上面我們講過此時這個signal發(fā)送到系統(tǒng)中除了pid=1的進(jìn)程以外的其他所有當(dāng)前進(jìn)程有權(quán)發(fā)送signal的所有進(jìn)程:int retval = 0, count = 0; struct task_struct * p; //遍歷當(dāng)前所有process for_each_process(p) { if (task_pid_vnr(p) > 1 && !same_thread_group(p, current)) { //發(fā)送signal到每個thread group int err = group_send_sig_info(sig, info, p, PIDTYPE_MAX); ++count; if (err != -EPERM) ? retval = err; } } ret = count ? retval : -ESRCH;
直此船庇,信號的發(fā)送流程就介紹完了。
信號的處理
在Linux中侣监, signal被處理的時機(jī)是在系統(tǒng)調(diào)用完成返回到用戶態(tài)前作統(tǒng)一處理溢十。
-
我們先來簡單回顧下系統(tǒng)調(diào)用相關(guān)的知識點(diǎn)
簡單來說,傳統(tǒng)系統(tǒng)調(diào)用使用
int 0x80
中斷陷入內(nèi)核达吞,走的是中斷處理的流程张弛,需要作權(quán)限驗(yàn)證,內(nèi)核棧切換,segment的加載等等吞鸭,速度慢寺董。進(jìn)入到64位后,CPU提供了新的syscall
指令使用MSR
寄存器來加載code segment和系統(tǒng)調(diào)用處理入口刻剥,這被稱名快速系統(tǒng)調(diào)用
遮咖,在kernel啟動時被初始化(arch/x86/kernel/cpu/common.c):void syscall_init(void) { ? wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS); ? wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64); /* Flags to clear on syscall */ wrmsrl(MSR_SYSCALL_MASK, X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF| X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT); }
可以看到當(dāng)前的系統(tǒng)調(diào)用處理入口是
entry_SYSCALL_64
,這是段匯編造虏,位于arch/x86/entry/entry_64.S
中御吞,我們簡單過下這段匯編:ENTRY(entry_SYSCALL_64) //交換用戶態(tài)和內(nèi)核態(tài)gs寄存器, //交換內(nèi)核棧和用戶態(tài)棧漓藕, //交換CR3,頁表 //這段執(zhí)行完后,%rsp寄存器指向內(nèi)核棧 swapgs /* tss.sp2 is scratch space. */ movq? %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp movq? PER_CPU_VAR(cpu_current_top_of_stack), %rsp //在內(nèi)核棧上構(gòu)造pt_regs,保存用戶CS陶珠,DS,用戶態(tài)棧到內(nèi)核棧 pushq? $__USER_DS? ? ? ? /* pt_regs->ss */ pushq? PER_CPU_VAR(cpu_tss_rw + TSS_sp2)? /* pt_regs->sp */ pushq? %r11? ? ? ? ? /* pt_regs->flags */ pushq? $__USER_CS? ? ? ? /* pt_regs->cs */ pushq? %rcx? ? ? ? ? /* pt_regs->ip */ ntry_SYSCALL_64_after_hwframe) pushq? %rax? ? ? ? ? /* pt_regs->orig_ax */ // 具體的中斷處理 call? do_syscall_64? .... //后面就是前面操作的逆操作 //從pt_regs里恢復(fù)%rcx,%r11 movq? RCX(%rsp), %rcx movq? RIP(%rsp), %r11 .... USERGS_SYSRET64 END(entry_SYSCALL_64)
上面有一句很重要
pushq? %rcx /* pt_regs->ip
,%rcx
寄存器保存著系統(tǒng)調(diào)用完成后返回到用戶空間的地址享钞,這個地址被保存到了當(dāng)前的內(nèi)核棧上揍诽,即pt_regs->ip
。最后的
USERGS_SYSRET64
返回用戶空間栗竖,它的實(shí)現(xiàn)是:swapgs; sysretq;
sysretq
指令從RCX寄存器
加載下一條指令地址到RIP
寄存器暑脆,CPU繼續(xù)執(zhí)行,這樣就返回了用空間狐肢。上面這段內(nèi)容雖簡單添吗,但很重要,我們后面會用到份名。
往下執(zhí)行的調(diào)用路徑是
do_syscall_64
->syscall_return_slowpath
->exit_to_usermode_loop
, 我們重點(diǎn)來看exit_to_usermode_loop
://這里是一個調(diào)度點(diǎn) if (cached_flags & _TIF_NEED_RESCHED) schedule(); ... /* deal with pending signal delivery */ if (cached_flags & _TIF_SIGPENDING) do_signal(regs);
-
signal的處理碟联,主要在
do_signal
中,我們只保留其主要流程:void do_signal(struct pt_regs *regs) { struct ksignal ksig; ... if (get_signal(&ksig)) { ... handle_signal(&ksig, regs); return; } .... /* If there's no signal to deliver, restore the saved sigmask back */ restore_saved_sigmask(); }
通過上面的講解我們知道signal被處理的時機(jī)是在系統(tǒng)調(diào)用處理完后在返回到用戶態(tài)之前同窘,此時還是處于內(nèi)核態(tài)玄帕。如果針對待處理的signal我們在應(yīng)用程序中設(shè)置了用戶自定義的handler,此時需要在內(nèi)核態(tài)調(diào)用這個handler, 但是handler是用戶態(tài)代碼想邦,內(nèi)核態(tài)不能直接調(diào)用裤纹,這里需要在用戶態(tài)創(chuàng)建臨時棧幀來運(yùn)行這個handler, 運(yùn)行完成后再返回到內(nèi)核態(tài),然后再次返回到用戶態(tài)丧没,完成整個的系統(tǒng)調(diào)用流程鹰椒。
網(wǎng)上有張經(jīng)典圖,我們借用一下呕童,來說明這個過程:
void handle_signal(struct ksignal *ksig, struct pt_regs *regs) {
...
failed = (setup_rt_frame(ksig, regs) < 0);
signal_setup_done(failed, ksig, stepping);
}
setup_rt_frame
-> __setup_rt_frame
:建立新的用戶空間棧漆际,將相關(guān)參數(shù)從內(nèi)核空間拷貝到用戶空間。我們先看一下如果建新的用戶空間棧:
void get_sigframe(struct k_sigaction *ka, struct pt_regs *regs, size_t frame_size,
void __user **fpstate) {
...
//regs->sp就是在系統(tǒng)調(diào)用之初保存的當(dāng)前用戶態(tài)棧
unsigned long sp = regs->sp;
...
//我們在用sigaction設(shè)置signal處理handler時允許用戶設(shè)置獨(dú)立的用于handler的stack
if (ka->sa.sa_flags & SA_ONSTACK) {
if (sas_ss_flags(sp) == 0)
sp = current->sas_ss_sp + current->sas_ss_size;
}
...
}
直接新的用戶空間棧建立完成夺饲。
此時系統(tǒng)調(diào)用返回用戶空間需要返回到signal設(shè)置的handler里奸汇,因此需要重置pt_regs
結(jié)構(gòu)施符,這段代碼在__setup_rt_frame
中:
regs->sp = (unsigned long)frame; //上面新建立的用戶棧
regs->ip = (unsigned long)ksig->ka.sa.sa_handler; // ip設(shè)置成handler,這個ip最終會被加載到%RCX擂找,供sysret使用
regs->ax = (unsigned long)sig;
regs->dx = (unsigned long)&frame->info;
regs->cx = (unsigned long)&frame->uc;
直此戳吝,萬事俱備,系統(tǒng)調(diào)用返回到用戶態(tài)的handler, 執(zhí)行贯涎。
用戶態(tài)的handler執(zhí)行完成后听哭,我們還需要返回內(nèi)核態(tài),這是怎么做到的呢塘雳?
在上面說過的__setup_rt_frame
代碼中陆盘,除了設(shè)置regs->sp
為新建的用戶態(tài)棧內(nèi),還會向此棧中壓入調(diào)用需要的參數(shù)败明,和返回地址:
/* 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);
上面最重要的unsafe_put_user(ksig->ka.sa.sa_restorer, &frame->pretcode, Efault);
, 執(zhí)行完用戶態(tài)的handler后隘马,ksig->ka.sa.sa_restorer
會被pop到%RIP執(zhí)行,這個ka.sa.sa_restorer
必須被設(shè)置肩刃,我們在調(diào)用glibc提供的sigaction
函數(shù)時祟霍,blibc幫我們作了設(shè)置:
__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);
}
...
}
glibc中設(shè)置restorer的代碼如下:
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
RESTORE (restore_rt, __NR_rt_sigreturn)
到這里我們看到從用戶態(tài)再次進(jìn)入到內(nèi)核態(tài)杏头,其實(shí)是通過__NR_rt_sigreturn
系統(tǒng)調(diào)用盈包,其實(shí)現(xiàn)如下:
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;
}
主要是恢復(fù)最初系統(tǒng)調(diào)用完成,為執(zhí)行用戶態(tài)signal handler切換stack frame之前的stack和上下文醇王,最后返回這次系統(tǒng)調(diào)用的結(jié)果呢燥。
后記
直此,我們的Linux signal之旅暫告段落寓娩,還有很多signal的細(xì)節(jié)我們沒有詳述叛氨,這里給大家展現(xiàn)一下signal的來龍去脈,如有疏漏棘伴,歡迎批評指定寞埠。