-
Notifications
You must be signed in to change notification settings - Fork 66
Utilizing Monotonic Clocks for Timers
Version: 2.2.5.6
In this update, the internal structures oc_timer
and oc_etimer
have undergone significant revisions to incorporate the use of a monotonic clock instead of the absolute time system clock (CLOCK_REALTIME to CLOCK_MONOTONIC/CLOCK_MONOTONIC_RAW on a POSIX system). The purpose of these timers is to measure durations accurately, and the adoption of a steady monotonic clock provides a more reliable implementation.
The shift from relying on the system clock is motivated by its vulnerability to system time changes. Such changes can occur either through user intervention or by synchronization with a timer server. In a particularly problematic scenario, adjusting the system time backward by one hour would result in no timers firing for at least one hour.
By transitioning to a monotonic clock, the timers become immune to variations in the system time. This ensures consistent and predictable behavior, eliminating the risk of timers failing to trigger due to unexpected system time adjustments.
Monotonic clock has been implemented on Linux, Android, Windows, and ESP32. The new available function oc_clock_time_monotonic
should return a steady clock not affected by time adjustments, which counts ticks since boot time or some other point in the past.
On Arduino, OpenThread, and Zephyr, the oc_clock_time_monotonic
is not yet implemented, and the implementation fallbacks to oc_clock_time
.
To check at runtime whether monotonic clock is available, call oc_clock_time_has_monotonic_clock
.
Changes to timers affect the main event loop. The standard implementation of the loop in applications previously used oc_main_poll
which returns the absolute time of the next event. This time was used to calculate the next wake-up time.
while (!terminated) {
oc_clock_time_t next_event = oc_main_poll();
pthread_mutex_lock(&mutex);
if (next_event == 0) {
pthread_cond_wait(&cv, &mutex);
} else {
struct timespec ts;
ts.tv_sec = (next_event / OC_CLOCK_SECOND);
ts.tv_nsec = (next_event % OC_CLOCK_SECOND) * 1.e09 / OC_CLOCK_SECOND;
pthread_cond_timedwait(&cv, &mutex, &ts);
}
pthread_mutex_unlock(&mutex);
}
The event loop utilizes oc_etimer
, so the returned time was an absolute time obtained by oc_clock_time
. However, since oc_etimer
has been reworked, it no longer returns absolute time. It still uses oc_etimer
, but to avoid changing the behavior of oc_main_poll
, it calculates the absolute time of the next event based on the monotonic time of the next event, the current monotonic time, and the current absolute time. oc_main_poll
has been deprecated, and the new call oc_main_poll_v1
is intended to replace it. It returns the time of the next event in monotonic time.
The new run loop on Linux should utilize oc_main_poll_v1
. By default, pthread_cond_timedwait
uses absolute time (CLOCK_REALTIME clock), but newer Linux platforms support the pthread_condattr_setclock
system call. It allows changing the clock used by the condition variable. In the IoT library, the POSIX CLOCK_MONOTONIC_RAW is used to get monotonic time, unfortunately, this clock is not supported by pthread_condattr_setclock
, so the time returned by oc_main_poll_v1
must be translated to time in CLOCK_MONOTONIC before it can be used by pthread_cond_timedwait
.
For example, on Ubuntu 22.04, a possible implementation looks like this:
int main() {
// ...
pthread_mutex_t mutex;
pthread_cond_t cv;
pthread_condattr_t attr;
err = pthread_condattr_init(&attr);
if (err != 0) {
return -1;
}
err = pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
if (err != 0) {
pthread_condattr_destroy(&attr);
return -1;
}
err = pthread_cond_init(&cv, &attr);
if (err != 0) {
pthread_condattr_destroy(&attr);
return -1;
}
pthread_condattr_destroy(&attr);
while (!terminated) {
oc_clock_time_t next_event_mt = oc_main_poll_v1();
pthread_mutex_lock(&mutex);
if (oc_main_needs_poll()) {
pthread_mutex_unlock(&mutex);
continue;
}
if (next_event_mt == 0) {
pthread_cond_wait(&cv, &mutex);
} else {
struct timespec next_event = { 0, 0 };
oc_clock_time_t next_event_cv;
if (oc_clock_monotonic_time_to_posix(next_event_mt, CLOCK_MONOTONIC,
&next_event_cv)) {
next_event = oc_clock_time_to_timespec(next_event_cv);
}
pthread_cond_timedwait(&cv, &mutex, &next_event);
}
pthread_mutex_unlock(&mutex);
}
// ...
}
To ensure correct synchronization when using this form the run loop your signalization callback must call pthread_cond_signal
with locked mutex
.
void
signal_event_loop(void)
{
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cv);
pthread_mutex_unlock(&mutex);
}
Condition variables on Windows use relative time, so the migration is simpler:
int main() {
// ...
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
CONDITION_VARIABLE cv;
InitializeConditionVariable(&cv);
while (!terminated) {
oc_clock_time_t next_event_mt = oc_main_poll_v1();
EnterCriticalSection(&cs);
if (oc_main_needs_poll()) {
LeaveCriticalSection(&cs);
continue;
}
if (next_event_mt == 0) {
SleepConditionVariableCS(&cv, &cs, INFINITE);
} else {
oc_clock_time_t now_mt = oc_clock_time_monotonic();
if (now_mt < next_event_mt) {
SleepConditionVariableCS(
&cv, &cs, (DWORD)((next_event_mt - now_mt) * 1000 / OC_CLOCK_SECOND));
}
}
LeaveCriticalSection(&g_cs);
}
// ...
}
To ensure correct synchronization when using this form the run loop your signalization callback must call WakeConditionVariable
inside the critical section cs
.
void
signal_event_loop(void)
{
EnterCriticalSection(&cs);
WakeConditionVariable(&cv);
LeaveCriticalSection(&cs);
}
A race condition between pthread_cond_signal and pthread_cond_wait/pthread_cond_timedwait (Linux, Android, ESP32); WakeConditionVariable and SleepConditionVariableCS (Windows) was found in an earlier version of the code snippets. See https://github.com/iotivity/iotivity-lite/issues/519 . The code has been updating to avoid the race condition.
- Implement scheduling at an absolute time (update).
- Update ESP32 port to use
oc_main_poll_v1
. - Update Android port to use
oc_main_poll_v1
.
By following these steps, the utilization of monotonic clocks for timers will be more efficient and reliable across different platforms, ensuring accurate measurement of durations and eliminating the impact of system time changes.