読者です 読者をやめる 読者になる 読者になる

xv6のブートシーケンスメモ

xv6 kernel

φ(゚-゚=)メモニャン xv6の某勉強会に行けないので適当にメモってみた。

ということでブートシーケンスを適当に見ていきます!

リアルモードからプロテクトモードへの移行

自前ブートローダの場合:
bootasm.Sが16bitモードから起動する場合の処理で、32bitへの移行とgdtの初期設定をしてからbootmain.cのbootmain()を呼ぶ
bootmain()からメモリにロードされたカーネルのバイナリがELFかチェック
ELFだった場合はセグメントをロードしていく

Grubのブートプロトコル使用時:
xv6はgrubのブートプロトコルに対応しているのでgrubから起動した場合は最初にentry.Sのコードに制御が移り、この段階ですでにプロテクトモードになっているはず。

bootmain()から呼ばれた場合もboootmain.Sのほうでプロテクトモードに移行済み。

ここでやるのはページングの設定でページサイズを4MBでページングを有効にする
ページディレクトリにはentrypgdirという変数が使われている
このentrypgdirはmain.cで↓のように定義されているのと

pde_t entrypgdir[];  // For entry.S

main.cの一番最後に以下のように初期化されている。

// Boot page table used in entry.S and entryother.S.
// Page directories (and page tables), must start on a page boundary,
// hence the "__aligned__" attribute.  
// Use PTE_PS in page directory entry to enable 4Mbyte pages.
__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
  // Map VA's [0, 4MB) to PA's [0, 4MB)
  [0] = (0) + PTE_P + PTE_W + PTE_PS,
  // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
  [KERNBASE>>PDXSHIFT] = (0) + PTE_P + PTE_W + PTE_PS,
};

ページングの設定が終わったらmain.cのmain()に制御が移る

ここからはmain()の初期化の順番で見ていきます。

1.kvmalloc()
vm.cで定義
この関数はentry.Sで設定したページディレクトリ(entrypgdir)からkpgdirにページディレクトリを設定しなおす
ということはentry.Sではページングの仮設定だけで、本当の設定はここで実施しているということ
switchkvm()はcr3レジスタにkpgdirのアドレスをセットするだけで、本当の処理はswitchkvm()で実施。

kvmalloc(void)
{
  kpgdir = setupkvm(enter_alloc);
  switchkvm();
}

switchkvm()も主要なのは以下のループで、

 for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
    if(mappages(pgdir, k->virt, k->phys_end - k->phys_start, 
                (uint)k->phys_start, k->perm, alloc) < 0)

kmapはvm.cで以下のように初期化済み。NELEMはマクロでkmapの配列数を返す形のマクロ(sizeof(hoge)/sizeof(hoge[0])しているだけ)。

static struct kmap {
  void *virt;
  uint phys_start;
  uint phys_end;
  int perm;
} kmap[] = {
  { P2V(0), 0, 1024*1024, PTE_W},  // I/O space
  { (void*)KERNLINK, V2P(KERNLINK), V2P(data),  0}, // kernel text+rodata
  { data, V2P(data), PHYSTOP,  PTE_W},  // kernel data, memory
  { (void*)DEVSPACE, DEVSPACE, 0, PTE_W},  // more devices
};

mappages()でPTEのエントリを作っていく。


2.mpinit()
その名の通りでマルチプロセッサの場合に、APを初期化する。ただし、APの起動はここではやらないでstartothers()で実施している
MP環境を気にしないなら読み飛ばしてもそんなに問題はないんじゃないでしょうか。

3.lapicinit()
これも名前の通りでlocal APICの初期化。これも必要に応じて読むという感じで問題はないんじゃないでしょうか。

4.seginit()
これはgdtに関する設定を実行。ここではcpu数分の処理が行われる。場所はvm.c
設定しているのはカーネルでのコード、データセグメントの設定とユーザ空間側でのコードとデータセグメントの設定
ここでgdtの設定が行われるのは理由として、ブートローダにbootasm.Sを使った場合・Grubを使った場合ともにgdtの設定は仮なので正しい設定をしてあげる必要があるからですね。
ブートローダとしてGrubを使った場合、「プロテクトモードにしてからカーネルを起動するけど、gdtとかは早めに再設定してね!」というようなことがGrubのドキュメントに書いてあった記憶があります(前にカーネルを自作した時に読んだ記憶ががが)。

5.picinit()
関数はpicirq.cに。関数先頭のコメントから8259Aの初期化するよーと書かれています

6.ioapicinit()
関数本体はioapic.cに。これに限らないけど、ハードウェアの初期化とかを知りたい場合にxv6のコードを読むのはわかりやすい気がしますね。コード量もそんなにない(なさそう)ので。

7.consoleinit()
これもまたシンプルな関数となってますね。
consoleinit()としては、ロック周りの初期化、コンソールに読み書きする関数のセット、キーボード割り込みを有効をしてます。

void
consoleinit(void)
{
  initlock(&cons.lock, "console");
  initlock(&input.lock, "input");

  devsw[CONSOLE].write = consolewrite;
  devsw[CONSOLE].read = consoleread;
  cons.locking = 1;

  picenable(IRQ_KBD);
  ioapicenable(IRQ_KBD, 0);
}

最初のinitlock()ここでロックを使いたいわけじゃなくて、今後使うためにここで初期化ということですね。
ロックに関してはspinlock.cに関数がそろってます。

void
initlock(struct spinlock *lk, char *name)
{
  lk->name = name;
  lk->locked = 0;
  lk->cpu = 0;
}

8.uartinit()
uartはこれ(http://ja.wikipedia.org/wiki/UART)ですね。main()のコメントにもあるようにシリアルポートを初期化すると。ファイルはuart.cです。
シリアルポートの初期化が終わるとuartputc()を使って"xv6..."をコンソールに書き込んでます。

9.pinit()
proc.cにいます。これはmain()のコメントからプロセステーブルの初期化と書かれてますが、実際はinitlock()でロック用の変数を初期化するだけです。
ロックの名前がptableなのでプロセステーブルを操作する時に使用する変数かと思います。

10.tvinit()
名前から何の初期化なのか分からなかったのですが割り込みベクター(idt)の初期化です。trap.cに書かれています。
処理は見ての通りで、とくに変わったことはしていません。

void
tvinit(void)
{
  int i;

  for(i = 0; i < 256; i++)
    SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
  SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
  
  initlock(&tickslock, "time");
}

11.binit()
バッファキャッシュの初期化です。コードはbio.cに。
バッファキャッシュはbio.cの先頭のほうでこのように定義されてます。

struct {
  struct spinlock lock;
  struct buf buf[NBUF];

  // Linked list of all buffers, through prev/next.
  // head.next is most recently used.
  struct buf head;
} bcache;

bcache自体はロック変数とbuf構造体の変数、buf構造体のリンクリストの先頭要素を持っていて、
本当のキャッシュに関するデータはbuf構造体(buf.h)にあります。

struct buf {
  int flags;
  uint dev;
  uint sector;
  struct buf *prev; // LRU cache list
  struct buf *next;
  struct buf *qnext; // disk queue
  uchar data[512];
};

コメントにも出てきてますがバッファキャッシュはLRUにて管理されるようです。
binit()はこれらの構造体のリンクリストを初期化する程度なのでこれくらいにしときます。

12.fileinit()
これもpinit()と同じくロック変数の初期化だけです。関数本体はfile.cです。

13.iinit()
これもロック変数の初期化だけです。fs.cにいます。main()のコメントからinodeのキャッシュ操作用のロック変数ぽいです。

14.ideinit()
これってide.cとmemdisk.cの2ファイルに存在しますね。main()のコメントからするとide.cのほうのideinit()が呼ばれると思いますが。
処理内容としてはIDEの割り込みを有効にしてからIDEディスクが起動するまで待つ。セカンダリIDEがあれば「ある」ということを記憶しておく。

15.timerinit()
ユニプロセッサ環境の場合のみtimerinit()が呼ばれてタイマ割り込みを受けるための準備が入る。関数はtimer.cに存在。

16.startothers()
mpinit()で初期されたAPの起動処理が行われる。
スタックとかコードセグメントなんかの設定をしてからlapicstartap()で実際に起動する処理をする。

17.kinit()
kalloc.cでこのように実装されてます。これも見たままということですよね。

// Initialize free list of physical pages.
void
kinit(void)
{
  char *p;

  initlock(&kmem.lock, "kmem");
  p = (char*)PGROUNDUP((uint)newend);
  for(; p + PGSIZE <= (char*)p2v(PHYSTOP); p += PGSIZE)
    kfree(p);
}

newendはこのようにstaticで宣言されてます。

static char *newend;

ここでnewendは最初0なのでPGROUNDUPでページサイズ単位に切り上げしたところで0のままです。ということで必ずどこかで別の値が設定されているはずですね。
この変数が操作されるのはenter_alloc()なのでこれがkinit()よりも前に呼ばれているはずです。

// A simple page allocator to get off the ground during entry
char *
enter_alloc(void)
{
  if (newend == 0)
    newend = end;

  if ((uint) newend >= KERNBASE + 0x400000)
    panic("only first 4Mbyte are mapped during entry");
  void *p = (void*)PGROUNDUP((uint)newend);
  memset(p, 0, PGSIZE);
  newend = newend + PGSIZE;
  return p;
}

これは多分、main.cのstartothers()にある以下のスタックに使用するメモリ領域を確保するときかなと思います。
このkvmmallocでもenter_alloc()を引数に渡してますが、これはメモリアローけーたーとして関数ポインタを渡しているだけで、

kvmalloc(void)
{
  kpgdir = setupkvm(enter_alloc);
  switchkvm();
}

実際にenter_alloc()が最初に使われるのはstartothers()の以下の行っぽいです。

stack = enter_alloc();

また、main()のコメントでstartothers()はkinit()の前に呼ぶことと書かれているのでここで最初にenter_alloc()が最初に使用されるのでしょう。

  startothers();    // start other processors (must come before kinit)
  kinit();         // initialize memory allocator
||<  
  
18.userinit()
これはproc.cです。ここで一番最初のユーザプロセスが作られると書かれていますがぱっと見ではinitコマンドを呼ぶとかという感じではないですね。
>|c|
// 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;
}

怪しいのは_binary_initcode_start、_binary_initcode_endですがこれはinitcode.Sのこれのことかと。

# 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を動かすコードになってますね。
ということで基本通りにinitプログラムが最初のユーザプロセスになりますね。
プロセスのデータを保存する領域の確保はallocproc()で行っていて、プロセステーブルを検索して最初に見つかった未使用のところを使用します。
pidnextpidという変数で管理されていて、初期値は1なので/initpid1で実行されます。

18.mpmain()
main()の最後に呼ばれる関数ですが、mpenter()から呼ばれるパスもあります。mpenter()はstartothers()の中でAPに設定されています。
>|c|
*(void**)(code-8) = mpenter;

それはさておきコードはmain.cにあって以下のようになってます。

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

ふと気付いたのはidtinit()idtの最終的な設定がここで行われているということですね。
コードはこれですが、lidt命令ってidtrのアドレスを設定するんじゃなかったでしたっけ? idtって割り込みベクタのテーブルだから違うような (ー_ー;)ウーン、、、

idtinit(void)
{
  lidt(idt, sizeof(idt));
}

mpmainに戻ってこの行は

  xchg(&cpu->started, 1); // tell startothers() we're up

startothers()の最後のこのwhileループを抜けるためのものですね。

    // wait for cpu to finish mpmain()
    while(c->started == 0)
      ;

そして、最後にscheduler()が呼ばれて/initのプロセスが走るとかしていくわけですね。

とりあえず雑感です。教育用OSという意味ではMinixが比較対象になると思いますが、コードはMinixよりも読みやすいと思います。
理由としてはコード量が小さいのとモノリシックカーネルなので。もちろん私の個人的な感想ですけどね^^;
Minix v2のカーネルハックしたりMinix本読書会でオペレーティングシステム 第3版を読んでるのでMinixのコードに触れる機会はそこそこあるのでそれらを踏まえてはいます…)
あと「はじめて読む486」の知識があればカーネルが起動するまでの流れは分かりやすいと思います。