rust-vmmが提供するクレートでKVMを操作する——kvm-ioctls/kvm-bindings/vm-memory
KVMを操作するVMMを、rust-vmmというプロジェクトが提供するkvm-ioctls/kvm-bindings/vm-memoryクレートを使って実装します。rust-vmmは単一のクレート名ではなく、機能ごとに分割された複数のクレートを公開しているGitHub上のプロジェクトで、Firecrackerを含む複数のVMM実装がこれらのクレートを部品として使っています。ioctl番号の計算やKVMのC構造体の移植、mmapによるゲストメモリ確保といった処理を、これらのクレートが提供する型やメソッドに置き換えていきます。
バージョンは事前に動作確認済みの組み合わせに固定しました。
[dependencies]
kvm-ioctls = "0.25.0"
kvm-bindings = "0.14.1"
vm-memory = { version = "0.18.0", features = ["backend-mmap"] }
libc = "0.2.189"vm-memoryはbackend-mmapフィーチャーを明示的に有効化しないと、今回使うGuestMemoryMmapが公開されません。
以下、自前実装(ゲストメモリを複数リージョンに分割する——KVMのメモリスロットとホスト・ゲスト間の直接共有時点のコード)とクレートを使った実装を並べて、いくつか具体的に見ていきます。
メモリリージョンの登録
自前実装では、KVMのC構造体をそのまま移植したKvmUserspaceMemoryRegionを定義し、ioctl番号も自分で計算していました。
#[repr(C)]
struct KvmUserspaceMemoryRegion {
slot: u32,
flags: u32,
guest_phys_addr: u64,
memory_size: u64,
userspace_addr: u64,
}
const KVM_SET_USER_MEMORY_REGION: u64 = iow::<KvmUserspaceMemoryRegion>(0x46);クレートを使うと、この構造体はkvm-bindingsのkvm_userspace_memory_region(フィールド名も同一)として、ioctl番号の計算やその呼び出しはkvm-ioctlsのVmFd::set_user_memory_regionとして、あらかじめ用意されています。
let region = kvm_userspace_memory_region {
slot,
flags: 0,
guest_phys_addr: guest_addr,
memory_size: size as u64,
userspace_addr: host_addr as u64,
};
unsafe {
vm.set_user_memory_region(region).unwrap();
}構造体の中身を自分で組み立てる点は変わりませんが、フィールドの並びやサイズをC側の定義と一致させる責任、iowでのビットパック計算はクレート側に任せられます。
ゲストメモリの確保
自前実装では、mmapをラップしたmap_guest_memoryというヘルパーを自分で用意していました。
unsafe fn map_guest_memory(size: usize) -> *mut libc::c_void {
let mem = unsafe {
libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
assert_ne!(mem, libc::MAP_FAILED, "mmap(guest memory) failed");
mem
}vm-memoryクレートでは、ゲスト物理アドレスとサイズの組を渡すだけで、複数リージョン分のmmapをまとめて済ませてくれるGuestMemoryMmap::from_rangesが用意されています。
let guest_memory = GuestMemoryMmap::<()>::from_ranges(&[
(GuestAddress(CODE_ADDR), CODE_SIZE),
(GuestAddress(DATA_ADDR), DATA_SIZE),
])
.unwrap();ゲストメモリへの読み書きも、自前実装では生ポインタへのptr::copy_nonoverlappingや*self.ptr.add(offset)を直接書いていましたが、クレートを使うとBytesトレイトのwrite_slice/read_objをゲスト物理アドレス(GuestAddress)指定で呼ぶだけになります。
guest_memory.write_slice(&CODE, GuestAddress(CODE_ADDR)).unwrap();
let data_after: u8 = guest_memory.read_obj(GuestAddress(DATA_ADDR)).unwrap();VM Exitの判定とI/Oデータの取得
自前実装では、kvm_run構造体のexit_reason(u32)を見てmatchし、I/O exitの場合は構造体中のio_data_offsetから生ポインタでデータを読み出していました。
match (*run).exit_reason {
KVM_EXIT_HLT => break,
KVM_EXIT_IO => {
if (*run).io_direction == KVM_EXIT_IO_OUT && (*run).io_port == 0x3f8 {
let data = (run as *const u8).add((*run).io_data_offset as usize);
out.write_all(&[*data]).unwrap();
out.flush().unwrap();
}
}
other => panic!("unhandled KVM exit reason: {}", other),
}kvm-ioctlsではVcpuFd::run()がVcpuExitという列挙型を返し、exitの種類ごとに必要なデータがバリアントの値として直接渡ってきます。
match vcpu.run().unwrap() {
VcpuExit::Hlt => break,
VcpuExit::IoOut(port, data) => {
if port == 0x3f8 {
out.write_all(data).unwrap();
out.flush().unwrap();
}
}
other => panic!("unhandled KVM exit reason: {:?}", other),
}VcpuExit::IoOut(port, data)のdataは&[u8]のスライスとしてすでに切り出された状態で渡ってくるため、io_data_offsetから自分でポインタを進める処理が不要になります。matchが扱っているのが生のu32ではなく型付きの列挙型になったことで、扱っていないexit理由を網羅的に検討しやすくなる点も違いです。
残るunsafe
set_user_memory_regionはunsafe fnです。userspace_addrが実際に有効なメモリを指しているか、リージョン同士が重なっていないかはクレート側では検証できず、呼び出し元が保証する必要があるためです。今回のコードでunsafeが残るのはこの1箇所だけで、それ以外は全て安全なRustのコードになっています。
実行結果はこれまでと変わらず、4(シリアル経由)に続けてhost read guest physical 0x2000 directly: 4が出力されます。