xv6のメモリ管理周りのコードリーディング

xv6のページング周りをちょっと見てたので記録をφ(..)メモメモ

参照したドキュメントはbook-rev10.pdfです。

book-rev10.pdfのP21、 Figure 1-2にxv6のメモリレイアウトがあります。

f:id:masami256:20180305233136p:plain

仮想アドレスの0から0x80000000がユーザー空間で、0x80000000〜0xFFFFFFFFがカーネル空間ですね。カーネル空間のうち先頭の0x100000バイトはBIOSの領域となってます。

カーネルアドレス空間vm.cにあるkmap構造体で管理してます。

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

KERNBASEやKERNLINKなどの定数はmemorylayout.hにて定義されています。

#define EXTMEM  0x100000            // Start of extended memory
#define PHYSTOP 0xE000000           // Top physical memory
#define DEVSPACE 0xFE000000         // Other devices are at high addresses

// Key addresses for address space layout (see kmap in vm.c for layout)
#define KERNBASE 0x80000000         // First kernel virtual address
#define KERNLINK (KERNBASE+EXTMEM)  // Address where kernel is linked

V2Pはこんなマクロです。

#define V2P(a) (((uint) (a)) - KERNBASE)

ここでkmapのコメントにあるようなメモリレイアウトにしてるわけですね。

//   0..KERNBASE: user memory (text+data+stack+heap), mapped to
//                phys memory allocated by the kernel
//   KERNBASE..KERNBASE+EXTMEM: mapped to 0..EXTMEM (for I/O space)
//   KERNBASE+EXTMEM..data: mapped to EXTMEM..V2P(data)
//                for the kernel's instructions and r/o data
//   data..KERNBASE+PHYSTOP: mapped to V2P(data)..PHYSTOP,
//                                  rw data + free physical memory
//   0xfe000000..0: mapped direct (devices such as ioapic)

このkmap構造体を使ってマッピングを行っているのがsetupkvm()です。setupkvm()の戻り値の型はpde_t *で、ようはページグローバルディレクトリです。これのアドレスがcr3レジスタに設定されます。

setupkvm()を呼ぶのは4箇所有ります。

exec()ではexecシステムコールで既存のプロセスのアドレス空間を新しいプロセスのアドレス空間に変える時に呼んでいます。userinit()は一番最初のユーザープロセスを作る時に呼んでいます。 kvmalloc()ではカーネルのスケジューラー用に作っているようです。最後のcopyuvm()はfork()の実行時に親プロセスのページテーブルをコピーする時に呼んでいます。

実際にマッピングを行っているのはmappages()です。mappages()は引数としてページグローバルディレクトリの先頭アドレス、マッピングしたい仮想アドレス、そのアドレスのサイズ、仮想アドレスにマッピングする物理アドレス、ページに設定するパーミッションを受け取ります。1番目のページグローバルディレクトリは呼び出し側がkalloc()でメモリを確保しています。kalloc()はkalloc.cに実装があって、空きページを1ページ確保して返す関数です。

mappages()を呼び出している箇所は4箇所です。

setupkvm()は先程も出てきたとおりで、kmap構造体のデータを実際にマッピングしてます。inituvm()はuserinit()の処理中に呼ばれます。_binary_initcode_startなどはカーネルをビルドした後にできるkernel.symというファイルで確認できます。allocuvm()はユーザー空間の大きさを大きくする時に呼んでいます。copyuvm()はfork()の実行時に呼ばれます。

ページフォルトですがxv6ではデマンドページングはやっていないのでエラーとして処理してます。処理しているのはtrap.cにあるtrap()です。カーネルアドレス空間ページフォルトが発生した場合はpanic、ユーザー空間の場合はプロセスを終了させるようです。

default:
    if(myproc() == 0 || (tf->cs&3) == 0){
      // In kernel, it must be our mistake.
      cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n",
              tf->trapno, cpuid(), tf->eip, rcr2());
      panic("trap");
    }
    // In user space, assume process misbehaved.
    cprintf("pid %d %s: trap %d err %d on cpu %d "
            "eip 0x%x addr 0x%x--kill proc\n",
            myproc()->pid, myproc()->name, tf->trapno,
            tf->err, cpuid(), tf->eip, rcr2());
    myproc()->killed = 1;

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)

はじめてのOSコードリーディング ~UNIX V6で学ぶカーネルのしくみ (Software Design plus)