POSIX 时间管理
UTC 时间与本地时间
UTC(Universal Time Coordinated),即世界协调时间,在实际使用中,它等同于 GMT(Greenwich Mean Time),即格林威治标准时间。UTC 时间以 1970 年 1 月 1 日 0 时 0 分 0 秒为基准时间,并以秒为最小计数单位。以英国伦敦格林威治(本初子午线)为中时区,将地球分为东西各 12 个时区,各时区之间相差 1 小时,这就是各个时区的本地时间。由于地球自西向东旋转,因此东时区时间早于中时区时间,西时区时间晚于中时区时间。
#include <time.h>
time_t time(time_t *time);
time_t timelocal(time_t *time);
函数 time 原型分析:
- 此函数返回类型为 time_t 的 UTC 时间。
- 输出参数 time 为获得的 UTC 时间,与返回值相同,该参数可以为 NULL。
函数 timelocal 原型分析:
- 此函数返回类型为 time_t 的本地时间。
- 输出参数 time 为获得的本地时间,与返回值相同,该参数可以为 NULL。
time_t 在某些类 Unix 系统中定义为一个 32 位的有符号整型数,能表示的最大正秒数为 2147483647 秒,它能表示的最晚时间为 2038 年 1 月 19 日 03:14:07,这意味着,如果超过这个时间将溢出。SylixOS 中将 time_t 定义为 64 位有符号整型数,因此不存在上述问题。使用下面的函数可以处理更高精度的时间。
#include <time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
int settimeofday(const struct timeval *tv, const struct timezone *tz);
函数 gettimeofday 原型分析:
- 此函数返回 0,没有错误值返回。
- 输出参数 tv 为一个 timeval 结构的指针,保存获取的时间信息。
- 输出参数 tz 为一个 timezone 结构的指针,保存获取的时区信息。
结构 timeval 的定义如下:
struct timeval {
time_t tv_sec; /* seconds */
LONG tv_usec; /* microseconds */
};
tv_usec 的值为 0~999999,即不超过 1 秒,tv_sec 与 tv_usec 组成了当前的时间。注意该时间是 UTC 时间。结构 timezone 的定义如下:
struct timezone {
int tz_minuteswest; /* 是格林威治时间往西方的时差 */
/* 以分钟为单位 (东8区) -60*8 */
int tz_dsttime; /* 时间的修正方式 必须为0 */
};
tz_minuteswest 的定义与前面讲的不同,这里的定义为相对于格林威治时间向西方的时差,因此东时区的时区值为负数。
#include <time.h>
void tzset(void);
tzset 函数设置系统的时区,该函数没有任何参数,实际上它内部获取一个名称为 TZ 的环境变量,该变量是对时区的描述,在 SylixOS 中,tzset 函数使用环境变量 TZ 的当前值经过一定的处理后赋值给全局变量 timezone、tzname(目前 SylixOS 不支持 daylight),TZ 的描述如下:
# echo $TZ
CST-8:00:00
CST(China Standard Time)即表示中国标准时间,相对于格林威治向西方的时差为负的 8 小时 0 分 0 秒,实际就是东八区。可使用如下的程序设置当前时区:
#include <stdlib.h>
#include <time.h>
int main(int argc, char *argv[])
{
system("TZ=CST-6:0:0");
tzset();
return (0);
}
以上程序将当前时区设置为东 6 区,以下两条 Shell 命令有完全相同的效果。
# TZ=CST-6:0:0
# tzsync
时间格式转换
上一节获取的时间均是单一地以秒来表示,不符合正常使用习惯,因此有以下函数将该时间转换为我们通常熟悉的时间格式。
#include <time.h>
struct tm *gmtime(const time_t *time);
struct tm *gmtime_r(const time_t *time, struct tm *ptmBuffer);
函数 gmtime 原型分析:
- 此函数成功返回 tm 结构的指针,失败返回 NULL。
- 参数 time 为本地时间。
函数 gmtime_r 原型分析:
- 此函数成功返回 tm 结构的指针,失败返回 NULL。
- 参数 time 是本地时间。
- 输出参数 ptmBuffer 是 tm 结构缓冲区。
需要注意的是,gmtime 函数是不可重入的,因此是非线程安全的。通过程序清单 gmtime测试(Test_Gmtime)和程序清单 gmtime_r测试(Test_Gmtime_r)可以看出这一点。
结构体 tm 描述了我们通常习惯的用法,其定义如下:
程序清单 tm结构体
struct tm {
INT tm_sec; /* seconds after the minute - [0, 59] */
INT tm_min; /* minutes after the hour - [0, 59] */
INT tm_hour; /* hours after midnight - [0, 23] */
INT tm_mday; /* day of the month - [1, 31] */
INT tm_mon; /* months since January - [0, 11] */
INT tm_year; /* years since 1900 */
INT tm_wday; /* days since Sunday - [0, 6] */
INT tm_yday; /* days since January 1 - [0, 365] */
#define tm_day tm_yday
INT tm_isdst; /* Daylight Saving Time flag */
/* must zero */
};
gmtime 将 time_t 类型的时间转换为 tm 类型的时间。从函数名可以得知,它将输入参数 time 当做是 UTC 时间(通常也叫做 GMT 时间),内部并不会进行时区的转换处理。因此,当我们使用 UTC 时间作为参数时,返回的 tm 指针代表的是 UTC 时间,使用本地时间作为参数时,返回的 tm 指针代表的是本地时间。
注意:
gmtime 返回的指针实际上指向的是一个内部全局变量,因此连续调用该函数试图获取不同的时间将得到同一个时间值,如程序清单 gmtime测试(Test_Gmtime)所示。
程序清单 gmtime测试
#include <time.h>
int main (int argc, char *argv[])
{
struct tm *tm_old;
struct tm *tm_new;
time_t time_old;
time_t time_new;
time_old = time(NULL);
sleep(10);
time_new = time(NULL);
tm_old = gmtime(&time_old);
tm_new = gmtime(&time_new);
fprintf(stdout, "old: %d:%d\n", tm_old->tm_min, tm_old->tm_sec);
fprintf(stdout, "new: %d:%d\n", tm_new->tm_min, tm_new->tm_sec);
return (0);
}
运行该程序后,其结果如下,可见两个时间是一样的。实际上 tm_old 与 tm_new 都指向同一个对象,也就是最后一次调用的结果,即 tm_new。
# ./Test_Gmtime
old: 55:12
new: 55:12
为解决以上问题,有与之对应的 gmtime_r 函数,后缀 _r 表示这是一个可重入的版本,它多了一个输出参数 ptmBuffer ,将原来使用的内部全局变量改为让用户给出输出结果的缓冲区对象,这样只要用户使用不同的缓冲区对象,就会得到不同的结果。程序清单 使用 gmtime_r 处理同样的问题。
程序清单 gmtime_r测试
#include <time.h>
int main (int argc, char *argv[])
{
struct tm tm_old;
struct tm tm_new;
time_t time_old;
time_t time_new;
time_old = time(NULL);
sleep(10);
time_new = time(NULL);
gmtime_r(&time_old, &tm_old);
gmtime_r(&time_new, &tm_new);
fprintf(stdout, "old: %d:%d\n", tm_old.tm_min, tm_old.tm_sec);
fprintf(stdout, "new: %d:%d\n", tm_new.tm_min, tm_new.tm_sec);
return (0);
}
程序运行后结果如下,可见两者之间的时间差和预想的一样为 10 秒。
# ./Test_Gmtime_r
old: 55:41
new: 55:51
在上面的程序中,我们必须定义保存结果的两个数据对象,而不是像之前那样仅仅定义两个指针,这样两个对象分别保存了不同的数据,达到了程序本来的目的。上面的例子展示的是在单个线程里面连续调用 gmtime 产生的问题。在多线程中,我们必须使用 gmtime_r。后面还有几个函数也有相同的问题以及同样的解决措施,本书将不再对它们的可重入版本函数进行详细说明,仅仅列出它们的函数原型。
#include <time.h>
struct tm *localtime(const time_t *time);
struct tm *localtime_r(const time_t *time, struct tm *ptmBuffer);
localtime 与 gmtime 的功能相同,但是它内部会进行 UTC 时间到本地时间的转换处理。因此,正确的使用方法是传入的参数为 UTC 时间。
#include <time.h>
char *asctime(const struct tm *ptm);
char *asctime_r(const struct tm *ptm, char *pcBuffer);
函数 asctime 原型分析:
- 此函数成功返回格式化后的时间字符串指针,失败返回 NULL。
- 参数 ptm 为一个 tm 结构的指针。
注意,asctime 函数处理参数 ptm 时,将不会作任何时区转换,如同 gmtime 函数一样,程序应该根据需要传入需要的 tm 数据对象。asctime 函数返回的时间字符串的格式如“Tue May 21 13:46:22 1991\n”,因此在使用该函数的可重入版本 asctime_r 时,其参数 pcBuffer 必须保证长度不小于 26 个字节。
#include <time.h>
char *ctime(const time_t *time);
char *ctime_r(const time_t *time, char *pcBuffer);
函数 ctime 原型分析:
- 此函数成功返回格式化后的时间字符串指针,失败返回 NULL。
- 输入参数 time 为 time_t 类型的指针。
ctime 函数可以将本地时间转换成符合人类习惯的字符串格式,该字符串格式与 asctime 函数转换后的相似,因此其可重入版本函数 ctime_r 中的参数 pcBuffer 长度必须保证不小于 26 个字节。注意 ctime 内部会进行 UTC 时间到本地时间的转换。
#include <time.h>
time_t mktime(struct tm *ptm);
time_t timegm(struct tm *ptm);
函数 mktime 原型分析:
- 该函数成功返回转换后的 UTC 时间。
- 输入参数 ptm 为 tm 类型的指针。
mktime 函数的功能是将本地时间转换为 UTC 时间,它内部会进行本地时间到 UTC 时间的转换,因此正确的输入参数应该为本地时间。与之功能相同的 timegm 则只是进行 tm 到 time_t 数据类型的转换,正确的输入参数应该为 UTC 时间。
可以调用以下函数计算两个 time_t 类型时间之间的差值。
#include <time.h>
double difftime(time_t time1, time_t time2);
高精度时间
#include <time.h>
clock_t clock(void);
函数 clock 返回自系统启动后到目前为止经过的时钟计数,该计数类型为 clock_t。这里的时钟就是指时钟节拍,因此 clock 函数的功能与 Lw_Time_Get 函数相同。
使用 clock_getres 可以获得系统高精度时间的精度。
#include <time.h>
int clock_getres(clockid_t clockid, struct timespec *res);
函数 clock_getres 原型分析:
- 该函数成功返回 0,失败返回错误码。
- 输入参数 clockid 为时钟源 ID,其类型 clockid_t 有以下定义:
时钟源名称 | 说明 |
---|---|
CLOCK_REALTIME | 代表实际的物理时间 |
CLOCK_MONOTONIC | 单调增长时间 |
CLOCK_PROCESS_CPUTIME_ID | 进程从启动开始所消耗的 CPU 时间 |
CLOCK_THREAD_CPUTIME_ID | 线程从启动开始所消耗的 CPU 时间 |
- 输出参数 res 保存获得的时间精度,可达到 1 纳秒精度,其类型 timespec 的定义如下:
struct timespec {
time_t tv_sec; /* seconds */
LONG tv_nsec; /* nanoseconds */
};
由于 CLOCK_REALTIME 代表实际的物理时间,因此对系统时间的修改将会影响它。CLOCK_MONOTONIC 自系统启动后一直增长,且不受任何操作的影响,通常使用 CLOCK_MONOTONIC 时钟源来计算两个操作之间的时间差。
#include <time.h>
int clock_gettime(clockid_t clockid, struct timespec *tv);
函数 clock_gettime 原型分析:
- 该函数成功时返回 0,失败时返回-1 并设置错误码。
- 参数 clockid 是时钟源,如上表所示。
- 输出参数 tv 保存获得的高精度时间。
clock_gettime 函数根据不同的时钟源类型获得 struct timespec 类型的时间值,注意,clock_gettime 获得的是 UTC 时间。
#include <time.h>
int clock_settime(clockid_t clockid, const struct timespec *tv);
函数 clock_settime 原型分析:
- 该函数成功时返回 0,失败时返回-1 并设置错误码。
- 参数 clockid 指定时钟源(只能为 CLOCK_REALTIME)。
- 参数 tv 为需要设置的高精度时间。
因为 CLOCK_MONOTONIC 不受任何操作的影响,而 CLOCK_PROCESS_CPUTIME_ID 和 CLOCK_THREAD_CPUTIME_ID 仅由系统内部更新,因此 clock_settime 的时钟源只能是 CLOCK_REALTIME。
#include <time.h>
int clock_nanosleep(clockid_t clockid,
int flags,
const struct timespec *rqtp,
struct timespec *rmtp);
函数 clock_nanosleep 原型分析:
- 此函数成功时返回 0,失败时返回-1 并设置错误号。
- 参数 clockid 指定了时钟源。
- 参数 flags 是时间类型(如 TIMER_ABSTIME)。
- 参数 rqtp 指定了睡眠时间。
- 输出参数 rmtp 返回睡眠剩余时间。
clock_nanosleep 与 nanosleep 相似可以使进程睡眠指定的纳秒时间,不同的是,如果参数 flags 指定为绝对时间(TIMER_ABSTIME),则 rmtp 不再有意义。睡眠剩余时间的意义通常是指睡眠被信号中断后剩余的时间。
获得进程或线程时钟源
#include <time.h>
int clock_getcpuclockid(pid_t pid, clockid_t *clock_id);
函数 clock_getcpuclockid 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 pid 是进程 ID。
- 输出参数 clock_id 返回时钟源类型如下表所示。
调用 clock_getcpuclockid 函数可以获得指定进程 pid 的时钟源类型,SylixOS 总是返回 CLOCK_PROCESS_CPUTIME_ID。
时钟源名称 | 说明 |
---|---|
CLOCK_REALTIME | 代表实际的物理时间 |
CLOCK_MONOTONIC | 单调增长时间 |
CLOCK_PROCESS_CPUTIME_ID | 进程从启动开始所消耗的 CPU 时间 |
CLOCK_THREAD_CPUTIME_ID | 线程从启动开始所消耗的 CPU 时间 |
#include <pthread.h>
int pthread_getcpuclockid(pthread_t thread, clockid_t *clock_id);
- 此函数成功返回 0,失败返回错误号。
- 参数 thread 是线程 ID。
- 输出参数 clock_id 返回时钟类型。
调用 pthread_getcpuclockid 函数可以获得指定线程 thread 的时钟源类型,SylixOS 总是返回 CLOCK_THREAD_CPUTIME_ID。
时间相关的扩展操作
针对 timeval 结构,系统提供了几个有用的宏,方便操作该结构对象,注意这些操作并不是 POSIX 中定义的,其存在于 Linux 和大多数类 Unix 系统中。它们的定义如下(虽然他们都是宏定义,但是这里还是根据它们的实际使用方式,以函数的形式给出它们的定义):
#include <sys/time.h>
void timeradd(struct timeval *a, struct timeval *b, struct timeval *result);
void timersub(struct timeval *a, struct timeval *b, struct timeval *result);
void timerclear(struct timeval *tvp);
int timerisset(struct timeval *tvp);
int timercmp(struct timeval *a, struct timeval *b, CMP);
timeradd 将 a、b 时间相加,结构保存于 result 中,内部会自动处理微秒到秒的进位问题,timersub 将 a 时间减去 b 时间,结构保存于 result 中,内部会自动处理微秒到秒的进位问题。注意,这两个操作内部不会有溢出或大小等任何安全性检测,因此结果可能出现不符合预期时间值,这些问题需要应用程序处理。
timerclear 将一个时间值清零,timerisset 检测该时间值是否为 0。
timercmp 比较两个时间,CMP 为一个操作符,如 >、==、<、!=、<= 等。
以下程序展示了这些宏的使用方法。
#include <stdio.h>
#include <sys/time.h>
#define timeval_show(des, tv) \
fprintf(stdout, des "sec = %llu, usec = %lu.\n", \
tv.tv_sec, tv.tv_usec)
int main(int argc, char *argv[])
{
struct timeval a;
struct timeval b;
struct timeval result;
a.tv_sec = 100;
a.tv_usec = 800000;
b.tv_sec = 80;
b.tv_usec = 330000;
timeval_show("time a: ", a);
timeval_show("time b: ", b);
timeradd(&a, &b, &result);
timeval_show("time a + b: ", result);
timersub(&a, &b, &result);
timeval_show("time a - b: ", result);
timerclear(&result);
if (timerisset(&result)) {
timeval_show("time is set: ", result);
} else {
timeval_show("time is zero: ", result);
}
if (timercmp(&a, &b, >)) {
fprintf(stdout, "time a is larger than time b.\n");
} else {
fprintf(stderr, "time a is not larger than time b.\n");
}
return (0);
}
程序运行后,输出以下结果:
# ./timeext_test
time a: sec = 100, usec = 800000.
time b: sec = 80, usec = 330000.
time a + b: sec = 181, usec = 130000.
time a - b: sec = 20, usec = 470000.
time is zero: sec = 0, usec = 0.
time a is larger than time b.