能引起什么问题呢?分为两个方面来进行讲述:
(1)在子进程中调用fork函数(此处不涉及锁);在主线程中调用fork()
在不涉及锁的情况下,在线程中调用fork一般不会出现什么问题。子线程中调用fork函数创建进程,都可以看作是对进程的创建,因为对于内核来说,不明确的区分进程和线程,所以在线程中创建进程都是对整个地址空间的拷贝。
(2)在子线程中加锁,然后在主线程中调用fork函数。
子进程通过继承整个地址空间的副本,也从父进程那里继承了所有的互斥量、条件变量和读写锁。如果父进程中包含多个线程,就需要清理锁,负责就会出现死锁。
下面用一个例子来看看。
#include <pthread.h>
#include <unistd.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *thread(void *arg)
{
printf("thread is running!\n");
pthread_mutex_lock(&mutex);
struct timespec ts = {10, 0};
nanosleep(&ts, 0); //睡眠10秒
pthread_mutex_unlock(&mutex);
return 0;
}
int main(int argc, char *argv[])
{
pthread_t tid;
pthread_create(&tid, NULL, thread, NULL);
if (fork() == 0) //子线程形成死锁
{
printf("child process start!\n");
thread(NULL);
printf("child process end!\n");
return 0;
}
sleep(100);
pthread_join(tid, NULL); //等待子线程结束
exit(0);
}
该程序会形成死锁。
以下是说明死锁的理由:
一般的,fork做如下事情
1.父进程的内存数据会原封不动的拷贝到子进程中
2.子进程在单线程状态下被生成
在内存区域里,静态变量mutex的内存会被拷贝到子进程里.而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里.fork的这两个特征就是造成死锁的原因.
对于上述程序的分析:
1.线程里的thread()先执行.
2.thread执行的时候会给互斥体变量mutex加锁.
3.mutex变量的内容会原样拷贝到fork出来的子进程中(因为线程执行nanosleep函数睡10秒,程序处理切换到子进程执行。在此之前,mutex变量的内容已经被线程改写成锁定状态).
4.子进程再次调用thread的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁).。
5.子线程的thread执行完成之后会把自己的mutex释放,但这是的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响.
6、子进程调用fork函数,生成子进程,子进程的thread函数用的mutex处于”锁定状态”,而且,解除锁定的线程在子进程里不存在
7、子进程的处理开始,子进程调用thread函数,子进程再次锁定已经是被锁定状态的mutex,然后就造成死锁
随便说一下,malloc函数就是一个维持自身固有mutex的典型例子,通常情况下它是fork-unsafe的.依赖于malloc函数的函数有很多,例如printf函数等,也是变成fork-unsafe的.
直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家.”fork后马上调用exec的场合,是作为一个特列不会产生问题的”.什么原因呢..?exec函数一被调用,进程的”内存数据”就被临时重置成非常漂亮的状态.因此,即使在多线程状态的进程里,fork后不马上调用一切危险的函数,只是调用exec函数的话,子进程将不会产生任何的误动作.但是,请注意这里使用的”马上”这个词.即使exec前仅仅只是调用一回printf(“I’mchild process”),也会有死锁的危险.
注:exec函数里指明的命令一被执行,该命令的内存映像就会覆盖父进程的内存空间.用一个全新的程序替换了当前进程的正文、堆栈、数据。所以,父进程里的任何数据将不复存在.
查看前面进程创建中,子进程在创建后,是写时复制的,也就是子进程刚创建时,与父进程一样的副本,当exce后,那么老的地址空间被丢弃,而被新的exec的命令的内存的印像覆盖了进程的内存空间,所以锁的状态无关紧要了。
如何规避灾难呢?
规避方法1:做fork的时候,在它之前让其他的线程完全终止.
规避方法2:fork后在子进程中马上调用exec函数
译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.
规避方法3:”其他线程”中,不做fork-unsafe的处理
规避方法4:使用pthread_atfork函数
#include
int pthread_atfork(void (*prepare) (void),void (*parent) (void), void (*child)(void))
规避方法5:在多线程程序里,不使用fork