一、简介
前段时间,线上的redis实例出现了一些异常的情况,具体变现就是bgsave子进程hang住了,从而引发了后续的很多问题,通过排查最终发现是localtime
相关函数引起的,这里做一下总结记录。
C 库函数 *struct tm *localtime(const time_t timer) 作用是根据本地时区信息将 time 函数获取的 UTC 时间调整为为本地时间,并将具体的时间信息填充到tm结构体之中;
二、详细介绍
由于localtime函数的具体底层实现的缘由,在某些场景下会触发localtime函数导致的死锁问题,这里详细的分析原因以及后续的处理方案;
2.1 底层实现分析
localtime
函数底层的调用栈信息为:
localtime() => __tz_convert() |
localtime
函数的底层代码实现(代码位于./time/localtime.c
):
/* Return the `struct tm' representation of *T in local time. */ |
__tz_convert
函数的底层代码实现(代码位于./time/tzset.c
):
/* Return the `struct tm' representation of *TIMER in the local timezone. |
解释分析:
__tz_convert
使用的是一个static
变量的tzset_lock
全局锁;
- 线程安全?:由于使用直接返回的是一个全局变量,这里并不是线程安全的;
- 信号安全?:
localtime
与localtime_r
这两个函数都不是信号安全的,如果在信号处理函数中使用,就要考虑到死锁的情况,比如,程序调用localtime_r
,加锁后信号发生,信号处理函数中也调用localtime_r
的话,会因为获取不到锁所以一直阻塞;
对于线上资源出现异常的分析
对于redis类资源,本身存在一个主线程以及一些bio线程,对于bio线程来说,输出日志是很常见的事情,但是存在这么一种场景,当父进程中其中一个bio线程正在输出日志,此时fork了一个子进程开始执行bgsave,由于子进程会继承父进程的锁,所以对于子进程来说,当它尝试输出有一些日志的信息,就会由于已经拥有一个锁而导致出现死锁的情况,进而导致子进程会出现hang住的情况。
2.2 解决方案
使用全局变量的方式timezone
以及daylight
的相关信息,避免调用localtime
函数;
方案参考(redis的官方解决方案):
|
三、相关函数解析
3.1 time()
time
函数会返回从公元 1970 年1 月1 日的UTC 时间
从0 时0 分0 秒算起到现在所经过的秒数。如果 t 并非空指针的话,此函数也会将返回值存到t 指针所指的内存;
函数声明:time_t time(time_t *t)
返回值:成功则返回秒数,失败则返回((time_t)-1)值,错误原因存于errno 中;
3.2 gettimeofday()
gettimeofday()
函数会将目前的时间存储在 tv 所指的结构中,将当地时区的信息则放到 tz 所指的结构中并返回;
函数声明:int gettimeofday (struct timeval * tv, struct timezone * tz)
结构体定义:
struct timeval { |
tz_dsttime
所代表的状态信息为:
- DST_NONE //不使用
- DST_USA //美国
- DST_AUST //澳洲
- DST_WET //西欧
- DST_MET //中欧
- DST_EET //东欧
- DST_CAN //加拿大
- DST_GB //大不列颠
- DST_RUM //罗马尼亚
- DST_TUR //土耳其
- DST_AUSTALT //澳洲(1986 年以后)
返回值:成功则返回0,失败返回-1,错误代码存于errno;
3.3 setlocale()
C 库函数 - *char *setlocale(int category, const char locale) 设置或读取地域化信息;
函数声明:char *setlocale(int category, const char *locale)
- category – 这是一个已命名的常量,指定了受区域设置影响的函数类别;
- LC_ALL 包括下面的所有选项;
- LC_COLLATE 字符串比较。参见 strcoll();
- LC_CTYPE 字符分类和转换。例如 strtoupper();
- LC_MONETARY 货币格式,针对 localeconv();
- LC_NUMERIC 小数点分隔符,针对 localeconv();
- LC_TIME 日期和时间格式,针对 strftime();
- LC_MESSAGES 系统响应;
- locale – 如果 locale 是 NULL 或空字符串 “”,则区域名称将根据环境变量值来设置,其名称与上述的类别名称相同;
返回值:如果成功调用 setlocale(),则返回一个对应于区域设置的不透明的字符串。如果请求无效,则返回值是 NULL
3.4 tzset()
C 库函数 - *char *setlocale(int category, const char locale) 设置或读取地域化信息,tzset
函数在实现的时候是通过内部的tzset_internal
函数来完成的;
调用方式:
- 显式调用:直接执行
tzset
函数。显式调用内部的tzset_internal
函数,强制tzset
不管何种情况一律重新加载TZ
信息或者/etc/localtime
信息; - 隐式调用:执行
localtime
的时候会隐式调用tzset
函数。隐式调用内部的tzset_internal
函数,只有在TZ
发生变化,或者加载文件名发生变化的时候才会再次加载时区信息【如果只是/etc/localtime
的内容发生了变化,而文件名/etc/localtime
没有变化,则不会再次加载时区信息,导致localtime
函数调用仍然以老时区转换UTC时间到本地时间】;