rustのArcについてその2

前回

以下で利用したplaygroundのリンク

スライスからのArcの作成

Arcのソースをみていて気づきましたが,impl<T: Clone> From<&[T]> for Arc<[T]>などが実装されており,CopyあるいはCloneが実装されているスライスからArcを作成することができます.(RCも同様です)

例えばこれを使うと,Arc<str> (=Arc<[u8]>)が作れます.

Arc<[T]>はfat pointerとなり,構造的には以下の様になります.

f:id:mm_i:20190214233307p:plain
Arc<T>

x86_64環境なら最初の8byteがデータへのポインタ,そのあと8byteがデータのサイズです.

作成例

let s: Arc<str> = Arc::from("str");
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[64, 10, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[115, 116, 114]

Arc<String>だと文字列にアクセスするまで二回ポインタを辿らないといけませんが(前回の記事参照),これなら一回で済みます.

BoxからのArcの作成

impl<T: ?Sized> From<Box<T>> for Arc<T>も実装されています. 内部動作としてはBoxの中身をArc側にmemcpyして,box側のメモリを解放することになります.

こちらの場合,CopyあるいはCloneが実装されていなくてもArcが作成できます.T: ?SizedなのでArc<[T]>が作れます.

struct A(i32);
let s: Box<[A]> = Box::new([A(0),A(1),A(2)]);
let s: Arc<[A]> = Arc::from(s);
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[224, 11, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0]

まぁこの場合そもそも普通に Arc::new([A(0), A(1), A(2)])すればいいんですが.

Arc<str>Stringから作成したい場合は,String::into_boxed_str()を使ってBox<str>にしてからArcにできます.

let s: Arc<str> = Arc::from(format!("str").into_boxed_str());
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[192, 11, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[115, 116, 114]

VecからのArcの作成

impl<T> From<Vec<T>> for Arc<[T]>もあります.

let s: Arc<[i32]> = Arc::from(vec![0,1,2]);
println!("{:?}", as_raw_bytes(&s));
println!("{:?}", as_raw_bytes(&*s));
[16, 12, 247, 79, 223, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0]

こちらも動作的にはArcが新規に割り当てたメモリ領域にVecのデータを全部移すことになります.

servo_arc

servoにはservo_arcという,Arcの派生実装が含まれています.

以下の様な特徴があります.

  • weakカウントがなくてstrongカウントのみ
  • dynamically-sized type (DST)のサポート

DSTに関しては以前記事を書きましたservo_arcを使うと,以下の様なことができます.

#[derive(Debug)]
struct Header {
    a: u32,
    b: u32,
    c: u32,
};
let header = Header { a: 1, b: 2, c: 3 };
let data: Vec<u32> = vec![4, 5, 6];
let a = servo_arc::Arc::from_header_and_iter(header, data.into_iter());

println!("{:?}", &a);
println!("{:?}", as_raw_bytes(&a));
println!("{:?}", as_raw_bytes(&*a));
HeaderSlice { header: Header { a: 1, b: 2, c: 3 }, slice: [4, 5, 6] }
[240, 44, 64, 90, 168, 127, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0]

Arc::from_header_and_iter()Arc<HeaderSlice<H, [T]>>を作成します.HeaderSliceは以下の様に定義されます.

pub struct HeaderSlice<H, T: ?Sized> {
    /// The fixed-sized data.
    pub header: H,

    /// The dynamically-sized data.
    pub slice: T,
}

Arc<HeaderSlice<H, [T]>>は以下の様な構造になります.

f:id:mm_i:20190214234050p:plain
Arc<HeaderSlice<H, [T]>>

これの何が嬉しいのかというと,zero-length arrayを受け取るCの関数をFFIで呼ぶ時なんかに便利かなと思います.servoではどう使われてるんでしょう(未確認)

rustのMutexの内部構造

前回に引き続き,今度はMutexの話です.

Raph Levien氏の図を引用すると,Mutexは以下のような構造になっています.

f:id:mm_i:20190213182929p:plain
Raph Levien, Copyright 2017 Google Inc., released under Creative Commons BY

また,データ構造としては以下のように定義されています(一部抜粋).

pub struct Mutex<T: ?Sized> {
    inner: Box<sys::Mutex>,
    poison: poison::Flag,
    data: UnsafeCell<T>,
}

ここから,以下のことが分かります.

  1. Mutex自体はデータをヒープに置かない
  2. mutex本体(sys::Mutex)は,ヒープに置かれる

sys::Mutexは,*nix環境ではpthread_mutex_tです. したがって,Mutexのlock/unlockはpthread_mutex_lock()およびpthread_mutex_unlock()に対応しています. mutex本体をヒープに置くのは,OSによっては(pthreadが?)mutex本体のアドレスが変わらないことを前提としているからだそうです(このあたりの実装は未確認). ヒープに置いておかないと,Mutexをmoveしたときmutex本体のアドレスが変わってしまいます.

ちなみに,RwLockMutexとほぼ同じ構造をしています.

https://github.com/rust-lang/rust/blob/1.32.0/src/libstd/sync/rwlock.rs

pub struct RwLock<T: ?Sized> {
    inner: Box<sys::RWLock>,
    poison: poison::Flag,
    data: UnsafeCell<T>,
}

Mutexを利用するときはマルチスレッドで何かしたい場合なので,実際に利用する場合は,Arcと組み合わせてArc<Mutex<T>>のような形で利用されることが多いと思います.

parking_lot

さて,上のMutexの図には"parking_lotを使ってみたら?"と書いてあります. parking_lotはスピンロックを使ったmutexを提供します.

以下のような感じでMutexが定義されます(一部抜粋).

// lock_api/src/mutex.rc
pub struct Mutex<R: RawMutex, T: ?Sized> {
    raw: R,
    data: UnsafeCell<T>,
}

// src/mutex.rc
type Mutex<T> = lock_api::Mutex<RawMutex, T>;

// src/raw_mutex.rs
pub struct RawMutex {
    state: AtomicU8,
}

unsafe impl RawMutexTrait for RawMutex {
    ...
    #[inline]
    fn lock(&self) {
        if self
            .state
            .compare_exchange_weak(0, LOCKED_BIT, Ordering::Acquire, Ordering::Relaxed)
            .is_err()
        {
            self.lock_slow(None);
        }
        unsafe { deadlock::acquire_resource(self as *const _ as usize) };
    }
    ...
}

stdのMutexと比較すると,以下の特徴があります.

  • pthreadではなくて(adaptive)スピンロックを利用する
  • mutex本体をヒープに割り当てる必要がない
  • poisonフィールドもない
    • このフィールドは,ロックを保持したスレッドがpanicしたかどうか検出するためのもの
    • parking_lotの場合,panicしたらlockは解放されるようになっている

さて,こんなparking_lotですが,ちょうど今parking_lotをlibstdに取り込もうという動きがあるようです.

深く議論は追っていませんが,しばらくするとstdに追加されてるかもしれません(というか,OS nativeなmutexの代わりにバックエンドとして利用されることになりそう?).

rustのArcの内部構造

rustのスマートポインタの構成については,Raph Levien氏の以下の図が参考になります.

今回はここで特にArcについて見ていきたいと思います.

Arcは以下のように定義されています(一部抜粋).

pub struct Arc<T: ?Sized> {
    ptr: NonNull<ArcInner<T>>,
    phantom: PhantomData<T>,
}

struct ArcInner<T: ?Sized> {
    strong: atomic::AtomicUsize,
    weak: atomic::AtomicUsize,
    data: T,
}

impl<T: ?Sized> Deref for Arc<T> {
    type Target = T;

    #[inline]
    fn deref(&self) -> &T {
        &self.inner().data
    }
}

具体例

具体例としてArc<String>で確認してみます.

Raph Levien氏の図を参考に書くと,Arc<String>の構造は以下のようになります.ちなみにString自体はVec<u8>です

f:id:mm_i:20190212220649p:plain
Arc<String>

実際にメモリ上の表現を確認してみます(playgroundへのリンク).

as_raw_bytes()で与えられたリファレンスを[u8]のスライスとして解釈してprintします.

use std::sync::Arc;

fn as_raw_bytes<'a, T: ?Sized>(x: &'a T) -> &'a [u8] {
    unsafe { std::slice::from_raw_parts(x as *const T as *const u8, std::mem::size_of_val(x)) }
}

fn main() {
    let s: Arc<String> = Arc::new(format!("str"));

    println!("[1] {:?}", as_raw_bytes(&s));
    println!("[2] {:?}", as_raw_bytes(&*s));
    println!("[3] {:?}", as_raw_bytes(&**s));
}

実行結果

[1] [96, 90, 157, 11, 128, 85, 0, 0]
[2] [64, 90, 157, 11, 128, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
[3] [115, 116, 114]

[1]がArcが保持するArcInnerへのポインタのアドレスです.

[2]は,ArcInnerの中身...ではなくてArcのDerefによって,ArcInnerのdata部分が表示されています. これは今回の場合StringすなわちVec<u8>です.Vec<u8>は最初に要素先頭へのポインタ,その次が要素の長さ(len),最後が容量(cap)です.

[3]はStringが保持する文字列("str")です.

さて,ArcInnderのstrong countおよびweak countを確認するにはどうしたら良いでしょうか. ArcInnerへのリファレンスが作れれば良いのですが,ArcInner自体はpublicでないのでそれはできません.

そこで必殺技としてtransmute_copyを使って無理やりArcのポインタをu8の配列に変換させて表示させてみます.

{
    let s = s.clone();
    let arc_inner = unsafe {
        std::mem::transmute_copy::<
            Arc<String>,
            &[u8; std::mem::size_of::<atomic::AtomicUsize>() * 2
                 + std::mem::size_of::<String>()],
        >(&s)
    };
    println!("{:?}", &arc_inner[..]);
    println!("strong={}, weak={}", Arc::strong_count(&s), Arc::weak_count(&s));
}
[2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 64, 90, 157, 11, 128, 85, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0]
strong=2, weak=0

これでstrong countおよびweak countが確認できました. (ArcInnerのweak countは1ですが,Arc::weak_count()はweak countから1引いた数を返すのでこれで合っています)

ちなみにtransmute_copyではなくtransumuteを使うと,Arcのデストラクタが呼ばれなくなるのでメモリリークします.

into_rawfrom_raw

Arcにはinto_raw()from_raw()というメソッドがあります.

Arc::into_raw()はArcが保持するTを指すraw pointerを返します. また,Arc::from_raw()はraw pointerからArcを作成します.

ここで,Arc::from_raw()で渡すraw pointerは,Arc::into_raw()で得られたものでなければならないとドキュメントに書いてあります. というのもArc::from_raw()は与えられたraw pointerからstrong countとweak countのサイズ分だけのオフセットを引いたアドレスを使ってArcを作成するからです.

https://github.com/rust-lang/rust/blob/1.32.0/src/liballoc/sync.rs#L407

    pub unsafe fn from_raw(ptr: *const T) -> Self {
        // Align the unsized value to the end of the ArcInner.
        // Because it is ?Sized, it will always be the last field in memory.
        let align = align_of_val(&*ptr);
        let layout = Layout::new::<ArcInner<()>>();
        let offset = (layout.size() + layout.padding_needed_for(align)) as isize;

        // Reverse the offset to find the original ArcInner.
        let fake_ptr = ptr as *mut ArcInner<T>;
        let arc_ptr = set_data_ptr(fake_ptr, (ptr as *mut u8).offset(-offset));

        Arc {
            ptr: NonNull::new_unchecked(arc_ptr),
            phantom: PhantomData,
        }
    }

逆に言えばこういうデータ構造を最初にメモリに作成すればArc::from_raw()からArcが作成できるわけですね. おそらくバージョン・環境依存の動作だと思いますし,需要は謎ですが...


一年以上振りにrustを触っていますが,スマートポインタの内部構造で少し混乱したので書いてみました.この辺り,コンパイラがderef coercionやauto dereferenceなど勝手にやってくれる分,少々分かりにくい気がします. といってもrustでは基本的にポインタ演算はできませんし,コンパイラに型チェックまかしておけば困ることはほぼないのですが. よく設計されているなとつくずく思います.

QEMU/KVM上のゲストのハイパーコールをQEMU側に渡す方法

前回説明したように,KVMでは,ゲストのハイパーコール(Intel CPUの場合はVMCALL命令,AMDの場合はVMMCALL命令)はKVM側で処理され,ioctl側に戻ることなくゲストに戻ります.

QEMU/KVMにおいて,独自にハイパーコールを追加してQEMU側で処理するには修正が必要です.

対象環境

方法

KVM

diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index fbda5a917c5b..fba797631a3f 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -78,6 +78,7 @@
 #define KVM_REQ_HV_STIMER              KVM_ARCH_REQ(22)
 #define KVM_REQ_LOAD_EOI_EXITMAP       KVM_ARCH_REQ(23)
 #define KVM_REQ_GET_VMCS12_PAGES       KVM_ARCH_REQ(24)
+#define KVM_REQ_HYPERCALL              KVM_ARCH_REQ(25)
 
 #define CR0_RESERVED_BITS                                               \
        (~(unsigned long)(X86_CR0_PE | X86_CR0_MP | X86_CR0_EM | X86_CR0_TS \
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index f049ecfac7bb..2accaba0e75a 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -7002,6 +7002,10 @@ int kvm_emulate_hypercall(struct kvm_vcpu *vcpu)
                ret = kvm_pv_send_ipi(vcpu->kvm, a0, a1, a2, a3, op_64_bit);
                break;
 #endif
+       case KVM_HC_MY_HYPERCALL:
+               kvm_make_request(KVM_REQ_HYPERCALL, vcpu);
+               ret = 0;
+               break;
        default:
                ret = -KVM_ENOSYS;
                break;
@@ -7624,6 +7628,11 @@ static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
                        r = 0;
                        goto out;
                }
+               if (kvm_check_request(KVM_REQ_HYPERCALL, vcpu)) {
+                       vcpu->run->exit_reason = KVM_EXIT_MY_HYPERCALL;
+                       r = 0;
+                       goto out;
+               }
 
                /*
                 * KVM_REQ_HV_STIMER has to be processed after
diff --git a/include/uapi/linux/kvm.h b/include/uapi/linux/kvm.h
index 2b7a652c9fa4..1c2707d5b70c 100644
--- a/include/uapi/linux/kvm.h
+++ b/include/uapi/linux/kvm.h
@@ -235,6 +235,7 @@ struct kvm_hyperv_exit {
 #define KVM_EXIT_S390_STSI        25
 #define KVM_EXIT_IOAPIC_EOI       26
 #define KVM_EXIT_HYPERV           27
+#define KVM_EXIT_MY_HYPERCALL     28
 
 /* For KVM_EXIT_INTERNAL_ERROR */
 /* Emulate instruction failed. */
diff --git a/include/uapi/linux/kvm_para.h b/include/uapi/linux/kvm_para.h
index 6c0ce49931e5..a6a0417ee029 100644
--- a/include/uapi/linux/kvm_para.h
+++ b/include/uapi/linux/kvm_para.h
@@ -28,6 +28,7 @@
 #define KVM_HC_MIPS_CONSOLE_OUTPUT     8
 #define KVM_HC_CLOCK_PAIRING           9
 #define KVM_HC_SEND_IPI                10
+#define KVM_HC_MY_HYPERCALL            11
 
 /*
  * hypercalls use architecture specific

QEMU

diff --git a/linux-headers/linux/kvm.h b/linux-headers/linux/kvm.h
index f11a7eb49c..0b44980eda 100644
--- a/linux-headers/linux/kvm.h
+++ b/linux-headers/linux/kvm.h
@@ -235,6 +235,7 @@ struct kvm_hyperv_exit {
 #define KVM_EXIT_S390_STSI        25
 #define KVM_EXIT_IOAPIC_EOI       26
 #define KVM_EXIT_HYPERV           27
+#define KVM_EXIT_MY_HYPERCALL     28
 
 /* For KVM_EXIT_INTERNAL_ERROR */
 /* Emulate instruction failed. */
diff --git a/target/i386/kvm.c b/target/i386/kvm.c
index b2401d13ea..685c3ac58e 100644
--- a/target/i386/kvm.c
+++ b/target/i386/kvm.c
@@ -3622,6 +3622,21 @@ int kvm_arch_handle_exit(CPUState *cs, struct kvm_run *run)
         ioapic_eoi_broadcast(run->eoi.vector);
         ret = 0;
         break;
+    case KVM_EXIT_MY_HYPERCALL:
+        ret = kvm_getput_regs(cpu, 0);
+        if (ret < 0) {
+            return ret;
+        }
+        fprintf(stderr, "KVM: KVM_EXIT_MY_HYPERCALL %d, %ld, %ld, %ld, %ld, %ld\n",
+                                                    run->exit_reason,
+                                                    cpu->env.regs[R_EAX],
+                                                    cpu->env.regs[R_EBX],
+                                                    cpu->env.regs[R_ECX],
+                                                    cpu->env.regs[R_EDX],
+                                                    cpu->env.regs[R_ESI]);
+        cpu->env.regs[R_EAX] = 0;
+        ret = kvm_getput_regs(cpu, 1);
+        break;
     default:
         fprintf(stderr, "KVM: unknown exit reason %d\n", run->exit_reason);
         ret = -1;

解説

QEMU側からkvm_vcpu_ioctl(cpu, KVM_RUN, 0)すると,いろいろあってKVMvcpu_enter_guest()にきます. この中のkvm_x86_ops->run(vcpu)でVMENTRY, kvm_x86_ops->handle_exit(vcpu)でVMEIXT後の処理がおこなわれます. ちなみにVMENTRYやVMEXITが関数ポインタになっているのはIntelAMD双方に対応するためです.

VMCALL命令でVMEXITした場合は,handle_vmcall()からkvm_emulate_hypercall()が呼ばれ,ここでハイパーコールが処理されます. vcpu_enter_guest()のコメントに書いてある通り,kvm_emulate_hypercall()の戻り値で1以外の値を返せばioctl側に制御が戻ります.そうしてもいいのですが,するとQEMU側で命令エミュレーション(RIPを進める処理)をおこなう必要があります.

今回はその代わりにKVMVCPU requestという機能を使っています. これはもともとはカーネルのスレッドがvCPU側に何かリクエストをするために使用する機能のようです. ハイパーコール処理の中で,自分が定義したハイパーコール番号だった場合,kvm_make_request()でリクエストの登録し,あとはこれまで通り命令のエミュレーション自体はKVMにまかせます.このままだとまたKVMはゲストにVMENTRYしますが,vcpu_enter_guest()の先頭の方でkvm_check_request()し,もし対応するリクエストが存在したらexit_reasonをその旨に書き換えてユーザランド側に戻るようにしています.

QEMUはioctlから戻ってきたら,kvm_arch_handle_exit() (accel/kvm/kvm-all.c => target/i386/kvm.c) でVMEXIT後の処理をするので,ここにハイパーコール用の処理を追加しています. QEMUからはkvm_getput_regs()を使ってゲストのレジスタを取得することが可能です.

QEMUコードリーディングメモ

主にイベントループとKVM周り.

QEMU Main Loop

  • QEMUの主処理はGLibを使ったイベントループ.
  • ゲストのvCPUは別のスレッドで実行(後述)
vl.c:main() =>
  vl.c:main_loop() =>
    util/main-loop.c:main_loop_wait() =>
      loop {
        slirp_pollfds_fill();
        os_host_main_loop_wait() =>
          g_main_context_acquire(context)
          glib_pollfds_fill() =>
            g_main_context_prepare()
            g_main_context_query()

          qemu_mutex_unlock_iothread()
          qemu_poll_ns() =>
            g_poll()
          qemu_mutex_lock_iothread()

          glib_pollfds_poll() =>
            g_main_cotext_disapatch()
          g_main_context_release(context)
        slirp_pollfds_poll()
      }
  • QEMUはiothreadに関してlockを持っている
    • イベントのpoll中はこのiothreadのlockを外す
  • イベントループ処理の流れを理解するにはGLibの動作を知る必要がある.

GLibメモ

  • GSource

    • http://maemo.org/api_refs/4.0/glib/glib-The-Main-Event-Loop.html#GSource
    • GLibにおけるイベントを表現
      • file descriptors
        • file
        • pipe
        • sockets
        • ...
      • timeout event
    • sourceの主要な関数
      • prepare()
        • poll()の前に呼ばれる
        • sourceが(pollすることなく)準備完了であればtrueを返す
            • timeout eventでtimeoutしてたらtrue
            • file descriptorの場合は,通常はpollするのでfalse
      • check()
        • poll()後に呼ばれる
        • dispatchすることができるならtrueを返す
      • dispatch()
        • イベントに対応付けられたcallback関数を実行
    • 各sourceにはpriorityが存在
  • 基本的なイベント処理の流れ

    • g_source_attach()でcontextに対してsourceを追加
    • メインループ
      • g_main_context_prepare(context, &max_priority)
        • コンテキストが持つ各GSourceのprepare()を呼ぶ
        • ここで,max_priorityにprepare()がtrueの中から最大のpriorityが設定される.
      • g_main_context_query(context, max_priority, ..., fds)
        • max_priorityを持つsourceのfile descriptorを取得
        • この関数によって実際にpoll()するfdを選択する
      • g_poll()
        • poll
      • g_main_context_check()
        • dispatch可能なfdをチェック
      • g_main_cotext_disapatch()
        • コールバック関数を呼ぶ

I/O handler

  • 仮想デバイスを処理するためのハンドラ (GSource)
  • 初期化
    • util/main-loop.c:qemu_init_main_loop()
      • iohandler_init() => util/async.c:aio_context_new()
    • g_source_attach()でiohandlerをdefault contextのsourceに追加
  • コールバック関数の登録
    • util/iohandler.c:qemu_set_fd_handler()

KVM 初期化

  • 全体の初期化
    • accel/kvm/kvm_all.c:kvm_init()
vl.c:main() =>
  accel/accel.c:configure_accelerator() =>
    accel_init_machine() =>
       acc->init_machine() = accel/kvm/kvm_all.c:kvm_init()
  • マシン初期化時に,vCPUごとにスレッドが作成される.
target/i386/cpu.c:x86_cpu_realizefn() =>
    cpus.c:qemu_init_vcpu() =>
        qemu_kvm_start_vcpu()
            qemu_kvm_start_vcpu() =>
                qemu_thread_create(..., qemu_kvm_cpu_thread_fn, ...)

KVM vCPU event loop

cpus.c:qemu_kvm_cpu_thread_fn =>
  qemu_mutex_lock_iothread()
  loop {
      accel/kvm/kvm-all.c:kvm_cpu_exec() =>
         qemu_mutex_unlock_iothread()
         do {
            kvm_vmrun_ioctl(cpu, KVM_RUN, 0)
            ret = handle_vmexit()
         }while(ret == 0);
         qemu_mutex_lock_iothread()
      qemu_wait_io_event()
  }
  qemu_mutex_unlock_iothread()
  • 各vCPUはiothreadのロックを持つ
  • KVM_RUN実行中はiothreadのロックを外す

KVMRUN

  • ※ これはLinux側の処理
virt/kvm/kvm_main.c:kvm_vcpu_ioctl() =>
  arch/x86/kvm/x86.c:kvm_arch_vcpu_ioctl_run() =>
    vcpu_run() =>
      loop {
         ret = {
            vcpu_enter_guest() =>
              kvm_x86_ops->run(vcpu) = arch/x86/kvm/vmx.c:vmx_vcpu_run() =>
                VMLAUNCH
            kvm_x86_ops->handle_exit(vcpu) = arch/x86/kvm/vmx.c:vmx_handle_exit() =>
              kvm_vmx_exit_handlers[exit_reason](vcpu)
         }
         if (ret >= 0) break
      }
  • VMCALLの処理
arch/x86/kvm/vmx.c:handle_vmcall() =>
  arch/x86/kvm/x86.c:kvm_emulate_hypercall() =>
    handle_vm_call or kvm_skip_emulated_instruction()
  • VMCALLはKVM内で処理され,ioctl側に制御が戻らない
  • VMCALLでioctl側に制御を渡すには,KVMの修正が必要

続く?

Bareflankを使ってみる

前から気になっていたBareflankを少し触って見ました.

あまりドキュメントがないようなので半分メモがてらBareflankについて書いてみます.


BareflankはいわゆるThin-hypervisorの一種です. 複数VMの動作を目的とする通常のハイパーバイザとは異なり,そのようなハイパーバイザは基本的に一つのゲストOSを実行し,ゲストOSのフックや解析をおこないます. セキュリティや研究目的で用いられることが多いです. そのようなハイパーバイザは(特に最近)結構あって,

などがあります.これらと比較してBareflankは"hypervisor Software Development Toolkit"を謳っており,特徴として

  • Modern C++ (C++17)で実装 (C++ STLのサポート)
  • モジュール構成による高い拡張性
  • Windows, Linuxのサポート.さらにUEFIブート可能

が挙げられます.

とりあえず動かす

公式のCompilation Instructionsを参考にすれば動くと思います. ただ最近も良く開発されており,masterブランチはunstableだったりすることがあるので場合によっては適当なタグをチェックアウトした方がいいかもしれません.

以下,Linuxで動かしたときの実行結果です.

% git clone https://github.com/bareflank/hypervisor.git
% cd hypervisor
% mkdir build; cd build
% cmake ..
% make -j8
% make driver_quick
% make quick
% make status
vmm running
Built target status
% make dump
Scanning dependencies of target dump
[0] DEBUG: host os is now in a vm
[1] DEBUG: host os is now in a vm
[2] DEBUG: host os is now in a vm
[3] DEBUG: host os is now in a vm

Built target dump
% dmesg
...
[345609.788134] [BAREFLANK DEBUG]: dev_init succeeded
[345613.817106] [BAREFLANK DEBUG]: dev_open succeeded
[345613.822454] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822471] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822478] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822480] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822491] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822495] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822579] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822612] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.822632] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.822637] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823104] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823214] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823324] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823359] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823526] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823573] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823583] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823590] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823598] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823600] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823669] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823713] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823731] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823745] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.823755] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE_LENGTH: succeeded
[345613.823762] [BAREFLANK DEBUG]: IOCTL_ADD_MODULE: succeeded
[345613.872731] [BAREFLANK DEBUG]: IOCTL_LOAD_VMM: succeeded
[345613.872741] [BAREFLANK DEBUG]: dev_release succeeded
[345613.876854] [BAREFLANK DEBUG]: dev_open succeeded
[345613.911072] [BAREFLANK DEBUG]: IOCTL_START_VMM: succeeded
[345613.911077] [BAREFLANK DEBUG]: dev_release succeeded
[345616.927978] [BAREFLANK DEBUG]: dev_open succeeded
[345616.932016] [BAREFLANK DEBUG]: dev_release succeeded

構成

Linuxで使用する場合について簡単に見てみます.

前述の通りBareflankはC++で開発されています. BareflankはOSの機能が使えないベアメタル上で動作するため,そのためのC++ランタイムが実装されています.

Bareflankのロード

Bareflankはカーネルモジュールをロードすることで起動します(カーネルモジュール自体は当然ながらCで書かれています). このときBareflank本体はelfバイナリとしてコンパイルされており,カーネルモジュールはそのelfバイナリを動的にロードして呼び出すということをおこなっています. このときの_start_func()(Bareflankのエントリポイント)はbfvmm/src/entry/entry.cpp:bfmain()になります.

仮想化の流れ

カーネルモジュールからBF_REQUEST_VMM_INITを受け取ったBareflankは自身を仮想化します.

処理の流れは高度に抽象化されていて分かりにくいですが,private_init_vmm()g_vcm->create()によってbfvmm:vcpuインスタンスが作成され,g_vcm->run()によりintel_x64::vcpu::run_delegate::run_delegate()が実行されます. ここで::intel_x64::vmcs::launch() => ::intel_x64::vm::launch_demote() => _vmlaunch_demote()VMLAUNCHが以下のように実行されます.

global _vmlaunch_demote
_vmlaunch_demote:
    call _vmlaunch_trampoline
    ret

_vmlaunch_trampoline:

    pop rsi

    mov rdi, 0x0000681E     ; VMCS_GUEST_RIP
    vmwrite rdi, rsi

    mov rdi, 0x0000681C     ; VMCS_GUEST_RSP
    vmwrite rdi, rsp

    mov rax, 0x1
    vmlaunch
    mov rax, 0x0

    jmp rsi

_vmlaunch_trampolineではスタック上の関数の戻り値をVMCS_GUEST_RIPに設定したのちVMLAUNCHします. これによりVMLAUNCHが成功した場合はvmlaunch_demoteの呼び出し元から仮想化した状態で処理が再開されます.

なおVMCSの初期化はvcpuコンストラクタ内から呼ばれるintel_x64::vmxのコンスタラクタでおこなっています.

Exit handler

VMCSのhost ripにはexit_handler_entry設定されていますexit_handler_entryは第一引数(rdi)に[gs:0x00A0]を設定してbfvmm::intel_x64::exit_handler::handle(bfvmm::intel_x64::exit_handler*)を呼び出しますが,この[gs:0x00A0]の値はexit_handler_ptr経由で設定されています.

exit handlerはexit reason毎にリストで複数持てるようになっており,add_handler()を利用してハンドラを登録します.

拡張

Bareflank/hypervisorだけでは仮想化するだけでほとんど何もしません. EPTも使っていません. 何か具体的なことをするにはハイパーバイザを拡張する必要があります.

普通であればここでハイパーバイザのコードを直接修正するわけですが,ここからが"Extensible hypervisor"の本領発揮といったところで,CMakeを駆使したビルドスクリプトによりBareflankは主要な関数(クラス)を別ファイルに記述してオーバライドできるようになっています.

拡張方法の概要はextension_instructions.mdに書いてあります. Bareflank/hypervisor_example_cpuidcountにexit handlerを追加する例があるので,とりあえずこれを参考にするのが良いと思います.

hypervisor_example_cpuidcountではvcpu_factory::make()をオーバライドしbfvmm::intel_x64::vcpuを継承して作成したvcpuを返すようにしています. vcpuのコンストラクタの中でexit_handler()->add_handlerを呼び出すことでexit handlerを追加しています.

この例ではcpuid命令によりvmexitが発生する度にm_countをインクリメントし,bareflankが終了時にm_countの値を出力しています. ちなみにデフォルトではシルアルポートのCOM0に出力されます.

EPT Hook

Bareflank/extended_apisにEPTハンドラ等のベースがあります. EPTを利用したい場合はこれを拡張するのが簡単だと思います.

Bareflank/extended_apis_example_hookにextended_apisを利用した拡張例があります.

所感

BareflankはModern C++で書かれたthin hypervisorとして(まだまだ活発に開発中ですが)一番の完成度ではないでしょうか. C++のランタイムがきちんと移植されていること,そしてあの(正直どうなってるか分かってない)ビルドシステムがすごいです. UEFIからType1 hypervisorとしても起動できるようなので後で試してみようと思います. まだ応用例は多くはないようですが,これからに期待です.

最後に,何かあればgitterで質問すれば多分答えてもらえると思います.

VFIOによるデバイス操作

VFIO (Virtual Function I/O)Linuxにおいてユーザスペースでデバイスを操作するためのフレームワークの一つです. ユザースペースドライバといえばuioもありますが,uioとVFIOの主要な違いの一つはVFIOはIOMMUを利用するという点です*1.uioはLinux 2.6.23から,VFIOは3.6から本体にマージされています.

VFIOはもともとはKVM/QEMUで安全にドライバを作成するのが主目的だったようですが,もちろんそれ以外でも使えます.KVM/QEMU以外でVFIOを利用している主要なプロジェクトにはDPDKがあります*2

あまりVFIOでデバイスを操作する例が見つからなかったので,動作確認のために簡単にIntel 82574Lのドライバを書いてみました(ドライバと呼んでいいレベルなのか分かりませんが..).

VFIOは情報が少なく最初は戸惑いましたが,慣れれば便利に使えると思います.

以下,主要点をまとめます.

事前準備

VFIOを利用する場合にはCONFIG_VFIOなどのカーネルオプションを有効にする必要があります. といっても主要なディストリビューションならおそらく有効になってると思います.

VFIOでPCIバイスを操作するためには,vfio-pciドライバをデバイスにbindする必要があります. これは例えば,以下のようにしてできます.

% lspci -nn | grep -i Ether
86:00.0 Ethernet controller [0200]: Intel Corporation 82574L Gigabit Network Connection [8086:10d3]
% sudo modprobe vfio-pci
% echo 0000:86:00.0 | sudo tee -a /sys/bus/pci/devices/0000:86:00.0/driver/unbind
% echo 8086 10d3 | sudo tee -a /sys/bus/pci/drivers/vfio-pci/new_id

バイスにvfio-pciを割り当てると,/dev/vfio/vfio および, /dev/vfio/<num> というファイルが作成されます. <num>はIOMMUのグループ番号です.IOMMUは基本デバイスごとに設定できますが,場合によってはIOMMUがデバイスを区別できない場合があり,そうしたものがまとめられてIOMMUグループになります*3.例えば,マルチファンクションのPCIバイスは一つのIOMMUグループに属します. ここで注意点として,VFIOでデバイスを操作するためには,そのデバイスが属するIOMMUグループに含まれる全てのデバイスに対してvfio-pciをbindする必要があります.

あるデバイスのIOMMUグループの確認は,

readlink /sys/bus/pci/devices/<ssss:bb:dd.f>/iommu_group

あるデバイスが属するIOMMUグループに含まれるデバイスは,

ls -l /sys/bus/pci/devices/<ssss:bb:dd.f>/iommu_group/devices

で確認できます.

/dev/vfio/<num>ファイルはデフォルトではrootのみしかopenできませんが,ファイル所有者をchownしてあげればsudoいらずでデバイスが操作できるようになります.

ということでvfio-pciの割り当ては少々面倒ですが,vfio-pci-bind.shを使うと指定したデバイス + 同一のIOMMUグループに属するデバイス全てにvfio-pciをbindし,さらに/dev/vfio/<num>chownしてくれるので便利です(unbindの機能はないようですが).

ちなみにIOMMUグループの番号はどうやら<ssss:bb:dd.f>の昇順につけられているようです.

バイス操作の基本

VFIOでのデバイス操作する場合, /dev/vfio/<num>をopenし,得られたfdに対してioctl経由で設定をおこないます. また,コンテナとIOMMU Typeの設定も必要です.

コンテナはIOMMUグループをまとめたもので,VFIOではこのコンテナに対して同一のIOMMUの設定が適用されます. また,IOMMU TypeはVT-dの場合はVFIO_TYPE1_IOMMUを指定します.Type1というなんとも安直なネーミングですね..

この辺りはopen_vfio()でやっています.

基本情報の取得

バイスのconfiguration spaceやBAR空間などの情報の取得は,ioctl(fd, VFIO_DEVICE_GET_REGION_INFO, idx)でできます. メモリ空間がread/write/mmap 可能かなどの情報が取得できます. このとき,idx0-5がBAR-5の情報,idx 6がconfiguration spaceの情報になります.

また,割り込みの情報はioctl(fd, VFIO_DEVICE_GET_IRQ_INFO, idx)で取得できます. ここでVRIO_IRQ_INFO_EVENTFDフラグがついているものはeventfdを使って割り込みを受け取ることができます(後述). idx 0がINTx, idx 1がMSI, idx 2がMSI-Xです.

この辺りはget_device_info()でやっています.

バイスのメモリ空間へのアクセス

Configuration spaceやBAR空間へは,/dev/vfio/<num>をopenしたデスクリプタに対して,適当なオフセットでread/writeすることでおこないます. オフセットはioctl(VFIO_DEVICE_GET_REGION_INFO)で取得できます.

領域によってはmmapできます.mmapは自動でキャッシュが無効化されるようにmmapされます.ちなみに,configuration spaceはmmapできません.これは,VFIOがconfiguration spaceのフィールドの一部を仮想化する場合があるからです.

dump_configuration_space()でconfiguration spaceのアクセスをおこなっています.

DMA有効化

VFIOとは直接関係ありませんが,DMAを使用するにはPCI configuration spaceのcommand registerを操作してbus masterをenableにする必要があります(enable_bus_master()). これをうっかり忘れるとちゃんとドライバを書いたはずなのにDMAが動かないという悲しい現象が発生します.

DMA用IOMMUの設定

init_rx_buf()init_tx_buf()でパケット送受信用のバッファを割り当てています. ここで,ringの各descriptorはそれぞれ自身のバッファを保持しますが,このアドレスはIOMMUに対する仮想アドレス(IOVA)になります. そして,IOVAとvirtual addressの対応付けをioctl(VFIO_IOMMU_MAP_DMA)でおこないます. VFIOによって仮想アドレスが物理アドレスへ変換され,iovaとphysical addressのマッピングがIOMMUに登録されます.また,登録した領域は自動でpinningされます.

ここで,IOVAをどうするかという問題がでてきます.IOVAは任意に割り当てられますが*4,ぱっと以下の3つが思いつくと思います.

  1. 0から割り当てる
  2. 仮想アドレスをそのまま使う
  3. 物理アドレスを使う

1.の方法はIOVAを0から順番に降るという単純な方法です.

2.は,仮想アドレスをIOVAとして使うので,何も変換がいらず楽です.が,IOMMUの種類によってはIOVAのアドレス幅がCPUの仮想アドレス幅よりも小さいものがあります. VT-dでは,IOMMUによってサポートするIOVAのアドレス幅が39bit (3-level paging)のものと,48bit (4-level paging)のものがあります. IOMMUがサポートするアドレス幅はVT-dのcapability registerから分かります.Linuxではdmesgからcapabilityが確認できます.

% dmesg | grep DMAR
[    0.000000] ACPI: DMAR 0x000000007E8D6550 0000A8 (v01 INTEL  KBL      00000001 INTL 00000001)

ここで, 0x000000007E8D6550がcapability regisetrの値で,この21:16がMGAW (Maximum Guest Address Width)です. この場合MGAW = 100110 = 38 なので,これはアドレス幅が39bitを意味します.従ってこの環境だと仮想アドレスはそのままではIOVAとして使用することはできません. i7-7700, Z270 という比較的最近の構成だと思うんですが..

3.の物理アドレスを使う方法は,ようするにIOMMU的にはIOVA = physical addressのマッピングを作成することになります. これの何が嬉しいかというと,IOMMUをサポートしていないマシンでIOMMUを使用しない場合とアドレスの処理を同じにすることができます.DPDKではこうしています. Linuxではユーザ空間から物理アドレスを求めるための関数は提供されていないので,/proc/self/pagemapをパースすることになります. この場合/proc/self/pagemapの読み込みはCAP_SYS_ADMINが必要なので,要するにsudoが必要になります.

ちなみに,今回は最大でも4096byteの領域しか割り当てていないので関係ないですが,IOMMUを使用しているのでDMA領域は連続である必要はありません. また,本来であればhuge pageを使うべきです.

一般ユーザがカーネル内でロックできるメモリ量は通常制限されています.これはulimit -mで確認できます. 容量を増やすには,一時的にはsudo prlimit --melcok=-1, 恒久的には /etc/security/limits.conf に設定を書きます.(無制限にするなら * - memlock -1

割り込み

VFIOでは割り込みはeventfdで処理します. ioctl(VFIO_DEVICE_SET_IRQS)によって,割り込みとeventfdの対応付けをおこないます. MSIMSI-Xは複数の割り込みベクタを持つことができますが,この場合各割り込みベクタごとにevnetfdが対応します.

この辺りの処理をおこなっているのがenable_intx(), enable_msi(), enable_msix()になります. 82574Lの場合,MSIは一つの割り込みベクタのみなので,INTxとソフトウェア的にはほとんど変わりません. MSI-Xでは5つベクタが持てるので,それぞれに対してeventfdを割り当てています.

eventfdの処理はepollでおこなっています.epollを使うことで,複数のeventfdの中からイベントがあったfdを検知することができます. INTx, MSIの場合にはfdが一つしかないので,単純にeventfdをblockn readして待つことも可能です.

この辺りの処理がhandle_intr()です.

デバッグ

IOMMUの処理にはいくつかtracepointが定義されており,以下のようにして確認できます.

cd /sys/kernel/debug/tracing/
echo 1 > events/iommu/enable
cat trace

例えば,IOMMUに設定したmappingの情報が確認できます.

また,eventにはIOMMU page fautのeventもありますが,どうやらVT-dの場合はこのiommu page fault eventが発火することはないようです. その代わりに,IOMMU page faultが発生した場合はdmesgから確認できます.

後処理

今回書いたソースでは手抜きのため後処理してませんが,本来であればちゃんとioctl(VFIO_IOMMU_UNMAP_DMA)をすべきでしょう. ただ,明示的にunmapしなくてもプログラム終了時に自動でunmapしてくれます.

e1000参考ソース

最後に,e1000のドライバを書く際の参考ソースをあげておきます.

*1:IOMMUを利用しない no IOMMUモードもあることはあります

*2:DPDKにはVFIOとuio,双方のユーザスペースドライバが含まれています

*3:このIOMMUグループはPCの構成に固有のもので変更できません

*4:実際にはDMAに使用不可能なメモリ領域がRMRRとしてACPI DMARの中で通知されますが,普通RMRRの領域とアドレスが被ることは99%ないと思います