版權聲明:本文為本文為部落客原創文章,轉載請注明出處。如有錯誤,歡迎指正。部落格位址:https://www.cnblogs.com/wsg1100/
目錄
- 1. Linux信号
- 1.1注冊信号處理函數
- 1.2 信号的發送
- 1.3 信号的處理
- 2 linux 多線程信号
涉及硬體底層,本文以X86平台講解。
信号是事件發生時對程序的通知機制,是作業系統提供的一種軟體中斷。信号提供了一種異步處理事件的方法,信号與硬體中斷的相似之處在于打斷了程式執行的正常流程,例如,中斷使用者鍵入中斷鍵(Ctrl+C),會通過信号機制停止應用程式。
- 信号的幾種産生方式:
- 按鍵産生 當使用者按某些按鍵時,引發終端産生的信号。 ctrl+C産生 SIGINT信号、ctrl+\産生SIGQUIT信号、ctrl+z産生SIGTSTP 信号
- 系統調用産生 程序調用系統調用函數将任意信号發送給另一個程序或程序組。如:kill() 、raise()、abort()
- 軟體條件産生 當作業系統檢測到某種軟體條件時,使用信号通知有關程序。如:定時器函數alarm()
- 硬體異常産生 由硬體檢測到某些條件,并通知核心,然後核心為該條件發生時正在運作的程序産生适當的信号。如:非法操作記憶體(段錯誤)SIGSEGV信号、除0操作(浮點數除外)SIGFPE 信号、總線錯誤SIGBUS信号。
- 指令産生 其時都是系統調用的封裝,如:
。kill -9 pid
- 當某個信号産生時,程序可告訴核心其處理方式:
- 執行系統預設動作。linux下可通過
來檢視具體信号及預設處理動作,大多數信号的系統預設動作是終止程序。man 7 signal
- 忽略此信号。SIGKILL和SIGSTOP信号不能被忽略,它們用于在任何時候中斷或結束某一程序。。
- 捕捉信号。告訴核心在某種信号産生時,調用一個使用者處理函數。在處理函數中,執行使用者對該信号的具體處理。SIGKILL和SIGSTOP信号不能捕捉。
- 信号的兩種狀态
- 抵達 遞送并且到達程序。
- 未決 産生和遞達之間的狀态。主要由于阻塞(屏蔽)導緻該狀态。
可以使用
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,表使用預設屬性。
是以,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 的意思是幀。
- 在 get_sigframe 中會得到 pt_regs 的 sp 變量,也就是原來這個程式在使用者态的棧頂指針,然後 get_sigframe 中,我們會将 sp 減去 sizeof(struct rt_sigframe),也就是把這個棧幀塞到了使用者棧裡面。
- 将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。
【原創】xenomai核心解析--信号signal(一)---Linux信号機制
這意味着,本來傳回使用者态應該接着原來系統調用的代碼執行的,現在不了,要執行 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還誤以為從上次的系統調用傳回的呢。
至此,整個信号處理過程才全部結束。
信号的發送與處理是一個複雜的過程,這裡來總結一下。
- 假設我們有一個程序 A,main 函數裡面調用系統調用進入核心。
- 按照系統調用的原理,會将使用者态棧的資訊儲存在 pt_regs 裡面,也即記住原來使用者态是運作到了 line A 的地方。
- 在核心中執行系統調用讀取資料。
- 當發現沒有什麼資料可讀取的時候,隻好進入睡眠狀态,并且調用 schedule 讓出 CPU,這是程序排程第一定律。
- 将程序狀态設定為 TASK_INTERRUPTIBLE,可中斷的睡眠狀态,也即如果有信号來的話,是可以喚醒它的。
- 其他的程序或者 shell 發送一個信号,有四個函數可以調用 kill,tkill,tgkill,rt_sigqueueinfo
- 四個發送信号的函數,在核心中最終都是調用 do_send_sig_info
- do_send_sig_info 調用 send_signal 給程序 A 發送一個信号,其實就是找到程序 A 的task_struct,或者加入信号集合,為不可靠信号,或者加入信号連結清單,為可靠信号
- do_send_sig_info 調用 signal_wake_up 喚醒程序 A。
- 程序 A 重新進入運作狀态 TASK_RUNNING,根據程序排程第一定律,一定會接着schedule 運作。
- 程序 A 被喚醒後,檢查是否有信号到來,如果沒有,重新循環到一開始,嘗試再次讀取資料,如果還是沒有資料,再次進入 TASK_INTERRUPTIBLE,即可中斷的睡眠狀态。
- 當發現有信号到來的時候,就傳回目前正在執行的系統調用,并傳回一個錯誤表示系統調用被中斷了。
- 系統調用傳回的時候,會調用 exit_to_usermode_loop,這是一個處理信号的時機
- 調用 do_signal 開始處理信号
- 根據信号,得到信号處理函數 sa_handler,然後修改 pt_regs 中的使用者态棧的資訊,讓pt_regs 指向 sa_handler。同時修改使用者态的棧,插入一個棧幀 sa_restorer,裡面儲存了原來的指向 line A 的 pt_regs,并且設定讓 sa_handler 運作完畢後,跳到 sa_restorer 運作。
- 傳回使用者态,由于 pt_regs 已經設定為 sa_handler,則傳回使用者态執行 sa_handler。
- sa_handler 執行完畢後,信号處理函數就執行完了,接着根據第 15 步對于使用者态棧幀的修改,會跳到 sa_restorer 運作。
- sa_restorer 會調用系統調用 rt_sigreturn 再次進入核心。
- 在核心中,rt_sigreturn 恢複原來的 pt_regs,重新指向 line A。
- 從 rt_sigreturn 傳回使用者态,還是調用 exit_to_usermode_loop。
- 這次因為 pt_regs 已經指向 line A 了,于是就到了程序 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核心源代碼情景分析》
作者:wsg1100
出處:http://www.cnblogs.com/wsg1100/
本文版權歸作者和部落格園共有,歡迎轉載,但必須給出原文連結,并保留此段聲明,否則保留追究法律責任的權利。