Linux信号机制概述



Linux信号机制概述
 
还是先看看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;
         }  www.2cto.com  
         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");
         }  www.2cto.com  
         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。  www.2cto.com  
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;  www.2cto.com  
                   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) {
         ……  www.2cto.com  
                   /*实际的信号处理*/
                   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 {  www.2cto.com  
                            /*从挂起队列中获取信号*/
                            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)
{  www.2cto.com  
         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;  www.2cto.com  
         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);
   www.2cto.com  
         regs->cs = __USER32_CS;
         regs->ss = __USER32_DS;
         return 0;
}
        终于看到内核怎么处理了吧,由于信号的handler为用户空间程序,系统需要返回到用户空间执行,所以我们看到,内核直接设置ip为handler,代码段cs为用户空间代码段,数据段ds、es以及栈ss等都设置为用户空间的,该函数完成后,程序将冲用户空间的handler处开始执行。上面程序设置ia32_sigreturn系统调用作为程序返回的调用。整体的运行原理图如下(该图参考《深入理解Linux内核框架》):
 

 
Linux内核信号机制用到很多地方,最常见的是进程间通信,原理较简单,并且异步调用。
 
 
作者 bullbat

相关内容

    暂无相关文章