多线程程序里不准使用fork
一般的,fork做如下事情
- 父进程的内存数据会原封不动的拷贝到子进程中
- 子进程在单线程状态下被生成
在内存区域里,静态变量 mutex 的内存会被拷贝到子进程里。而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里。fork 的这两个特征就是造成死锁的原因。
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
void *doit(void *arg)
{
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
fprintf(stderr, "lock... pid = %d\n", getpid());
/* 线程持有锁,进入睡眠 */
struct timespec tc = {10, 0};
nanosleep(&tc, 0);
fprintf(stderr, "unlock... pid = %d\n", getpid());
pthread_mutex_unlock(&mutex);
return NULL;
}
int main(int argc, char **argv)
{
pid_t pid;
pthread_t t;
struct timespec tc = {2, 0};
pthread_create(&t, 0, doit, 0);
// sleep,让线程执行 doit() 并占有锁
nanosleep(&tc, 0);
if ((pid = fork()) == 0) {
/* 在线程持有锁期间进行fork,在子进程中,线程将会占有锁并死去 */
fprintf(stderr, "[child] fork...\n");
/* 再次试图加锁,此时进入死锁 */
doit(NULL);
return 0;
}
fprintf(stderr, "[parent] child pid = %d\n", pid);
pthread_join(t, 0);
return 0;
}
死锁原因的详细解释 ---
- 线程里的 doit() 先执行
- doit() 执行的时候会给互斥体变量 mutex 加锁
- mutex 变量的内容会原样拷贝到 fork 出来的子进程中(在此之前,mutex 变量的内容已经被线程改写成锁定状态)
- 子进程再次调用 doit() 的时候,在锁定互斥体 mutex 的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个 mutex 锁)
- 线程的 doit() 执行完成之前会把自己的 mutex 释放,但这是的 mutex 和子进程里的 mutex 已经是两份内存。所以即使释放了 mutex 锁也不会对子进程里的 mutex 造成什么影响
像这里的 doit() 函数那样的,在多线程里因为 fork 而引起问题的函数,我们把它叫做“fork-unsafe函数”。反之,不能引起问题的函数叫做“fork-safe函数”。
随便说一下,malloc 函数就是一个维持自身固有 mutex 的典型例子,通常情况下它是 fork-unsafe 的。依赖于 malloc 函数的函数有很多,例如 printf 函数等,也是变成 fork-unsafe 的。
直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家“fork 后马上调用 exec 的场合,是作为一个特列不会产生问题的”。exec 函数一被调用,进程的“内存数据”就被临时重置成非常漂亮的状态。因此,即使在多线程状态的进程里,fork 后不马上调用一切危险的函数,只是调用 exec 的话,子进程将不会产生任何的误动作。
如何规避灾难呢?
规避方法1:做fork的时候,在它之前让其他的线程完全终止
规避方法2:fork后在子进程中马上调用exec函数
规避方法3:“其他线程”中,不做fork-unsafe的处理
规避方法4: 使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数
规避方法5:在多线程程序里,不使用 fork
参考
http://www.cppblog.com/lymons/archive/2008/06/01/51836.html
来源:oschina
链接:https://my.oschina.net/u/209876/blog/672450