线程与进程, 线程与信号
线程与进程
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接收到信号并处理之。