Linux signal 那些事儿 (3)【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4065797.html

这篇博客,想集中在signal 与线程的关系上,顺带介绍内核signal相关的结构。如何组织我其实并没想好,想到哪就写到哪里吧。主题一定会落在signal之内而不跑题。
    提到signal与thread的关系,就得先提POSIX标准。POSIX标准决定了Linux为何将signal如此实现:
   1 信号处理函数必须在多线程应用的所有线程之间共享,但是,每个线程要有自己的挂起信号掩码和阻塞信号掩码。
   2 POSIX 函数kill/sigqueue必须面向所有的多线程应用而不是某个特殊的线程。
   3 每个发给多线程应用的信号仅传送给1个线程,这个线程是由内核从不会阻塞该信号的线程中随意选出。
   4 如果发送一个致命信号到多线程,那么内核将杀死该应用的所有线程,而不仅仅是接收信号的那个线程。

    上面是POSIX标准,也就是提出来的要求,Linux要遵循POSIX标准,那Linux是怎么做到的呢?
    到了此处,我们需要理清一些基本的概念:

  1. struct task_struct {
  2.     pid_t pid;
  3.     pid_t tgid
  4.       .....
  5.     struct task_struct *group_leader;    /* threadgroup leader */
  6.       ......
  7.     struct list_head thread_group;
  8.         ....
  9. }

    从字面意思上看 pid,是process id,其则不然,pid是thread id。从字面意思上看,tgid是thread group id,其则是真正的pid。
   
有点绕是不是?对于一个多线程的程序,无论是哪个线程执行getpid,结果都是一样的,最终返回的同一个值
tgid。如果我们实现了gettid(很不幸的是glibc没有这个函数,所以我们要用syscall),我们就会发现,各个线程返回的值不同,此时,返回的值是内核task_struct中的pid。对于多线程应用/proc/pid/task可以看到的,就是线程的thread
id,也就是task_struct中的pid。
   
    我在我的博文Linux线程之线程 线程组 进程 轻量级进程(LWP)提到了这个问题。我不想多浪费笔墨赘述。
    group
leader字段,指向线程组的第一个线程。对于我们自己的程序而言,main函数所在的线程,也就是线程组的第一个线程,所以group
leader就会他自己。一旦用pthread_create创建了线程,那么main所在的线程,还有创建出来的线程,隶属于同一个线程组,线程的group
leader还是main函数所在的线程id。
    thread_group,同一线程组的所有线程的队列。对于group_leader,这是一个队列头,对于同一线程组的其他线程,通过这个字段挂入队列。可以根据这个队列,遍历线程组的所有线程。
     是时候看看内核代码了,下面的代码属于do_fork函数及copy_process函数的一些代码。       

  1.     p->pid = pid_nr(pid);
  2.     p->tgid = p->pid;
  3.     if (clone_flags & CLONE_THREAD)//创建线程,tgid等于当前线程的
  4.         p->tgid = current->tgid;
  5.     p->group_leader = p;
  6.     INIT_LIST_HEAD(&p->thread_group);
  7.     if (clone_flags & CLONE_THREAD) { //线程处理部分,group_leader都是第一个线程。同时挂入队列
  8.         current->signal->nr_threads++;
  9.         atomic_inc(&current->signal->live);
  10.         atomic_inc(&current->signal->sigcnt);
  11.         p->group_leader = current->group_leader;
  12.         list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
  13.     }

   
代码表明,第一个线程呢,pid和tgid相同,都是分配的那个pid,group_leader也是自己。后面第二个线程,pid是自己的,但是tgid
等于创建者的tgid,group_leader指向第一个线程的task_struct.
后面创建的所有的线程,都会挂入队列,方便遍线程组的所有线程。
    有了线程组的概念,我们就可以进一步解释signal相关的内容了。 

  1.     /* signal handlers */
  2.     struct signal_struct *signal;
  3.     struct sighand_struct *sighand;
  4.     sigset_t blocked, real_blocked;
  5.     sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */
  6.     struct sigpending pending;

    
   
线程组里面的所有成员共享一个signal_struct类型结构,同一线程组的多线程的task_struct
中的signal指针都是指向同一个signal_struct。sighand成员变量也是如此,统一个线程组的多个线程指向同一个signalhand_struct结构。  

  1. static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)
  2. {
  3.     struct signal_struct *sig;
  4.     if (clone_flags & CLONE_THREAD) //线程,直接返回,表明同一线程组共享
  5.         return 0;
  6.     sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);
  7.     tsk->signal = sig;
  8.     if (!sig)
  9.         return -ENOMEM;
  10.     sig->nr_threads = 1;
  11.     atomic_set(&sig->live, 1);
  12.     atomic_set(&sig->sigcnt, 1);
  13.     init_waitqueue_head(&sig->wait_chldexit);
  14.     sig->curr_target = tsk;
  15.         。。。。
  16. }
  17. static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)
  18. {
  19.     struct sighand_struct *sig;
  20.     if (clone_flags & CLONE_SIGHAND) {
  21.         atomic_inc(&current->sighand->count); //如果发现是线程,直接讲引用计数++,无需分配sighand_struct结构
  22.         return 0;
  23.     }
  24.     sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
  25.     rcu_assign_pointer(tsk->sighand, sig);
  26.     if (!sig)
  27.         return -ENOMEM;
  28.     atomic_set(&sig->count, 1);
  29.     memcpy(sig->action, current->sighand->action, sizeof(sig->action));
  30.     return 0;
  31. }

    这就基本实现了多线程应用中,信号处理程序是共享的,因为他们共用一个signalhand_struct。
    上一篇博文提到,signal->shared_pending
和pending两个挂起信号相关的数据结构,此处我们可以具体讲解了。signal是线程组共享的结构,自然下属的shared_pending也是线程组共享的。就像POSIX提到的,kill/sigqueue发送信号,发送的对象并不是线程组某个特定的线程,而是整个线程组。自然,如果kernel会将信号记录在全线程组共享的signal->shared_pending,表示,线程组收到信号X一枚。
    有筒子说了,我就要给某个特定的线程发信号,有没有办法,内核怎么办?这是个好问题。   

  1.   int tkill(int tid, int sig);
  2.   int tgkill(int tgid, int tid, int sig)

    这两个API是给线程组特定线程发信号的,毫不意外,内核会将信号记录在线程自己的结构pending中。

  1. pending = group ? &t->signal->shared_pending : &t->pending;

    对于kill/sigqueue,__send_signal传进来的是group是true,对于tkill/tgkill传进来的是false。会将信号写入相应的挂起信号位图。

  1. static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
  2.             int group, int from_ancestor_ns)
  3. {
  4.     struct sigpending *pending;
  5.     struct sigqueue *q;
  6.     int override_rlimit;
  7.     int ret = 0, result;
  8.     assert_spin_locked(&t->sighand->siglock);
  9.     result = TRACE_SIGNAL_IGNORED;
  10.     if (!prepare_signal(sig, t,
  11.             from_ancestor_ns || (info == SEND_SIG_FORCED)))
  12.         goto ret;
  13.     pending = group ? &t->signal->shared_pending : &t->pending;   // tkill用的自己的pending,
  14.                                                                   // kill/sigqueue用的线程组共享的signal->shared_pending
  15.     /*
  16.      * Short-circuit ignored signals and support queuing
  17.      * exactly one non-rt signal, so that we can get more
  18.      * detailed information about the cause of the signal.
  19.      */
  20.     result = TRACE_SIGNAL_ALREADY_PENDING;
  21.     if (legacy_queue(pending, sig))
  22.         goto ret;
  23.     result = TRACE_SIGNAL_DELIVERED;
  24.     /*
  25.      * fast-pathed signals for kernel-internal things like SIGSTOP
  26.      * or SIGKILL.
  27.      */
  28.     if (info == SEND_SIG_FORCED)
  29.         goto out_set;
  30.     /*
  31.      * Real-time signals must be queued if sent by sigqueue, or
  32.      * some other real-time mechanism. It is implementation
  33.      * defined whether kill() does so. We attempt to do so, on
  34.      * the principle of least surprise, but since kill is not
  35.      * allowed to fail with EAGAIN when low on memory we just
  36.      * make sure at least one signal gets delivered and don't
  37.      * pass on the info struct.
  38.      */
  39.     if (sig < SIGRTMIN)
  40.         override_rlimit = (is_si_special(info) || info->si_code >= 0);
  41.     else
  42.         override_rlimit = 0;
  43.     q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
  44.         override_rlimit);
  45.     if (q) {
  46.         list_add_tail(&q->list, &pending->list);
  47.         switch ((unsigned long) info) {
  48.         case (unsigned long) SEND_SIG_NOINFO:
  49.             q->info.si_signo = sig;
  50.             q->info.si_errno = 0;
  51.             q->info.si_code = SI_USER;
  52.             q->info.si_pid = task_tgid_nr_ns(current,
  53.                             task_active_pid_ns(t));
  54.             q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
  55.             break;
  56.         case (unsigned long) SEND_SIG_PRIV:
  57.             q->info.si_signo = sig;
  58.             q->info.si_errno = 0;
  59.             q->info.si_code = SI_KERNEL;
  60.             q->info.si_pid = 0;
  61.             q->info.si_uid = 0;
  62.             break;
  63.         default:
  64.             copy_siginfo(&q->info, info);
  65.             if (from_ancestor_ns)
  66.                 q->info.si_pid = 0;
  67.             break;
  68.         }
  69.         userns_fixup_signal_uid(&q->info, t);
  70.     } else if (!is_si_special(info)) {
  71.         if (sig >= SIGRTMIN && info->si_code != SI_USER) {
  72.             /*
  73.              * Queue overflow, abort. We may abort if the
  74.              * signal was rt and sent by user using something
  75.              * other than kill().
  76.              */
  77.             result = TRACE_SIGNAL_OVERFLOW_FAIL;
  78.             ret = -EAGAIN;
  79.             goto ret;
  80.         } else {
  81.             /*
  82.              * This is a silent loss of information. We still
  83.              * send the signal, but the *info bits are lost.
  84.              */
  85.             result = TRACE_SIGNAL_LOSE_INFO;
  86.         }
  87.     }
  88. out_set:
  89.     signalfd_notify(t, sig);
  90.     sigaddset(&pending->signal, sig);//修改位图,表明该信号存在挂起信号。
  91.     complete_signal(sig, t, group);
  92. ret:
  93.     trace_signal_generate(sig, info, t, group, result);
  94.     return ret;
  95. }

    线程存在一个很让人迷惑的问题,如何让线程组的所有线程一起退出。我们都知道,多线程的程序有一个线程访问了非法地址,引发段错误,会造成所有线程一起退出。这也是多线程程序脆弱的地方。但是如何做到的呢?
    do_signal--->get_signal_to_deliver中,会选择信号,如果发现需要退出,会执行do_group_exit。这个名字顾名思义了,线程组退出。   

  1. void
  2. do_group_exit(int exit_code)
  3. {
  4.     struct signal_struct *sig = current->signal;
  5.     BUG_ON(exit_code & 0x80); /* core dumps don't get here */
  6.     if (signal_group_exit(sig))
  7.         exit_code = sig->group_exit_code;
  8.     else if (!thread_group_empty(current)) {
  9.         struct sighand_struct *const sighand = current->sighand;
  10.         spin_lock_irq(&sighand->siglock);
  11.         if (signal_group_exit(sig))
  12.             /* Another thread got here before we took the lock. */
  13.             exit_code = sig->group_exit_code;
  14.         else {
  15.             sig->group_exit_code = exit_code;
  16.             sig->flags = SIGNAL_GROUP_EXIT;
  17.             zap_other_threads(current);
  18.         }
  19.         spin_unlock_irq(&sighand->siglock);
  20.     }
  21.     do_exit(exit_code);
  22.     /* NOTREACHED */
  23. }

    如果是多线程,会走入到else中,主要的操作都在zap_other_threads函数中:

  1. /*
  2.  * Nuke all other threads in the group.
  3.  */
  4. int zap_other_threads(struct task_struct *p)
  5. {
  6.     struct task_struct *t = p;
  7.     int count = 0;
  8.     p->signal->group_stop_count = 0;
  9.     while_each_thread(p, t) {
  10.         task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
  11.         count++;
  12.         /* Don't bother with already dead threads */
  13.         if (t->exit_state)
  14.             continue;
  15.         sigaddset(&t->pending.signal, SIGKILL);
  16.         signal_wake_up(t, 1);
  17.     }
  18.     return count;
  19. }

   
不多说了,就是给每一个线程都挂上一个SIGKILL的信号,当CPU选择线程执行时候的时候,自然会处理这个信号,而对SIGKILL的处理,会再次调用do_group_exit。这一次会调用do_exit退出。当线程组所有进程都执行过之后,整个线程组就消亡了。
    
   
讲完这些,需要讲block了。我第一篇就讲到,我们有时候需要阻塞某些信号。POSIX说了多线程中每个线程要有自己的阻塞信号。不必说,task_struct中的blocked就是阻塞信号位图。我们的glibc的sigprocmask函数,就是设置进程的blocked。
    那些block的信号为何不能传递,内核是怎么做到的?
        

  1. int next_signal(struct sigpending *pending, sigset_t *mask)
  2. {
  3.     unsigned long i, *s, *m, x;
  4.     int sig = 0;
  5.     s = pending->signal.sig;
  6.     m = mask->sig;
  7.     /*
  8.      * Handle the first word specially: it contains the
  9.      * synchronous signals that need to be dequeued first.
  10.      */
  11.     x = *s &~ *m;
  12.     if (x) {
  13.         if (x & SYNCHRONOUS_MASK)
  14.             x &= SYNCHRONOUS_MASK;
  15.         sig = ffz(~x) + 1;
  16.         return sig;
  17.     }
  18.     switch (_NSIG_WORDS) {
  19.     default:
  20.         for (i = 1; i < _NSIG_WORDS; ++i) {
  21.             x = *++s &~ *++m;
  22.             if (!x)
  23.                 continue;
  24.             sig = ffz(~x) + i*_NSIG_BPW + 1;
  25.             break;
  26.         }
  27.         break;
  28.     case 2:
  29.         x = s[1] &~ m[1];
  30.         if (!x)
  31.             break;
  32.         sig = ffz(~x) + _NSIG_BPW + 1;
  33.         break;
  34.     case 1:
  35.         /* Nothing to do */
  36.         break;
  37.     }
  38.     return sig;
  39. }

   
m就是task_struct中的blocked,阻塞的信号就不会不会被取出传递了。很有意思的一点是信号传递的顺序。在Linux
programming
interface一书中提到小signo优先的策略,比如SIGINT(2)和SIGQUIT(3)同时存在,SIGINT(2) 先deliver,然后才是SIGQUIT(3).我们看代码,很有意思的是有同步信号:

  1. #define SYNCHRONOUS_MASK \
  2.     (sigmask(SIGSEGV) | sigmask(SIGBUS) | sigmask(SIGILL) | \
  3.      sigmask(SIGTRAP) | sigmask(SIGFPE) | sigmask(SIGSYS))

    有SIGSEGV SIGBUS SIGILL SIGTRAP SIGFPE SIGSYS,那么这几个信号优先。没有这几个信号,按照小信号优先。当然了,这些是Linux kernel的实现,毕竟不是POSIX标准,不可依赖这种顺序。
   另外,dequeue很有意思,先去task_struct中的pending中取,取不到再去整个线程组共享的shered_pending位图去取。    

  1. int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)
  2. {
  3.     int signr;
  4.     /* We only dequeue private signals from ourselves, we don't let
  5.      * signalfd steal them
  6.      */
  7.     signr = __dequeue_signal(&tsk->pending, mask, info);
  8.     if (!signr) {
  9.         signr = __dequeue_signal(&tsk->signal->shared_pending,
  10.                      mask, info);

       。。。。
}

参考文献:
Linux2.6内核中的线程组初探2 Linux kernel 3.8.0

时间: 2024-10-24 18:16:59

Linux signal 那些事儿 (3)【转】的相关文章

Linux signal那些事儿【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4061386.html Linux编程,信号是一个让人爱恨交加又不得不提的一个领域.最近我集中学习了Linux的signal相关的内容,分享出来,也为防止自己忘记.     信号的本质是异步.异步一这个词,听着高端大气上档次,又让人云山雾绕,其则不然.其实我们想想,我们这个世界是异步的,每个人干事儿,并不总是A->B->C->D这种.比如我在网上买了东西,我其实并不知道快递几时能到.我可能在公司里

Linux signal 那些事儿(2)【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4064447.html 上一篇博文,基本算是给glibc的signal函数翻了个身.现在glibc的signal基本修正了传统的UNIX的一些弊端,我们说signal并没有我们想象的那么不堪.但是signal也有不尽人意的地方.比如信号处理期间,我们期望屏蔽某些信号,而不仅仅是屏蔽自身,这时候signal就不行了.信号既然是进程间通信IPC的一种机制,我们期望获取更多的信息,而不仅仅是signo,这时候s

Linux signal 那些事儿(4)信号的deliver顺序【转】

转自:http://blog.chinaunix.net/uid-24774106-id-4084864.html 上一篇博文提到了,如果同时有多个不同的信号处于挂起状态,kernel如何选择deliver那个信号.        next_signal 负责从挂起信号中选择deliver的signo:当然,有线程显存私有的penging,有线程组共有的pending,对于线程而言,先从自己私有的pending中选,处理完毕私有的才会去处理线程组共有的pending,这个逻辑的代码在: int

linux那点事儿(八)----shell 编程

一个简单的shell程序                                                                  下面直接来看一个shell程序. ----------------- #!/bin/sh # This is to show what a example looks like. echo "Our first example" echo # This inserts an empty line in output echo &qu

linux signal 用法和注意事项

http://blog.chinaunix.net/uid-9354-id-2425031.html 所以希望能用相同方式处理信号的多次出现,最好用sigaction.信号只出现并处理一次,可以用signal.   signal函数每次设置具体的信号处理函数(非SIG_IGN)只能生效一次,每次在进程响应处理信号时,随即将信号处理函数恢复为默认处理方式.所以如果想多次相同方式处理某个信号,通常的做法是,在响应函数开始,再次调用signal设置,如下图: int sig_int(); //My s

linux那点事儿(中)

      今天是辞职后的第一天,本来想写写工作总结,还有许多东西需要整理和学习.这是我继毕业之后的第二次焦虑和迷茫.希望我能早点找到工作吧!        步入正题,其实,linux要学的东西非常多.不是我分个上.中.下三篇博文就能写完的.不过,既然弄了个"上"出来,题目我就不改了.这篇就定为 "中"吧!如果写了"下",还没写完,也许会弄个"下续" ,"下续二"出来.^_^ :) ,也许,写了这个就没下了

初学者必看:Linux压缩那些事儿

Linux的压缩命令的源文件只能有一个,这意味在压缩之前不得不先将要压缩的所有文件打包成一个包,然后再压缩包,这样来完成对多个文件的压缩.所以在了解解压缩之前就必须先了解打包命令.Linux的打包一般都是通过tar命令来完成的,通过man tar,可以得到一些信息,tar来完成不同的动作是通过指定不同的参数来完成的,通常使用的也就是--x/c,这两个参数,它们的字面含义是c--create,x--extract,创建和分包提取,另外还有一些不常用的参数,比如a,将tar添加到另外一个tar包的末

linux那点事儿(六)----进程管理详解(推荐)

目录:(内容较多,加个目录) |-进程管理  进程常用命令 |- w查看当前系统信息 |- ps进程查看命令 |- kill终止进程 |- 一个存放内存中的特殊目录/proc |- 进程的优先级 |- 进程的挂起与恢复 |- 通过top命令查看进程 计划任务 |- 计划任务的重要性 |- 一次性计划at和batch |- 周期性计划crontab 进程管理的概念                                                                    

linux那点事儿(五)----用户管理常用命令

  上一节,将的那内容有些复杂,如果是新手,又不想一下子对用户管理了解的那么深入,欢迎阅读本节内容.      ps:其实,对前面两节内容做了大的调整,在方便自己查阅的同时,也希望方便别人的阅读.呵呵!   用户组权限实例