Linux 编程之时间篇:wall time, cpu time 和 timer

Posted 拭心

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 编程之时间篇:wall time, cpu time 和 timer相关的知识,希望对你有一定的参考价值。

文章目录

在 Java/JS 中获取时间非常简单(System.currentTimeMillis() System.nanoTime(); new Date().getTime() 等等),在 C/C++ 中则略微复杂一点。

今天我们就来了解下,Linux C 编程中,时间相关的概念和获取方式。

Linux 时间的相关知识

UNIX 系统使用从 1970 年 1 月 1 日 零点零分以来,经过的时间,作为绝对时间。

也就是说,在 UNIX 系统中,绝对时间也是相对的。

软件时钟:内核初始化,特定频率的计数器,一个计时器间隔叫做一个 tick 或 jiffy。UNIX 系统中,系统计时频率被称为 HZ。

HZ 具体多大,是和体系结构相关的,在 x86 上,该值为 100,也就是一秒钟运行 100次;于是一个间隔 jiffy 就等于 1/100 秒。

获取当前设备的 tck (每秒多少次):

void get_tick() 
    long hz = sysconf(_SC_CLK_TCK);
    LOG("_SC_CLK_TCK >> %ld", hz);

输出内容:

zsx_linux: _SC_CLK_TCK >> 100

硬件时钟:大多数计算机有一个电池供电的硬件时钟,内核关闭时会将日期、时间存储进去,启动时再读取。这样即使计算机意外关闭,重新打开,时间也是正确的。

wall time: 墙上表的时间,即真实的时间

获取当前时间

time_t (秒)

typedef __time_t time_t;

time_t 表示自新纪元以来已流逝的秒数,定义在 <time.h>中,可以通过 time() 函数获取:

    //获取当前时间(返回的其实也是个相对时间,自 1970.1.1 来流逝的秒数)
    //并不精确,这个值假定能被 4 整出的年份都是闰年
    time_t t;
    auto time_ret = time(&t);
    LOG("time_t > Current time: %ld, time_ret: %ld", t, time_ret);

上述代码的运行结果:

zsx_linux: time_t > Current time: 1642330676, time_ret: 1642330676

有时候我们需要获取当前的年月日时分秒,Linux 也提供了相应的 struct 和获取方式。

tm

tm (time meaning?)结构体中包含各个维度的时间的信息:

struct tm 
  int tm_sec;
  int tm_min;
  int tm_hour;
  int tm_mday;
  int tm_mon;	//自一月以来的月数
  int tm_year;	//自 1900 年以来的年数
  int tm_wday;	//自周日以来的天数(0~6)
  int tm_yday;	//今年第几天(0~365)
  int tm_isdst;
  long int tm_gmtoff;
  const char* tm_zone;
;

我们可以通过 gmtime 获取 time_t 时间的具体信息:

    time_t t;
    auto time_ret = time(&t);
    LOG("time_t > Current time: %ld, time_ret: %ld", t, time_ret);

    //把时间,转换成年月日时分秒
    struct tm *_tm = gmtime(&t);
    LOG("time meaning > year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d",
            _tm->tm_year + 1900, _tm->tm_mon + 1, _tm->tm_mday,
            _tm->tm_hour, _tm->tm_min, _tm->tm_sec);
    LOG("time meaning > day of week: %d, day of year: %d",_tm->tm_wday, _tm->tm_yday);

运行结果:

zsx_linux: time meaning > year: 2020, month: 11, day: 16, hour: 11, minute: 20, second: 39
zsx_linux: time meaning > day of week: 0, day of year: 15

timeval (微秒)

timeval 可以获取精确到微秒级别的时间,它的结构:

struct timeval 
  __kernel_time_t tv_sec;
  __kernel_suseconds_t tv_usec;
;

通过 gettimeofday 获取当前的时间:

int gettimeofday(struct timeval* __tv, struct timezone* __tz);
long long get_current_time_millis() 
    //微秒级精度
    struct timeval t;
    int ret = gettimeofday(&t, nullptr);
    if (!ret) 
        LOG("get_current_time_millis,  second: %ld, usecond: %ld", t.tv_sec, t.tv_usec);
    

    return (long long )t.tv_sec * 1000 + t.tv_usec / 1000;

调用结果:

current_millis > 1642332541496
//当时的 time_t: 1642332541

timespec (纳秒)

timespec 人如其名,可以获取到更加具体的时间:纳秒

它的结构:

struct timespec 
  __kernel_time_t tv_sec;
  long tv_nsec;
;

可以看到,和 timeval 的区别就是结构体的第二个成员变量。

timespec 可以通过 clock_gettime 获取。

int clock_gettime(clockid_t __clock, struct timespec* __ts);

clock_gettime 的第一个参数 clockid_t 表示特定的 POSIX 时钟,有这几种:

  • CLOCK_MONITONIC: 系统启动后的时间,单调递增
  • CLOCK_REALTIME: wall time,真实时间 (这个是可移植的)
  • CLOCK_PROCESS_CPUTIME_ID: 进程的 CPU time
  • CLOCK_THREAD_CPUTIME_ID: 线程的 CPU time

CLOCK_PROCESS_CPUTIME_ID 和 CLOCK_THREAD_CPUTIME_ID 可以提供更高精度的时间,纳秒级别。

我们可以通过 clock_getres 查看不同类型的时间源精度,它会返回各种时间源在秒和纳秒上的精度。

        struct timespec ts;
        //1.获取 clockid_t 指定的【时间精度】
        int ret = clock_getres(CLOCK_MONOTONIC, &ts);
        if (ret) 
            //error
         else 
            LOG("clock_getres >>> clockid: %d, sec: %ld, nsec: %ld \\n", clockid, ts.tv_sec, ts.tv_nsec);
        

举一个例子,通过 clock_getres 获取不同 POSIX 时钟的精度及通过 clock_gettime 获取时钟对应的时间:

void test_time_spec() 
    clockid_t clocks[] = CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID, -1;
    int i;
    clockid_t clockid;
    for (i = 0; (clockid = clocks[i]) != -1; i++) 
        struct timespec ts;
        //1.获取 clock 指定的【时间精度】
        int ret = clock_getres(clockid, &ts);
        if (ret) 
            //error
         else 
            LOG("clock_getres: %d, sec: %ld, nsec: %ld \\n", clockid, ts.tv_sec, ts.tv_nsec);
        

        //2. 将 clockid 指定的时间源,保存到 timespec 中
        ret = clock_gettime(clockid, &ts);
        if (!ret) 
            LOG("clock_gettime: %d, sec: %ld, nsec: %ld \\n", clockid, ts.tv_sec, ts.tv_nsec);
        
    

上面的代码运行时,会输出当时的真实时间、开机时间、进程 CPU 时间、线程 CPU 时间:

//真实时钟
zsx_linux: clock_getres >>> clockid: 0, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 0, sec: 1642333116, nsec: 488803273 
//单调时钟
zsx_linux: clock_getres >>> clockid: 1, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 1, sec: 414690, nsec: 13117483
//进程 CPU 时间
zsx_linux: clock_getres >>> clockid: 2, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 2, sec: 0, nsec: 358629050
//线程 CPU 时间
zsx_linux: clock_getres >>> clockid: 3, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 3, sec: 0, nsec: 333296654

获取进程的 CPU 时间

除了上述的 CLOCK_PROCESS_CPUTIME_ID 时钟,我们还可以通过 tims 函数获取当前进程和子进程的进程时间。

#include <sys/times.h>
clock_t times(struct tms* __buf);

结果用 tms 表示:

struct tms 
  __kernel_clock_t tms_utime;	//用户态时间,执行用户代码的时间
  __kernel_clock_t tms_stime;	//内核态时间,执行 syscall 及 pagefault 等时间
  __kernel_clock_t tms_cutime;	//child process, 子进程的用户态时间
  __kernel_clock_t tms_cstime;	//子进程的内核态时间
;

注意,tms 的成员变量,单位都是 tck/jiffy 数

void test_get_cpu_time() 

    struct tms buf;
    //获取发起进程及子进程消耗的 cpu 【时钟信号数】,不是时间?
    clock_t clock = times(&buf);

    //用户态时间,除了执行内核代码的时间,还有 pagefault 等触发内核行为的时间
    LOG("Process utime: %ld, stime: %ld", buf.tms_utime, buf.tms_stime);
    //子进程的时间统计,终止于子进程退出,且父进程对其调用了 waitpid(或相关函数)
    LOG("Child process utime: %ld, stime: %ld", buf.tms_cutime, buf.tms_cstime);

睡眠和阻塞

睡眠与阻塞的区别?

睡眠,本质上还是会占用 CPU,只是不执行代码,浪费 CPU;而阻塞,则会让出 CPU,在需要继续执行时,内核会重新唤醒进程,让其从阻塞到运行状态。

当睡眠比较频繁且比较短的时候,使用阻塞是更好的方式。

睡眠的几种方式

  • sleep,单位是秒
  • usleep,单位是微秒
  • nanosleep,单位是纳秒
  • clock_nanosleep,单位是纳秒
#include <unistd.h>

//返回剩余未睡眠的秒数,一般为 0,除非被信号中断
unsigned int sleep(unsigned int __seconds);
int usleep(useconds_t __microseconds);
int nanosleep(const struct timespec* __request, struct timespec* __remainder);

#include <time.h>
int clock_nanosleep(clockid_t __clock, int __flags, const struct timespec* __request, struct timespec* __remainder);

clock_nanosleep 和前三个不同的点在于,可以设置指定的时钟源,比如真实时间、相对时间(睡眠 CPU 时间没有意义)。

阻塞

想对比睡眠,也可以通过 select 实现阻塞。

int select(int __fd_count, fd_set* __read_fds, fd_set* __write_fds, fd_set* __exception_fds, struct timeval* __timeout);

可以通过这样的调用,实现让进程睡眠:

void test_block() 
    struct timeval tv = .tv_sec = 1, .tv_usec = 1000;
    select(0, nullptr, nullptr, nullptr, &tv);


void main() 
    auto begin = get_current_time_millis();
    test_block();
    auto end_block = get_current_time_millis();
    LOG("block time: %lld", (end_block - begin));


测试结果:

zsx_linux: block time: 2003

使用 select 在文件描述符上阻塞,性能更好,会让出 CPU,进入阻塞状态,在超出时间后,内核会唤醒该进程,而不是让进程不断的循环。

定时器

Linux 上也支持在一定时间后延迟通知进程的机制,即定时器,支持这些定时器:

  • alarm
  • interval timer
  • timer_create

alarm

#include <unistd.h>
unsigned int alarm(unsigned int __seconds);

alarm 会在 N 秒(真实时间)后发送 SIGALRM 信号给调用进程,比如这样:

void alarm_handler(int signo) 
    LOG("alarm_handler called: %d, is SIGALRM? %d", signo, signo == SIGALRM);


void test_alarm() 
    signal(SIGALRM, alarm_handler);

    //延迟 3 秒执行,一次性
    alarm(3);

只会执行一次。

如果发送之前有未发送的 alarm,会取消之前的。

interval timer

#include <sys/time.h>

int getitimer(int __which, struct itimerval* __current_value);
int setitimer(int __which, const struct itimerval* __new_value, struct itimerval* __old_value);

计时器和 alarm 的区别在于,不只执行一次,也可以支持不同的信号

#define ITIMER_REAL 0	//真实时间后发送  SIGALRM
#define ITIMER_VIRTUAL 1	//进程 utime 后发送 SIGVTALRM
#define ITIMER_PROF 2	//utime + stime 后,发送 SIGPROF

我们可以通过这样进行测试:

    auto ret = signal(SIGALRM, alarm_handler);
    LOG("register SIGALRM handler ret: %d", ret);
    ret = signal(SIGVTALRM, alarm_handler);
    LOG("register SIGVTALRM handler ret: %d", ret);
    ret = signal(SIGPROF, alarm_handler);
    LOG("register SIGPROF handler ret: %d", ret);

    struct timeval begin=.tv_sec=1, .tv_usec=0;
    struct timeval interval=.tv_sec=2, .tv_usec=0;

    struct itimerval itimer=.it_interval=interval, .it_value = begin ;
    int ret = setitimer(ITIMER_PROF, &itimer, nullptr);
    LOG("setitimer ret: %d", ret);

在我测试的过程中,发现设置 ITIMER_REAL 类型的计时器可以定时执行,但设置 ITIMER_VIRTUAL 和 ITIMER_PROF 类型的,却一直不执行,非常郁闷。

最后发现是因为我的测试代码过于简单,使用 CPU 的时间没有达到设置的定时间隔!后来加了一个无限循环执行的子线程,就正常了。

这充分说明了后两种模式的特点:以进程的 CPU 时间作为定时间隔!

ISO C++ requires field designators to be specified in declaration order; field ‘it_value’ will be initialized after field ‘it_interval’
初始化结构体时,参数顺序不能乱穿,需要按照定义的顺序

高级定时器 timer_t

interval timer 很不错,支持根据 CPU 时间作为定时器,但它还不够强大。

在 Linux 中,最强大的定时器是 timer_t,它提供了三个函数,用于创建、初始化和移除:

  • timer_create
  • timer_settime
  • timer_delete
int timer_delete(timer_t __timer);
int timer_gettime(timer_t __timer, struct itimerspec* __ts);
int timer_getoverrun(timer_t __timer);

(1)使用 timer_create 创建定时器:

int timer_create(clockid_t __clock, struct sigevent* __event, timer_t* __timer_ptr);
  • 第一个参数是前面介绍过的,时钟类型,也支持 walltime 和 CPU time
  • 第二个参数是要发送的信号事件 (若不传 sigevent,默认发 SIGALRM,不注册信号处理函数会崩溃)
  • 第三个参数要初始化的 timer_t 地址

sigevent 详情:

typedef struct sigevent 
  sigval_t sigev_value;
  int sigev_signo;	//到时后要发送的信号
  int sigev_notify;	//可以指定不同的行为
  union 
    int _pad[SIGEV_PAD_SIZE];
    int _tid;
    struct 
      void(* _function) (sigval_t);		//定时执行的函数
      void * _attribute;
     _sigev_thread;
   _sigev_un;
 sigevent_t;

可以看到,这个 sigevent 可以配置的点很多,包括要发送的信号 (sigev_signo),执行的函数等,其中 sigev_notify 比较关键,它支持这几种类型:

#define SIGEV_SIGNAL 0	//定时器到点后,内核给进程发的信号是 sigev_signo,但信号处理函数里应该用参数的 sigev_value (?? 不太明白什么场景需要,用于传递数据?)
#define SIGEV_NONE 1	//到点后什么都不干
#define SIGEV_THREAD 2	//到店后,创建一个新线程(每个定时器只会创建一个线程),执行 sigev_notify_function,返回后终止(可以通过 _sigev_thread 修改线程的行为)

创建仅仅指定了时钟类型,和信号事件,还没有设置时间,接下来我们需要初始化定时器。

(2)使用 timer_settime 初始化定时器

int timer_settime(timer_t __timer, int __flags, const struct itimerspec* __new_value, struct itimerspec* __old_value);
  • 第一个参数是我们刚创建的 timer_t
  • 第二个参数用于设置是绝对时间还是相对时间
  • 第三个参数是定时时间(精度为纳秒)
  • 第四个参数是用于保存之前的定时时间的指针

(3)执行完成后,调用 timer_delete 即可停止

举个例子:

static timer_t sample_timer;
static int timer_exec_time;

void alarm_handler_timer_t(sigval_t sigval) 
    LOG("alarm_handler_timer_t called, %d , tid: %d", sigval, gettid());
    int signo = sigval.sival_int;
    LOG("[timer_t sig handler]called: %d, is SIGALRM? %d, is SIGVTALRM? %d, is SIGPROF? %d",
        signo, signo == SIGALRM, signo == SIGVTALRM, signo == SIGPROF);

    struct itimerspec its;
    int ret = timer_gettime(sample_timer, &its);
    LOG("timer_gettime ret: %d , interval: %d", ret, its.it_interval.tv_sec);
    timer_exec_time++;

    if (timer_exec_time > 5) 
        ret = timer_delete(sample_timer);
        LOG(" [timer_delete ] >>>> %d", ret);
    


//高级定时器
void test_timer_t() 

    //1.创建定时器
    struct sigevent _sigevent;
    _sigevent.sigev_signo = SIGPROF;
    _sigevent.sigev_notify = SIGEV_THREAD;
    _sigevent.sigev_notify_function = alarm_handler_timer_t;
//    _sigevent.sigev_signo = SIGUSR1;
//    _sigevent.sigev_notify = SIGEV_SIGNAL;  //发送信号,但改变 value
    _sigevent.sigev_notify_function = alarm_handler_timer_t;
    //不传递 sigevent,默认发 SIGALRM
    int ret = timer_create(CLOCK_REALTIME, &_sigevent, &sample_timer);
    LOG("timer_create ret: %d, tid: %d", ret, gettid());

    //2.设置时间

    struct itimerspec its;
    its.it_value.tv_sec = 1;
    its.it_value.tv_nsec = 0;
    its.it_interval.tv_sec = 3;
    its.it_interval.tv_nsec = 0;
    ret = timer_settime(sample_timer, 0, &its, nullptr);
    LOG("timer_settime ret: %d", ret);


总结

这篇文章主要讲了 Linux 中获取当前 wall time 和 cpu time 的几种方式,以及睡眠、阻塞的方式,还有三种定时器。

获取 cpu time 一般用于性能监控、分析,在某些情况下,我们需要了解到进程、线程的真实 cpu 占用情况;Linux 定时器的一个典型场景是采样监控,比如定时抓取堆栈聚合得出 cpu 使用时间高的函数。

Thanks

《Linux 系统编程》

以上是关于Linux 编程之时间篇:wall time, cpu time 和 timer的主要内容,如果未能解决你的问题,请参考以下文章

Linux 编程之时间篇:wall time, cpu time 和 timer

linux里面线程编译运行问题

Python3 与 C# 并发编程之~ 进程先导篇

linux网络编程之-----基础理论篇

UNIX 中的 wall-clock-time、user-cpu-time 和 system-cpu-time 具体是啥?

linux篇linux进程(上)