前章でカーネルとアプリを分離しました。ただ、アプリからアプリを呼び出す方法が無いので、このままでは単一のアプリしか実行できません。
この章では、以下の2つの仕組みを実装します。
本書におけるフォアグラウンド実行とバックグラウンド実行のそれぞれを図示すると図2.1の通りです。
図2.1: 本書におけるフォアグラウンド実行とバックグラウンド実行の動き
「フォアグラウンド」・「バックグラウンド」と言うには「どのアプリがキー入力等の入力を受け取るか」も関わりますが、本書ではそこまで扱わず、単に「バックグラウンド実行の場合、呼びだされたアプリは並列で実行」ができれば良いものとします*1。
[*1] キー入力等の入力をフォアグラウンド/バックグラウンドにどのように実装するかは、キー入力等のシステムコールの実装次第です。例えばキーボード割り込みで呼び出されるユーザーハンドラを登録する機能をシステムコール化するようにすれば、コンテキストスイッチ時に呼び出すユーザーハンドラを変えることでキー入力値を渡すアプリを切り替える事ができます。ユーザーハンドラ登録のシステムコール実装は表紙アプリのサンプルコード「A01_cover」に実装例がありますので興味があれば見てみてください。
まずはフォアグラウンド実行をできるようにしてみます。
この項のサンプルディレクトリは「021_fg」です。
フォアグラウンド実行を実現するには、最低限、openとexecをアプリから呼び出すことができれば良いです。この節ではこれらをシステムコール化します。
編集するファイルはsyscall.cだけです(リスト2.1)。
リスト2.1: 021_fg/syscall.c
#include <intr.h> #include <pic.h> #include <fbcon.h> #include <fs.h> /* 追加 */ #include <proc.h> /* 追加 */ /* ・・・ 省略 ・・・ */ enum SYSCCALL_NO { SYSCALL_PUTC, SYSCALL_OPEN, /* 追加 */ SYSCALL_EXEC, /* 追加 */ MAX_SYSCALL_NUM }; unsigned long long do_syscall_interrupt( unsigned long long syscall_id, unsigned long long arg1, unsigned long long arg2 __attribute__((unused)), unsigned long long arg3 __attribute__((unused))) { unsigned long long ret_val = 0; switch (syscall_id) { case SYSCALL_PUTC: putc((char)arg1); break; /* 追加(ここから) */ case SYSCALL_OPEN: ret_val = (unsigned long long)open((char *)arg1); break; case SYSCALL_EXEC: exec((struct file *)arg1); break; /* 追加(ここまで) */ } /* PICへ割り込み処理終了を通知(EOI) */ set_pic_eoi(SYSCALL_INTR_NO); return ret_val; } /* ・・・ 省略 ・・・ */
システムコール割り込みハンドラのswitchに条件を追加し、openやexecを呼び出すだけです。
openとexecのシステムコールを使ってアプリを呼び出してみます。
「03_call_fg」という名前でappsディレクトリ内に作成することにします。
まず、lib.hとlib.cにopenとexecを呼び出すための定義を追加します(リスト2.2、リスト2.3)。
リスト2.2: 021_fg/apps/03_call_fg/include/lib.h
#pragma once /* 追加(ここから) */ #define FILE_NAME_LEN 28 struct __attribute__((packed)) file { char name[FILE_NAME_LEN]; unsigned int size; unsigned char data[0]; }; /* 追加(ここまで) */ void putc(char c); struct file *open(char *file_name); /* 追加 */ void exec(struct file *file); /* 追加 */
リスト2.3: 021_fg/apps/03_call_fg/lib.c
enum SYSCCALL_NO { SYSCALL_PUTC, SYSCALL_OPEN, /* 追加 */ SYSCALL_EXEC, /* 追加 */ MAX_SYSCALL_NUM }; /* ・・・ 省略 ・・・ */ void putc(char c) { syscall(SYSCALL_PUTC, c, 0, 0); } /* 追加(ここから) */ struct file *open(char *file_name) { return (struct file *)syscall( SYSCALL_OPEN, (unsigned long long)file_name, 0, 0); } void exec(struct file *file) { syscall(SYSCALL_EXEC, (unsigned long long)file, 0, 0); } /* 追加(ここまで) */
そして、リスト2.4の様にapp.cを作成すると"puta"という実行バイナリを実行できます。
リスト2.4: 021_fg/apps/021_call_fg/app.c
#include <lib.h> int main(void) { putc('Z'); exec(open("puta")); putc('B'); return 0; }
別バイナリを呼び出す前後が分かるように、リスト2.4ではexec関数の前後にputc関数で'Z'の出力と'B'の出力を置いてみました。
"02_putc"のアプリを"puta"という名前でファイルシステムへ入れておくことにします。
以下の様なコマンドを実行してファイルシステムイメージfs.imgを作成してください。
$ cd 021_fg/apps/02_putc/ $ make $ cp test ../puta $ cd ../03_call_fg/ $ make $ cp test ../ $ cd ../ $ ../tools/create_fs.sh test puta $ ls fs.img fs.img # 生成できていることを確認
このfs.imgを使って実行すると、図2.2の結果となりました。
図2.2: 021_fgの実行結果
'Z'を出力した後で、puta実行バイナリにより'A'が出力され、putaから戻ってきた後'B'を出力しています。
これで、カーネル側でアプリを呼び出した時と同じようにアプリからアプリを呼び出せるようになりました。
次に、バックグラウンド実行を実現してみます。
この項のサンプルディレクトリは「022_bg」です。
バックグラウンド実行を実現するためには、前著で実装したスケジューラを使います。
しかし、前著の状態では、スケジュール対象のタスク等がカーネルのソースコードにハードコードされています。この節では、ハードコードされたタスクや実験コードの削除や、実験用に作ったタスク生成機能を汎用化等を行い、次の節でこれらの機能を使いやすいようにしておきます。
なお、この節で行う作業が済んだものはサンプルディレクトリ「022_bg_refactor」にあります。この節を飛ばす場合はこのディレクトリの状態から次の節を始めてみてください。
編集するファイルはmain.c(リスト2.5)とsched.c(リスト2.6)です。
リスト2.5: 022_bg/main.c
/* ・・・ 省略 ・・・ */ struct __attribute__((packed)) platform_info { struct framebuffer fb; void *rsdp; }; /* void do_taskA(void); 削除 */ #define INIT_APP "test" /* 追加 */ void start_kernel(void *_t __attribute__((unused)), struct platform_info *pi, void *_fs_start) { /* ・・・ 省略 ・・・ */ /* ファイルシステムの初期化 */ fs_init(_fs_start); /* 削除(ここから) /* 実験として"test"という実行ファイルを実行してみる */ exec(open("test")); while (1) cpu_halt(); 削除(ここまで) */ /* スケジューラの初期化 */ sched_init(); /* CPUの割り込み有効化 */ enable_cpu_intr(); /* スケジューラの開始 */ sched_start(); /* 削除(ここから) /* タスクAの開始 */ do_taskA(); 削除(ここまで) */ /* 追加(ここから) */ /* initアプリ起動 */ exec(open(INIT_APP)); /* 追加(ここまで) */ /* haltして待つ */ while (1) cpu_halt(); } /* 削除(ここから) void do_taskA(void) { while (1) { putc('A'); volatile unsigned long long wait = 10000000; while (wait--); } } 削除(ここまで) */
リスト2.6: 022_bg/sched.c
/* ・・・ 省略 ・・・ */ #include <fbcon.h> #include <fs.h> /* 追加 */ #define SCHED_PERIOD (5 * MS_TO_US) #define NUM_TASKS 1 /* 変更 */ #define TASK_STASK_BYTES 4096 /* 変更 */ unsigned long long task_sp[MAX_TASKS]; /* 変更 */ volatile unsigned int current_task = 0; unsigned char task_stack[MAX_TASKS - 1][TASK_STASK_BYTES]; /* 変更 */ unsigned int num_tasks = 1; /* 追加 */ /* 削除(ここから) void do_taskB(void) { while (1) { putc('B'); volatile unsigned long long wait = 10000000; while (wait--); } } 削除(ここまで) */ void schedule(unsigned long long current_rsp) { task_sp[current_task] = current_rsp; current_task = (current_task + 1) % num_tasks; /* 変更 */ set_pic_eoi(HPET_INTR_NO); asm volatile ("mov %[sp], %%rsp" :: [sp]"a"(task_sp[current_task])); asm volatile ( "pop %rdi\n" "pop %rsi\n" "pop %rbp\n" "pop %rbx\n" "pop %rdx\n" "pop %rcx\n" "pop %rax\n" "iretq\n"); } /* 変更(ここから) */ void sched_init(void) { /* 5ms周期の周期タイマー設定 */ ptimer_setup(SCHED_PERIOD, schedule); } void enq_task(struct file *f) { unsigned long long start_addr = (unsigned long long)f->data; /* 予めタスクのスタックを適切に積んでおき、スタックポインタを揃える */ unsigned long long *sp = (unsigned long long *)((unsigned char *)task_stack[num_tasks] + TASK_STASK_BYTES); unsigned long long old_sp = (unsigned long long)sp; /* push SS */ --sp; *sp = 0x10; /* push old RSP */ --sp; *sp = old_sp; /* push RFLAGS */ --sp; *sp = 0x202; /* push CS */ --sp; *sp = 8; /* push RIP */ --sp; *sp = start_addr; /* push GR */ unsigned char i; for (i = 0; i < 7; i++) { --sp; *sp = 0; } task_sp[num_tasks] = (unsigned long long)sp; num_tasks++; } /* 変更(ここまで) */ /* ・・・ 省略 ・・・ */
主に実験用の処理の削除です。
その他に、リスト2.5ではカーネルが起動するアプリを"INIT_APP"と定義してそれを初期化処理の最後に呼び出すようにしたり、リスト2.6ではsched_init関数内のタスク生成処理から汎用的なタスク生成関数(enq_task関数)を作ったりしています。
enq_task関数は次節で使うので、include/sched.hにプロトタイプ宣言を追加しておいてください。
この節の変更を行ったあとは、ここまでで、ちゃんと変わらず実行できるか、一応ビルドして確認してみてください。
前節で、スケジューラを動作させながら、シングルタスクでカーネルから別アプリを起動できるようになりました。
あとは、ランキューへのアプリ追加をシステムコール化できれば、アプリが任意の実行バイナリを並列で実行できます。
この節では、"SYSCALL_ENQ_TASK"というシステムコールとして実装してみます(リスト2.7)。
リスト2.7: 022_bg/sched.c
/* ・・・ 省略 ・・・ */ #include <proc.h> #include <sched.h> /* 追加 */ #define SYSCALL_INTR_NO 0x80 enum SYSCCALL_NO { SYSCALL_PUTC, SYSCALL_OPEN, SYSCALL_EXEC, SYSCALL_ENQ_TASK, /* 追加 */ MAX_SYSCALL_NUM }; unsigned long long do_syscall_interrupt( unsigned long long syscall_id, unsigned long long arg1, unsigned long long arg2 __attribute__((unused)), unsigned long long arg3 __attribute__((unused))) { unsigned long long ret_val = 0; switch (syscall_id) { /* ・・・ 省略 ・・・ */ /* 追加(ここから) */ case SYSCALL_ENQ_TASK: enq_task((struct file *)arg1); break; /* 追加(ここまで) */ } /* PICへ割り込み処理終了を通知(EOI) */ set_pic_eoi(SYSCALL_INTR_NO); return ret_val; } /* ・・・ 省略 ・・・ */
前節で関数化したenq_task関数をシステムコールで呼び出しています。
それでは、追加したシステムコールを試してみます。
まずは、並列で動作させるために呼び出される側のアプリを用意します。
「04_putb_inf」というディレクトリ名のアプリで、app.cの内容はリスト2.8の通りとします。
リスト2.8: 022_bg/apps/04_putb_inf/app.c
#include <lib.h> #define WAIT_CLK 1000000 int main(void) { while (1) { unsigned long long wait = WAIT_CLK; putc('B'); while (wait--); } return 0; }
文字'B'を永遠に出し続けるだけです。
加えて、Makefileの"TARGET ="を"putb"へ変更し、生成される実行バイナリ名が"putb"になるようにしておきます。
それ以外の内容は前回のアプリと同じです。
次に、このputbバイナリをランキューへ追加するアプリを「05_call_putb」という名前で作成します。
まず、SYSCALL_ENQ_TASKを呼び出す関数をlib.cに追加します(リスト2.9)。
リスト2.9: 022_bg/apps/05_call_putb/lib.c
enum SYSCCALL_NO { SYSCALL_PUTC, SYSCALL_OPEN, SYSCALL_EXEC, SYSCALL_ENQ_TASK, /* 追加 */ MAX_SYSCALL_NUM }; /* ・・・ 省略 ・・・ */ void exec(struct file *file) { syscall(SYSCALL_EXEC, (unsigned long long)file, 0, 0); } /* 追加(ここから) */ void enq_task(struct file *file) { syscall(SYSCALL_ENQ_TASK, (unsigned long long)file, 0, 0); } /* 追加(ここまで) */
lib.hへenq_task関数のプロトタイプ宣言を追加するとapp.cから呼び出せるようになります。
実験として、app.cはリスト2.10のように作ってみます。
リスト2.10: 022_bg/apps/05_call_putb/app.c
#include <lib.h> #define WAIT_CLK 1000000 int main(void) { enq_task(open("putb")); while (1) { unsigned long long wait = WAIT_CLK; putc('A'); while (wait--); } return 0; }
"putb"バイナリを開き、enq_taskでランキューへ追加すると、カーネルのスケジューラにより並列で実行されます。
そのあとは、文字'A'を永遠に出し続けるだけです。
実行結果の見た目は前著のスケジューラの実験と同じで、AとBそれぞれの文字が画面に表示されれば成功です(図2.3)。
図2.3: 022_bgの実行結果