Rustで書かれたVMM firecrackerを読もう!(3)~mainのテストとVmm::new()~
tomo-wait-for-it-yuki.hatenablog.com
前回は、src/main.rs
のmain関数を読みました。そういえば、main関数のテストを紹介しようと思って忘れていたので、今回書きます。
mainのテスト
src/main.rs
に含まれるユニットテストです。まずは、ヘルパー関数です。
#[cfg(test)] mod tests { ... // テストのヘルパー関数 // log_pathのファイルに、expectationsの文字列3つが含まれているかどうかをテストする fn validate_backtrace( log_path: &str, expectations: &[(&'static str, &'static str, &'static str)], ) { let f = File::open(log_path).unwrap(); let reader = BufReader::new(f); let mut pass = false; let mut expectation_iter = expectations.iter(); let mut expected_words = expectation_iter.next().unwrap(); for ln_res in reader.lines() { let line = ln_res.unwrap(); if !(line.contains(expected_words.0) && line.contains(expected_words.1) && line.contains(expected_words.2)) { continue; } if let Some(w) = expectation_iter.next() { expected_words = w; continue; } pass = true; break; } assert!(pass); }
続いて、main関数のテストです。ここでは、panicが発生したときに、backtraceが出力されることをテストしています。
#[test] fn test_main() { const FIRECRACKER_INIT_TIMEOUT_MILLIS: u64 = 100; // There is no reason to run this test if the default socket path exists. assert!(!Path::new(DEFAULT_API_SOCK_PATH).exists()); // ログを保存するための一時ファイルを作成します。 let log_file_temp = NamedTempFile::new().expect("Failed to create temporary output logging file."); let metrics_file_temp = NamedTempFile::new().expect("Failed to create temporary metrics logging file."); let log_file = String::from(log_file_temp.path().to_path_buf().to_str().unwrap()); // Start Firecracker in a separate thread thread::spawn(|| { main(); }); // Wait around for a bit, so Firecracker has time to initialize and create the // API socket. thread::sleep(Duration::from_millis(FIRECRACKER_INIT_TIMEOUT_MILLIS)); // If Firecracker hasn't finished initializing yet, something is really wrong! assert!(Path::new(DEFAULT_API_SOCK_PATH).exists()); // init()のpipeにSomeを与えているので、指定したログファイルにログを出力します。 LOGGER .init( "TEST-ID", Some(log_file_temp.path().to_str().unwrap().to_string()), Some(metrics_file_temp.path().to_str().unwrap().to_string()), ).expect("Could not initialize logger."); // panicを起こします。panic発生時、main関数で設定したhook関数が呼ばれ、backtraceがログに出力されます。 let _ = panic::catch_unwind(|| { panic!("Oh, noes!"); }); // 期待するbacktraceがログファイルに出力されていることをテストします。 validate_backtrace( log_file.as_str(), &[ // Lines containing these words should have appeared in the log, in this order ("ERROR", "main.rs", "Panic occurred"), ("ERROR", "main.rs", "stack backtrace:"), ("0:", "0x", "backtrace::"), ], ); // Clean up fs::remove_file(DEFAULT_API_SOCK_PATH).expect("failure in removing socket file"); }
前回読んだmain関数の中で、panic発生時のhookを仕掛けていました。その内容は次のような処理でした。この処理が、想定通り動作していることをテストしています。
panic::set_hook(Box::new(move |info| { error!("Panic occurred: {:?}", info); METRICS.vmm.panic_count.inc(); let bt = Backtrace::new(); error!("{:?}", bt); // Log the metrics before aborting. if let Err(e) = LOGGER.log_metrics() { error!("Failed to log metrics on abort. {}:?", e); } }));
Vmm::new()
前回、main() -> start_vmm_thread() -> Vmm::new()
、と制御が遷移する処理を追いかけましたが、Vmm::new()には深入りしませんでした。そこで、Vmm::new()を見ていきます。
Vmm
は巨大な構造体です。Vmm::new()では、この巨大な構造体を初期化する処理を行います。その中には、VMの作成も含まれています。
vmm/src/lib.rs
struct Vmm { kvm: KvmContext, vm_config: VmConfig, shared_info: Arc<RwLock<InstanceInfo>>, // guest VM core resources guest_memory: Option<GuestMemory>, kernel_config: Option<KernelConfig>, kill_signaled: Option<Arc<AtomicBool>>, vcpu_handles: Option<Vec<thread::JoinHandle<()>>>, exit_evt: Option<EpollEvent<EventFd>>, vm: Vm, // guest VM devices mmio_device_manager: Option<MMIODeviceManager>, legacy_device_manager: LegacyDeviceManager, drive_handler_id_map: HashMap<String, usize>, // If there is a Root Block Device, this should be added as the first element of the list // This is necessary because we want the root to always be mounted on /dev/vda block_device_configs: BlockDeviceConfigs, network_interface_configs: NetworkInterfaceConfigs, #[cfg(feature = "vsock")] vsock_device_configs: VsockDeviceConfigs, epoll_context: EpollContext, // api resources api_event: EpollEvent<EventFd>, from_api: Receiver<Box<VmmAction>>, write_metrics_event: EpollEvent<TimerFd>, // The level of seccomp filtering used. Seccomp filters are loaded before executing guest code. // See `seccomp::SeccompLevel` for more information about seccomp levels. seccomp_level: u32, }
Vmmの初期化に必要な要素を準備しています。まずは、epollでファイルディスクリプタのイベント監視を追加しています。
vmm/src/lib.rs
impl Vmm { fn new( api_shared_info: Arc<RwLock<InstanceInfo>>, api_event_fd: EventFd, from_api: Receiver<Box<VmmAction>>, seccomp_level: u32, kvm_fd: Option<RawFd>, ) -> Result<Self> { // epollでイベント発生を監視する let mut epoll_context = EpollContext::new()?; // If this fails, it's fatal; using expect() to crash. let api_event = epoll_context .add_event(api_event_fd, EpollDispatch::VmmActionRequest) .expect("Cannot add API eventfd to epoll."); let write_metrics_event = epoll_context .add_event( // non-blocking & close on exec TimerFd::new_custom(ClockId::Monotonic, true, true).map_err(Error::TimerFd)?, EpollDispatch::WriteMetrics, ).expect("Cannot add write metrics TimerFd to epoll.");
api_event_fd
は、API Serverからのイベント通知のためのファイルディスクリプタです。監視するイベントは、EpollDispatch::VmmActionRequest
とあるので、API ServerからVMMへコマンドが送られるのだと推測できます。
main関数内で、API Serverのオブジェクトからファイルディスクリプタをcloneしていました。
src/main.rs main()
let api_event_fd = server .get_event_fd_clone() .expect("Cannot clone API eventFD.");
次にVMインスタンスを生成します。kvm_fd
は/dev/kvm
で良いはずです。
let block_device_configs = BlockDeviceConfigs::new(); let kvm = KvmContext::new(kvm_fd)?; let vm = Vm::new(kvm.fd()).map_err(Error::Vm)?;
Vm::new()
では、kvmのVMインスタンス作成をKVMに依頼します。とりあえず、ゲストOSのメモリはNone
で作成しています。
vmm/src/vstate.rs
impl Vm { /// Constructs a new `Vm` using the given `Kvm` instance. pub fn new(kvm: &Kvm) -> Result<Self> { //create fd for interacting with kvm-vm specific functions let vm_fd = kvm.create_vm().map_err(Error::VmFd)?; Ok(Vm { fd: vm_fd, guest_mem: None, }) }
Kvm.create_vm()
を見ていきます。
kvm/src/lib.rs
pub struct Kvm { kvm: File, } impl Kvm { ... /// Creates a VM fd using the KVM fd (KVM_CREATE_VM). /// A call to this function will also initialize the supported cpuid (KVM_GET_SUPPORTED_CPUID) /// and the size of the vcpu mmap area (KVM_GET_VCPU_MMAP_SIZE). pub fn create_vm(&self) -> Result<VmFd> { // Safe because we know kvm is a real kvm fd as this module is the only one that can make // Kvm objects. let ret = unsafe { ioctl(&self.kvm, KVM_CREATE_VM()) }; if ret >= 0 { // Safe because we verify the value of ret and we are the owners of the fd. let vm_file = unsafe { File::from_raw_fd(ret) }; let run_mmap_size = self.get_vcpu_mmap_size()?; let kvm_cpuid: CpuId = self.get_supported_cpuid(MAX_KVM_CPUID_ENTRIES)?; Ok(VmFd { vm: vm_file, supported_cpuid: kvm_cpuid, run_size: run_mmap_size, }) } else { errno_result() } }
注目するのはlet ret = unsafe { ioctl(&self.kvm, KVM_CREATE_VM()) };
の部分で、普通に/dev/kvmをioctlで叩いてVMを作成しています。
作成したVMのファイルでVmFd
を初期化し、関数の戻り値としています。
ここまでをまとめると、Vmm::new()では、API Serverからイベント通知を受け取れるためのデータと、/dev/kvmを叩いて生成したVMインスタンスから、Vmmオブジェクトを作成しようとしています。 最終的に、Vmmは次のように初期値を与えられて作成されます。
vmm/src/lib.rs Vmm::new()
Ok(Vmm { kvm, vm_config: VmConfig::default(), shared_info: api_shared_info, guest_memory: None, kernel_config: None, kill_signaled: None, vcpu_handles: None, exit_evt: None, vm, mmio_device_manager: None, legacy_device_manager: LegacyDeviceManager::new().map_err(Error::CreateLegacyDevice)?, block_device_configs, drive_handler_id_map: HashMap::new(), network_interface_configs: NetworkInterfaceConfigs::new(), #[cfg(feature = "vsock")] vsock_device_configs: VsockDeviceConfigs::new(), epoll_context, api_event, from_api, write_metrics_event, seccomp_level, })
少し拾っておくと、vm_config
はデフォルト値が設定されています。デフォルト値は次の通りです。
vmm/src/vmm_config/machine_config.rs
impl Default for VmConfig { fn default() -> Self { VmConfig { vcpu_count: Some(1), mem_size_mib: Some(128), ht_enabled: Some(false), cpu_template: None, } } }
CPU1コア、メモリサイズ128MB、(多分)hyperthreadingをdisable、がデフォルト設定になります。
もう1点、legacy_device_manager
なるメンバーがいます。LegacyDeviceManagerは次のように定義されており、IOバス上のUARTとi8042(PS/2コントローラでしたっけ?)を管理しているようです。
vmm/src/device_manager/legacy.rs
/// The `LegacyDeviceManager` is a wrapper that is used for registering legacy devices /// on an I/O Bus. It currently manages the uart and i8042 devices. /// The `LegacyDeviceManger` should be initialized only by using the constructor. pub struct LegacyDeviceManager { pub io_bus: devices::Bus, pub stdio_serial: Arc<Mutex<devices::legacy::Serial>>, pub i8042: Arc<Mutex<devices::legacy::I8042Device>>, pub com_evt_1_3: EventFd, pub com_evt_2_4: EventFd, pub stdin_handle: io::Stdin, }