Rustで書かれたVMM firecrackerを読もう!(3)~mainのテストとVmm::new()~

github.com

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()では、kvmVMインスタンス作成を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,
}

次回は、API Serverの初期化か、API Serverから、どのようにVmmが叩かれるか見ていきたいです。