多线程环境下需要使用互斥量等数据来进行线程间数据同步,然而同时使用同步对象与信号处理很容易造成死锁,本文探索如何在多线程环境下来进行信号处理。

在多线程环境中,为了防止信号中断线程,通常把信号加到每个线程的信号屏蔽字中。然后安排专用线程来处理信号

线程对信号的处理

信号处理

每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的

每个线程都有自己的信号屏蔽字,这意味着单个线程可以阻止某些信号被递送到该线程pthread_sigmask())。

由于信号的处理是线程共享的,因此当某个线程修改了某个给定信号的处理行为以后,所有线程都会共享这个处理行为的改变。

换句话说,如果一个线程对某个给定信号设置的处理方式是忽略该信号,那么另一个线程可以通过两种方式撤销上述线程的信号选择:恢复信号的默认处理行为,或者为信号设置一个新的信号处理程序

信号递送

进程中信号是递送到单个线程的。如果一个信号与硬件故障有关,那么该信号一般会被发送到引起该事件的线程中,而其他信号则被发送到任意一个线程中。

进程可以使用sigprocmask()函数阻止信号递送,而线程可以通过调用pthread_sigmask()函数阻止信号递送到调用线程中

#include <signal.h>

int pthread_sigmask(int how, const sigset_t* set, sigset_t* oldset);
// 返回值:成功返回0,失败返回错误编号

pthread_sigmask()函数与sigprocmask()函数基本相同,除了pthread_sigmask()函数是返回错误编号而不设置errno

sigprocmask()函数的说明见sigprocmask函数

信号处理

线程可以通过调用sigwait()函数等待一个或多个信号的出现。sigwait()会使调用线程阻塞以等待接收信号。

#include <signal.h>

int sigwait(const sigset_t* restrict set, int* restrict sig);
// 返回值:成功返回0,失败返回错误编号

set参数指定线程要等待的信号集,sig参数指向的整数保存收到的信号。

为了避免信号丢失,线程在调用sigwait()之前,必须阻塞set参数指定的信号,否则在解除阻塞与调用sigwait()函数之间存在时间窗口。sigwait()函数会原子性地解除信号集的阻塞状态并阻塞线程以等待信号,直到有新的信号被递送

如果set参数指定的信号集中的某些信号在线程调用sigwait()函数时已经处于挂起状态或是未决的1,那么sigwait()函数则会直接返回并在返回之间从进程中移除那些处于挂起等待状态的信号,而不阻塞线程。

如果支持排队信号,并且有信号的多个实例被挂起,那么sigwait()函数仅会移除该信号的一个实例,其余信号实例继续挂起排队。

如果多个线程在调用sigwait()函数等待同一个信号而阻塞,那么在信号递送的时候,就仅有一个线程可以从sigwait()函数返回。

如果进程使用sigaction()函数为一个信号建立了信号处理程序,而线程也在调用sigwait()函数等待该信号,那么如何递送信号有操作系统决定,可以是sigwait()函数返回,也可以是激活信号处理程序,二者选一。

sigsuspend不同的是,sigwait()函数传入的参数是要接收的信号,而非屏蔽的信号。相同点在于都会将解除信号阻塞与等待信号合并为一个原子操作。

主动发送信号

kill命令或函数可以将信号发送给进程。

使用pthread_kill()函数可以将信号发送给线程。

#include <signal.h>

int pthread_kill(pthread_t thread, int sig);
// 返回值:成功返回0,失败返回错误编号

可以设置sig为0来检测线程是否存在。

如果信号的默认处理动作是终止进程,那么把该信号传递给线程会导致进程退出。

多线程中信号处理方案

sigwait()函数有一个很重要的作用就是可以将异步产生的信号转为同步来处理。

因此在多线程环境中,为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中。然后安排专用线程来处理信号

因为多线程环境下调用的函数大多都不是线程安全的(如malloc, pthread_*等),如果信号中断线程就会引发异常错误。

信号处理专用线程可以进行函数调用,而不必关系哪些函数是异步信号安全的。因为这些函数调用来自正常的线程上下文。

示例

在下面的示例程序中,进程如果收到SIGQUIT信号后返回。

由于是多线程环境,因此其中主线程阻塞了SIGQUITSIGINT信号,并专门创建了一个处理信号的线程。

信号处理线程收到SIGQUIT信号后修改全局条件,并通过条件变量发送信号通知主线程,主线程检测到变化之后停止阻塞并返回。

详细代码如下:

#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int quitflag = 0;  // 进程返回标志,由线程接收到指定信号后设置为非0
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 线程要接收的信号,同时也是主线程需要屏蔽的信号
static sigset_t mask;

static void* thr_fn(void* arg)
{
    int err, signo;
    for (;;)
    {
        // 等待mask参数中指定的信号并阻塞
        err = sigwait(&mask, &signo);
        if (err != 0)
        {
            printf("sigwait: %s\n", strerror(err));
            exit(-1);
        }

        switch (signo)
        {
            case SIGINT:
                printf("\ncaught interrupt\n");
                break;
            case SIGQUIT:
                printf("\ncaught quit\n");
                pthread_mutex_lock(&mutex);
                quitflag = 1;
                pthread_mutex_unlock(&mutex);
                pthread_cond_signal(&cond);
                return (0);

            default:
                printf("unexpected signal %d\n", signo);
                exit(1);
        }
    }
}

int main(int argc, char* argv[])
{
    int err;
    sigset_t oldmask;
    pthread_t tid;
    // 初始化mask,设置要接收或屏蔽的信号
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGQUIT);
    // 屏蔽信号,防止信号发送给主线程
    if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
    {
        printf("error: %s\n", strerror(err));
        exit(-1);
    }

    // 创建信号接收线程
    err = pthread_create(&tid, NULL, thr_fn, 0);
    if (err != 0)
    {
        printf("pthread_create: %s\n", strerror(err));
        exit(-1);
    }

    // 加锁等待条件
    pthread_mutex_lock(&mutex);
    while (quitflag == 0)
        pthread_cond_wait(&cond, &mutex);
    pthread_mutex_unlock(&mutex);

    // 已经捕捉到的SIGQUIT信号,重置quitflag
    quitflag = 0;

    // 恢复信号设置
    if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
    {
        perror("sigprocmask: ");
        exit(-1);
    }

    exit(0);
}
// 执行结果如下:
// ^C
// caught interrupt
// ^C
// caught interrupt
// ^C
// caught interrupt
// ^\
// caught quit

注解


  1. 信号的处理分为两个过程,分别是信号的产生与递送。如果信号已经产生,但是被进程阻塞,不能被递送,此时信号就是挂起状态或称为未决的。 ↩︎