线程和fork()

    在多线程程序里,在”自身以外的线程存在的状态”下一使用fork的话,就可能引起各种各样的问题.比较典型的例子就是,fork出来的子进程可能会死锁.请不要,在不能把握问题的原委的情况下就在多线程程序里fork子进程.

能引起什么问题呢?分为两个方面来进行讲述:

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,然后就造成死锁

 像这里的thread函数那样的,在多线程里因为fork而引起问题的函数,我们把它叫做”fork-unsafe函数”.反之,不能引起问题的函数叫做”fork-safe函数”.虽然在一些商用的UNIX,源于OS提供的函数(系统调用),在文档里有fork-safety的记载,但是在Linux(glibc)里当然不会被记载.即使在POSIX里也没有特别的规定,所以那些函数是fork-safe,几乎不能判别.不明白的话,作为unsafe考虑的话会比较好一点吧.Wolfram Gloger说过,调用异步信号安全函数是规格标准。

随便说一下,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的时候,在它之前让其他的线程完全终止.
     fork之前,让其他的线程完全终止的话,则不会引起问题.但这仅仅是可能的情况.还有,因为一些原因而其他线程不能结束就执行了fork的时候,就会是产生出一些解析困难的不具合的问题.

规避方法2:fork后在子进程中马上调用exec函数
     不用使用规避方法1的时候,fork后不调用任何函数(printf)就马上调用execl,exec系列的函数.如果在程序里不使用”没有execfork”的话,这应该就是实际的规避方法吧.
译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.

规避方法3:”其他线程”中,不做fork-unsafe的处理
      除了调用fork的线程,其他的所有线程不要做fork-unsafe的处理.为了提高数值计算的速度而使用线程的场合,这可能是fork-safe的处理,但是在一般的应用程序里则不是这样的.即使仅仅是把握了那些函数是fork-safe,做起来还不是很容易的.fork-safe函数,必须是异步信号安全函数,而他们都是能数的过来的.因此,malloc/new,printf这些函数是不能使用的.

规避方法4:使用pthread_atfork函数

#include
int pthread_atfork(void (*prepare) (void),void (*parent) (void), void (*child)(void))

     使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数,在这个回调函数内,协商清除进程的内存数据.但是关于OS提供的函数(:malloc),在回调函数里没有清除它的方法.因为malloc里使用的数据结构在外部是看不见的.因此,pthread_atfork函数几乎是没有什么实用价值的.

规避方法5:在多线程程序里,不使用fork

 就是不使用fork的方法.即用pthread_create来代替fork.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值