Linux programming time: wall time, cpu time and timer

Getting time in Java/JS is very simple (system. Currenttimemillis()) nanoTime(); new Date(). Gettime (), etc.), which is a little more complicated in C/C + +.

Today, let's learn about time related concepts and acquisition methods in Linux C programming.

Linux time related knowledge

The UNIX system uses the elapsed time since 0:00 on January 1, 1970 as the absolute time.

In other words, in UNIX system, absolute time is also relative.

Software clock: kernel initialization, counter of specific frequency, and a timer interval is called a tick or jiffy. In UNIX system, the system timing frequency is called HZ.

The specific size of HZ is related to the architecture. On x86, the value is 100, that is, it runs 100 times a second; So an interval jiffy is equal to 1 / 100 of a second.

Get the tck of the current device (how many times per second):

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

Output content:

zsx_linux: _SC_CLK_TCK >> 100

Hardware clock: most computers have a battery powered hardware clock. When the kernel is turned off, the date and time will be stored and read at startup. In this way, the time is correct even if the computer is accidentally shut down and turned on again.

wall time: the time on the wall, that is, the real time

Get current time

time_t (second)

typedef __time_t time_t;

time_t represents the number of seconds elapsed since the new era, defined in < time h> In, you can get the following through the time() function:

    //Get the current time (the returned time is actually a relative time, the number of seconds elapsed since January 1, 1970)
    //It's not accurate. This value assumes that all the years that can be rounded by 4 are leap years
    time_t t;
    auto time_ret = time(&t);
    LOG("time_t > Current time: %ld, time_ret: %ld", t, time_ret);

The running result of the above code:

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

Sometimes we need to obtain the current month, day, hour, minute and second. Linux also provides the corresponding struct and acquisition method.

tm

tm (time meaning?) The structure contains the time information of each dimension:

struct tm {
  int tm_sec;
  int tm_min;
  int tm_hour;
  int tm_mday;
  int tm_mon;	//Months since January
  int tm_year;	//Years since 1900
  int tm_wday;	//Days since Sunday (0-6)
  int tm_yday;	//The day of this year (0 ~ 365)
  int tm_isdst;
  long int tm_gmtoff;
  const char* tm_zone;
};

We can get time through gmtime_ T specific information of time:

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

    //Convert the time into hour, minute and second of month, day and year
    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);

Operation results:

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

Timeout (microseconds)

timeval can obtain the time accurate to the microsecond level. Its structure is as follows:

struct timeval {
  __kernel_time_t tv_sec;
  __kernel_suseconds_t tv_usec;
};

Get the current time through gettimeofday:

int gettimeofday(struct timeval* __tv, struct timezone* __tz);
long long get_current_time_millis() {
    //Microsecond accuracy
    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;
}

Call result:

current_millis > 1642332541496
//Time at that time_t: 1642332541

timespec (nanosecond)

timespec, as its name suggests, can get more specific time: nanoseconds!

Its structure:

struct timespec {
  __kernel_time_t tv_sec;
  long tv_nsec;
};

You can see that the difference between and timeval is the second member variable of the structure.

timespec can be accessed through clock_gettime get.

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

clock_ The first parameter of gettime is clockid_t refers to a specific POSIX clock, including:

  • CLOCK_MONITONIC: the time after the system is started, increasing monotonically
  • CLOCK_REALTIME: wall time, real time (this is portable)
  • CLOCK_PROCESS_CPUTIME_ID: CPU time of the process
  • CLOCK_THREAD_CPUTIME_ID: CPU time of thread

CLOCK_PROCESS_CPUTIME_ID and CLOCK_THREAD_CPUTIME_ID can provide higher precision time, nanosecond level.

We can use clock_getres checks the precision of different types of time sources. It will return the precision of various time sources in seconds and nanoseconds.

        struct timespec ts{};
        //1. Get the clockid_t specified [time precision]
        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);
        }

For example, through clock_getres obtains the accuracy of different POSIX clocks and through clock_gettime get the time corresponding to the clock:

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. Obtain the [time precision] specified by 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. Save the time source specified by clockid to timespec
        ret = clock_gettime(clockid, &ts);
        if (!ret) {
            LOG("clock_gettime: %d, sec: %ld, nsec: %ld \n", clockid, ts.tv_sec, ts.tv_nsec);
        }
    }
}

When the above code runs, it will output the real time, startup time, process CPU time and thread CPU time at that time:

//Real clock
zsx_linux: clock_getres >>> clockid: 0, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 0, sec: 1642333116, nsec: 488803273 
//Monotonic clock
zsx_linux: clock_getres >>> clockid: 1, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 1, sec: 414690, nsec: 13117483
//Process CPU time
zsx_linux: clock_getres >>> clockid: 2, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 2, sec: 0, nsec: 358629050
//Thread CPU time
zsx_linux: clock_getres >>> clockid: 3, sec: 0, nsec: 1 
zsx_linux: clock_gettime: 3, sec: 0, nsec: 333296654

Gets the CPU time of the process

In addition to the above CLOCK_PROCESS_CPUTIME_ID clock, we can also obtain the process time of the current process and sub process through the tims function.

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

The results are expressed in tms:

struct tms {
  __kernel_clock_t tms_utime;	//User status time, the time of executing user code
  __kernel_clock_t tms_stime;	//Kernel state time, execution time of syscall and pagefault, etc
  __kernel_clock_t tms_cutime;	//Child process, user status time of child process
  __kernel_clock_t tms_cstime;	//Kernel state time of subprocess
};

Note that the member variables of tms are all tck/jiffy numbers

void test_get_cpu_time() {

    struct tms buf{};
    //Get the cpu [number of clockticks] consumed by the initiating process and sub process, not the time?
    clock_t clock = times(&buf);

    //User mode time includes not only the time of executing kernel code, but also the time of triggering kernel behavior such as pagefault
    LOG("Process utime: %ld, stime: %ld", buf.tms_utime, buf.tms_stime);
    //The time statistics of the child process ends when the child process exits and the parent process calls waitpid (or related function)
    LOG("Child process utime: %ld, stime: %ld", buf.tms_cutime, buf.tms_cstime);
}

Sleep and obstruction

What's the difference between sleep and obstruction?

Sleep, in essence, will still occupy the CPU, but does not execute code, wasting CPU; Blocking will give up the CPU. When it needs to continue execution, the kernel will wake up the process again from blocking to running state.

When sleep is frequent and short, blocking is a better way.

Several ways of sleep

  • sleep, in seconds
  • usleep, in microseconds
  • nanosleep, in nanoseconds
  • clock_nanosleep, in nanoseconds
#include <unistd.h>

//Returns the number of seconds left without sleep, usually 0, unless interrupted by a signal
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_ The difference between nanosleep and the first three is that you can set the specified clock source, such as real time and relative time (sleep CPU time is meaningless).

block

If you want to compare sleep, you can also achieve blocking through select.

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

This call can be used to make the process sleep:

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));
}

Test results:

zsx_linux: block time: 2003

Using select to block the file descriptor has better performance. It will give up the CPU and enter the blocking state. After the time is exceeded, the kernel will wake up the process instead of letting the process cycle continuously.

timer

Linux also supports the mechanism of delaying the notification process after a certain time, that is, timers. These timers are supported:

  • alarm
  • interval timer
  • timer_create

alarm

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

alarm will send SIGALRM signal to the calling process after N seconds (real time), for example:

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

void test_alarm() {
    signal(SIGALRM, alarm_handler);

    //Delayed for 3 seconds, one-time
    alarm(3);
}

It will only be executed once.

If there is an unsent alarm before sending, the previous alarm will be cancelled.

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);

The difference between timer and alarm is that it can be executed not only once, but also support different signals:

#define ITIMER_REAL 0 	// Send SIGALRM after real time
#define ITIMER_VIRTUAL 1 	// Send SIGVTALRM after process utime
#define ITIMER_PROF 2 	// After utime + stime, send SIGPROF

We can pass the test in this way:

    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);

During my testing, I found that itimer was set_ Real type timer can be executed regularly, but itimer is set_ Virtual and ITIMER_PROF type, but it has not been implemented, which is very depressed.

Finally, it was found that my test code was too simple, and the time of using CPU did not reach the set timing interval! Later, a sub thread of infinite loop execution was added, and it was normal.

This fully illustrates the characteristics of the latter two modes: Taking the CPU time of the process as the timing interval!

ISO C++ requires field designators to be specified in declaration order; field 'it_value' will be initialized after field 'it_interval'
When initializing a structure, the order of parameters cannot be disorderly. It needs to follow the defined order

Advanced timer_t

interval timer is very good. It supports timer based on CPU time, but it is not powerful enough.

In Linux, the most powerful timer is timer_t. It provides three functions for creating, initializing, and removing:

  • 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) Using timer_create timer:

int timer_create(clockid_t __clock, struct sigevent* __event, timer_t* __timer_ptr);
  • The first parameter is the clock type described earlier. It also supports walltime and CPU time
  • The second parameter is the signal event to be sent (if sigevent is not sent, SIGALRM will be sent by default, and the signal processing function will crash if it is not registered)
  • The third parameter is the timer to initialize_ T address

sigevent details:

typedef struct sigevent {
  sigval_t sigev_value;
  int sigev_signo;	//Signal to be sent after that time
  int sigev_notify;	//You can specify different behaviors
  union {
    int _pad[SIGEV_PAD_SIZE];
    int _tid;
    struct {
      void(* _function) (sigval_t);		//Timed function
      void * _attribute;
    } _sigev_thread;
  } _sigev_un;
} sigevent_t;

It can be seen that there are many points that can be configured for this sigevent, including the signal to be transmitted (sigev_signo), the functions to be executed, etc., in which sigev_notify is critical. It supports these types:

#define SIGEV_SIGNAL 0 	// After the timer arrives, the signal sent by the kernel to the process is sigev_signo, but the sigev of the parameter should be used in the signal processing function_ Value (? I don't quite understand what scenario needs to be used to transfer data?)
#define SIGEV_NONE 1 	// Don't do anything when it's time
#define SIGEV_THREAD 2 	// After arriving at the store, create a new thread (each timer will only create one thread) to execute sigev_notify_function, terminate after returning (you can modify the behavior of the thread through _sigev_thread)

The creation only specifies the clock type and signal event, and the time has not been set. Next, we need to initialize the timer.

(2) Using timer_settime initialization timer

int timer_settime(timer_t __timer, int __flags, const struct itimerspec* __new_value, struct itimerspec* __old_value);
  • The first parameter is the timer we just created_ t
  • The second parameter is used to set whether it is absolute time or relative time
  • The third parameter is the timing time (with an accuracy of nanoseconds)
  • The fourth parameter is the pointer used to save the previous timing time

(3) after execution, call timer_. Delete to stop

for instance:

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);
    }
}

//Advanced timer
void test_timer_t() {

    //1. Create timer
    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;  // Send signal, but change value
    _sigevent.sigev_notify_function = alarm_handler_timer_t;
    //sigevent is not passed, and SIGALRM is sent by default
    int ret = timer_create(CLOCK_REALTIME, &_sigevent, &sample_timer);
    LOG("timer_create ret: %d, tid: %d", ret, gettid());

    //2. Set time

    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);

}

summary

This article mainly talks about several ways to obtain the current wall time and cpu time in Linux, as well as the ways of sleep and blocking, as well as three timers.

Obtaining cpu time is generally used for performance monitoring and analysis. In some cases, we need to know the real cpu occupation of processes and threads; A typical scenario of Linux timers is sampling monitoring, such as regularly fetching stack aggregation to get functions with high cpu usage time.

Thanks

Linux system programming

Keywords: Linux Operation & Maintenance server

Added by robmarston on Sat, 19 Feb 2022 18:44:13 +0200