问题场景:
线程1:localtime --> fork -- 子进程1 --> localtime
线程2:localtime --> fork -- 子进程2 --> localtime
线程x: localtime --> fork -- 子进程x --> localtime
问题分析:
同事使用上述场景时候,遇到了死锁问题,死锁位置在随机子进程的localtime函数内,通过对glibc源码分析发现,localtime内部存在全局锁,而fork出的子进程会复制父进程的全局变量(但不共享),那么就存在如下场景会出现死锁问题:
线程1:localtime → 加锁 tzset_lock = 1 ----→ 解锁 tzset_lock = 0
↓ 调度 ↑ 调度
线程2: fork ----------------→ wait
↓
子进程2: tzset_lock = 1 (此时复制过来的全局锁是锁住的状态,且不和主进程共享,子进程2一旦调用localtime走到加锁的步骤,必然会死锁)
glibc源码如下,localtime实际调用的是__tz_convert函数:
/* This locks all the state variables in tzfile.c and this file. */
__libc_lock_define_initialized (static, tzset_lock)
struct tm *
__tz_convert (const time_t *timer, int use_localtime, struct tm *tp)
{
long int leap_correction;
int leap_extra_secs;
if (timer == NULL)
{
__set_errno (EINVAL);
return NULL;
}
__libc_lock_lock (tzset_lock); //实际tzset_lock就是个全局的mutex锁
/* Update internal database according to current TZ setting.
POSIX.1 8.3.7.2 says that localtime_r is not required to set tzname.
This is a good idea since this allows at least a bit more parallelism. */
tzset_internal (tp == &_tmbuf && use_localtime, 1);
if (__use_tzfile)
__tzfile_compute (*timer, use_localtime, &leap_correction,
&leap_extra_secs, tp);
else
{
if (! __offtime (timer, 0, tp))
tp = NULL;
else
__tz_compute (*timer, tp, use_localtime);
leap_correction = 0L;
leap_extra_secs = 0;
}
if (tp)
{
if (! use_localtime)
{
tp->tm_isdst = 0;
tp->tm_zone = "GMT";
tp->tm_gmtoff = 0L;
}
if (__offtime (timer, tp->tm_gmtoff - leap_correction, tp))
tp->tm_sec += leap_extra_secs;
else
tp = NULL;
}
__libc_lock_unlock (tzset_lock);
return tp;
}
问题延伸:
最开始同事解决的方案是自定义了localtime,规避了上述问题,但又发现执行system时候也有概率产生死锁,原理实际和localtime类似,system也会使用全局锁。
以此类推,glibc应该存在很多函数都有上述场景的死锁问题,建议大家在类似场景使用的时候了解清楚实现机制,避免死锁问题。
解决方案:
对于上述场景,将fork改为vfork就可以解决glibc存在的一些列死锁问题,vfork的子进程与父进程之间共享全局变量,这样父进程解锁后可以同步给子进程,子进程就不会存在死锁问题。