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

07线程信号处理

专栏内容

  • 参天引擎内核架构
    本专栏一起来聊聊参天引擎内核架构,以及如何实现多机的数据库节点的多读多写,与传统主备,MPP的区别,技术难点的分析,数据元数据同步,多主节点的情况下对故障容灾的支持。

  • 手写数据库toadb
    本专栏主要介绍如何从零开发,开发的步骤,以及开发过程中的涉及的原理,遇到的问题等,让大家能跟上并且可以一起开发,让每个需要的人成为参与者。
    本专栏会定期更新,对应的代码也会定期更新,每个阶段的代码会打上tag,方便阶段学习。

开源贡献

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

前言

现代的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>
int pthread_sigmask(int how, const sigset_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>
int pthread_kill(pthread_t thread, int sig);

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

这里需要特别注意:

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

信号等待

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

#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict sig);

int sigwaitinfo(const sigset_t *restrict set,
                       siginfo_t *_Nullable restrict info);

int sigtimedwait(const sigset_t *restrict set,
                       siginfo_t *_Nullable restrict info,
                       const struct timespec *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>

static void *
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;
    }
}

int main(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");

    return 0;    
}

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

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

结尾

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

作者邮箱:[email protected]
如有错误或者疏漏欢迎指出,互相学习。

猜你喜欢

转载自blog.csdn.net/senllang/article/details/135517293