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
UNIX 中的 wall-clock-time、user-cpu-time 和 system-cpu-time 具体是啥?