多线程中fork与mutex

  在多线程程序中fork出一个新进程,发现新的进程没法正常工做。由于:在使用fork时会将原来进程中的全部内存数据复制一份保存在子进程中。可是在拷贝的时候,可是线程是没法被拷贝的。若是在原来线程中加了锁,在使用的时候会形成死锁。能够将开线程的代码放在fork之后。也就是放在新的子进程中进行建立。promise

  在多线程程序里,在”自身之外的线程存在的状态”下一使用fork的话,就可能引发各类各样的问题.比较典型的例子就是,fork出来的子进程可能会死锁.请不要,在不能把握问题的原委的状况下就在多线程程序里fork子进程.
  在子进程的执行开始处调用doit()时,发生死锁的机率会很高.安全

void* doit(void*) 
{
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    struct timespec ts = {10, 0}; nanosleep(&ts, 0); // // 睡10秒
    pthread_mutex_unlock(&mutex);
    return 0;
}

int main(void)
{
        pthread_t t;
        pthread_create(&t, 0, doit, 0);    // 作成并启动子线程
        if (fork() == 0)
        {
              //子进程
             //在子进程被建立的瞬间,父的子进程在执行nanosleep的场合比较多
              doit(0);
              return 0;
        }
        pthread_join(t, 0); // 等待子线程结束
}    

如下是说明死锁的理由:
通常的,fork作以下事情数据结构

  1. 父进程的内存数据会原封不动的拷贝到子进程中
  2. 子进程在单线程状态下被生成

  在内存区域里,静态变量mutex的内存会被拷贝到子进程里.并且,父进程里即便存在多个线程,但它们也不会被继承到子进程里. fork的这两个特征就是形成死锁的缘由.多线程

  1. 线程里的doit()先执行.
  2. doit执行的时候会给互斥体变量mutex加锁.
  3. mutex变量的内容会原样拷贝到fork出来的子进程中(在此以前,mutex变量的内容已经被线程改写成锁定状态).
  4. 子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,因此就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁).
  5. 线程的doit执行完成以前会把本身的mutex释放,但这是的mutex和子进程里的mutex已是两分内存.因此即便释放了mutex锁也不会对子进程里的mutex形成什么影响.

  以上程序执行流程异步

  1. 在fork前的父进程中,启动了线程1和2
  2. 线程1调用doit函数
  3. doit函数锁定本身的mutex
  4. 线程1执行nanosleep函数睡10秒
  5. 在这儿程序处理切换到线程2
  6. 线程2调用fork函数
  7. 生成子进程
  8. 这时,子进程的doit函数用的mutex处于”锁定状态”,并且,解除锁定的线程在子进程里不存在
  9. 子进程的处理开始
  10. 子进程调用doit函数
  11. 子进程再次锁定已是被锁定状态的mutex,而后就形成死锁

  像这里的doit函数那样的,在多线程里由于fork而引发问题的函数,咱们把它叫 作”fork-unsafe函数”.反之,不能引发问题的函数叫作”fork-safe函数”.虽然在一些商用的UNIX里,源于OS提供的函数(系统调 用),在文档里有fork-safety的记载,可是在 Linux(glibc)里固然!不会被记载.即便在POSIX里也没有特别的规定,因此那些函数是fork-safe的,几乎不能判别.不明白的话,做 为unsafe考虑的话会比较好一点吧.(2004/9/12追记)Wolfram Gloger说过,调用异步信号安全函数是规格标准,因此试着调查了一下,在pthread_atforkの这个地方里有” In the meantime*5, only a short list of async-signal-safe library routines are promised to be available.”这样的话.好像就是这样.async

malloc函数就是一个维持自身固有mutex的典型例子,一般状况下它是fork-unsafe的.依赖于malloc函数的函数有不少,例如printf函数等,也是变成fork-unsafe的.函数


直目前为止,已经写上了thread+fork是危险的,可是有一个特例须要告诉你们.”fork后立刻调用exec的场合,是做为一个特列不会产生问题的”. 什么缘由呢..?exec函数*6一被调用,进程的”内存数据”就被临时重置成很是漂亮的状态.所以,即便在多线程状态的进程里,fork后不立刻调用一切危险的函数,只是调用exec函数的话,子进程将不会产生任何的误动做.可是,请注意这里使用的”立刻”这个词.即便exec前仅仅只是调用一回printf(“I’m child process”),也会有死锁的危险.
译者注:exec函数里指明的命令一被执行,该命令的内存映像就会覆盖父进程的内存空间.因此,父进程里的任何数据将不复存在.spa

本blog的理解:查看前面进程建立中,子进程在建立后,是写时复制的,也就是子进程刚建立时,与父进程同样的副本,当exce后,那么老的地址空间被丢弃,而被新的exec的命令的内存的印像覆盖了进程的内存空间,因此锁的状态可有可无了。

规避方法1:作fork的时候,在它以前让其余的线程彻底终止.
  在fork以前,让其余的线程彻底终止的话,则不会引发问题.但这仅仅是可能的状况.还有,由于一些缘由而其余线程不能结束就执行了fork的时候,就会是产生出一些解析困难的不具合的问题.
规避方法2:fork后在子进程中立刻调用exec函数
  不用使用规避方法1的时候,在fork后不调用任何函数(printf等)就立刻调用execl等,exec系列的函数.若是在程序里不使用”没有exec就fork”的话,这应该就是实际的规避方法吧.把本来子进程应该作的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.线程

规避方法3:”其余线程”中,不作fork-unsafe的处理
  除了调用fork的线程,其余的全部线程不要作 fork-unsafe的处理.为了提升数值计算的速度而使用线程的场合*7,这多是fork- safe的处理,可是在通常的应用程序里则不是这样的.即便仅仅是把握了那些函数是fork-safe的,作起来还不是很容易的.fork-safe函 数,必须是异步信号安全函数,而他们都是能数的过来的.所以,malloc/new,printf这些函数是不能使用的.调试

规避方法4:使用pthread_atfork函数,在即将fork以前调用事先准备的回调函数.apue中详细介绍了它
  使用pthread_atfork函数,在即将 fork以前调用事先准备的回调函数,在这个回调函数内,协商清除进程的内存数据.可是关于OS提供的函数 (例:malloc),在回调函数里没有清除它的方法.由于malloc里使用的数据结构在外部是看不见的.所以,pthread_atfork函数几乎 是没有什么实用价值的.
规避方法5:在多线程程序里,不使用fork
  就是不使用fork的方法.即用pthread_create来代替fork.这跟规避策2同样都是比较实际的方法,值得推荐.

  1. 生成子进程的系统调用
  2. 全局变量和函数内的静态变量
  3. 若是使用Linux的话,查看pthread_atfork函数的man手册比较好.关于这些流程都有一些解释.
  4. Solaris和HP-UX等
  5. 从fork后到exec执行的这段时间
  6. execve系统调用
  7. 仅仅作四则演算的话就是fork-safe的

  总结:在程序正常运行时出现不能正常工做,而在调试时又能正常工做。则能够考虑死锁的状况!

相关文章
相关标签/搜索