応用編 第2章: ユーザー/カーネルモードと SVC#

特権レベルを分離し、SVC(Supervisor Call。ユーザーのコードから OS の機能を呼ぶための命令)でシステムコール(ユーザーのコードが OS に仕事を頼む入口)を生やします。

サンプルコード#

  • ディレクトリ: docs/os-on-arduino/code/adv2_syscall

  • ビルドと書き込み:

uv run pio run -d adv2_syscall -t upload
uv run pio device monitor -b 115200

コードを見る: docs/os-on-arduino/code/adv2_syscall

ソースコード#

この章のスケッチです。ファイル名をクリックすると開きます。コードは原稿リポジトリから自動で同期されているので、ここに出ているものが常に最新です。

src/main.cpp — 94 行

GitHub で開く

 1/**
 2 * 応用編 第2章: ユーザー/カーネルモードと SVC
 3 *
 4 * タスクが Serial や os_sleep() を直接呼ばず、すべて svc 命令を経由して
 5 * カーネルに依頼する形にしたデモ。
 6 *
 7 * user_putchar() などのラッパは svc を発行するだけで、実際に Serial を
 8 * 触るのは SVC_Handler の先にいる sys_putchar()。ユーザーコードから
 9 * ペリフェラルへの直接アクセスが 1 箇所も無いことがポイント。
10 *
11 * なお本章 2.3 の「非特権モードへ落とす」(CONTROL.nPRIV=1)は、
12 * PendSV でタスクごとに CONTROL を切り替える改造が要る。本書はそこを
13 * 擬似コードで示すにとどめているため、このサンプルも特権スレッドのまま
14 * 動かしている。SVC の仕組み自体は特権/非特権のどちらからでも同じ。
15 */
16
17#include <Arduino.h>
18#include "os_kernel.h"
19#include "os_syscall.h"
20#include "os_syscall_user.h"
21
22// システムコールだけで書いた文字列出力
23static void user_print(const char *s)
24{
25    while (*s) user_putchar(*s++);
26}
27
28static void user_print_int(int v)
29{
30    char buf[12];
31    snprintf(buf, sizeof(buf), "%d", v);
32    user_print(buf);
33}
34
35// ユーザータスク A: 自分の PID を名乗って寝るだけ
36static void task_a(void)
37{
38    while (1) {
39        user_print("[A] pid=");
40        user_print_int(user_getpid());
41        user_print("\r\n");
42        user_sleep(1000);
43    }
44}
45
46// ユーザータスク B: yield を挟みながら数える
47static void task_b(void)
48{
49    int n = 0;
50    while (1) {
51        user_print("[B] count=");
52        user_print_int(n++);
53        user_print("\r\n");
54        user_yield();
55        user_sleep(1500);
56    }
57}
58
59// 未定義の SVC 番号を呼ぶと -1 (ENOSYS 相当) が返ることを確かめる
60static void task_probe(void)
61{
62    user_sleep(2500);
63    int r = (int)SYSCALL0(99);
64    user_print("[probe] svc #99 -> ");
65    user_print_int(r);
66    user_print("  (-1 なら ENOSYS 相当が返っている)\r\n");
67    user_exit(0);
68    while (1) user_sleep(1000);   // ここへは戻らない
69}
70
71void setup()
72{
73    Serial.begin(115200);
74    while (!Serial) { }
75
76    Serial.println(F("=== Advanced 2: SVC system calls ==="));
77    Serial.println(F("以降の出力はすべて svc 経由(sys_putchar)で出ています"));
78
79    os_init();
80
81    // SVC の優先度を下げておく(重要)。
82    // リセット直後の SVC は最高優先度 0 のため、SVC_Handler の中で
83    // Serial.write() がバッファ空きを待つと、それを配る UART/USB 割り込み
84    // (優先度はもっと低い)が永遠に走れず、システム全体が止まる。
85    // 「ペリフェラル割り込みより低く、PendSV/SysTick(15) より高く」が正解。
86    NVIC_SetPriority(SVCall_IRQn, 14);
87
88    os_create_task(task_a,     "UserA", 512);
89    os_create_task(task_b,     "UserB", 512);
90    os_create_task(task_probe, "Probe", 512);
91    os_start();
92}
93
94void loop() { }
include/os_kernel.h — 74 行

GitHub で開く

 1/**
 2 * os_kernel.h - Mini OS Kernel Header
 3 * Chapter 6: Memory Protection with HardFault Handler
 4 */
 5
 6#ifndef OS_KERNEL_H
 7#define OS_KERNEL_H
 8
 9#include <stdint.h>
10
11#ifdef __cplusplus
12extern "C" {
13#endif
14
15typedef enum {
16    TASK_READY,
17    TASK_RUNNING,
18    TASK_BLOCKED,
19    TASK_SUSPENDED,
20    TASK_TERMINATED
21} TaskState;
22
23typedef struct {
24    uint32_t *sp;
25    uint32_t *stack_base;
26    uint32_t stack_size;
27    TaskState state;
28    uint8_t id;
29    uint8_t priority;
30    char name[16];
31    void (*entry)(void);
32    uint32_t wake_tick;
33    uint32_t cpu_ticks;
34} TCB;
35
36#define MAX_TASKS 8
37#define DEFAULT_STACK_SIZE 512
38// タスク用スタックをまとめて置く領域。os_create_task() は
39// ここから要求されたぶんだけ切り出す。
40// 既定より大きなスタックを要求するタスク(シェルなど)があるため、
41// MAX_TASKS * DEFAULT_STACK_SIZE では足りない。余裕を持たせる。
42#define STACK_POOL_SIZE (MAX_TASKS * DEFAULT_STACK_SIZE + 2048)
43#define IDLE_STACK_SIZE 256
44#define TIME_SLICE_MS 10
45
46void os_init(void);
47int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size);
48void os_start(void);
49void os_yield(void);
50void os_sleep(uint32_t ms);
51
52void os_suspend_task(int id);
53void os_resume_task(int id);
54void os_terminate_task(int id);
55
56int os_get_task_count(void);
57const char* os_get_task_name(int id);
58TaskState os_get_task_state(int id);
59int os_get_current_task(void);
60int os_get_cpu_usage(int task_id);
61uint32_t os_get_tick(void);
62
63extern TCB g_tasks[MAX_TASKS];
64extern volatile int g_task_count;
65extern volatile int g_current_task;
66extern volatile int g_next_task;
67extern volatile uint32_t g_system_ticks;
68extern const uint32_t g_tcb_size;
69
70#ifdef __cplusplus
71}
72#endif
73
74#endif
include/os_syscall.h — 33 行

GitHub で開く

 1// os_syscall.h
 2//
 3// 本書 応用編 第2章「ユーザー/カーネルモードと SVC」のシステムコール番号と、
 4// カーネル側の実体。番号は svc 命令の即値としてそのまま埋め込まれる。
 5
 6#pragma once
 7
 8#include <stdint.h>
 9
10#define SYS_putchar  0
11#define SYS_yield    1
12#define SYS_sleep    2
13#define SYS_exit     3
14#define SYS_getpid   4
15#define SYS_NR       5   // システムコール総数
16
17#ifdef __cplusplus
18extern "C" {
19#endif
20
21// カーネル側の実体(SVC_Handler から呼ばれる)
22void sys_putchar(char c);
23void sys_yield(void);
24void sys_sleep(uint32_t ms);
25void sys_exit(int code);
26int  sys_getpid(void);
27
28// SVC ハンドラの C 側ディスパッチャ
29void svc_dispatch_c(uint32_t *sp);
30
31#ifdef __cplusplus
32}
33#endif
include/os_syscall_user.h — 38 行

GitHub で開く

 1// os_syscall_user.h
 2//
 3// ユーザー側のラッパ。svc 命令の即値はコンパイル時定数でなければならないため、
 4// static inline にして呼び出し元で定数畳み込みされることを前提にしている。
 5
 6#pragma once
 7
 8#include <stdint.h>
 9#include "os_syscall.h"
10
11static inline uint32_t _svc_call(uint8_t num, uint32_t a0,
12                                 uint32_t a1, uint32_t a2)
13{
14    register uint32_t r0 __asm("r0") = a0;
15    register uint32_t r1 __asm("r1") = a1;
16    register uint32_t r2 __asm("r2") = a2;
17    uint32_t ret;
18
19    __asm volatile (
20        "svc %[n]"
21        : "=r" (r0)
22        : [n] "i" (num), "r" (r0), "r" (r1), "r" (r2)
23        : "memory"
24    );
25    ret = r0;
26    return ret;
27}
28
29// ★ 引数がインライン即値である必要があるため、番号ごとにマクロ展開
30#define SYSCALL0(n)         _svc_call((n), 0, 0, 0)
31#define SYSCALL1(n, a)      _svc_call((n), (uint32_t)(a), 0, 0)
32#define SYSCALL2(n, a, b)   _svc_call((n), (uint32_t)(a), (uint32_t)(b), 0)
33
34static inline void user_putchar(char c)   { SYSCALL1(SYS_putchar, c); }
35static inline void user_yield(void)       { SYSCALL0(SYS_yield); }
36static inline void user_sleep(uint32_t m) { SYSCALL1(SYS_sleep,  m); }
37static inline void user_exit(int code)    { SYSCALL1(SYS_exit,   code); }
38static inline int  user_getpid(void)      { return (int)SYSCALL0(SYS_getpid); }
src/os_kernel.cpp — 231 行

GitHub で開く

  1/**
  2 * os_kernel.cpp - Mini OS Kernel Implementation
  3 * Chapter 6: Memory Protection
  4 */
  5
  6#include "os_kernel.h"
  7#include <Arduino.h>
  8#include <string.h>
  9
 10TCB g_tasks[MAX_TASKS];
 11volatile int g_task_count = 0;
 12volatile int g_current_task = -1;
 13volatile int g_next_task = -1;
 14volatile uint32_t g_system_ticks = 0;
 15
 16const uint32_t g_tcb_size = sizeof(TCB);
 17
 18static uint32_t g_stack_pool[STACK_POOL_SIZE / 4];
 19static uint32_t g_stack_used;   // 切り出し済みのワード数
 20
 21static void idle_task(void) { while (1) __WFI(); }
 22
 23// タスク関数が return したときの受け皿。例外フレームの LR スロットは
 24// ここを指す (第3章 3.4)。EXC_RETURN を置く場所ではない。
 25static void task_exit(void)
 26{
 27    g_tasks[g_current_task].state = TASK_TERMINATED;
 28    os_yield();
 29    while (1) { }  // ここには到達しない
 30}
 31
 32static uint32_t* init_task_stack(uint32_t *stack_top, void (*entry)(void))
 33{
 34    uint32_t *sp = stack_top;
 35    *(--sp) = 0x01000000;
 36    *(--sp) = (uint32_t)entry;
 37    *(--sp) = (uint32_t)task_exit;  // LR: タスクが return したときの戻り先
 38    *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
 39    // FPU を使ったタスクは例外フレームが拡張される(FP レジスタ込み)。
 40    // どちらのフレームで中断したかは EXC_RETURN の値に現れるため、
 41    // タスクごとに EXC_RETURN も保存する。初期値は「PSP・基本フレーム」。
 42    *(--sp) = 0xFFFFFFFD;
 43    *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
 44    *(--sp) = 0; *(--sp) = 0; *(--sp) = 0; *(--sp) = 0;
 45    return sp;
 46}
 47
 48void os_init(void)
 49{
 50    memset(g_tasks, 0, sizeof(g_tasks));
 51    g_task_count = 0;
 52    g_current_task = -1;
 53    g_next_task = -1;
 54    g_system_ticks = 0;
 55    os_create_task(idle_task, "idle", IDLE_STACK_SIZE);
 56}
 57
 58int os_create_task(void (*entry)(void), const char *name, uint32_t stack_size)
 59{
 60    if (g_task_count >= MAX_TASKS) return -1;
 61    int id = g_task_count;
 62    TCB *task = &g_tasks[id];
 63    task->id = id;
 64    task->state = TASK_READY;
 65    task->priority = id;
 66    task->entry = entry;
 67    task->wake_tick = 0;
 68    task->cpu_ticks = 0;
 69    strncpy(task->name, name, sizeof(task->name) - 1);
 70    if (stack_size == 0) {
 71        stack_size = DEFAULT_STACK_SIZE;
 72    }
 73    uint32_t words = (stack_size + 3) / 4;
 74    if (g_stack_used + words > STACK_POOL_SIZE / 4) {
 75        return -1;          // プールが尽きた。黙って縮めず、失敗として返す
 76    }
 77    g_stack_used += words;
 78    task->stack_size = stack_size;
 79    task->stack_base = &g_stack_pool[g_stack_used];   // スタックは上端から下へ伸びる
 80    task->sp = init_task_stack(task->stack_base, entry);
 81    g_task_count++;
 82    return id;
 83}
 84
 85void os_schedule(void)
 86{
 87    for (int i = 0; i < g_task_count; i++) {
 88        if (g_tasks[i].state == TASK_BLOCKED && g_system_ticks >= g_tasks[i].wake_tick) {
 89            g_tasks[i].state = TASK_READY;
 90        }
 91    }
 92
 93    int next = g_current_task;
 94    do {
 95        next = (next + 1) % g_task_count;
 96        if (g_tasks[next].state == TASK_READY || g_tasks[next].state == TASK_RUNNING) break;
 97    } while (next != g_current_task);
 98
 99    if (next != g_current_task || g_current_task < 0) {
100        if (g_current_task >= 0 && g_tasks[g_current_task].state == TASK_RUNNING) {
101            g_tasks[g_current_task].state = TASK_READY;
102        }
103        g_tasks[next].state = TASK_RUNNING;
104        g_next_task = next;
105        SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
106    }
107}
108
109void os_yield(void) { os_schedule(); }
110void os_sleep(uint32_t ms) {
111    if (g_current_task > 0) {
112        g_tasks[g_current_task].state = TASK_BLOCKED;
113        g_tasks[g_current_task].wake_tick = g_system_ticks + ms;
114        os_schedule();
115    }
116}
117void os_suspend_task(int id) {
118    if (id > 0 && id < g_task_count) {
119        g_tasks[id].state = TASK_SUSPENDED;
120        if (id == g_current_task) os_schedule();
121    }
122}
123void os_resume_task(int id) {
124    if (id > 0 && id < g_task_count && g_tasks[id].state == TASK_SUSPENDED) {
125        g_tasks[id].state = TASK_READY;
126    }
127}
128void os_terminate_task(int id) {
129    if (id > 0 && id < g_task_count) {
130        g_tasks[id].state = TASK_TERMINATED;
131        if (id == g_current_task) os_schedule();
132    }
133}
134
135uint32_t os_get_tick(void) { return g_system_ticks; }
136int os_get_task_count(void) { return g_task_count; }
137const char* os_get_task_name(int id) { return (id >= 0 && id < g_task_count) ? g_tasks[id].name : ""; }
138TaskState os_get_task_state(int id) { return (id >= 0 && id < g_task_count) ? g_tasks[id].state : TASK_TERMINATED; }
139int os_get_current_task(void) { return g_current_task; }
140
141void os_start(void)
142{
143    NVIC_SetPriority(PendSV_IRQn, 0xFF);
144    SysTick_Config(SystemCoreClock / 1000 * TIME_SLICE_MS);
145    g_current_task = -1;
146    g_next_task = 0;
147    // Do NOT switch CONTROL to PSP here — PSP is not yet initialized.
148    // PendSV returns with EXC_RETURN=0xFFFFFFFD, which automatically
149    // switches thread mode to PSP after loading the first task's context.
150    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
151    while (1) { }
152}
153
154extern "C" void SysTick_Handler(void) {
155    // 実際に経過した時間を、いま走っていたタスクの取り分として計上する。
156    // os_schedule() 側で数えると os_yield() の回数まで混ざってしまう。
157    if (g_current_task >= 0) {
158        g_tasks[g_current_task].cpu_ticks += TIME_SLICE_MS;
159    }
160    g_system_ticks += TIME_SLICE_MS;
161    os_schedule();
162}
163
164extern "C" __attribute__((naked)) void PendSV_Handler(void)
165{
166    __asm volatile (
167        "   MRS     R0, PSP              \n"
168        "   LDR     R1, =g_current_task  \n"
169        "   LDR     R2, [R1]             \n"
170        "   CMP     R2, #0               \n"
171        "   BLT     _skip_save           \n"
172        // FPU を使って中断したタスク(EXC_RETURN の bit4 が 0)は、
173        // ハードウェアが積まない S16-S31 もここで退避する
174        "   TST     LR, #0x10            \n"
175        "   IT      EQ                   \n"
176        "   VSTMDBEQ R0!, {S16-S31}      \n"
177        // R4-R11 と EXC_RETURN(LR) を保存する
178        "   STMDB   R0!, {R4-R11, LR}    \n"
179        "   LDR     R3, =g_tasks         \n"
180        "   LDR     R4, =g_tcb_size      \n"
181        "   LDR     R4, [R4]             \n"
182        "   MUL     R2, R2, R4           \n"
183        "   ADD     R3, R3, R2           \n"
184        "   STR     R0, [R3]             \n"
185        "_skip_save:                     \n"
186        "   LDR     R1, =g_next_task     \n"
187        "   LDR     R2, [R1]             \n"
188        "   LDR     R1, =g_current_task  \n"
189        "   STR     R2, [R1]             \n"
190        "   LDR     R3, =g_tasks         \n"
191        "   LDR     R4, =g_tcb_size      \n"
192        "   LDR     R4, [R4]             \n"
193        "   MUL     R2, R2, R4           \n"
194        "   ADD     R3, R3, R2           \n"
195        "   LDR     R0, [R3]             \n"
196        "   LDMIA   R0!, {R4-R11, LR}    \n"
197        // FPU を使って中断していたタスクなら S16-S31 も復元する
198        "   TST     LR, #0x10            \n"
199        "   IT      EQ                   \n"
200        "   VLDMIAEQ R0!, {S16-S31}      \n"
201        "   MSR     PSP, R0              \n"
202        "   BX      LR                   \n"
203    );
204}
205
206// CPU使用率を取得(パーセント)
207// 直前にこの関数を呼んだ時点からの差分で計算する。
208// ps のようにタスクごとにループで呼ぶため、基準時刻もタスクごとに持つ。
209int os_get_cpu_usage(int task_id)
210{
211    if (task_id < 0 || task_id >= g_task_count) {
212        return -1;
213    }
214
215    static uint32_t last_total[MAX_TASKS] = {0};
216    static uint32_t last_task_ticks[MAX_TASKS] = {0};
217
218    uint32_t total = g_system_ticks;
219    uint32_t delta_total = total - last_total[task_id];
220
221    if (delta_total == 0) {
222        return 0;
223    }
224
225    uint32_t delta_task = g_tasks[task_id].cpu_ticks - last_task_ticks[task_id];
226
227    last_total[task_id] = total;
228    last_task_ticks[task_id] = g_tasks[task_id].cpu_ticks;
229
230    return (delta_task * 100) / delta_total;
231}
src/os_svc.cpp — 95 行

GitHub で開く

 1// os_svc.cpp
 2//
 3// 本書 応用編 第2章 2.5「SVC_Handler の実装」。
 4//
 5// SVC 番号は命令そのものに埋め込まれているので、ハンドラからは
 6// 「スタックに積まれた戻り先 PC の 2 バイト手前」を読んで取り出す。
 7
 8#include <Arduino.h>
 9#include "os_syscall.h"
10#include "os_kernel.h"
11
12// ---- カーネル側のシステムコール実体 ----
13//
14// 本文では svc_dispatch_c から呼ぶ形だけを示しているので、中身はここで書く。
15// いずれも「ユーザーが直接触れないもの」をカーネルが代行する形になっている。
16
17void sys_putchar(char c)
18{
19    Serial.write(c);
20}
21
22void sys_yield(void)
23{
24    os_yield();
25}
26
27void sys_sleep(uint32_t ms)
28{
29    os_sleep(ms);
30}
31
32void sys_exit(int code)
33{
34    (void)code;
35    os_terminate_task(os_get_current_task());
36}
37
38int sys_getpid(void)
39{
40    return os_get_current_task();
41}
42
43// ---- ディスパッチャ ----
44
45// 引数 sp は SVC を発行したコードのスタックフレーム先頭を指す
46extern "C" void svc_dispatch_c(uint32_t *sp)
47{
48    // sp[0]=R0, sp[1]=R1, sp[2]=R2, sp[3]=R3
49    // sp[6]=元の PC (SVC の次の命令)
50    uint8_t  *pc      = (uint8_t *)sp[6];
51    uint8_t   svc_num = pc[-2];          // svc 命令の下位バイト = 番号
52
53    uint32_t arg0 = sp[0];
54    uint32_t ret  = 0;
55
56    switch (svc_num) {
57    case SYS_putchar:
58        sys_putchar((char)arg0);
59        break;
60    case SYS_yield:
61        sys_yield();
62        break;
63    case SYS_sleep:
64        sys_sleep(arg0);  // ms 単位
65        break;
66    case SYS_exit:
67        sys_exit((int)arg0);
68        break;
69    case SYS_getpid:
70        ret = (uint32_t)sys_getpid();
71        break;
72    default:
73        ret = (uint32_t)-1;  // ENOSYS 相当
74        break;
75    }
76
77    // 戻り値は R0 (sp[0]) に書き戻す
78    sp[0] = ret;
79}
80
81// ---- 例外ハンドラ本体 ----
82//
83// SVC が特権スレッドから来たか非特権スレッドから来たかで、引数が積まれている
84// スタックが MSP か PSP かが変わる。EXC_RETURN の bit2 で見分ける。
85
86extern "C" __attribute__((naked)) void SVC_Handler(void)
87{
88    __asm volatile (
89        "TST   lr, #4            \n"  // EXC_RETURN bit2: 0=MSP, 1=PSP
90        "ITE   EQ                \n"
91        "MRSEQ r0, MSP           \n"  // bit2==0 なら MSP
92        "MRSNE r0, PSP           \n"  // bit2==1 なら PSP
93        "B     svc_dispatch_c    \n"  // R0 = スタックポインタ
94    );
95}

実機での出力#

Arduino UNO R4 WiFi の実機で実際に取得した出力です(macOS / Windows の 両方で同じ結果を確認しています)。手元の出力と見比べてください。

出力はすべて svc 経由(sys_putchar)で出ています。

../_images/adv2_svc.gif

[A] と [B] が交互に出続けるところ。1 文字ずつ svc を通って 出ています。冒頭の [probe] はモニタを開く前に流れ終わっています#

[probe] svc #99 -> -1  (-1 なら ENOSYS 相当が返っている)
[A] pid=1
[B] count=2
[A] pid=1
[B] count=3
[A] pid=1
[A] pid=1
[B] count=4
[A] pid=1
[B] count=5
...

1 行目の ENOSYS(Linux で「そのシステムコールは無い」を表すエラー番号)は、未定義の SVC 番号を呼ぶとそれに相当する -1 が返ることを確かめています。

つまずきやすいポイント#

(読者からの質問に応じて随時追記します。質問は GitHub Issues へどうぞ)