Keep learning, keep living...

0%

Glibc mktime函数时区信息分析

在程序中, 时间一般有两种表示方法:

  • UNIX时间戳(UNIX timestamp): 表示的是从UTC时间1970年1月1日0时0分0秒起至现在的总秒数, 它也叫做epochUNIX时间,POSIX时间等等。在同一时刻,全球所有地方的UNIX时间戳都相同。

  • 本地时间: 是以人类可读的格式表示的时间,比如2022-9-24 00:00:00。由于时区概念的存在,在同一UNIX时间戳所表示的时间点,各时区的本地时间是不同的,如下图:

CST(China Standard Time)时区中时间为2022-09-24 00:00:00的时刻,UTC时间为2022-09-23 16:00:00

支持国际化的程序往往将时间值存储为UNIX时间戳, 使用时在两种格式之间进行转换。

C语言程序可以使用glibcmktimelocaltime函数在两种格式之间进行转换。timelocalmktime功能一致。

mktime是将本地时间转换为UNIX时间戳,所以它所返回的时间戳是和本地时区有关系的。

下面看一下mktime的具体实现是如何使用时区信息的。

以下源码来源于CentOS 7.8.2003glibc-2.17-307.el7.1.src.rpm

mktime的实现位于glibc源码的time/mktime.c文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Convert *TP to a time_t value.  */
time_t
mktime (struct tm *tp)
{
#ifdef _LIBC
/* POSIX.1 8.1.1 requires that whenever mktime() is called, the
time zone names contained in the external variable 'tzname' shall
be set as if the tzset() function had been called. */
__tzset ();
#endif

return __mktime_internal (tp, __localtime_r, &localtime_offset);
}

#ifdef weak_alias
weak_alias (mktime, timelocal)
#endif

可以看到,mktime会自动调用__tzset来处理时区信息。

从代码中也可以看到timelocal就是mktime的弱引用,是相同的函数。

__tzset实现位于time/tzset.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void
__tzset (void)
{
__libc_lock_lock (tzset_lock);

tzset_internal (1, 1);

if (!__use_tzfile)
{
/* Set `tzname'. */
__tzname[0] = (char *) tz_rules[0].name;
__tzname[1] = (char *) tz_rules[1].name;
}

__libc_lock_unlock (tzset_lock);
}
weak_alias (__tzset, tzset)

函数调用tzset_internal来处理TZ环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/* Interpret the TZ envariable.  */
static void
internal_function
tzset_internal (int always, int explicit)
{
static int is_initialized;
const char *tz;

if (is_initialized && !always)
return;
is_initialized = 1;

/* Examine the TZ environment variable. */
tz = getenv ("TZ");
if (tz == NULL && !explicit)
/* Use the site-wide default. This is a file name which means we
would not see changes to the file if we compare only the file
name for change. We want to notice file changes if tzset() has
been called explicitly. Leave TZ as NULL in this case. */
tz = TZDEFAULT;
if (tz && *tz == '\0')
/* User specified the empty string; use UTC explicitly. */
tz = "Universal";

/* A leading colon means "implementation defined syntax".
We ignore the colon and always use the same algorithm:
try a data file, and if none exists parse the 1003.1 syntax. */
if (tz && *tz == ':')
++tz;

/* Check whether the value changed since the last run. */
if (old_tz != NULL && tz != NULL && strcmp (tz, old_tz) == 0)
/* No change, simply return. */
return;

if (tz == NULL)
/* No user specification; use the site-wide default. */
tz = TZDEFAULT;

tz_rules[0].name = NULL;
tz_rules[1].name = NULL;

/* Save the value of `tz'. */
free (old_tz);
old_tz = tz ? __strdup (tz) : NULL;

/* Try to read a data file. */
__tzfile_read (tz, 0, NULL);
if (__use_tzfile)
return;

/* No data file found. Default to UTC if nothing specified. */

if (tz == NULL || *tz == '\0'
|| (TZDEFAULT != NULL && strcmp (tz, TZDEFAULT) == 0))
{
memset (tz_rules, '\0', sizeof tz_rules);
tz_rules[0].name = tz_rules[1].name = "UTC";
if (J0 != 0)
tz_rules[0].type = tz_rules[1].type = J0;
tz_rules[0].change = tz_rules[1].change = (time_t) -1;
update_vars ();
return;
}

__tzset_parse_tz (tz);
}

TZ环境变量的值为空("")时,使用Universal时区文件,表示UTC时间。

TZ环境变量支持的格式参考:

如果没有指定TZ变量,则使用TZDEFAULT

TZDEFAULTtimezone/tzfile.h中定义为localtime:

1
2
3
#ifndef TZDEFAULT
#define TZDEFAULT "localtime"
#endif /* !defined TZDEFAULT */

接着,调用__tzfile_read尝试去解析时区文件。

__tzfile_read定义在time/tzfile.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const char default_tzdir[] = TZDIR;

......

if (*file != '/')
{
const char *tzdir;

tzdir = getenv ("TZDIR");
if (tzdir == NULL || *tzdir == '\0')
tzdir = default_tzdir;
if (__asprintf (&new, "%s/%s", tzdir, file) == -1)
goto ret_free_transitions;
file = new;
}

当环境变量TZ值不是绝对路径地址,尝试使用环境变量TZDIR的值作为时区文件目录。如果没有值,则使用宏TZDIR

它定义在timezone/tzfile.h:

1
2
3
#ifndef TZDIR
#define TZDIR "/usr/local/etc/zoneinfo" /* Time zone object file directory */
#endif /* !defined TZDIR */

但实际上最终的目录不是这里,源码time/Makefile中指定了宏定义:

1
2
3
tz-cflags = -DTZDIR='"$(zonedir)"' \
-DTZDEFAULT='"$(localtime-file)"' \
-DTZDEFRULES='"$(posixrules-file)"'

zonedir定义在Makeconfig:

1
2
3
4
5
# Where to install the timezone data files (which are machine-independent).
ifndef zonedir
zonedir = $(datadir)/zoneinfo
endif
inst_zonedir = $(install_root)$(zonedir)
1
2
3
4
5
# Where to install machine-independent data files.
# These are the timezone database, and the locale database.
ifndef datadir
datadir = $(prefix)/share
endif

prefix是由glibc.specconfiguremake一路传递。

glibc.spec中使用的是%{_prefix}, 它的默认值为/usr, 因而默认的时区文件目录为/usr/share/zoneinfo

我的机器本地时区是CST:

1
2
3
4
5
6
7
8
9
[root@default ~]# timedatectl
Local time: Sat 2022-09-24 16:04:11 CST
Universal time: Sat 2022-09-24 08:04:11 UTC
RTC time: Sat 2022-09-24 08:04:09
Time zone: Asia/Shanghai (CST, +0800)
NTP enabled: yes
NTP synchronized: yes
RTC in local TZ: no
DST active: n/a

CST时间2022-09-24 16:00:00的时间戳为1664006400

如果想要转换非本地时区的时间,可以在程序中设置TZ环境变量,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <time.h>
#include <string.h>

int main(int argc, char *argv[]) {
struct tm tm;
time_t t1;

if (argc > 1) {
setenv("TZ", argv[1], 1);
}

tm.tm_year = 2022 - 1900;
tm.tm_mon = 9 - 1;
tm.tm_mday = 24;
tm.tm_hour = 16;
tm.tm_min = 0;
tm.tm_sec = 0;

t1 = mktime(&tm);

printf("timestamp: %d\n", t1);

return 0;
}

执行结果:

1
2
3
4
[root@default ~]# ./a.out
timestamp: 1664006400
[root@default ~]# ./a.out 'Asia/Tokyo'
timestamp: 1664002800

可以看到当把时区设置为东京时区,时间戳减少了3600,因为东京时区是东九区,而北京时间是东八区。

参考: