Processes in Linux with C — exercises with cooks that multiply, buffers that double and exit codes that lie (and the kitchen that hired seven cooks)
I wanted my head chef to hire three cooks, so I put the fork() inside a three-round loop. I ran it and the terminal filled up: “Cook 1”, “Cook 2”, “Cook 2”, “Cook 3”, “Cook 3”, “Cook 3”, “Cook 3”. Seven messages for three cooks. I counted the PIDs: all different. The output was not being repeated: there really were more processes than I had asked for. The culprit was a single missing line, and it is today’s first “What’s wrong here?” snippet.
These exercises close the processes topic. If you came here directly, review the theory and the processes in C practice first, because here I no longer give you the code.
Table of Contents
How to use this article
- Read the statement and the expected output of each level, and write your program without looking at the solutions.
- If you get stuck for more than ten minutes, open the 🔵 hint for that level, and only that one.
- In “What’s wrong here?”, predict the output before reading the explanation. Getting the prediction wrong is exactly what makes it stick.
- Compare your solution with mine and, above all, read “Why it is done this way”: that is where the decisions are that separate a program that works from one that works by chance. At the end you have the cheat sheet for review.
In every exercise the PIDs in your output will be different from mine and will change on every run. What has to match is the relationship between them (the PID the parent receives is the child’s, and so on) and the rest of the data.
Level 1 (basic): the cook introduces itself
Write a program in which the chef creates one cook with fork(). The cook prints its PID and its chef’s and ends. The chef waits for the cook to finish and then prints its own PID and the value fork() returned to it. Check in the output that the numbers match between the two messages. Also handle the case where fork() fails.
Expected output (with your PIDs):
Cook: my PID is 444 and my chef is 443 Chef: my PID is 443 and fork() returned 444
The chef has to print after the cook every time, not only when the scheduler feels like it. Which call guarantees that? And remember that the child’s PID, seen from the parent, is exactly what fork() returns.
Level 2 (intermediate): how many dishes have you made?
Two cooks prepare 4 and 7 dishes. Each one returns its number of dishes to the chef (no shared variables: you already know they do not work). The chef waits for both in the order they finish, prints how many dishes each cook made (identifying it by its number, 1 or 2) and the total. It is exercise 6.5 of the lab with fixed numbers instead of random ones, so the output can be checked.
Expected output (the order of the two cooks may change between runs):
Cook 2: 7 dishes Cook 1: 4 dishes Total dishes: 11
The number travels with exit(). To read it, wait(&status) and WEXITSTATUS. And to know which cook it belongs to, compare the PID returned by wait with the ones you stored when creating the children.
Level 3 (challenge): the service statistics
Five cooks take 4, 8, 2, 10 and 6 minutes. Each minute is simulated with 0.1 seconds of waiting (usleep), so the fastest one finishes first. Each cook returns its time to the chef. The chef collects all five in the order they arrive, prints who arrives and with what time, and at the end works out the mean time and the standard deviation (the population one: square root of the mean of the squared differences from the mean), with two decimals. It is exercise 6.6 of the lab with fixed times.
Expected output:
Arriving: cook 3: 2 min Arriving: cook 1: 4 min Arriving: cook 5: 6 min Arriving: cook 2: 8 min Arriving: cook 4: 10 min Mean time: 6.00 min Standard deviation: 2.83 min
Store each time in an array as it arrives. For the deviation you need sqrt from <math.h>, and when compiling you have to add -lm at the end: gcc -Wall -std=c11 level3.c -o level3 -lm. With -std=c11, usleep is hidden: put #define _DEFAULT_SOURCE before the #include lines.
What’s wrong here?
Three snippets, three mistakes I ran into (or saw classmates make) doing the lab. Read each one, predict what it prints and then keep reading.
Snippet 1: the loop that hires too many
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
printf("Cook %d up and running (PID %d)\n", i + 1, getpid());
}
}
/* every process waits for all its children before ending */
while (wait(NULL) > 0) {
}
return 0;
}
Predict: how many “Cook … up and running” lines come out?
Real output:
Cook 1 up and running (PID 394) Cook 2 up and running (PID 395) Cook 2 up and running (PID 397) Cook 3 up and running (PID 398) Cook 3 up and running (PID 396) Cook 3 up and running (PID 399) Cook 3 up and running (PID 400)
Seven, and with seven different PIDs. The cook born in round i = 0 prints its message… and since there is no exit(), it leaves the if and stays in the loop, with its own copy of i. In round i = 1 it calls its own fork(), just like the chef. Every process alive at the start of a round creates a new child, so the number of processes doubles in each round: 1 → 2 → 4 → 8. And each new child prints one line with the round it was born in.
The count, round by round:
Round i | Processes before the fork | New children (each prints “Cook i+1”) | Processes after |
|---|---|---|---|
| 0 | 1 | 1 | 2 |
| 1 | 2 | 2 | 4 |
| 2 | 4 | 4 | 8 |
1 + 2 + 4 = 7 lines and 8 processes in total (counting the chef, which does not print). The while (wait(NULL) > 0) makes every process wait for all its children, so nobody ends too early, but the order of the lines is up to the scheduler.
With 10 rounds it would be 2¹⁰ = 1024 processes and 1023 lines. With an infinite loop, the system runs out of free processes: that is what is known as a fork bomb. A missing exit() is not a detail.
The fix is one line: the cook ends inside its branch.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
printf("Cook %d up and running (PID %d)\n", i + 1, getpid());
exit(0); /* the cook ends here and does not go back to the loop */
}
}
while (wait(NULL) > 0) {
}
return 0;
}
Real output of the fix:
Cook 2 up and running (PID 408) Cook 1 up and running (PID 407) Cook 3 up and running (PID 409)
Three lines, three cooks. (Notice that cook 2 spoke before cook 1: all three work at the same time and the order is not guaranteed.)
Make every child end on the last line of its branch with exit(), even if you think it is not needed. It is the simplest way to never have a “loose” child running the parent’s code.
Snippet 2: the order received twice
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
printf("Order received. ");
pid_t pid = fork();
if (pid == 0) {
printf("Cook: to the stoves\n");
exit(0);
}
wait(NULL);
printf("Chef: dish served\n");
return 0;
}
Predict: “Order received.” is printed before the fork(), only once… right?
Real output:
Order received. Cook: to the stoves Order received. Chef: dish served
It comes out twice. And not because the child runs the printf again: the child is born after that line. The key is that printf does not write straight to the screen but to a buffer in memory that is flushed when a \n arrives (in a terminal), when it fills up or when the process ends. “Order received. ” has no \n, so at the moment of the fork() it is still in the buffer… and the buffer is the process’s memory, so it is copied to the child. Each one adds its line and, when ending, each one flushes its whole buffer.
This bug is even trickier if you redirect the output to a file (./buffer > log.txt). There printf does not flush the buffer even with \n, and any message before the fork() may appear twice even if it ends with a newline.
The fix is to flush the buffer before creating the child:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
printf("Order received. ");
fflush(stdout); /* flushes the buffer BEFORE fork: nothing is left to copy */
pid_t pid = fork();
if (pid == 0) {
printf("Cook: to the stoves\n");
exit(0);
}
wait(NULL);
printf("Chef: dish served\n");
return 0;
}
Real output of the fix:
Order received. Cook: to the stoves Chef: dish served
Now “Order received. ” comes out once: fflush(stdout) wrote it to the screen before the fork() and the child inherited an empty buffer. The child carries on on the same line because there was no newline, which is exactly what the code asked for.
The practical rule: fflush(stdout) right before every fork() if you have printed something. It costs nothing and saves you this mystery.
Snippet 3: the slowest cook in the world
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
exit(7); /* the cook took 7 minutes */
}
int status;
wait(&status);
printf("The cook took %d minutes\n", status);
return 0;
}
Predict: the cook called exit(7). How many minutes does the chef print?
Real output:
The cook took 1792 minutes
1792 minutes: almost 30 hours for one dish. The 7 is there, but hidden: wait does not store the exit code as it is, but in the second byte of the status integer, that is, multiplied by 256. 7 × 256 = 1792. The low byte is reserved for the signal that might have ended the process.
When a result comes out as a multiple of 256 (256, 512, 1792…), you are almost certainly printing status instead of WEXITSTATUS(status).
The fix uses the macros from <sys/wait.h>:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
exit(7); /* the cook took 7 minutes */
}
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("The cook took %d minutes\n", WEXITSTATUS(status));
}
return 0;
}
Real output of the fix:
The cook took 7 minutes
WIFEXITED confirms that the cook ended with exit and WEXITSTATUS takes the 7 out of the second byte.
If you want to see the shift with your own eyes, print status >> 8: it gives the same 7. But in your code use the macro, which is the portable way.
Solutions
Level 1 solution
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
/* the cook introduces itself */
printf("Cook: my PID is %d and my chef is %d\n", getpid(), getppid());
exit(0);
}
/* the chef waits so its message always comes afterwards */
wait(NULL);
printf("Chef: my PID is %d and fork() returned %d\n", getpid(), pid);
return 0;
}
Real output:
Cook: my PID is 444 and my chef is 443 Chef: my PID is 443 and fork() returned 444
Check: the cook says its PID is 444, and the chef says fork() returned 444. The cook says its chef is 443, and the chef says its PID is 443. Both pairs match, which is exactly what the statement asked for.
Why it is done this way:
- **
wait(NULL)before the chef’s message.** Without it, the order of the two lines would depend on the scheduler. We passNULLbecause we do not care how the cook ended, only that it has ended. - **
exit(0)at the end of the child’s branch.** Even though there is no loop afterwards, if the cook did not leave it would run thewait(which would return -1 since it has no children) and the chef’sprintf: you would see two “Chef” lines and the second would say that fork() returned 0. - **
return 1ifpid < 0.** If there is no child, there is no point carrying on; the 1 tells whoever launched the program that something went wrong.
Level 2 solution
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int dishes[2] = {4, 7}; /* dishes each cook prepares */
pid_t pids[2];
for (int i = 0; i < 2; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
return 1;
}
if (pids[i] == 0) {
/* each cook returns its number of dishes as the exit code */
exit(dishes[i]);
}
}
int total = 0;
for (int k = 0; k < 2; k++) {
int status;
pid_t done = wait(&status); /* whichever finishes first */
if (WIFEXITED(status)) {
int p = WEXITSTATUS(status);
/* we find out which cook it was by comparing the PID */
int num = (done == pids[0]) ? 1 : 2;
printf("Cook %d: %d dishes\n", num, p);
total += p;
}
}
printf("Total dishes: %d\n", total);
return 0;
}
Real output:
Cook 2: 7 dishes Cook 1: 4 dishes Total dishes: 11
In my run cook 2 finished first, and the program identified it correctly. Total: 4 + 7 = 11 dishes, the same result whatever the arrival order.
The collection, under the magnifying glass:
int total = 0;
for (int k = 0; k < 2; k++) {
int status;
pid_t done = wait(&status); /* whichever finishes first */
if (WIFEXITED(status)) {
int p = WEXITSTATUS(status);
/* we find out which cook it was by comparing the PID */
int num = (done == pids[0]) ? 1 : 2;
printf("Cook %d: %d dishes\n", num, p);
total += p;
}
}
printf("Total dishes: %d\n", total);
Round k | wait returns | Is it pids[0]? | num | WEXITSTATUS | total |
|---|---|---|---|---|---|
| 0 | PID of cook 2 | no | 2 | 7 | 7 |
| 1 | PID of cook 1 | yes | 1 | 4 | 11 |
Why it is done this way:
- **
waitand notwaitpid**, because the statement asks to collect in the order they finish. Withwaitpid(pids[k], …)cook 1 would always come out first. - **
(done == pids[0]) ? 1 : 2**: with two children one comparison is enough. With more, you would use the search loop from program 1 of the practice. - **
if (WIFEXITED(status))** before reading the code: if a cook died because of a signal, we do not add a meaningless number. - 4 and 7 fit in one byte. If a cook prepared 300 dishes, this solution would not work (44 would arrive).
Level 3 solution
#define _DEFAULT_SOURCE /* asks glibc for usleep(), which -std=c11 hides */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <math.h>
#define N 5
int main(void) {
int minutes[N] = {4, 8, 2, 10, 6};
pid_t pids[N];
for (int i = 0; i < N; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
return 1;
}
if (pids[i] == 0) {
/* each simulated minute lasts 0.1 s: the fastest one finishes first */
usleep(minutes[i] * 100000);
exit(minutes[i]);
}
}
int collected[N]; /* the times, in the order they arrive */
for (int k = 0; k < N; k++) {
int status;
pid_t done = wait(&status);
int num = 0;
for (int i = 0; i < N; i++) {
if (pids[i] == done) {
num = i + 1;
}
}
collected[k] = WEXITSTATUS(status);
printf("Arriving: cook %d: %d min\n", num, collected[k]);
}
/* mean: sum divided by N, in double so no decimals are lost */
double sum = 0;
for (int k = 0; k < N; k++) {
sum += collected[k];
}
double mean = sum / N;
/* standard deviation: root of the mean of the squares of (x - mean) */
double squares = 0;
for (int k = 0; k < N; k++) {
squares += (collected[k] - mean) * (collected[k] - mean);
}
double deviation = sqrt(squares / N);
printf("Mean time: %.2f min\n", mean);
printf("Standard deviation: %.2f min\n", deviation);
return 0;
}
Real output:
Arriving: cook 3: 2 min Arriving: cook 1: 4 min Arriving: cook 5: 6 min Arriving: cook 2: 8 min Arriving: cook 4: 10 min Mean time: 6.00 min Standard deviation: 2.83 min
The cooks arrive in the order of their times: cook 3 (2 min, 0.2 s), cook 1 (4 min), cook 5 (6 min), cook 2 (8 min) and cook 4 (10 min). The whole program takes as long as the slowest one, 1 second, because the five wait at the same time.
Now the sums by hand. The mean: (2 + 4 + 6 + 8 + 10) / 5 = 30 / 5 = 6.00. For the standard deviation:
Time x | x − mean | (x − mean)² |
|---|---|---|
| 2 | −4 | 16 |
| 4 | −2 | 4 |
| 6 | 0 | 0 |
| 8 | 2 | 4 |
| 10 | 4 | 16 |
| Sum | 40 |
40 / 5 = 8, and the square root of 8 is 2.828…, which with two decimals is 2.83. It matches the output.
The processes part is the collection:
int collected[N]; /* the times, in the order they arrive */
for (int k = 0; k < N; k++) {
int status;
pid_t done = wait(&status);
int num = 0;
for (int i = 0; i < N; i++) {
if (pids[i] == done) {
num = i + 1;
}
}
collected[k] = WEXITSTATUS(status);
printf("Arriving: cook %d: %d min\n", num, collected[k]);
}
Why it is done this way:
- **
usleep(minutes[i] * 100000)**:usleeptakes microseconds, so 4 minutes are 400 000 µs = 0.4 s. Withsleep(whole seconds) the program would take 10 seconds. - **
#define _DEFAULT_SOURCEon the first line**:-std=c11asks for “pure” standard C and glibc hides the functions that are not, likeusleep. That macro, defined before any#include, asks it to show them (you saw it in the preprocessor topic: macros change what the compiler sees). - **
collected[k]in arrival order**: the mean and the deviation do not depend on the order, so there is no need to reorder; but this way the output tells the real story of the service. - **
doubleforsum,meanandsquares**: with integers, the mean of 4, 8 and 3 (15 / 3) would be fine, but the mean of 4 and 7 would give 5 instead of 5.5. - **
-lmwhen compiling**:sqrtlives in the maths library, which gcc does not link by default. Without-lmyou will seeundefined reference to 'sqrt'.
Watch it step by step in Python Tutor
Python Tutor runs code line by line and draws the variables (choose C in the drop-down). Since it cannot create processes, let’s look at the part of snippet 3 that can be simulated: which integer the parent receives depending on the number the child passes to exit(), including the out-of-range case.
#include <stdio.h>
/* what the parent receives depending on the number the child passes to exit() */
int main(void) {
int codes[3] = {7, 255, 300};
for (int i = 0; i < 3; i++) {
/* the system only keeps the low byte (0-255), shifted 8 bits */
int status = (codes[i] & 0xFF) << 8;
int received = (status >> 8) & 0xFF; /* what WEXITSTATUS would give */
printf("exit(%d): status=%d, WEXITSTATUS=%d\n", codes[i], status, received);
}
return 0;
}
Output:
exit(7): status=1792, WEXITSTATUS=7 exit(255): status=65280, WEXITSTATUS=255 exit(300): status=11264, WEXITSTATUS=44
What to watch step by step:
- In the first round,
statusis 1792: the same number as in snippet 3.receivedis 7 again. - In the second round, 255 gives a
statusof 65280 (255 × 256) andreceived255: it is the largest value that fits. - In the third round, look at
codes[i] & 0xFF: from 300 only 44 is left (300 − 256). That is whyexit(300)reaches the parent as 44. - Compare the two columns of each line:
statusis alwaysreceived× 256. That is all theWEXITSTATUSmacro undoes.
Cheat sheet: processes in C
The essentials in 30 seconds
fork()creates a copy of the process: it returns 0 to the child, the child’s PID to the parent and -1 on failure.- The child always ends with
exit()inside its branch. - Memory is copied: what the child changes, the parent does not see.
execreplaces the program (same PID) and does not return if it works. To launch another program:fork+execin the child.wait(&status)waits for the first one to finish;waitpid(pid, &status, 0), for a specific one.- The
exit(n)code is read withWEXITSTATUS(status)and goes from 0 to 255. fflush(stdout)beforefork()if you have printed something.
I want… / Code / Result
| I want… | Code | Result |
|---|---|---|
| My PID and my parent’s | getpid(), getppid() | Two pid_t (integers) |
| Create a child | pid_t pid = fork(); | Two processes from here on |
| Know whether I am the child | if (pid == 0) | Only the child gets in |
| Wait for any child | pid_t done = wait(&status); | PID of the one that finished, -1 if none are left |
| Wait for a specific one | waitpid(pid, &status, 0); | Blocks until that one finishes |
| Return a number | exit(7); | The parent reads 7 with WEXITSTATUS |
Know whether it ended with exit | WIFEXITED(status) | True or false |
| Know which signal killed it | WIFSIGNALED(status), WTERMSIG(status) | For example, 6 with abort() |
| Run another program (list) | execlp("date", "date", NULL); | It becomes date |
| Run another program (array) | char *a[] = {"ls", "-l", NULL}; execvp(a[0], a); | It becomes ls -l |
Report a failed exec | perror(a[0]); exit(127); | ls: No such file or directory, code 127 |
Templates from the exercises
Create N children that work at the same time and collect them:
pid_t pids[N];
for (int i = 0; i < N; i++) {
pids[i] = fork();
if (pids[i] < 0) { perror("fork"); exit(1); }
if (pids[i] == 0) {
/* child i's work */
exit(result); /* 0..255 */
}
}
for (int k = 0; k < N; k++) {
int status;
pid_t done = wait(&status); /* the first one to finish */
if (WIFEXITED(status)) {
int r = WEXITSTATUS(status);
/* look up done in pids[] to know who it was */
}
}
Run a command like the shell does and find out how it went:
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv); /* argv ends in NULL */
perror(argv[0]);
exit(127);
}
int status;
waitpid(pid, &status, 0);
int code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
These templates give you the three levels: level 1 with N = 1, level 2 with N = 2 and the sum of the codes (11 dishes) and level 3 with N = 5, usleep in the child and the mean 6.00 and the deviation 2.83 at the end.
Symptom / Likely cause
| Symptom | Likely cause |
|---|---|
| More child messages appear than children you created | Missing exit() in the child’s branch inside a loop |
A text from before the fork() appears twice | It was still in the buffer: missing \n or fflush(stdout) |
| The exit code comes out as 256, 512, 1792… | You print status instead of WEXITSTATUS(status) |
exit(300) arrives as 44 | The code is only one byte (0-255) |
| The parent does not see what the child changed | Memory is copied, not shared |
Nothing after the exec comes out | Normal: exec worked and replaced the program |
| The prompt appears before the child’s messages | The parent did not call wait() |
unknown type name 'pid_t' | Missing <sys/types.h> (or <sys/wait.h>) with -std=c11 |
implicit declaration of function 'usleep' | Missing #define _DEFAULT_SOURCE before the #include lines |
undefined reference to 'sqrt' | Missing -lm at the end of the gcc command |
Before handing it in, ask yourself
- Do all my children end with
exit()inside their branch? - Do I call one
wait(orwaitpid) for every child I create? - Do I read the exit code with
WIFEXITEDandWEXITSTATUS, and does it fit in one byte? - Have I explained in the report that the PIDs and the order of the messages change on every run?
Did this article help you? Here is how you can support SergioLearns →
