版權聲明:本文爲本文爲博主原創文章,轉載請註明出處。若有錯誤,歡迎指正。博客地址:https://www.cnblogs.com/wsg1100/html
涉及硬件底層,本文以X86平臺講解。linux
信號是事件發生時對進程的通知機制,是操做系統提供的一種軟件中斷。信號提供了一種異步處理事件的方法,信號與硬件中斷的類似之處在於打斷了程序執行的正常流程,例如,中斷用戶鍵入中斷鍵(Ctrl+C),會經過信號機制中止應用程序。shell
kill -9 pid
。man 7 signal
來查看具體信號及默認處理動做,大多數信號的系統默認動做是終止進程。可使用kill –l
命令查看當前系統可以使用的信號.編程
$ kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
每一個信號都有一個惟一的ID,其中1-31號信號爲常規信號(也叫普通訊號或標準信號)是不可靠信號,產生屢次只記錄一次;34-64稱之爲可靠信號,支持排隊,與驅動編程等相關。可靠與不可靠在後面的內核分析中會看到。數組
用戶進程對信號的常見操做:註冊信號處理函數和發送信號。若是咱們不想讓某個信號執行默認操做,一種方法就是對特定的信號註冊相應的信號處理函數(捕捉),設置信號處理方式的是signal() 函數。注意:如下內容是基於用戶進程描述的。數據結構
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
該函數由ANSI定義,因爲歷史緣由在不一樣版本的Unix和不一樣版本的Linux中可能有不一樣的行爲。若是咱們在 Linux 下面執行 man signal 的話,會發現 Linux 不建議咱們直接用這個方法,而是改用 sigaction。多線程
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask;/* mask last for extensibility */ int sa_flags; void (*sa_restorer)(void); };
sa_restorer
:該元素是過期的,不該該使用,POSIX.1標準將不指定該元素。(棄用)
sa_sigaction
:當sa_flags被指定爲SA_SIGINFO標誌時,使用該信號處理程序。(不多使用)
① sa_handler
:指定信號捕捉後的處理函數名(即註冊函數)。也可賦值爲SIG_IGN表忽略 或 SIG_DFL表執行默認動做。
② sa_mask
: 調用信號處理函數時,所要屏蔽的信號集合(信號屏蔽字)。注意:僅在處理函數被調用期間屏蔽生效,是臨時性設置。當註冊了某個信號捕捉函數,當這個信號處理函數執行的過程當中,若是再有其餘信號,哪怕相同的信號到來的時候,這個信號處理函數就會被中斷。若是信號處理函數中是關於全局變量的處理,執行期間,同一信號再次到來再次執行,這樣的話,同步、死鎖這些都要想好。sa_mask
就是用來設置這個信號處理期間須要屏蔽哪些信號。阻塞的常規信號不支持排隊,產生屢次只記錄一次,後32個實時信號支持排隊。架構
③ sa_flags
:一般設置爲0,表使用默認屬性。koa
因此,sigaction()與signal()的區別在於,sigaction()可讓你更加細緻地控制信號處理的行爲。而 signal 函數沒有給你機會設置這些。須要注意的是,signal ()不是系統調用,而是 glibc 封裝的一個函數。異步
/*glibc-2.28\signal\signal.h*/ # define signal __sysv_signal /*glibc-2.28\sysdeps\posix\sysv_signal.c*/ __sighandler_t __sysv_signal (int sig, __sighandler_t handler) { struct sigaction act, oact; ..... act.sa_handler = handler; __sigemptyset (&act.sa_mask); act.sa_flags = SA_ONESHOT | SA_NOMASK | SA_INTERRUPT; act.sa_flags &= ~SA_RESTART; if (__sigaction (sig, &act, &oact) < 0) return SIG_ERR; return oact.sa_handler; }
能夠看到signal ()的默認設置,sa_flags 設置爲SA_ONESHOT | SA_NOMASK | SA_INTERRUPT
並清除了SA_RESTART
,SA_ONESHOT 意思是,這裏設置的信號處理函數,僅僅起做用一次。用完了一次後,就設置回默認行爲。這其實並非咱們想看到的。畢竟咱們一旦安裝了一個信號處理函數,確定但願它一直起做用,直到我顯式地關閉它。SA_NOMASK表示信號處理函數執行過程當中不阻塞任何信號。
設置了SA_INTERRUPT,清除SA_RESTART,因爲信號的到來是不可預期的,有可能程序正在進行漫長的系統調用,這個時候一個信號來了,會中斷這個系統調用,去執行信號處理函數,那執行完了之後呢?系統調用怎麼辦呢?
這時候有兩種處理方法,一種就是 SA_INTERRUPT,也即系統調用被中斷了,就再也不重試這個系統調用了,而是直接返回一個 -EINTR 常量,告訴調用方,這個系統調用被信號中斷,可是怎麼處理你看着辦。另一種處理方法是 SA_RESTART。這個時候系統調用會被自動從新啓動,不須要調用方本身寫代碼。
於是,建議使用 sigaction()函數,根據本身的須要定製參數。
系統調用小節說到過,glibc 中的文件 syscalls.list定義了庫函數調用哪些系統調用,這裏看sigaction。
/*glibc-2.28\sysdeps\unix\syscalls.list*/ sigaction - sigaction i:ipp __sigaction sigaction
__sigaction 會調用 __libc_sigaction,並最終調用的系統調用是rt_sigaction。
int __sigaction (int sig, const struct sigaction *act, struct sigaction *oact) { ..... return __libc_sigaction (sig, act, oact); } 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 / 8); 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; }
在看系統調用前先看一下進程內核管理結構task_struct 裏面關於信號處理的字段。
struct task_struct { .... /* Signal handlers: */ struct signal_struct *signal; struct sighand_struct *sighand; sigset_t blocked; sigset_t real_blocked; /* Restored if set_restore_sigmask() was used: */ sigset_t saved_sigmask; struct sigpending pending; unsigned long sas_ss_sp; size_t sas_ss_size; unsigned int sas_ss_flags; ...... }
blocked
定義了哪些信號被阻塞暫不處理,pending
表示哪些信號尚等待處理(未決),sighand
表示哪些信號設置了信號處理函數。sas_ss_xxx
這三個變量用於表示信號處理函數默認使用用戶態的函數棧,或開闢新的棧專門用於信號處理。另外信號須要區分進程和線程,進入 struct signal_struct *signal 能夠看到,還有一個 struct sigpending shared_pending
。pending
是本任務的(線程也是一個輕量級任務),shared_pending
線程組共享的。
回到系統調用,linux內核中爲了兼容過去、兼容32位,關於信號的實現有不少種,由不一樣的宏控制,這裏只看系統調用 rt_sigaction。
/*kernel\signal.c*/ SYSCALL_DEFINE4(rt_sigaction, int, sig, const struct sigaction __user *, act, struct sigaction __user *, oact, size_t, sigsetsize) { struct k_sigaction new_sa, old_sa; int ret = -EINVAL; .... if (act) { if (copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa))) return -EFAULT; } ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL); if (!ret && oact) { if (copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa))) return -EFAULT; } out: return ret; }
在rt_sigaction裏,將用戶態的struct sigaction拷貝爲內核態的struct k_sigaction,而後調用do_sigaction(),
每一個進程內核數據結構裏,struct task_struct 中有關於信號處理的幾個成員,其中sighand裏面有個action,是一個k_sigaction的數組,其中記錄着進程的每一個信號的struct k_sigaction。do_sigaction()就是將用戶設置的k_sigaction保存到這個數組中,同時將該信號原來的k_sigaction保存到oact中,拷貝到用戶空間。
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact) { struct task_struct *p = current, *t; struct k_sigaction *k; sigset_t mask; if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig))) return -EINVAL; k = &p->sighand->action[sig-1]; spin_lock_irq(&p->sighand->siglock); if (oact) *oact = *k; if (act) { sigdelsetmask(&act->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP)); *k = *act; ....... } spin_unlock_irq(&p->sighand->siglock); return 0; }
到此一個信號處理函數註冊完成了。
信號的產生多種多樣,不管如何產生,都是由內核去處理的,這裏以最簡單的kill系統調用來看信號的發送過程。
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig) { struct siginfo info; info.si_signo = sig; info.si_errno = 0; info.si_code = SI_USER; info.si_pid = task_tgid_vnr(current); info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); return kill_something_info(sig, &info, pid); }
kill->kill_something_info->kill_pid_info->group_send_sig_info->do_send_sig_info
int do_send_sig_info(int sig, struct siginfo *info, struct task_struct *p, bool group) { unsigned long flags; int ret = -ESRCH; if (lock_task_sighand(p, &flags)) { ret = send_signal(sig, info, p, group); unlock_task_sighand(p, &flags); } return ret; }
一樣使用tkill或者 tgkill 發送信號給某個線程,最終都是調用了 do_send_sig_info
函數。
tkill->do_tkill->do_send_specific->do_send_sig_info tgkill->do_tkill->do_send_specific->do_send_sig_info
do_send_sig_info 會調用 send_signal,進而調用 __send_signal。
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t, int group, int from_ancestor_ns) { struct sigpending *pending; struct sigqueue *q; int override_rlimit; int ret = 0, result; assert_spin_locked(&t->sighand->siglock); result = TRACE_SIGNAL_IGNORED; if (!prepare_signal(sig, t, from_ancestor_ns || (info == SEND_SIG_FORCED))) goto ret; pending = group ? &t->signal->shared_pending : &t->pending; ..... if (legacy_queue(pending, sig)) goto ret; ...... if (sig < SIGRTMIN) override_rlimit = (is_si_special(info) || info->si_code >= 0); else override_rlimit = 0; q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE, override_rlimit); if (q) { list_add_tail(&q->list, &pending->list); switch ((unsigned long) info) { case (unsigned long) SEND_SIG_NOINFO: q->info.si_signo = sig; q->info.si_errno = 0; q->info.si_code = SI_USER; q->info.si_pid = task_tgid_nr_ns(current, task_active_pid_ns(t)); q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); break; case (unsigned long) SEND_SIG_PRIV: q->info.si_signo = sig; q->info.si_errno = 0; q->info.si_code = SI_KERNEL; q->info.si_pid = 0; q->info.si_uid = 0; break; default: copy_siginfo(&q->info, info); if (from_ancestor_ns) q->info.si_pid = 0; break; } userns_fixup_signal_uid(&q->info, t); } ...... out_set: signalfd_notify(t, sig); sigaddset(&pending->signal, sig); complete_signal(sig, t, group); ret: trace_signal_generate(sig, info, t, group, result); return ret; }
先看是要使用哪一個pending,若是是kill發送的,就是要發送給整個進程,應該使用t->signal->shared_pending,這裏面的信號是整個進程全部線程共享的;若是是tkill發送的,也就是發送給某個線程,應該使用t->pending,這是線程的task_struct獨享的。
struct sigpending 結構以下。
struct sigpending { struct list_head list; sigset_t signal; };
有兩個成員變量,一個sigset_t表示收到了哪些變量,另外一個是一個鏈表,也表示接收到的信號。若是都表示收到了信號,這二者有什麼區別呢?接着往下看 __send_signal 裏面的代碼。接下來,調用legacy_queue。若是知足條件,那就直接退出。那 legacy_queue 裏面判斷的是什麼條件呢?咱們來看它的代碼。
static inline int legacy_queue(struct sigpending *signals, int sig) { return (sig < SIGRTMIN) && sigismember(&signals->signal, sig); } #define SIGRTMIN 32
當信號小於 SIGRTMIN,也即 32 的時候,若是這個信號已經在集合裏面了就直接退出了。這樣會形成什麼現象呢?就是信號的丟失。例如,咱們發送給進程 100 個SIGUSR1(對應的信號爲 10),那最終可以被咱們的信號處理函數處理的信號有多少呢?這就很差說了,好比總共 5 個 SIGUSR1,分別是 A、B、C、D、E。
若是這五個信來得太密。A 來了,可是信號處理函數還沒來得及處理,B、C、D、E 就都來了。根據上面的邏輯,由於 A 已經將 SIGUSR1 放在 sigset_t 集合中了,於是後面四個都要丟失。 若是是另外一種狀況,A 來了已經被信號處理函數處理了,內核在調用信號處理函數以前,咱們會將集合中的標誌位清除,這個時候 B 再來,B 仍是會進入集合,仍是會被處理,也就不會丟。
這樣信號可以處理多少,和信號處理函數何時被調用,信號多大頻率被髮送,都有關係,並且從後面的分析,咱們能夠知道,信號處理函數的調用時間也是不肯定的。看小於 32 的信號如此不靠譜,咱們就稱它爲不可靠信號。
對於對於大於32的信號呢?接着往下看 __send_signal 裏面的代碼,會調用__sigqueue_alloc分配一個struct sigqueue 對象,而後經過 list_add_tail 掛在 struct sigpending 裏面的鏈表上。這樣,連續發送100個信號過來,就會在鏈表上掛100項,不會丟,靠譜多了。所以,大於 32 的信號咱們稱爲可靠信號。固然,隊列的長度也是有限制的,執行ulimit -a
命令能夠查看信號限制 pending signals (-i) 15348。
$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 15348 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 15348 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited
當信號掛到了 task_struct 結構以後,最後調用 complete_signal。這裏面的邏輯也很簡單,就是說,既然這個進程有了一個新的信號,趕忙找一個線程處理一下(在多線程的程序中,若是不作特殊的信號阻塞處理,當發送信號給進程時,由系統選擇一個線程來處理這個信號)。
static void complete_signal(int sig, struct task_struct *p, int group) { struct signal_struct *signal = p->signal; struct task_struct *t; /* * Now find a thread we can wake up to take the signal off the queue. * * If the main thread wants the signal, it gets first crack. * Probably the least surprising to the average bear. */ if (wants_signal(sig, p)) t = p; else { /* * Otherwise try to find a suitable thread. */ t = signal->curr_target; while (!wants_signal(sig, t)) { t = next_thread(t); if (t == signal->curr_target) /* * No thread needs to be woken. * Any eligible threads will see * the signal in the queue soon. */ return; } signal->curr_target = t; } ...... /* * The signal is already in the shared-pending queue. * Tell the chosen thread to wake up and dequeue it. */ signal_wake_up(t, sig == SIGKILL); return; }
在找到了一個進程或者線程的 task_struct 以後,咱們要調用 signal_wake_up,來企圖喚醒它,signal_wake_up 會調用 signal_wake_up_state。
void signal_wake_up_state(struct task_struct *t, unsigned int state) { set_tsk_thread_flag(t, TIF_SIGPENDING); if (!wake_up_state(t, state | TASK_INTERRUPTIBLE)) kick_process(t); }
signal_wake_up_state 裏面主要作了兩件事情。第一,就是給這個線程設置TIF_SIGPENDING,這就說明其實信號的處理和進程的調度是採起這樣一種相似的機制。
當發現一個進程應該被調度的時候,咱們並不直接把它趕下來,而是設置一個標識位TIF_NEED_RESCHED,表示等待調度,而後等待系統調用結束或者中斷處理結束,從內核態返回用戶態的時候,調用 schedule 函數進行調度。信號也是相似的,當信號來的時候,咱們並不直接處理這個信號,而是設置一個標識位 TIF_SIGPENDING,來表示已經有信號等待處理。一樣等待系統調用結束,或者中斷處理結束,從內核態返回用戶態的時候,再進行信號的處理。
signal_wake_up_state 的第二件事情,就是試圖喚醒這個進程或者線程。wake_up_state 會調用 try_to_wake_up 方法。這個函數就是將這個進程或者線程設置爲 TASK_RUNNING,而後放在運行隊列中,這個時候,當隨着時鐘不斷的滴答,早晚會被調用。若是 wake_up_state 返回 0,說明進程或者線程已是 TASK_RUNNING 狀態了,若是它在另一個 CPU 上運行,則調用 kick_process 發送一個處理器間中斷,強制那個進程或者線程從新調度,從新調度完畢後,會返回用戶態運行。這是一個時機會檢查TIF_SIGPENDING 標識位。
信號已經發送到位了,何時真正處理它呢?
就是在從系統調用或者中斷返回的時候,我們講調度的時候講過,不管是從系統調用返回仍是從中斷返回,都會調用 exit_to_usermode_loop,重點關注_TIF_SIGPENDING 標識位。
static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags) { while (true) { /* We have work to do. */ enable_local_irqs(); if (cached_flags & _TIF_NEED_RESCHED) schedule(); ..... /* deal with pending signal delivery */ if (cached_flags & _TIF_SIGPENDING) do_signal(regs);/*有信號掛起*/ if (cached_flags & _TIF_NOTIFY_RESUME) { clear_thread_flag(TIF_NOTIFY_RESUME); tracehook_notify_resume(regs); } ...... if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS)) break; } }
若是在前一個環節中,已經設置了 _TIF_SIGPENDING,咱們就調用 do_signal 進行處理。
void do_signal(struct pt_regs *regs) { struct ksignal ksig; if (get_signal(&ksig)) { /* Whee! Actually deliver the signal. */ handle_signal(&ksig, regs); return; } /* Did we come from a system call? */ if (syscall_get_nr(current, regs) >= 0) { /* Restart the system call - no handlers present */ switch (syscall_get_error(current, regs)) { case -ERESTARTNOHAND: case -ERESTARTSYS:/*一個系統調用因沒有數據阻塞,但然被信號喚醒,但系統調用還沒完成,會返回該標誌*/ case -ERESTARTNOINTR: regs->ax = regs->orig_ax; regs->ip -= 2; break; case -ERESTART_RESTARTBLOCK: regs->ax = get_nr_restart_syscall(regs); regs->ip -= 2; break; } } /* * If there's no signal to deliver, we just put the saved sigmask * back. */ restore_saved_sigmask(); }
do_signal 會調用 handle_signal。按說,信號處理就是調用用戶提供的信號處理函數,可是這事兒沒有看起來這麼簡單,由於信號處理函數是在用戶態的。
要回答這個問題還須要回憶系統調用的過程。這個進程當時在用戶態執行到某一行 Line A,調用了一個系統調用,當 syscall 指令調用的時候,會從這個寄存器裏面拿出函數地址來調用,也就是調用entry_SYSCALL_64
。
entry_SYSCALL_64
函數先使用swaps
切換到內核棧,而後保存當前用戶態的寄存器到內核棧,在內核棧的最高地址端,存放的是結構 pt_regs.這樣,這樣,在內核 pt_regs 裏面就保存了用戶態執行到了 Line A的指針。
如今咱們從系統調用返回用戶態了,按說應該從 pt_regs 拿出 Line A,而後接着 Line A 執行下去,可是爲了響應信號,咱們不能回到用戶態的時候返回 Line A 了,而是應該返回信號處理函數的起始地址。
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs) { bool stepping, failed; struct fpu *fpu = ¤t->thread.fpu; .... /* Are we from a system call? */ if (syscall_get_nr(current, regs) >= 0) { /* If so, check system call restarting.. */ switch (syscall_get_error(current, regs)) { case -ERESTART_RESTARTBLOCK: case -ERESTARTNOHAND: regs->ax = -EINTR; break; /*當發現出現錯誤 ERESTARTSYS 的時候,咱們就知道這是從一個沒有調用完的系統調用返回的, 設置系統調用錯誤碼 EINTR。*/ case -ERESTARTSYS: if (!(ksig->ka.sa.sa_flags & SA_RESTART)) { regs->ax = -EINTR;/*設置系統調用錯誤碼 EINTR。*/ break; } /* fall through */ case -ERESTARTNOINTR: regs->ax = regs->orig_ax; regs->ip -= 2; break; } } ...... failed = (setup_rt_frame(ksig, regs) < 0); ...... signal_setup_done(failed, ksig, stepping); }
這個時候,就須要干預和本身來定製 pt_regs 了。這個時候,要看,是否從系統調用中返回。若是是從系統調用返回的話,還要區分是從系統調用中正常返回,仍是在一個非運行狀態的系統調用中,由於會被信號中斷而返回。
這裏解析一個最複雜的場景。還記得我們解析進程調度的時候,舉的一個例子,就是從一個 tap 網卡中讀取數據。當時主要關注 schedule 那一行,也即若是當發現沒有數據的時候,就調用 schedule,本身進入等待狀態,而後將 CPU 讓給其餘進程。具體的代碼以下:
static ssize_t tap_do_read(struct tap_queue *q, struct iov_iter *to, int noblock, struct sk_buff *skb) { ...... while (1) { if (!noblock) prepare_to_wait(sk_sleep(&q->sk), &wait, TASK_INTERRUPTIBLE); /* Read frames from the queue */ skb = skb_array_consume(&q->skb_array); if (skb) break; if (noblock) { ret = -EAGAIN; break; } if (signal_pending(current)) { ret = -ERESTARTSYS; break; } /* Nothing to read, let's sleep */ schedule(); } ...... }
這裏咱們關注和信號相關的部分。這實際上是一個信號中斷系統調用的典型邏輯。
首先,咱們把當前進程或者線程的狀態設置爲 TASK_INTERRUPTIBLE,這樣才能是使這個系統調用能夠被中斷。
其次,能夠被中斷的系統調用每每是比較慢的調用,而且會由於數據不就緒而經過 schedule讓出 CPU 進入等待狀態。在發送信號的時候,咱們除了設置這個進程和線程的_TIF_SIGPENDING 標識位以外,還試圖喚醒這個進程或者線程,也就是將它從等待狀態中設置爲 TASK_RUNNING。
當這個進程或者線程再次運行的時候,咱們根據進程調度第必定律,從 schedule 函數中返回,而後再次進入 while 循環。因爲這個進程或者線程是由信號喚醒的,而不是由於數據來了而喚醒的,於是是讀不到數據的,可是在 signal_pending 函數中,咱們檢測到了_TIF_SIGPENDING 標識位,這說明系統調用沒有真的作完,因而返回一個錯誤ERESTARTSYS,而後帶着這個錯誤從系統調用返回。
而後,咱們到了 exit_to_usermode_loop->do_signal->handle_signal。在這裏面,當發現出現錯誤ERESTARTSYS 的時候,咱們就知道這是從一個沒有調用完的系統調用返回的,設置系統調用錯誤碼 EINTR。
接下來,咱們就開始折騰 pt_regs 了,主要經過調用 setup_rt_frame->__setup_rt_frame。
static int __setup_rt_frame(int sig, struct ksignal *ksig, sigset_t *set, struct pt_regs *regs) { struct rt_sigframe __user *frame; void __user *fp = NULL; int err = 0; frame = get_sigframe(&ksig->ka, regs, sizeof(struct rt_sigframe), &fp); ..... if (ksig->ka.sa.sa_flags & SA_SIGINFO) { if (copy_siginfo_to_user(&frame->info, &ksig->info)) return -EFAULT; } put_user_try { /* Create the ucontext. */ put_user_ex(frame_uc_flags(regs), &frame->uc.uc_flags); put_user_ex(0, &frame->uc.uc_link); save_altstack_ex(&frame->uc.uc_stack, regs->sp); /* Set up to return from userspace. If provided, use a stub already in userspace. */ /* x86-64 should always use SA_RESTORER. */ if (ksig->ka.sa.sa_flags & SA_RESTORER) { put_user_ex(ksig->ka.sa.sa_restorer, &frame->pretcode); } .... } put_user_catch(err); err |= setup_sigcontext(&frame->uc.uc_mcontext, fp, regs, set->sig[0]); err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set)); ...... /* Set up registers for signal handler */ regs->di = 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; regs->cs = __USER_CS; .... return 0; }
frame 的類型是 rt_sigframe。frame 的意思是幀。
將sa.sa_restorer的地址保存到frame的pretcode中。這個操做比較重要。
原來的 pt_regs 不能丟,調用setup_sigcontext將原來的 pt_regs 保存在了 frame 中的 uc_mcontext 裏面。
信號處理期間要屏蔽的信號set也保存到frame->uc.uc_sigmask。
在 __setup_rt_frame 把 regs->sp 設置成等於 frame。這就至關於強行在程序原來的用戶態的棧裏面插入了一個棧幀。
並在最後將 regs->ip 設置爲用戶定義的信號處理函數 sa_handler。
這意味着,原本返回用戶態應該接着原來系統調用的代碼執行的,如今不了,要執行 sa_handler 了。那執行完了之後呢?按照函數棧的規則,彈出上一個棧幀來,也就是彈出了 frame。按照函數棧的規則。函數棧裏面包含了函數執行完跳回去的地址。
那若是咱們假設 sa_handler 成功返回了,怎麼回到程序原來在用戶態運行的地方呢?那就是在 __setup_rt_frame 中,經過 put_user_ex,將 sa_restorer 放到 frame->pretcode 裏面。當 sa_handler 執行完以後,會執行iret指令,彈出返回地址,也就到 sa_restorer 執行。sa_restorer 這是什麼呢?
我們在 sigaction 介紹的時候就沒有介紹它,在 Glibc 的 __libc_sigaction 函數中也沒有注意到,它被賦值成了 restore_rt。這其實就是 sa_handler 執行完畢以後,立刻要執行的函數。從名字咱們就能感受到,它將恢復原來程序運行的地方。
在 Glibc 中,咱們能夠找到它的定義,它居然調用了一個系統調用,系統調用號爲__NR_rt_sigreturn。
RESTORE (restore_rt, __NR_rt_sigreturn) #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"
在內核裏面找到 __NR_rt_sigreturn 對應的系統調用。
asmlinkage long sys_rt_sigreturn(void) { 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(VERIFY_READ, frame, sizeof(*frame))) goto badframe; if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set))) 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; ..... }
在這裏面,把上次填充的那個 rt_sigframe 拿出來,而後 restore_sigcontext 將 pt_regs恢復成爲原來用戶態的樣子。從這個系統調用返回的時候,應用A還誤覺得從上次的系統調用返回的呢。
至此,整個信號處理過程才所有結束。
信號的發送與處理是一個複雜的過程,這裏來總結一下。
上節說了linux信號指的是linux進程或者整個進程組,其實線程和進程在linux內核中都是task,發送信號能夠是kill或tkill,對於在多線程的程序中,若是不作特殊的信號阻塞處理,當發送信號給進程時,從上面內核代碼中看到,由系統選擇一個線程來處理這個信號。
每一個線程由本身的信號屏蔽字,可是信號的處理是進程中全部線程貢獻的。這意味着單個線程能夠阻止某些信號,可是當某個線程修改了給定信號的相關處理行爲後,該進程下的全部線程都必須共享這個處理方式的改變。換句話說:signal或者sigaction是進程裏面的概念,他們是針對整個進程進行控制的,是全部線程共享的,某個線程調用了signal或者sigaction更改了給定信號的處理方式,那麼進程信號處理方式就改變。
若是一個信號與硬件故障相關,那麼該信號通常會發給引發該事件的線程中去。
線程的信號操做接口以下。線程使用pthread_sigmask來阻止信號發送。
#include<signal.h> int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
set
參數包含線程用於修改信號屏蔽字的信號集,oset
若是不爲NULL,並把oset設置爲siget_t結構的地址,來獲取當前的信號屏蔽字。how
:SIG_BLOCK將set加入到線程信號屏蔽字中,SIG_SETMASK用信號集set替換線程當前的信號屏蔽字;SUG_UNBLOCK,從當前線程信號屏蔽字中移除set中的信號。
線程可經過sigwait等待一個或多個信號的出現。
#include<signal.h> int siwait(const sigset_t *restrict set, int *restrich signop) int sigwaitinfo(const sigset_t *set, siginfo_t *si); int sigtimedwait (const sigset_t *set, siginfo_t *si, const struct timespec *timeout);
set參數指定線程等待的信號集,signop指向整數將包含發送信號的數量。sigwait從set中選擇一個未決信號(pending),從線程的未決信號集中移除該信號,並在sig中返回該信號值。若是set中的全部信號都不是pending狀態,則sigwait會阻塞調用它的線程,直到set中的信號變爲pending。
除了返回信息方面,sigwaitinfo的行爲基本上與sigwait相似。sigwait在sig中返回觸發的信號值;而sigwaitinfo的返回值就是觸發的信號值,而且若是info不爲NULL,則sigwaitinfo返回時,還會在siginfo_t *info中返回更多該信號的信息.
線程可經過pthread_kill把信號發送給線程。
#include<signal.h> int pthread_kill(pthread_t thread,int signo);
能夠傳一個0值得signo來檢查一個線程是否存在。若是信號的默認處理動做是終止該進程,那麼把該信號傳遞給某個線程任然會殺死整個進程。
參考:
英特爾® 64 位和 IA-32 架構軟件開發人員手冊第 3 卷 :系統編程指南
極客時間專欄-趣談Linux操做系統 《linux內核源代碼情景分析》