// SPDX-License-Identifier: GPL-2.0
/*
* KVM dirty logging page splitting test
*
* Based on dirty_log_perf.c
*
* Copyright (C) 2018, Red Hat, Inc.
* Copyright (C) 2023, Google, Inc.
*/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <linux/bitmap.h>
#include "kvm_util.h"
#include "test_util.h"
#include "memstress.h"
#include "guest_modes.h"
#include "ucall_common.h"
#define VCPUS 2
#define SLOTS 2
#define ITERATIONS 2
static uint64_t guest_percpu_mem_size = DEFAULT_PER_VCPU_MEM_SIZE;
static enum vm_mem_backing_src_type backing_src = VM_MEM_SRC_ANONYMOUS_HUGETLB;
static u64 dirty_log_manual_caps;
static bool host_quit;
static int iteration;
static int vcpu_last_completed_iteration[KVM_MAX_VCPUS];
struct kvm_page_stats {
uint64_t pages_4k;
uint64_t pages_2m;
uint64_t pages_1g;
uint64_t hugepages;
};
static void get_page_stats(struct kvm_vm *vm, struct kvm_page_stats *stats, const char *stage)
{
stats->pages_4k = vm_get_stat(vm, "pages_4k");
stats->pages_2m = vm_get_stat(vm, "pages_2m");
stats->pages_1g = vm_get_stat(vm, "pages_1g");
stats->hugepages = stats->pages_2m + stats->pages_1g;
pr_debug("\nPage stats after %s: 4K: %ld 2M: %ld 1G: %ld huge: %ld\n",
stage, stats->pages_4k, stats->pages_2m, stats->pages_1g,
stats->hugepages);
}
static void run_vcpu_iteration(struct kvm_vm *vm)
{
int i;
iteration++;
for (i = 0; i < VCPUS; i++) {
while (READ_ONCE(vcpu_last_completed_iteration[i]) !=
iteration)
;
}
}
static void vcpu_worker(struct memstress_vcpu_args *vcpu_args)
{
struct kvm_vcpu *vcpu = vcpu_args->vcpu;
int vcpu_idx = vcpu_args->vcpu_idx;
while (!READ_ONCE(host_quit)) {
int current_iteration = READ_ONCE(iteration);
vcpu_run(vcpu);
TEST_ASSERT_EQ(get_ucall(vcpu, NULL), UCALL_SYNC);
vcpu_last_completed_iteration[vcpu_idx] = current_iteration;
/* Wait for the start of the next iteration to be signaled. */
while (current_iteration == READ_ONCE(iteration) &&
READ_ONCE(iteration) >= 0 &&
!READ_ONCE(host_quit))
;
}
}
static void run_test(enum vm_guest_mode mode, void *unused)
{
struct kvm_vm *vm;
unsigned long **bitmaps;
uint64_t guest_num_pages;
uint64_t host_num_pages;
uint64_t pages_per_slot;
int i;
struct kvm_page_stats stats_populated;
struct kvm_page_stats stats_dirty_logging_enabled;
struct kvm_page_stats stats_dirty_pass[ITERATIONS];
struct kvm_page_stats stats_clear_pass[ITERATIONS];
struct kvm_page_stats stats_dirty_logging_disabled;
struct kvm_page_stats stats_repopulated;
vm = memstress_create_vm(mode, VCPUS, guest_percpu_mem_size,
SLOTS, backing_src, false);
guest_num_pages = (VCPUS * guest_percpu_mem_size) >> vm->page_shift;
guest_num_pages = vm_adjust_num_guest_pages(mode, guest_num_pages);
host_num_pages = vm_num_host_pages(mode, guest_num_pages);
pages_per_slot = host_num_pages / SLOTS;
TEST_ASSERT_EQ(host_num_pages, pages_per_slot * SLOTS);
TEST_ASSERT(!(host_num_pages % 512),
"Number of pages, '%lu' not a multiple of 2MiB", host_num_pages);
bitmaps = memstress_alloc_bitmaps(SLOTS, pages_per_slot);
if (dirty_log_manual_caps)
vm_enable_cap(vm, KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2,
dirty_log_manual_caps);
/* Start the iterations */
iteration = -1;
host_quit = false;
for (i = 0; i < VCPUS; i++)
vcpu_last_completed_iteration[i] = -1;
memstress_start_vcpu_threads(VCPUS, vcpu_worker);
run_vcpu_iteration(vm);
get_page_stats(vm, &stats_populated, "populating memory");
/* Enable dirty logging */
memstress_enable_dirty_logging(vm, SLOTS);
get_page_stats(vm, &stats_dirty_logging_enabled, "enabling dirty logging");
while (iteration < ITERATIONS) {
run_vcpu_iteration(vm);
get_page_stats(vm, &stats_dirty_pass[iteration - 1],
"dirtying memory");
memstress_get_dirty_log(vm, bitmaps, SLOTS);
if (dirty_log_manual_caps) {
memstress_clear_dirty_log(vm, bitmaps, SLOTS, pages_per_slot);
get_page_stats(vm, &stats_clear_pass[iteration - 1], "clearing dirty log");
}
}
/* Disable dirty logging */
memstress_disable_dirty_logging(vm, SLOTS);
get_page_stats(vm, &stats_dirty_logging_disabled, "disabling dirty logging");
/* Run vCPUs again to fault pages back in. */
run_vcpu_iteration(vm);
get_page_stats(vm, &stats_repopulated, "repopulating memory");
/*
* Tell the vCPU threads to quit. No need to manually check that vCPUs
* have stopped running after disabling dirty logging, the join will
* wait for them to exit.
*/
host_quit = true;
memstress_join_vcpu_threads(VCPUS);
memstress_free_bitmaps(bitmaps, SLOTS);
memstress_destroy_vm(vm);
TEST_ASSERT_EQ((stats_populated.pages_2m * 512 +
stats_populated.pages_1g * 512 * 512), host_num_pages);
/*
* Check that all huge pages were split. Since large pages can only
* exist in the data slot, and the vCPUs should have dirtied all pages
* in the data slot, there should be no huge pages left after splitting.
* Splitting happens at dirty log enable time without
* KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2 and after the first clear pass
* with that capability.
*/
if (dirty_log_manual_caps) {
TEST_ASSERT_EQ(stats_clear_pass[0].hugepages, 0);
TEST_ASSERT(stats_clear_pass[0].pages_4k >= host_num_pages,
"Expected at least '%lu' 4KiB pages, found only '%lu'",
host_num_pages, stats_clear_pass[0].pages_4k);
TEST_ASSERT_EQ(stats_dirty_logging_enabled.hugepages, stats_populated.hugepages);
} else {
TEST_ASSERT_EQ(stats_dirty_logging_enabled.hugepages, 0);
TEST_ASSERT(stats_dirty_logging_enabled.pages_4k >= host_num_pages,
"Expected at least '%lu' 4KiB pages, found only '%lu'",
host_num_pages, stats_dirty_logging_enabled.pages_4k);
}
/*
* Once dirty logging is disabled and the vCPUs have touched all their
* memory again, the hugepage counts should be the same as they were
* right after initial population of memory.
*/
TEST_ASSERT_EQ(stats_populated.pages_2m, stats_repopulated.pages_2m);
TEST_ASSERT_EQ(stats_populated.pages_1g, stats_repopulated.pages_1g);
}
static void help(char *name)
{
puts("");
printf("usage: %s [-h] [-b vcpu bytes] [-s mem type]\n",
name);
puts("");
printf(" -b: specify the size of the memory region which should be\n"
" dirtied by each vCPU. e.g. 10M or 3G.\n"
" (default: 1G)\n");
backing_src_help("-s");
puts("");
}
int main(int argc, char *argv[])
{
int opt;
TEST_REQUIRE(get_kvm_param_bool("eager_page_split"));
TEST_REQUIRE(get_kvm_param_bool("tdp_mmu"));
while ((opt = getopt(argc, argv, "b:hs:")) != -1) {
switch (opt) {
case 'b':
guest_percpu_mem_size = parse_size(optarg);
break;
case 'h':
help(argv[0]);
exit(0);
case 's':
backing_src = parse_backing_src_type(optarg);
break;
default:
help(argv[0]);
exit(1);
}
}
if (!is_backing_src_hugetlb(backing_src)) {
pr_info("This test will only work reliably with HugeTLB memory. "
"It can work with THP, but that is best effort.\n");
}
guest_modes_append_default();
dirty_log_manual_caps = 0;
for_each_guest_mode(run_test, NULL);
dirty_log_manual_caps =
kvm_check_cap(KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2);
if (dirty_log_manual_caps) {
dirty_log_manual_caps &= (KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE |
KVM_DIRTY_LOG_INITIALLY_SET);
for_each_guest_mode(run_test, NULL);
} else {
pr_info("Skipping testing with MANUAL_PROTECT as it is not supported");
}
return 0;
}