Detect time jumps in Linux

Posted by Marcus Folkesson on Monday, November 11, 2024

Detect time jumps in Linux

Recently I had a case where the application created multiple timers for different events. After the timers was created, the NTP client updates the system time resulting in that all timers being off and needing to be adjusted. Adjust the timers is one thing, but how do we detect the time jump?

It's not the first time I'm in a situation where I have to deal with such time jumps, I guess it's quite a common case, so why not write a few words about it?

/media/tux-time-jump.png

The realtime clock

The clock that the NTP client adjusts is the real-time clock, that is, CLOCK_REALTIME. There are several other clocks [1] in a Linux system, but most of them are not affected by these discontinuous jumps.

Detect time jumps

To detect time jumps we will create a realtime timer with the TFD_TIMER_CANCEL_ON_SET flag. The flag is described in the man page [2] for timerfd_create():

TFD_TIMER_CANCEL_ON_SET
        If this flag is specified along with TFD_TIMER_ABSTIME and
        the clock for this timer is CLOCK_REALTIME or
        CLOCK_REALTIME_ALARM, then mark this timer as cancelable
        if the real-time clock undergoes a discontinuous change
        (settimeofday(2), clock_settime(2), or similar).  When
        such changes occur, a current or future read(2) from the
        file descriptor will fail with the error ECANCELED.

Also, see the NOTES section in the same man page:

NOTES
Suppose the following scenario for CLOCK_REALTIME or
CLOCK_REALTIME_ALARM timer that was created with
timerfd_create():

        (1)  The timer has been started (timerfd_settime()) with the
        TFD_TIMER_ABSTIME and TFD_TIMER_CANCEL_ON_SET flags;

        (2)  A discontinuous change (e.g., settimeofday(2)) is
        subsequently made to the CLOCK_REALTIME clock; and

        (3)  the caller once more calls timerfd_settime() to rearm the
        timer (without first doing a read(2) on the file
                        descriptor).

        In this case the following occurs:

        •  The timerfd_settime() returns -1 with errno set to ECANCELED.
        (This enables the caller to know that the previous timer was
         affected by a discontinuous change to the clock.)

        •  The timer is successfully rearmed with the settings provided
        in the second timerfd_settime() call.  (This was probably an
                        implementation accident, but won't be fixed now, in case there
                        are applications that depend on this behaviour.)

So, basically what we have to do is:

  • Create a timer for the CLOCK_REALTIME clock using timerfd_create()
  • Use timerfd_settime() with the flags TFD_TIMER_ABSTIME and TFD_TIMER_CANCEL_ON_SET
  • Read from the file descriptor or, in case of O_NONBLOCK, read the return value and look for ECANCELED.

Example

If we put everything together we end up with something like this:

 1#include <cstdlib>
 2#include <cstdint>
 3#include <cstdio>
 4#include <iostream>
 5#include <limits>
 6
 7#include <unistd.h>
 8#include <sys/timerfd.h>
 9
10int main(int argc, char *argv[])
11{
12	struct itimerspec timer;
13	timer.it_value.tv_sec = std::numeric_limits<time_t>::max();
14
15	uint64_t val;
16	int fd;
17	int ret;
18
19	fd = timerfd_create(CLOCK_REALTIME, 0);
20	if (fd == -1) {
21		std::cerr << "timerfd_create failed" << std::endl;
22		return EXIT_FAILURE;
23	}
24
25	if (timerfd_settime(fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &timer, NULL) == -1) {
26		std::cerr << "timerfd_settime failed" << std::endl;
27		return EXIT_FAILURE;
28	}
29
30	while (1) {
31		// Will block until time is changed
32		ret = read(fd, &val, sizeof(uint64_t));
33		std::cout << "Time has changed!" << std::endl;
34	}
35
36	exit(EXIT_SUCCESS);
37}