版本依据
Glibc-2.33, Kernel-5.10.117
对于不需要时间同步的情况来说,在程序运行前 env 中设置好 TZ 的相关参数即可,这里主要记录存在 NTP 服务情况下的处理方式
问题?
在多进程下,NTP 由另一个应用的时间服务进行托管调用,经过多次调用后发现,只有托管的应用的时间处于正常的时区,其他的时间虽然进行了同步但是时区未同步,后来查询源码后发现,setenv()⚡ 和 puttenv()⚡ 两个函数在 glibc 中实际上调用的是 __add_to_environ()⚡ ,而这个函数是对应用空间中的全局变量 char **environ 进行操作,实际上并没有将时区相关的参数传递给内核:
flowchart TB
f_putenv("putenv()")
f_setenv("setenv()")
f_add_to_environ("__add_to_environ()")
environ("char** environ")
f_putenv --> f_add_to_environ
f_setenv --> f_add_to_environ
f_add_to_environ --> environ
所以无论怎么修改,都不会影响到其他程序的时区
尝试解决
那应该如何让其他程序时区发生改变呢?后来翻了下 Ubuntu 的 timedate 设置时区 method_set_timezone()⚡ 的源码,发现大致的流程如下:
static int method_set_timezone(sd_bus_message *m, void *userdata, sd_bus_error *error) { |
第一步的 context_write_data_timezone()⚡ 是在 /usr/share/zoneinfo/UTC 下选择对应的时区文件创建对应的软链接到 /etc/localtime ,然后通过 tzset() 同步自身应用的时区,最后通过clock_set_timezone()⚡ 中调用 settimeofday() 来同步给内核时区:
int clock_set_timezone(int *ret_minutesdelta) { |
然而移植这段代码后发现,其他进程并没有同步时区,然后我使用 gettimeofday() 拿到 timezone 中的 tz_minuteswest 进行比对发现,无论是自身设置过的应用还是其他应用均返回为 0,说明并没有设置成功,但设置却未返回异常。随后我看了下 glibc 中的实现,发现 gettimeofday() 的实际现实流程如下:
flowchart TB
gettimeofday("gettimeofday()")
__gettimeofday("__gettimeofday()")
__gettimeofday64("__gettimeofday64()")
zmem("memset(tz, 0, sizeof(struct timezone))")
__clock_gettime64("__clock_gettime64()")
sys_call("INLINE_SYSCALL_CALL(clock_gettime(64))")
gettimeofday --> __gettimeofday --> __gettimeofday64 --> zmem & __clock_gettime64
__clock_gettime64 --> sys_call
而 settimeofday() 的实际现实流程如下:
flowchart TB
settimeofday("settimeofday()")
__settimeofday("__settimeofday()")
__settimeofday64("__settimeofday64()")
sys_call_1("INLINE_SYSCALL_CALL(settimeofday(tz))")
__clock_settime64("__clock_settime64()")
sys_call_2("INLINE_SYSCALL_CALL(clock_settime(64))")
settimeofday --> __settimeofday --> __settimeofday64
__settimeofday64 --> judge{tz != 0\nAND\ntv != 0}
judge -->|true| sys_call_1
judge --->|false| inval[INVAL]
__settimeofday64 --> __clock_settime64 --> sys_call_2
由于 glibc 动态库由原厂编译并提供,为了进一步验证我的判断,我在使用 settimeofday() 之后通过库函数以及系统调用输出内核的时区设置:
void test_settimeofday() |
在运行后发现,发现确实能通过系统回调拿到之前设置的时区值
探索内核源码
虽然上面的个方式证明了 settimeofday() 成功修改了内核时区,但是多进程的时区依然未同步,在阅读内核中的 settimeofday() 源码后有所发现,源码如下:
int do_sys_settimeofday64(const struct timespec64 *tv, const struct timezone *tz) |
进一步查看 timekeeping_warp_clock() 源码:
/* |
其中注释里面已经给出了解决方案,使用这个方式是更倾向于将系统时间置为 UTC 时间,如果多次修改反而会带来精度损失,而且 timekeeping_inject_offset() 中有锁来保证执行的正确性,所以如果有多个应用频繁调用的话也会带来性能上的损失,因此 do_sys_settimeofday64() 中保证这个操作只执行一次也是如此
因此最好的解决方案是保持系统的时间为 UTC 时间,通过修改应用的 TZ 属性和 tzset() 来修改进程的时区设置
解决方案
有两种解决方案:
1. 完善 /etc/localtime 并替换 localtime_r() 为 localtime()
查看下 Linux 下的 /etc/localtime 就不难发现其实是个软连接:
z@z:~$ ls -lh /etc/localtime |
只需要将 /usr/share/zoneinfo/ 复制到开发板上并链接到 /etc/localtime 即可,为什么可以这样?这个其实是由于 tzset() 的代码所决定:
/* Interpret the TZ envariable. */ |
TZDEFAULT 是一个宏定义,赋值在 glibc/time/Makefile 和 glibc/timezone/Makefile ,定义在 glibc/Makeconfig,通常情况下一般定义在 /etc/localtime ,有自定义需求则需要查看下定义的路径在哪
localtime() 和 localtime_r() 的区别
两者底层的调用均是 __tz_convert()⚡,主要区别在于传递的 tp 不一样,localtime() 传递的是一个全局变量的 _tmbuf,而 localtime_r() 则是由调用者传入:
/* The C Standard says that localtime and gmtime return the same pointer. */ |
而 __tz_convert() 正是通过判断 tp 来判断是否读取环境和时区文件并进行进一步解析:
/* Return the `struct tm' representation of TIMER in the local timezone. |
而 localtime_r() 就需要调用者手动调用 tzset() 来进一步控制时区:
// 设置TZ |
有什么缺点?
关键在于 localtime() ,它在未读取到 TZ 变量时会去读取配置文件进行调整时区,以至于在每次调用的时候均会进行这样的操作,导致了不必要的开销,这也是为什么 spdlog 调用的是 localtime_r() 而不是它。那么由此可以引入第二套方案
2. 采用 IPC 方式动态设置应用时区的环境变量
其实很简单,在拥有 IPC 中间件的基础上只需要传递对应的数据再通过上面的 tzset() 的流程就可以了