0


【linux 多线程并发】多线程模型下的信号通信处理,与多进程处理的比较,属于相同进程的线程信号分发机制

07线程信号处理

专栏内容

  • 参天引擎内核架构 本专栏一起来聊聊参天引擎内核架构,以及如何实现多机的数据库节点的多读多写,与传统主备,MPP的区别,技术难点的分析,数据元数据同步,多主节点的情况下对故障容灾的支持。
  • 手写数据库toadb 本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。 本专栏会定期更新,对应的代码也会定期更新,每个阶段的代码会打上tag,方便阶段学习。

开源贡献

  • toadb开源库

个人主页:我的主页
管理社区:开源数据库
座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物.

文章目录

前言

现代的CPU都是多core处理器,而且在intel处理器中每个core又可以多个processor,形成了多任务并行处理的硬件架构,在服务器端的处理器上架构又有一些不同,传统的采用SMP,也就是对称的多任务处理架构,每个任务都可以对等的访问所有内存,外设等,而如今在ARM系列CPU上,多采用NUMA架构,它将CPU核分了几个组,给每个组的CPU core分配了对应的内存和外设,CPU访问对应的内存和外设时速度最优,跨组访问时性能会降底一些。

随着硬件技术的持续发展,它们对一般应用的性能优化能力越来越强,同时对于服务器软件的开发,提出更高要求,要想达到极高的并发和性能,就需要充分利用当前硬件架构的特点,对它们进行压榨。那么,我们的应用至少也是要采用多任务架构,不管是多线程还是多进程的多任务架构,才可以充分利用硬件的资源,达到高效的处理能力。

当然多任务框架的采用,不仅仅是多线程的执行,需要对多任务下带来的问题进行处理,如任务执行返回值获取,任务间数据的传递,任务执行次序的协调;当然也不是任务越多处理越快,要避免线程过多导致操作系统夯住,也要防止任务空转过快导致CPU使用率飙高。

本专栏主要介绍使用多线程与多进程模型,如何搭建多任务的应用框架,同时对多任务下的数据通信,数据同步,任务控制,以及CPU core与任务绑定等相关知识的分享,让大家在实际开发中轻松构建自已的多任务程序。

概述

信号是linux平台下一个重要的并发通信方式,对于简单的指令可以非常方便的通知到另外的并发任务,同时利用了软中断的机制,让信号的接收变得很高效。

我们知道信号一般针对的都是进程,NTPL线程库和C标准库给我们提供了一些API,可以在线程级别发送和接收信号,同时可以控制信号阻塞状态。

线程级别的信号处理API分为以下几类:

  • 线程信号阻塞状态控制 pthread_sigmask
  • 线程信号发送 pthread_kill
  • 信号等待处理sigwait,sigtimedwait,sigwaitinfo

线程信号处理流程

信号从产生到处理掉,整个过程可能需要花费很长时间,也可能非常快,中间会经过几个过程:

  • 信号产生,也就是发送信号成功;
  • 信号投递,也就是信号被传递给了信号接收者;
  • 信号等待处理,信号接收者将信号放入等待处理队列;如果是非实时信号,队列中只有一个相同信号,实时信号多个相同信号都会入队;如果接收者选择忽略该信号,则信号被丢弃;
  • 信号处理,接收者调用信号处理函数处理信号;这里的信号处理函数可以是用户设置的,也可以是系统默认的;

在这几个过程中,中间两个过程认为是信号未决,等待处理队列可以认为是未决队列,当我们阻塞某个信号时,它就会一直在等待队列中,直到信号阻塞状态取消才会调用信号处理函数。

那么涉及信号处理的,就有三个内容:

  • 信号掩码
  • 信号未决队列
  • 信号处理函数

下面我们来看一下进程与线程信号处理流程,以及它们的不同点。

进程信号的处理

进程会有一个信号掩码(屏蔽字,其中的信号会被阻塞)信号一般会先到达进程,然后再分发到进程所属的线程,当然分发给谁,这就很难确定。

信号未决队列,进程会有一个,它里面会有所有到达的信号;

信号处理函数的设置,也是进程级别,也就是说信号处理函数只有一个,不管那个线程修改了,进程所属的所有线程都会改变。

线程信号的处理

对于线程,它是好像是下级部门,指令下达就有点困难。
信号掩码和信号未决队列,这两个每个线程都会有独立的一份,各线程可以自己设置,默认是从创建者线程继承而来,在线程创建时,被创建的线程的信号未决队列会被初始化为空。

如前面所说,信号处理函数没有线程独立的。

线程的困难就来了,对于信号是否能收到,在进程收到信号时,会分发给不阻塞该信号的其中一个线程,不确定会发给那个线程,对于线程来讲,难啊。

线程也有一个优势,就是获取的信号未决队列,是当前线程的未决列表与进程队列的未决列表的并集。

线程级的信号处理,一般采用将它需要处理的信号阻塞住,进程级的阻塞,这样所有线程都不会处理该信号,然后线程就可以从未决队列检查是否有自己想要的信号了。

信号阻塞掩码设置

#include<signal.h>intpthread_sigmask(int how,constsigset_t*set,sigset_t*oldset);

通过此函数可以设置线程的信号阻塞掩码值,它与进程信号设置函数

sigprocmask

功能和使用方法一样,只是应用对象不同,一个是多线程下的,后者是多进程并发。

参数取值说明

how 的取值如下:

  • SIG_BLOCK , set 信号集为阻塞信号,加入到原来的阻塞信号列表中,也就是增加set中的信号到阻塞信号列表中;
  • SIG_UNBLOCK , 在阻塞信号列表中移除set中设置的信号,也就是将set中的信号解除阻塞;当然对于当前没有阻塞的信号,不能进行解除阻塞;
  • SIG_SETMASK , 将阻塞信号列表替换为 set中的信号列表,也就是将阻塞信号列表设置为set中的信号;

set 是当前需要设置的信号列表;

oldset 返回旧的信号列表,可以为NULL,则不接收旧的信号列表;

而信号集的设置, 有一套函数如下案例中的

sigemptyset

清空集合,

sigaddset

添加信号到集合。

发送信号

向进程发送信号的函数是kill,但是它会向进程内的所有线程都会发送信号;

给某一指定线程发送信号,在NPTL线程库中定义了专门的函数。

#include<signal.h>intpthread_kill(pthread_t thread,int sig);

参数也非常明确,线程的标识符和需要发送的信号ID;

这里需要特别注意:

  • 只能向当前线程所属的进程范围内的线程发送信号,当然包括自己
  • 线程必须是存在的,不存在时会有些未定义的行为

信号等待

等待信号集set中指定的信号到达,这些信号需要所有线程设置为阻塞状态,在信号未决列表中的信号都会被检测到,取出当前线程需要检测的信号。

#include<signal.h>intsigwait(constsigset_t*restrict set,int*restrict sig);intsigwaitinfo(constsigset_t*restrict set,siginfo_t*_Nullable restrict info);intsigtimedwait(constsigset_t*restrict set,siginfo_t*_Nullable restrict info,conststructtimespec*restrict timeout);

这三个函数都是C标准库提供,并不是NPTL线程库提供的,所以它们的函数命名不带pthread。

当信号没有到达时,会处于阻塞状态;当信号到达时,第一个函数会返回信号数字值,而后两个会返回信号的信号结构siginfo_t;

与前两个不同的时,第三个函数可以指定超时时间,当信号未到达,又到了超时时间时,返回错误-1,此时errno为EAGAIN;

  • 参数说明

set ,信号信,设置了要等待的信号;

代码案例

通过一个例子来看一下。

/* 
 * created by senllang 2024/1/4 
 * mail : [email protected] 
 *
 * Copyright (c) 2023-2024 senllang

 * This is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 * http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 * 
 */#include<errno.h>#include<pthread.h>#include<signal.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>staticvoid*sig_thread(void*arg){sigset_t*set = arg;int s, sig;for(;;){
        s =sigwait(set,&sig);if(s !=0)perror("sigwait");printf("Signal handling thread got signal %d\n", sig);if(sig == SIGQUIT)break;}}intmain(int argc,char*argv[]){pthread_t thread;sigset_t set;int s;void*res;/* Block SIGQUIT and SIGUSR1; other threads created by main()
       will inherit a copy of the signal mask. */sigemptyset(&set);sigaddset(&set, SIGQUIT);sigaddset(&set, SIGUSR1);
    s =pthread_sigmask(SIG_BLOCK,&set,NULL);if(s !=0)perror("pthread_sigmask");

    s =pthread_create(&thread,NULL,&sig_thread,&set);if(s !=0)perror("pthread_create");

    s =pthread_join(thread,&res);if(s !=0)perror("pthread_join");return0;}

这段程序将信号

SIGQUIT

SIGUSR1

添加到阻塞信号列表中,之后这两个信号到达进程时会被阻塞在信号队列中,同时它创建的线程也会继承这个信号掩码。

阻塞信号的意思是,线程收到了,只是将它放入信号未决队列中不调用处理函数,为了检测是否线程收到了这两个信号,我们在子线程中通过信号集进行检测,当收到我们发出的信号时进行打印,当收到

SIGQUIT

时,退出线程。

执行结果如下:

[senllang@hatch example_07]$ gcc -lpthread threadSignalSetmask.c 
[senllang@hatch example_07]$ ./a.out &[1]2560005[senllang@hatch example_07]$ kill-USR1 %1
Signal handling thread got signal 10[senllang@hatch example_07]$ kill-USR1 %1
Signal handling thread got signal 10[senllang@hatch example_07]$ kill-QUIT %1
Signal handling thread got signal 3[1]+  Done                    ./a.out

可以看到主线程和子线程收到这两个信号后,都没有反应,被阻塞了,而我们的信号集检测发现确实收到了这两个信号;
kill 命令是给进程发达了信号,该信号会给当前进程的所有线程都会发送相同信号,所以主线程和子线程都会收到。

总结

在目前线程信号实现的机制下,多线程的信号处理,信号处理会在单独的某个线程进行。比如进程退出时,也是由进程给每一个线程再发送退出信号,多线程的实现方法与此类似。

在具有事件循环的应用中,在信号的的 handler 中,可以将信号直接放入程序的队列中,立刻返回。这样直到线程从程序的队列中取出这个信号为止,整个线程看起来就像没有“中断”。

本文所涉及的代码已经上传到工程hatchCode, 在multipleThreads/example_07目录下;

结尾

非常感谢大家的支持,在浏览的同时别忘了留下您宝贵的评论,如果觉得值得鼓励,请点赞,收藏,我会更加努力!

作者邮箱:study@senllang.onaliyun.com
如有错误或者疏漏欢迎指出,互相学习。


本文转载自: https://blog.csdn.net/senllang/article/details/135517293
版权归原作者 韩楚风 所有, 如有侵权,请联系我们删除。

“【linux 多线程并发】多线程模型下的信号通信处理,与多进程处理的比较,属于相同进程的线程信号分发机制”的评论:

还没有评论