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(&current->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(&current->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, &current->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内核信号机制用到很多地方,最常见的是进程间通信,原理较简单,并且异步调用。

 

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐