]>
Commit | Line | Data |
---|---|---|
1 | #include <stdio.h> | |
2 | #include <mach/mach_vm.h> | |
3 | #include <mach/mach_port.h> | |
4 | #include <mach/mach_host.h> | |
5 | #include <mach-o/dyld.h> | |
6 | #include <sys/sysctl.h> | |
7 | #include <sys/kdebug.h> | |
8 | #include <sys/mman.h> | |
9 | #include <sys/kern_memorystatus.h> | |
10 | #include <ktrace/session.h> | |
11 | #include <dispatch/private.h> | |
12 | ||
13 | #ifdef T_NAMESPACE | |
14 | #undef T_NAMESPACE | |
15 | #endif | |
16 | #include <darwintest.h> | |
17 | #include <darwintest_utils.h> | |
18 | ||
19 | T_GLOBAL_META( | |
20 | T_META_NAMESPACE("xnu.vm"), | |
21 | T_META_CHECK_LEAKS(false) | |
22 | ); | |
23 | ||
24 | #define TIMEOUT_SECS 1500 | |
25 | ||
26 | #if TARGET_OS_EMBEDDED | |
27 | #define ALLOCATION_SIZE_VM_REGION (16*1024) /* 16 KB */ | |
28 | #define ALLOCATION_SIZE_VM_OBJECT ALLOCATION_SIZE_VM_REGION | |
29 | #else | |
30 | #define ALLOCATION_SIZE_VM_REGION (1024*1024*100) /* 100 MB */ | |
31 | #define ALLOCATION_SIZE_VM_OBJECT (16*1024) /* 16 KB */ | |
32 | #endif | |
33 | #define MAX_CHILD_PROCS 100 | |
34 | ||
35 | #define ZONEMAP_JETSAM_LIMIT_SYSCTL "kern.zone_map_jetsam_limit=60" | |
36 | ||
37 | #define VME_ZONE_TEST_OPT "allocate_vm_regions" | |
38 | #define VM_OBJECTS_ZONE_TEST_OPT "allocate_vm_objects" | |
39 | #define GENERIC_ZONE_TEST_OPT "allocate_from_generic_zone" | |
40 | ||
41 | #define VM_TAG1 100 | |
42 | #define VM_TAG2 101 | |
43 | ||
44 | enum { | |
45 | VME_ZONE_TEST = 0, | |
46 | VM_OBJECTS_ZONE_TEST, | |
47 | GENERIC_ZONE_TEST, | |
48 | }; | |
49 | ||
50 | static int current_test_index = 0; | |
51 | static int num_children = 0; | |
52 | static bool test_ending = false; | |
53 | static bool within_dispatch_source_handler = false; | |
54 | static dispatch_source_t ds_signal = NULL; | |
55 | static ktrace_session_t session = NULL; | |
56 | ||
57 | static char testpath[PATH_MAX]; | |
58 | static pid_t child_pids[MAX_CHILD_PROCS]; | |
59 | static pthread_mutex_t test_ending_mtx; | |
60 | ||
61 | static void allocate_vm_regions(void); | |
62 | static void allocate_vm_objects(void); | |
63 | static void allocate_from_generic_zone(void); | |
64 | static void cleanup_and_end_test(void); | |
65 | static void setup_ktrace_session(void); | |
66 | static void spawn_child_process(void); | |
67 | static void run_test_for_zone(int index); | |
68 | ||
69 | extern void mach_zone_force_gc(host_t host); | |
70 | ||
71 | static void allocate_vm_regions(void) | |
72 | { | |
73 | uint64_t alloc_size = ALLOCATION_SIZE_VM_REGION, i = 0; | |
74 | ||
75 | printf("[%d] Allocating VM regions, each of size %lld KB\n", getpid(), (alloc_size>>10)); | |
76 | for (i = 0; ; i++) { | |
77 | mach_vm_address_t addr = (mach_vm_address_t)NULL; | |
78 | ||
79 | /* Alternate VM tags between consecutive regions to prevent coalescing */ | |
80 | int flags = VM_MAKE_TAG((i % 2)? VM_TAG1: VM_TAG2) | VM_FLAGS_ANYWHERE; | |
81 | ||
82 | if ((mach_vm_allocate(mach_task_self(), &addr, (mach_vm_size_t)alloc_size, flags)) != KERN_SUCCESS) { | |
83 | break; | |
84 | } | |
85 | } | |
86 | printf("[%d] Number of allocations: %lld\n", getpid(), i); | |
87 | ||
88 | /* Signal to the parent that we're done allocating */ | |
89 | kill(getppid(), SIGUSR1); | |
90 | ||
91 | while (1) { | |
92 | pause(); | |
93 | } | |
94 | } | |
95 | ||
96 | static void allocate_vm_objects(void) | |
97 | { | |
98 | uint64_t alloc_size = ALLOCATION_SIZE_VM_OBJECT, i = 0; | |
99 | ||
100 | printf("[%d] Allocating VM regions, each of size %lld KB, each backed by a VM object\n", getpid(), (alloc_size>>10)); | |
101 | for (i = 0; ; i++) { | |
102 | mach_vm_address_t addr = (mach_vm_address_t)NULL; | |
103 | ||
104 | /* Alternate VM tags between consecutive regions to prevent coalescing */ | |
105 | int flags = VM_MAKE_TAG((i % 2)? VM_TAG1: VM_TAG2) | VM_FLAGS_ANYWHERE; | |
106 | ||
107 | if ((mach_vm_allocate(mach_task_self(), &addr, (mach_vm_size_t)alloc_size, flags)) != KERN_SUCCESS) { | |
108 | break; | |
109 | } | |
110 | /* Touch the region so the VM object can actually be created */ | |
111 | *((int *)addr) = 0; | |
112 | /* OK to free this page. Keeps us from holding a lot of dirty pages */ | |
113 | madvise((void *)addr, (size_t)alloc_size, MADV_FREE); | |
114 | } | |
115 | printf("[%d] Number of allocations: %lld\n", getpid(), i); | |
116 | ||
117 | /* Signal to the parent that we're done allocating */ | |
118 | kill(getppid(), SIGUSR1); | |
119 | ||
120 | while (1) { | |
121 | pause(); | |
122 | } | |
123 | } | |
124 | ||
125 | static void allocate_from_generic_zone(void) | |
126 | { | |
127 | uint64_t i = 0; | |
128 | ||
129 | printf("[%d] Allocating mach_ports\n", getpid()); | |
130 | for (i = 0; ; i++) { | |
131 | mach_port_t port; | |
132 | ||
133 | if ((mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port)) != KERN_SUCCESS) { | |
134 | break; | |
135 | } | |
136 | } | |
137 | printf("[%d] Number of allocations: %lld\n", getpid(), i); | |
138 | ||
139 | /* Signal to the parent that we're done allocating */ | |
140 | kill(getppid(), SIGUSR1); | |
141 | ||
142 | while (1) { | |
143 | pause(); | |
144 | } | |
145 | } | |
146 | ||
147 | static void cleanup_and_end_test(void) | |
148 | { | |
149 | int i; | |
150 | ||
151 | /* | |
152 | * The atend handler executes on a different dispatch queue. | |
153 | * We want to do the cleanup only once. | |
154 | */ | |
155 | pthread_mutex_lock(&test_ending_mtx); | |
156 | if (test_ending) { | |
157 | pthread_mutex_unlock(&test_ending_mtx); | |
158 | return; | |
159 | } | |
160 | test_ending = true; | |
161 | pthread_mutex_unlock(&test_ending_mtx); | |
162 | ||
163 | T_LOG("Number of processes spawned: %d", num_children); | |
164 | T_LOG("Cleaning up..."); | |
165 | ||
166 | /* Disable signal handler that spawns child processes, only if we're not in the event handler's context */ | |
167 | if (ds_signal != NULL && !within_dispatch_source_handler) { | |
168 | dispatch_source_cancel_and_wait(ds_signal); | |
169 | } | |
170 | ||
171 | /* Kill all the child processes that were spawned */ | |
172 | for (i = 0; i < num_children; i++) { | |
173 | kill(child_pids[i], SIGKILL); | |
174 | } | |
175 | for (i = 0; i < num_children; i++) { | |
176 | int status = 0; | |
177 | if (waitpid(child_pids[i], &status, 0) < 0) { | |
178 | T_LOG("waitpid returned status %d", status); | |
179 | } | |
180 | } | |
181 | sleep(1); | |
182 | ||
183 | /* Force zone_gc before starting test for another zone or exiting */ | |
184 | mach_zone_force_gc(mach_host_self()); | |
185 | ||
186 | /* End ktrace session */ | |
187 | if (session != NULL) { | |
188 | ktrace_end(session, 1); | |
189 | } | |
190 | } | |
191 | ||
192 | static void setup_ktrace_session(void) | |
193 | { | |
194 | int ret = 0; | |
195 | ||
196 | T_LOG("Setting up ktrace session..."); | |
197 | session = ktrace_session_create(); | |
198 | T_QUIET; T_ASSERT_NOTNULL(session, "ktrace_session_create"); | |
199 | ||
200 | ktrace_set_completion_handler(session, ^{ | |
201 | ktrace_session_destroy(session); | |
202 | T_END; | |
203 | }); | |
204 | ||
205 | /* Listen for memorystatus_do_kill trace events */ | |
206 | ret = ktrace_events_single(session, (BSDDBG_CODE(DBG_BSD_MEMSTAT, BSD_MEMSTAT_DO_KILL)) | DBG_FUNC_END, ^(ktrace_event_t event) { | |
207 | int i; | |
208 | bool received_jetsam_event = false; | |
209 | ||
210 | /* We don't care about jetsams for any other reason except zone-map-exhaustion */ | |
211 | if (event->arg2 == kMemorystatusKilledZoneMapExhaustion) { | |
212 | T_LOG("[memorystatus_do_kill] jetsam reason: zone-map-exhaustion, pid: %lu", event->arg1); | |
213 | if (current_test_index == VME_ZONE_TEST || current_test_index == VM_OBJECTS_ZONE_TEST) { | |
214 | /* | |
215 | * For the VM map entries zone we try to kill the leaking process. | |
216 | * Verify that we jetsammed one of the processes we spawned. | |
217 | */ | |
218 | for (i = 0; i < num_children; i++) { | |
219 | if (child_pids[i] == (pid_t)event->arg1) { | |
220 | received_jetsam_event = true; | |
221 | break; | |
222 | } | |
223 | } | |
224 | } else { | |
225 | received_jetsam_event = true; | |
226 | } | |
227 | ||
228 | T_ASSERT_TRUE(received_jetsam_event, "Received jetsam event as expected"); | |
229 | cleanup_and_end_test(); | |
230 | } | |
231 | }); | |
232 | T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_events_single"); | |
233 | ||
234 | ret = ktrace_start(session, dispatch_get_main_queue()); | |
235 | T_QUIET; T_ASSERT_POSIX_ZERO(ret, "ktrace_start"); | |
236 | } | |
237 | ||
238 | static void spawn_child_process(void) | |
239 | { | |
240 | pid_t pid = -1; | |
241 | char *launch_tool_args[4]; | |
242 | within_dispatch_source_handler = true; | |
243 | ||
244 | T_QUIET; T_ASSERT_LT(num_children, MAX_CHILD_PROCS, "Spawned %d children. Timing out...", MAX_CHILD_PROCS); | |
245 | ||
246 | launch_tool_args[0] = testpath; | |
247 | launch_tool_args[1] = "-n"; | |
248 | launch_tool_args[3] = NULL; | |
249 | ||
250 | if (current_test_index == VME_ZONE_TEST) { | |
251 | launch_tool_args[2] = VME_ZONE_TEST_OPT; | |
252 | } else if (current_test_index == VM_OBJECTS_ZONE_TEST) { | |
253 | launch_tool_args[2] = VM_OBJECTS_ZONE_TEST_OPT; | |
254 | } else if (current_test_index == GENERIC_ZONE_TEST) { | |
255 | launch_tool_args[2] = GENERIC_ZONE_TEST_OPT; | |
256 | } | |
257 | ||
258 | /* Spawn the child process */ | |
259 | int rc = dt_launch_tool(&pid, launch_tool_args, false, NULL, NULL); | |
260 | if (rc != 0) { | |
261 | T_LOG("dt_launch tool returned %d with error code %d", rc, errno); | |
262 | } | |
263 | T_QUIET; T_ASSERT_POSIX_SUCCESS(pid, "dt_launch_tool"); | |
264 | ||
265 | child_pids[num_children++] = pid; | |
266 | within_dispatch_source_handler = false; | |
267 | } | |
268 | ||
269 | static void run_test_for_zone(int index) | |
270 | { | |
271 | int ret, dev; | |
272 | size_t dev_size = sizeof(dev); | |
273 | uint32_t testpath_buf_size = sizeof(testpath); | |
274 | ||
275 | T_ATEND(cleanup_and_end_test); | |
276 | T_SETUPBEGIN; | |
277 | ||
278 | current_test_index = index; | |
279 | ||
280 | ret = sysctlbyname("kern.development", &dev, &dev_size, NULL, 0); | |
281 | T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "sysctl kern.development failed"); | |
282 | if (dev == 0) { | |
283 | T_SKIP("Skipping test on release kernel"); | |
284 | } | |
285 | ||
286 | ret = _NSGetExecutablePath(testpath, &testpath_buf_size); | |
287 | T_QUIET; T_ASSERT_POSIX_ZERO(ret, "_NSGetExecutablePath"); | |
288 | T_LOG("Executable path: %s", testpath); | |
289 | ||
290 | /* | |
291 | * If the timeout specified by T_META_TIMEOUT is hit, the atend handler does not get called. | |
292 | * So we're queueing a dispatch block to fire after TIMEOUT_SECS seconds, so we can exit cleanly. | |
293 | */ | |
294 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, TIMEOUT_SECS * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ | |
295 | T_ASSERT_FAIL("Timed out after %d seconds", TIMEOUT_SECS); | |
296 | }); | |
297 | ||
298 | /* | |
299 | * Create a dispatch source for the signal SIGUSR1. When a child is done allocating zone memory, it | |
300 | * sends SIGUSR1 to the parent. Only then does the parent spawn another child. This prevents us from | |
301 | * spawning many children at once and creating a lot of memory pressure. | |
302 | */ | |
303 | signal(SIGUSR1, SIG_IGN); | |
304 | ds_signal = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGUSR1, 0, dispatch_get_main_queue()); | |
305 | T_QUIET; T_ASSERT_NOTNULL(ds_signal, "dispatch_source_create"); | |
306 | ||
307 | dispatch_source_set_event_handler(ds_signal, ^{ | |
308 | /* Wait a few seconds before spawning another child. Keeps us from allocating too aggressively */ | |
309 | sleep(5); | |
310 | spawn_child_process(); | |
311 | }); | |
312 | dispatch_activate(ds_signal); | |
313 | ||
314 | /* Set up a ktrace session to listen for jetsam events */ | |
315 | setup_ktrace_session(); | |
316 | ||
317 | T_SETUPEND; | |
318 | ||
319 | /* Spawn the first child process */ | |
320 | T_LOG("Spawning child processes to allocate zone memory...\n\n"); | |
321 | spawn_child_process(); | |
322 | ||
323 | dispatch_main(); | |
324 | } | |
325 | ||
326 | T_HELPER_DECL(allocate_vm_regions, "allocates VM regions") | |
327 | { | |
328 | allocate_vm_regions(); | |
329 | } | |
330 | ||
331 | T_HELPER_DECL(allocate_vm_objects, "allocates VM objects and VM regions") | |
332 | { | |
333 | allocate_vm_objects(); | |
334 | } | |
335 | ||
336 | T_HELPER_DECL(allocate_from_generic_zone, "allocates from a generic zone") | |
337 | { | |
338 | memorystatus_priority_properties_t props; | |
339 | ||
340 | /* | |
341 | * We want to move the processes we spawn into the idle band, so that jetsam can target them first. | |
342 | * This prevents other important BATS tasks from getting killed, specially in LTE where we have very few | |
343 | * processes running. | |
344 | */ | |
345 | props.priority = JETSAM_PRIORITY_IDLE; | |
346 | props.user_data = 0; | |
347 | ||
348 | if (memorystatus_control(MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES, getpid(), 0, &props, sizeof(props))) { | |
349 | printf("memorystatus call to change jetsam priority failed\n"); | |
350 | exit(-1); | |
351 | } | |
352 | ||
353 | allocate_from_generic_zone(); | |
354 | } | |
355 | ||
356 | /* | |
357 | * T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL) changes the zone_map_jetsam_limit to a | |
358 | * lower value, so that the test can complete faster. | |
359 | * The test allocates zone memory pretty aggressively which can cause the system to panic | |
360 | * if the jetsam limit is quite high; a lower value keeps us from panicking. | |
361 | */ | |
362 | T_DECL( memorystatus_vme_zone_test, | |
363 | "allocates elements from the VM map entries zone, verifies zone-map-exhaustion jetsams", | |
364 | T_META_ASROOT(true), | |
365 | T_META_TIMEOUT(1800), | |
366 | /* T_META_LTEPHASE(LTE_POSTINIT), | |
367 | */ | |
368 | T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL)) | |
369 | { | |
370 | run_test_for_zone(VME_ZONE_TEST); | |
371 | } | |
372 | ||
373 | T_DECL( memorystatus_vm_objects_zone_test, | |
374 | "allocates elements from the VM objects and the VM map entries zones, verifies zone-map-exhaustion jetsams", | |
375 | T_META_ASROOT(true), | |
376 | T_META_TIMEOUT(1800), | |
377 | /* T_META_LTEPHASE(LTE_POSTINIT), | |
378 | */ | |
379 | T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL)) | |
380 | { | |
381 | run_test_for_zone(VM_OBJECTS_ZONE_TEST); | |
382 | } | |
383 | ||
384 | T_DECL( memorystatus_generic_zone_test, | |
385 | "allocates elements from a zone that doesn't have an optimized jetsam path, verifies zone-map-exhaustion jetsams", | |
386 | T_META_ASROOT(true), | |
387 | T_META_TIMEOUT(1800), | |
388 | /* T_META_LTEPHASE(LTE_POSTINIT), | |
389 | */ | |
390 | T_META_SYSCTL_INT(ZONEMAP_JETSAM_LIMIT_SYSCTL)) | |
391 | { | |
392 | run_test_for_zone(GENERIC_ZONE_TEST); | |
393 | } |