Linux进程间通信——信号
1.信号的介绍
信号( Signal )是 Unix, 类Unix以及其他POSIX兼容的操作系统中进程间通信的一种有限制的手段。
- 1)信号是在软件层面上对中断机制的一种模拟,是一种异步通信方式。
- 2)信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用呀来通知用户空间进程发生了那些系统事件——用来提醒进程一个事件已经发生,当一个信号发送给一个进程,操作系统中断了进程正常的控制流程),此时,任何非原子操作都将被中断。
- 3)如果该进程当前未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才传递给进程
在linux系统中,提供了几十种信号,分别代表不同的意义。通过下面的bash命令,可以得到Linux中各种信号,供64种:
~$ kill -l
信号可以在任何时候发送给某一个进程,进程需要为这个信号配置信号处理函数吗。当某个信号发生时,就默认执行这个函数就可以。
通过以下指令可以查看各个信号的具体含义和对应的处理方法
man 7 signal
由上表可见,信号的处理大致分为三种:
- 执行默认操作(default action)。Linux对每种信号都规定了默认操作,例如上面列表中的Action 列的Term, Core等:
- Term : “ Default action is to terminate the process ”就是终止进程的默认操作
- Ign:“ Default action is to ignore the signal "就是忽略进程的默认操作
- Core:” Default action is to terminate the process and dump core (see core(5)) “就是终止进程并生成 core dump 文件(通常用于调试)的默认操作
- Stop : " Default action is to stop the process "就是停止进程的默认操作
- Cont : " Default action is to continue the process if it is currently stopped "就是恢复执行已被停止的进程的默认操作
- 捕捉信号。我们可以为信号定义一个信号处理函数,当信号发生时,我们就执行相应的信号处理函数。
- 忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程。
2.信号的来源
信号事件的来源有两种:
- 硬件来源(本质是对硬件中断的一种模拟)
- 软件来源,最常用发送信号的系统函数是 kill, alarm和settimer以及sigqueue函数,软件来源还包括一些非法运算等操作
信号可以在直接与用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
如果该进程当前未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给 它;如果一个信号 被进程设置为阻塞,则该进程的传递就被延迟,直到其阻塞取消时才被传递给进程。
3.信号与中断的异同
相似之处:
- 均是异步通信方式
- 均会注册处理函数,都是用于对当前的任务进行一些处理,如调度,停止等。当检测出有信号或中断请求时,都会暂停当前任务而转去执行相应的处理程序。
- 在处理程序结束后,都会返回断点
- 信号和中断都可进行屏蔽
但是二者实际上有很大不同,其主要区别有:
- 中断和信号虽然都可能源于硬件和软件,但是中断处理函数注册于内核之中,由内核中运行,而信号的处理函数注册于用户态,内核收到信号后根据当前任务task_struct结构体中的信号相关的数据结构找寻对应的处理函数并最终在用户态处理
- 中断作用于内核全局,而信号作用于当前任务(进程)。即信号影响的往往是一个进程,而中断处理如果出现问题则会导致整个Linux内核的崩溃。
- 中断是有优先级的,而信号不存在,信号之间都是“平等”的;
- 中断是及时响的,而信号具有很大的时延
既然信号是中断的一种模拟,那为什么不直接采用中断而是设计了信号呢?
(1)安全性:由于中断的异常处理可能会导致内核崩溃,所以直接操作会维系饿系统稳定和安全
(2)简化:信号将复杂的硬件中断机制抽象成简单事件通知,用户进程只需关心”发生了什么“,不关心”中断源是如何处理的“
(3)灵活性:信号可进程捕获,忽略或自定义处理,而中断相应必须立即,原子地完成,不允许复杂的用户处理逻辑
4.注册信号处理函数
有些时候我们希望能够让信号运行一些特殊处理功能,所以有了自定义的信号处理函数。注册API主要有signal()和sigaction() (比较推荐)。
/* Set the handler for the signal SIG to HANDLER,returning the old handler, or SIG_ERR on error. */
__sighandler_t
signal (int sig, __sighandler_t handler)
{__set_errno (ENOSYS);return SIG_ERR;
}
其主要区别在于sigaction()对于信号sig会绑定对应的结构体sigaction而不仅仅是一个处理函数sighandler_t。这样做的好处是可以更精细地控制信号处理,通过不同参数实现不同的效果。例如sa_flags可以设置如:
- SA_ONESHOT:信号处理函数仅作用一次,之后启用默认行为
- SA_NOMASK:该信号处理函数执行过程中允许被其他信号或者相同信号中断,即不屏蔽
- SA_INTERRUPT:该信号处理函数若执行过程中被中断,则不会再调度回该函数继续执行,而是直接返回-EINTR,将执行逻辑交还给调用方
- SA_RESTART:与SA_INTERRUPT相反,会自动重启该函数
/* Structure describing the action to be taken when a signal arrives. */
struct sigaction{/* Signal handler. */
#if defined __USE_POSIX199309 || defined __USE_XOPEN_EXTENDEDunion{/* Used if SA_SIGINFO is not set. */__sighandler_t sa_handler;/* Used if SA_SIGINFO is set. */void (*sa_sigaction) (int, siginfo_t *, void *);}__sigaction_handler;
# define sa_handler __sigaction_handler.sa_handler
# define sa_sigaction __sigaction_handler.sa_sigaction
#else__sighandler_t sa_handler;
#endif/* Additional set of signals to be blocked. */__sigset_t sa_mask;/* Special flags. */int sa_flags;};
4.1 用户调用
通常用户会在代码中这样编写
struct sigaction act;
act.sa_handler = my_handler; // 或 sa_sigaction = my_sigaction
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
4.2 进入glibc层
用户在代码中调用的 sigaction本质上就是 __sigaction()
/* If ACT is not NULL, change the action for SIG to *ACT.If OACT is not NULL, put the old action for SIG in *OACT. */
int
__sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
{if (sig <= 0 || sig >= NSIG || is_internal_signal (sig)){__set_errno (EINVAL);return -1;}internal_sigset_t set;if (sig == SIGABRT)__abort_lock_wrlock (&set);int r = __libc_sigaction (sig, act, oact);if (sig == SIGABRT)__abort_lock_unlock (&set);return r;
}
首先进行合法性检查,再对sig == SIGABRT的特殊信号进行加锁处理,然后调用__libc_sigaction(sig, act, oact),通常最终系统调用为rt_sigacton().
rt_sigaction()是Linux内核对信号注册的实现,内核会维护每个进程的信号处理表
- 如果act非空,内核会将信号编号对应的handler设置为你传入的处理函数,同时记录掩码和flags等
- 如果oact非空,内核会将原先的处理方式返回给用户
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;
}
do_sigaction()会将用户层传来的信号处理函数赋值给当前任务task_struct_current对应的sighand->action[]数组中sig信号对应的位置,以用于之后调用。
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;
......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;
}
5.发送信号
不论通过kiil还是sigqueue系统调用还是通过tkill或者tgkill发送指定线程的信号,其最终调用的均是do_send_sig_info()函数,其调用链如下所示:
kill()->kill_something_info()->kill_pid_info()->group_send_sig_info()->do_send_sig_info()tkill()->do_tkill()->do_send_specific()->do_send_sig_info()tgkill()->do_tkill()->do_send_specific()->do_send_sig_info()rt_sigqueueinfo()->do_rt_sigqueueinfo()->kill_proc_info()->kill_pid_info()->group_send_sig_info()->do_send_sig_info()
do_send_sig_info() 会调用 send_signal(),进而调用 __send_signal()。这里代码比较复杂,主要逻辑如下
- 根据发送信号的类型判断是共享信号还是线程独享信号,由此赋值pending。如果是 kill 发送的,也就是发送给整个进程的,就应该发送给 t->signal->shared_pending,这里面是整个进程所有线程共享的信号;如果是 tkill 发送的,也就是发给某个线程的,就应该发给 t->pending,这里面是这个线程的 task_struct 独享的。
- 调用 legacy_queue()判断是否为可靠信号,不可靠则直接退出
- 调用__sigqueue_alloc() 分配一个 struct sigqueue 对象,然后通过 list_add_tail 挂在 struct sigpending 里面的链表上。
- 调用 complete_signal()分配线程处理该信号
5.1信号的排队和丢失
不可靠信号(传统信号,编号 < SIGRTMIN,通常为1~31)
- 通过legacy_queue()判断:如果该信号已存在于目标的信号集合(sigpending)中,则直接丢弃新来的同类型信号(不重复排队)。
- 这样做的后果是:同一个不可靠信号只会被投递一次,多次发送会丢失。
这也是“不可靠信号”的核心定义(不是说系统不可靠,而是信号可能丢失)。
可靠信号(实时信号,编号 ≥ SIGRTMIN,通常为32~64)
- 每一个实时信号都可以排队,即多次发送会在队列中保存多份,不会丢失。
- 通过__sigqueue_alloc()分配sigqueue对象,链入待处理队列。
static inline int legacy_queue(struct sigpending *signals, int sig)
{return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}#define SIGRTMIN 32
#define SIGRTMAX _NSIG
#define _NSIG 64
5.2信号投递与唤醒过程
- 信号(无论可靠、不可靠)加入进程/线程的sigpending或shared_pending集合/队列。
- 调用complete_signal()寻找适合的线程处理该信号:
- 优先找主线程。
- 多线程时,遍历线程组,找到合适的线程唤醒。
- 如果是致命信号(如SIGKILL),则全组强制唤醒并终止。
-
static void complete_signal(int sig, struct task_struct *p, enum pid_type type) {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 if ((type == PIDTYPE_PID) || thread_group_empty(p))/** There is just one thread and it does not need to be woken.* It will dequeue unblocked signals before it runs again.*/return;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;}/** Found a killable thread. If the signal will be fatal,* then start taking the whole group down immediately.*/if (sig_fatal(p, sig) &&!(signal->flags & SIGNAL_GROUP_EXIT) &&!sigismember(&t->real_blocked, sig) &&(sig == SIGKILL || !p->ptrace)) {/** This signal will be fatal to the whole group.*/if (!sig_kernel_coredump(sig)) {/** Start a group exit and wake everybody up.* This way we don't have other threads* running and doing things after a slower* thread has the fatal signal pending.*/signal->flags = SIGNAL_GROUP_EXIT;signal->group_exit_code = sig;signal->group_stop_count = 0;t = p;do {task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);sigaddset(&t->pending.signal, SIGKILL);signal_wake_up(t, 1);} while_each_thread(p, t);return;}}/** 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; }
- 唤醒的关键是调用signal_wake_up():
- 设置线程的TIF_SIGPENDING标志,表示“有信号待处理”。
- 尝试唤醒目标线程(如果已在TASK_RUNNING状态,则通过IPI强制调度)。
-
static inline void signal_wake_up(struct task_struct *t, bool resume) {signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0); }void signal_wake_up_state(struct task_struct *t, unsigned int state) {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.*/if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))kick_process(t); }
6. 信号的处理
这里我们以一个从tap 网卡中读取数据的例子来分析信号的处理逻辑。从网卡读取数据会通过系统调用进入内核,之后通过函数调用表找到对应的函数执行。在读的过程中,如果没有数据处理则会调用schedule()函数主动让出CPU进入休眠状态并等待再次唤醒。
tap_do_read()主要逻辑为:
- 把当前进程或者线程的状态设置为 TASK_INTERRUPTIBLE,这样才能使这个系统调用可以被中断。
- 可以被中断的系统调用往往是比较慢的调用,并且会因为数据不就绪而通过 schedule() 让出 CPU 进入等待状态。在发送信号的时候,我们除了设置这个进程和线程的 _TIF_SIGPENDING 标识位之外,还试图唤醒这个进程或者线程,也就是将它从等待状态中设置为 TASK_RUNNING。当这个进程或者线程再次运行的时候,会从 schedule() 函数中返回,然后再次进入 while 循环。由于这个进程或者线程是由信号唤醒的而不是因为数据来了而唤醒的,因而是读不到数据的,但是在 signal_pending() 函数中,我们检测到了 _TIF_SIGPENDING 标识位,这说明系统调用没有真的做完,于是返回一个错误 ERESTARTSYS,然后带着这个错误从系统调用返回。
- 如果没有信号,则继续调用schedule()让出CPU
static ssize_t tap_do_read(struct tap_queue *q,struct iov_iter *to,int noblock, struct sk_buff *skb)
{DEFINE_WAIT(wait);ssize_t ret = 0;if (!iov_iter_count(to)) {kfree_skb(skb);return 0;}if (skb)goto put;while (1) {if (!noblock)prepare_to_wait(sk_sleep(&q->sk), &wait,TASK_INTERRUPTIBLE);/* Read frames from the queue */skb = ptr_ring_consume(&q->ring);if (skb)break;if (noblock) {ret = -EAGAIN;break;}if (signal_pending(current)) {ret = -ERESTARTSYS;break;}/* Nothing to read, let's sleep */schedule();}if (!noblock)finish_wait(sk_sleep(&q->sk), &wait);put:if (skb) {ret = tap_put_user(q, skb, to);if (unlikely(ret < 0))kfree_skb(skb);elseconsume_skb(skb);}return ret;
}
schedule()会在系统调用负担会或者中断返回的时刻调用exit_to_usermode_loop(),在任务调度中的标记位为_TIF_NEED_RESCHED,而对于信号来说就是_TIF_SIGPENDING。
static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{while (true) {
......if (cached_flags & _TIF_NEED_RESCHED)schedule();
....../* deal with pending signal delivery */if (cached_flags & _TIF_SIGPENDING)do_signal(regs);
......if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS))break;}
}
do_signal()函数会调用handle_signal()函数,这里主要存在一个问题使得逻辑变得较为复杂:信号处理函数定义于用户态,而调度过程位于内核态。
/** Note that 'init' is a special process: it doesn't get signals it doesn't* want to handle. Thus you cannot kill init even with a SIGKILL even by* mistake.*/
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();
}
handle_signal()会判断当前是否从系统调用调度而来,当发现错误码为ERESTARTSYS的时候就知道这是从一个没有调用完的系统调用返回的,设置系统错误码为EINTR。由于此处不会直接返回任务调度前记录的用户态状态,而是进入注册好的信号处理函数,因此需要调用setup_rt_frame()构建新的寄存器结构体pt_regs。
static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{bool stepping, failed;
....../* 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;case -ERESTARTSYS:if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {regs->ax = -EINTR;break;}/* fallthrough */case -ERESTARTNOINTR:regs->ax = regs->orig_ax;regs->ip -= 2;break;}}
......failed = (setup_rt_frame(ksig, regs) < 0);
......signal
setup_rt_frame()主要调用__setup_rt_frame(),主要逻辑为:
- 调用get_sigframe()得到regs中的sp寄存器值,即原进程用户态的栈顶指针,将sp减去sizeof(struct rt_sigframe)从而把该新建栈帧压入栈
- 调用put_user_ex(),将 sa_restorer 按照函数栈的规则放到了 frame->pretcode 里面。函数栈里面包含了函数执行完跳回去的地址,当 sa_handler 执行完之后,弹出的函数栈是 frame,也就应该跳到 sa_restorer 的地址
- 调用setup_sigcontext() 里面,将原来的 pt_regs 保存在了 frame 中的 uc_mcontext 里
- 填充regs,将regs->ip设置为自定义的信号处理函数sa_handler,将栈顶regs->sp设置为新栈帧frame地址
static int setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs) { ......return __setup_rt_frame(ksig->sig, ksig, set, regs); ...... }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); ......put_user_try { ....../* Set up to return from userspace. If provided, use a stubalready 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);} else {/* could use a vstub here */err |= -EFAULT;}} 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));if (err)return -EFAULT;/* 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 thenext 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;/** Set up the CS and SS registers to run signal handlers in* 64-bit mode, even if the handler happens to be interrupting* 32-bit or 16-bit code.** SS is subtle. In 64-bit mode, we don't need any particular* SS descriptor, but we do need SS to be valid. It's possible* that the old SS is entirely bogus -- this can happen if the* signal we're trying to deliver is #GP or #SS caused by a bad* SS value. We also have a compatbility issue here: DOSEMU* relies on the contents of the SS register indicating the* SS value at the time of the signal, even though that code in* DOSEMU predates sigreturn's ability to restore SS. (DOSEMU* avoids relying on sigreturn to restore SS; instead it uses* a trampoline.) So we do our best: if the old SS was valid,* we keep it. Otherwise we replace it.*/regs->cs = __USER_CS;if (unlikely(regs->ss != __USER_DS))force_valid_ss(regs);return 0; }
7.信号的发送、处理总结
- 假设我们有一个进程A会从tap网卡中读取数据,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_restorer会调用系统调用rt_sigreturn再次进入内核
- 在内核中,rt_sigreturn恢复到原来的pt_regs,重新指向line A
- 从rt_sigreturn返回用户态,还是调用exit_to_usermode_loop()
- 这次因为pt_regs已经指向line A了,于是就到了进程A中接着系统调用之后运行,当然这个系统调用返回的是它被中断了没有执行完的错误
参考引用:linux_kernel_wiki/文章/进程管理/进程间通信之信号.md at main · 0voice/linux_kernel_wiki