线程与进程, 线程与信号

线程与进程

fork子进程如何复制多线程的父进程?

考虑一个问题:父进程在fork之前,已经创建了多个线程,那么再调用fork,新建子进程具有和父进程同样数量的线程吗?是否会复制父进程的所有线程?

答案是否定的。 fork子进程只会复制调用fork的线程,不会复制父进程的其他线程。既然是复制,因而子进程会自动继承父进程中的互斥锁(条件变量、信号量)的状态。i.e. 如果互斥量在父进程中被上锁,到子进程也会被上锁。

然而,子进程并不清楚从父进程继承来的互斥量具体的锁状态,因为是复制的线程。互斥量有可能加锁,有可能解锁。如果已经加锁,子进程再次加锁,会导致永远无法解锁(死锁)。

如下面的例子,子进程处于死锁状态

/**
 * 线程与进程示例程序
 * fork子进程只会复制调用fork的执行线程,并不会复制父进程其他子线程。子进程自动>继承父进程中互斥锁(条件变量/信号量)的状态。父进程中已经加锁的互斥锁,子进程也>会被锁住
 */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex;

/* 父进程调用,会锁住父进程中的mutex*/
void *another(void *arg)
{
    printf("in child thread, lock the mutex\n");
    pthread_mutex_lock(&mutex);
    sleep(5);
    pthread_mutex_unlock(&mutex);
}

int main()
{

    pthread_mutex_init(&mutex, NULL);
    pthread_t id;
    pthread_create(&id, NULL, another, NULL);

    /* 父进程中的主线程暂停1s,以确保在执行fork前子线程已经开始运行并获得互斥量mutex */
    sleep(1);
    int pid;

    /* 由于前面已经sleep 1S,fork时互斥量mutex已经加锁,fork复制调用线程的锁状态,也是加锁的 */
    if ((pid = fork()) < 0) {
        perror("fork error");
        pthread_join(id, NULL);
        pthread_mutex_destroy(&mutex);
        return 1;
    }
    else if (pid == 0) { /* child */
        printf("i am in the child, want to get the lock\n");

        /* 子进程从父进程继承来互斥锁mutex的状态,该互斥锁处于锁住状态,这是由父进程中的子线程执>行pthread_mutex_lock引起的。因此下面语句会导致子进程一直阻塞,而从逻辑上来说,虽然单独的子进程不>应该是阻塞的 */
        pthread_mutex_lock(&mutex);
        printf("I cannot run to here, oop...\n");
        pthread_mutex_unlock(&mutex);
        exit(0);
    }
    else { /* parent */
        wait(NULL);
    }
    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

pthread_atfork 清理多线程父进程锁状态

如何解决fork子进程继承父进程锁状态导致的死锁问题?
之前这篇文章Linux 系统编程学习笔记 - 线程,有提到过pthread_atfork函数,可以建立3个fork句柄帮助清理互斥锁的状态,以确保fork调用之后父进程、子进程都有一个确定的锁状态。

pthread_atfork解决fork子进程锁状态问题,示例程序:

/**
 * 线程与进程示例程序
 * 利用pthread_atfork解决fork子进程继承父进程锁状态,而不清楚锁状态的问题
 */

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t mutex;

/* 父进程调用,会锁住父进程中的mutex*/
void *another(void *arg)
{
    printf("in child thread, lock the mutex\n");
    pthread_mutex_lock(&mutex);
    sleep(5);
    pthread_mutex_unlock(&mutex);
}

/* fork调用创建出子进程之前被执行,用来锁定父进程中所有的互斥锁 */
void prepare()
{
    pthread_mutex_lock(&mutex);
}

/* fork调用创建出子进程之后,释放父、子进程中被锁主的互斥锁 */
void infork()
{
    pthread_mutex_unlock(&mutex);
}

int main()
{
    pthread_mutex_init(&mutex, NULL);
    pthread_t id;
    pthread_create(&id, NULL, another, NULL);

    /* 父进程中的主线程暂停1s,以确保在执行fork前子线程已经开始运行并获得互斥量mutex */
    sleep(1);
    int pid;

    pthread_atfork(prepare, infork, infork);

    /* 由于前面已经sleep 1S,fork时互斥量mutex已经加锁,fork复制调用线程的锁状态,也是加锁的 */
    if ((pid = fork()) < 0) {
        perror("fork error");
        pthread_join(id, NULL);
        pthread_mutex_destroy(&mutex);
        return 1;
    }
    else if (pid == 0) { /* child */
        printf("i am in the child, want to get the lock\n");

        /* 子进程从父进程继承来互斥锁mutex的状态,该互斥锁处于锁住状态,这是由父进程中的子线程执>行pthread_mutex_lock引起的。因此下面语句会导致子进程一直阻塞,而从逻辑上来说,虽然单独的子进程不>应该是阻塞的 */
        pthread_mutex_lock(&mutex);
        printf("I cannot run to here, oop...\n");
        pthread_mutex_unlock(&mutex);
        exit(0);
    }
else { /* parent */
        wait(NULL);
    }

    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);

    return 0;
}

线程与信号

线程的signal mask

每个线程都拥有自己独立的信号屏蔽字(signal mask),共享信号处理以及信号处理函数。
i.e. 线程可以选择是否屏蔽信号,但是捕获方式(SIG_DFL, SIG_IGN或捕获)是共享的,捕获函数也是共享的。

可以用sigprocmask设置进程的signal mask,如何设置线程的signal mask?
注:sigaction修改的是进程对应某个信号的捕获函数,以及处理信号期间的signal mask,而非任意情况下的signal mask。

线程使用pthread_sigmask设置线程的signal mask,sigwait阻塞线程等待指定信号并处理之。
参见Linux 系统编程学习笔记 - 线程

实例:一个线程处理所有信号

如果每个线程都单独处理信号,可能会有不同的信号处理方式,不同的signal mask,很容易导致逻辑错误。如果让一个专门的线程处理所有信号,如何实现?

步骤:
1)主线程创建出子线程之前,就调用pthread_sigmask设置好signal mask,屏蔽(阻塞)所有信号。这样,新建的所有子线程会自动继承这个signal mask,从而不会响应被屏蔽信号。

2)在某个线程中,调用sigwait等待信号并处理

注意:一旦使用sigwait,就不应该在外对应信号设置信号处理函数,因为程序接收到信号时,只能有一个起作用。

一个专门的线程处理指定信号示例

#include <signal.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define handle_error_en(en, msg) \
    do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

void *sig_thread(void *arg)
{
    sigset_t *set = (sigset_t *)arg;
    int s, sig;
    for (; ;) {
        /* 2. 调用sigwait等待信号 */
        s = sigwait(set, &sig);
        if (s != 0) {
            handle_error_en(s, "sigwait error");
        }
        printf("Signal handling thread got signal %d\n", sig);
    }
}

int main()
{
    pthread_t thread;
    sigset_t set;
    int s;

    /* 1. 在主线程中设置signal mask */
    sigemptyset(&set);
    sigaddset(&set, SIGQUIT);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGUSR1);
    s = pthread_sigmask(SIG_BLOCK, &set, NULL);
    if (s != 0)
        handle_error_en(s, "pthread_sigmask error");

    s = pthread_create(&thread, NULL, &sig_thread, (void *)&set);
    if (s != 0)
        handle_error_en(s, "pthread_create error");

    pause();
    return 0;
}

从运行结果,可以看到,即使线程也继承了main线程的signal mask屏蔽了信号,但是依然可以用sigwait接收到信号并处理之。

posted @ 2021-07-22 16:35  明明1109  阅读(361)  评论(0编辑  收藏  举报