这篇博客,想集中在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. = __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'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 *= 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. = pending->signal.sig;
  6. = mask->sig;

  7. /*
  8. * Handle the first word specially: it contains the
  9. * synchronous signals that need to be dequeued first.
  10. */
  11. = *&~ *m;
  12. if (x) {
  13. if (& SYNCHRONOUS_MASK)
  14. &= SYNCHRONOUS_MASK;
  15. sig = ffz(~x) + 1;
  16. return sig;
  17. }

  18. switch (_NSIG_WORDS) {
  19. default:
  20. for (= 1; i < _NSIG_WORDS; ++i) {
  21. = *++&~ *++m;
  22. if (!x)
  23. continue;
  24. sig = ffz(~x) + i*_NSIG_BPW + 1;
  25. break;
  26. }
  27. break;

  28. case 2:
  29. = 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'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);
。。。。

}



参考文献:

1 Linux2.6内核中的线程组初探

2 Linux kernel 3.8.0

Linux signal 那些事儿 (3)相关推荐

  1. Linux signal那些事儿【转】

    转自:http://blog.chinaunix.net/uid-24774106-id-4061386.html Linux编程,信号是一个让人爱恨交加又不得不提的一个领域.最近我集中学习了Linu ...

  2. linux 线程退出 signal,Linux signal 那些事儿 (3)

    这篇博客,想集中在signal 与线程的关系上,顺带介绍内核signal相关的结构.如何组织我其实并没想好,想到哪就写到哪里吧.主题一定会落在signal之内而不跑题. 提到signal与thread ...

  3. linux signal函数用法,linux信号机制之sigaction构造体浅析,signal 函数,信号捕捉.

    来自:http://hi.baidu.com/phenix_yw/blog/item/6eb4ca391d1479f23a87ce19.html 信号安装函数sigaction(int signum, ...

  4. linux signal 处理

    linux signal 处理 说明: 本文主要翻译自ULK 3rd chapter 11. 主要受 http://blog.csdn.net/yunsongice 影响,故发表在csdn. 另外,本 ...

  5. Linux signal()

    Linux signal() 转载请注明作者和原文出处,原文地址:https://blog.csdn.net/sym_07/article/details/87940232 1. 概念 信号(sign ...

  6. 关于Linux的那些事儿--系统状态检测命令

    关于Linux的那些事儿–系统状态检测命令 1.ifconfig命令:用于获取网卡配置与网络状态等信息.格式:ifconfig [网卡名] [参数]. ether:物理地址 flags=4163< ...

  7. Linux Signal信号表

    [版权申明]转载请附上出处链接 Linux Signal信号表 在终端运行kill -l命令, 可查看Linux支持的信号列表: $kill -l1) SIGHUP 2) SIGINT 3) SIGQ ...

  8. linux——signal信号

    linux--signal信号(SIGHUP.SIGINT.SIGQUIT.SIGILL.SIGTRAP.SIGABRT...........................)_夜风的博客-CSDN博 ...

  9. Linux Signal及Golang中的信号处理

    转载地址:https://colobu.com/2015/10/09/Linux-Signals/ 信号(Signal)是Linux, 类Unix和其它POSIX兼容的操作系统中用来进程间通讯的一种方 ...

  10. Linux signal 编程(转载)

    转载地址:http://blog.sina.com.cn/s/blog_4b226b92010119l5.html 当服务器close一个连接时,若client端接着发数据.根据TCP协议的规定,会收 ...

最新文章

  1. cookie、 sessionStorage 、localStorage之间的区别和使用
  2. 谷歌让AI芯片学会“下崽”,下一代TPU就让AI自己设计
  3. android中webview loadUrl(String url,Map header)方法和postUrl(String url,byte[] postData)方法同时使用问题;...
  4. 自动转换开关(ATS)在数据中心配电系统中的应用
  5. 【SICP练习】144 练习3.82
  6. 用PHP生成word文件
  7. 分支语句语法格式小结 java
  8. 云原生时代的 YAML 教程
  9. android系统文件的权限
  10. ubuntu 12.04 修改 grub 启动参数
  11. 基于PID算法的水箱温度控制系统
  12. 长方形内正方形Square
  13. word怎么删除参考文献的横线_2016版Word 中参考文献上面的横线是怎么去掉的啊,求助求助?...
  14. 例题 2-1 aabb 2-2 3n+1问题
  15. 数据结构:C#语言与面向对象技术(1)
  16. 正则函数--search/match/findall/sub/split
  17. 教你找到免费的Google Translate API(谷歌翻译接口)+C#版的Google翻译函数
  18. String的charAt方法
  19. 有道云笔记Windows版不显示图片
  20. [AHK]EMEDITOR 加上 AHK 高亮色彩配置

热门文章

  1. python发布代码图片_gitpython模块与代码发布项目流程图
  2. Mac 抓包工具wireshark使用
  3. 阿里云服务器 发送邮箱 STMP 25端口 465端口问题 Javamail 25被禁用
  4. IE8的模式修改优化Windows7
  5. Linux安装Nginx1.7.4、php5.5.15和配置
  6. 返回两个时间范围内的一个随机时间
  7. Centos 6 编译安装 Apache 2.4
  8. Android--读取通讯录并添加联系人
  9. Excel 取消身份证的科学计数法显示形式
  10. matlab用mkdir在指定的文件夹下创建新的文件夹,并把图像保存在该文件夹内