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.
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.
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() returns | In which process | Meaning |
|---|---|---|
-1 | the parent (there is no child) | Creating the child failed – too many processes or no memory. errno says why. |
0 | the child | You are the new process. Your parent is getppid(). |
| a positive number | the parent | You are the original process, and this is the child's PID – keep it to wait for the child. |
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).
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).
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).
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).
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 waitA 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).
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). forkreturns twice: the child's PID in the parent, 0 in the child; -1 means no child was created.- After
forkthe memory is copied, not shared – a change in one process is invisible in the other. Open file descriptors are inherited. nforks in a row give 2n processes; a child that should only do one job ends withexit.- Output buffered by stdio is copied by
fork: callfflush(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
structwith fixed-size arrays is one record; pass it by pointer, and check every input call for the end of input.