---
title: 'Process Management'
source: 'https://academia.sh/en/courses/c-systems-programming/process-management'
course: 'Systems Programming with C'
language: en
updated: '2026-09-26T03:02:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'shows that after fork the two processes hold the same variable as independent copies, measuring both directions'
    - 'explains the rule that fflush(stdout) is mandatory before fork by measuring the copy risk of a buffered structure'
    - 'describes in prose, without running the code, why the exec family replaces the process image'
---

# Process Management

fork splits a process in two and hands the child a copy of the memory at that moment; once the copy is made, the two sides are independent of each other — no change reaches the other side in either direction.

The previous lesson showed that a descriptor derived with `dup` shares the same open
file description: when one moved the offset, the other saw it. `fork` does the
opposite. The moment it is called it hands the child a copy of the process's memory,
and once that copy is made the two sides are completely independent of each other.
One of the course's four classes, **copy**, describes exactly this: the change stays
only on the side that made it, the other side never sees it. This lesson measures
that independence in both directions, across two separate `fork` calls and two
different data types: first two plain integers, then through the macros that read
the exit status, and finally through a small structure that models standard output's
buffer.

## Measuring the Copy in Both Directions

The measurement units stay the same: `observe.h` and `observe.c` are identical to
the previous lesson's. The new part in this file is `process_measurement.c`, which
uses two `fork` calls back to back: the first measures two plain integers in both
directions, the second measures a small structure that models standard output's
buffer the same way.

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

```c
// process_measurement.c — two separate forks, two separate data types, the same copy rule
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "observe.h"

/* Model: not a real stdio buffer, a small structure that follows the same copy rule */
typedef struct {
    char data[32];
    int length;
} ModelBuffer;

static void model_write(ModelBuffer *b, const char *s) {
    int n = 0;
    while (s[n] != '\0' && b->length < 31) {
        b->data[b->length] = s[n];
        b->length++;
        n++;
    }
}

int main(void) {
    int value_a = 0;
    int value_b = 0;

    fflush(stdout);
    pid_t p = fork();
    if (p == 0) {
        value_a = 1;      /* changes only in the child's own copy */
        _exit(value_b);   /* exits without ever seeing the parent's later change */
    }

    value_b = 7;           /* after fork, changes only in the parent's copy */
    int status = 0;
    waitpid(p, &status, 0);

    observe("child's variable after fork", COPY, value_a == 1);
    observe("parent's variable after fork", COPY, WEXITSTATUS(status) == 7);
    printf("child's normal exit (WIFEXITED): %s\n", WIFEXITED(status) ? "yes" : "no");

    /* second fork: does the model buffer follow the same rule too */
    ModelBuffer buffer;
    memset(&buffer, 0, sizeof buffer);
    model_write(&buffer, "previous");   /* model of content accumulated before fork, not yet flushed */

    fflush(stdout);
    pid_t p2 = fork();
    if (p2 == 0) {
        model_write(&buffer, "child");
        _exit(buffer.length);
    }
    model_write(&buffer, "parent");
    int status2 = 0;
    waitpid(p2, &status2, 0);

    observe("did the child's addition to the buffer reach the parent", COPY,
           WEXITSTATUS(status2) == buffer.length);

    observe_summary();
    return 0;
}
```

```text
child's variable after fork        missed   copy
parent's variable after fork       missed   copy
child's normal exit (WIFEXITED): yes
did the child's addition to the buffer reach the parent missed   copy
classes: copy 3, shared 0, handoff 0, unobserved 0
reached 0, not reached 3
```

**SI3.** Both of the first two lines say `missed`. The child sets `value_a` to `1` in
its own copy; after `waitpid`, the parent looks at its own `value_a` and still finds
`0` — the child's write never reached the parent's copy. Symmetrically, the parent
sets its own `value_b` to `7` after `fork` returns; the child exits with
`_exit(value_b)` without ever seeing that line, and since `value_b` in its own copy
is still `0`, its exit code is also `0`. When the parent reads that with
`WEXITSTATUS` and finds `0`, not `7`, it confirms that its own change never reached
the child at all. Both fall into the same class: **copy**. `fork` copies once, and
after that the two sides are unaware of each other — which side writes first and
which writes second makes no difference here, because there is no shared record,
there are two independent copies.

## Measuring the fflush Rule

**SI4.** The third line carries this independence from an integer into a multi-line
buffer. `ModelBuffer` stands in for a real `stdio` buffer: before `fork` it
accumulates the text `"previous"`, just like an unflushed output line waiting to be
written to the terminal. When `fork` is called this structure is copied along with
the rest of memory too — the child adds `"child"` to its own copy and reports its
length through the exit code, the parent adds `"parent"` to its own copy. Because
`"child"` and `"parent"` are different lengths, the two sides' final lengths never
come out equal, and the measurement reads this as `missed`: the child's addition to
its own buffer never reached the parent's buffer. The rule that holds in every lesson
of the course is exactly what this measurement shows: the `fflush(stdout)` call
before `fork` clears whatever unflushed content sits inside the real `stdio` buffer,
so there is nothing left to copy. Skip that line, and content that has not yet been
written — like `"previous"` — sits in both the parent's and the child's copy
together; if each flushes its own buffer at its own close, the same line appears
twice — once from the parent, once from the child, because now both carry a separate
copy belonging to the same class, **copy**. `process_measurement.c` closes off this
risk twice: `fflush(stdout)` is called right before each of the two `fork` calls, and
the `observe` calls themselves only run on the parent side after `waitpid`; the child
never prints anything, it exits silently with `_exit`.

## Exit Status Macros

**SI5.** The `status` that `waitpid` fills in is a single integer, but it encodes two
separate questions: how the process ended, and with what value. `WEXITSTATUS(status)`
only answers the second question — it returns the value the child gave to `_exit` or
`return` — but this macro is only meaningful if the first question is affirmative,
that is, if the child really did exit the normal way. `WIFEXITED(status)` answers the
first question: it confirms that the child ended by its own choice, not through a
signal. This lesson finds `WIFEXITED` true after both `fork` calls, because both
children exit with `_exit`; the distinction stays abstract until the upcoming
lesson on signals shows the macro's counterpart, a process that ends through a
signal. The point here is this: relying on `WEXITSTATUS` without a `WIFEXITED` check
can be misleading, because the `status` field of a process that ended through a
signal carries the same bits for a different purpose — this lesson does not run
that case, but records here why the macro is not sufficient on its own.

The value `WEXITSTATUS` returns is limited to eight bits, because POSIX guarantees
carrying the exit code at that width; both measurements inside
`process_measurement.c` were chosen to stay under that limit — `value_b` is seven,
the model buffer's length is under thirty-one. A program that gives `_exit` a
three-digit or negative value and then reads the exit code expecting that value can
produce a misleading result because of this eight-bit truncation; this lesson does
not measure that case separately, but the reason the `observe` calls choose small
fixed values is exactly to avoid that limit.

`WIFEXITED`'s counterpart is `WIFSIGNALED`: this macro returns true if the child
ended because of a signal, and in that case `WTERMSIG(status)` reads which signal
ended it. This lesson does not run that macro, following the rule that forbids
printing a signal number or name — it would neither produce a printable value nor
add anything to this lesson's measurement axis, because a process that ended through
a signal has no such thing as an exit code, only the signal that ended it. A program
that relies on `WEXITSTATUS` after `waitpid` without checking both `WIFEXITED` and
`WIFSIGNALED` ends up reading an undefined value as if it were an exit code, for a
child that ended through a signal; so the distinction between the two is not merely
a style preference, it is a precondition for reading correctly.

## The exec Family

**SI6.** After `fork`, the child process is usually followed by a second step: a call
from the `exec` family. `fork` copies memory but does not change the program code —
the child is still a copy of the same program. `exec`, on the other hand, keeps the
process identity but rebuilds the process **image** from scratch: the code, data,
and stack are replaced with a new program's, and when the call succeeds it does not
return. This lesson does not run `exec`, because the question it measures — who sees
what — loses its meaning after `exec`: the old process's variables, its `value_a`,
its `value_b`, its model buffer, are all gone, replaced with new ones. Using `fork`
and `exec` together is the foundation of a shell starting a new program: the shell
first produces its own copy with `fork`, the child process inherits the environment
variables and open descriptors, and then loads the desired program over that copy
with `exec`. The parent shell never changes; only the child calls `exec`, and once it
succeeds it is no longer the shell, it is the program that was called.

The members of the `exec` family differ from each other along three axes, all in
prose: how they take their arguments (`execl` and similar take a variable number of
arguments, `execv` and similar take an array), how they find the program (`execve`
only accepts a full path, `execvp` searches the `PATH` environment variable), and how
they inherit the environment (`execve` and `execvpe` accept a new environment array,
the others use the calling process's environment as is). What they all share is that
there is no return value — if the call succeeds it does not return, if it fails it
returns `-1` and the calling process continues with its own variables. An `exec`
called without `fork` irreversibly turns the calling process into another program;
that is why a shell starting a new program without losing its own process depends on
producing a copy first with `fork` and calling `exec` in that copy. Open file
descriptors stay open across `exec` by default too — unless the `FD_CLOEXEC` flag is
set, the descriptors the child inherited from `fork` are still valid in the new
program; a shell's implementation of redirection syntax (`>`, `<`) rests exactly on
this: the shell first moves a descriptor to the desired slot with `dup2`, then calls
`exec`, and the new program uses that descriptor without ever knowing where it came
from.

How `fork` produces its memory copy is not this lesson's subject; the operating
systems course's memory management section already answered that question — the
kernel most of the time does not actually duplicate pages, it marks them with
copy-on-write, and the real copy only happens when one side writes. This lesson does
not touch that internal machinery; the only thing it measures is that, whatever
mechanism does the copying, the two sides end up independent — whether it is an
integer or a multi-line buffer.

The `ModelBuffer` measurement does not imitate a real stdio buffer's internal
structure; it is just a small structure of our own, following the same copy rule.
The generality here matters: the byte sequence a real `FILE*` buffer holds, and the
integer variable this lesson measures elsewhere, are both subject to the same copy
rule at the moment of `fork` — the child takes the parent's state at that moment,
and afterward the two change independently. `ModelBuffer` makes this generality
concrete: a counter three bytes wide and an array thirty-two bytes wide get equal
treatment from `fork`'s point of view. The difference in real stdio is that the
library's decision, not the programmer's, determines when the buffer is flushed — a
line-buffered terminal flushes at the end of every line, a fully buffered file only
flushes when the buffer fills or the program closes. `fork` does not change this
flush timing, it only splits the content accumulated up to that point into two
independent copies; what violates the rule is not the absence of an `fflush(stdout)`
call, it is the expectation that accumulated content will be flushed twice.

## The Bounding Measurement

This measurement's boundary is that it is independent of timing, and that is a
deliberate design choice. Whichever order the child's `_exit` call and the parent's
later write run in, the result does not change, because neither writes into the same
memory — each writes into its own copy. That is why the measurement needs no `sleep`
or wait; the `waitpid` call alone is enough, because it confirms not the ordering but
the independence of the copies. The model buffer measurement carries the same
boundary: choosing `"child"` and `"parent"` at different lengths is not arbitrary —
it is deliberate, so the result comes out the same on every run, because this lesson
measures not a race condition but only the presence of copying. Other classes in the
course — handoff in particular — can be sensitive to ordering, but for the copy
class, ordering never changes the result.

Another question this measurement does not cover is how the result changes with the
number of `fork` calls. `process_measurement.c` calls two `fork`s back to back, the
second only after the first child has fully closed — the second `fork` does not wait
for the first child to drop out of the process table, because `waitpid` already
collected the first one. Had a program instead started two children at the same
time, the second without waiting for the first to finish, both children would still
each carry a separate copy of the same parent memory; the copy class's guarantee is
independent of the number of children, because every `fork` call produces its own
new, independent copy on its own, unaware of previous children. This lesson does not
produce more than one child at a time, because what the result shows is already
fully proven through a single pair; extra children would only repeat the same
guarantee, not introduce a new class or a new boundary.

## Summary

- `fork` hands the child a copy of the memory state at the moment it is called; once
  the copy is made, the two sides are independent.
- This independence runs both ways: a variable the child writes does not reach the
  parent, and a variable the parent writes after `fork` does not reach the child
  either.
- The same independence is not limited to a single integer; a larger structure, like
  a multi-line buffer, follows the same rule too.
- `fflush(stdout)` is mandatory before `fork`; otherwise buffered content gets
  copied and the same line can be written twice.
- `WIFEXITED` and `WEXITSTATUS` are read together: one says how the process ended,
  the other with what value; without one, the other can be misleading.
- The `exec` family rebuilds the process image from scratch and does not return;
  this lesson does not run it, because the question of copied variables becomes
  meaningless after `exec`.

## Next Step

The next lesson moves to a completely different way of carrying a change than the
variables `fork` copies: a flag a signal handler raises, a handoff that reaches the
main flow only once.
