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 を云々する方向ッス。