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?
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}