- マルチタスク
- マルチタスクの種類
- ARM64アーキテクチャにおけるタスクコンテキスト
- 関数呼び出し
- タスク切り替え関数の方針
- タスク切り替え関数を実装しよう
- タスクの定義
- タスクスケジューラの用意
- タスクスケジューラを動かしてみよう
- 実行
- 最後に
執筆者 : 金沢工業大学 工学部 情報工学科 橘 真吾 (インターン生)
監修者:高橋 浩和
※ 「ARM64 OSを作ろう」連載記事一覧はこちら
※ 「ARM64 OS」のコードはGitHubにて公開しています。
前回はU-Bootを用いてUART通信で "Hello World!" を繰り返し出力するプログラムが動くところまできました。今回は、ハードコードされた複数のタスクが切り替わりながら同時実行できる仕組みを作ります。タイマー割り込みを用いたタスク切り替えは次回から行っていきます。
マルチタスク
マルチタスクとは複数のプログラムがあたかも同時に実行しているかのように見せる機能です。 みなさんがブラウジングをしながら文章作成ができるのもこの機能のおかげです。 しかし、CPUは基本的には1度に1つの処理しかできませんが、どのようにしてマルチタスクを実現しているのでしょうか?
実はCPUが高速でタスクを切り替えているからです。
それぞれのタスクは、あたかも自分がCPUを専有しているかのようにプログラムを実行することができます。 各タスクに仮想的なCPUを割り当てることにより実現します。仮想的なCPUとは、実CPUのコピーです。各タスクを管理するデータ構造を用意し、その中にCPUのイメージを保存します。 タスクが動作するときは、タスク管理データにあるCPUのイメージを実CPUに転送します。タスクが動作を中断するときは、実CPUの状態をタスク管理データの中に保存します。

マルチタスクの種類
今回、実装難易度を下げるために、実装するマルチタスクはノンプリエンプティブ・マルチタスクにします。
マルチタスクにも2種類あり、タスクの切り替えタイミングをタスクが決めることができ、タスクがCPUを解放したタイミングでOSがタスクの切り替えを行うノンプリエンプティブ・マルチタスクと、OSがタスクごとのCPU割り当てを管理し、キーボード入力などの割り込みがあった場合にOSが強制介入し、スケジューリングするプリエンプティブ・マルチタスクがあります。 タイマー割り込みによる強制切り替えの仕組みが複雑になるため、今回は実装の簡素化を優先し、ノンプリエンプティブ方式を採用します。
ARM64アーキテクチャにおけるタスクコンテキスト
この連載のOSでは実装単純化のため、ここではタスクは浮動小数点演算を行わないものとします。 浮動小数点レジスタの退避・復帰処理が複雑になるため、本稿では対象外とします。
関数呼び出し
タスク切り替え関数を実装する前に、ARM64環境における関数呼び出しの仕組みを見ておきます。
ARM64 ABIの関数呼び出し規約ではARM64の汎用レジスタの利用方法を定めており、関数呼び出しに関して汎用レジスタを3つのグループに分けることができます。
1. 呼び出し先関数退避レジスタ(Callee saved registers)。
- 呼ばれた側の関数が、使用する場合は必ず保存・復元する責任を持つレジスタです。
- 呼び出し元から見ると、関数呼び出しを跨いで値が変化しないことが保証されます。
- レジスタ番号としては
X19-X30(LR)の間。
2. 関数呼び出し元退避レジスタ(Caller saved registers)。
- 呼び出された関数内で自由に使用してよいレジスタです。
- 呼び出し元は、関数呼び出しでレジスタ値を壊されたくない時は、関数呼び出し前に退避しなければなりません。
- レジスタ番号としては
X0-X18の間。
3. 変更不可。値が常に一定であるレジスタ。
- XZR(ゼロレジスタ)。
| レジスタ名 | 性質 | 説明 |
|---|---|---|
| X0-X7 | Caller-saved | 関数引数、関数戻り値(これを超える引数はスタック経由) |
| X8 | Caller-saved | 関数の戻り値がレジスタ(X0-X7)に収まりきらない巨大なデータ構造(C言語などで言うStructure/Union)である場合、呼び出し元はX8レジスタにメモリ上の戻り値保存用アドレスを設定します。 |
| X9-X15 | Caller-saved | 一時レジスタ(テンポラリレジスタ) |
| X16-X17 | Caller-saved | リンカが生成するコードやダイナミックリンクライブラリ呼び出しで一時的に使用するレジスタ。ユーザーは基本的に使用しない |
| X18 | Caller-saved | プラットフォーム用レジスタ(自作OSなら使用は自由だが、通常は避けた方が良い) |
| X19-X28 | Callee-saved | 非揮発性レジスタ。関数呼び出しを跨いで値が保持される |
| X29(FP) | Callee-saved | フレームポインタ。現在実行している関数のスタックフレームの底 |
| X30(LR) | Callee-saved | リンクレジスタ、関数戻り先アドレス。ret命令はこのレジスタを参照する |
| SP | Stack Pointer | スタックの先端。常に 16 バイトアライメントが必要 |
| XZR | Constant | 常にゼロ |
関数実行中、その関数が使用するスタック領域はSP(スタックポインタ)とFP(フレームポインタ)で示されます。 現在実行中の関数が使用するスタック領域の上端(低位アドレス側)をSP(スタックポインタ)が指し、終端(高位アドレス側)をFP(フレームポインタ、X29)が指します。 FP(X29)はCallee-savedレジスタであり、関数の呼び出し元のスタックフレームの先頭アドレスを指すことで、デバッガがコールスタックを遡る時などに利用されます。 ただし、gccの最適化によりフレームポインタを使用するコードは殆どが削除されてしまいます*1。

1. 引数と戻り値(X0 - X7)
- 引数:
X0 - X7の最大8個を使います。9個目以降や大きな構造体はスタックを使います。 - 戻り値:
X0(場合によりX1も)を使います。
2. 作業レジスタ(Caller-saved:X0 - X15)
X0 - X15は、関数内で自由に使えます(壊していいレジスタ)。- 注意点として、
X0 - X7は「引数用兼、作業用」であり、X9 - X15は純粋な「作業用」です。 - 関数呼び出し元がこれらの値を維持したい場合は、関数を呼ぶ前にスタックに退避します。
3. 永続レジスタ(Callee-saved:X19 - X29)
X19 - X29は、関数終了時に呼び出し前の状態に戻す必要があります。- これを使う場合は、関数冒頭でスタックに保存し、終了直前に復元します。
4. 戻り番地(LR / X30)
X30(LR)に関する記述はその通りです。別の関数を呼ぶ(BL命令など)とX30が上書きされるため、関数呼び出しがネストする場合はスタックへの退避が必須です。
タスク切り替え関数の方針
タスク切り替え関数にもARM64 ABI関数呼び出し規約(関数呼び出しの約束事)を守らせます。
タスク切り替え関数は、現在実行しているタスクが利用しているレジスタの値を保存し、次に動作するタスク用に保存されているレジスタ値を読み込みます。 タスク切り替え関数も普通の関数と同じく「呼び出し先関数退避レジスタ(Callee-saved)」(X19-X29)と、関数の戻り番地情報X30(LR)を保存します。 通常の関数ではその関数内で利用するレジスタに関してのみ「呼び出し先関数退避レジスタ(Callee-saved)」の値を保存すれば良いのですが、タスク切り替え関数では全ての「呼び出し先関数退避レジスタ(Callee-saved)」の値を保存する必要があります。それをしないとタスクの実行が中断している間に動作する他のタスクにより、これらレジスタが書き換えられてしまう可能性があります。
「関数呼び出し元退避レジスタ(Caller-saved)」は、タスク切り替え関数の中で保存する必要はありません。 コンパイラは関数呼び出し時に、呼び出し元で値を保持したい Caller-saved レジスタを自動的にスタックに保存・復元する処理を生成するため、コンテキストスイッチのアセンブリ側では Callee-saved レジスタのみを保存すれば十分です。
16バイトアライメント制約
STP/LDP命令は64ビット(8バイト)のレジスタ2つを同時に操作します。つまり1回のメモリアクセスで8バイト × 2 = 16バイトを読み書きするため、アクセス先のアドレスは16バイトの倍数でなければなりません。これはハードウェア上の制約であり、ARM64ではスタックポインタ(SP)が常に16バイトアライメントされていることが求められます。
このため、task_stack配列には__attribute__((aligned(16)))属性を付与して16バイト境界に配置します。
また、後で説明しますがタスクコンテキストの保存形式を表すcontext構造体のサイズも16バイトの倍数となっています(contextはX19〜X30の12個 × 8バイト = 96バイトで16バイトの倍数になっています)。
context構造体は、タスク切り替えによりタスクが使っていたレジスタをスタック上に保存した時の形式を表しています。
タスク切り替え関数を実装しよう
単純化のため、OS起動時に定義したタスクが全て起動し、永久に動き続ける(タスクは終了しない)ものとします。 タスク切り替え(コンテキストスイッチ)をアセンブリコードで記述し、タスクの構造体の定義・タスクの初期化・次のタスクの選択などはC言語で行います。
タスク切り替え
各タスクにそのタスクを管理するためのデータ構造(TaskControl)を割り当てます。
spメンバ(スタックポインタの退避領域)とtask_stack(タスクスタック本体)をひとつの構造体にまとめることで、タスクの実行コンテキストを一元管理します。
タスク切り替え時には、SP以外の「呼び出し先関数退避レジスタ(Callee-saved)」はこのスタック域に保存することにします。 タスク切り替え関数からの戻り先アドレス(LR)もスタック域に保存します。 LRの保存はPC(プログラムカウンタ)の保存に相当します。タスクが実行を再開する時はスタック上にあるLRの値をPCに読み込みます。
以下にTaskControl構造体の定義を載せます。前節で述べたスタックの16バイトアライメント制約を満たすため、task_stackメンバには__attribute__((aligned(16)))属性を付けています。
struct TaskControl { unsigned long sp; unsigned long task_stack[STACKSIZE] __attribute__((aligned(16))); } TaskControl[NUMBER_OF_TASKS];
TaskSwitch関数は、引数currentが指すTaskControlで管理されるタスクから、
引数nextが指すTaskControlで管理されるタスクへ切り替えます。
void TaskSwitch(struct TaskControl *current, struct TaskControl *next) { switch_context(&next->sp, ¤t->sp); }
タスク切り替え(コンテキストスイッチ)の処理は以下のようになります。 汎用レジスタの保存、復帰処理をアセンブリコードで記述します。
1. 現タスクの退避
STP命令でX19–X30の値をプリデクリメントしながら現タスクのスタックに積む
2. 現タスクのスタックポインタの退避
SPの値を一旦X2に移してからX1の指すメンバ(現タスクのTaskControl.sp)に格納する。ARM64(AArch64)ではSTP命令のソースレジスタにSPを指定できないため。
3. 次タスクのスタックポインタの復帰
X0の指すメンバ(次タスクのTaskControl.sp)の値をX3経由でSPに代入し、スタックを切り替える
4. 次タスクの復帰
- ポストインクリメントで
LDP命令を実行し、X19–X30の値を復元する
5. コンテキストスイッチを完了する
retによりX30(LR)に保存されていた次タスクの復帰アドレスへ処理が移り、コンテキストスイッチが完了する

今回新しくコンテキストスイッチを実装するために新しくprimitive.Sを作成します。
touch primitive.S nano primitive.S
※お好きなエディタで
.text
.globl switch_context
.type switch_context,@function
.balign 4
switch_context:
stp x29, x30, [sp, #-0x10]!
stp x27, x28, [sp, #-0x10]!
stp x25, x26, [sp, #-0x10]!
stp x23, x24, [sp, #-0x10]!
stp x21, x22, [sp, #-0x10]!
stp x19, x20, [sp, #-0x10]!
mov x2, sp
str x2, [x1]
ldr x3, [x0]
mov sp, x3
ldp x19, x20, [sp], #0x10
ldp x21, x22, [sp], #0x10
ldp x23, x24, [sp], #0x10
ldp x25, x26, [sp], #0x10
ldp x27, x28, [sp], #0x10
ldp x29, x30, [sp], #0x10
ret
.size switch_context,.-switch_context
ARM64はStore操作およびLoad操作を行う際に、2つのレジスタを同時に操作できるSTP命令とLDP命令を備えています。これによりメモリアクセスを効率よく行うことができます。
タスクの定義
タスクそれぞれが、次に動作するタスクを指定してタスク切り替え関数TaskSwitchを呼び出すというプログラムを用意しました。
typedef enum { TASK1 = 0, TASK2, TASK3, NUMBER_OF_TASKS, } TaskIdType; void Task1(void) { while (1) { print_message("Task1\n"); TaskSwitch(&TaskControl[TASK1], &TaskControl[TASK2]); } } void Task2(void) { while (1) { print_message("Task2\n"); TaskSwitch(&TaskControl[TASK2], &TaskControl[TASK3]); } } void Task3(void) { while (1) { print_message("Task3\n"); TaskSwitch(&TaskControl[TASK3], &TaskControl[TASK1]); } }
タスクの初期化
まだ一度も動作したことのないタスクは、そのままではタスク切り替えを行うことができません。
TaskControlとスタックにタスクの情報が何も入っていないためです。
そこで「タスクエントリを実行しようとした瞬間にswitch_context関数が呼ばれ実行が中断している」という状態をスタック上に作り出すInitTask関数を用意します。
InitTask関数は、スタック上のタスクコンテキストの保存形式を表すcontext構造体を使い、以下の手順で初期化します。
1. タスクのコンテキスト(Callee savedレジスタ)の退避先を決める
- タスクのスタック上に、コンテキスト保存域を用意する。
task_stack末尾(高アドレス側)からcontextのサイズ分だけ低位のアドレスをコンテキストの退避先アドレスとする。
2. タスクのエントリポイントの設定
- X30(LR)の保存域にタスクエントリのアドレスを書き込む。初回の
switch_contextのretでここへジャンプすることでタスクが起動する。 - X19–X29の保存域は
clearbss関数によってゼロ初期化済みのため、ここでの初期化は省略する。
3. 各タスクのスタックポインタの設定
- で求めたコンテキストの退避先アドレスを
TaskControl.spに保存する。
- で求めたコンテキストの退避先アドレスを

#define STACKSIZE 0x1000 typedef struct { unsigned long x19; unsigned long x20; unsigned long x21; unsigned long x22; unsigned long x23; unsigned long x24; unsigned long x25; unsigned long x26; unsigned long x27; unsigned long x28; unsigned long x29; unsigned long x30; // LR } context; void InitTask(TaskIdType task, void (*entry)()) { context* p = (context *)&TaskControl[task].task_stack[STACKSIZE] - 1; p->x30 = (unsigned long)entry; TaskControl[task].sp = (unsigned long)p; }
最初のタスクの起動
タスクの初期化により、初めて動作するタスクに対してタスク切り替え関数が動作するようになりましたが、
一番最初に動作するタスクには切り替え元となるタスクが存在しません。
最初のタスクの起動用には専用の関数を用意します。この関数は、タスク切り替え関数の後半処理(スタック上の値のレジスタへの読み込み)のみを行ないます。
タスク切り替え関数(switch_context)の途中に、最初のタスクの起動関数の入り口(load_context)を埋め込むことで実現することにします*2。
.globl switch_context
.globl load_context
.type switch_context,@function
.balign 4
switch_context:
stp x29, x30, [sp, #-0x10]!
stp x27, x28, [sp, #-0x10]!
stp x25, x26, [sp, #-0x10]!
stp x23, x24, [sp, #-0x10]!
stp x21, x22, [sp, #-0x10]!
stp x19, x20, [sp, #-0x10]!
mov x2, sp
str x2, [x1]
load_context:
ldr x3, [x0]
mov sp, x3
ldp x19, x20, [sp], #0x10
ldp x21, x22, [sp], #0x10
ldp x23, x24, [sp], #0x10
ldp x25, x26, [sp], #0x10
ldp x27, x28, [sp], #0x10
ldp x29, x30, [sp], #0x10
ret
.size switch_context,.-switch_context
BSSセクションの初期化
C言語では、初期値を指定しないグローバル変数やstatic変数はBSSセクションと呼ばれるメモリ領域に配置されます。
int counter; // ← BSSセクションに配置 int initialized = 100; // ← BSSセクションではない(dataセクション)
C言語の仕様では、BSSセクションの変数は0で初期化されることが保証されています。 しかし、電源投入直後のメモリにはランダムな値(不定なデータ)が入っている可能性があるため、明示的に0で初期化する必要があります。
通常のアプリケーション開発では、OSがプログラム起動時に自動的にこの処理を行いますが、OS自体を開発している今回は、自分でBSSセクションを初期化しなければなりません。
そのためにclearbss()関数を用意し、main関数の最初で呼び出します。
clearbss()関数ではリンク時にリンカスクリプトで定義された_bss_startから_bss_endまでの間を0で埋めます。
また、下記の例では、タスク3つを初期化した後、TASK1タスクを最初に動作するタスクとして選択しています。
static void clearbss(void) { unsigned long long *p; extern unsigned long long _bss_start[]; extern unsigned long long _bss_end[]; for (p = _bss_start; p < _bss_end; p++) { *p = 0LL; } } void main(void) { clearbss(); // BSSセクションを0で初期化 InitTask(TASK1, Task1); InitTask(TASK2, Task2); InitTask(TASK3, Task3); CurrentTask = TASK1; load_context(&TaskControl[CurrentTask].sp); }
タスクスケジューラの用意
次に動作するタスクを選択するという作業からタスクを解放します。 タスクスケジューラを呼び出すと、タスクスケジューラは何らかのアルゴリズムに基づき次に動作させるタスクを選択し、タスク切り替え関数を呼び出します。
今回は単純なラウンドロビンアルゴリズムを採用します。
タスクスケジューラScheduleを呼び出すと、現在動作してるタスクの次に定義されているタスクを選択し、そのタスクに実行権を渡します。
今実行しているタスクがTask1だとするとTASK1は列挙型の値0を表すため、CurrentTaskに0が代入され、そこに1が加算され、1となり、NUMBER_OF_TASKSは3なので、1 % 3 = 1、つまりTASK2が次のタスクとなるのです。
このようにして次のタスクへと実行権が遷移していきます。
TaskIdType CurrentTask; static TaskIdType ChooseNextTask(void) { return (CurrentTask + 1) % NUMBER_OF_TASKS; } void Schedule(void) { TaskIdType from = CurrentTask; CurrentTask = ChooseNextTask(); TaskSwitch(&TaskControl[from], &TaskControl[CurrentTask]); }
タスクスケジューラを動かしてみよう
タスクからのタスク切り替え関数TaskSwitch呼び出しを、タスクスケジューラSchedule呼び出しに変更します。
void Task1(void) { while (1) { print_message("Task1\n"); Schedule(); } } void Task2(void) { while (1) { print_message("Task2\n"); Schedule(); } } void Task3(void) { while (1) { print_message("Task3\n"); Schedule(); } }
実行
これでC言語のタスクの定義とアセンブリによるコンテキストスイッチを実装できたので実行してみましょう。
今回も関数ごとに説明したので、バラバラに書かれているため、以下に完全なコードを掲示しておきます。
main.c
#include <stdarg.h> #include <stdint.h> #define TRUE 1 #define FALSE 0 #define UART_BASE 0xFE215040U #define UART_TX (UART_BASE + 0U*4U) #define UART_LSR (UART_BASE + 5U*4U) #define THR_EMPTY 0x20U #define STACKSIZE 0x1000 typedef enum { TASK1 = 0, TASK2, TASK3, NUMBER_OF_TASKS, } TaskIdType; struct TaskControl { unsigned long sp; unsigned long task_stack[STACKSIZE] __attribute__((aligned(16))); } TaskControl[NUMBER_OF_TASKS]; extern void Schedule(void); extern int switch_context(unsigned long *next_sp, unsigned long* sp); extern void load_context(unsigned long *sp); extern void TaskSwitch(struct TaskControl *current, struct TaskControl *next); typedef struct { unsigned long x19; unsigned long x20; unsigned long x21; unsigned long x22; unsigned long x23; unsigned long x24; unsigned long x25; unsigned long x26; unsigned long x27; unsigned long x28; unsigned long x29; unsigned long x30; // LR } context; static void clearbss(void); void uart_putchar(char c); void print_message(const char* s, ...); void Task1(void); void Task2(void); void Task3(void); static TaskIdType ChooseNextTask(void); void Schedule(void); void TaskSwitch(struct TaskControl *current, struct TaskControl *next); void InitTask(TaskIdType task, void (*entry)()); static void put_char(char c); TaskIdType CurrentTask; static void clearbss(void) { unsigned long long *p; extern unsigned long long _bss_start[]; extern unsigned long long _bss_end[]; for (p = _bss_start; p < _bss_end; p++) { *p = 0LL; } } void uart_putchar(char c) { volatile uint32_t * const uart = (uint32_t *)UART_TX; volatile uint32_t * const status = (uint32_t *)UART_LSR; while ( !(*status & THR_EMPTY) ) ; *uart = c; } void print_message(const char* s, ...) { va_list ap; va_start(ap, s); while (*s) { if (*s == '%' && *(s+1) == 'x') { unsigned long v = va_arg(ap, unsigned long); _Bool print_started = FALSE; int i; s += 2; for (i = 15; i >= 0; i--) { unsigned long x = (v & 0xFUL << i*4) >> i*4; if (print_started || x != 0U || i == 0) { print_started = TRUE; uart_putchar(x < 10 ? ('0' + x) : ('a' + x - 10)); } } } else { if (*s == '\n') { uart_putchar('\r'); } uart_putchar(*s++); } } va_end(ap); } void Task1(void) { while (1) { print_message("Task1\n"); Schedule(); } } void Task2(void) { while (1) { print_message("Task2\n"); Schedule(); } } void Task3(void) { while (1) { print_message("Task3\n"); Schedule(); } } void TaskSwitch(struct TaskControl *current, struct TaskControl *next) { switch_context(&next->sp, ¤t->sp); } static TaskIdType ChooseNextTask(void) { return (CurrentTask + 1) % NUMBER_OF_TASKS; } void Schedule(void) { TaskIdType from = CurrentTask; CurrentTask = ChooseNextTask(); TaskSwitch(&TaskControl[from], &TaskControl[CurrentTask]); } void InitTask(TaskIdType task, void (*entry)()) { context* p = (context *)&TaskControl[task].task_stack[STACKSIZE] - 1; p->x30 = (unsigned long)entry; TaskControl[task].sp = (unsigned long)p; } void main(void) { clearbss(); InitTask(TASK1, Task1); InitTask(TASK2, Task2); InitTask(TASK3, Task3); CurrentTask = TASK1; load_context(&TaskControl[CurrentTask].sp); }
primitive.S
.globl switch_context
.globl load_context
.type switch_context,@function
.balign 4
switch_context:
stp x29, x30, [sp, #-0x10]!
stp x27, x28, [sp, #-0x10]!
stp x25, x26, [sp, #-0x10]!
stp x23, x24, [sp, #-0x10]!
stp x21, x22, [sp, #-0x10]!
stp x19, x20, [sp, #-0x10]!
mov x2, sp
str x2, [x1]
load_context:
ldr x3, [x0]
mov sp, x3
ldp x19, x20, [sp], #0x10
ldp x21, x22, [sp], #0x10
ldp x23, x24, [sp], #0x10
ldp x25, x26, [sp], #0x10
ldp x27, x28, [sp], #0x10
ldp x29, x30, [sp], #0x10
ret
.size switch_context,.-switch_context
make sudo cp arm64os.img /boot/firmware/ sync sudo reboot
Raspberry Pi 4Bが再起動し、U-Bootの画面が出たときにHit any key to stop autoboot:の値が0になるまでにEnterキーを押すことでU-Bootでコマンドを実行できるようになります。
U-Boot 2023.07.02 (Dec 13 2025 - 21:44:31 +0900)
DRAM: 948 MiB (effective 7.9 GiB)
RPI 4 Model B (0xd03115)
Core: 211 devices, 17 uclasses, devicetree: board
MMC: mmcnr@7e300000: 1, mmc@7e340000: 0
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1...
In: serial
Out: vidconsole
Err: vidconsole
Net: eth0: ethernet@7d580000
PCIe BRCM: link up, 5.0 Gbps x1 (SSC)
starting USB...
Bus xhci_pci: Register 5000420 NbrPorts 5
Starting the controller
USB XHCI 1.00
scanning bus xhci_pci for devices... 5 USB Device(s) found
scanning usb for storage devices... 1 Storage Device(s) found
Hit any key to stop autoboot: 0
これが表示されたらEnterキーを押す。
そこで以下のコマンドを入力してください。
U-Boot> fatload mmc 0:1 0x1000 boot_arm64os.scr U-Boot> source 0x1000
これら2つのコマンドを実行することにより、以下のような実行結果が表示されるはずです。
U-Boot> fatload mmc 0:1 0x1000 boot_arm64os.scr 297 bytes read in 72 ms (3.9 KiB/s) U-Boot> source 0x1000 ## Executing script at 00001000 4208 bytes read in 79 ms (51.8 KiB/s) 54874 bytes read in 23 ms (2.3 MiB/s) Working FDT set to 3000000 ## Booting kernel from Legacy Image at 04000000 ... Image Name: Image Type: AArch64 U-Boot Standalone Program (uncompressed) Data Size: 4144 Bytes = 4 KiB Load Address: 04000000 Entry Point: 04000000 Verifying Checksum ... OK Loading Standalone Program ## Starting application at 0x04000000 ... Task1 Task2 Task3 Task1 Task2 Task3 Task1 Task2 Task3 Task1 Task2 Task3 Task1 Task2 Task3 : :
今回も正常にプログラムが動作していることを確認することができました。
最後に
この先の連載では、このタスクスケジューラ呼び出しという作業からもタスクを解放します。
今回はタスク切り替え関数を実装しました。このタスク切り替えの仕組みは、Linuxなどの大きなOSでも殆ど同じです。 ここで紹介したプログラムはgithubにて公開しています。まだまだOSと呼べるようなものではありませんが、今後少しずつ機能を追加しOSらしくしていきます。 github.com
次回は割り込みを扱います。OSが割り込みを受付け、割り込みハンドラを起動する仕組みを作ります。