计算机系统中的时间

本文不完全的总结了 UNIX 环境编程中关于时间的操作。

计算机中有两种不同的时间参考系:一种为人们所熟悉的挂在墙上的时钟,即日历时间;另一种则为处理器时间,由于现代计算机系统一般为多任务系统,所以进程并不一直占用处理器。

计算机系统工作在数个不同的时间维度中,典型的 CPU 一个周期时间大约为 10^-9s, CPU 固定间隔时间的中断一般为 0.01s, 而人能够分辨的最小时间间隔在 0.1 s 左右。计算机能够提供的最小时间精度取决于其具体实现,一般计算机最少能提供其 CPU 时间片长度的时钟分辨率,一般为 10ms, 一些系统可以利用 CPU 周期数获取最高分辨率为 1ns 的时钟精度。

获取当前日期和时间

简单的,在 shell 中输入 date 命令即可打印出当前的日期和时间。

UNIX 系统提供的基本时间服务为国际标准时间公元1970年1月1日零点以来所经过的秒数,这个秒数是以数据类型 time_t 表示的,称为日历时间,可以使用 C 库函数 time 获得。另外 C 标准库 time.h 头文件中提供了一系列转换时间的函数,把 time 函数获得的秒数转换为可读的、本地的日期和时间。

#include <time.h>
time_t time(time_t *calptr); 

值得注意的是,time_t 被定义为 long int 类型,在32位系统中,最大只能表示到 2038 年左右。所幸随着64位系统的普及,这一问题将不复存在。

使用 gettimeofday 函数,能够获得比 time 函数具有更高分辨率的日历时间,最高能够达到微妙(10^-6s)级,但这取决与系统的具体实现。在有些平台上,操作系统能够使用 CPU 周期数来获取当前时间,能够达到微妙级的精度,但有些系统实现则使用系统时钟,一般只有 10ms (毫秒,10^-3s)的精度。

#include <sys/time.h>

struct timeval {
    long tv_sec; /* Seconds */
    long tv_usec; /* Microseconds */
}

int gettimeofday(struct timeval *tv, NULL);

在 linux 系统中,gettimeofday 的第二个参数应该简单的设置为 NULL, 因为它指向一个未被实现的校正时区的特性。

使用 clock_gettime 函数指定 clk_id 为 CLOCK_REALTIME 时可以获取系统纳秒级的日历时间。

#include <time.h>

struct timespec {
    time_t tv_sec; /* seconds */
    long tv_nsec; /* nanoseconds */
};

int clock_gettime(clockid_t clk_id, struct timespec *tp);

编译程序时需要链接 -lrt 库,否则会报错。

测量程序执行的时间

1 使用 clock 函数

1.1 使用

clock 函数也是 C 标准库 time.h 头文件中的函数,clock 函数返回程序执行开始到当前使用的处理器时间,数据类型为 clock_t. 宏 CLOCKS_PER_SEC 定义了每秒钟对应的 clock 数。在程序需要测量运行时间的地方开始和结束分别调用一次 clock 函数,将结果相减除以 CLOCKS_PER_SEC 即为程序执行时间的秒数。

1.2 clock 函数的实现与精度:

现代的计算机系统一般为多任务系统,即可以“同时”运行多个进程。注意这里同时并不代表真正的同时运行(不考虑多个 CPU 情况),而是将 CPU 时间分成固定大小的时间片(一般为 10ms), 多个进程使用某种调度算法轮流执行,由于人并不能分辨这么短的时间间隔,所以看起来计算机是同时执行多个进程的。clock 函数的实现是基于这一事实,所以 clock 的精度取决与所在系统 CPU 分片时间的大小。

在 UNIX 系统中,可以使用下面函数获取所在系统每秒时钟滴答数:

#include <unistd.h>

sysconf(_SC_CLK_TCK)

在笔者计算机上(linux 32位系统,酷睿处理器),该值为 100. 这个结果与我所设想的有所不同:在几十年前 CPU 频率只有几 MHZ 时,该值就为 100, 如今处理器速度增长了数千倍,本以为该值也会增加,却没有太大变化。

经过实验证明 clock 函数在笔者电脑上的精度为 10ms. 虽然 CLOCKS_PER_SEC 值一般为 1000 或 1000000, 但并不代表 clock 函数能够达到相应的精度,连续多次调用 clock 函数所返回的值可能是相同的。

由于 clock 函数的精度有限,所以只适合用来测量执行时间相对较长的程序,但在这种情况下,有更好的替代方法,所以不推荐使用 clock 函数来测量程序执行时间。

2 使用clock_gettime 函数。

当指定 clock_gettime clk_id 为 CLOCK_PROCESS_CPUTIME_ID 可以获取更高精度的进程处理器时间。

3 使用 time 命令

3.1 使用

time 命令使用很简单,在所要执行的命令前加上 time 即可,比如:

time ./a.out

程序在执行完毕后会打印出程序执行的实际时间、用户态时间和核心态时间。实际时间为程序开始到结束所经过的日历时间,用户态时间和核心态时间相加为程序使用的处理器时间。

3.2 time 命令的实现与精度

通过分析 time 程序源码,知道 time 程序使用了 gettimeofday 函数来获取程序执行的实际时间,使用 wait3 函数来获取程序的处理器时间,包括用户态和核心态。

gettimeofday 函数前面已经叙述过,wait3 函数在大部分类 UNIX 系统中都有提供,可以用来获取子进程的结束状态信息,其中包括进程的运行时间。该时间是使用一种“记账”的方法来测量的。操作系统维护所有进程的信息,每隔固定的时间(该时间并不一定和 CPU 时间片相同,通过实验,笔者所得到的该时间大小为 4ms, 其中的具体实现不太清楚),确定当前所运行的进程和其所处的状态,然后将对应的时间增加。

所以 time 命令的精度并不比 clock 函数高很多,并且只能测量程序的执行时间,不能只测量一个程序段或函数的执行时间。在程序执行时间很短时,误差会比较大。

4 基于计算机周期数

在一些计算机系统中,可以使用特殊的方法获得 CPU 的当前周期数,该值每经过一个 CPU 周期会加 1, 由于CPU 的频率是固定的,所以可以通过这个方法获得相当高精度的时间。当前 CPU 频率都在数 GHZ 左右,所以可以达到纳秒(10^-9s)级的精度。

在 x86 架构的 CPU 上,可以使用特殊的汇编代码获取当前 CPU 周期数,这需要将汇编代码嵌入到 C 程序中。另外,在如此高的精度下,影响程序运行时间的因素很多,比如中断、缓存等。想了解更多的童鞋可以参考《深入理解计算机系统》一书第9章 测量程序执行时间。

定时器

简单的应用可以使用 sleep 函数:

#include <unistd.h>

unsigned int sleep(unsigned int seconds);

int usleep(useconds_t usec);

其功能如其名,其使进程休眠一定的秒数。usleep 函数可以提供最高到微妙级的精度,但和 gettimeofday 一样,其精度依赖与系统的具体实现。

如果要在程序不被阻塞的情况下使用定时器,可以使用 alarm 函数,这涉及到了信号处理:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

useconds_t ualarm(useconds_t usecs, useconds_t interval);

alarm 函数功能是在给定的秒数过后给进程发送一个 SIGALRM 信号,仅发送一次。程序必须使用 signal 函数来处理该信号,否则会默认结束进程。ualarm 函数则提供了一个更高精度的定时器,其精度同样有赖于具体实现。ualarm 是已经过时的功能,不被推荐使用的,替代者为: nanosleep 和 setitimer.

#include <time.h>

struct timespec {
    time_t tv_sec; /* seconds */
    long tv_nsec; /* nanoseconds */
};

int nanosleep(const struct timespec *req, struct timespec *rem);

nanosleep 虽然能提供纳秒级的精度,但其精度有赖于具体实现。req 为睡眠时间,当睡眠被其他的信号唤醒时, nanosleep 返回 -1, 并讲剩余的睡眠时间保存到 rem 中,如果不为 rem 不为 NULL 的话, 该值可以被用于下一次 nanosleep 调用。

alarm 函数定时使用的时间为日历时间,setitimer 函数则提供了基于进程时间的定时器以及其他功能。

#include <sys/time.h>

struct itimerval {
    struct timeval it_interval; /* timer interval */
    struct timeval it_value; /* current value */
};

struct timeval {
     long tv_sec; /* seconds */
     long tv_usec; /* microseconds */
};

int getitimer(int which, struct itimerval *curr_value);

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

setitimer 支持3种类型的定时器,setitimer 第一个参数 which 指定定时器类型(上面三种之一)

ITIMER_REAL : 以系统真实的时间来计算,它送出 SIGALRM 信号。

ITIMER_VIRTUAL : -以该进程在用户态下花费的时间来计算,它送出 SIGVTALRM 信号。

ITIMER_PROF : 以该进程在用户态下和内核态下所费的时间来计算,它送出 SIGPROF 信号。

第二个参数 new_value 是结构 itimerval 的一个实例,it_interval 为定时时间,current value 为距下个定时的时间间隔,一般设为和 it_interval 相同;第三个参数 old_value 如果不为 NULL,则将老的设置的 it_interval 保存到 old_value 里。

setitimer 为其所在进程设置一个定时器,该定时器持续有效,当 it_interval 和 it_value 值为 0 时,关闭计时器。

getitimer 则获取到下一个定时剩余的时间,即 it_value 的值。

参考书

Computer Systems: A Programmer’s Perspective (深入理解计算机系统)

Advanced Programming in the UNIX Environment (UNIX环境高级编程)

计算机系统中的时间》上有2条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注