Session 2 · Operating Systems

Processes and fork

fork() copies the calling process and returns twice. What the child inherits, why variables are not shared, how several forks form a tree, why output can appear twice, and what zombies and orphans are.

16 min readPID, PPID, getpid and getppidfork return valuesCopied memory, not sharedProcess trees and exit in the childThe stdio buffer trap and fflushZombies, orphans and a record struct

Everything else in this course – signals, pipes, message queues, shared memory – is about processes talking to each other. This session creates them. On Unix there is exactly one way for a program to start a new process: fork(), which makes a copy of the process that calls it. The copy runs the same code from the same point, and the only thing that tells the two apart is what fork returned.

It sounds simple, and it is – but it has consequences that surprise everyone once: variables are not shared after the fork, a child that forgets to exit keeps running the parent's code, output can appear twice, and a child nobody waits for turns into a zombie. All of these show up in the small example programs below.

The model in one breath: fork duplicates the calling process – memory, open files, even the unwritten output buffer – and returns twice: the child's PID in the parent, 0 in the child. From then on there are two independent processes, and the parent is responsible for waiting for the child.

What a process is

A program is a file on the disk. A process is a program that is running: the code loaded into memory, plus everything the kernel keeps track of while it runs (Fig. 1). The same program started twice gives two processes, with separate memory and separate numbers.

process – PID 4500, parent (PPID) 4321memory (only this process sees it)code (text)data – global variablesheap – malloc ↓… free …stack – local variables ↑kept by the kernelfds: 0 stdin · 1 stdout · 2 stderrPID 4500 · PPID 4321 · owner (UID)state: running, sleeping, zombie …saved registers, program countercurrent directory, environment, signals
Fig. 1 · A process: private memory on the left, the bookkeeping the kernel keeps for it on the right.

The kernel identifies each process by its PID. Every process except the very first one was created by another process, its parent, whose PID it remembers as its PPID. A process can ask for both:

#include <unistd.h>      // fork, getpid, getppid
#include <sys/wait.h>    // wait, waitpid

printf("my PID: %i, my parent: %i\n", getpid(), getppid());

Run it from the shell and the parent is the shell itself – compare with echo $$, which prints the shell's PID. ps -f shows both columns for your processes, ps aux for all of them.

process
A running instance of a program: memory, open files, an ID and a state, managed by the kernel.
PID
Process ID – the number the kernel gives a process while it runs. getpid() returns it.
PPID
Parent process ID – the PID of the process that created this one. getppid() returns it.
child
A process created by fork. The process that called fork is its parent.

fork: one call, two returns

fork() takes no arguments. It creates a new process – the child – as a copy of the caller, and from that moment two processes execute the next line after the call. The return value is the only difference between them (Fig. 2):

fork() returnsIn which processMeaning
-1the parent (there is no child)Creating the child failed – too many processes or no memory. errno says why.
0the childYou are the new process. Your parent is getppid().
a positive numberthe parentYou are the original process, and this is the child's PID – keep it to wait for the child.
parentPID 4500fork()parent continuesfork() returned 4501 – the child's PIDchild – a new process, PID 4501fork() returned 0 · PPID 4500one process,one call …… two processes, two returns, same next line of code-1: no child (too many processes,no memory) – check it
Fig. 2 · Before fork: one process. After it: two, at the same line, with different return values.

The whole idiom is an if on that value. This is the course's first example, with the parent waiting for the child (waitpid gets its own session – the next one):

fork.c – the three branches

int notacommonvalue = 1;
pid_t child = fork();
if (child < 0)                       // -1: no child was created
{
    perror("fork");
    exit(1);
}
if (child > 0)                       // parent: child = the PID of the new process
{
    waitpid(child, &status, 0);      // wait until that child ends
    printf("The value is %i in parent process\n", notacommonvalue);   // 1
}
else                                 // child: fork returned 0
{
    notacommonvalue = 5;             // changes the child's own copy
    printf("The value is %i in child process\n", notacommonvalue);    // 5
}
$ ./fork
The value is 1 before forking
The value is 5 in child process
The value is 1 in parent process (remain the original)

Copied, not shared

The child gets a copy of the parent's memory: every variable, global or local, with the value it had at the moment of the fork. From then on the two copies are independent. When the child sets notacommonvalue = 5, the parent's variable stays 1 – that is what the example's name is about (Fig. 3).

before forkparentnotacommonvalue= 1forkafter fork, child sets it to 5parentnotacommonvalue= 1 (unchanged)child – its own copynotacommonvalue= 5Two address spaces: the same variable name, the same address even – but two different memory cells.Linux copies lazily (copy-on-write): a page is duplicated only when one of the processes writes to it.To exchange data, processes need the kernel: exit codes, signals, pipes, shared memory – later sessions.
Fig. 3 · Each process changes only its own copy. Even a global variable is not a shared variable.

This is the big difference from threads, which you may know from Java: threads of one process share the same memory, so a change in one is visible in the others (and needs synchronized or volatile to be safe). Processes share nothing by default. To pass data between them you need the kernel – the exit code now, then signals, pipes, message queues and shared memory in the coming sessions.

Several forks: the process tree

What does this print?

fork();     // line 1
fork();     // line 2
fork();     // line 3
printf("hello\n");

Eight lines of hello. Every process that exists when a fork line runs executes it, children included. Line 1 turns one process into two, line 2 turns two into four, line 3 four into eight. With n forks in a row you get 2n processes, forming a tree (Fig. 4).

process Poriginalprocess Amade by line 1process Dmade by line 2process Gmade by line 3process Emade by line 3process Bmade by line 2process Fmade by line 3process Cmade by line 3n forks in a row → 2ⁿ processes: each line doubles the number of running copies.
Fig. 4 · fork(); fork(); fork(); – P has three children, its first child has two, and so on: eight processes, eight hellos.

Creating exactly the children you want

Usually you want the parent to create a fixed number of children, each doing one job. The course examples put each child's job into a function. The function forks; the parent gets the PID back and returns it; the child does its work and then calls exit – it must never return to main:

fork3.c – a child in a function

pid_t child1(void)
{
    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(1); }
    if (pid > 0)
        return pid;              // parent: back to main with the child's PID

    // only the child gets here
    notacommonvalue = 5;
    printf("The value is %i in child1 process\n", notacommonvalue);
    exit(0);                     // the child must not return into main
}

fork3.c – main starts two children, then waits

pid_t child1_pid = child1();       // both children run from here on
pid_t child2_pid = child2();

waitpid(child1_pid, &status, 0);   // then wait for each of them
waitpid(child2_pid, &status, 0);

Without the exit(0), the child would return from child1() into main and carry on with the parent's code: it would call child2() itself and create a grandchild (Fig. 5).

child ends with exit(0) – 3 processesmainchild1(); child2();child 1prints, exit(0)child 2prints, exit(0)main waits for both and prints lastexit forgotten – 4 processesmainchild1(); child2();child 1returns into main!child 2grandchildchild 1 called child2()
Fig. 5 · The child's branch ends in exit. A forgotten exit lets the child run the parent's code and fork again.

The same rule in a loop, the usual way to start N children:

for (int i = 0; i < 3; i++)
{
    pid_t pid = fork();
    if (pid == 0)
    {
        printf("child %i, PID %i\n", i, getpid());
        exit(0);                 // without this the child continues the loop and forks too
    }
}
while (wait(NULL) > 0)           // wait returns -1 when no child is left
    ;

The stdio buffer trap

Run the first example with its output going into a pipe instead of the terminal, without the fflush line, and one line appears twice:

$ ./fork | cat          # stdout is a pipe, no fflush before fork
The value is 1 before forking
The value is 5 in child process
The value is 1 before forking
The value is 1 in parent process (remain the original)

The cause is the stdio buffer from the first session. printf does not write to the kernel immediately; it collects text in a buffer inside the process. When standard output is a terminal, the buffer is emptied at every '\n' (line-buffered). When it is a file or a pipe, the buffer is emptied only when it is full or the process exits (fully buffered). So at the moment of the fork, the line is still in the parent's memory – and fork copies memory. Both processes then hold the line, and both write it when they exit (Fig. 6).

parent, before forkprintf("…before…\n")buffer: before…forkparentbuffer: before…child – copied memorybuffer: before…pipe / file: the line arrives twiceexit() flushes itexit() flushes it tooto a terminal stdout is line-buffered: the '\n' flushes it, so the bug hides while you test by handfix: fflush(stdout) right before fork – the buffer is empty when it is copied
Fig. 6 · fork copies the unwritten buffer. Two processes flush the same text.

The fix is one line: empty the buffer before forking.

fork.c – flush, then fork

printf("The value is %i before forking\n", notacommonvalue);
fflush(stdout);                  // empty the buffer BEFORE the process is copied
pid_t child = fork();

Zombies and orphans

When a process ends, the kernel keeps a small record of it: its PID and its exit status, for the parent to collect with wait or waitpid. Between the child's end and the parent's wait, the child is a zombie: it runs no code and holds no memory, but it still occupies an entry in the process table. ps shows it with state Z and the word <defunct>:

pid_t pid = fork();
if (pid == 0)
    exit(0);             // the child ends at once...
sleep(30);               // ...the parent does not wait() for 30 seconds
waitpid(pid, NULL, 0);   // only now is the zombie removed
$ ps -o pid,ppid,stat,cmd
  PID  PPID STAT CMD
 4321  4300 Ss   -bash
 4500  4321 S+   ./zombie
 4501  4500 Z+   [zombie] <defunct>

The opposite case is an orphan: the parent ends while the child still runs. The child is not killed. It is adopted – by PID 1, or by a process that registered itself as a "subreaper" (some desktop sessions do) – so getppid() suddenly returns a different number. The adopter waits for its adopted children, so orphans do not stay zombies (Fig. 7).

if (fork() == 0)
{
    printf("parent before: %i\n", getppid());
    sleep(2);                                 // meanwhile the parent ends
    printf("parent after:  %i\n", getppid()); // a different PID: the child was adopted
    exit(0);
}
exit(0);                                      // the parent does not wait
zombie – the child ends first, the parent does not wait yetparentrunning … … … wait() →childrunningZ – zombie: only the exit status leftexit(0)reaped by wait()orphan – the parent ends firstparentrunning, exit()childPPID 4500adopted: PPID becomes 1 (or a subreaper)the adopter waits for it, so an orphan never stays a zombie
Fig. 7 · Zombie: the child ended, the parent has not waited yet. Orphan: the parent ended first, and the child gets a new parent.

A struct for a record

The other example of the week has nothing to do with processes: it is the start of the record keeper you build in the first assignment. A struct groups the fields of one record. With fixed-size char arrays the text lives inside the struct, so the struct can be copied with = and has a fixed size:

struktura.c – one record

struct driver
{
    unsigned int id;
    char name[50];          // fixed-size arrays: the text lives inside the struct
    char email[50];
    char phone[50];
    unsigned int exp;
};

Passing a struct to a function by value copies all of it. Passing a pointer copies only its address, and the function reaches the caller's struct through ->: d->exp is short for (*d).exp. A const pointer promises that the function only reads it (Fig. 8).

struct driver did · name[50] · email[50] · phone[50] · expsizeof(d) = 160 bytesf(d)the function gets a copy: 160 byteschanges stay in the copyf(&d)the function gets an address: 8 bytesd->exp++ changes the caller's dconst struct driver * – read only, enforced by the compiler
Fig. 8 · By value: a 160-byte copy. By pointer: an 8-byte address to the original.

struktura.c – creating a record, optionally from an existing one

void driver_creation(const struct driver *createdriver)   // NULL: a new, empty record
{
    struct driver a = {0};          // every field 0, every string empty
    if (createdriver != NULL)
        a = *createdriver;          // struct assignment copies all fields, arrays included
    a.id = id++;
    /* ... read the fields, then append a to the file ... */
}

The example takes a pointer that may be NULL: NULL means "a new record", a real pointer means "start from this record and let the user keep fields". The = {0} initialiser makes every number 0 and every string empty, so no field contains garbage if it is skipped.

Reading the fields safely

struktura.c – one text field from the keyboard

void read_in_console_messages(const char *console_text, char *result)
{
    printf("%s", console_text);
    if (scanf(" %49[^\n]", result) != 1)   // the leading space skips a leftover '\n'
        result[0] = '\0';                   // end of input: an empty string, not garbage
}

%49[^\n] reads up to 49 characters until the end of the line – spaces included, unlike %s – and leaves room for the 0 in a char[50]. The space before it skips the newline left over from the previous input. Checking the return value matters for the same reason as with fgets: at the end of input, scanf writes nothing, and the buffer would keep whatever it held. The menu loop needs the same care with getchar:

struktura.c – the menu loop

command = getchar();
if (command == EOF)          // Ctrl+D: leave the menu instead of looping forever
    break;
/* ... switch (command) ... */
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)   // drop the rest of the line
    ;

Practice

Compile each with -Wall -Wextra. Before running a fork program, write down what you expect it to print – then run it, and also run it as ./prog | cat.

Task 1

Who is who

Fork once. In both processes print "parent" or "child", the own PID, the parent's PID, and in the parent also the child's PID. Check that the numbers fit together, and that the parent's parent is your shell (echo $$).

Approach

Branch on the return value; don't forget the parent's wait (here: wait(NULL) after the print).

pid_t pid = fork();
if (pid == 0)
    printf("child:  PID %i, parent %i\n", getpid(), getppid());
else
    printf("parent: PID %i, parent %i, child %i\n", getpid(), getppid(), pid);

Task 2

Predict, then run

How many times does this print A, and how many times B? Run it to the terminal and through | cat. Then add one line that makes the answer the same in both cases.

printf("A");      // no '\n': stays in the buffer
fork();
printf("B\n");
Explanation

B twice – one per process. A also twice, even on the terminal: without '\n' the A is still in the buffer when fork copies it. fflush(stdout) right before fork gives one A.

Task 3

Three numbered children

Start three children from a loop. Each prints its number (0, 1, 2) and its PID, sleeps a second, and exits. The parent prints "all done" only after all three have ended. Then delete the exit in the child and count the lines – explain the number.

Approach

The loop from the "several forks" section, plus sleep(1) in the child. Without exit, every child continues the loop and forks as well: 1 + 2 + 4 = 7 "child" lines instead of 3, and all eight processes reach the end and print "all done".

Task 4

Not shared

The child sets a variable to 10 and exits; the parent waits and prints the variable. What does it print? Make the child return the value 10 to the parent another way.

int sum = 0;
pid_t pid = fork();
if (pid == 0) { sum = 10; exit(0); }
waitpid(pid, NULL, 0);
printf("%i\n", sum);    // what does it print – and why?
Approach

It prints 0 – the child changed its own copy. The simplest channel back is the exit code: exit(10) in the child, and WEXITSTATUS(status) after waitpid(pid, &status, 0) in the parent. It holds only 0–255; the next session covers it.

Task 5

See a zombie and an orphan

Write the zombie program from this page and look at it with ps -u $USER -o pid,ppid,stat,cmd from a second terminal while the parent sleeps. Then write the orphan program and run it – which PID does the child report as its new parent?

Approach

Start the zombie program with & so you keep the prompt, or use a second SSH session. The orphan's new parent is 1 on many systems; on a desktop session it may be another process – look it up with ps -p.

Task 6

Change a record through a pointer

Write void raise_exp(struct driver *d) that increases the experience of a driver by one, and void print_driver(const struct driver *d). Call both from main. What does the compiler say if print_driver tries to change a field?

Approach

-> on the pointer, & at the call. Writing through a const pointer is a compile error ("assignment of member in read-only object").

void raise_exp(struct driver *d)
{
    d->exp++;            // d->exp is (*d).exp – the caller's struct changes
}
raise_exp(&a);

Summary

  • A process is a running program with its own memory, file descriptors, PID and PPID (getpid, getppid).
  • fork returns twice: the child's PID in the parent, 0 in the child; -1 means no child was created.
  • After fork the memory is copied, not shared – a change in one process is invisible in the other. Open file descriptors are inherited.
  • n forks in a row give 2n processes; a child that should only do one job ends with exit.
  • Output buffered by stdio is copied by fork: call fflush(stdout) before forking.
  • A child that ended but was not waited for is a zombie (Z, <defunct>); a child whose parent ended is an orphan and gets adopted.
  • A struct with fixed-size arrays is one record; pass it by pointer, and check every input call for the end of input.