Rustで書かれたVMM firecrackerを読もう!(2)~API ServerとVMMの初期化~

github.com

tomo-wait-for-it-yuki.hatenablog.com

前回は、main関数でLOGGERのオブジェクトを作るところまで見ました。

設計を知る

少し公式ドキュメントを読んで、知識を仕入れました。 firecrackerは、KVMを使ってVMを作成します。VMMにつき1つのゲストOSだけを持つタイプのVMMのようです。つまり、複数のゲストを立ち上げる場合には、個別にVMMごと起動する、という方式です。Noahがそんな感じなんでしたっけ?

ただし、firecrackerのバイナリには、リソースを隔離するための仕組みはないようです。jailerがcgroupsなどを使って、リソース隔離する責務を負っています。そのため、firecracker単体でも起動はできますが、基本は、jailerで隔離した状態でfirecrackerを立ち上げる、というのが想定する使い方のようです。

1つのインスタンスは、3つのスレッドから構成されます。API Server, VMM, vCPU, です。CPUの設定を複数個にすれば、vCPUのスレッドを複数個持つことができます。 ということで、API ServerがVMMのコマンドを受け付けて、VMMがKVMを制御し、KVMから上がってきたイベントをVMMのハンドラが処理する、という流れで作られていそうです。

ちらっと覗いたとき、deviceは、ネットワークデバイス、ブロックデバイス、シリアル、くらいしかなかったです。

では、昨日の続きを見ていきます。

src/main.rs

LOGGERを作った後、initで初期化しています。この時の引数は、instance id, log_pipe, metrics_pipeです。

fn main() {
    LOGGER
        .init(&"", None, None)
        .expect("Failed to register logger");

instance idは置いておいて、2つのpipeはログをどこに出力するか、を切り替えるのに利用します。Noneの場合は、stdout/stderrに出力されます。Someでファイルパスを渡すと、そのファイルにログを残すようにします。 これは後ほど、テストで出るため、覚えておきましょう!

次に、SIGSYSのsignal handlerを登録しています。中身を追ってみると、本当にSIGSYSのhandlerを登録しているだけでした。

    // If the signal handler can't be set, it's OK to panic.
    seccomp::setup_sigsys_handler().expect("Failed to register signal handler");

次は、panic発生時のフックを登録しています。abortで死ぬ前に、panic発生時のbacktraceがログに残るようになっています。

    // Start firecracker by setting up a panic hook, which will be called before
    // terminating as we're building with panic = "abort".
    // It's worth noting that the abort is caused by sending a SIG_ABORT signal to the process.
    panic::set_hook(Box::new(move |info| {
        // We're currently using the closure parameter, which is a &PanicInfo, for printing the
        // origin of the panic, including the payload passed to panic! and the source code location
        // from which the panic originated.
        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);
        }
    }));

このあたりはベアメタルプログラミングで良く書くのでおなじみですね。 panic発生時に渡されるPanicInfoは、プログラムの何行目でpanicになったか、といった情報を含んでいます。backtrace crateのBacktraceインスタンスを作ると、debug出力でbacktraceが取れるみたいです。面白いですね。これは今度詳しく見てみたいです。

ここからは、コマンドライン引数の処理をしています。特に面白みはないので、少し飛ばします。

    let cmd_arguments = App::new("firecracker")
        .version(crate_version!())
        .author(crate_authors!())
        .about("Launch a microvm.")
        .arg(
            Arg::with_name("api_sock")
                .long("api-sock")
                .help("Path to unix domain socket used by the API")
                .default_value(DEFAULT_API_SOCK_PATH)
                .takes_value(true),
        ).arg(
            Arg::with_name("context")
                .long("context")
                .help("Additional parameters sent to Firecracker.")
                .takes_value(true),
        ).get_matches();
...

ここから、API ServerとVMMを作る準備が始まります。まずは、共有リソースを作って、API Serverを作っています。

    let shared_info = Arc::new(RwLock::new(InstanceInfo {
        state: InstanceState::Uninitialized,
        id: instance_id,
    }));
    let mmds_info = MMDS.clone();
    let (to_vmm, from_api) = channel();
    let server = ApiServer::new(mmds_info, shared_info.clone(), to_vmm).unwrap();

shared_infoは、API ServerとVMMとの間で共有される情報のようです。 MMDSは、microVM metadata serviceだそうです。今のところ、意味するところはよくわかりません。追々判明するでしょう。

channelで非同期のSenderとReceiverを作成しています。API ServerとVMMを繋ぐチャネルですね。

ここまでで作成した、VMMとの情報共有リソースを渡して、ApiServerを初期化しています。

次は、VMMです。API Serverからイベントを受ける取るために使うであろうファイルディスクリプタと、jailerで起動している場合に受け取れるらしいKVMのファイルディスクリプタを取得しています。

    let api_event_fd = server
        .get_event_fd_clone()
        .expect("Cannot clone API eventFD.");

    let kvm_fd = if is_jailed {
        Some(jailer::KVM_FD)
    } else {
        None
    };

    let _vmm_thread_handle =
        vmm::start_vmm_thread(shared_info, api_event_fd, from_api, seccomp_level, kvm_fd);

vmm/src/lib.rsでは、Vmmのオブジェクトを作成して、threadを生成します。

pub fn start_vmm_thread(
...
) -> thread::JoinHandle<()> {
    thread::Builder::new()
        .name("fc_vmm".to_string())
        .spawn(move || {
            // If this fails, consider it fatal. Use expect().
            let mut vmm = Vmm::new(
...
            ).expect("Cannot create VMM.");
            match vmm.run_control() {
                Ok(()) => vmm.stop(0),
                Err(_) => vmm.stop(1),
            }
        }).expect("VMM thread spawn failed.")
}

jailerから起動していない場合は、Vmm::new()を辿っていくと、自前で/dev/kvmをたたいてKVMオブジェクトを作成しています。

// A wrapper around opening and using `/dev/kvm`.
///
/// The handle is used to issue system iocts.
pub struct Kvm {
    kvm: File,
}

impl Kvm {
    /// Opens `/dev/kvm/` and returns a `Kvm` object on success.
    pub fn new() -> Result<Self> {
        // Safe because we give a constant nul-terminated string and verify the result.
        let ret = unsafe { open("/dev/kvm\0".as_ptr() as *const c_char, O_RDWR) };
        if ret < 0 {
            return errno_result();
        }

        // Safe because we verify that ret is valid and we own the fd.
        Ok(unsafe { Self::new_with_fd_number(ret) })
    }

ちょっとVMMの初期化は少し長くなりそうなので、後日にします。

とうとう初期化大詰めです。API Serverのsocketをbindして、API Serverを起動します。ここでも、jailerからの起動時は、ファイルディスクリプタを受け取るようです。そうでない場合、パスからsocketを取得します。

    let uds_path_or_fd = if is_jailed {
        UnixDomainSocket::Fd(jailer::LISTENER_FD)
    } else {
        UnixDomainSocket::Path(bind_path)
    };

    match server.bind_and_run(uds_path_or_fd, start_time_us, start_time_cpu_us) {
        Ok(_) => (),
        Err(Error::Io(inner)) => match inner.kind() {
            // エラー処理
        },
        Err(Error::Eventfd(inner)) => panic!(
            // エラー処理
        ),
    }

bind_and_runはエラーが発生するまで実行を続ける関数です。Coreはtokioのイベントループみたいですね。

    // TODO: does tokio_uds also support abstract domain sockets?
    pub fn bind_and_run<P: AsRef<Path>>(
...
    ) -> Result<()> {
        let mut core = Core::new().map_err(Error::Io)?;
        let handle = Rc::new(core.handle());
...
        // This runs forever, unless an error is returned somewhere within f (but nothing happens
        // for errors which might arise inside the connections we spawn from f, unless we explicitly
        // do something in their future chain). When this returns, ongoing connections will be
        // interrupted, and other futures will not complete, as the event loop stops working.
        core.run(f)
    }

ちょっと非同期周りは弱点なので、日を改めて出直します。 次回は、Vmm::new()かtokioのCore::new()あたりを見ていきます。