最近的项目开发中,频繁遇到了时间戳相关的问题,如时间回退至1970年、时区错误及时间同步不准确等。鉴于此前仅对时间接口的使用有所了解而未深入探究其原理,本篇文章进行一次系统性整理,以便后续参考。文章若存在一些错误,可在留言区明确指出。
注:文末提供本文源码获取方式。文章不定时更新,喜欢本公众号系列文章,可以星标公众号,避免遗漏干货文章。源码开源,如果对您有帮助,帮忙分享、点赞加收藏喔!
Linux 中的时间形式主要以两种形式呈现:
进程时间
即进程消耗的时间,包含用户空间代码运行的时间和在内核在该进程消耗的时间(不包括进程被挂起或停止的时间)。单调时间
是一种始终递增的时间计数器,不受系统时钟调整的影响,常用于计算程序内部的持续时间。GMT(Greenwich Mean Time 格林威治时间)
基于英国伦敦附近的格林尼治天文台的本初子午线的标准时间UTC(Universal Time Coordinated 世界标准时间)
一种国际标准时间,与GMT几乎相同,但更精确,用于避免地球自转速度变化带来的影响本地时间
根据用户所在地理位置所采用的时间,会随地理位置的不同而有所差异,同时也会受到夏令时等因素的影响 时间编程中常用要用到的时间结构体有time_t
、timeval
、timespec
、tm
。《Unix环境高级编程》中一张图准确的反应出time_t
和tm
之间的关系:
时间函数之间的关系
time_t
:最简单的数据湖结构,表示从1970年1月1日00:00:00
UTC到现在的秒数。tm
:包含日期和时间的具体组成部分(年、月、日、时、分、秒等),通常由time_t
转换而来,用于显示或解析时间。timeval
:微秒级精度,包含秒(tv_sec)和微秒(tv_usec)。timespec
:纳秒级精度,包含秒(tv_sec)和纳秒(tv_nsec)。clock_t
:表示程序执行过程中消耗的CPU时间,单位是CLOCKS_PER_SEC
。time_t time(time_t *tloc)
;1970年1月1日00:00:00 UTC
以来的秒数。如果tloc
不是NULL
,则返回的时间值也会存储在tloc
指向的位置。(time_t)(-1)
。int gettimeofday(struct timeval *tv, struct timezone *tz)
;time()
更高的精度,可以获取当前时间精确到微秒。struct timeval
包含两个成员:tv_sec
(秒数)和tv_usec
(微秒数)。struct timezone
已经废弃,通常传入NULL
。0
,出错时返回-1
,并设置errno
。CLOCK_REALTIME
描述:系统实时钟,反映当前的实际时间。
特点:受系统时间调整的影响。CLOCK_MONOTONIC
描述:单调时钟,从某个未指定的起点开始计时。
特点:不受系统时间调整的影响,适合用于测量时间间隔。CLOCK_PROCESS_CPUTIME_ID
描述:当前进程的CPU时间。
特点:包括用户态和内核态的CPU时间。CLOCK_MONOTONIC_RAW
(可选)
描述:高精度单调时钟,不受系统时间调整的影响。
特点:提供更高的时间分辨率。CLOCK_REALTIME_COARSE
(可选)
描述:较低精度的系统实时钟。
特点:速度快,但精度较低。CLOCK_MONOTONIC_COARSE
(可选)
描述:较低精度的单调时钟。
特点:速度快,但精度较低。int clock_gettime(clockid_t clk_id, struct timespec *tp)
;struct timespec
包含两个成员:tv_sec
(秒数)和tv_nsec
(纳秒数)。clk_id
参数指定了要查询的时间源(带有“可选”指并非所有系统都必须支持):0
,出错时返回-1
,并设置errno
。clock_t times(struct tms *buf)
;struct tms
包含四个成员:tms_utime
(用户态运行时间)、tms_stime
(内核态运行时间)、tms_cutime
(子进程用户态运行时间)、tms_cstime
(子进程内核态运行时间),所有时间都以时钟滴答数(clock ticks
)表示。-1L
。int stime(const time_t *t)
;t
是一个指向time_t
类型变量的指针,该变量包含了自1970年1月1日00:00:00
UTC以来的秒数。0
,失败时返回-1
,并设置errno
。stime()
函数通常需要root权限才能执行,且至Linux 2.6.x
之后版本不推荐使用,本地glibc 2.35
实测已无法编译此函数。int settimeofday(const struct timeval *tv, const struct timezone *tz)
;tv
指向一个struct timeval
结构,该结构包含了秒数和微秒数,用来表示新的系统时间。tz
指向一个struct timezone
结构,该结构包含了分钟偏移量和夏令时标志位,不过在现代系统中,通常不需要设置时区信息,因此可以传递NULL
。0
,失败时返回-1
,并设置errno
。stime()
类似,settimeofday()
也需要适当的权限才能改变系统时间。int clock_settime(clockid_t clk_id, const struct timespec *tp)
;clk_id
标识的时钟。tp
指向一个struct timespec
结构,该结构包含了秒数和纳秒数,可以用来非常精确地设置时间。通常只允许设置时间源CLOCK_REALTIME
(系统实时钟)。0
,失败时返回-1
,并设置errno
。root
权限,而其他类型的时钟通常不允许设置。char *asctime(const struct tm *timeptr); / char *asctime_r(const struct tm *timeptr, char *buf)
;struct tm
结构转换为字符串格式,格式为 "Sun Sep 16 01:03:52 1979\n
"。asctime_r
是线程安全版本。asctime
返回的字符串是静态分配的,多次调用会覆盖前一次的结果。time_t mktime(struct tm *timeptr)
;struct tm
结构转换为time_t
类型的时间值。time_t
类型的时间值,失败时返回(time_t)(-1)
。mktime
可能会修改传入的struct tm
结构中的某些字段。char *ctime(const time_t *timep); / char *ctime_r(const time_t *timep, char *buf)
;time_t
类型的时间值转换为字符串格式,格式为 "Sun Sep 16 01:03:52 1979\n
"。ctime_r
是线程安全版本。ctime
返回的字符串是静态分配的,多次调用会覆盖前一次的结果。struct tm *gmtime(const time_t *timep); / struct tm *gmtime_r(const time_t *timep, struct tm *result)
;time_t
类型的时间值转换为 UTC 时间的struct tm
结构。gmtime_r
是线程安全版本。struct tm
结构的指针,失败时返回NULL
。gmtime
返回的struct tm
结构是静态分配的,多次调用会覆盖前一次的结果。struct tm *localtime(const time_t *timep); / struct tm *localtime_r(const time_t *timep, struct tm *result)
;time_t
类型的时间值转换为本地时间的struct tm
结构。localtime_r
是线程安全版本。struct tm
结构的指针,失败时返回NULL
。localtime
返回的struct tm
结构是静态分配的,多次调用会覆盖前一次的结果。double difftime(time_t time1, time_t time0)
;time_t
类型的时间值之间的差值,以秒为单位。size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)
;format
将struct tm
结构转换为字符串,并存储在str
中。最多写入maxsize
个字符(包括终止符\0
)。\0
),如果缓冲区太小无法容纳结果,则返回0
。str
足够大,以避免溢出。 时区会影响到本地时间与UTC
时间之间的转换(即本地时间 = UTC + 时区)。 查阅了一些文档,目前Ubuntu上时区记录在路径/etc/localtime
,其通常为软链接,指向具体的时区文件,例如/etc/localtime -> /usr/share/zoneinfo/Asia/Shanghai
。通过修改/etc/localtime
指向即可修改为对应的时区(/etc/timezone
也会记录当前时区,但似乎仅用于显示)。
time
void TestGetTime()
{
// time UTC时间戳
time_t tmt1 = time(NULL);
printf("timestamp : %ld\n", tmt1);
// ctime_r UTC时间戳转换为本地时间字符串
char cbuf[50] = {0};
ctime_r(&tmt1, cbuf);
printf("ctime_r : %ld(%6d) %s", tmt1, 0, cbuf);
// gmtime_r UTC时间戳转换为UTC时间字符串
tm gtm;
time_t tmt2;
char gbuf[50] = {0};
gmtime_r(&tmt1, >m);
asctime_r(>m, gbuf);
tmt2 = mktime(>m); // mktime 会自动减时区
printf("gmtime_r : %ld(%6ld) %s %s", tmt2, tmt2-tmt1, gtm.tm_zone, gbuf);
// 将时间戳转换为本地时间
tm ltm;
time_t tmt3;
char lbuf[50] = {0};
localtime_r(&tmt1, <m);
asctime_r(<m, lbuf);
tmt3 = mktime(<m);
printf("localtime_r: %ld(%6ld) %s %s", tmt3, tmt3-tmt1, ltm.tm_zone, lbuf);
char buf3[50] = {0};
strftime(buf3, 50, "%Z %a %b %d %H:%M:%S %Y", <m);
printf("strftime : %ld(%6ld) %s\n", tmt3, tmt3-tmt1, buf3);
}
timestamp : 1732450363
ctime_r : 1732450363( 0) Sun Nov 24 20:12:43 2024
gmtime_r : 1732421563(-28800) CST Sun Nov 24 12:12:43 2024
localtime_r: 1732450363( 0) CST Sun Nov 24 20:12:43 2024
strftime : 1732450363( 0) CST Sun Nov 24 20:12:43 2024
gmtime_r
打印的是UTC时间戳,与本地时间相差28800s (8h)
,即本地与UTC时间相差8h
。
void Testgettimeofday()
{
struct timeval tv;
gettimeofday(&tv, NULL);
printf("tv_sec: %ld, tv_usec: %ld\n", (long)tv.tv_sec, (long)tv.tv_usec);
}
void Testsettimeofday()
{
Testgettimeofday();
struct timeval tv1;
tv1.tv_sec = 1731985300;
tv1.tv_usec = 100;
int ret = settimeofday(&tv1, NULL);
if (ret == -1) {
perror("settimeofday");
}
Testgettimeofday();
}
tv_sec: 1732450828, tv_usec: 890873
tv_sec: 1731985300, tv_usec: 150
注意在调用设置时间接口时,需要root权限执行,否则会设置失败。
void Testclock_gettime()
{
std::string name[] = {
"CLOCK_REALTIME",
"CLOCK_MONOTONIC",
"CLOCK_PROCESS_CPUTIME_ID",
"CLOCK_THREAD_CPUTIME_ID",
"CLOCK_MONOTONIC_RAW",
"CLOCK_REALTIME_COARSE",
"CLOCK_MONOTONIC_COARSE",
"CLOCK_BOOTTIME",
"CLOCK_REALTIME_ALARM",
"CLOCK_BOOTTIME_ALARM",
};
// printf("Test clock_gettime\n");
printf("%-25s %10s %10s\n", "CLOCK TYPE", "SEC", "NSEC");
printf("-----------------------------------------------------------------------------\n");
for (int i = 0; i <= CLOCK_BOOTTIME_ALARM; i++) {
struct timespec ts;
clock_gettime(i, &ts);
printf("%-25s: %10ld, %10ld\n", name[i].c_str(), (long)ts.tv_sec, (long)ts.tv_nsec);
}
printf("-----------------------------------------------------------------------------\n");
}
void Testclock_settime()
{
Testclock_gettime();
// Only CLOCK_REALTIME is allowed to be set
struct timespec ts1;
ts1.tv_sec = 1731985300;
ts1.tv_nsec = 100;
int ret = clock_settime(CLOCK_REALTIME, &ts1);
if (ret == -1) {
perror("clock_settime");
}
Testclock_gettime();
}
CLOCK TYPE SEC NSEC
-----------------------------------------------------------------------------
CLOCK_REALTIME : 1732451153, 160842537
CLOCK_MONOTONIC : 45250, 516265743
CLOCK_PROCESS_CPUTIME_ID : 0, 908800
CLOCK_THREAD_CPUTIME_ID : 0, 910400
CLOCK_MONOTONIC_RAW : 45249, 35729391
CLOCK_REALTIME_COARSE : 1732451153, 145187465
CLOCK_MONOTONIC_COARSE : 45250, 500594052
CLOCK_BOOTTIME : 45250, 516287258
CLOCK_REALTIME_ALARM : 1732451153, 160881972
CLOCK_BOOTTIME_ALARM : 45250, 516289642
-----------------------------------------------------------------------------
CLOCK TYPE SEC NSEC
-----------------------------------------------------------------------------
CLOCK_REALTIME : 1731985300, 32053
CLOCK_MONOTONIC : 45250, 516347493
CLOCK_PROCESS_CPUTIME_ID : 0, 988400
CLOCK_THREAD_CPUTIME_ID : 0, 989400
CLOCK_MONOTONIC_RAW : 45249, 35795309
CLOCK_REALTIME_COARSE : 1731985300, 100
CLOCK_MONOTONIC_COARSE : 45250, 516314680
CLOCK_BOOTTIME : 45250, 516352354
CLOCK_REALTIME_ALARM : 1731985300, 38528
CLOCK_BOOTTIME_ALARM : 45250, 516353885
-----------------------------------------------------------------------------
从测试结果看,更改系统时间时,仅有时间源CLOCK_REALTIME
、CLOCK_REALTIME_ALARM
会随之修改而跳变,其他时间源不会随着系统时间的修改而跳变。在了解这些特性后,在编写应用程序时选择合适的时间源,以满足不同的需求。
void TestTimeWithSleep(int sec)
{
std::string name[] = {
"CLOCK_REALTIME",
"CLOCK_MONOTONIC",
"CLOCK_PROCESS_CPUTIME_ID",
"CLOCK_THREAD_CPUTIME_ID",
"CLOCK_MONOTONIC_RAW",
"CLOCK_REALTIME_COARSE",
"CLOCK_MONOTONIC_COARSE",
"CLOCK_BOOTTIME",
"CLOCK_REALTIME_ALARM",
"CLOCK_BOOTTIME_ALARM",
};
struct timespec ots[10];
for (int i = 0; i < 10; i++) {
clock_gettime(i, &ots[i]);
}
sleep(sec);
struct timespec nts[10];
for (int j = 0; j < 10; j++) {
clock_gettime(j, &nts[j]);
}
printf("%-25s %10s %10s %10s %10s %7s %8s\n", "CLOCK TYPE", "OLDSEC", "OLDNSEC", "NEWSEC", "NEWNSEC", "DIFFSEC", "DIFFNSEC");
printf("-------------------------------------------------------------------------------------------\n");
for (int i = 0; i <= CLOCK_BOOTTIME_ALARM; i++) {
printf("%-25s: %10ld %10ld %10ld %10ld %7ld %8ld\n",
name[i].c_str(), (long)ots[i].tv_sec, (long)ots[i].tv_nsec,
(long)nts[i].tv_sec, (long)nts[i].tv_nsec, (long)(nts[i].tv_sec - ots[i].tv_sec), (long)(nts[i].tv_nsec - ots[i].tv_nsec));
}
}
sleep 5s 结果如下:
CLOCK TYPE OLDSEC OLDNSEC NEWSEC NEWNSEC DIFFSEC DIFFNSEC
-------------------------------------------------------------------------------------------
CLOCK_REALTIME : 1732451618 944834581 1732451623 945683524 5 848943
CLOCK_MONOTONIC : 45716 300258307 45721 301107230 5 848923
CLOCK_PROCESS_CPUTIME_ID : 0 1010700 0 1048800 0 38100
CLOCK_THREAD_CPUTIME_ID : 0 1011000 0 1050000 0 39000
CLOCK_MONOTONIC_RAW : 45714 819871428 45719 820723025 5 851597
CLOCK_REALTIME_COARSE : 1732451618 935986823 1732451623 935984705 5 -2118
CLOCK_MONOTONIC_COARSE : 45716 291410495 45721 291408377 5 -2118
CLOCK_BOOTTIME : 45716 300260726 45721 301110251 5 849525
CLOCK_REALTIME_ALARM : 1732451618 944837451 1732451623 945687049 5 849598
CLOCK_BOOTTIME_ALARM : 45716 300287520 45721 301111127 5 823607
从上述结果看,CLOCK_PROCESS_CPUTIME_ID
和CLOCK_THREAD_CPUTIME_ID
没有记录sleep 5s的时间,也应征了上述所描述的进程挂起或停止时,进程时间不会记录。
用times
接口验证会更明显,sleep
前后times
获取的时间值基本没有变化。
void TestSetTimeZone(const std::string& tz)
{
int ret = 0;
std::string target = "/usr/share/zoneinfo/" + tz;
ret = unlink("/etc/localtime");
if (ret == -1) {
perror("unlink");
}
ret = symlink(target.c_str(), "/etc/localtime");
if (ret == -1) {
perror("symlink");
return;
}
tzset();
TestGetTimeZone();
TestGetTime();
}
设置时区America/New_York
timestamp : 1732452775
ctime_r : 1732452775( 0) Sun Nov 24 07:52:55 2024
gmtime_r : 1732470775( 18000) EST Sun Nov 24 12:52:55 2024
localtime_r: 1732452775( 0) EST Sun Nov 24 07:52:55 2024
strftime : 1732452775( 0) EST Sun Nov 24 07:52:55 2024
通过打印可看出时区已经显示EST
,与Asia/Shanghai
时区相差了13h。
wait_for
会随着时间跳变而异常。尽管印象中,不应该这样,其依赖的应该是相对时间即单调时间。经过查阅相关资料,发现gcc版本和glibc版本对wait_for都有影响,gcc >=10 且 glibc >= 2.30 才会对程序行为没有影响。用心感悟,认真记录,写好每一篇文章,分享每一框干货。