Loading...
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 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 | #include <stdio.h> #include <mach/mach_vm.h> #include <mach/mach_port.h> #include <mach/mach_host.h> #include <mach-o/dyld.h> #include <sys/sysctl.h> #include <sys/kdebug.h> #include <sys/mman.h> #include <sys/kern_memorystatus.h> #include <ktrace/session.h> #include <dispatch/private.h> #ifdef T_NAMESPACE #undef T_NAMESPACE #endif #include <darwintest.h> #include <darwintest_utils.h> T_GLOBAL_META( T_META_NAMESPACE("xnu.vm"), T_META_CHECK_LEAKS(false) ); #define TIMEOUT_SECS 1500 #if TARGET_OS_EMBEDDED #define ALLOCATION_SIZE_VM_REGION (16*1024) /* 16 KB */ #define ALLOCATION_SIZE_VM_OBJECT ALLOCATION_SIZE_VM_REGION #else #define ALLOCATION_SIZE_VM_REGION (1024*1024*100) /* 100 MB */ #define ALLOCATION_SIZE_VM_OBJECT (16*1024) /* 16 KB */ #endif #define MAX_CHILD_PROCS 100 #define ZONEMAP_JETSAM_LIMIT_SYSCTL "kern.zone_map_jetsam_limit=60" #define VME_ZONE_TEST_OPT "allocate_vm_regions" #define VM_OBJECTS_ZONE_TEST_OPT "allocate_vm_objects" #define GENERIC_ZONE_TEST_OPT "allocate_from_generic_zone" #define VM_TAG1 100 #define VM_TAG2 101 enum { VME_ZONE_TEST = 0, VM_OBJECTS_ZONE_TEST, GENERIC_ZONE_TEST, }; static int current_test_index = 0; static int num_children = 0; static bool test_ending = false; static bool within_dispatch_source_handler = false; static dispatch_source_t ds_signal = NULL; static ktrace_session_t session = NULL; static char testpath[PATH_MAX]; static pid_t child_pids[MAX_CHILD_PROCS]; static pthread_mutex_t test_ending_mtx; static void allocate_vm_regions(void); static void allocate_vm_objects(void); static void allocate_from_generic_zone(void); static void cleanup_and_end_test(void); static void setup_ktrace_session(void); static void spawn_child_process(void); static void run_test_for_zone(int index); extern void mach_zone_force_gc(host_t host); static void allocate_vm_regions(void) { uint64_t alloc_size = ALLOCATION_SIZE_VM_REGION, i = 0; printf("[%d] Allocating VM regions, each of size %lld KB\n", getpid(), (alloc_size>>10)); for (i = 0; ; i++) { mach_vm_address_t addr = (mach_vm_address_t)NULL; /* Alternate VM tags between consecutive regions to prevent coalescing */ int flags = VM_MAKE_TAG((i % 2)? VM_TAG1: VM_TAG2) | VM_FLAGS_ANYWHERE; if ((mach_vm_allocate(mach_task_self(), &addr, (mach_vm_size_t)alloc_size, flags)) != KERN_SUCCESS) { break; } } printf("[%d] Number of allocations: %lld\n", getpid(), i); /* Signal to the parent that we're done allocating */ kill(getppid(), SIGUSR1); while (1) { pause(); } } static void allocate_vm_objects(void) { uint64_t alloc_size = ALLOCATION_SIZE_VM_OBJECT, i = 0; printf("[%d] Allocating VM regions, each of size %lld KB, each backed by a VM object\n", getpid(), (alloc_size>>10)); for (i = 0; ; i++) { mach_vm_address_t addr = (mach_vm_address_t)NULL; /* Alternate VM tags between consecutive regions to prevent coalescing */ int flags = VM_MAKE_TAG((i % 2)? VM_TAG1: VM_TAG2) | VM_FLAGS_ANYWHERE; if ((mach_vm_allocate(mach_task_self(), &addr, (mach_vm_size_t)alloc_size, flags)) != KERN_SUCCESS) { break; } /* Touch the region so the VM object can actually be created */ *((int *)addr) = 0; /* OK to free this page. Keeps us from holding a lot of dirty pages */ madvise((void *)addr, (size_t)alloc_size, MADV_FREE); } printf("[%d] Number of allocations: %lld\n", getpid(), i); /* Signal to the parent that we're done allocating */ kill(getppid(), SIGUSR1); while (1) { pause(); } } static void allocate_from_generic_zone(void) { uint64_t i = 0; printf("[%d] Allocating mach_ports\n", getpid()); for (i = 0; ; i++) { mach_port_t port; if ((mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port)) != KERN_SUCCESS) { break; } } printf("[%d] Number of allocations: %lld\n", getpid(), i); /* Signal to the parent that we're done allocating */ kill(getppid(), SIGUSR1); while (1) { pause(); } } static void cleanup_and_end_test(void) { int i; /* * The atend handler executes on a different dispatch queue. * We want to do the cleanup only once. */ pthread_mutex_lock(&test_ending_mtx); if (test_ending) { pthread_mutex_unlock(&test_ending_mtx); return; } test_ending = true; pthread_mutex_unlock(&test_ending_mtx); T_LOG("Number of processes spawned: %d", num_children); T_LOG("Cleaning up..."); /* Disable signal handler that spawns child processes, only if we're not in the event handler's context */ if (ds_signal != NULL && !within_dispatch_source_handler) { dispatch_source_cancel_and_wait(ds_signal); } /* Kill all the child processes that were spawned */ for (i = 0; i < num_children; i++) { kill(child_pids[i], SIGKILL); } for (i = 0; i < num_children; i++) { int status = 0; if (waitpid(child_pids[i], &status, 0) < 0) { T_LOG("waitpid returned status %d", status); } } sleep(1); /* Force zone_gc before starting test for another zone or exiting */ mach_zone_force_gc(mach_host_self()); /* End ktrace session */ if (session != NULL) { ktrace_end(session, 1); } } static void setup_ktrace_session(void) { int ret = 0; T_LOG("Setting up ktrace session..."); session = ktrace_session_create(); T_QUIET; T_ASSERT_NOTNULL(session, "ktrace_session_create"); ktrace_set_completion_handler(session, ^{ ktrace_session_destroy(session); T_END; }); /* Listen for memorystatus_do_kill trace events */ ret = ktrace_events_single(session, (BSDDBG_CODE(DBG_BSD_MEMSTAT, BSD_MEMSTAT_DO_KILL)) | DBG_FUNC_END, ^(ktrace_event_t event) { int i; bool received_jetsam_event = false; /* We don't care about jetsams for any other reason except zone-map-exhaustion */ if (event->arg2 == kMemorystatusKilledZoneMapExhaustion) { T_LOG("[memorystatus_do_kill] jetsam reason: zone-map-exhaustion, pid: %lu", event->arg1); if (current_test_index == VME_ZONE_TEST || current_test_index == VM_OBJECTS_ZONE_TEST) { /* * For the VM map entries zone we try to kill the leaking process. * Verify that we jetsammed one of the processes we spawned. */ for (i = 0; i < num_children; i++) { if (child_pids[i] == (pid_t)event->arg1) { received_jetsam_event = true; break; } } } else { received_jetsam_event = true; } T_ASSERT_TRUE(received_jetsam_event, "Received jetsam event as expected"); cleanup_and_end_test(); } }); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_events_single"); ret = ktrace_start(session, dispatch_get_main_queue()); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_start"); } static void spawn_child_process(void) { pid_t pid = -1; char *launch_tool_args[4]; within_dispatch_source_handler = true; T_QUIET; T_ASSERT_LT(num_children, MAX_CHILD_PROCS, "Spawned %d children. Timing out...", MAX_CHILD_PROCS); launch_tool_args[0] = testpath; launch_tool_args[1] = "-n"; launch_tool_args[3] = NULL; if (current_test_index == VME_ZONE_TEST) { launch_tool_args[2] = VME_ZONE_TEST_OPT; } else if (current_test_index == VM_OBJECTS_ZONE_TEST) { launch_tool_args[2] = VM_OBJECTS_ZONE_TEST_OPT; } else if (current_test_index == GENERIC_ZONE_TEST) { launch_tool_args[2] = GENERIC_ZONE_TEST_OPT; } /* Spawn the child process */ int rc = dt_launch_tool(&pid, launch_tool_args, false, NULL, NULL); if (rc != 0) { T_LOG("dt_launch tool returned %d with error code %d", rc, errno); } T_QUIET; T_ASSERT_POSIX_SUCCESS(pid, "dt_launch_tool"); child_pids[num_children++] = pid; within_dispatch_source_handler = false; } static void run_test_for_zone(int index) { int ret, dev; size_t dev_size = sizeof(dev); uint32_t testpath_buf_size = sizeof(testpath); T_ATEND(cleanup_and_end_test); T_SETUPBEGIN; current_test_index = index; ret = sysctlbyname("kern.development", &dev, &dev_size, NULL, 0); T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "sysctl kern.development failed"); if (dev == 0) { T_SKIP("Skipping test on release kernel"); } ret = _NSGetExecutablePath(testpath, &testpath_buf_size); T_QUIET; T_ASSERT_POSIX_ZERO(ret, "_NSGetExecutablePath"); T_LOG("Executable path: %s", testpath); /* * If the timeout specified by T_META_TIMEOUT is hit, the atend handler does not get called. * So we're queueing a dispatch block to fire after TIMEOUT_SECS seconds, so we can exit cleanly. */ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, TIMEOUT_SECS * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ T_ASSERT_FAIL("Timed out after %d seconds", TIMEOUT_SECS); }); /* * Create a dispatch source for the signal SIGUSR1. When a child is done allocating zone memory, it * sends SIGUSR1 to the parent. Only then does the parent spawn another child. This prevents us from * spawning many children at once and creating a lot of memory pressure. */ signal(SIGUSR1, SIG_IGN); ds_signal = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGUSR1, 0, dispatch_get_main_queue()); T_QUIET; T_ASSERT_NOTNULL(ds_signal, "dispatch_source_create"); dispatch_source_set_event_handler(ds_signal, ^{ /* Wait a few seconds before spawning another child. Keeps us from allocating too aggressively */ sleep(5); spawn_child_process(); }); dispatch_activate(ds_signal); /* Set up a ktrace session to listen for jetsam events */ setup_ktrace_session(); T_SETUPEND; /* Spawn the first child process */ T_LOG("Spawning child processes to allocate zone memory...\n\n"); spawn_child_process(); dispatch_main(); } T_HELPER_DECL(allocate_vm_regions, "allocates VM regions") { allocate_vm_regions(); } T_HELPER_DECL(allocate_vm_objects, "allocates VM objects and VM regions") { allocate_vm_objects(); } T_HELPER_DECL(allocate_from_generic_zone, "allocates from a generic zone") { memorystatus_priority_properties_t props; /* * We want to move the processes we spawn into the idle band, so that jetsam can target them first. * This prevents other important BATS tasks from getting killed, specially in LTE where we have very few * processes running. */ props.priority = JETSAM_PRIORITY_IDLE; props.user_data = 0; if (memorystatus_control(MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES, getpid(), 0, &props, sizeof(props))) { printf("memorystatus call to change jetsam priority failed\n"); exit(-1); } allocate_from_generic_zone(); } /* * T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL) changes the zone_map_jetsam_limit to a * lower value, so that the test can complete faster. * The test allocates zone memory pretty aggressively which can cause the system to panic * if the jetsam limit is quite high; a lower value keeps us from panicking. */ T_DECL( memorystatus_vme_zone_test, "allocates elements from the VM map entries zone, verifies zone-map-exhaustion jetsams", T_META_ASROOT(true), T_META_TIMEOUT(1800), /* T_META_LTEPHASE(LTE_POSTINIT), */ T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL)) { run_test_for_zone(VME_ZONE_TEST); } T_DECL( memorystatus_vm_objects_zone_test, "allocates elements from the VM objects and the VM map entries zones, verifies zone-map-exhaustion jetsams", T_META_ASROOT(true), T_META_TIMEOUT(1800), /* T_META_LTEPHASE(LTE_POSTINIT), */ T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL)) { run_test_for_zone(VM_OBJECTS_ZONE_TEST); } T_DECL( memorystatus_generic_zone_test, "allocates elements from a zone that doesn't have an optimized jetsam path, verifies zone-map-exhaustion jetsams", T_META_ASROOT(true), T_META_TIMEOUT(1800), /* T_META_LTEPHASE(LTE_POSTINIT), */ T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL)) { run_test_for_zone(GENERIC_ZONE_TEST); } |