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

提到signal与thread的关系,就得先提POSIX标准。POSIX标准决定了Linux为何将signal如此实现:

1 信号处理函数必须在多线程应用的所有线程之间共享,但是,每个线程要有自己的挂起信号掩码和阻塞信号掩码。

2 POSIX 函数kill/sigqueue必须面向所有的多线程应用而不是某个特殊的线程。

3 每个发给多线程应用的信号仅传送给1个线程,这个线程是由内核从不会阻塞该信号的线程中随意选出。

4 如果发送一个致命信号到多线程,那么内核将杀死该应用的所有线程,而不仅仅是接收信号的那个线程。

上面是POSIX标准,也就是提出来的要求,Linux要遵循POSIX标准,那Linux是怎么做到的呢?

到了此处,我们需要理清一些基本的概念:

struct task_struct {

pid_t pid;

pid_t tgid

.....

struct task_struct *group_leader;    /* threadgroup leader */

......

struct list_head thread_group;

....

}

从字面意思上看 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函数的一些代码。

p->pid = pid_nr(pid);

p->tgid = p->pid;

if (clone_flags & CLONE_THREAD)//创建线程,tgid等于当前线程的

p->tgid = current->tgid;

p->group_leader = p;

INIT_LIST_HEAD(&p->thread_group);

if (clone_flags & CLONE_THREAD) { //线程处理部分,group_leader都是第一个线程。同时挂入队列

current->signal->nr_threads++;

atomic_inc(&current->signal->live);

atomic_inc(&current->signal->sigcnt);

p->group_leader=current->group_leader;

list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);

}

代码表明,第一个线程呢,pid和tgid相同,都是分配的那个pid,group_leader也是自己。后面第二个线程,pid是自己的,但是tgid 等于创建者的tgid,group_leader指向第一个线程的task_struct. 后面创建的所有的线程,都会挂入队列,方便遍线程组的所有线程。

有了线程组的概念,我们就可以进一步解释signal相关的内容了。

/* signal handlers */

struct signal_struct *signal;

struct sighand_struct *sighand;

sigset_t blocked, real_blocked;

sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */

struct sigpending pending;

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

static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)

{

struct signal_struct *sig;

if (clone_flags & CLONE_THREAD) //线程,直接返回,表明同一线程组共享

return 0;

sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);

tsk->signal = sig;

if (!sig)

return -ENOMEM;

sig->nr_threads = 1;

atomic_set(&sig->live, 1);

atomic_set(&sig->sigcnt, 1);

init_waitqueue_head(&sig->wait_chldexit);

sig->curr_target = tsk;

。。。。

}

static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)

{

struct sighand_struct *sig;

if (clone_flags & CLONE_SIGHAND) {

atomic_inc(&current->sighand->count); //如果发现是线程,直接讲引用计数++,无需分配sighand_struct结构

return 0;

}

sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);

rcu_assign_pointer(tsk->sighand, sig);

if (!sig)

return -ENOMEM;

atomic_set(&sig->count, 1);

memcpy(sig->action, current->sighand->action, sizeof(sig->action));

return 0;

}

这就基本实现了多线程应用中,信号处理程序是共享的,因为他们共用一个signalhand_struct。

上一篇博文提到,signal->shared_pending 和pending两个挂起信号相关的数据结构,此处我们可以具体讲解了。signal是线程组共享的结构,自然下属的shared_pending也是线程组共享的。就像POSIX提到的,kill/sigqueue发送信号,发送的对象并不是线程组某个特定的线程,而是整个线程组。自然,如果kernel会将信号记录在全线程组共享的signal->shared_pending,表示,线程组收到信号X一枚。

有筒子说了,我就要给某个特定的线程发信号,有没有办法,内核怎么办?这是个好问题。

int tkill(int tid, int sig);

int tgkill(int tgid, int tid, int sig)

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

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

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

static int __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;

int ret = 0, result;

assert_spin_locked(&t->sighand->siglock);

result = TRACE_SIGNAL_IGNORED;

if (!prepare_signal(sig, t,

from_ancestor_ns || (info == SEND_SIG_FORCED)))

goto ret;

pending=group? &t->signal->shared_pending: &t->pending; // tkill用的自己的pending,

// kill/sigqueue用的线程组共享的signal->shared_pending

/*

* Short-circuit ignored signals and support queuing

* exactly one non-rt signal, so that we can get more

* detailed information about the cause of the signal.

*/

result = TRACE_SIGNAL_ALREADY_PENDING;

if (legacy_queue(pending, sig))

goto ret;

result = TRACE_SIGNAL_DELIVERED;

/*

* fast-pathed signals for kernel-internal things like SIGSTOP

* or SIGKILL.

*/

if (info == SEND_SIG_FORCED)

goto out_set;

/*

* Real-time signals must be queued if sent by sigqueue, or

* some other real-time mechanism. It is implementation

* defined whether kill() does so. We attempt to do so, on

* the principle of least surprise, but since kill is not

* allowed to fail with EAGAIN when low on memory we just

* make sure at least one signal gets delivered and don't

* pass on the info struct.

*/

if (sig < SIGRTMIN)

override_rlimit = (is_si_special(info) || info->si_code >= 0);

else

override_rlimit = 0;

q = __sigqueue_alloc(sig, 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 = from_kuid_munged(current_user_ns(), 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;

}

userns_fixup_signal_uid(&q->info, t);

} else if (!is_si_special(info)) {

if (sig >= SIGRTMIN && info->si_code != SI_USER) {

/*

* Queue overflow, abort. We may abort if the

* signal was rt and sent by user using something

* other than kill().

*/

result = TRACE_SIGNAL_OVERFLOW_FAIL;

ret = -EAGAIN;

goto ret;

} else {

/*

* This is a silent loss of information. We still

* send the signal, but the *info bits are lost.

*/

result = TRACE_SIGNAL_LOSE_INFO;

}

}

out_set:

signalfd_notify(t, sig);

sigaddset(&pending->signal,sig);//修改位图,表明该信号存在挂起信号。

complete_signal(sig, t, group);

ret:

trace_signal_generate(sig, info, t, group, result);

return ret;

}

线程存在一个很让人迷惑的问题,如何让线程组的所有线程一起退出。我们都知道,多线程的程序有一个线程访问了非法地址,引发段错误,会造成所有线程一起退出。这也是多线程程序脆弱的地方。但是如何做到的呢?

do_signal--->get_signal_to_deliver中,会选择信号,如果发现需要退出,会执行do_group_exit。这个名字顾名思义了,线程组退出。

void

do_group_exit(int exit_code)

{

struct signal_struct *sig = current->signal;

BUG_ON(exit_code & 0x80); /* core dumps don't get here */

if (signal_group_exit(sig))

exit_code = sig->group_exit_code;

else if (!thread_group_empty(current)) {

struct sighand_struct *const sighand = current->sighand;

spin_lock_irq(&sighand->siglock);

if (signal_group_exit(sig))

/* Another thread got here before we took the lock. */

exit_code = sig->group_exit_code;

else {

sig->group_exit_code = exit_code;

sig->flags = SIGNAL_GROUP_EXIT;

zap_other_threads(current);

}

spin_unlock_irq(&sighand->siglock);

}

do_exit(exit_code);

/* NOTREACHED */

}

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

/*

* Nuke all other threads in the group.

*/

int zap_other_threads(struct task_struct *p)

{

struct task_struct *t = p;

int count = 0;

p->signal->group_stop_count = 0;

while_each_thread(p, t) {

task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);

count++;

/* Don't bother with already dead threads */

if (t->exit_state)

continue;

sigaddset(&t->pending.signal, SIGKILL);

signal_wake_up(t, 1);

}

return count;

}

不多说了,就是给每一个线程都挂上一个SIGKILL的信号,当CPU选择线程执行时候的时候,自然会处理这个信号,而对SIGKILL的处理,会再次调用do_group_exit。这一次会调用do_exit退出。当线程组所有进程都执行过之后,整个线程组就消亡了。

讲完这些,需要讲block了。我第一篇就讲到,我们有时候需要阻塞某些信号。POSIX说了多线程中每个线程要有自己的阻塞信号。不必说,task_struct中的blocked就是阻塞信号位图。我们的glibc的sigprocmask函数,就是设置进程的blocked。

那些block的信号为何不能传递,内核是怎么做到的?

int next_signal(struct sigpending *pending, sigset_t *mask)

{

unsigned long i, *s, *m, x;

int sig = 0;

s = pending->signal.sig;

m = mask->sig;

/*

* Handle the first word specially: it contains the

* synchronous signals that need to be dequeued first.

*/

x = *s &~ *m;

if (x) {

if (x & SYNCHRONOUS_MASK)

x &= SYNCHRONOUS_MASK;

sig = ffz(~x) + 1;

return sig;

}

switch (_NSIG_WORDS) {

default:

for (i = 1; i < _NSIG_WORDS; ++i) {

x = *++s &~ *++m;

if (!x)

continue;

sig = ffz(~x) + i*_NSIG_BPW + 1;

break;

}

break;

case 2:

x = s[1] &~ m[1];

if (!x)

break;

sig = ffz(~x) + _NSIG_BPW + 1;

break;

case 1:

/* Nothing to do */

break;

}

return sig;

}

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

#define SYNCHRONOUS_MASK \

(sigmask(SIGSEGV) | sigmask(SIGBUS) | sigmask(SIGILL) | \

sigmask(SIGTRAP) | sigmask(SIGFPE) | sigmask(SIGSYS))

有SIGSEGV SIGBUS SIGILL SIGTRAP SIGFPE SIGSYS,那么这几个信号优先。没有这几个信号,按照小信号优先。当然了,这些是Linux kernel的实现,毕竟不是POSIX标准,不可依赖这种顺序。

另外,dequeue很有意思,先去task_struct中的pending中取,取不到再去整个线程组共享的shered_pending位图去取。

int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)

{

int signr;

/* We only dequeue private signals from ourselves, we don't let

* signalfd steal them

*/

signr = __dequeue_signal(&tsk->pending, mask, info);

if (!signr) {

signr = __dequeue_signal(&tsk->signal->shared_pending,

mask, info);

。。。。

}

参考文献:

1

linux 线程退出 signal,Linux signal 那些事儿 (3)相关推荐

  1. Linux线程退出、资源回收、资源清理的方法

    首先说明线程中要回收哪些资源,理解清楚了这点之后在思考资源回收的问题. 1.子线程创建时从父线程copy出来的栈内存; 线程退出有多种方式,如return,pthread_exit,pthread_c ...

  2. linux 线程退出资源回收,有关linux线程资源回收的有关问题

    有关linux线程资源回收的问题 使用linux c编程的,开启一个线程,这个线程中申请了一些资源.如果需要这个线程马上取消并且回收申请的资源怎么办啊? 前面使用了pthread_cancel()这个 ...

  3. linux线程调用完类就退出,linux下 c中怎么让才能安全关闭线程 和 linux线程退出时执行的程序(线程清理处理程序)简单例子...

    多线程退出有三种方式: (1)执行完成后隐式退出: (2)由线程本身显示调用pthread_exit 函数退出: pthread_exit (void * retval) ; (3)被其他线程用pth ...

  4. 线程退出【Linux学习】pthread_create主线程与创建的新线程之间退出关系

    本篇文章个人在青岛吃饭的时候突然想到的...最近就有想写几篇关于线程退出的文章,所以回家到之后就奋笔疾书的写出来发布了 我们在一个线程中经常会创立另外的新线程,如果主线程退出,会不会影响它所创立的新线 ...

  5. linux线程退出正确姿势demo

    当一个非分离的线程终止后,该线程的内存资源(线程描述符和栈)并不会被释放,直到有线程对它使 用了pthread_join时才被释放. 因此,必须对每个创建为非分离的线程调用一次pthread_join ...

  6. arm linux线程退出,【ARMLinux】linux系统下多线程编程

    /**************************************************************************************** * 文件名: pro ...

  7. linux 线程带参数,Linux中多线程编程并传递多个参数的简单例子

    今天上午实验了Linux下的多线程编程,并将多个参数传递给线程要执行的函数. 以下是实验程序的源代码: /*********************** pthread.c ************* ...

  8. linux线程 ppt,4 linux线程与进程.ppt

    4 linux线程与进程 第4章 进程与线程 4.1进程 2.进程 什么是程序? 进程的定义:可并发执行的程序在一个数据集合上的运行过程. 进程的特性 动态性:进程具有一定的生命周期,它由创建而产生, ...

  9. Linux线程的同步,linux线程同步

    我是linux和linux线程的新手.我花了一些时间谷歌搜索试图理解可用于线程同步的所有函数之间的差异.我还有一些问题. 我找到了所有这些不同类型的同步,每个同步都有许多锁定,解锁,测试锁等功能. & ...

最新文章

  1. 用ILSpy查看Session.SessionID的生成算法
  2. 腾讯云视频流量服务器,腾讯云服务器有流量限制吗
  3. django-Modelform
  4. 菜单消失_减肥的你,哪些食物应该从你的菜单消失?
  5. 从头搭建 IntelliJ IDEA 环境,从放弃到爱不释手!
  6. 【HDU - 4348】To the moon(主席树,区间更新)
  7. LeetCode 914. 卡牌分组(最大公约数)
  8. OpenShift 4 之Istio-Tutorial (3) 监控微服务运行
  9. 详解AI加速器:为什么说现在是AI加速器的黄金时代?
  10. (一)深度学习入门之单个神经元
  11. apicloud aui 做底部导航
  12. 投影坐标系的shp数据,如何获取到它地理坐标系下的经纬度坐标
  13. QT Q_OBJECT使用注意事项
  14. 考驾照 一把过 74天拿证 这速度还可以?
  15. 孤独的人在孤独的地方...
  16. 北京市社保定点医疗机构查询【2021年1月】
  17. 在线图片格式转换,svg转png,jpg转ico
  18. Spring Boot 整合jackson
  19. 计算机教师招聘板书设计,教师招聘或资格证面试试讲—试讲提分技巧之板书设计...
  20. 如何对连续型数据进行离散化处理,并进行OneHot编码?

热门文章

  1. SAP Spartacus CMS 页面加载逻辑和性能的优化
  2. css align-items的测试
  3. 使用setup函数替代beforeEach函数进行Angular单元测试
  4. SAP CDS view自学教程之九:cube view和query view的实现原理
  5. 如何查询当前SAP用户所属的组织单元(organization unit)
  6. 如何关闭Windows10任务栏里的应用图标
  7. Send mail via http client - CL_SAM_SESSION_QUEUE_SENDER
  8. Visual Studio Code里关于ESLint的错误消息
  9. setProperty will trigger ui re-render 南京同事提的问题
  10. 如何修改新浪微博对其他应用的授权