Skip to content

Commit 229ddd1

Browse files
committed
Add preemptive userspace multitasking
1 parent e9c7055 commit 229ddd1

File tree

4 files changed

+312
-0
lines changed

4 files changed

+312
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ httpd/httpd
44
mbus/mbus
55
picosh/picosh
66
ringbuffer/ringbuffer
7+
8+
# external source files
9+
preempt_sched/list.h

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- [rcu\_list](rcu_list/): A concurrent linked list utilizing the simplified RCU algorithm.
1717
- [ringbuf\_shm](ringbuf_shm/): An optimized lock-free ring buffer with shared memory.
1818
- [qsbr](qsbr/): An implementation of Quiescent state based reclamation (QSBR).
19+
- [preempt\_sched](preempt_sched/): A preemptive userspace multitasking based on a SIGALRM signal.
1920

2021
## License
2122

preempt_sched/Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
CFLAGS = -O2 -Wall -std=gnu99
2+
3+
TARGET = task_sched
4+
all: $(TARGET)
5+
%: %.c
6+
$(CC) $(CFLAGS) -o $@ $^
7+
8+
task_sched.c: list.h
9+
10+
list.h:
11+
wget -q https://raw.githubusercontent.com/sysprog21/linux-list/master/include/list.h
12+
touch $@
13+
14+
indent:
15+
clang-format -i task_sched.c
16+
17+
check: all
18+
./task_sched
19+
20+
clean:
21+
$(RM) $(TARGET) *~
22+
23+
distclean: clean
24+
$(RM) list.h

preempt_sched/task_sched.c

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/* task_sched: preemptive multitasking in userspace based on SIGALRM signal.
2+
*
3+
* This program starts 3 sorting routines, execution of each is preempted by
4+
* SIGALRM signal, simulating an OS timer interrupt. Each routine is an
5+
* execution context, which can do a voluntary scheduling (calling schedule()
6+
* directly) or be preempted by a timer, and in that case nonvoluntary
7+
* scheduling occurs.
8+
*
9+
* The default time slice is 10ms, that means that each 10ms SIGALRM fires and
10+
* next context is scheduled by round robin algorithm.
11+
*/
12+
13+
#define _GNU_SOURCE
14+
#include <signal.h>
15+
#include <stdbool.h>
16+
#include <stdint.h>
17+
#include <stdio.h>
18+
#include <stdlib.h>
19+
#include <ucontext.h>
20+
#include <unistd.h>
21+
22+
#include "list.h"
23+
24+
static int preempt_count = 0;
25+
static void preempt_disable(void)
26+
{
27+
preempt_count++;
28+
}
29+
static void preempt_enable(void)
30+
{
31+
preempt_count--;
32+
}
33+
34+
static void local_irq_save(sigset_t *sig_set)
35+
{
36+
sigset_t block_set;
37+
sigfillset(&block_set);
38+
sigdelset(&block_set, SIGINT);
39+
sigprocmask(SIG_BLOCK, &block_set, sig_set);
40+
}
41+
42+
static void local_irq_restore(sigset_t *sig_set)
43+
{
44+
sigprocmask(SIG_SETMASK, sig_set, NULL);
45+
}
46+
47+
#define task_printf(...) \
48+
({ \
49+
preempt_disable(); \
50+
printf(__VA_ARGS__); \
51+
preempt_enable(); \
52+
})
53+
54+
typedef void(task_callback_t)(void *arg);
55+
56+
struct task_struct {
57+
struct list_head list;
58+
ucontext_t context;
59+
void *stack;
60+
task_callback_t *callback;
61+
void *arg;
62+
bool reap_self;
63+
};
64+
65+
static struct task_struct *task_current, task_main;
66+
static LIST_HEAD(task_reap);
67+
68+
static void task_init(void)
69+
{
70+
INIT_LIST_HEAD(&task_main.list);
71+
task_current = &task_main;
72+
}
73+
74+
static struct task_struct *task_alloc(task_callback_t *func, void *arg)
75+
{
76+
struct task_struct *task = calloc(1, sizeof(*task));
77+
task->stack = calloc(1, 1 << 20);
78+
task->callback = func;
79+
task->arg = arg;
80+
return task;
81+
}
82+
83+
static void task_destroy(struct task_struct *task)
84+
{
85+
list_del(&task->list);
86+
free(task->stack);
87+
free(task);
88+
}
89+
90+
static void task_switch_to(struct task_struct *from, struct task_struct *to)
91+
{
92+
task_current = to;
93+
swapcontext(&from->context, &to->context);
94+
}
95+
96+
static void schedule(void)
97+
{
98+
sigset_t set;
99+
local_irq_save(&set);
100+
101+
struct task_struct *next_task =
102+
list_first_entry(&task_current->list, struct task_struct, list);
103+
if (next_task) {
104+
if (task_current->reap_self)
105+
list_move(&task_current->list, &task_reap);
106+
task_switch_to(task_current, next_task);
107+
}
108+
109+
struct task_struct *task, *tmp;
110+
list_for_each_entry_safe (task, tmp, &task_reap, list) /* clean reaps */
111+
task_destroy(task);
112+
113+
local_irq_restore(&set);
114+
}
115+
116+
union task_ptr {
117+
void *p;
118+
int i[2];
119+
};
120+
121+
static void local_irq_restore_trampoline(struct task_struct *task)
122+
{
123+
sigdelset(&task->context.uc_sigmask, SIGALRM);
124+
local_irq_restore(&task->context.uc_sigmask);
125+
}
126+
127+
__attribute__((noreturn)) static void task_trampoline(int i0, int i1)
128+
{
129+
union task_ptr ptr = {.i = {i0, i1}};
130+
struct task_struct *task = ptr.p;
131+
132+
/* We switch to trampoline with blocked timer. That is safe.
133+
* So the first thing that we have to do is to unblock timer signal.
134+
* Paired with task_add().
135+
*/
136+
local_irq_restore_trampoline(task);
137+
task->callback(task->arg);
138+
task->reap_self = true;
139+
schedule();
140+
141+
__builtin_unreachable(); /* shall not reach here */
142+
}
143+
144+
static void task_add(task_callback_t *func, void *param)
145+
{
146+
struct task_struct *task = task_alloc(func, param);
147+
if (getcontext(&task->context) == -1)
148+
abort();
149+
150+
task->context.uc_stack.ss_sp = task->stack;
151+
task->context.uc_stack.ss_size = 1 << 20;
152+
task->context.uc_stack.ss_flags = 0;
153+
task->context.uc_link = NULL;
154+
155+
union task_ptr ptr = {.p = task};
156+
makecontext(&task->context, (void (*)(void)) task_trampoline, 2, ptr.i[0],
157+
ptr.i[1]);
158+
159+
/* When we switch to it for the first time, timer signal must be blocked.
160+
* Paired with task_trampoline().
161+
*/
162+
sigaddset(&task->context.uc_sigmask, SIGALRM);
163+
164+
preempt_disable();
165+
list_add_tail(&task->list, &task_main.list);
166+
preempt_enable();
167+
}
168+
169+
static void timer_handler(int signo, siginfo_t *info, ucontext_t *ctx)
170+
{
171+
if (preempt_count) /* once preemption is disabled */
172+
return;
173+
174+
/* We can schedule directly from sighandler because Linux kernel cares only
175+
* about proper sigreturn frame in the stack.
176+
*/
177+
schedule();
178+
}
179+
180+
static void timer_init(void)
181+
{
182+
struct sigaction sa = {.sa_handler = (void (*)(int)) timer_handler,
183+
.sa_flags = SA_SIGINFO};
184+
sigfillset(&sa.sa_mask);
185+
sigaction(SIGALRM, &sa, NULL);
186+
}
187+
188+
static void timer_create(unsigned int usecs)
189+
{
190+
ualarm(usecs, usecs);
191+
}
192+
static void timer_cancel(void)
193+
{
194+
ualarm(0, 0);
195+
}
196+
197+
static void timer_wait(void)
198+
{
199+
sigset_t mask;
200+
sigprocmask(0, NULL, &mask);
201+
sigdelset(&mask, SIGALRM);
202+
sigsuspend(&mask);
203+
}
204+
205+
static int cmp_u32(const void *a, const void *b, void *arg)
206+
{
207+
uint32_t x = *(uint32_t *) a, y = *(uint32_t *) b;
208+
uint32_t diff = x ^ y;
209+
if (!diff)
210+
return 0; /* *a == *b */
211+
diff = diff | (diff >> 1);
212+
diff |= diff >> 2;
213+
diff |= diff >> 4;
214+
diff |= diff >> 8;
215+
diff |= diff >> 16;
216+
diff ^= diff >> 1;
217+
return (x & diff) ? 1 : -1;
218+
}
219+
220+
static inline uint32_t random_shuffle(uint32_t x)
221+
{
222+
/* by Chris Wellons, see: <https://nullprogram.com/blog/2018/07/31/> */
223+
x ^= x >> 16;
224+
x *= 0x7feb352dUL;
225+
x ^= x >> 15;
226+
x *= 0x846ca68bUL;
227+
x ^= x >> 16;
228+
return x;
229+
}
230+
231+
#define ARR_SIZE 1000000
232+
static void sort(void *arg)
233+
{
234+
char *name = arg;
235+
236+
preempt_disable();
237+
uint32_t *arr = malloc(ARR_SIZE * sizeof(uint32_t));
238+
preempt_enable();
239+
240+
task_printf("[%s] %s: begin\n", name, __func__);
241+
242+
uint32_t r = getpid();
243+
for (int i = 0; i < ARR_SIZE; i++)
244+
arr[i] = (r = random_shuffle(r));
245+
246+
task_printf("[%s] %s: start sorting\n", name, __func__);
247+
248+
qsort_r(arr, ARR_SIZE, sizeof(uint32_t), cmp_u32, name);
249+
250+
for (int i = 0; i < ARR_SIZE - 1; i++)
251+
if (arr[i] > arr[i + 1]) {
252+
task_printf("[%s] %s: failed: a[%d]=%u, a[%d]=%u\n", name, __func__,
253+
i, arr[i], i + 1, arr[i + 1]);
254+
abort();
255+
}
256+
257+
task_printf("[%s] %s: end\n", name, __func__);
258+
259+
preempt_disable();
260+
free(arr);
261+
preempt_enable();
262+
}
263+
264+
int main()
265+
{
266+
timer_init();
267+
task_init();
268+
269+
task_add(sort, "1"), task_add(sort, "2"), task_add(sort, "3");
270+
271+
preempt_disable();
272+
timer_create(10000); /* 10 ms */
273+
274+
while (!list_empty(&task_main.list) || !list_empty(&task_reap)) {
275+
preempt_enable();
276+
timer_wait();
277+
preempt_disable();
278+
}
279+
280+
preempt_enable();
281+
timer_cancel();
282+
283+
return 0;
284+
}

0 commit comments

Comments
 (0)