プロセス構造体
OS自作もプロセス作成まで来ました。 プロセスを管理するための構造体を作成します。
プロセス構造体に必要なものを用意するべく、プロセス管理構造体を定義します。
#define PROCS_MAX 8 // 最大プロセス数 #define PROC_UNUSED 0 // 未使用のプロセス管理構造体 #define PROC_RUNNABLE 1 // 実行可能なプロセス struct process { int pid; // プロセスID int state; // プロセスの状態 vaddr_t sp; // コンテキストスイッチ時のスタックポインタ uint8_t stack[8192]; // カーネルスタック };
stateでPROC_UNUSEDやPROC_RUNNABLEなどそのプロセスの状態がわかるようにしています。
spによってそのプロセスが使用しているスタックポインタを保持します。コンテキストスイッチ時にカーネルスタックに元のレジスタの状態など復元できるようにしておきます。
コンテキストスイッチ
以下、コンテキストスイッチの関数をインラインアセンブリで定義します。
__attribute__((naked)) void switch_context(uint32_t *prev_sp, uint32_t *next_sp) { __asm__ __volatile__( "addi sp, sp, -13 * 4\n" "sw ra, 0 * 4(sp)\n" "sw s0, 1 * 4(sp)\n" "sw s1, 2 * 4(sp)\n" "sw s2, 3 * 4(sp)\n" "sw s3, 4 * 4(sp)\n" "sw s4, 5 * 4(sp)\n" "sw s5, 6 * 4(sp)\n" "sw s6, 7 * 4(sp)\n" "sw s7, 8 * 4(sp)\n" "sw s8, 9 * 4(sp)\n" "sw s9, 10 * 4(sp)\n" "sw s10, 11 * 4(sp)\n" "sw s11, 12 * 4(sp)\n" "sw sp, (a0)\n" "lw sp, (a1)\n" "lw ra, 0 * 4(sp)\n" "lw s0, 1 * 4(sp)\n" "lw s1, 2 * 4(sp)\n" "lw s2, 3 * 4(sp)\n" "lw s3, 4 * 4(sp)\n" "lw s4, 5 * 4(sp)\n" "lw s5, 6 * 4(sp)\n" "lw s6, 7 * 4(sp)\n" "lw s7, 8 * 4(sp)\n" "lw s8, 9 * 4(sp)\n" "lw s9, 10 * 4(sp)\n" "lw s10, 11 * 4(sp)\n" "lw s11, 12 * 4(sp)\n" "addi sp, sp, 13 * 4\n" "ret\n" ); }
上記コンテキストスイッチ関数では、コンテキストスイッチ前のプロセス時のレジスタををスタックに格納し、次に使用するプロセスの状態をレジスタに復元します。
次にプロセスを作成する際の関数です。
struct process *create_process(uint32_t pc) { // 空いているプロセス管理構造体を探す struct process *proc = NULL; int i; for (i = 0; i < PROCS_MAX; i++) { if (procs[i].state == PROC_UNUSED) { proc = &procs[i]; break; } } if (!proc) PANIC("no free process slots"); // switch_context() で復帰できるように、スタックに呼び出し先保存レジスタを積む uint32_t *sp = (uint32_t *) &proc->stack[sizeof(proc->stack)]; *--sp = 0; // s11 *--sp = 0; // s10 *--sp = 0; // s9 *--sp = 0; // s8 *--sp = 0; // s7 *--sp = 0; // s6 *--sp = 0; // s5 *--sp = 0; // s4 *--sp = 0; // s3 *--sp = 0; // s2 *--sp = 0; // s1 *--sp = 0; // s0 *--sp = (uint32_t) pc; // ra // 各フィールドを初期化 proc->pid = i + 1; proc->state = PROC_RUNNABLE; proc->sp = (uint32_t) sp; return proc; }
最初のfor文までで、プロセスIDを決定しています。プロセス構造体の空きがなければプロセスは作成せず、カーネルパニックとなります。
次にプロセスのレジスタ状態を保持するスタックを作成しています。 s11からpcまでスタックを積み上げています。
最後にプロセスid、状態、スタックポインタを格納して完成したプロセスを返します。
テストとして、下記のようにプロセスAとプロセスbを作成し、互いにコンテキストスイッチを行わせます。
struct process *proc_a; struct process *proc_b; void proc_a_entry(void) { printf("starting process A\n"); while (1) { putchar('A'); switch_context(&proc_a->sp, &proc_b->sp); for (int i = 0; i < 30000000; i++) __asm__ __volatile__("nop"); } } void proc_b_entry(void) { printf("starting process B\n"); while (1) { putchar('B'); switch_context(&proc_b->sp, &proc_a->sp); for (int i = 0; i < 30000000; i++) __asm__ __volatile__("nop"); } } void kernel_main(void) { memset(__bss, 0, (size_t) __bss_end - (size_t) __bss); WRITE_CSR(stvec, (uint32_t) kernel_entry); proc_a = create_process((uint32_t) proc_a_entry); proc_b = create_process((uint32_t) proc_b_entry); proc_a_entry();
実行結果は以下のように、コンテキストスイッチが行われていることが確認できます。
starting process A Astarting process B BABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAQE