xv6-rev6 読み (8)
Lions' 本の 6 章以降をざくっとナナメに読んだのですが面白い。bootstrap な部分は得に newproc 手続きを呼び出すあたりからクライマックス感満点。
で、xv6 はどうなってるのか、ってことで中身を掘ってみました。
ざっくり
main 手続きから
- userinit 手続きで init を kickoff する用意
- mpmain 手続きで init を kickoff
というカンジ。Lions' 本 (というか PDP11) では savu とか retu という手続きを駆使して init な手続きの kickoff を行なっておるのですが、xv6 では stack を上手に使って init の kickoff を行なっているようです。
とりあえず
なんとなく確認できたので順に纏めてみます。xv6 な main.c の main 手続き末端が核心です。
userinit(); // first user process // Finish setting up this processor in mpmain. mpmain(); }
mpmain 手続きからは戻らないはず。順に userinit 手続きから確認していきます。定義を以下に引用。
// Set up first user process. void userinit(void) { struct proc *p; extern char _binary_initcode_start[], _binary_initcode_size[]; p = allocproc(); initproc = p; if((p->pgdir = setupkvm(kalloc)) == 0) panic("userinit: out of memory?"); inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size); p->sz = PGSIZE; memset(p->tf, 0, sizeof(*p->tf)); p->tf->cs = (SEG_UCODE << 3) | DPL_USER; p->tf->ds = (SEG_UDATA << 3) | DPL_USER; p->tf->es = p->tf->ds; p->tf->ss = p->tf->ds; p->tf->eflags = FL_IF; p->tf->esp = PGSIZE; p->tf->eip = 0; // beginning of initcode.S safestrcpy(p->name, "initcode", sizeof(p->name)); p->cwd = namei("/"); p->state = RUNNABLE; }
なんとなくヤッてることを以下に列挙。
- allocproc 手続きで proc 構造体の領域確保とか初期化などして戻りを p に代入
- kernel な page directory な領域を確保して初期化して p->pgdir に設定
- inituvm 手続きで initcode.S な実行イメージを p->pgdir の 0 番地にロード
- _binary_initcode_size は PGSIZE より小さくなければならない模様
- p->tf (trapframe) な領域の初期設定
- p->tf->eip に 0 を設定してるのは上の inituvm 手続きに関連してるはず
- name、cwd、state 属性の設定
- state は RUNNABLE にしておくことで scheduler で拾うはず
allocproc 手続きが Lions' 本の newproc 手続きに該当 (完全に、ではないですが) する模様。とりあえず核心部分を以下に引用しときます。
// Allocate kernel stack. if((p->kstack = kalloc()) == 0){ p->state = UNUSED; return 0; } sp = p->kstack + KSTACKSIZE; // Leave room for trap frame. sp -= sizeof *p->tf; p->tf = (struct trapframe*)sp; // Set up new context to start executing at forkret, // which returns to trapret. sp -= 4; *(uint*)sp = (uint)trapret; sp -= sizeof *p->context; p->context = (struct context*)sp; memset(p->context, 0, sizeof *p->context); p->context->eip = (uint)forkret; return p; // Allocate kernel stack. if((p->kstack = kalloc()) == 0){ p->state = UNUSED; return 0; } sp = p->kstack + KSTACKSIZE; // Leave room for trap frame. sp -= sizeof *p->tf; p->tf = (struct trapframe*)sp; // Set up new context to start executing at forkret, // which returns to trapret. sp -= 4; *(uint*)sp = (uint)trapret; sp -= sizeof *p->context; p->context = (struct context*)sp; memset(p->context, 0, sizeof *p->context); p->context->eip = (uint)forkret; return p;
ちなみに p は ptable.proc 配列のどこかの要素のポインタ。で、kernel stack な領域を確保してスタックポインタを設定したら
- トラップフレーム push
- forkret からの戻り (trapret) を push
- p->context を push
で、p->context な領域を初期化して forkret 手続きのアドレスを p->context->eip に設定してますね。このスタック操作なあたりが_コルーチンジャンプ_なナニにつながってる模様です。
ええと、この時点での類推なんですが
- eip に forkret が設定されてるのでこの手続きが実行される
- forkret 手続きからの戻り先は trapret 手続き
というカンジ。ここからは scheduler の出番なのかどうか。
scheduler 手続き
proc.c で核心部分が以下。
// Switch to chosen process. It is the process's job // to release ptable.lock and then reacquire it // before jumping back to us. proc = p; switchuvm(p); p->state = RUNNING; swtch(&cpu->scheduler, proc->context); switchkvm();
p は state 属性が RUNNABLE な要素です。swtch 手続きからは戻ってこないはずなのですがどうなのでしょうか。
定義が swtch.S で以下。
.globl swtch swtch: movl 4(%esp), %eax movl 8(%esp), %edx # Save old callee-save registers pushl %ebp pushl %ebx pushl %esi pushl %edi # Switch stacks movl %esp, (%eax) movl %edx, %esp # Load new callee-save registers popl %edi popl %esi popl %ebx popl %ebp ret
ええと、cpu->scheduler にこれまでの esp が格納されて proc->context から esp にロードされるはずなんだけど大丈夫かな。で、ret 命令で forkret に制御が移るなずなんですが、根拠が不明。
あ、context 構造体の定義が以下で
struct context { uint edi; uint esi; uint ebx; uint ebp; uint eip; };
ret で eip 属性が取り出されてそっちに戻る (類推) ってことで良いかな。で、forkret 手続きからの戻りは stack 的に trapret になるはず。この手続きは trapasm.S にて定義されてて以下。
# Return falls through to trapret... .globl trapret trapret: popal popl %gs popl %fs popl %es popl %ds addl $0x8, %esp # trapno and errcode iret
で、iret によって stack に push されてるトラップフレームが展開されるはず。これは newproc 手続きで領域確保なんですが、実際に属性に値が設定されるのは userinit 手続きですね。
ぶっちゃけるとここから init が kickoff されることになる訳です。本当かなぁ。
とりあえず
ここでエントリ投入して微妙な部分は別途補足入れます。あと、明日は雨らしいので、引き隠って puppet を云々する方向ッス。