mmapとページフォールト

はじめに

メモリ管理周りで勘違いしていた部分をコードを追いながら説明してみます。

概要

Q1: qemuはゲストメモリ領域をmmap(2)してるんだよね?

A1: してますがコードには呼び出し箇所はありません*1mmapposix_memalign(3)内で呼び出されます。

Q2: ゲストより先に(qemuメモリ空間内の)ゲストメモリ領域にqemuがアクセスした場合どうなる?

A2: qemuページフォールトが起きますが、SPTEは割当てられません。そのため、ゲストが当該ページにアクセスすると再度ページフォールトが起きます。SPTEが設定されるのはこのときです。

調査対象バージョン

  • x86_64 (Intel EPTあり)
    • シャドウページテーブルはさすがに今さらなので(^^;
  • linux-2.6.37.6
    • 2.6.38はasynchronous page faultを実装していて読みにくかったので2.6.37にしました
  • qemu-kvm-0.14.0

mmap(2)はどこで呼ばれている?

まずは、KVMqemuとゲストのメモリの関係を説明する際によく登場するmmapが、実際にどこで呼ばれているか調べてみました。とりあえずqemuをstraceしてみると、

mmap(NULL, 268443648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa9532fb000
madvise(0x7fa9532fc000, 268435456, 0xc /* MADV_??? */) = 0
ioctl(3, KVM_CHECK_EXTENSION, 0x10) = 1
ioctl(4, KVM_SET_USER_MEMORY_REGION, 0x7fff2ab282e0) = 0
ioctl(4, KVM_SET_USER_MEMORY_REGION, 0x7fff2ab282e0) = 0

確かに呼ばれていることがわかります。mmapに続いて、madvise(2)とioctl(2)で、当該メモリ領域がKSMでマージ可能であることとゲストのメモリ領域であることをカーネルに伝えます。

qemu-kvmのコードでは、qemu_ram_alloc_from_ptr@exec.c辺りが該当するようです。この少し上にmmapが呼ばれているのが見えるのですが、それはS390向けのもので、x86の場合はqemu_vmalloc@oslib-posix.cが呼ばれるみたいです。この関数を追っていくと、最後にposix_memalign(3)を呼んでいます。posix_memalignは指定したアラインメントでメモリを割り当てる、どちらかというとmallocに近いライブラリ関数です(freeできますし)。

あれ?mmapは?と訝しんで他のコードも見てみたのですが、どうもこのコードで正しいようです。もしやと思い、straceではなくltrace -S(ライブラリコールとシステムコールを同時にトレースするコマンド)を使ってみると、

posix_memalign(0x7fff03c4ba60, 4096, 0x10000000, 0x2cc2127, 56
SYS_mmap(0, 0x10002000, 3, 34, 0xffffffff) = 0x7f6db7c05000<... posix_memalign resumed> ) = 0
madvise(0x7f6db7c06000, 0x10000000, 12, 0x7f6db7c05ff0, 0xffffffff
SYS_madvise(0x7f6db7c06000, 268435456, 12) = 0<... madvise resumed> ) = 0

ありました。posix_memalignがmmapを呼んでます。そういえばmallocmmapすることがあるので、この動作は最初に気づくべきでした。。。

ちなみにioctlはkvm_vm_ioctl@kvm-all.cで呼ばれます。この関数は、hw/pc.cで、qemu_ram_alloc(_from_ptr)の後に呼び出されているcpu_register_physical_memoryを辿っていくと見つかります。(コールバックでKVM向けのコードが呼ばれているみたいです。)なおメモリ領域はメモリスロット(qemu-kvmではKVMSlot、カーネル内ではkvm_memory_slot構造体)で管理されています。

qemuが先にページフォールトを起こしたらどうなる?

SPTEがまだ存在しないページにゲストがアクセスすると、ページフォールトが起きてKVMに処理が移り、SPTEを設定したあと処理がゲストに戻される。ゲストでページフォールトが起きるときはこんな感じで動作するはず。では、仮想DMAなどで先にqemuプロセスの方でページフォールトが起きたときはどうなるんだろう?という疑問がふと湧いてきました。

前述のゲストメモリ領域に対して実行されるioctlの行く末を、カーネルの中まで追ってみてもメモリ領域に対して特に特別な処理を行なっていませんでした。つまり、qemu(のゲストメモリ領域)でページフォールトが起きたときにもLinuxの通常のメモリ管理のコード(ページフォールトハンドラ)が呼ばれるようです。SPTEはこの際に設定されるものだと思っていたので、またしても、あれ?と思いました。

実はLinuxの通常のメモリ管理コードに(mmu_notifierのような)フックがあるのかと思い少しコードを追ってみました。

Linuxページフォールトが起きると、IDTに設定されたdo_page_faultからhandle_mm_faultが呼ばれます。handle_mm_faultでは当該メモリ領域の種類によって処理を振り分けます。qemuのゲストメモリ領域はanonymousページなのでdo_anonymous_pageが呼ばれ、フォールを起こしたアドレスのページ割当て、PTEの設定などが行われます。

ひと通りコードを見てみたのですが、残念ながらKVMのコードを呼び出している場所はありませんでした。どうやら、SPTEは設定されず、ゲストが当該ページにアクセスされたときに再度ページフォールトが起きる仕様のようです*2

というわけで、もう知りたいことはわかったのですが、せっかくなので今度はKVM(EPT)のページフォールト処理を見ていきます。

EPTの場合、VM exitでKVMに処理が戻ったあとexit reasonを調べてページフォールトが起きたとわかったら、tdp_page_faultへ飛んできます*3。ゲスト(ページ)フレーム番号(GFN)が渡されるので、tdp_page_faultはこれを元にPFNを割り出し、GFNとPFNをマッピングするSPTE*4を設定します。

PFNの割り出しはgfn_to_pfnという関数で行われます。この関数はまずKVMが管理するメモリスロットを使ってGFNからホスト仮想アドレス(HVA)、つまりqemuの仮想アドレスを算出し、仮想アドレスに対応するページを取得、そしてそのページからPFNを手にいれます。名前からは想像できないくらい色々な処理をやっています。

HVAから対応するページを取得するために、gfn_to_pfnはget_user_pages_fastというLinuxメモリ管理ではお馴染みのget_pageの派生関数を呼びます。get_user_pages_fastはfollow_pageというページテーブルを辿る関数を使ってページを取得しようとします。もしページが存在しない場合(またCoWページに書き込みアクセスした場合など)はhandle_mm_faultを呼び出してページを割り当て、そのページのPTEを設定します。この処理は、通常のページフォールトハンドラと同じです。つまり、この時点でqemu仮想アドレス空間の当該ページは既にPTEが設定され、次にqemuがアクセスしてもページフォールトは起きない、ということです。

いずれにせよページを割り当てたのでそこからPFNを取得することができるので、次は、GFNとPFNをマッピングするSPTEを設定する処理です。tdp_page_faultはgfn_to_pfnで得たPFNとGFNを__direct_mapに渡します。この関数からmmu_set_spteset_spteupdate_spteが呼ばれSPTEが設定されます。

かなり大雑把です。詳細はコードを参照ください。kvm.txtでコードで使われる用語を把握してから読むと理解しやすくなると思います。

おわりに

実際にコードを追ってみたことで、勘違いしていたり理解が浅かった部分が少し解消されました。やっぱり正しく理解するにはコードを読まないといけませんね。

ところで、コードを読むまでは、ゲストのメモリ領域はqemuの仮想アドレス空間mmapマッピングされている、という感じで理解していたのですが、実際は、qemuのメモリ領域の一部をゲストのメモリ領域とみなしているだけ、の方が実状に近いみたいです。

*1:x86の場合です。昔はあったのかも?

*2:pr_debugを使って実際にそうなってることは確認しましたが、煩雑なので省略。もっと賢い方法ないですかね。

*3:TDP=Two-Dimensional Paging。Intel EPTやAMD VRI(NPT)のアーキテクチャ中立な名称です。

*4:EPTでもシャドウという名前を使っているようです。