技術

[xv6 #16] Chapter 1 – The first process – Code: Running a process

テキストの25〜27ページ

本文

最初のプロセスの状態が準備できたら、実行することができる。
main関数がuserinit関数を呼んだ後、プロセスを開始するためにmpmain関数はshceduler関数を呼ぶ。
schedulerは、p->stateがRUNNABLEにセットされたプロセスを探す。
しかしこの時点では一つのプロセスだけが見つかる。
それはinitprocである。
(initprocは前回userinitの中で出てきました。元はproc.cに定義されてるstatic変数(proc構造体)です。)
cpuごとの変数procへ、見つかったプロセスをセットし、対象のプロセスのページテーブルを使うことをハードウェアに伝えるためにswitchuvm関数を呼ぶ。
switchkvm関数は、カーネルのコードとデータのための同一のマッピングを全てのプロセスのページテーブルに持たせるようにするので、カーネルを実行中にページテーブルを切り替える事が出来る。
switchuvm関数もまた、新しいタスク状態セグメント(task state segment)SEG_TSSを生成する。
それは、%ssへSEG_KDATA<<3を、%espへ(uint)proc->kstack+KSTACKSIZE(そのプロセスのカーネルスタックの一番上)をセットすることによってカーネルモードへの移行による割り込みを制御するためハードウェアと対話する。
タスク状態セグメントについては第2章でまた説明する。

main.cのmpmain関数

// Common CPU setup code.
static void
mpmain(void)
{
  cprintf("cpu%d: starting\n", cpu->id);
  idtinit();       // load idt register
  xchg(&cpu->started, 1); // tell startothers() we're up
  scheduler();     // start running processes
}

proc.cのscheduler関数

//PAGEBREAK: 42
// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run
//  - swtch to start running that process
//  - eventually that process transfers control
//      via swtch back to the scheduler.
void
scheduler(void)
{
  struct proc *p;

  for(;;){
    // Enable interrupts on this processor.
    sti();

    // Loop over process table looking for process to run.
    acquire(&ptable.lock);
    for(p = ptable.proc; p < &ptable.proc&#91;NPROC&#93;; p++){
      if(p->state != RUNNABLE)
        continue;

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

      // Process is done running for now.
      // It should have changed its p->state before coming back.
      proc = 0;
    }
    release(&ptable.lock);

  }
}

vm.cのswitchuvm関数

// Switch TSS and h/w page table to correspond to process p.
void
switchuvm(struct proc *p)
{
  pushcli();
  cpu->gdt[SEG_TSS] = SEG16(STS_T32A, &cpu->ts, sizeof(cpu->ts)-1, 0);
  cpu->gdt[SEG_TSS].s = 0;
  cpu->ts.ss0 = SEG_KDATA << 3;
  cpu->ts.esp0 = (uint)proc->kstack + KSTACKSIZE;
  ltr(SEG_TSS << 3);
  if(p->pgdir == 0)
    panic("switchuvm: no pgdir");
  lcr3(v2p(p->pgdir));  // switch to new address space
  popcli();
}

そしたらschedulerはp->stateをRUNNINGにセットし、切替先のプロセスのカーネルスレッドへコンテキストスイッチさせるためにswtchを呼ぶ。
(switchじゃなくてswtchなので注意。以前switchって書いてる部分があったかも。Cの予約語と被らないようにするためですかね)
swtchは現在のレジスタを保存し、対象のカーネルスレッドの保存されたレジスタ(proc->context)をx86のハードウェアのレジスタにロードする。
それは、スタックポインタと命令ポインタを含む。
現在のコンテキストは、プロセスではなく、CPUごとの特別なスケジューラコンテキストであるので、schedulerはどこかのプロセスのカーネルスレッドコンテキストの中ではなく、CPUごとのストレージ(cpu->scheduler)の中に現在のハードウェアレジスタを保存するようswtchに伝える。
第4章でのその切替についてのより詳細な説明を行う。
最後のret命令(swtchの最後)はスタックから新しい%eipを取り出し、コンテキストスイッチを完了させる。
そうやって、プロセッサは、プロセスpのカーネルスレッドを実行している状態になる。

swtch.S

# Context switch
#
#   void swtch(struct context **old, struct context *new);
# 
# Save current register context in old
# and then load register context from new.

.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

allocprocはinitprocのp->context->eipをforkretに設定するので、retはforkretから再開する。
forkretはptable.lockを解放する。(詳しくは第3章)
最初の呼び出しでは(これがそれだが)、forkret関数は、main関数からは実行出来ない初期化処理を実行する。
なぜmain関数から実行できないかというと、そのプロセスのカーネルスタックとともに通常のプロセスのコンテキストで実行されなければならないからである。
そして、forkretから返る。
allocprocはスタック上でp->contextの後に配置したので、取り出された後はtrapretがトップになる。
それでtrapretは%espへp->tfを設定しつつ実行を開始する。
trapretは、カーネルコンテキストと共にswtchがしたように、トラップフレームを登っていくためにpop命令を使う。
(謎)
そして、popal命令は、普通のレジスタを復元し、popl命令で%gs, %fs, %es, %dsを復元する。
trapnoとerrcodeの二つのフィールドを飛ばすためにaddlを使ってる。
最後に、iret命令で、スタックから%cs, %eip, %flagsを取り除く。
トラップフレームの内容は、CPUに転送された。
だからプロセッサはトラップフレームで指定された%eipから続行する。
initprocについては、これは仮想アドレス0、initcode.Sの最初の命令を意味する。
(initcode.Sは以前出てきましたね。)

proc.cのallocproc関数(前回も載せてます)

//PAGEBREAK: 32
// Look in the process table for an UNUSED proc.
// If found, change state to EMBRYO and initialize
// state required to run in the kernel.
// Otherwise return 0.
static struct proc*
allocproc(void)
{
  struct proc *p;
  char *sp;

  acquire(&ptable.lock);
  for(p = ptable.proc; p < &ptable.proc&#91;NPROC&#93;; p++)
    if(p->state == UNUSED)
      goto found;
  release(&ptable.lock);
  return 0;

found:
  p->state = EMBRYO;
  p->pid = nextpid++;
  release(&ptable.lock);

  // 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;
}

proc.cのforkret関数

// A fork child's very first scheduling by scheduler()
// will swtch here.  "Return" to user space.
void
forkret(void)
{
  static int first = 1;
  // Still holding ptable.lock from scheduler.
  release(&ptable.lock);

  if (first) {
    // Some initialization functions must be run in the context
    // of a regular process (e.g., they call sleep), and thus cannot 
    // be run from main().
    first = 0;
    initlog();
  }
  
  // Return to "caller", actually trapret (see allocproc).
}

trapasm.Sのtrapret

  # Return falls through to trapret...
.globl trapret
trapret:
  popal
  popl %gs
  popl %fs
  popl %es
  popl %ds
  addl $0x8, %esp  # trapno and errcode
  iret

この点では、%eipはゼロを保持し、%espは4096を保持する。
それらは、プロセスのアドレス空間における仮想アドレスである。
プロセッサのページングハードウェアは、それらを物理アドレスに翻訳する。
allocuvm関数は、このプロセスに割り当てられた物理メモリを指すための仮想アドレスゼロのページのPTEをセットアップし、そのPTEをそのプロセスが使えるようにPTE_Uでマークする。
PTE_UがセットされたPTEはそのプロセスのページテーブルでは他には存在しない。
userinitが、プロセスのユーザコードをCPL=3で実行するために%csの下位ビットを設定するという事実は、ユーザコードが、PTE_UがセットされたPTEだけを使えて、そして%cr3のような機密扱いのハードウェアレジスタを変更出来ないという事になる。
(CPL: Current Privilege level 現在の特権レベルの事かと)
なので、プロセスは自分自身のメモリを使うことを強制される。

proc.cのuserinit関数

//PAGEBREAK: 32
// 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;
}

initcode.Sは、$argv, $init, $0の3つの値をスタックにプッシュすることからはじめ、%eaxにSYS_execをセットし、int T_SYSCALLを実行する。
それは、execシステムコールを実行するためにカーネルに伝える。
もしすべてうまく行ったら、execは決して戻らない。
$initという名前が付けられたプログラムの実行を開始する。
$initは、”/init”というNUL終端文字列へのポインタである。
もしexecが失敗し戻ってきたときは、確実に戻らないようにするためにinitcodeはexitシステムコールを呼びつづける。

initcode.S

# Initial process execs /init.

#include "syscall.h"
#include "traps.h"


# exec(init, argv)
.globl start
start:
  pushl $argv
  pushl $init
  pushl $0  // where caller pc would be
  movl $SYS_exec, %eax
  int $T_SYSCALL

# for(;;) exit();
exit:
  movl $SYS_exit, %eax
  int $T_SYSCALL
  jmp exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

execシステムコールへの引数は、$initと$argvである。
最後の$0は、この手書きのシステムコールを通常のシステムコールに見せかけるためにある。
その辺の詳しいことは第2章で。
前の通り、このセットアップは、最初のプロセスの特別なやり方(その場合、それが最初のシステムコールである)は避けている。
その代わり、基本的な操作のためにxv6が提供すべきコードを再利用する。

感想

これでやっと最初のプロセスinitcodeが実行できたってところですかね。

mpmainは論理CPUごとに実行されるみたいなので、スケジューラも論理CPUごとに実行されるみたいですね。
マルチプロセッサ関連の話は後で出てくるんでしょう。

正直半分も理解できてない感じです。
理解しているつもりの部分に関しても自信はないです。
ただまだ最初のプロセスが起動するまでについての章なので、それ以外の部分がまだ概要レベルの説明にとどまってるというのもあるかと思います。
とりあえずこのまま読み続けて全体が見えるようになれば理解が深まるはずだと思っています。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示