cyan's blog

しょーもない事しか書いていません

KVM_SET_USER_MEMORY_REGION in Linux x86 part1

Linux(tag v5.4)におけるVM ioctl KVM_SET_USER_MEMORY_REGIONリクエストの処理の流れについてのメモ.読んだ順に書いているだけなのでまとまっていない.

前提

kvmを利用したいアプリケーションはkvm APIを利用することでVMを管理するための諸構造,vCPUなどをkvmに作成してもらい,それを利用することが出来る. しかし,メモリ空間を含む仮想化されたHWはアプリケーション側が用意しなくてはならない.一般的にqemuなどがこのポジションである.

つまり,VMのゲスト物理アドレス空間とqemuなどのユーザアプリケーションの使用するホスト仮想アドレス空間マッピングを管理する必要がある.本稿ではアプリケーションが用意したゲストメモリ空間をKVMで作成したVM上に登録,編集,削除するAPIである KVM_SET_USER_MEMORY_REGIONの挙動について記述する.

kvm APIを利用する際はまずキャラクタデバイスファイル/dev/kvmをopenすることでsystem ioctlを利用することが出来る. system ioctlではAPIバージョン,エクステンションの確認,VMの作成などを行う.

/dev/kvmKVM_CREATE_VMリクエストを送信するとanonymous-inodeに紐付けられたファイルのfile descriptorである VM file descriptorが返され,このファイルディスクリプタ(vmfd)を用いてVM ioctlを利用できる. VM ioctlではvCPUの作成,ゲストメモリ空間の作成/編集/削除など,VM単位の操作を行う.

このvmfdに対してioctlでKVM_SET_USER_MEMORY_REGIONリクエストを発行することでゲストのメモリ空間を作成/編集/削除することができる.

KVM_CREATE_VM

まずVMを作成し,vmfdを取得するためのAPIである/dev/kvmに対するKVM_CREATE_VMリクエストについて大まかに記載する. VM ioctlの動作の詳細が本稿のメインテーマなので詳細には解説しない.

はじめにkvmバイスファイルの作成処理を追う.

kvm_init

virt/kvm/kvm_main.c l4316

int kvm_init(void *opaque, unsigned vcpu_size, unsigned vcpu_align,
                  struct module *module)
{

(中略)

        r = misc_register(&kvm_dev);
        if (r) {
                pr_err("kvm: misc device register failed\n");
                goto out_unreg;
        }

(中略)
}

miscデバイスファイルを作成する際にmiscdevice構造体kvm_devを引数に使用.

kvm_dev

virt/kvm/kvm_main.c l3586

static struct file_operations kvm_chardev_ops = {
        .unlocked_ioctl = kvm_dev_ioctl,
        .llseek         = noop_llseek,
        KVM_COMPAT(kvm_dev_ioctl),
};

static struct miscdevice kvm_dev = {
        KVM_MINOR,
        "kvm",
        &kvm_chardev_ops,
};

kvmバイスファイルのfile_operationsはkvm_chardev_ops構造体で定義されており,ioctlの処理を担う関数はunlocked_ioctlメンバのkvm_dev_ioctl関数である.

kvm_dev_ioctl

kvm_dev_ioctl in virt/kvm/kvm_main.c l3564

static long kvm_dev_ioctl(struct file *filp,
                          unsigned int ioctl, unsigned long arg)
{
        long r = -EINVAL;

        switch (ioctl) {
        case KVM_GET_API_VERSION:
                if (arg)
                        goto out;
                r = KVM_API_VERSION;
                break;
        case KVM_CREATE_VM:
                r = kvm_dev_ioctl_create_vm(arg);
                break;

(中略)

out:        
        return r;
}

ioctlのリクエスト内容に従ってswitch, caseで分岐が行われ,kvm_dev_ioctl_create_vm関数が呼び出される.

kvm_dev_ioctl_create_vm

kvm_dev_ioctl_create_vm in virt/kvm/kvm_main.c l3500

static int kvm_dev_ioctl_create_vm(unsigned long type)
{
        int r;
        struct kvm *kvm;
        struct file *file;

        kvm = kvm_create_vm(type);
        if (IS_ERR(kvm))
                return PTR_ERR(kvm);

(中略)

        file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);
        if (IS_ERR(file)) {
                put_unused_fd(r);
                r = PTR_ERR(file);
                goto put_kvm;
        }

        /*
         * Don't call kvm_put_kvm anymore at this point; file->f_op is
         * already set, with ->release() being kvm_vm_release().  In error
         * cases it will be called by the final fput(file) and will take
         * care of doing kvm_put_kvm(kvm).
         */
        if (kvm_create_vm_debugfs(kvm, r) < 0) {
                put_unused_fd(r);
                fput(file);
                return -ENOMEM;
        }
        kvm_uevent_notify_change(KVM_EVENT_CREATE_VM, kvm);

        fd_install(r, file);
        return r;

(中略)

}

kvm_dev_ioctl_create_vm関数は新規VMを管理するためのkvm構造体の作成,メモリスロット,IOバス割当,初期化などの処理を行い, 作成されたVMを管理するためにファイルを作成し,そのvmfdを返す. kvmを利用するアプリケーションはこのvmfdへのioctlを通じてVMを管理する.e.g. vCPUの作成,メモリ領域の作成/編集/削除など(作成されたVMはvCPUもメモリ領域も割り当てられていない) このうちメモリ領域の作成/編集/削除を担うものがKVM_SET_USER_MEMORY_REGIONである.

なお実際のkvm APIの利用フローを確かめるためにはqemuソースコードが良い.accel/kvm/kvm-all.cから辿って読む.

KVM_SET_USER_MEMORY_REGION

kvm_dev_ioctl_create_vm関数で作成されたファイルへ登録されたfile_operations構造体はkvm_vm_fopsであるため,まずはここをチェック.

kvm_vm_fops

kvm_vm_fops in virt/kvm/kvm_main.c l3493

static struct file_operations kvm_vm_fops = {
        .release        = kvm_vm_release,
        .unlocked_ioctl = kvm_vm_ioctl,
        .llseek         = noop_llseek,
        KVM_COMPAT(kvm_vm_compat_ioctl),
};

unlocked_ioctlメンバからvmfdへのioctlを処理する関数はkvm_vm_ioctlであることがわかる.

kvm_vm_ioctl

kvm_vm_ioctl in virt/kvm/kvm_main.c l3263

static long kvm_vm_ioctl(struct file *filp,
                           unsigned int ioctl, unsigned long arg)
{
        struct kvm *kvm = filp->private_data;
        void __user *argp = (void __user *)arg;
        int r;

        if (kvm->mm != current->mm)
                return -EIO;
        switch (ioctl) {
        case KVM_CREATE_VCPU:
                r = kvm_vm_ioctl_create_vcpu(kvm, arg);
                break;
        case KVM_ENABLE_CAP: {
                struct kvm_enable_cap cap;

                r = -EFAULT;
                if (copy_from_user(&cap, argp, sizeof(cap)))
                        goto out;
                r = kvm_vm_ioctl_enable_cap_generic(kvm, &cap);
                break;
        }
        case KVM_SET_USER_MEMORY_REGION: {
                struct kvm_userspace_memory_region kvm_userspace_mem;

                r = -EFAULT;
                if (copy_from_user(&kvm_userspace_mem, argp,
                                                sizeof(kvm_userspace_mem)))
                        goto out;

                r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
                break;
        }

(中略)

out:
        return r;
}

kvm_vm_ioctl_set_memory_region関数をkvm構造体,アプリケーションから受け取ったkvm_userspace_mem構造体を渡し呼び出す.kvm_userspace_mem構造体の定義は以下の通り.

kvm_userspace_memory_region

kvm_userspace_memory_region in include/uapi/linux/kvm.h l97

/* for KVM_SET_USER_MEMORY_REGION */
struct kvm_userspace_memory_region {
        __u32 slot;
        __u32 flags;
        __u64 guest_phys_addr;
        __u64 memory_size; /* bytes */
        __u64 userspace_addr; /* start of the userspace allocated memory */
};

/*
 * The bit 0 ~ bit 15 of kvm_memory_region::flags are visible for userspace,
 * other bits are reserved for kvm internal use which are defined in
 * include/linux/kvm_host.h.
 */
#define KVM_MEM_LOG_DIRTY_PAGES (1UL << 0)
#define KVM_MEM_READONLY        (1UL << 1)

各メンバは

  • slot:
    • 31-16: address space id.
    • 15-0: slot id
  • flags:
    • 15-0: for usespace
      • 0: Dirty Trackingを実現するためのbit.VMライブ移送とかに必要.
      • 1: 文字通りメモリ空間をread onlyにする.VM,ホスト両方のメモリ保護だったりsecure bootだったり色々使う.
  • guest_phys_addr: そのまま
  • memory_size: そのまま
  • userspace_addr: そのまま

kvm_vm_ioctl_set_memory_regionについて見ていく.

kvm_vm_ioctl_set_memory_region

kvm_vm_ioctl_set_memory_region in virt/kvm/kvm_main.c l1160

static int kvm_vm_ioctl_set_memory_region(struct kvm *kvm,
                                          struct kvm_userspace_memory_region *mem)
{
        if ((u16)mem->slot >= KVM_USER_MEM_SLOTS) [0]
                return -EINVAL;

        return kvm_set_memory_region(kvm, mem);
}

slot idがユーザスペースからアクセス可能なメモリスロット数を示すKVM_USER_MEM_SLOTS以上になっていないか調べて[0]kvm_set_memory_regionを呼ぶだけ.次はkvm_set_memory_regionについて.

kvm_set_memory_region

kvm_set_memory_region in virt/kvm/kvm_main.c l1148

int kvm_set_memory_region(struct kvm *kvm,
                          const struct kvm_userspace_memory_region *mem)
{
        int r;

        mutex_lock(&kvm->slots_lock);
        r = __kvm_set_memory_region(kvm, mem);
        mutex_unlock(&kvm->slots_lock);
        return r;
}

ロックを獲得して__kvm_set_memory_regionを呼び出し,ロックを解放するだけ.

次は__kvm_set_memory_regionについて.長いので分割して見ていく.

__kvm_set_memory_region part1

__kvm_set_memory_region in virt/kvm/kvm_main.c l977 part1

/*
 * Allocate some memory and give it an address in the guest physical address
 * space.
 *
 * Discontiguous memory is allowed, mostly for framebuffers.
 *
 * Must be called holding kvm->slots_lock for write.
 */
int __kvm_set_memory_region(struct kvm *kvm,
                            const struct kvm_userspace_memory_region *mem)
{
        int r;
        gfn_t base_gfn;
        unsigned long npages;
        struct kvm_memory_slot *slot;
        struct kvm_memory_slot old, new;
        struct kvm_memslots *slots = NULL, *old_memslots;
        int as_id, id;
        enum kvm_mr_change change;

        r = check_memory_region_flags(mem); [1]
        if (r)
                goto out;

~~~

check_memory_region_flags

check_memory_region_flags in virt/kvm/kvm_main.c l927

static int check_memory_region_flags(const struct kvm_userspace_memory_region *mem)
{
        u32 valid_flags = KVM_MEM_LOG_DIRTY_PAGES;

#ifdef __KVM_HAVE_READONLY_MEM
        valid_flags |= KVM_MEM_READONLY;
#endif

        if (mem->flags & ~valid_flags)
                return -EINVAL;

        return 0;
}

有効なフラグ以外がmem->flagsに含まれていないか確認する[1].

__kvm_set_memory_region part2

__kvm_set_memory_region in virt/kvm/kvm_main.c l977 part2

~~~
        r = -EINVAL;
        as_id = mem->slot >> 16;  [2]
        id = (u16)mem->slot; [3]

        /* General sanity checks */
        if (mem->memory_size & (PAGE_SIZE - 1)) [4]
                goto out;
        if (mem->guest_phys_addr & (PAGE_SIZE - 1)) [5]
                goto out;
        /* We can read the guest memory with __xxx_user() later on. */
        if ((id < KVM_USER_MEM_SLOTS) &&
            ((mem->userspace_addr & (PAGE_SIZE - 1)) ||
             !access_ok((void __user *)(unsigned long)mem->userspace_addr,
                        mem->memory_size))) [6]
                goto out;
        if (as_id >= KVM_ADDRESS_SPACE_NUM || id >= KVM_MEM_SLOTS_NUM) [7]
                goto out;
        if (mem->guest_phys_addr + mem->memory_size < mem->guest_phys_addr) [8]
                goto out;

はじめに引数として受け取ったkvm_userspace_memory_region構造体について,各メンバの値の正当性の確認が行われる.

  • mem->slotを16bitずつ上下に分割してas_id, idの2つに代入[2][3].
  • memory_size,guest_phys_addrに対するページサイズアライメントチェック[4].
  • slot IDがuser memory slotの範囲に収まり,かつ(userspace_addrのページサイズアライメントチェックがNGもしくはuserspace_addrからmemory_size分にアクセスできない)場合はエラーに[6]
  • as_idがKVM_ADDRESS_SPACE_NUM以上である,もしくはslot IDがKMV_MEM_SLOTS_NUM以上であればエラーに[7]
  • オーバーフローチェック[8]

KVM_USER_MEM_SLOTSはユーザスペースからアクセス可能なメモリ領域のスロットの数を定義している.のでslot IDがuser memory slotのものなのにuserspace_addrのアライメントが取れていなかったりuserspace_addrからmemory_size分がアクセス不能であることはおかしいということ[6].逆にアクセス不能なメモリスロットの数はKVM_PRIVATE_MEM_SLOTSで定義され,このKVM_USER_MEM_SLOTSとKVM_PRIVATE_MEM_SLOTSを加算したものがKVM_MEM_SLOTS_NUMとして定義される.[7]はas_idがaddress spaceの数以上になっていないか,slot idが最大値(=KVM_MEM_SLOTS_NUM)を超えていないか確認しているだけ.

private slotの話をしてしまったが,そもそもioctl KVM_SET_USER_MEMORY_REGIONリクエストからこの関数を呼び出すなら,kvm_vm_ioctl_set_memory_regionの[0]でslot id >= KVM_USER_MEM_SLOTSでないことは確認しているので,privateなslotについて何も考えることはない.(KVM_SET_USER_MEMORY_REGIONだよ)

access_ok

ユーザメモリ空間がアクセス可能であるかをチェックするマクロ.であるのでアーキテクチャごとに実装が異なる.x86での定義は以下の通り. ドキュメントにあるとおり,アーキテクチャによっては指定されたメモリ空間がユーザ空間に収まっているかをチェックするだけであり,x86でもそう.

access_ok in arch/x86/include/asm/uaccess.h l76

 * access_ok - Checks if a user space pointer is valid
 * @addr: User space pointer to start of block to check
 * @size: Size of block to check
 *
 * Context: User context only. This function may sleep if pagefaults are
 *          enabled.
 *
 * Checks if a pointer to a block of memory in user space is valid.
 *
 * Note that, depending on architecture, this function probably just
 * checks that the pointer is in the user space range - after calling
 * this function, memory access functions may still return -EFAULT.
 *
 * Return: true (nonzero) if the memory block may be valid, false (zero)
 * if it is definitely invalid.
 */
#define access_ok(addr, size)                                   \
({                                                                      \
        WARN_ON_IN_IRQ();                                               \
        likely(!__range_not_ok(addr, size, user_addr_max()));           \
})

WARN_ON_IN_IRQはCONFIG_DEBUG_ATOMIC_SLEEPがyの時にのみ実体を持つ.デバッグ環境の動作までは深追いしないので__range_not_okから.

__range_not_ok

__range_not_ok in arch/x86/include/asm/uaccess.h l62

#define __range_not_ok(addr, size, limit)                               \
({                                                                      \
        __chk_user_ptr(addr);                                           \
        __chk_range_not_ok((unsigned long __force)(addr), size, limit); \
})

__chk_user_ptrはSparseを使用する際以外は(void)0なのでスキップ.Sparseとは何?→Documentation/dev-tools/sparseを参照.

__chk_range_no_okは

return addr + size > limit

といった動作をするインライン関数であるが,その実装には工夫が見られる.別記事にして取り上げているので詳細はそちらを参照されたい.

yumaueda.hatenablog.com

つまり,__range_not_okはaddr + size > limitを評価し,真偽を返すマクロである. 引数limitにはaccess_okの定義内にてuser_addr_max()の返り値を渡している. user_addr_max()はcurrent->uhread_addr_limit.segを返すマクロである.

access_okは__range_not_okの返り値の否定を返すため,その動作は

return addr + size > current->uhread_addr_limit.seg

ということになり,指定されたaddr, sizeがcurrent taskのメモリ空間内に収まっていれば真であるので,指定されたメモリ空間がユーザ空間に収まっているかのみチェックしている.

__kvm_set_memory_region part3

__kvm_set_memory_region in virt/kvm/kvm_main.c l977 part3

        slot = id_to_memslot(__kvm_memslots(kvm, as_id), id); [9]
        base_gfn = mem->guest_phys_addr >> PAGE_SHIFT; [10]
        npages = mem->memory_size >> PAGE_SHIFT; [11]

        if (npages > KVM_MEM_MAX_NR_PAGES) [12]
                goto out;

        new = old = *slot; [13]

        new.id = id;
        new.base_gfn = base_gfn;
        new.npages = npages;
        new.flags = mem->flags;
  • as_idの正当性をチェックしつつ対応するkvm_memslots構造体を__kvm_memslots関数で求め,kvm_memslots構造体とslot idから対応するkvm_memory_slot構造体を求める[9]
  • mem->guest_phys_addrをページフレームナンバーへ変換[10]
  • mem->memory_sizeをページ数へ変換[11]
  • ページ数がスロットに割当可能な範囲内かチェック[12]
  • new, oldに[9]で得た編集したいメモリスロットに対応するkvm_memroy_slot構造体の値を代入[13]
  • その後,newに引数memから得た,新しいslot id,base_gfn(メモリスロットの対応するゲスト物理アドレス空間のベースページのフレームナンバ),npages(同空間のページ数),flagsを代入

[9]のas_id,slot idに対応するkvm_memory_slot構造体を求める処理,その中で用いられる__kvm_memslots関数とid_to_memslot関数について補足するため,まずメモリスロットの構造について整理する.

struct kvm

kvm in include/linux/kvm_host.h l 443

struct kvm {
        spinlock_t mmu_lock;
        struct mutex slots_lock;
        struct mm_struct *mm; /* userspace tied to this vm */
        struct kvm_memslots __rcu *memslots[KVM_ADDRESS_SPACE_NUM];

(略)

kvm構造体はその直下にメモリスロットを編集する際に使用するロックであるmutex構造体のメンバslots_lock,各アドレススペースごとにメモリスロットを管理するためのkvm_memslots構造体の配列へのポインタmemslotsメンバを持つ.配列へのインデックスとしてas_idを使用することで各kvm_memslots構造体へアクセスできる.

slots_lockはKVM_SET_USER_MEMORY_REGIONから呼び出される場合kvm_set_memory_regionで確保されている.

struct kvm_memslots

kvm_memslots in include/linux/kvm_host.h l434

struct kvm_memslots {
        u64 generation;
        struct kvm_memory_slot memslots[KVM_MEM_SLOTS_NUM];
        /* The mapping table from slot id to the index in memslots[]. */
        short id_to_index[KVM_MEM_SLOTS_NUM];
        atomic_t lru_slot;
        int used_slots;
};

kvm_memslots構造体は配下にkvm_memory_slot構造体の配列であるmemslotsメンバを持つ.しかしkvm_memslots構造体のケースと異なり,kvm_memory_slot構造体の配列memslotsのインデックスはslot idではない. slot idとmemslotsのインデックスをマップする配列id_to_indexを用いてmemslotsのインデックスを得て,そのインデックスを用いて各要素にアクセスする.

struct kvm_memory_slot

kvm_memory_slot in include/linux/kvm_host.h l343

struct kvm_memory_slot {
        gfn_t base_gfn;
        unsigned long npages;
        unsigned long *dirty_bitmap;
        struct kvm_arch_memory_slot arch;
        unsigned long userspace_addr;
        u32 flags;
        short id;
};

このkvm_memory_slot構造体がメモリスロットを表現する最小単位のデータ構造である.そのメンバは基本的にここまでで登場したデータ構造と対応するものがほとんどである. まだ登場していないdirty_bitmapはdirty trackingを実現するためのbitmap,kvm_arch_memory_slotはアーキテクチャ特有のデータを管理する構造体である.

__kvm_memslots

__kvm_memslots in include/linux/kvm_host.h l626

static inline struct kvm_memslots *__kvm_memslots(struct kvm *kvm, int as_id)
{
        as_id = array_index_nospec(as_id, KVM_ADDRESS_SPACE_NUM);
        return srcu_dereference_check(kvm->memslots[as_id], &kvm->srcu,
                        lockdep_is_held(&kvm->slots_lock) ||
                        !refcount_read(&kvm->users_count));
}

__kvm_memslots()はas_idが正当な値かを確認し,正当な値であればそのas_idに対応するkvm_memslots構造体のポインタをkvm構造体のメンバmemslotsから返す. 不当な値であればaddress space 0に対応するもののポインタを返す. このas_idの正当性チェックはarray_index_nospecを用いて行われる.array_index_nospecは[0, KVM_ADDRESS_SPACE_NUM)にas_idが含まれていればas_idをそのまま返し,そうでなければ0を返す. 一見シンプルな処理であるが,高速化のために内部ではインラインアセンブリを用いた記述が行われている.詳細は以下の記事を参照されたい.

yumaueda.hatenablog.com

でも__kvm_set_memory_region part2の[7]でas_idがKVM_ADDRESS_SPACE_NUM以上でないかチェックしているからこの正当性チェックは意味あるのか?わからないから誰か教えてください.

id_to_memslot

id_to_memslot in include/linux/kvm_host.h l646

id_to_memslot(struct kvm_memslots *slots, int id)
{
        int index = slots->id_to_index[id];
        struct kvm_memory_slot *slot;

        slot = &slots->memslots[index];

        WARN_ON(slot->id != id);
        return slot;
}

id_to_memslot()はkvm->memslotsから取得したkvm_memslots構造体のポインタから当該構造体のid_to_indexメンバを参照し,目的のメmem->userspace_addrモリスロットに対応する kvm_memslots.memslots[]のインデックスを得て,その後目的のメモリスロットのポインタを返す.

__kvm_set_memory_region part4

このパートではこれから行われるメモリスロットに対する処理の種類を決定する. 処理の種類は

  • KVM_MR_CREATE: メモリスロットの作成
  • KVM_MR_MOVE: メモリスロットがホスト仮想アドレス領域をマップするゲスト物理アドレス領域のベースを移動
  • KVM_MR_FLAGS_ONLY: flagだけの変更
  • KVM_MR_DELETE: メモリスロットの削除

の4つである.

__kvm_set_memory_region in virt/kvm/kvm_main.c l977 part4

        if (npages) {
                if (!old.npages)
                        change = KVM_MR_CREATE;
                else { /* Modify an existing slot. */
                        if ((mem->userspace_addr != old.userspace_addr) ||
                            (npages != old.npages) ||
                            ((new.flags ^ old.flags) & KVM_MEM_READONLY))
                                goto out;

                        if (base_gfn != old.base_gfn)
                                change = KVM_MR_MOVE;
                        else if (new.flags != old.flags)
                                change = KVM_MR_FLAGS_ONLY;
                        else { /* Nothing to change. */
                                r = 0;
                                goto out;
                        }
                }
        } else {
                if (!old.npages)
                        goto out;

                change = KVM_MR_DELETE;
                new.base_gfn = 0;
                new.flags = 0;
        }

メモリスロットに対する操作の判定は以下のように行われる.

  • npagesが0でない
    • old.npagesが0
      • 当該メモリスロットはまだ利用されていないので操作はメモリスロットの作成となる
    • old.npagesが0でない
      • (mem->userspace_addr != old.userspace_addr) || (npages != old.npages) || ((new.flags ^ old.flags) & KVM_MEM_READONLY) true
        • エラー.npages,old.npagesが0でない時点で操作はホスト仮想アドレス空間のマップ先のゲスト物理アドレス空間のベースを移動するか,flagsの変更のどちらか
        • なので,mem->userspace_addr == old.userspace_addr,npages == old.npagesは許容されない
        • flagsを変更するといってもreadonlyフラグの変更は許されない,つまりフラグの変更=実質dirtytracking用
      • (mem->userspace_addr != old.userspace_addr) || (npages != old.npages) || ((new.flags ^ old.flags) & KVM_MEM_READONLY) false
        • base_gfn != old.base_gfn
          • メモリスロットに対する操作はマップ先のゲスト物理アドレス空間のベースの移動
        • base_gfn == old.base_gfn
          • new.flags != old.flags
            • メモリスロットに対する操作はフラッグの変更
          • new.flags == old.flags
            • やることなし
  • npagesが0
    • old.npagesが0
      • エラー.まだ使用されていないスロットに対して削除を行うことになる.
    • old.npagesが0でない
      • メモリスロットに対する操作は削除になる.このときnew.npagesは既に0であるので,残りのbase_gfn,flagsも0とする

__kvm_set_memory_region part5

        if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) { [14]
                /* Check for overlaps */
                r = -EEXIST;
                kvm_for_each_memslot(slot, __kvm_memslots(kvm, as_id)) {
                        if (slot->id == id)
                                continue;
                        if (!((base_gfn + npages <= slot->base_gfn) ||
                              (base_gfn >= slot->base_gfn + slot->npages)))
                                goto out;
                }
        }

        /* Free page dirty bitmap if unneeded */
        if (!(new.flags & KVM_MEM_LOG_DIRTY_PAGES)) [15]
                new.dirty_bitmap = NULL;

        r = -ENOMEM;
        if (change == KVM_MR_CREATE) { [16]
                new.userspace_addr = mem->userspace_addr;

                if (kvm_arch_create_memslot(kvm, &new, npages))
                        goto out_free;
        }

        /* Allocate page dirty bitmap if needed */
        if ((new.flags & KVM_MEM_LOG_DIRTY_PAGES) && !new.dirty_bitmap) { [17]
                if (kvm_create_dirty_bitmap(&new) < 0)
                        goto out_free;
        }
  • [14]は書いてあるとおりKVM_MR_CREATE || KVM_MR_MOVEのときの処理.as_idに対応するkvm_memslots構造体中のメンバmemslotsの各kvm_memory_slot構造体についてイテレーションを行い,現在作成,移動しようとしているメモリスロットのマップ先ゲスト物理アドレス空間が既存のメモリスロットのマップ先と重複しないかチェック.重複していたら-EEXISTを返す.
  • [15]でもしnew.flagsにKVM_MEM_LOG_DIRTY_PAGESが含まれていないならnew.dirty_bitmapをクリアしておく.
  • [16]でメモリスロットに対する操作がKVM_MR_CREATEであればnew.userspace_addrにmem->userspace_addrを代入し,kvm_arch_create_memslotをコールする.この関数の内部では基本必要なデータ構造をkvcallocでアロケートしていくので,失敗したら-ENOMEM
  • [17]でもしnew.flagsにKVM_MEM_LOG_DIRTY_PAGESが立っていてかつnew.dirty_bitmapが存在しない状況であればkvzallocでdirty bitmap領域をアロケートする.dirty bitmapはbitmapを用いてdirty trackingを実現するための仕組みであるが,これについて詳細に書くと別方式のdirty ringについても書かなきゃいけないので書かない.内部では基本dirty bitmapの領域のアロケートが行われているので失敗したら-ENOMEM.後でdirty bitmap / ringの記事も作りたいな...

[17]のdirty bitmapについては本稿で解説しないので,[16]のkvm_arch_create_memslotについて詳しく見ていく.

kvm_arch_create_memslot

コード中にlpageという単語が多く出てくるが,これはlarge pageのことである.これだけ頭に留めておくとスッと読める.

kvm_arch_create_memslot in arch/x86/kvm/x86.c l9646

int kvm_arch_create_memslot(struct kvm *kvm, struct kvm_memory_slot *slot,
                            unsigned long npages)
{
        int i;

        for (i = 0; i < KVM_NR_PAGE_SIZES; ++i) { [18]
                struct kvm_lpage_info *linfo;
                unsigned long ugfn;
                int lpages;
                int level = i + 1;

                lpages = gfn_to_index(slot->base_gfn + npages - 1,
                                      slot->base_gfn, level) + 1; [19]

                slot->arch.rmap[i] =
                        kvcalloc(lpages, sizeof(*slot->arch.rmap[i]),
                                 GFP_KERNEL_ACCOUNT); [20]
                if (!slot->arch.rmap[i])
                        goto out_free;

(略)

はじめにKVM_NR_PAGE_SIZES回のループが行われる[18].この定数&関連する定数の定義は以下の通り.

KVM_NR_PAGE_SIZES

KVM_NR_PAGE_SIZES in arch/x86/include/asm/kvm_host.h l104

/* KVM Hugepage definitions for x86 */
enum {
        PT_PAGE_TABLE_LEVEL   = 1,
        PT_DIRECTORY_LEVEL    = 2,
        PT_PDPE_LEVEL         = 3,
        /* set max level to the biggest one */
        PT_MAX_HUGEPAGE_LEVEL = PT_PDPE_LEVEL,
};
#define KVM_NR_PAGE_SIZES       (PT_MAX_HUGEPAGE_LEVEL - \
                                 PT_PAGE_TABLE_LEVEL + 1)

従ってKVM_NUR_PAGE_SIZESの値は3-1+1=3となり,[18]では3回のループが行われる. その後いくつか変数の宣言が行われ,[19]でgfn_to_indexを呼び出され,結果に1を足した値がlpagesに格納される. ここでは各ページサイズを使用したときにゲスト物理アドレス空間を表すために必要なページ数を計算する. gfn_to_indexの詳細を追う.

gfn_to_index

gfn_to_index in arch/x86/include/asm/kvm_host.h l120

gfn_to_indexの定義

static inline gfn_t gfn_to_index(gfn_t gfn, gfn_t base_gfn, int level)
{
        /* KVM_HPAGE_GFN_SHIFT(PT_PAGE_TABLE_LEVEL) must be 0. */
        return (gfn >> KVM_HPAGE_GFN_SHIFT(level)) -
                (base_gfn >> KVM_HPAGE_GFN_SHIFT(level));
}

KVM_HPAGEGFN_SHIFTは以下のように定義される.

#define KVM_HPAGE_GFN_SHIFT(x)  (((x) - 1) * 9)

9でピンと来る人が多いと思うが,gfn >> KVM_HPAGE_GFN_SHIFT()は各ページテーブル構造の階層に応じてフレームナンバをページテーブル構造のインデックスに変換していることになる.これはページサイズに応じて,とも言いかえられる.

e.g.

  • ページテーブル(level=1)内のエントリのインデックス
    • = gfn >> 0 (=((1-1)*9)) -> ページサイズ4KiB (2^12)
  • ページディレクトリ(level=2)内のエントリのインデックス
    • = gfn >> 9 (=((2-1)*9)) -> ページサイズ2MiB(2^12*2^9)
  • ページディレクトリポインタポインタテーブル(level=3)内のエントリのインデックス
    • = gfn >> 18 (=((3-1)*9)) -> ページサイズ1GiB(2^12*2^9*2^9)

その後,[20]でkvm_memory_slot.arch.rmap[i]にlpages*sizeof(*slot->arch.rmap[i])分のメモリを確保している. rmapはgfnに対応するpte_listをページサイズごとに格納するデータ構造であり,kvm_memory_slot構造体のメンバarchであるkvm_arch_memory_slot構造体の中に定義されている. rmapがあることでホストはゲストの使用するページを効率的に追跡できる.例えばゲスト物理メモリ空間はホストから見るとqemuなどのユーザアプリケーションのメモリ空間であるため,当然swap-outされることがある.このとき,kvm側でEPTなどのエントリを編集しなければならないが,rmapがgfn<->pteの参照を可能にしていると,無駄なページウォークなくこれが実現できる.

kvm_arch_memory_slot構造体の定義を見ていく.

struct kvm_arch_memory_slot

kvm_arch_memory_slot in arch/x86/include/asm/kvm_host.h l794

struct kvm_arch_memory_slot {
        struct kvm_rmap_head *rmap[KVM_NR_PAGE_SIZES];
        struct kvm_lpage_info *lpage_info[KVM_NR_PAGE_SIZES - 1];
        unsigned short *gfn_track[KVM_PAGE_TRACK_MAX];
};

kvm_rmap_head in arch/x86/include/asm/kvm_host.h l308

struct kvm_rmap_head {
        unsigned long val;
};

素数KVM_NR_PAGE_SIZESであること,[20]でkvm_memory_slot.arch.rmap[i]にlpages*sizeof(*slot->arch.rmap[i])=lpages*sizeof(struct kvm_rmap_head)分のメモリ空間をアロケートしていることから, rmapは各ページサイズ数分,”メモリスロットがマップするメモリ空間のページ数個の要素を持つkvm_rmap_head構造体の配列”,へのポインタを持つ配列であることがわかる.kvm_rmap_headにはpte_listへのポインタが格納され,gfn<->pteの参照を実現する.

Memo ioctl? unlocked_ioctl? compat_ioctl?

unlocked_ioctlメンバは通常のioctl処理を記載.compat_ioctlメンバには32bit systemcallからの呼び出し時の処理を記載する. 32bit ABIでビルドされたアプリを32bit ABIを有効にしてビルドしたLinuxで実行した際などに使用される.