summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/kvm/x86_64/dirty_log_page_splitting_test.c
blob: ee3b384b991c8be2957bdcf56fa52aa6a4a00a26 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// 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"

#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;
}