LKNTが仮想マシンにメモリをどのように割り当てているのか調べました. 流れとしては,ゲスト作成時にメモリを確保,KVMに確保したメモリを伝える,ゲストが終了する際に確保していたメモリを開放という手順です. 今回はユーザ側のコードですが,いずれはKVMの内部でメモリがどのように扱われているのかも調べてみたいです.
ユーザインタフェース
$ ./kvm run -m 256
ユーザがゲストに割り当てるメモリのサイズを指定するにはコマンドライン引数を用います.
メモリ確保
static u64 ram_size;
指定した値は,グローバル変数のram_sizeに格納されます.
build-run.c/int kvm_cmd_run
if (!ram_size) ram_size = get_ram_size(nrcpus);
コマンドライン引数で指定しなかった場合どうなるのでしょうか? build-run.cの中でram_sizeに値が格納されているかどうかを確認し,格 納されていない場合はget_ram_size関数で割り当てるメモリサイズを算出 し,ram_sizeに格納します.
build-run.c
#define RAM_SIZE_RATIO 0.8 static u64 get_ram_size(int nr_cpus) { u64 available; u64 ram_size; ram_size = 64 * (nr_cpus + 3); available = host_ram_size() * RAM_SIZE_RATIO; if (!available) available = MIN_RAM_SIZE_MB; if (ram_size > available) ram_size = available; return ram_size; }
get_ram_sizeには使用可能なCPUコア数を渡しています.そして,コア数からメモリサイズを算出します. コア数が2なら,64x(2+3)=320MBとなるようです. ゲストに割り当てられるメモリサイズはマシン全体のメモリサイズにRAM_SIZE_RATIOを掛けたサイズとなります. 算出したメモリサイズが割り当てられる限界値を越えている場合,限界値に修正してram_sizeを返却します.
build.c/host_ram_size
static u64 host_ram_size(void) { long page_size; long nr_pages; nr_pages = sysconf(_SC_PHYS_PAGES); if (nr_pages < 0) { pr_warning("sysconf(_SC_PHYS_PAGES) failed"); return 0; } page_size = sysconf(_SC_PAGE_SIZE); if (page_size < 0) { pr_warning("sysconf(_SC_PAGE_SIZE) failed"); return 0; } return (nr_pages * page_size) >> MB_SHIFT; }
get_ram_sizeの中で呼び出されているhost_ram_size関数はホストマシンのメモリサイズを調べる関数です. sysconf(_SC_PHYS_PAGES)で物理メモリのページ数を取得,sysconf(_SC_PAGE_SIZE)でページサイズをバイト単位で取得します. ページ数xページサイズ=メモリサイズ(バイト)となります. 最後に単位をメガバイトに変更して返却します. メガバイトに変換する時,右シフトを使っています.
build.c
#define MB_SHIFT (20)
1メガバイト = 1048576バイトで,2進数にすると1048576 = 0b100000000000000000000となります. これを20ビット右シフトすると1になり,バイトからメガバイトへ変換ができるわけです.
build-run.c/int kvm_cmd_run
if (ram_size < MIN_RAM_SIZE_MB) die("Not enough memory specified: %lluMB (min %lluMB)", ram_size, MIN_RAM_SIZE_MB); if (ram_size > host_ram_size()) pr_warning("Guest memory size %lluMB exceeds host physical RAM size %lluMB", ram_size, host_ram_size()); ram_size <<= MB_SHIFT;
ゲストに割り当てるメモリのサイズが最小サイズより大きいか,もしくはホストのメモリサイズより大きくなっていないか確認します. 問題なければ,ram_sizeをメガバイトからバイトに戻しています.
KVMへ確保したメモリを登録する
さて,kvmの初期化関数に移動しましょう.
builtin-run.c/kvm_cmd_run
kvm = kvm__init(dev, ram_size, guest_name);
kvm_initは,さらにアーキテクチャ固有のinit関数を呼び出しています.
kvm.c/kvm__init
kvm__arch_init(kvm, kvm_dev, ram_size, name);
x86/kvm.c
/* Architecture-specific KVM init */ void kvm__arch_init(struct kvm *kvm, const char *kvm_dev, u64 ram_size, const char *name) { struct kvm_pit_config pit_config = { .flags = 0, }; int ret; ret = ioctl(kvm->vm_fd, KVM_SET_TSS_ADDR, 0xfffbd000); if (ret < 0) die_perror("KVM_SET_TSS_ADDR ioctl"); ret = ioctl(kvm->vm_fd, KVM_CREATE_PIT2, &pit_config); if (ret < 0) die_perror("KVM_CREATE_PIT2 ioctl"); kvm->ram_size = ram_size; if (kvm->ram_size < KVM_32BIT_GAP_START) { kvm->ram_start = mmap(NULL, ram_size, PROT_RW, MAP_ANON_NORESERVE, -1, 0); } else { kvm->ram_start = mmap(NULL, ram_size + KVM_32BIT_GAP_SIZE, PROT_RW, MAP_ANON_NORESERVE, -1, 0); if (kvm->ram_start != MAP_FAILED) { /* * We mprotect the gap (see kvm__init_ram() for details) PROT_NONE so that * if we accidently write to it, we will know. */ mprotect(kvm->ram_start + KVM_32BIT_GAP_START, KVM_32BIT_GAP_SIZE, PROT_NONE); } } if (kvm->ram_start == MAP_FAILED) die("out of memory"); madvise(kvm->ram_start, kvm->ram_size, MADV_MERGEABLE); ret = ioctl(kvm->vm_fd, KVM_CREATE_IRQCHIP); if (ret < 0) die_perror("KVM_CREATE_IRQCHIP ioctl"); }
kvm->ram_sizeにコマンドライン引数から取得した/自動生成したメモリサイズを格納します. ここで,32bitのギャップについてチェックしています.
#define KVM_32BIT_GAP_START ((1ULL << 32) - KVM_32BIT_GAP_SIZE) #define KVM_32BIT_GAP_SIZE (768 << 20)
KVM_32BIT_GAP_SIZEは (768<<20) = 805306368 = 768MB. KVM_32BIT_GAP_STARTは ((1ULL << 32) – KVM_32BIT_GAP_SIZE) = (4294967296 – 768MB) = (4096MB -768MB) = 3328MB.
指定したram_sizeが3328MBより小さい場合,そのサイズをmmapで確保します. ram_sizeが3328MB以上の場合,ram_size+768MBをmmapでマッピングします. メモリのマップに成功した場合,mprotectを使ってkvm->ram_start + KVM_32BIT_GAP_START〜KVM_32BIT_GAP_SIZEのメモリ範囲のアクセス保護を設定しています. PROT_NOTEは,メモリに全くアクセスできない設定となります. メモリのマップに失敗した場合はdieです.
madviceはメモリの利用に関するアドバイスを設定する関数です. kvm->ram_startからkvm->ram_sizeの範囲について,MADV_MERGEABLEに設定しています.MADV_MERGEABLEは,ユーザがマージしてもよいという設定です.
メモリの設定
build-run.cでしばらく先にあるkvm__init_ramでメモリ設定を行います.
builtin-run.c
kvm__init_ram(kvm);
x86/kvm.c
void kvm__init_ram(struct kvm *kvm) { u64 phys_start, phys_size; void *host_mem; if (kvm->ram_size < KVM_32BIT_GAP_START) { /* Use a single block of RAM for 32bit RAM */ phys_start = 0; phys_size = kvm->ram_size; host_mem = kvm->ram_start; kvm__register_mem(kvm, phys_start, phys_size, host_mem); } else { /* First RAM range from zero to the PCI gap: */ phys_start = 0; phys_size = KVM_32BIT_GAP_START; host_mem = kvm->ram_start; kvm__register_mem(kvm, phys_start, phys_size, host_mem); /* Second RAM range from 4GB to the end of RAM: */ phys_start = 0x100000000ULL; phys_size = kvm->ram_size - phys_size; host_mem = kvm->ram_start + phys_start; kvm__register_mem(kvm, phys_start, phys_size, host_mem); } }
ここでも32ビットギャップを調べて,結果によって挙動を変えています. メモリサイズが3328MBより小さい場合,ゲストに設定する仮想メモリのサイズにkvm->ram_sizeを,ホストメモリの開始位置にkvm->ram_startを格納し,kvm__register_memを呼び出してVMのメモリの設定を行います.
3328MB以上の場合,仮想メモリのサイズに3328MBを設定してVMのメモリ設定を行い, その後,残りのサイズを再びVMに設定しています.
/kvm.c
*/ void kvm__register_mem(struct kvm *kvm, u64 guest_phys, u64 size, void *userspace_addr) { struct kvm_userspace_memory_region mem; int ret; mem = (struct kvm_userspace_memory_region) { .slot = kvm->mem_slots++, .guest_phys_addr = guest_phys, .memory_size = size, .userspace_addr = (unsigned long)userspace_addr, }; ret = ioctl(kvm->vm_fd, KVM_SET_USER_MEMORY_REGION, &mem); if (ret < 0) die_perror("KVM_SET_USER_MEMORY_REGION ioctl"); }
ioctlのKVM_SET_USER_MEMORY_REGIONを使い,VMにメモリの変更を許可します.
KVM_SET_USER_MEMORY_REGIONはKVMのAPIです. mem_slotsがマザーボードのメモリスロットに対応するようです. 3328MB以上のメモリを扱う場合は,複数スロットに分割するわけですね. これで,VMがメモリを使えるようになります.
メモリの開放
VMを終了するときにメモリを開放する必要があります.kvm__deleteの中で開放されています.
builtin-run.c
kvm__delete(kvm);
kvm.c
void kvm__delete(struct kvm *kvm) { kvm__stop_timer(kvm); munmap(kvm->ram_start, kvm->ram_size); kvm_ipc__stop(); kvm__remove_socket(kvm->name); free(kvm); }
munmapを使い,指定したアドレス範囲のマップを消去するだけのようです.