版本依据
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()
的流程就可以了