Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[実装例]コンテキストスイッチの実装 #11

Open
wants to merge 1 commit into
base: pcb
Choose a base branch
from

Conversation

rihib
Copy link
Owner

@rihib rihib commented Aug 28, 2024

思考プロセス

コンテキストスイッチを実装することで、複数のプロセスを交互に切り替えて実行していくことができるようになる。

create_process

  • まずプロセスを作るcreate_process関数を定義する必要がある
  • プロセスは前回実装したPCB(process構造体)を使って表現できる
  • 最大でPROCS_MAX個のプロセスしか作ることができないため、PROCS_MAX個のプロセスを要素として持つ配列procsを定義し、それを使ってプロセスを管理することにする。使用していないプロセスは状態をPROC_UNUSEDに設定しておき、作成済みのプロセスは状態をPROC_RUNNABLEに変更すれば良い
  • プロセスを作成するには、配列procsを走査して、未使用のプロセスを探して、各フィールドを初期化した上で返せば良い。もしも未使用のプロセスがなければカーネルパニックを起こす
  • プロセス(process構造体)は、PID、プロセスの状態、カーネルスタック、スタックポインタ、ページテーブルをフィールドとして持つのでこれらを1つずつ初期化していけば良い
  • まずPIDは配列の先頭から単純に1〜PROCS_MAX+1と割り振っていけば良い
  • プロセスの状態は前述したようにPROC_RUNNABLEに変更する
  • カーネルスタックはコンテキストスイッチの際にそのプロセスのコンテキスト(汎用レジスタの値)を保持しておくためのスペースなので、作成するときには0で初期化しておけば良い。ただし、リターンアドレスにはそのプロセスが実行したい関数の先頭アドレスで保持しておく必要がある(コンテキストスイッチした際にどのプログラムを実行するのかを指定する必要がある)。そのため、create_process関数の引数として受け取り、そのアドレスを使ってリターンアドレスを初期化する
  • スタックポインタはカーネルスタックに汎用レジスタの値を格納し終わった際のスタックトップの値で初期化すれば良い。コンテキストスイッチの際はこのスタックポインタの値を使って汎用レジスタの値を復元するためである
  • ページテーブルは、あらかじめカーネルのページをマッピングしたもので初期化する必要がある。そうしないとそのプロセスは仮想アドレスを使って物理アドレスにアクセスできなくなってしまう。map_page関数を使ってマップする。カーネルページは仮想アドレスと物理アドレスを同一のものとして扱いたいため、仮想ページと同じ物理ページをマップする。その際、フラグにはPAGE_R、PAGE_W、PAGE_Xを有効化したものを引数として渡す。カーネルページなのでUモードでアクセスできてしまうとセキュリティ上ダメなので、PAGE_Uは有効にしない
  • 上記のように初期化したプロセス(process構造体)を返す

switch_context

  • プロセスを作れるようにしたら、次はプロセス間でコンテキストスイッチできるようにする必要がある
  • コンテキストスイッチをするには、現在実行中のプロセスのコンテキスト(汎用レジスタの値)をカーネルスタックに保存し、切り替えたいプロセスのコンテキストを復元すれば良い
  • 現在実行中のプロセスのコンテキストをカーネルスタックに保存したらその際のスタックポインタの値を現在実行中のプロセスのspフィールドに保存する必要がある。また切り替えたいプロセスのコンテキストを復元するために、コンテキストが格納されているカーネルスタックのスタックポインタの値を知る必要がある。そのため、両方のスタックポインタのポインタを引数としてとるswitch_context関数を作り、switch_context(&proc_prev->sp, &proc_next->sp);と呼び出せるようにしたい
  • 切り替えたいプロセスのコンテキストを復元したら最後にret命令を実行することで、切り替えたいプロセスのリターンアドレスに制御を移すことができる

テスト

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();

    PANIC("unreachable here!");
}

switch_context(&proc_a->sp, &proc_b->sp);

proc_a_entry関数が実行されると、switch_context関数によってプロセスAの現在の状態が保存され、そのスタックトップがproc_a->spに格納され、proc_b->spに格納されているspからプロセスBの状態が復元される。これによってプロセスBを作るときに、スタックを初期化した際にraにセットしたproc_b_entry関数の先頭アドレスがraレジスタにセットされるため、proc_b_entry関数に制御が移る。

asm volatile("nop");

nop命令は「何もしない」命令で、これをしばらく繰り返すループを入れることで、文字での出力が速すぎてターミナルを操作できなくなるのを防いでいる。

$ ./run.sh

starting process A
Astarting process B
BABABABABABABABABABABABABABABABABABABABABABABABABABA

起動時のメッセージが1回ずつ表示され、その後は「ABABAB...」と交互に表示される。

@rihib rihib changed the title コンテキストスイッチの実装 [deprecated]コンテキストスイッチの実装 Aug 28, 2024
@rihib rihib closed this Aug 28, 2024
@rihib rihib reopened this Aug 28, 2024
@rihib rihib changed the base branch from main to pcb August 28, 2024 00:55
@rihib rihib changed the title [deprecated]コンテキストスイッチの実装 コンテキストスイッチの実装 Aug 28, 2024
@rihib rihib changed the title コンテキストスイッチの実装 [実装例]コンテキストスイッチの実装 Aug 28, 2024
if (!proc) PANIC("no free process slots");

// switch_context() で復帰できるように、スタックに呼び出し先保存レジスタを積む
uint32_t *sp = (uint32_t *)&proc->stack[sizeof(proc->stack)];
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

スタックは下に成長していくため、何も入っていない状態だとspの値は配列stackの先頭アドレスと等しい。


// switch_context() で復帰できるように、スタックに呼び出し先保存レジスタを積む
uint32_t *sp = (uint32_t *)&proc->stack[sizeof(proc->stack)];
*--sp = 0; // s11
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--spはスタックポインタの値を1つ(4バイト)減らし、次に*sp = 0となるので、新しいspが指すメモリ位置に0が格納される。

*--sp = 0; // s2
*--sp = 0; // s1
*--sp = 0; // s0
*--sp = (uint32_t)pc; // ra
Copy link
Owner Author

@rihib rihib Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_process関数はpc(実行開始アドレス)を引数として受け取る。つまりそのプロセスで実行される関数の先頭アドレスを引数として受け取る。それをスタックに保存することでコンテキストスイッチ時にリターンアドレスレジスタにセットし、retを実行することで、プロセスで実行したい関数に制御が移すことができる。

@@ -168,3 +196,83 @@ void map_page(uint32_t *table1, uint32_t vaddr, paddr_t paddr, uint32_t flags) {
uint32_t *table0 = (uint32_t *)((table1[vpn1] >> 10) * PAGE_SIZE);
table0[vpn0] = ((paddr / PAGE_SIZE) << 10) | flags | PAGE_V;
}

struct process *create_process(uint32_t pc) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_process関数で行っているのはスタックの初期化である。そのため、プロセスの実行が進むにつれて汎用レジスタの値は変わってくる。なのでコンテキストスイッチをする際には現在の汎用レジスタの値を保存し直す必要がある。


uint32_t *page_table = (uint32_t *)alloc_pages(1);

// カーネルのページをマッピングする
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

各プロセスに対して、カーネル空間のページ(__kernel_baseから__free_ram_endまでの範囲)をプロセスのページテーブルにマッピングすることで、そのプロセスがカーネルモードに移行した際に、カーネルコードやデータに適切にアクセスできるようにしている。

すでにSv32が有効化されているのでマップしないとアクセスできなくなるのと、カーネルページについては同じ値の仮想アドレスと物理アドレスをマップすることで、今までと同様に物理アドレスでアクセスできるかのようにしている。

return proc;
}

__attribute__((naked)) void switch_context(uint32_t *prev_sp,
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この関数はprev_spにコンテキストスイッチする前のspの値を格納し、next_spに格納されているspの値から新しいコンテキストにスイッチする。

スタックに汎用レジスタの値が格納されているので、それを使って汎用レジスタの値を切り替えることでコンテキストスイッチを実現する。上記はスタックポインタの値を拡張し、汎用レジスタの値を格納した後、a0(prev_sp)にスタック領域の先頭(スタックトップ)を格納している。

次に、a1(next_sp)から新しいスタックトップのアドレス(スタックポインタの値)をスタックポインタに格納し、そこに格納されている汎用レジスタの値を復元することで新しいプロセスのコンテキストに切り替えている。

最後にretを実行することで、切り替えたプロセスが持っていたリターンアドレスに制御を移す。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant