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分のページを追加できた。