The Pedigree Project 0.1
vfork-benchmark/main.c
1/* Copyright (c) 2026, Pedigree Developers. */
2#define _GNU_SOURCE
3#include <errno.h>
4#include <fcntl.h>
5#include <inttypes.h>
6#include <limits.h>
7#include <stdint.h>
8#include <stdio.h>
9#include <stdlib.h>
10#include <string.h>
11#include <time.h>
12#include <unistd.h>
13
14#include <sys/mman.h>
15#include <sys/resource.h>
16#include <sys/syscall.h>
17#include <sys/wait.h>
18
19#define CHILD_STATUS 23
20#define MAX_SIZES 8
21
22static char self[PATH_MAX];
23static char* child_arguments[] = {self, "--worker", NULL};
24static char* child_environment[] = {"LC_ALL=C", "PATH=/usr/bin:/bin", NULL};
25static volatile int probe_value;
26static int call_fork;
27
28struct options {
29 unsigned iterations, samples, warmup;
30 unsigned sizes[MAX_SIZES];
31 size_t size_count;
32 int mode;
33 const char* expect;
34};
35
36struct usage {
37 uint64_t parent_user, parent_system, child_user, child_system;
38};
39
40static int failure(const char* operation) {
41 fprintf(stderr, "VFORK-BENCH: FAIL operation=%s errno=%d\n", operation, errno);
42 return -1;
43}
44
45static uint64_t timeval_us(const struct timeval* value) {
46 return (uint64_t)value->tv_sec * 1000000 + (uint64_t)value->tv_usec;
47}
48
49static int usage_snapshot(struct usage* result) {
50 struct rusage parent, children;
51 if (getrusage(RUSAGE_SELF, &parent) || getrusage(RUSAGE_CHILDREN, &children))
52 return failure("getrusage");
53 result->parent_user = timeval_us(&parent.ru_utime);
54 result->parent_system = timeval_us(&parent.ru_stime);
55 result->child_user = timeval_us(&children.ru_utime);
56 result->child_system = timeval_us(&children.ru_stime);
57 return 0;
58}
59
60static int monotonic_ns(uint64_t* result) {
61 struct timespec value;
62 // The syscall samples current ticks; the vDSO can use the last timer snapshot.
63 if (syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &value))
64 return failure("clock_gettime");
65 *result = (uint64_t)value.tv_sec * 1000000000 + (uint64_t)value.tv_nsec;
66 return 0;
67}
68
69static int reap(pid_t child, int expected) {
70 int status;
71 pid_t waited;
72 do {
73 waited = waitpid(child, &status, 0);
74 } while (waited < 0 && errno == EINTR);
75 if (waited != child)
76 return failure("waitpid");
77 if (!WIFEXITED(status) || WEXITSTATUS(status) != expected) {
78 fprintf(stderr, "VFORK-BENCH: FAIL operation=child-status status=%d\n", status);
79 return -1;
80 }
81 return 0;
82}
83
84static int spawn(int execute) {
85 pid_t child = call_fork ? fork() : vfork();
86 if (!child) {
87 if (execute) {
88 execve(self, child_arguments, child_environment);
89 _exit(127);
90 }
91 _exit(CHILD_STATUS);
92 }
93 if (child < 0)
94 return failure(call_fork ? "fork" : "vfork");
95 return reap(child, CHILD_STATUS);
96}
97
98static const char* probe(void) {
99 // This untimed semantic check is separate from the exec/_exit-only workload.
100 probe_value = 0;
101 pid_t child = vfork();
102 if (!child) {
103 probe_value = 1;
104 _exit(CHILD_STATUS);
105 }
106 if (child < 0) {
107 failure("vfork-probe");
108 return NULL;
109 }
110 if (reap(child, CHILD_STATUS))
111 return NULL;
112 return probe_value ? "shared" : "fork";
113}
114
115static int warm_executable(void) {
116 int fd = open(self, O_RDONLY);
117 if (fd < 0)
118 return failure("open-self");
119 char buffer[8192];
120 ssize_t count;
121 do {
122 count = read(fd, buffer, sizeof(buffer));
123 } while (count > 0 || (count < 0 && errno == EINTR));
124 int saved_errno = errno;
125 if (close(fd))
126 return failure("close-self");
127 if (count < 0) {
128 errno = saved_errno;
129 return failure("read-self");
130 }
131 return spawn(1);
132}
133
134static int measure(const char* variant, unsigned mib, int execute, unsigned sample,
135 unsigned iterations) {
136 struct usage before, after;
137 uint64_t start, end;
138 if (usage_snapshot(&before) || monotonic_ns(&start))
139 return -1;
140 for (unsigned i = 0; i < iterations; ++i)
141 if (spawn(execute))
142 return -1;
143 if (monotonic_ns(&end) || usage_snapshot(&after))
144 return -1;
145 if (end < start || after.parent_user < before.parent_user ||
146 after.parent_system < before.parent_system || after.child_user < before.child_user ||
147 after.child_system < before.child_system)
148 return failure("nonmonotonic-counter");
149 printf("sample,%s,%s,%s,%u,%u,%u,%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 "\n",
150 variant, call_fork ? "fork" : "vfork", execute ? "exec" : "exit", mib, sample, iterations,
151 end - start, after.parent_user - before.parent_user,
152 after.parent_system - before.parent_system, after.child_user - before.child_user,
153 after.child_system - before.child_system);
154 return 0;
155}
156
157static int number(const char* text, unsigned minimum, unsigned maximum, unsigned* value) {
158 char* end;
159 errno = 0;
160 unsigned long parsed = strtoul(text, &end, 10);
161 if (!*text || *end || errno || parsed < minimum || parsed > maximum)
162 return -1;
163 *value = (unsigned)parsed;
164 return 0;
165}
166
167static int sizes(const char* text, struct options* options) {
168 options->size_count = 0;
169 while (*text && options->size_count < MAX_SIZES) {
170 char* end;
171 errno = 0;
172 unsigned long value = strtoul(text, &end, 10);
173 if (end == text || errno || value > 1024 || (*end && *end != ','))
174 return -1;
175 options->sizes[options->size_count++] = (unsigned)value;
176 if (!*end)
177 return 0;
178 text = end + 1;
179 }
180 return -1;
181}
182
183static void help(void) {
184 puts(
185 "Usage: vfork-benchmark [--iterations N] [--samples N] [--warmup N]\n"
186 " [--sizes 0,16,64] [--mode both|exec|exit] [--expect shared|fork]\n"
187 " [--call vfork|fork]\n"
188 "Measures sequential process creation plus child completion. Sizes are parent\n"
189 "payload MiB added to the program's baseline memory, not measured total RSS.\n"
190 "Payload pages are touched once before warmup and remain untouched in timing.");
191}
192
193int main(int argc, char** argv) {
194 if (argc == 2 && !strcmp(argv[1], "--worker"))
195 _exit(CHILD_STATUS);
196 struct options options = {30, 5, 3, {0, 16, 64}, 3, 2, NULL};
197 for (int i = 1; i < argc; ++i) {
198 const char* name = argv[i];
199 if (!strcmp(name, "--help")) {
200 help();
201 return 0;
202 }
203 if (++i == argc)
204 goto invalid;
205 const char* value = argv[i];
206 if (!strcmp(name, "--iterations")) {
207 if (number(value, 1, 10000, &options.iterations))
208 goto invalid;
209 } else if (!strcmp(name, "--samples")) {
210 if (number(value, 1, 100, &options.samples))
211 goto invalid;
212 } else if (!strcmp(name, "--warmup")) {
213 if (number(value, 0, 1000, &options.warmup))
214 goto invalid;
215 } else if (!strcmp(name, "--sizes")) {
216 if (sizes(value, &options))
217 goto invalid;
218 } else if (!strcmp(name, "--mode")) {
219 options.mode = !strcmp(value, "both") ? 2
220 : !strcmp(value, "exec") ? 1
221 : !strcmp(value, "exit") ? 0
222 : -1;
223 if (options.mode < 0)
224 goto invalid;
225 } else if (!strcmp(name, "--expect")) {
226 if (strcmp(value, "shared") && strcmp(value, "fork"))
227 goto invalid;
228 options.expect = value;
229 } else if (!strcmp(name, "--call")) {
230 if (strcmp(value, "vfork") && strcmp(value, "fork"))
231 goto invalid;
232 call_fork = !strcmp(value, "fork");
233 } else
234 goto invalid;
235 }
236 setvbuf(stdout, NULL, _IOLBF, 0);
237 long page_size = sysconf(_SC_PAGESIZE);
238 ssize_t path_size = readlink("/proc/self/exe", self, sizeof(self) - 1);
239 if (page_size <= 0 || path_size <= 0 || path_size >= (ssize_t)sizeof(self) - 1)
240 return failure("startup") != 0;
241 self[path_size] = 0;
242 const char* variant = probe();
243 if (!variant)
244 return 1;
245 printf("VFORK-BENCH: PROBE variant=%s\n", variant);
246 if (options.expect && strcmp(variant, options.expect)) {
247 fprintf(stderr, "VFORK-BENCH: FAIL expected=%s observed=%s\n", options.expect, variant);
248 return 2;
249 }
250 if (warm_executable())
251 return 1;
252 printf(
253 "VFORK-BENCH: BEGIN iterations=%u samples=%u warmup=%u page_bytes=%ld "
254 "clock=monotonic-syscall payload=touched-once faults=unsupported maxrss=unsupported\n",
255 options.iterations, options.samples, options.warmup, page_size);
256 puts(
257 "kind,variant,call,mode,payload_mib,sample,iterations,wall_ns,parent_user_us,parent_system_"
258 "us,"
259 "children_user_us,children_system_us");
260 for (size_t size = 0; size < options.size_count; ++size) {
261 unsigned mib = options.sizes[size];
262 size_t bytes = (size_t)mib * 1024 * 1024;
263 volatile unsigned char* payload = NULL;
264 if (bytes) {
265 payload = mmap(NULL, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
266 if (payload == MAP_FAILED)
267 return failure("mmap") != 0;
268 for (size_t offset = 0; offset < bytes; offset += (size_t)page_size)
269 payload[offset] = 0x5a;
270 payload[bytes - 1] = 0xa5;
271 }
272 for (unsigned i = 0; i < options.warmup; ++i)
273 for (int mode = 0; mode < 2; ++mode)
274 if ((options.mode == 2 || options.mode == mode) && spawn(mode))
275 return 1;
276 for (unsigned sample = 1; sample <= options.samples; ++sample)
277 for (int position = 0; position < (options.mode == 2 ? 2 : 1); ++position) {
278 int mode = options.mode == 2 ? ((sample + position) & 1) : options.mode;
279 if (measure(variant, mib, mode, sample, options.iterations))
280 return 1;
281 }
282 if (bytes && munmap((void*)payload, bytes))
283 return failure("munmap") != 0;
284 }
285 puts("VFORK-BENCH: PASS");
286 return 0;
287
288invalid:
289 help();
290 return 2;
291}