5.2. kinit1関数

この関数は、大域変数kmemのfreelistに、kernelの終わりから物理アドレス4MBまでを追加する。

main関数からはkernelのendシンボルのアドレス(vstart)と、物理アドレス4MBの仮想アドレス(vend)を受け取る。

main.c

extern char end[]; // first address after kernel loaded from ELF file

/* 略 */

int
main(void)
{
  kinit1(end, P2V(4*1024*1024)); // phys page allocator

endは「2.4. ターゲットkernel」で見たリンカスクリプトkernel.ldによりbssセグメントの後ろ、elfの最後に作成されている。 アドレスはreadelfで確認でき、ここでは0x80116528になっている。
物理アドレスにすると0x116528。

readelf -s kernel | grep end

   257: 801035ae   196 FUNC    GLOBAL DEFAULT    1 end_op
   331: 80116528     0 NOTYPE  GLOBAL DEFAULT    4 end
   352: 8010b4ec     0 NOTYPE  GLOBAL DEFAULT    3 _binary_initcode_end
   503: 8010b576     0 NOTYPE  GLOBAL DEFAULT    3 _binary_entryother_end

物理アドレスから仮想アドレスへの変換はP2Vマクロを使う。V2Pマクロと同様にカーネルベースアドレスを使って変換する。

memlayout.h

#define KERNBASE 0x80000000         // First kernel virtual address

/* 略 */

#define P2V(a) ((void *)(((char *) (a)) + KERNBASE))

main関数から与えられたアドレスの範囲は、物理アドレスだと0x116528(vstart)から0x400000(vend)。
freelistに加えるためにPGROUNDUPマクロで4kBにアラインメントするので、開始アドレスは0x117000となり0x400000と差を取ると0x2E9AD8 = 0x2E9000で約3MB。

ページは大域変数kmemのfreelistフィールドに持っておく。 freelistフィールドはrun構造体の線形リストになっており、ページが必要な時はこのリストから取得し、解放するときはこのリストに戻すことになる。 kmem構造体はその他に2つのフィールドを持っており、lockフィールドは排他制御に使用し、use_lockフィールドは排他制御の必要性を示す。 use_lockが0のときは排他制御が不要なので、freelistへアクセスする際にkmemのロックを取らない。use_lockはkinit2関数で初めて1に初期化される。
spinlock構造体等を用いた排他制御に関しては、consoleinit関数で見る。

kalloc.c

struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  int use_lock;
  struct run *freelist;
} kmem;

kinit1関数は、大域変数kmemのロックを初期化し、freelistにアドレスend(vstart)の次のページから物理アドレス0x400000までを追加する。追加されるページは全て1埋めされる。

kalloc.c

void
kinit1(void *vstart, void *vend)
{
  initlock(&kmem.lock, "kmem");
  kmem.use_lock = 0;
  freerange(vstart, vend);
}

freerange関数

この関数は、指定された仮想アドレスの範囲をkmemのfreelistに加える。

freelistに加えられる範囲は、vstartの次の4kB境界から、vendの次の4kB境界までである。
PGROUNDUPマクロは引数として与えられたアドレスの次のページ境界のアドレスを計算する。

mmu.h

#define PGROUNDUP(sz)  (((sz)+PGSIZE-1) & ~(PGSIZE-1))

kalloc.c

void
freerange(void *vstart, void *vend)
{
  char *p;
  p = (char*)PGROUNDUP((uint)vstart);
  for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
    kfree(p);
}

kfree関数

この関数は、指定された仮想アドレスのページをkmemのfreelistに加える。ページサイズは4kBで内容は全て1で初期化される。

ページの開始アドレス(v)はページサイズで割り切れ、end以上PHYSTOP未満でなければならない。PHYSTOP(0xE000000)はxv6での物理アドレスの上限。この条件を満たさない場合、panic関数を呼び出す。
panic関数はコンソールのロックを取り、パニックした理由とコールスタックをコンソールに出力し、panickedフラグを1にして無限ループに入る。 コンソール出力にはcgaputc関数を使用する。

ページの初期化はmemset関数で1で埋めすることで行う。
kmemのuse_lockフィールドが1のとき、APが起動しており排他制御が必要となるため、freelistにページを加える間、acquire関数とrelease関数でロックの取得と解放を行う。 kinit1関数の呼び出しではuse_lockは0なので、排他制御は行わない。
初期化したページはfreelistの先頭に追加していく。

kalloc.c

void
kfree(char *v)
{
  struct run *r;

  if((uint)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  memset(v, 1, PGSIZE);

  if(kmem.use_lock)
    acquire(&kmem.lock);
  r = (struct run*)v;
  r->next = kmem.freelist;
  kmem.freelist = r;
  if(kmem.use_lock)
    release(&kmem.lock);
}

memlayout.h

#define PHYSTOP 0xE000000           // Top physical memory

memset関数

この関数は指定したアドレスに値をnバイト分書き込む。

書き込み先アドレス(dst)と書き込みサイズ(n)が4で割り切れる場合、stosl関数を使用して4バイトずつ書き込みを行う。 割り切れない場合はstosb関数を使って1バイトずつ書き込む。
4バイトずつ書き込む場合は、書き込む値(c)から1バイト分取り出し、それをシフトして4バイト並べた値を書き込む。

stosb関数とstosl関数は「3.2. bootmain関数」で見たinsl関数と同様にrepを使用したパターンになっている。

string.c

void*
memset(void *dst, int c, uint n)
{
  if ((int)dst%4 == 0 && n%4 == 0){
    c &= 0xFF;
    stosl(dst, (c<<24)|(c<<16)|(c<<8)|c, n/4);
  } else
    stosb(dst, c, n);
  return dst;
}

x86.h

static inline void
stosb(void *addr, int data, int cnt)
{
  asm volatile("cld; rep stosb" :
               "=D" (addr), "=c" (cnt) :
               "0" (addr), "1" (cnt), "a" (data) :
               "memory", "cc");
}

static inline void
stosl(void *addr, int data, int cnt)
{
  asm volatile("cld; rep stosl" :
               "=D" (addr), "=c" (cnt) :
               "0" (addr), "1" (cnt), "a" (data) :
               "memory", "cc");
}

これで大域変数kmemのfreelistに約3MB分のページを追加できた。