1 // SPDX-License-Identifier: GPL-2.0 2 /* 3 * access_tracking_perf_test 4 * 5 * Copyright (C) 2021, Google, Inc. 6 * 7 * This test measures the performance effects of KVM's access tracking. 8 * Access tracking is driven by the MMU notifiers test_young, clear_young, and 9 * clear_flush_young. These notifiers do not have a direct userspace API, 10 * however the clear_young notifier can be triggered by marking a pages as idle 11 * in /sys/kernel/mm/page_idle/bitmap. This test leverages that mechanism to 12 * enable access tracking on guest memory. 13 * 14 * To measure performance this test runs a VM with a configurable number of 15 * vCPUs that each touch every page in disjoint regions of memory. Performance 16 * is measured in the time it takes all vCPUs to finish touching their 17 * predefined region. 18 * 19 * Note that a deterministic correctness test of access tracking is not possible 20 * by using page_idle as it exists today. This is for a few reasons: 21 * 22 * 1. page_idle only issues clear_young notifiers, which lack a TLB flush. This 23 * means subsequent guest accesses are not guaranteed to see page table 24 * updates made by KVM until some time in the future. 25 * 26 * 2. page_idle only operates on LRU pages. Newly allocated pages are not 27 * immediately allocated to LRU lists. Instead they are held in a "pagevec", 28 * which is drained to LRU lists some time in the future. There is no 29 * userspace API to force this drain to occur. 30 * 31 * These limitations are worked around in this test by using a large enough 32 * region of memory for each vCPU such that the number of translations cached in 33 * the TLB and the number of pages held in pagevecs are a small fraction of the 34 * overall workload. And if either of those conditions are not true this test 35 * will fail rather than silently passing. 36 */ 37 #include <inttypes.h> 38 #include <limits.h> 39 #include <pthread.h> 40 #include <sys/mman.h> 41 #include <sys/types.h> 42 #include <sys/stat.h> 43 44 #include "kvm_util.h" 45 #include "test_util.h" 46 #include "perf_test_util.h" 47 #include "guest_modes.h" 48 49 /* Global variable used to synchronize all of the vCPU threads. */ 50 static int iteration = -1; 51 52 /* Defines what vCPU threads should do during a given iteration. */ 53 static enum { 54 /* Run the vCPU to access all its memory. */ 55 ITERATION_ACCESS_MEMORY, 56 /* Mark the vCPU's memory idle in page_idle. */ 57 ITERATION_MARK_IDLE, 58 } iteration_work; 59 60 /* Set to true when vCPU threads should exit. */ 61 static bool done; 62 63 /* The iteration that was last completed by each vCPU. */ 64 static int vcpu_last_completed_iteration[KVM_MAX_VCPUS]; 65 66 /* Whether to overlap the regions of memory vCPUs access. */ 67 static bool overlap_memory_access; 68 69 struct test_params { 70 /* The backing source for the region of memory. */ 71 enum vm_mem_backing_src_type backing_src; 72 73 /* The amount of memory to allocate for each vCPU. */ 74 uint64_t vcpu_memory_bytes; 75 76 /* The number of vCPUs to create in the VM. */ 77 int vcpus; 78 }; 79 80 static uint64_t pread_uint64(int fd, const char *filename, uint64_t index) 81 { 82 uint64_t value; 83 off_t offset = index * sizeof(value); 84 85 TEST_ASSERT(pread(fd, &value, sizeof(value), offset) == sizeof(value), 86 "pread from %s offset 0x%" PRIx64 " failed!", 87 filename, offset); 88 89 return value; 90 91 } 92 93 #define PAGEMAP_PRESENT (1ULL << 63) 94 #define PAGEMAP_PFN_MASK ((1ULL << 55) - 1) 95 96 static uint64_t lookup_pfn(int pagemap_fd, struct kvm_vm *vm, uint64_t gva) 97 { 98 uint64_t hva = (uint64_t) addr_gva2hva(vm, gva); 99 uint64_t entry; 100 uint64_t pfn; 101 102 entry = pread_uint64(pagemap_fd, "pagemap", hva / getpagesize()); 103 if (!(entry & PAGEMAP_PRESENT)) 104 return 0; 105 106 pfn = entry & PAGEMAP_PFN_MASK; 107 if (!pfn) { 108 print_skip("Looking up PFNs requires CAP_SYS_ADMIN"); 109 exit(KSFT_SKIP); 110 } 111 112 return pfn; 113 } 114 115 static bool is_page_idle(int page_idle_fd, uint64_t pfn) 116 { 117 uint64_t bits = pread_uint64(page_idle_fd, "page_idle", pfn / 64); 118 119 return !!((bits >> (pfn % 64)) & 1); 120 } 121 122 static void mark_page_idle(int page_idle_fd, uint64_t pfn) 123 { 124 uint64_t bits = 1ULL << (pfn % 64); 125 126 TEST_ASSERT(pwrite(page_idle_fd, &bits, 8, 8 * (pfn / 64)) == 8, 127 "Set page_idle bits for PFN 0x%" PRIx64, pfn); 128 } 129 130 static void mark_vcpu_memory_idle(struct kvm_vm *vm, int vcpu_id) 131 { 132 uint64_t base_gva = perf_test_args.vcpu_args[vcpu_id].gva; 133 uint64_t pages = perf_test_args.vcpu_args[vcpu_id].pages; 134 uint64_t page; 135 uint64_t still_idle = 0; 136 uint64_t no_pfn = 0; 137 int page_idle_fd; 138 int pagemap_fd; 139 140 /* If vCPUs are using an overlapping region, let vCPU 0 mark it idle. */ 141 if (overlap_memory_access && vcpu_id) 142 return; 143 144 page_idle_fd = open("/sys/kernel/mm/page_idle/bitmap", O_RDWR); 145 TEST_ASSERT(page_idle_fd > 0, "Failed to open page_idle."); 146 147 pagemap_fd = open("/proc/self/pagemap", O_RDONLY); 148 TEST_ASSERT(pagemap_fd > 0, "Failed to open pagemap."); 149 150 for (page = 0; page < pages; page++) { 151 uint64_t gva = base_gva + page * perf_test_args.guest_page_size; 152 uint64_t pfn = lookup_pfn(pagemap_fd, vm, gva); 153 154 if (!pfn) { 155 no_pfn++; 156 continue; 157 } 158 159 if (is_page_idle(page_idle_fd, pfn)) { 160 still_idle++; 161 continue; 162 } 163 164 mark_page_idle(page_idle_fd, pfn); 165 } 166 167 /* 168 * Assumption: Less than 1% of pages are going to be swapped out from 169 * under us during this test. 170 */ 171 TEST_ASSERT(no_pfn < pages / 100, 172 "vCPU %d: No PFN for %" PRIu64 " out of %" PRIu64 " pages.", 173 vcpu_id, no_pfn, pages); 174 175 /* 176 * Test that at least 90% of memory has been marked idle (the rest might 177 * not be marked idle because the pages have not yet made it to an LRU 178 * list or the translations are still cached in the TLB). 90% is 179 * arbitrary; high enough that we ensure most memory access went through 180 * access tracking but low enough as to not make the test too brittle 181 * over time and across architectures. 182 */ 183 TEST_ASSERT(still_idle < pages / 10, 184 "vCPU%d: Too many pages still idle (%"PRIu64 " out of %" 185 PRIu64 ").\n", 186 vcpu_id, still_idle, pages); 187 188 close(page_idle_fd); 189 close(pagemap_fd); 190 } 191 192 static void assert_ucall(struct kvm_vm *vm, uint32_t vcpu_id, 193 uint64_t expected_ucall) 194 { 195 struct ucall uc; 196 uint64_t actual_ucall = get_ucall(vm, vcpu_id, &uc); 197 198 TEST_ASSERT(expected_ucall == actual_ucall, 199 "Guest exited unexpectedly (expected ucall %" PRIu64 200 ", got %" PRIu64 ")", 201 expected_ucall, actual_ucall); 202 } 203 204 static bool spin_wait_for_next_iteration(int *current_iteration) 205 { 206 int last_iteration = *current_iteration; 207 208 do { 209 if (READ_ONCE(done)) 210 return false; 211 212 *current_iteration = READ_ONCE(iteration); 213 } while (last_iteration == *current_iteration); 214 215 return true; 216 } 217 218 static void *vcpu_thread_main(void *arg) 219 { 220 struct perf_test_vcpu_args *vcpu_args = arg; 221 struct kvm_vm *vm = perf_test_args.vm; 222 int vcpu_id = vcpu_args->vcpu_id; 223 int current_iteration = -1; 224 225 vcpu_args_set(vm, vcpu_id, 1, vcpu_id); 226 227 while (spin_wait_for_next_iteration(¤t_iteration)) { 228 switch (READ_ONCE(iteration_work)) { 229 case ITERATION_ACCESS_MEMORY: 230 vcpu_run(vm, vcpu_id); 231 assert_ucall(vm, vcpu_id, UCALL_SYNC); 232 break; 233 case ITERATION_MARK_IDLE: 234 mark_vcpu_memory_idle(vm, vcpu_id); 235 break; 236 }; 237 238 vcpu_last_completed_iteration[vcpu_id] = current_iteration; 239 } 240 241 return NULL; 242 } 243 244 static void spin_wait_for_vcpu(int vcpu_id, int target_iteration) 245 { 246 while (READ_ONCE(vcpu_last_completed_iteration[vcpu_id]) != 247 target_iteration) { 248 continue; 249 } 250 } 251 252 /* The type of memory accesses to perform in the VM. */ 253 enum access_type { 254 ACCESS_READ, 255 ACCESS_WRITE, 256 }; 257 258 static void run_iteration(struct kvm_vm *vm, int vcpus, const char *description) 259 { 260 struct timespec ts_start; 261 struct timespec ts_elapsed; 262 int next_iteration; 263 int vcpu_id; 264 265 /* Kick off the vCPUs by incrementing iteration. */ 266 next_iteration = ++iteration; 267 268 clock_gettime(CLOCK_MONOTONIC, &ts_start); 269 270 /* Wait for all vCPUs to finish the iteration. */ 271 for (vcpu_id = 0; vcpu_id < vcpus; vcpu_id++) 272 spin_wait_for_vcpu(vcpu_id, next_iteration); 273 274 ts_elapsed = timespec_elapsed(ts_start); 275 pr_info("%-30s: %ld.%09lds\n", 276 description, ts_elapsed.tv_sec, ts_elapsed.tv_nsec); 277 } 278 279 static void access_memory(struct kvm_vm *vm, int vcpus, enum access_type access, 280 const char *description) 281 { 282 perf_test_args.wr_fract = (access == ACCESS_READ) ? INT_MAX : 1; 283 sync_global_to_guest(vm, perf_test_args); 284 iteration_work = ITERATION_ACCESS_MEMORY; 285 run_iteration(vm, vcpus, description); 286 } 287 288 static void mark_memory_idle(struct kvm_vm *vm, int vcpus) 289 { 290 /* 291 * Even though this parallelizes the work across vCPUs, this is still a 292 * very slow operation because page_idle forces the test to mark one pfn 293 * at a time and the clear_young notifier serializes on the KVM MMU 294 * lock. 295 */ 296 pr_debug("Marking VM memory idle (slow)...\n"); 297 iteration_work = ITERATION_MARK_IDLE; 298 run_iteration(vm, vcpus, "Mark memory idle"); 299 } 300 301 static pthread_t *create_vcpu_threads(int vcpus) 302 { 303 pthread_t *vcpu_threads; 304 int i; 305 306 vcpu_threads = malloc(vcpus * sizeof(vcpu_threads[0])); 307 TEST_ASSERT(vcpu_threads, "Failed to allocate vcpu_threads."); 308 309 for (i = 0; i < vcpus; i++) { 310 vcpu_last_completed_iteration[i] = iteration; 311 pthread_create(&vcpu_threads[i], NULL, vcpu_thread_main, 312 &perf_test_args.vcpu_args[i]); 313 } 314 315 return vcpu_threads; 316 } 317 318 static void terminate_vcpu_threads(pthread_t *vcpu_threads, int vcpus) 319 { 320 int i; 321 322 /* Set done to signal the vCPU threads to exit */ 323 done = true; 324 325 for (i = 0; i < vcpus; i++) 326 pthread_join(vcpu_threads[i], NULL); 327 } 328 329 static void run_test(enum vm_guest_mode mode, void *arg) 330 { 331 struct test_params *params = arg; 332 struct kvm_vm *vm; 333 pthread_t *vcpu_threads; 334 int vcpus = params->vcpus; 335 336 vm = perf_test_create_vm(mode, vcpus, params->vcpu_memory_bytes, 337 params->backing_src); 338 339 perf_test_setup_vcpus(vm, vcpus, params->vcpu_memory_bytes, 340 !overlap_memory_access); 341 342 vcpu_threads = create_vcpu_threads(vcpus); 343 344 pr_info("\n"); 345 access_memory(vm, vcpus, ACCESS_WRITE, "Populating memory"); 346 347 /* As a control, read and write to the populated memory first. */ 348 access_memory(vm, vcpus, ACCESS_WRITE, "Writing to populated memory"); 349 access_memory(vm, vcpus, ACCESS_READ, "Reading from populated memory"); 350 351 /* Repeat on memory that has been marked as idle. */ 352 mark_memory_idle(vm, vcpus); 353 access_memory(vm, vcpus, ACCESS_WRITE, "Writing to idle memory"); 354 mark_memory_idle(vm, vcpus); 355 access_memory(vm, vcpus, ACCESS_READ, "Reading from idle memory"); 356 357 terminate_vcpu_threads(vcpu_threads, vcpus); 358 free(vcpu_threads); 359 perf_test_destroy_vm(vm); 360 } 361 362 static void help(char *name) 363 { 364 puts(""); 365 printf("usage: %s [-h] [-m mode] [-b vcpu_bytes] [-v vcpus] [-o] [-s mem_type]\n", 366 name); 367 puts(""); 368 printf(" -h: Display this help message."); 369 guest_modes_help(); 370 printf(" -b: specify the size of the memory region which should be\n" 371 " dirtied by each vCPU. e.g. 10M or 3G.\n" 372 " (default: 1G)\n"); 373 printf(" -v: specify the number of vCPUs to run.\n"); 374 printf(" -o: Overlap guest memory accesses instead of partitioning\n" 375 " them into a separate region of memory for each vCPU.\n"); 376 printf(" -s: specify the type of memory that should be used to\n" 377 " back the guest data region.\n\n"); 378 backing_src_help(); 379 puts(""); 380 exit(0); 381 } 382 383 int main(int argc, char *argv[]) 384 { 385 struct test_params params = { 386 .backing_src = VM_MEM_SRC_ANONYMOUS, 387 .vcpu_memory_bytes = DEFAULT_PER_VCPU_MEM_SIZE, 388 .vcpus = 1, 389 }; 390 int page_idle_fd; 391 int opt; 392 393 guest_modes_append_default(); 394 395 while ((opt = getopt(argc, argv, "hm:b:v:os:")) != -1) { 396 switch (opt) { 397 case 'm': 398 guest_modes_cmdline(optarg); 399 break; 400 case 'b': 401 params.vcpu_memory_bytes = parse_size(optarg); 402 break; 403 case 'v': 404 params.vcpus = atoi(optarg); 405 break; 406 case 'o': 407 overlap_memory_access = true; 408 break; 409 case 's': 410 params.backing_src = parse_backing_src_type(optarg); 411 break; 412 case 'h': 413 default: 414 help(argv[0]); 415 break; 416 } 417 } 418 419 page_idle_fd = open("/sys/kernel/mm/page_idle/bitmap", O_RDWR); 420 if (page_idle_fd < 0) { 421 print_skip("CONFIG_IDLE_PAGE_TRACKING is not enabled"); 422 exit(KSFT_SKIP); 423 } 424 close(page_idle_fd); 425 426 for_each_guest_mode(run_test, ¶ms); 427 428 return 0; 429 } 430