Skip to content

Commit 9e3c648

Browse files
committed
alarm: rework alarm virtualization
Fixes two bugs in alarm virtualization. 1. Addresses an unaccounted for case when inserting new alarms into the global list of outstanding alarms. In particular, it's possible for only `cur` to overflow and still happen before the alarm being added (see inline comments added to `root_insert`. 2. Rewrites the virtualization to avoid the possibility of alarms being added while the virtualizing upcall is iterating for expired alarms.
1 parent d74aaaf commit 9e3c648

File tree

4 files changed

+210
-21
lines changed

4 files changed

+210
-21
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Makefile for user application
2+
3+
# Specify this directory relative to the current application.
4+
TOCK_USERLAND_BASE_DIR = ../../..
5+
6+
# Which files to compile.
7+
C_SRCS := $(wildcard *.c)
8+
9+
# Include userland master makefile. Contains rules and flags for actually
10+
# building the application.
11+
include $(TOCK_USERLAND_BASE_DIR)/AppMakefile.mk
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Test Multiple Alarms (Ensure previous overflowing alarms aren't ignore)
2+
3+
This tests the virtual alarms available to userspace. It sets three
4+
alarms alarms. The first two overflow and should result in multiple
5+
alarms under the hood, and a third a 10 second alarm started after the
6+
second. The first alarm is set first, but expires second (1 second
7+
after overflow vs 10ms after overflow). The third alarm should expire
8+
last.
9+
10+
When successful, the first alarm should fire after a clock overflow +
11+
1 second, while the third should fire after a clock overflow + 10ms +
12+
10 seconds.
13+
14+
If the virtual alarm library is buggy, this test might fail by never
15+
firing the first alarm or firing it after the third alarm
16+
(specifically almost another clock overflow) because either alarm
17+
insertion or virtual alarm upcall handing misses it after the second
18+
alarm fires.
19+
20+
# Example Output
21+
22+
Correct:
23+
24+
(after an entire clock overflow, ~8 minutes)
25+
26+
```
27+
1 10498816 10496827
28+
2 86100992 86099200
29+
```
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
4+
#include <libtock-sync/services/alarm.h>
5+
6+
static void event_cb(uint32_t now, uint32_t expiration, void* ud) {
7+
int i = (int)ud;
8+
printf("%d %lu %lu\n", i, now, expiration);
9+
}
10+
11+
int main(void) {
12+
libtock_alarm_t t1;
13+
libtock_alarm_t t2;
14+
15+
uint32_t overflow_ms = libtock_alarm_ticks_to_ms(UINT32_MAX);
16+
17+
libtock_alarm_in_ms(overflow_ms + 1000, event_cb, (void*)1, &t1);
18+
libtocksync_alarm_delay_ms(overflow_ms + 10);
19+
libtock_alarm_in_ms(10000, event_cb, (void*)2, &t2);
20+
21+
while (1) {
22+
yield();
23+
}
24+
}

libtock/services/alarm.c

Lines changed: 146 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,39 @@
11
#include "alarm.h"
22
#include <assert.h>
3-
#include <limits.h>
43
#include <stdlib.h>
54

65
#define MAX_TICKS UINT32_MAX
76

7+
/** \brief Checks if `now` is between `reference` and `dt`, meaning
8+
* the alarm has not yet expired.
9+
*
10+
* Invariants:
11+
* 1. `now` hasn't wrapped `reference`, i.e., in an infinite space,
12+
* `now` - `reference` <= MAX_TICKS
13+
* 2. `dt` is at most MAX_TICKS, such that `reference + dt` at most
14+
* wraps back to `reference`.
15+
* 3. `now` happens after `reference`, so if `now` is not "within"
16+
* `reference` and `dt`, it is after, not before.
17+
* 4. `now` may be larger or smaller than `reference` since ticks
18+
* wrap.
19+
*
20+
* Corrolaries:
21+
* - if `now` is larger than `reference`, `now - reference` is
22+
* directly comparable to `dt` (1 & 2)
23+
* - if `now` is less than reference, `now - reference` wraps, and is
24+
* - comparable to `dt` because `now` is guaranteed to happened after
25+
* `reference` (3)
26+
*
27+
* The result is that the check is fairly simple, but its simplicitly
28+
* relies specifically on `now` never happening before `reference`,
29+
* and thus tied to the specific logic in `alarm_upcall` (see inline
30+
* comment on delaying executing callbacks until after checking all
31+
* outstanding alarms).
32+
*/
33+
static bool is_within(uint32_t now, uint32_t reference, uint32_t dt) {
34+
return now - reference < dt;
35+
}
36+
837
/** \brief Convert milliseconds to clock ticks
938
*
1039
* WARNING: This function will assert if the output
@@ -101,14 +130,20 @@ static void root_insert(libtock_alarm_ticks_t* alarm) {
101130
bool cur_overflows = (*cur)->reference > (UINT32_MAX - (*cur)->dt);
102131

103132
// This alarm (`cur`) happens after the new alarm (`alarm`) if:
104-
// - both overflow or neither overflow, and cur expiration is
133+
//
134+
// - both overflow or neither overflow and cur expiration is
105135
// larger than the new expiration
106-
// - or, only cur overflows
136+
// - only cur overflows, cur's reference is before new reference
137+
// (both started this epoch, cur must expire next epoch)
138+
// - only cur overflows, cur's reference is after new reference
139+
// (cur started in previous epoch, cur also must expire this
140+
// epoch) and cur's expiration is larger than the new
141+
// expiration.
107142
//
108143
// If the new alarm overflows and this alarm doesn't, this alarm
109144
// happens _before_ the new alarm.
110145
if (((cur_overflows == new_overflows) && (cur_expiration > new_expiration)) ||
111-
cur_overflows) {
146+
(cur_overflows && ((*cur)->reference < alarm->reference || cur_expiration > new_expiration))) {
112147
// insert before
113148
libtock_alarm_ticks_t* tmp = *cur;
114149
*cur = alarm;
@@ -145,27 +180,118 @@ static libtock_alarm_ticks_t* root_peek(void) {
145180
return root;
146181
}
147182

183+
/** \brief Upcall for internal virtual alarms
184+
*
185+
* This upcall checks the ordered list of outstanding alarms for
186+
* expired alarms, removes them from the list, and invokes their
187+
* callbacks.
188+
*
189+
* Invariants:
190+
* 1. The list of outstanding alarms is ordered by absolute expiration time.
191+
* 2. The alarm event that invoked this upcall is for the current `head` of the list.
192+
* 3. No alarms are added (or re-added) to the list between while
193+
* iterating through alarms
194+
* 4. For each alarm, `alarm->dt < MAX_TICKS + 1`
195+
*
196+
* Corrollaries:
197+
* - If the head of the list hasn't expired, no alarms in the tail of
198+
* the list have expired (1)
199+
* - `scheduled` happens after `head->reference` (2)
200+
* - `scheduled` happens before `head->reference + MAX_TICKS + 1` (4)
201+
* - For each alarm in the list, `now` happens after
202+
* `alarm->reference` (2, 3)
203+
* - For each alarm, `scheduled` cannot have cannot have wrapped
204+
* `alarm->reference` (1, 4)
205+
* - For each alarm, if `scheduled` and `now` are not on the same side of
206+
* `alarm->reference`, the alarm must have expired (1, 4)
207+
*
208+
* Critically, this upcall cannot allow any alarms to be added to the
209+
* ordered list while iterating by invoking upcalls, as that could
210+
* violate invariant (3) and result in `now` happening before some
211+
* `alarm->reference`.
212+
*
213+
* Some alarms that expire between `now` and the end of the upcall may
214+
* be "missed", which may mean they are delivered later. They should
215+
* still show up first in the list at the end, so will fire next.
216+
*/
148217
static void alarm_upcall(__attribute__ ((unused)) int kernel_now,
149-
__attribute__ ((unused)) int scheduled,
218+
int scheduled0,
150219
__attribute__ ((unused)) int unused2,
151220
__attribute__ ((unused)) void* opaque) {
221+
// `tocall` is a temporary list to keep track of expired alarms to call later.
222+
libtock_alarm_ticks_t* tocall = NULL;
223+
libtock_alarm_ticks_t* tocall_last = NULL;
224+
225+
// We want `scheduled` as unsigned so wrapping math works out correctly.
226+
uint32_t scheduled = scheduled0;
227+
228+
// Take the current tick value. We could use `kernel_now`, but would
229+
// potentially unnecessarily delay some alarms.
230+
uint32_t now;
231+
libtock_alarm_command_read(&now);
232+
233+
// We know this upcall is associated with the head, so it's easier
234+
// to deal with.
235+
libtock_alarm_ticks_t* head = root_pop();
236+
237+
// Formally, we should be able to just add head to
238+
// `tocall`, but let's just be defensive just in case there is an
239+
// errant alarm.
240+
assert(head != NULL && !is_within(scheduled, head->reference, head->dt));
241+
head->next = NULL;
242+
head->prev = NULL;
243+
tocall = head;
244+
tocall_last = head;
245+
246+
// Now iterate through the remaining alarms in case any of them have
247+
// also expired.
152248
for (libtock_alarm_ticks_t* alarm = root_peek(); alarm != NULL; alarm = root_peek()) {
153-
uint32_t now;
154-
libtock_alarm_command_read(&now);
155-
// has the alarm not expired yet? (distance from `now` has to be larger or
156-
// equal to distance from current clock value.
157-
if (alarm->dt > now - alarm->reference) {
158-
libtock_alarm_command_set_absolute(alarm->reference, alarm->dt);
249+
// has the alarm not expired yet?
250+
// Three cases:
251+
// 1. scheduled - reference is larger than now - reference:
252+
// ticks have wrapped reference, alarm must have expired.
253+
// 2. scheduled is no longer within reference + dt (alarm
254+
// expired)
255+
// 3. now is no longer within reference + dt (alarm expired)
256+
//
257+
// Simpler to check the non-expiring case first.
258+
if ((now - alarm->reference >= scheduled - alarm->reference) &&
259+
is_within(scheduled, alarm->reference, alarm->dt) &&
260+
is_within(now, alarm->reference, alarm->dt)) {
261+
// Nope, has not expired, nothing "after" this alarm has
262+
// expired either, since we add alarms in expiration order.
159263
break;
160264
} else {
265+
// Expired, add to `tocall` list.
161266
root_pop();
267+
alarm->next = NULL;
268+
alarm->prev = tocall_last;
269+
// If this expired, head must have also expired, so `tocall` and
270+
// `tocall_last` are non-null, so just add to the end.
271+
tocall_last->next = alarm;
272+
}
273+
}
162274

163-
if (alarm->callback) {
164-
uint32_t expiration = alarm->reference + alarm->dt;
165-
alarm->callback(now, expiration, alarm->ud);
166-
}
275+
for (libtock_alarm_ticks_t* alarm = tocall; alarm != NULL; alarm = tocall) {
276+
alarm->prev = NULL;
277+
tocall = alarm->next;
278+
alarm->next = NULL;
279+
if (alarm->callback) {
280+
uint32_t expiration = alarm->reference + alarm->dt;
281+
alarm->callback(now, expiration, alarm->ud);
167282
}
168283
}
284+
285+
head = root_peek();
286+
if (head != NULL) {
287+
// TODO(alevy): At this point, is it possible we've wrapped so far
288+
// past `reference` that we might end up delaying a technically
289+
// expired alarm by another timer wrap? I think technically yes,
290+
// though techincally the interface only guarantees alarms will
291+
// delay *at least* `dt`, so more is fine, and we could be delayed
292+
// arbitrarily long by the kernel anyway.
293+
libtock_alarm_command_set_absolute(head->reference, head->dt);
294+
}
169295
}
170296

171297
static int libtock_alarm_at_internal(uint32_t reference, uint32_t dt, libtock_alarm_callback cb, void* ud,
@@ -178,10 +304,6 @@ static int libtock_alarm_at_internal(uint32_t reference, uint32_t dt, libtock_al
178304
alarm->next = NULL;
179305

180306
root_insert(alarm);
181-
int i = 0;
182-
for (libtock_alarm_ticks_t* cur = root_peek(); cur != NULL; cur = cur->next) {
183-
i++;
184-
}
185307

186308
if (root_peek() == alarm) {
187309
libtock_alarm_set_upcall((subscribe_upcall*)alarm_upcall, NULL);
@@ -235,8 +357,10 @@ static void overflow_callback(__attribute__ ((unused)) uint32_t now,
235357
// schedule next intermediate alarm that will overflow
236358
tock_timer->overflows_left--;
237359

360+
const uint32_t max_ticks_in_ms = libtock_alarm_ticks_to_ms(MAX_TICKS);
361+
const uint32_t max_ms_in_ticks = ms_to_ticks(max_ticks_in_ms);
238362
libtock_alarm_at(last_timer_fire_time,
239-
MAX_TICKS,
363+
max_ms_in_ticks,
240364
(libtock_alarm_callback) overflow_callback,
241365
(void*) tock_timer,
242366
&(tock_timer->alarm));
@@ -253,6 +377,7 @@ int libtock_alarm_in_ms(uint32_t ms, libtock_alarm_callback cb, void* opaque, li
253377
// and the remainder ticks to reach the target length of time. The overflows use the
254378
// `overflow_callback` for each intermediate overflow.
255379
const uint32_t max_ticks_in_ms = libtock_alarm_ticks_to_ms(MAX_TICKS);
380+
const uint32_t max_ms_in_ticks = ms_to_ticks(max_ticks_in_ms);
256381
if (ms > max_ticks_in_ms) {
257382
// overflows_left is the number of intermediate alarms that need to be scheduled to reach the target
258383
// dt_ms. After the alarm in this block is scheduled, we have this many overflows left (hence the reason
@@ -263,7 +388,7 @@ int libtock_alarm_in_ms(uint32_t ms, libtock_alarm_callback cb, void* opaque, li
263388
alarm->user_data = opaque;
264389
alarm->callback = cb;
265390

266-
return libtock_alarm_at(now, MAX_TICKS, (libtock_alarm_callback)overflow_callback, (void*)(alarm),
391+
return libtock_alarm_at(now, max_ms_in_ticks, (libtock_alarm_callback)overflow_callback, (void*)(alarm),
267392
&(alarm->alarm));
268393
} else {
269394
// No overflows needed

0 commit comments

Comments
 (0)