注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

多线程下fork死锁分析  

2015-04-06 15:59:09|  分类: Linux系统编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、标准库的可重入问题
在现在的C库中,通常都考虑到了重入的问题,对于C库内部维护的变量通常都使用到了互斥锁来进行互斥。例如在C库动态内存管理,通过setenv对进程环境变量修改,对于系统时区的读取等。这些锁通常都是清纯无害,用户透明的,不过正如现实永远比电影精彩,一些诡异的现象就会在你不经意写下的代码里发生。
最近遇到的一个问题就是在多线程环境下fork一个进程,在fork之后感觉到了整个世界满满的恶意,调用一个localtime_r都会导致新派生的进程死锁。这个现象看起来非常不可思议,但是阳光下没有新鲜事,特别是有google的情况下,下面这篇文章简明扼要的描述、分析并复现了这个问题。
二、C库互斥锁的通常实现
C库通常通过lll_lock和lll_unlock接口操作futex来实现互斥,这种futex机制在ulrich drepper的《futex are tricky》中有详细的描述,对于最为朴素(相对于之后的robust futext等)的futex实现,通过一个数值0 1 2分别表示 未锁定 已锁定且无等待 已锁定且有等待。1和2的区别在于释放锁之后是否需要通过系统调用来进行唤醒操作,对于这一点的描述drepper的描述为
if (atomic_dec(val) != 1)
{
val=0;
futex_wake(&val,1);
}

这个简单的说明在glibc的实现为glibc-2.11\nptl\sysdeps\unix\sysv\linux\i386\lowlevellock.h
#if defined NOT_IN_libc || defined UP
# define __lll_unlock_asm LOCK_INSTR "subl $1, %0\n\t"
#else
# define __lll_unlock_asm "cmpl $0, %%gs:%P3\n\t"      \
 "je 0f\n\t"      \
 "lock\n"      \
 "0:\tsubl $1,%0\n\t"
#endif

#define lll_unlock(futex, private) \
  (void)      \
    ({ int ignore;      \
       if (__builtin_constant_p (private) && (private) == LLL_PRIVATE)      \
__asm __volatile (__lll_unlock_asm      \
  "jne _L_unlock_%=\n\t"      \
关键的代码在__lll_unlock_asm中的
subl $1,%0
和lll_unlock中的
jne _L_unlock_%=
解锁时将val的值减一,如果减一之后结果非零(说明原始值不等于1,对应于atomic_dec(val) != 1)则需要执行解锁动作,否则直接返回,不需要进入系统进行解锁。
三、多线程下的影响
在多线程环境下,如果有多个线程在执行,其中一个获得了锁(其它线程可能正在尝试获得这个锁并已经挂在这个锁上),此时这个val的值可能为1或者2,无论何种情况,新fork的进程将会继承到这个变量的值,在子进程也执行C库的同一个函数时,按照futex的约定,它会自觉的把自己挂在futex的等待队列上并进行等待,这个过程通过futex系统调用的wait命令完成。进入系统之后linux-2.6.21\kernel\futex.c:futex_wait-->>get_futex_key
/*
* Private mappings are handled in a simple way.
*
* NOTE: When userspace waits on a MAP_SHARED mapping, even if
* it's a read-only handle, it's expected that futexes attach to
* the object not the particular process.  Therefore we use
* VM_MAYSHARE here, not VM_SHARED which is restricted to shared
* mappings of _writable_ handles.
*/
if (likely(!(vma->vm_flags & VM_MAYSHARE))) {
key->private.mm = mm;
key->private.address = address;
return 0;
}
这里可以看到,新建立的futex使用了mm结构指针和位置作为了futex的键值,由于fork不和父进程共享mm_struct,所有新创建的futex和父进程的futex不会共享等待队列,也就意味着它永远不可能再被唤醒了。
四、clone+CLONE_VM是否会有问题
既然fork是由于和父进程没有共享mm_struct,那么clone系统调用如果共享了VM(CLONE_VM)是否就可以避免这种死锁呢?这个直观上看是这样的,因为phtread的创建就是设置了CLONE_VM这个标志位。而且stackoverflow上有一篇文章说clone+CLONE_VM会死锁,我把前面作者的展示代码中的fork修改为clone系统调用,试了相当长时间,发现使用带有CLONE_VM标志位的clone的确没有死锁,表示不是很理解为啥。
五、简单修改之后的测试代码
根据前面提到的代码简单修改,多创建一些线程,便于产生锁定
#include <sched.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h>
#include <sys/syscall.h> 
#include <sys/types.h> 
#include <linux/types.h> 
#include <pthread.h>
#include <sys/wait.h>
 
static void* worker(void* arg) {
  pthread_detach(pthread_self());
 
  for (;;) {
    setenv("foo", "bar", 1);
    usleep(100);
  }
 
  return NULL;
}
#define CHILD_STACK_SIZE 4096*4 

static void sigalrm(int sig) {
  char a = 'a';
  write(fileno(stderr), &a, 1);
}
int walkerfunc(void *arg)
      signal(SIGALRM, sigalrm);
      alarm(1);
      unsetenv("bar");
      exit(0);
}


#define MAXT 30 
int main() {
  pthread_t setenv_thread[MAXT];
  int i;
  for (i = 0; i < MAXT; i++)
{
  pthread_create(&setenv_thread[i], NULL, worker, 0);
 }
  for (;;) {
#ifdef CLONETEST
void *child_stack_start = malloc(CHILD_STACK_SIZE); 
int ret = clone(walkerfunc, child_stack_start +CHILD_STACK_SIZE, CLONE_VM, NULL, NULL, NULL, NULL);
#else
    if (fork() == 0) {
walkerfunc(0);
    }
 #endif
    wait3(NULL, WNOHANG, NULL);
    usleep(2500);
  }
 
  return 0;
}
[tsecer@Harry clonelock]# gcc -DCLONETEST -g  fork.c -lpthread -o clonetest
[tsecer@Harry clonelock]# gcc  -g  fork.c -lpthread -o forktest
[tsecer@Harry clonelock]# ./forktest 
aaaa^C
[tsecer@Harry clonelock]# ./clonetest 

  评论这张
 
阅读(273)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017