プロセスディスパッチャの実装――プロセススケジューリング(その2)UNIX USER 2004年6月号「Linuxカーネル2.6解読室」より転載

プロセスが切り替わるイメージを持つことはなかなか難しいため、実際のプロセスディスパッチャのコードを少しのぞいてみることにしましょう。ここでは、Intel x86用Linuxのコードを参照します。

» 2004年06月09日 00時00分 公開
[高橋浩和(VA Linux Systems Japan),UNIX USER]

 プロセスが切り替わるイメージを持つことはなかなか難しいため、実際のプロセスディスパッチャのコードを少しのぞいてみることにしましょう。ここでは、Intel x86用Linuxのコードを参照します。

context_switch関数

 Linuxカーネルのプロセスディスパッチャのコードは、context_switch関数にあります(リスト1)。context_switch関数は、プロセス空間の切り替え処理(switch_mm関数1)と、各種レジスタの切り替え処理(switch_to関数2)から成ります。プロセス空間の切り替え処理を効率化するために、その前後では様々な処理を行っています。これらの処理については、「空間管理」の回で詳しく説明します。

リスト1 kernel/sched.cのcontext_switch()関数
task_t * context_switch(runqueue_t *rq, task_t *prev,
task_t *next)
{
    struct mm_struct *mm = next->mm;
    struct mm_struct *oldmm = prev->active_mm;
    if (unlikely(!mm)) {
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);
    } else
        switch_mm(oldmm, mm, next); ← (1)
        if (unlikely(!prev->mm)) {
            prev->active_mm = NULL;
            WARN_ON(rq->prev_mm);
            rq->prev_mm = oldmm;
        }
        switch_to(prev, next, prev); ← (2)
    return prev;
}

switch_to関数

 リスト2のswitch_toマクロ関数が、プロセス切り替え処理の中で一番の核心となる部分です。switch_to関数は、各種レジスタの切り替えを行います。このレジスタ切り替え処理をもう少し詳しく見てみます。とくに、EIPレジスタ(命令ポインタ)とESPレジスタ(スタックポインタ)の切り替えに注目してください。

リスト2 include/asm-i386/system.hのswitch_to()マクロ関数
#define switch_to(prev,next,last) do { \
unsigned long esi,edi; \
asm volatile("pushfl\n\t" \ ← (3)
    "pushl %%ebp\n\t" \ ← (4)
    "movl %%esp,%0\n\t" \ ← (5)
    "movl %5,%%esp\n\t" \ ← (6)
    "movl $1f,%1\n\t" \ ← (7)
    "pushl %6\n\t" \ ← (8)
    "jmp __switch_to\n" \ ← (9)
    "1:\t" \ ← (10)
    "popl %%ebp\n\t" \ ← (11)
    "popfl" \ ← (12)
    :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
    "=a" (last),"=S" (esi),"=D" (edi) \
    :"m" (next->thread.esp),"m" (next->thread.eip), \
    "2" (prev), "d" (next)); \
} while (0)

 引数prevは、現在動作中のプロセスのtask_struct構造体を指しています。引数nextは、次に動作させるプロセスのtask_struct構造体を指しています。task_struct構造体中のthread.eipメンバーは、EIPレジスタを保存する領域です。また、thread.espメンバーは、ESPレジスタを保存する領域です。

 switch_to関数は、prevで示されるプロセスのESPレジスタを退避し(5)、nextで示されるプロセスのESPレジスタを復帰しています(6)。これら処理によって、プロセスのスタック領域の切り替えが実現します。

 プロセスprevが次回動作を再開するとき、(10)の個所から処理を行うようにEIPレジスタの退避域(thread.eip)に(10)のアドレスを退避します(7)。次に動作するプロセスnextの実行再開個所は、nextのレジスタの退避域(thread.eip)から取り出したEIPレジスタの値をスタック上に置いておきます。__switch_to関数を呼び出した後、__switch_to関数(9)から戻るとき、スタック上に積んだEIPレジスタの値が自動的にCPUに読み込まれます。その瞬間、プロセスnextの中断地点から、CPUは実行を再開することになります。

 またしばらく後に、プロセスprevは再度選択され、実行権が与えられます。そのときは、先ほどプロセスprevの復帰アドレスとしてthread.eipに退避した個所(10)から実行を再開します。

 ここのコードだけ見ていると、すべてのプロセスが同じ地点(10)から処理を再開することになるように見えますが、実はいくつか例外があります。forkシステムコールによって生まれたばかりの子プロセスは、forkシステムコールの後半処理から動作を始めます。この子プロセスの__switch_to関数からの戻り番地として、forkシステムコールの後半処理を開始アドレスとしてEIPレジスタの退避域(thread.eip)に登録しておきます。子プロセスはswitch_to関数の前半の処理(3〜9)を実行していません。突然__switch_to関数の中からわき出てきたように動き始めます。

 また、生成されたばかりのカーネルスレッドも同様に、独自の番地から実行を開始するようにEIPレジスタの退避域(thread.eip)を設定しています。カーネルスレッドも__switch_to関数の中からわき出てきたように動き始めます。

__switch_to関数

 __switch_to関数(リスト3)は、EIPとESP以外のレジスタの切り替えを行います(9)。プロセス切り替え処理を少しでも効率化するために、必要のないときはなるべくレジスタへのアクセスを減らす努力がなされています。最も特徴的な個所は、FPUレジスタ(浮動小数点演算レジスタ)の遅延切り替え機能です。

リスト3 arch/i386/kernel/process.c:__switch_to()関数
struct task_struct * __switch_to(struct task_struct *prev_p,
struct task_struct *next_p)
{
    struct thread_struct *prev = &prev_p->thread, *next = &next_p->thread;
    int cpu = smp_processor_id();
    struct tss_struct *tss = init_tss + cpu;

    __unlazy_fpu(prev_p); ← (13)

    load_esp0(tss, next->esp0); ← (14)
    load_TLS(next, cpu); ← (15)
    asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs)); ← (16)
    asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs)); ← (17)
    if (unlikely(prev->fs | prev->gs | next->fs | next->gs)) {
        loadsegment(fs, next->fs); ← (18)
        loadsegment(gs, next->gs); ← (19)
}
    if (unlikely(next->debugreg[7])) { ← (20)
        loaddebug(next, 0);
        loaddebug(next, 1);
        loaddebug(next, 2);
        loaddebug(next, 3);
        loaddebug(next, 6);
        loaddebug(next, 7);
}
    if (unlikely(prev->io_bitmap_ptr || next->io_bitmap_ptr)) { ←(21)
        if (next->io_bitmap_ptr) {
            memcpy(tss->io_bitmap, next->io_bitmap_ptr, IO_BITMAP_BYTES);
            tss->io_bitmap_base = IO_BITMAP_OFFSET;
        } else
            tss->io_bitmap_base = INVALID_IO_BITMAP_OFFSET;
    }
    return prev_p;
}

 プロセス切り替えでは、FPUレジスタの値も切り替える必要があります。__unlazy_fpu関数は、そのための関数です(13)。__unlazy_fpu関数は、呼び出された瞬間にはFPUレジスタを切り替えません。FPUレジスタには大きなレジスタが複数存在し、レジスタ値の退避/復帰にかかるコストが大きいため、少々トリッキーな手段を使って性能劣化を緩和しています。

 具体的には、__unlazy_fpu関数は、プロセスprevがFPUレジスタを利用している(プロセスprevが実行権を得てから、実行権を手放すまでにFPUレジスタを利用した)場合、プロセスprevのFPUレジスタ値の退避を行います。しかし、プロセスnextのFPUレジスタ値の復帰は行いません。その代わりに、FPUレジスタへのアクセスを禁止状態に変更します。この状態でプロセスnextがFPUレジスタにアクセスしようとすると、例外が発生し、その例外処理中にFPUレジスタの復帰処理を行います。プロセスnextが、FPUレジスタにアクセスしなかった場合は、例外が発生せず、FPUレジスタの復帰処理を省くことができます。FPUレジスタの復帰処理は、この先で浮動小数点演算を行うプロセスが動作するときまで行われません。実際にFPUレジスタを利用するプロセスは限られているため、FPUレジスタの切り替え回数を大幅に減らすことができます。

 ところで、一見FPUレジスタの退避処理も遅延させられそうに思えますが、なぜ遅延させていないのでしょうか? 理由の1つは、マルチプロセッサシステムの場合、このプロセスprevがほかのCPU上で再スケジューリングされてしまうことがあるためです。仮にそうなると、異なるCPU上のFPUレジスタ群をプロセスprevのもので初期化する必要があり、その処理は非常に苦労することになります。

汎用レジスタの退避

 ここまで見てきて気が付いた人もいると思いますが、汎用レジスタはいつ切り替えたのでしょうか? 実は、Intel x86ではこのタイミングで切り替えることは不要です。必要な情報はすでにスタック上に退避されています。もちろんこれはCPUアーキテクチャ依存(もちろん、コンパイラにも依存)です。関数呼び出しにおいて、呼び出された側でレジスタ値が壊れないことを保証しなければならないアーキテクチャである場合、それらのレジスタ値を退避する必要があります。

 この節では、Intel x86アーキテクチャ用のプロセス切り替えのコードを見てきました。このプロセス切り替えのコードは、CPUアーキテクチャごとに用意されています。しかし、その本質はIntel x86のものと同じです。Intel x86のコードと同じ視点から、ほかのCPUアーキテクチャ用のコードも理解することが可能だと思います。


 次回はプロセススケジューラについて解説していく。

このページで出てきた専門用語
EIPレジスタが自動的にCPUに読み込まれます
C言語の関数呼び出しの流れが、どのようなアセンブリ命令になるか知っておく必要がある。C言語における関数呼び出しはアセンブリ命令callに相当し、スタック上に関数呼び出し元の戻り番地を積む。逆にC言語のreturn文に相当するアセンブリ命令retは、スタック上から戻り番地をEIPに読み込む。

forkシステムコール
プロセスを生成するカーネル関数。Linux上のプロセスは必ずforkシステムコールによって生成される。前号参照。

なるべくレジスタへのアクセスを減らす努力
unlikely()マクロ関数は、引数として渡された条件がほとんどの場合に偽であることをコンパイラに教えるマクロ関数。コンパイラはこの指示に従い、条件が成り立たない場合に有利となるアセンブリコードを出力する。_ _switch_to()関数内でも、unlikely()付きの条件文がいくつも登場するが、ほとんどの場合にその条件文の中身は実行されないであろうことを意味している。

例外
CPUが許可されていない命令を実行したり、明示的に例外を発生させる命令を実行すると発生する。例外が発生すると、CPUはそのとき動作していた処理を中断し、例外処理の実行を開始する。この場合は、アクセスが禁止されているFPUレジスタを操作しようとしたため、例外が発生する。

UNIX USER 7月号表紙 最新号:UNIX USER 7月号の内容

第1特集
無線LANの構造と認証強化

第2特集
UNIX/LinuxからWindowsリソースを使うには?


[特別企画]
・新世代Very Secure FTPDを使いこなせ
・Fedora Core 2導入ガイド

Copyright(C) 2010 SOFTBANK Creative Inc. All Right Reserved.

注目のテーマ