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>
です.
実際にメモリ上の表現を確認してみます(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_raw
とfrom_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側で処理するには修正が必要です.
対象環境
方法
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
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)
すると,いろいろあってKVMのvcpu_enter_guest()
にきます.
この中のkvm_x86_ops->run(vcpu)
でVMENTRY, kvm_x86_ops->handle_exit(vcpu)
でVMEIXT後の処理がおこなわれます.
ちなみにVMENTRYやVMEXITが関数ポインタになっているのはIntelとAMD双方に対応するためです.
VMCALL命令でVMEXITした場合は,handle_vmcall()
からkvm_emulate_hypercall()
が呼ばれ,ここでハイパーコールが処理されます.
vcpu_enter_guest()
のコメントに書いてある通り,kvm_emulate_hypercall()
の戻り値で1以外の値を返せばioctl側に制御が戻ります.そうしてもいいのですが,するとQEMU側で命令エミュレーション(RIPを進める処理)をおこなう必要があります.
今回はその代わりにKVMのVCPU 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
- file descriptors
- 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()
続く?
Bareflankを使ってみる
前から気になっていたBareflankを少し触って見ました.
あまりドキュメントがないようなので半分メモがてらBareflankについて書いてみます.
BareflankはいわゆるThin-hypervisorの一種です. 複数VMの動作を目的とする通常のハイパーバイザとは異なり,そのようなハイパーバイザは基本的に一つのゲストOSを実行し,ゲストOSのフックや解析をおこないます. セキュリティや研究目的で用いられることが多いです. そのようなハイパーバイザは(特に最近)結構あって,
などがあります.これらと比較してBareflankは"hypervisor Software Development Toolkit"を謳っており,特徴として
が挙げられます.
とりあえず動かす
公式の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つが思いつくと思います.
- 0から割り当てる
- 仮想アドレスをそのまま使う
- 物理アドレスを使う
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の対応付けをおこないます.
MSIやMSI-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のドライバを書く際の参考ソースをあげておきます.
- Intel 82574 GbE Controller Family Datasheet, https://www.intel.ca/content/dam/doc/datasheet/82574l-gbe-controller-datasheet.pdf
- バイブル
- MINIX e1000, https://github.com/Stichting-MINIX-Research-Foundation/minix/tree/master/minix/drivers/net/e1000
- 基本部分が簡潔にまとまっています.おそらく一番分かりやすいと思います.
- FreeBSD e1000, https://github.com/freebsd/freebsd/tree/master/sys/dev/e1000
- Intelによる実装.実際にe1000のちゃんとしたドライバが欲しいならこれを移植した方がいいと思いますが,巨大なので最初に読むには向いてません.
- Redox e1000, https://github.com/redox-os/drivers/blob/master/e1000d/
- 必要最小限の実装 (rust)
- snabb driver, https://github.com/snabbco/snabb/blob/master/src/apps/intel_mp/intel_mp.lua
- old version, https://github.com/anttikantee/snabbswitch/blob/master/src/apps/intel/intel.lua
- こちらも簡潔にまとまっています (lua)
KVM GPUパススルー設定
環境
- i7-4790 (with VT-d)
- GeForce GTX 1080ti
- Linux Mint 18.3 Sylvia (Ubuntu xenial base) Cinnamon
やりたいこと
手順
archのwikiに丁寧書いてあります.
以下,自分がやった方法
事前準備
- デフォルトだとxenialのapt repositoryのlibvirtのバージョンが古いので,ppa:jacob/virtualisationを追加しておく
- grubのエントリに
iommu=pt intel_iommu=on
を追加 - qemu, libvirt, ovmf, virt-managerあたりのインストール
- libvirtd, virtlogd serviceをenable
Windowsインストール
- virt-managerからwindowsをとりあえずインストール
<os> ... <loader readonly='yes' type='pflash'>/usr/share/OVMF/OVMF_CODE-pure-efi.fd</loader> ... </os>
- virt-managerの
Add Hardware > PCI Host Device
からGPUデバイスを追加 - vendor idの偽装 ( これをしないとnvidiaのドライバをインストールしてもError 43が出てドライバを認識しない)
<features> <hyperv> ... <vendor_id state='on' value='whatever'/> ... </hyperv> ... <kvm> <hidden state='on'/> </kvm> <feature>
- virtual displayなど,不要なデバイスを削除
- これをしないとwindowsを起動させてもTian coreのロゴの画面から先に進まなかった
- windowsを起動後,nvidiaのドライバをインストール
結果
なんかインストール時に微妙にはまったりしたけど無事に動きました\(^o^)/
ACPI DMARメモ
- IOMMUの情報はACPIのDMARに格納されている
- DMARのデータ構造は,Intel VT-d ドキュメント (Intel® Virtualization Technology for Directed I/O) の8章に書いてある
- ざっくり以下のような構造
- DMAR (DMA Remapping Table)
- DRHD (DMA Remapping Hardware Unite Definition) (n個)
- RMRR (Reserved Memory Region Reporting) (m個)
- ATSR (ATS Capability Reporintg) (l個)
- DMAR (DMA Remapping Table)
- DRHDがIOMMUの情報
- RMRRはDMAに使えない予約済みメモリ領域の情報
DMAR tableの例 (Core i7-7770, ASRock Z270 Extreme4)
% sudo cp /sys/firmware/acpi/tables/DMAR % iasl -d DMAR % cat DMAR.dsl /* * Intel ACPI Component Architecture * AML/ASL+ Disassembler version 20160108-64 * Copyright (c) 2000 - 2016 Intel Corporation * * Disassembly of DMAR, Sat Mar 10 21:19:40 2018 * * ACPI Data Table [DMAR] * * Format: [HexOffset DecimalOffset ByteLength] FieldName : FieldValue */ [000h 0000 4] Signature : "DMAR" [DMA Remapping table] [004h 0004 4] Table Length : 000000A8 [008h 0008 1] Revision : 01 [009h 0009 1] Checksum : D2 [00Ah 0010 6] Oem ID : "INTEL " [010h 0016 8] Oem Table ID : "KBL " [018h 0024 4] Oem Revision : 00000001 [01Ch 0028 4] Asl Compiler ID : "INTL" [020h 0032 4] Asl Compiler Revision : 00000001 [024h 0036 1] Host Address Width : 26 [025h 0037 1] Flags : 01 [026h 0038 10] Reserved : 00 00 00 00 00 00 00 00 00 00 [030h 0048 2] Subtable Type : 0000 [Hardware Unit Definition] [032h 0050 2] Length : 0018 [034h 0052 1] Flags : 00 [035h 0053 1] Reserved : 00 [036h 0054 2] PCI Segment Number : 0000 [038h 0056 8] Register Base Address : 00000000FED90000 [040h 0064 1] Device Scope Type : 01 [PCI Endpoint Device] [041h 0065 1] Entry Length : 08 [042h 0066 2] Reserved : 0000 [044h 0068 1] Enumeration ID : 00 [045h 0069 1] PCI Bus Number : 00 [046h 0070 2] PCI Path : 02,00 [048h 0072 2] Subtable Type : 0000 [Hardware Unit Definition] [04Ah 0074 2] Length : 0020 [04Ch 0076 1] Flags : 01 [04Dh 0077 1] Reserved : 00 [04Eh 0078 2] PCI Segment Number : 0000 [050h 0080 8] Register Base Address : 00000000FED91000 [058h 0088 1] Device Scope Type : 03 [IOAPIC Device] [059h 0089 1] Entry Length : 08 [05Ah 0090 2] Reserved : 0000 [05Ch 0092 1] Enumeration ID : 02 [05Dh 0093 1] PCI Bus Number : F0 [05Eh 0094 2] PCI Path : 1F,00 [060h 0096 1] Device Scope Type : 04 [Message-capable HPET Device] [061h 0097 1] Entry Length : 08 [062h 0098 2] Reserved : 0000 [064h 0100 1] Enumeration ID : 00 [065h 0101 1] PCI Bus Number : 00 [066h 0102 2] PCI Path : 1F,00 [068h 0104 2] Subtable Type : 0001 [Reserved Memory Region] [06Ah 0106 2] Length : 0020 [06Ch 0108 2] Reserved : 0000 [06Eh 0110 2] PCI Segment Number : 0000 [070h 0112 8] Base Address : 000000007E091000 [078h 0120 8] End Address (limit) : 000000007E0B0FFF [080h 0128 1] Device Scope Type : 01 [PCI Endpoint Device] [081h 0129 1] Entry Length : 08 [082h 0130 2] Reserved : 0000 [084h 0132 1] Enumeration ID : 00 [085h 0133 1] PCI Bus Number : 00 [086h 0134 2] PCI Path : 14,00 [088h 0136 2] Subtable Type : 0001 [Reserved Memory Region] [08Ah 0138 2] Length : 0020 [08Ch 0140 2] Reserved : 0000 [08Eh 0142 2] PCI Segment Number : 0000 [090h 0144 8] Base Address : 000000007F800000 [098h 0152 8] End Address (limit) : 000000008FFFFFFF [0A0h 0160 1] Device Scope Type : 01 [PCI Endpoint Device] [0A1h 0161 1] Entry Length : 08 [0A2h 0162 2] Reserved : 0000 [0A4h 0164 1] Enumeration ID : 00 [0A5h 0165 1] PCI Bus Number : 00 [0A6h 0166 2] PCI Path : 02,00 Raw Table Data: Length 168 (0xA8) 0000: 44 4D 41 52 A8 00 00 00 01 D2 49 4E 54 45 4C 20 // DMAR......INTEL 0010: 4B 42 4C 20 00 00 00 00 01 00 00 00 49 4E 54 4C // KBL ........INTL 0020: 01 00 00 00 26 01 00 00 00 00 00 00 00 00 00 00 // ....&........... 0030: 00 00 18 00 00 00 00 00 00 00 D9 FE 00 00 00 00 // ................ 0040: 01 08 00 00 00 00 02 00 00 00 20 00 01 00 00 00 // .......... ..... 0050: 00 10 D9 FE 00 00 00 00 03 08 00 00 02 F0 1F 00 // ................ 0060: 04 08 00 00 00 00 1F 00 01 00 20 00 00 00 00 00 // .......... ..... 0070: 00 10 09 7E 00 00 00 00 FF 0F 0B 7E 00 00 00 00 // ...~.......~.... 0080: 01 08 00 00 00 00 14 00 01 00 20 00 00 00 00 00 // .......... ..... 0090: 00 00 80 7F 00 00 00 00 FF FF FF 8F 00 00 00 00 // ................ 00A0: 01 08 00 00 00 00 02 00 // ........