Linux信号机制概述
Linux信号机制概述
作者:bullbat
还是先看看Linux中用户空间怎么运用的,用户空间编程实例如下:
#include<signal.h>
#include<stdio.h>
#include<unistd.h>
/*下面为两个新的信号操作函数*/
void handler(int sig)
{
printf("Receive signal :%u\n",sig);
}
void sigroutine(int num)
{
switch(num)
{
case 1:
printf("SIGUP signal\n");
break;
case 2:
printf("SIGINT signal\n");
break;
case 3:
printf("SIGQUIT signal\n");
break;
default:
break;
}
return;
}
int main(void)
{
struct sigaction sa;
int count;
sa.sa_handler=handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags=0;
printf("task id is:%d\n",getpid());
/*下面四条语句为相应的信号设置新的处理方法*/
sigaction(SIGTERM,&sa,NULL);
signal(SIGHUP,sigroutine);
signal(SIGINT,sigroutine);
signal(SIGQUIT,sigroutine);
while(1)
{
sigsuspend(&sa.sa_mask);/*阻塞,一直等待信号到达*/
printf("loop\n");
}
return 0;
}
可见,用户空间调用了很多系统调用来实现信号的编程,为了弄清楚他的内在原理,决定将内核中的实现做一个大致的梳理。为了理清思路,我们由内核中实现信号操作涉及的关键数据结构关系画出下图,我们看到,内核中的数据结构实现较简单,主要分两部分,一部分用于信号操作(即handler),由进程的sighand字段开始;另一部分用于信号的挂起,由进程的signal和pending字段索引。
由关系图,我们大致观其实现原理如下:
1, 进程的所有信号(现为32个)由一个数组task->sighand->action[]保存,数组的下标即为信号的ID,比如SIGQUIT等,每个操作由一个数据结构sigaction实现,该字段的sa_handler即为实现的操作;
2, 进程对挂起的信号有两种队列,一种为所有进程共享的。该队列的每一项为一个sigqueue结构,通过该结构info字段的si_signo等属性可以定位到对应的信号ID。其中sigset_t结构为一个32位整型,用于定位到ID,即类似位图的表示。
我们看几个最基本的操作于内核中的实现。
1, 设置新的action;
系统调用signal用于实现这个功能,当然也可以用sigaction系统调用,
SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
{
struct k_sigaction new_sa, old_sa;
int ret;
new_sa.sa.sa_handler = handler;
new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
sigemptyset(&new_sa.sa.sa_mask);
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsignedlong)old_sa.sa.sa_handler;
}
该系统调用分配一个新的action后,调用do_sigaction完成实际工作,最终返回旧的action的handler。
int do_sigaction(int sig,struct k_sigaction *act, struct k_sigaction *oact)
{
struct task_struct *t = current;
struct k_sigaction *k;
sigset_t mask;
……
k = &t->sighand->action[sig-1];
spin_lock_irq(¤t->sighand->siglock);
if (oact)
*oact = *k;/*保存旧的action*/
if (act) {
sigdelsetmask(&act->sa.sa_mask,
sigmask(SIGKILL) | sigmask(SIGSTOP));
*k = *act;/*设置新的action*/
/*对两种handler的特殊处理*/
if (sig_handler_ignored(sig_handler(t, sig), sig)) {
……
}
}
spin_unlock_irq(¤t->sighand->siglock);
return 0;
}
实现很简单,先保存旧的action,用于系统调用返回,然后设置新的action。
2, 发送信号
发送信号的系统调用有很多,最终都会调用__send_signal()函数。
staticint __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;
……
/*找到需要挂起的队列*/
pending = group ? &t->signal->shared_pending : &t->pending;
……
/*分配队列项结构*/
q = __sigqueue_alloc(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 = 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;
}
} else if (!is_si_special(info)) {
if (sig >= SIGRTMIN && info->si_code != SI_USER)
return -EAGAIN;
}
out_set:
signalfd_notify(t, sig);/*唤醒action中的等待队列*/
sigaddset(&pending->signal, sig);/*设置信号ID位掩码,即上面所说的那个位图*/
complete_signal(sig, t, group);/*试着唤醒执行该信号的进程*/
return 0;
}
发送信号,即将该信号链接到制定进程的信号挂起队列上,最后试着唤醒执行该信号的进程。
3, 信号捕获与执行
说了这么一堆,但我们还不明白内核怎样确保进程的挂起信号得到处理呢?内核在允许进程恢复用户态下的执行之前,检查进程TIF_SIGPENDING标志的值。每当内核处理玩一个中断或异常时,就检查是否存在挂起信号,即我们可以这样理解,在每次由内核态切换到用户态前,内核都会发起信号队列的处理,具体的信号发起和体系结构有关,但最终为了处理阻塞的挂起信号,内核都会调用do_signal()函数,为说明程序执行框架,下面的代码省略了大部分无关的代码。
staticvoid do_signal(struct pt_regs *regs)
{
struct k_sigaction ka;
siginfo_t info;
int signr;
sigset_t *oldset;
……
/*获取挂起信号*/
signr = get_signal_to_deliver(&info, &ka, regs, NULL);
if (signr > 0) {
……
/*实际的信号处理*/
if (handle_signal(signr, &info, &ka, oldset, regs) == 0) {
current_thread_info()->status &= ~TS_RESTORE_SIGMASK;
}
return;
}
/*如果从系统调用返回*/
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 = NR_restart_syscall;
regs->ip -= 2;
break;
}
}
……
}
该系统调用主要分为两部分执行,首先是冲挂起队列中查找信号,然后是执行信号处理函数。
1)查找指定信号由do_signal()->get_signal_to_deliver()完成
int get_signal_to_deliver(siginfo_t *info,struct k_sigaction *return_ka,
struct pt_regs *regs, void *cookie)
{
struct sighand_struct *sighand = current->sighand;
struct signal_struct *signal = current->signal;
int signr;
relock:
……
for (;;) {
struct k_sigaction *ka;
……
signr = tracehook_get_signal(current, regs, info, return_ka);
if (unlikely(signr < 0))
goto relock;
if (unlikely(signr != 0))
ka = return_ka;
else {
/*从挂起队列中获取信号*/
signr = dequeue_signal(current, ¤t->blocked,
info);
……
ka = &sighand->action[signr-1];/*找到对应的action*/
}
if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */
continue;
if (ka->sa.sa_handler != SIG_DFL) {
/*将找到的action设置为参数返回*/
*return_ka = *ka;
if (ka->sa.sa_flags & SA_ONESHOT)
ka->sa.sa_handler = SIG_DFL;
break; /* will return non-zero "signr" value */
}
……
}//end for
spin_unlock_irq(&sighand->siglock);
return signr;
}
int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)
{
int signr;
/*先从私有挂起队列中寻找*/
signr = __dequeue_signal(&tsk->pending, mask, info);
if (!signr) {
/*当私有挂起队列中找不到时,从共享队列中找*/
signr = __dequeue_signal(&tsk->signal->shared_pending,
mask, info);
……
}
……
return signr;
}
内核先从自己私有的挂起信号队列中查找,当找不到时,再从共享信号队列中查找,以参数的形式返回找到的action。
2)信号处理do_signal()->handle_signal()
staticint
handle_signal(unsignedlong sig, siginfo_t *info, struct k_sigaction *ka,
sigset_t *oldset, struct pt_regs *regs)
{
int ret;
/* 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 (!(ka->sa.sa_flags & SA_RESTART)) {
regs->ax = -EINTR;
break;
}
/* fallthrough */
case -ERESTARTNOINTR:
regs->ax = regs->orig_ax;
regs->ip -= 2;
break;
}
}
……
/*实际处理*/
ret = setup_rt_frame(sig, ka, info, oldset, regs);
if (ret)
return ret;
……
return 0;
}
上面的setup_rt_frame()在Intel32体系下会最终调用下面函数完成。
int ia32_setup_frame(int sig,struct k_sigaction *ka,
compat_sigset_t *set,struct pt_regs *regs)
{
struct sigframe_ia32 __user *frame;
void __user *restorer;
int err = 0;
void __user *fpstate = NULL;
/* copy_to_user optimizes that into a single 8 byte store */
/*设置ia32_sigreturn系统调用,用于返回*/
staticconst struct {
u16 poplmovl;
u32 val;
u16 int80;
} __attribute__((packed)) code = {
0xb858, /* popl %eax ; movl $...,%eax */
__NR_ia32_sigreturn,
0x80cd, /* int $0x80 */
};
/*返回为栈中的一部分数据*/
frame = get_sigframe(ka, regs,sizeof(*frame), &fpstate);
……
/*设置为用户空间的各种寄存器,完成设置后,由于ip设置为handler处,所以调到该处执行*/
/* Set up registers for signal handler */
regs->sp = (unsignedlong) frame;
regs->ip = (unsignedlong) ka->sa.sa_handler;
/* Make -mregparm=3 work */
regs->ax = sig;
regs->dx = 0;
regs->cx = 0;
loadsegment(ds, __USER32_DS);
loadsegment(es, __USER32_DS);
regs->cs = __USER32_CS;
regs->ss = __USER32_DS;
return 0;
}
终于看到内核怎么处理了吧,由于信号的handler为用户空间程序,系统需要返回到用户空间执行,所以我们看到,内核直接设置ip为handler,代码段cs为用户空间代码段,数据段ds、es以及栈ss等都设置为用户空间的,该函数完成后,程序将冲用户空间的handler处开始执行。上面程序设置ia32_sigreturn系统调用作为程序返回的调用。整体的运行原理图如下(该图参考《深入理解Linux内核框架》):
Linux内核信号机制用到很多地方,最常见的是进程间通信,原理较简单,并且异步调用。
更多推荐
所有评论(0)