简单介绍下 Linux 中与时间相关的函数。
简介
现在通用的标准时间为 Coordinated Universal Time, UTC
,由世界上最精确的原子钟提供计时,而本地时间是 UTC+TimeZone
,也就是日常使用的时间。
另外,说到 UTC 就不得不提格林威治平均时 Greenwich Mean Time, GMT
,这是时区 0 的本地时间,也即是 GMT=UTC+0
,所以 GMT 和 UTC 时间值是相等的。
在 *nix
系统中,还有一个词 Epoch
,它指的是一个特定时间 1970-01-01 00:00:00 +0000 (UTC)
。
获取时间函数
简单介绍下与获取时间相关的系统调用。
time() 秒级
#include <time.h>
//----- time_t一般为long int,不过不同平台可能不同,打印可通过printf("%ju", (uintmax_t)ret)打印
time_t time(time_t *tloc);
char *ctime(const time_t *timep); // 错误返回NULL
char *ctime_r(const time_t *timep, char *buf); // buf至少26bytes,返回与buf相同
如果 tloc 不为 NULL
,那么数据同时会保存在 tloc 中,成功返回从 epoch 开始的时间,单位为秒;否则返回 -1
。
ftime() 毫秒级
#include <sys/timeb.h>
struct timeb {
time_t time; // 为1970-01-01至今的秒数
unsigned short millitm; // 毫秒
short timezonel; // 为目前时区和Greenwich相差的时间,单位为分钟,东区为负
short dstflag; // 非0代表启用夏时制
};
int ftime(struct timeb *tp);
总是返回 0 。
gettimeofday() 微秒级
gettimeofday()
函数可以获得当前系统的绝对时间。
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
int gettimeofday(struct timeval *tv, struct timezone *tz);
可以通过如下函数测试。
#include <stdio.h>
#include <sys/time.h>
int main(void)
{
int i = 30000000;
struct timeval begin, end, diff;
gettimeofday(&begin, NULL);
while (--i)
;
gettimeofday(&end, NULL);
timersub(&end, &begin, &diff);
printf("time: %ld.%06ld\n", diff.tv_sec, diff.tv_usec);
printf("time: %.6f\n", diff.tv_sec + diff.tv_usec / 1e6);
return 0;
}
clock_gettime() 纳秒级
编译连接时需要加上 -lrt
,不过不加也可以编译,应该是非实时的。struct timespect *tp
用来存储当前的时间,其结构和函数声明如下,返回 0 表示成功,-1 表示失败。
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
int clock_gettime(clockid_t clk_id, struct timespect *tp);
clk_id 用于指定计时时钟的类型,简单列举如下几种,详见 man 2 clock_gettime
。
- CLOCK_REALTIME/CLOCK_REALTIME_COARSE
系统实时时间 (wall-clock time),即从UTC 1970.01.01 00:00:00
开始计时的秒数,随系统实时时间改变而改变,包括通过系统函数手动调整系统时间,例如settime()
、settimeofday()
,或者通过adjtime()
、adjtimex()
或者 NTP 调整时间。 - CLOCK_MONOTONIC/CLOCK_MONOTONIC_COARSE
从系统启动开始计时,不受系统时间被用户改变的影响,但会受像adjtime()
或者 NTP 之类渐进调整的影响。 - CLOCK_MONOTONIC_RAW
与上述的CLOCK_MONOTONIC
相同,只是不会受adjtime()
以及 NTP 的影响。 - CLOCK_PROCESS_CPUTIME_ID
本进程到当前代码系统 CPU 花费的时间。 - CLOCK_THREAD_CPUTIME_ID
本线程到当前代码系统 CPU 花费的时间。
可以通过如下程序测试。
#include <time.h>
#include <stdio.h>
struct timespec diff(struct timespec start, struct timespec end)
{
struct timespec temp;
if ((end.tv_nsec-start.tv_nsec)<0) {
temp.tv_sec = end.tv_sec-start.tv_sec-1;
temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec;
} else {
temp.tv_sec = end.tv_sec-start.tv_sec;
temp.tv_nsec = end.tv_nsec-start.tv_nsec;
}
return temp;
}
int main(void)
{
int temp, i;
struct timespec t, begin, end;
clock_gettime(CLOCK_REALTIME, &t);
printf("CLOCK_REALTIME: %d, %d\n", t.tv_sec, t.tv_nsec);
clock_gettime(CLOCK_MONOTONIC, &t);
printf("CLOCK_MONOTONIC: %d, %d\n", t.tv_sec, t.tv_nsec);
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t);
printf("CLOCK_THREAD_CPUTIME_ID: %d, %d\n", t.tv_sec, t.tv_nsec);
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &begin);
printf("CLOCK_PROCESS_CPUTIME_ID: %d, %d\n", begin.tv_sec, begin.tv_nsec);
for (i = 0; i < 242000000; i++)
temp+=temp;
clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &end);
t = diff(begin, end);
printf("diff CLOCK_PROCESS_CPUTIME_ID: %d, %d\n", t.tv_sec, t.tv_nsec);
return 0;
}
时间转换相关
函数 mktime()
将 timeptr
所指向的结构转换为一个依据本地时区的 time_t
值,也就是自 1970.01.01
以来逝去时间的秒数,当发生错误时返回 -1
。
注意,同时会更新其入参,包括了 tm_wday
tm_isdst
等参数。其中比较关键的是夏令时的设置,其入参的影响如下:
0
完全不考虑夏令时的问题,直接将日历时间转换为秒数;1
考虑夏令时。-1
自动根据时区信息进行判断。
建议使用 -1
如下是测试。
struct tm {
int tm_sec; /* seconds [0, 59] */
int tm_min; /* minutes [0, 59] */
int tm_hour; /* hours [0, 23] */
int tm_mday; /* day of the month [1, 31] */
int tm_mon; /* month [0, 11] */
int tm_year; /* year now.year - 1900 */
int tm_wday; /* day of the week, [0, 6] 0->sunday */
int tm_yday; /* day in the year [0, 365] */
int tm_isdst; /* daylight saving time */
};
time_t mktime(struct tm *timeptr);
如下是简单的测试用例,首先参考 Linux 时间基本概念 中 DST 的介绍,将时区设置为 CET 时区。
#define _XOPEN_SOURCE
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#define log_info(...) do { printf(" info: " __VA_ARGS__); putchar('\n'); } while(0);
static void dump_tm(const struct tm *t, const char *var)
{
log_info("---> dump <struct tm> %s", var);
log_info(" %04d %02d %02d %02d:%02d:%02d",
t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
log_info(" tm_wday: %02d", t->tm_wday);
log_info(" tm_yday: %02d", t->tm_yday);
log_info(" tm_isdst: %02d", t->tm_isdst);
}
int main(void)
{
time_t dstt;
struct tm dst_tm;
strptime("2016-03-27 01:59:58", "%Y-%m-%d %H:%M:%S", &dst_tm);
dst_tm.tm_isdst = -1;
dstt = mktime(&dst_tm);
log_info("isdst == -1, result %ld", dstt);
dump_tm(&dst_tm, "local");
strptime("2016-03-27 02:00:01", "%Y-%m-%d %H:%M:%S", &dst_tm);
dst_tm.tm_isdst = -1;
dstt = mktime(&dst_tm);
log_info("isdst == -1, result %ld", dstt);
dump_tm(&dst_tm, "local");
strptime("2016-03-27 03:00:00", "%Y-%m-%d %H:%M:%S", &dst_tm);
dst_tm.tm_isdst = -1;
dstt = mktime(&dst_tm);
log_info("isdst == -1, result %ld", dstt);
dump_tm(&dst_tm, "local");
return 0;
}
可以看到,对于 2016-03-27 02:00:01
这种的非法值,mktime()
会将其修改为合法的时间,而返回的时间戳是没有发生跳变的。
注意,使用 strptime()
前,需要将 struct tm
结构体清空,否则不需要设置的字段 (例如 tm_isdst
) 会有脏数据。
时间转换
mktime()
localtime()
会根据时间戳或者年月日来更新星期情况,也就是当天是星期几。
所以,如果要通过当前日期获取到下个的星期 N 的信息,那么可以直接通过星期计算偏移,然后添加到日期中,即使超过了当月天数的限制,mktime()
也能够正确计算。
可以通过 man mktime
查看帮助文档,也就是说 mktime()
会忽略 tm_wday
以及 tm_yday
字段,会使用 tm_isdst
判断是否采用夏令时,同时会根据其它字段来修改 tm_wday
tm_yday
字段,同时其它字段如果超过了范围则会修正。
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#define log_info(...) do { printf(" info: " __VA_ARGS__); putchar('\n'); } while(0);
static void dump_tm(const struct tm *t, const char *var)
{
log_info("---> dump <struct tm> %s", var);
log_info(" %04d %02d %02d %02d:%02d:%02d",
t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
log_info(" tm_wday: %02d", t->tm_wday);
log_info(" tm_yday: %02d", t->tm_yday);
log_info(" tm_isdst: %02d", t->tm_isdst);
}
int main(void)
{
/*
* 2017-11-29 16:34:56 Wed 1511944496
* 2017-12-02 16:34:56 Tus 1512203696
*/
time_t ot, tt;
struct tm otm, ttm;
ot = 1511944496;
localtime_r(&ot, &ttm); /* it's Wednesday */
ttm.tm_wday = 1; /* change to Monday */
tt = mktime(&ttm); /* this will change to Wednesday */
dump_tm(&ttm, "correct");
ttm.tm_mday += 6 - ttm.tm_wday;
tt = mktime(&ttm);
dump_tm(&ttm, "correct");
/*
* 闰年
* 2014-02-27 16:34:56 Thu 1393490096
* 2014-03-04 16:34:56 Tus 1393922096
*/
ot = 1393490096;
localtime_r(&ot, &ttm);
if (ttm.tm_wday > 2)
ttm.tm_mday += ((6 - ttm.tm_wday) + 2 + 1); /* add Sunday */
else
ttm.tm_mday += 2 - ttm.tm_wday;
tt = mktime(&ttm);
dump_tm(&ttm, "correct");
/*
* 闰秒
* 2015-06-30 23:30:00 Thu 1435678200
* 2015-07-01 23:30:00 Thu 1435764600
*/
/*
* 夏令时开始
* 2016-03-27 01:58:00 Thu 1435678200
* 2016-03-27 03:58:00 Thu 1435678200
*/
char *tz;
tz = getenv("TZ");
setenv("TZ", "CET", 1);
tzset();
strptime("2016-03-27 01:58:00", "%Y-%m-%d %H:%M:%S", &otm);
otm.tm_isdst = -1;
ot = mktime(&otm);
log_info("isdst == -1, result %ld", ot);
dump_tm(&otm, "local");
strptime("2016-03-27 03:58:00", "%Y-%m-%d %H:%M:%S", &ttm);
ttm.tm_isdst = -1;
tt = mktime(&ttm);
log_info("isdst == -1, result %ld", tt);
dump_tm(&ttm, "local");
log_info("timestamp diff %ld", tt - ot);
/*
* 夏令时结束
* 2016-10-30 02:58:00 Thu 1435678200
* 2016-10-30 02:58:00 Thu 1435678200
*/
strptime("2016-10-30 02:58:00", "%Y-%m-%d %H:%M:%S", &otm);
otm.tm_isdst = -1;
ot = mktime(&otm);
log_info("isdst == -1, result %ld", ot);
dump_tm(&otm, "local");
strptime("2016-10-30 03:58:00", "%Y-%m-%d %H:%M:%S", &ttm);
ttm.tm_isdst = -1;
tt = mktime(&ttm);
log_info("isdst == -1, result %ld", tt);
dump_tm(&ttm, "local");
log_info("timestamp diff %ld", tt - ot);
return 0;
}
实际上程序比较怕时间回退,那么关于夏令时比较坑的是,夏令时的停止,此时时钟会向后回拨一次,也就是说,同一个小时的时间点出现了两次。
例如 CET (欧洲中部时间) 在 2016-10-30 02:59:59
下一秒会跳转到 2016-10-30 02:00:00
,也就是说 02:00:00
到 02:59:59
这一个小时的时间窗出现了两次。
那么,此时,如果要获取到中间的时间窗,在使用 mktime()
时就需要手动配置其中的 tm_isdst
字段。
字符串转换
其中 strftime()
用来格式化日期、日期时间和时间的函数,支持 date、datetime、time 等类,将其转换为字符串;而 strptime()
就是从字符串表示的日期时间按格式化字符串要求转换为相应的日期时间。
示例如下:
#define _XOPEN_SOURCE
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
struct tm tm;
char buffer[256], *rcs;
int rc;
memset(&tm, 0, sizeof(struct tm));
rcs = strptime("2001-11-12 18:31:01", "%Y-%m-%d %H:%M:%S", &tm);
if (rcs == NULL) { /* such as: 20010-10-12 */
printf("Format error\n");
return -1;
}
rc = strftime(buffer, sizeof(buffer), "%4-%m-%d %H:%M:%S (%Z %z)", &tm);
if (rc == 0) { /* Not enough buffer, */
buffer[sizeof(buffer) - 1] = 0;
printf("Got zero\n");
return -1;
}
puts(buffer);
return 0;
}
其中 strftime()
会按照格式将上述内容格式输出,如果不满足则原样复制,而且格式化字符基本已经占满所有 26 个字符。
gettimeofday() 效率
很多时候需要获取当前时间,如计算 http 耗时,数据库事务 ID 等,那么 gettimeofday()
这个函数做了些什么?内核 1ms 一次的时钟中断可以支持微秒精度吗?如果在系统繁忙时,频繁的调用它是否有问题吗?
gettimeofday()
是 C 库提供的函数,它封装了内核里的 sys_gettimeofday()
系统调用。
在 x86_64 体系上,使用 vsyscall
实现了 gettimeofday()
这个系统调用,简单来说,就是创建了一个共享的内存页面,它的数据由内核来维护,但是,用户态也有权限访问这个内核页面,由此,不通过中断 gettimeofday()
也就拿到了系统时间。
函数作用
gettimeofday()
会把内核保存的墙上时间和 jiffies 综合处理后返回给用户。先看看 gettimeofday()
是如何做的,首先它调用了 sys_gettimeofday()
系统调用。
asmlinkage long sys_gettimeofday(struct timeval __user *tv, struct timezone __user *tz)
{
if (likely(tv != NULL)) {
struct timeval ktv;
do_gettimeofday(&ktv);
if (copy_to_user(tv, &ktv, sizeof(ktv)))
return -EFAULT;
}
if (unlikely(tz != NULL)) {
if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
return -EFAULT;
}
return 0;
}
调用 do_gettimeofday()
取得当前时间存储到变量 ktv
上,并调用 copy_to_user()
复制到用户空间,每个体系都有自己的实现,这里就简单看下 x86_64 体系下 do_gettimeofday()
的实现:
void do_gettimeofday(struct timeval *tv)
{
unsigned long seq, t;
unsigned int sec, usec;
do {
seq = read_seqbegin(&xtime_lock);
sec = xtime.tv_sec;
usec = xtime.tv_nsec / 1000;
/* i386 does some correction here to keep the clock
monotonous even when ntpd is fixing drift.
But they didn't work for me, there is a non monotonic
clock anyways with ntp.
I dropped all corrections now until a real solution can
be found. Note when you fix it here you need to do the same
in arch/x86_64/kernel/vsyscall.c and export all needed
variables in vmlinux.lds. -AK */
t = (jiffies - wall_jiffies) * (1000000L / HZ) +
do_gettimeoffset();
usec += t;
} while (read_seqretry(&xtime_lock, seq));
tv->tv_sec = sec + usec / 1000000;
tv->tv_usec = usec % 1000000;
}
可以看到,该函数只是把 xtime
与 jiffies
修正后返回给用户,而 xtime
变量和 jiffies
的维护更新频率,就决定了时间精度,而 jiffies
一般每 10ms 或者 1ms 才处理一次时钟中断,那么这是不是意味着精度只到 1ms ?
微秒级精度
获取时间是通过 High Precision Event Timer 维护,这个模块会提供微秒级的中断,并更新 xtime 和 jiffies 变量;接着,看下 x86_64 体系结构下的维护代码:
static struct irqaction irq0 = {
timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL
};
这个 timer_interrupt()
函数会处理 HPET 时间中断,来更新 xtime 变量。
总结
//----- 将时间格式转为字符串,不修改时区,使用标准格式
char *asctime(const struct tm *tm);
char *asctime_r(const struct tm *tm, char *buf);
//----- 转换为本地时间,同样使用标准格式
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
//----- 将时间戳转换为GMT时区的标准时间
struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
//----- 将时间戳转换为本地时区的时间格式
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);
time_t mktime(struct tm *tm);
double difftime(time_t time1, time_t time0);
int gettimeofday(struct timeval *tv, struct timezone *tz);
int settimeofday(const struct timeval *tv , const struct timezone *tz);
示例如下。
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
static void dump_tm(const struct tm *t, const char *var)
{
log_info("---> dump <struct tm> %s", var);
log_info(" %04d %02d %02d %02d:%02d:%02d",
t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
log_info(" tm_wday: %02d", t->tm_wday);
log_info(" tm_yday: %02d", t->tm_yday);
log_info(" tm_isdst: %02d", t->tm_isdst);
}
static void dump_ts(const struct timespec *ts, const char *var)
{
log_info("---> dump <struct timespec> %s", var);
log_info(" %lds %ldns", ts->tv_sec, ts->tv_nsec);
}
static void dump_tv(const struct timeval *v, const char *zone)
{
log_info("---> dump <struct timeval> %s", zone);
log_info(" %lds %ldus", v->tv_sec, v->tv_usec);
}
int main(void)
{
/* system("date -R"); */
time_t time_now = time(NULL);
log_info("---> time now: %ld", time_now);
/* int gettimeofday(struct timeval *tv, struct timezone *tz); */
struct timeval tv;
gettimeofday(&tv, NULL);
dump_tv(&tv, "GMT");
/* struct tm *gmtime_r(const time_t *timep, struct tm *result); */
struct tm tm_gmt;
gmtime_r(&time_now, &tm_gmt);
dump_tm(&tm_gmt, "GMT");
time_t time_mk_gmt = mktime(&tm_gmt);
log_info("---> time gmt: %ld", time_mk_gmt);
/* struct tm *localtime_r(const time_t *timep, struct tm *result); */
struct tm tm_local;
localtime_r(&time_now, &tm_local);
dump_tm(&tm_local, "local");
time_t time_mk_local = mktime(&tm_local);
log_info("---> time local: %ld", time_mk_local);
/* int clock_gettime(clockid_t clk_id, struct timespec *tp); */
struct timespec tp;
clock_gettime(CLOCK_REALTIME, &tp);
dump_ts(&tp, "CLOCK_REALTIME");
/* system("cat /proc/uptime"); */
clock_gettime(CLOCK_MONOTONIC, &tp);
dump_ts(&tp, "CLOCK_MONOTONIC");
return 0;
}