Processes and Operations on Processes
What turns a program on disk into a running process, how its memory and state are organised, what the PCB stores, and the UNIX lifecycle of fork, exec, wait and exit, including zombies and orphans.
What you will be able to do
- Distinguish a program from a process and name the four sections of process memory with what lives in each.
- Draw the seven-state process diagram, explain every transition, and say which states live in main vs secondary memory.
- List the fields of a process control block and explain what each is for.
- Predict the output and process count of any C program built from
fork(),exec(),wait()andexit(). - Explain how a parent collects a child's exit status and what WEXITSTATUS returns.
- Tell a zombie from an orphan and explain why init adopts orphans.
From program to process
A program is a file. A process is that file, alive.
An operating system executes many kinds of programs. On a batch system they are called jobs; on a time-shared system they are user programs or tasks. Many modern process concepts are still expressed in terms of jobs (for example job scheduling), and the two terms are used interchangeably. Whatever you call them, the OS needs one concept to manage them all: the process.
Program vs process
Program (passive)
- A file containing a list of instructions stored on disk, often called an executable file.
- Does nothing by itself.
/usr/bin/firefoxsitting on your SSD is a program. - One program can give rise to many processes.
Process (active)
- An instance of a computer program that is being executed.
- Contains the program code and its current activity: a program counter pointing at the next instruction, plus a set of associated resources (registers, open files, memory).
- Three Firefox windows might be three processes born from the one program file.
Process memory: text, data, heap, stack
Four sections, two of which grow toward each other.
When the executable is loaded, the OS carves the process's memory into four sections. The picture on the slide shows them stacked from low address to high, with the heap growing upward and the stack growing downward into the gap between them.
The four sections of process memory
| Section | What it holds | When it is allocated | Example |
|---|---|---|---|
| Text | The compiled program code, read in from non-volatile storage when the program is launched | At load time; fixed size | The machine instructions of main() and every function |
| Data | Global and static variables | Allocated and initialised before `main` executes; fixed size | int counter = 0; at file scope, static int calls; |
| Heap | Dynamic memory allocation | At run time via malloc, free, new, delete; grows upward | char *buf = malloc(4096); |
| Stack | Local variables, function parameters, return addresses | Space reserved when variables are declared (at function entry), freed when they go out of scope; grows downward | int i; inside a function, the frame of every active call |
Why two growing regions? The heap and the stack both have sizes the compiler cannot know in advance. Placing them at opposite ends of the free space and letting them grow toward each other means neither needs a fixed limit; the process only runs out when they meet. That event is a heap-stack collision, and it crashes the program.
Facts the recap questions test
- Compiled code lives in the text section.
- Stack memory is automatically deallocated when variables go out of scope (a function returns), not when the program terminates and not on shutdown.
- The heap grows upward; the stack grows downward.
- A recursive function that never stops pushes a new frame per call and overflows the stack. This is the classic stack overflow.
- When heap and stack meet, a heap-stack collision occurs and the program crashes.
The seven process states
A process is always in exactly one state, and every arrow has a reason.
As a process executes it changes state. The basic five-state model is new, ready, run, wait, terminate. These slides use a seven-state model that adds two suspended states for processes swapped out to disk when main memory is full. Learn all seven with their transitions.
The seven states and how you enter each
- 1New. A program present in secondary memory is initiated for execution. The OS is creating the process but has not yet loaded it into main memory.
- 2Ready. The process has been loaded into main memory and is ready for execution. It waits for the processor. In a multiprogramming environment many processes sit here at once.
- 3Run. The process has been assigned the CPU and is executing. On a single CPU only one process is in this state.
- 4Terminate. The process moves here from run after its execution is completed. Its resources are released.
- 5Block or wait. The process moves here from run if it requires an I/O operation or some blocked resource. When the I/O completes or the resource becomes available it goes back to ready, never straight to run.
- 6Suspend ready. From ready: a higher-priority process must be executed but main memory is full, so this process is swapped out to disk. It returns to ready when main memory becomes available.
- 7Suspend wait. From wait: same reason, memory pressure while the process is blocked. When the resource becomes available it moves to suspend ready; when memory becomes available it moves to ready.
Where each state lives (the slide's Important Points table, memorise it)
| State | Present in |
|---|---|
| New | Secondary memory |
| Ready | Main memory |
| Run | Main memory |
| Wait | Main memory |
| Suspend wait | Secondary memory |
| Suspend ready | Secondary memory |
| Terminate | Neither (the process no longer occupies memory) |
The process control block
Everything the OS needs to freeze a process and thaw it later.
Each process is represented in the OS by a process control block (PCB), also called a task control block. It is the kernel's record card for the process: when the scheduler switches processes, it saves the running one's CPU state into its PCB and loads the next one's state from its PCB. Without the PCB there is no way to resume a paused process.
Fields of the PCB (all seven from the slides)
| Field | What it stores | Why the OS needs it |
|---|---|---|
| Process state | New, ready, running, waiting, and so on | The scheduler only picks from ready processes |
| Program counter | The address of the next instruction to execute | To resume exactly where the process stopped |
| CPU registers | Accumulators, stack pointers, general-purpose registers; number and type vary by architecture | All must be saved on an interrupt and restored on resume |
| CPU scheduling information | Process priority, pointers to scheduling queues, other scheduling parameters | Decides who runs next |
| Memory-management information | Information about the memory allocated to the process (base and limit registers, page tables) | Protects processes from each other; translates addresses |
| Accounting information | Amount of CPU and real time used, time limits, account numbers, process numbers | Billing, limits, ps and top output |
| I/O status information | List of I/O devices allocated to the process, list of open files | Know what to release when the process exits |
Process creation and the process tree
Every process except one was created by another process.
The OS must provide mechanisms for process creation and process termination. During execution a process may create several new processes; this is called spawning. The creating process is the parent, the new ones are its children, and each child can create children of its own. The result is a tree of processes. Most operating systems, including UNIX, Linux and Windows, identify each process by a unique process identifier (pid), an integer that is used as an index to access the process's attributes within the kernel.
The Linux tree on the slide, node by node
- init always has pid 1 and is the root parent of all user processes. After boot it creates servers such as a web or print server and an ssh server. (Modern distributions call it
systemd, still pid 1.) - kthreadd creates processes that do work on behalf of the kernel, for example
khelperandpdflush. - sshd manages clients that connect over ssh (secure shell).
- login manages clients who log on directly at the machine.
- A logged-in client is running the bash shell with pid 8416. From that shell the user has launched ps and the emacs editor, so both are children of bash.
- On UNIX and Linux, list processes with the
pscommand, for exampleps -elfor every process with full details.
After creating a child, the parent has two choices on each of two questions
Execution
- Parent continues to execute concurrently with its children.
- Parent waits until some or all of its children have terminated.
Address space
- Child is a duplicate of the parent (same program and data).
- Child has a new program loaded into it.
UNIX implements those choices with three system calls. fork() creates a child that is a duplicate of the parent (address space choice 1). exec() replaces the child's program with a new one (address space choice 2). wait() makes the parent wait for the child (execution choice 2). Not calling wait() is execution choice 1. Your shell does exactly this: it forks, the child execs ls, and the shell waits for ls to finish before printing the next prompt.
fork(): one process becomes two
After fork() there are two processes on the same line of code, and they disagree about one number.
fork() creates a child process that runs concurrently with the process that called it (the parent). The child has the same environment as its parent: a copy of the code, data, heap and stack, the same program counter, the same CPU registers, and the same open files. Only the PID is different. After fork(), both processes execute the next instruction following the call.
1pid_t fork(void);
What fork() returns, and to whom
| Return value | Meaning | Who receives it |
|---|---|---|
| Negative | Creation of the child failed (out of processes or memory) | The caller; no child exists |
| Zero | "You are the child" | The newly created child process |
| Positive | "You are the parent", and the value is the PID of the new child | The parent (caller) |
How many lines does this program print?
1#include <stdio.h>2#include <sys/types.h>3#include <unistd.h>4int main()5{6 fork();7 printf("Hello world!\n");8 return 0;9}
Answer
Hello world! Hello world!
After fork() there are two processes, and both continue from the line after the call. Each executes printf once, so the line appears twice.
How many times is hello printed, and how many child processes are created?
1int main()2{3 fork();4 fork();5 fork();6 printf("hello\n");7 return 0;8}
Answer
hello printed 8 times. 7 child processes created.
Each fork() doubles the number of processes: 1 becomes 2, then 4, then 8. All 8 print once. With n forks in sequence there are 2^n processes and 2^n minus 1 children (the original is not a child). Here n = 3, so 8 processes and 7 children.
Predict the output: fork() puzzles
Trace by hand, one process at a time. These are the exam favourites.
Predict the output.
1void fork_example()2{3 if (fork() == 0)4 printf("Hello from Child!\n");5 else6 printf("Hello from Parent!\n");7}89int main()10{11 fork_example();12 return 0;13}
Answer
Hello from Parent! Hello from Child! (or the two lines in the other order)
The child sees fork() return 0 and prints the child line. The parent sees the child's PID (positive) and prints the parent line. Both run concurrently, so either order is possible; the slides show both.
Predict the output. Assume the parent's PID is 1234 and the child's PID is 1235.
1int main()2{3 int x = 1;4 pid_t pid = fork();5 if (pid == 0)6 printf("Child has x = %d\n", ++x);7 else8 printf("Parent has x = %d\n", --x);9 printf("Bye from process %d with x = %d\n", getpid(), x);10 return 0;11}
Answer
Parent has x = 0 Bye from process 1234 with x = 0 Child has x = 2 Bye from process 1235 with x = 2
Each process has its own copy of x. The child increments its copy to 2, the parent decrements its copy to 0, and neither sees the other's change. getpid() reports each process's own PID. The parent and child pairs may interleave, but within one process the two lines stay in order.
How many child processes does this program create?
1int main()2{3 if(!fork())4 {5 if(!fork())6 fork();7 }8 fork();9}
Answer
7 child processes (8 processes in total)
Trace it. !fork() is true only in the child. The first fork makes P and C1; only C1 enters the block. Inside, C1 forks: C1 and C2; only C2 (where the second fork returned 0) runs the third fork, creating C3. Now 4 processes (P, C1, C2, C3) reach the last fork(), and each doubles, giving 8 processes. 8 minus the original is 7 children.
fork() into an if(fork()) and see how the count changes.exec(): same process, new program
The body stays, the brain is replaced.
fork() alone would only ever let you run copies of the same program. exec() is the other half. When a process calls exec(), the program named in the parameter replaces the entire process. The new program is loaded into the same process space. The PID does not change, and neither does the PPID. But the code, data, stack and heap are all replaced with those of the newly loaded program. No new process is created.
Because the old program is gone, any code after a successful `exec()` never runs. The only way to reach the line after exec() is if it fails (file not found, no permission), in which case it returns -1. In C, exec() is a family of six functions: execl(), execle(), execlp(), execv(), execve(), execvp(). The letters mean: l takes arguments as a list, v takes them as a vector (array), e lets you pass an environment, p searches the PATH.
example.c runs and calls execv on the compiled hello.c. Predict the output. Assume the PID is 4733.
1/* example.c */2#include <stdio.h>3#include <unistd.h>4#include <stdlib.h>5int main(int argc, char *argv[])6{7 printf("PID of example.c = %d\n", getpid());8 char *args[] = {"Hello", "World", NULL};9 execv("./hello", args);10 printf("Back to example.c");11 return 0;12}1314/* hello.c */15#include <stdio.h>16#include <unistd.h>17#include <stdlib.h>18int main()19{20 printf("We are in hello.c\n");21 printf("PID of hello.c is %d", getpid());22 return 0;23}
Answer
PID of example.c = 4733 We are in hello.c PID of hello.c is 4733
execv replaces example.c's code with hello.c's inside the same process, so the PID stays 4733. The line "Back to example.c" is never printed because that code no longer exists in the process once exec succeeds.
fork() vs exec() (the slide table, with the slide's wording)
| Feature | fork() | exec() |
|---|---|---|
| Definition | Allows a process to copy itself | Makes a new process image by replacing the existing process |
| Address space | Parent and child are in separate address spaces | The new program's address space replaces the calling process's address space |
| Parent process | There is a child process and a parent process after the call | There is only one process after the call (the same PID, new program) |
| Result | Makes a child equal to the parent | Makes the process run a different program |
| New PID? | Yes, the child gets a new PID | No, PID and PPID are unchanged |
wait() and exit(): termination and collecting the status
A process ends when it says so, or when its parent says so, and someone must read the result.
A process terminates when it finishes its final statement and asks the OS to delete it with the exit() system call. It may return a status value (typically an integer) to its parent, which the parent collects with wait(). The OS then deallocates all the process's resources: physical and virtual memory, open files, and I/O buffers. Because the parent needs to know its children's identities, the PID of each new child is passed to the parent at creation (that positive return value of fork()).
1#include <sys/types.h>2#include <sys/wait.h>34pid_t wait(int *wstatus);5/* Returns the PID of the child that terminated.6 wstatus: where the child's exit status is stored; pass NULL if you do not care. */
What wait() does
- Suspends execution of the current process until one of its children terminates. The parent leaves the run state and comes back to the ready queue when a child completes.
- Returns the PID of the child that terminated, so a parent with several children can tell which one finished.
- Stores the child's exit status in the integer pointed to by
wstatus. Decode it withWIFEXITED(status)(did it exit normally?) andWEXITSTATUS(status)(what number did it pass toexit?). - If the parent has no children,
wait()returns -1 immediately.
1int main() {2 pid_t p1, p2;3 int status;45 p1 = fork();6 if (p1 == 0) {7 sleep(2);8 return 1;9 }1011 p2 = fork();12 if (p2 == 0) {13 sleep(1);14 return 2;15 }1617 // Parent18 pid_t pid = wait(&status);19 printf("Child with PID %d terminated\n", pid);2021 pid = wait(&status);22 printf("Child with PID %d terminated\n", pid);2324 return 0;25}
Output
Child with PID <p2> terminated (after about 1 second) Child with PID <p1> terminated (after about 2 seconds)
The second child sleeps for 1 second and the first for 2, so the second child terminates first and the first wait() returns p2. wait() does not wait for a particular child; it returns whichever child finishes first. If you need a specific child, use waitpid().
Predict the output.
1#include <stdio.h>2#include <unistd.h>3#include <stdlib.h>4#include <sys/wait.h>56int main() {7 int num = 8;8 int status;910 if (fork() == 0) {11 // Child12 int square = num * num;13 exit(square); // Return square14 }15 else {16 // Parent17 wait(&status);1819 if (WIFEXITED(status)) {20 printf("Square = %d\n", WEXITSTATUS(status));21 }22 }23 return 0;24}
Answer
Square = 64
The child computes 64 and passes it to exit(). The parent blocks in wait(), then WIFEXITED confirms a normal exit and WEXITSTATUS extracts the 64. This is how a child sends one small integer result back to its parent.
Process termination: who ends a process, and how
A process exits itself, or a parent ends it. Either way the resources come back.
Three reasons a parent may terminate a child
- 1The child has exceeded its usage of some resource it was allocated.
- 2The task assigned to the child is no longer required.
- 3The parent is exiting, and the OS does not allow a child to continue if its parent terminates. When this rule applies, all children, grandchildren and so on are terminated: this is cascading termination.
1#include <stdio.h>2#include <unistd.h>3#include <signal.h>4#include <sys/wait.h>56int main() {7 pid_t pid = fork();89 if (pid == 0) {10 // Child process11 while (1) {12 printf("Child is running...\n");13 sleep(1);14 }15 } else {16 // Parent process17 sleep(5);18 printf("Parent terminating child...\n");19 kill(pid, SIGKILL);20 wait(NULL);21 printf("Child terminated.\n");22 }23 return 0;24}
Output
Child is running... Child is running... Child is running... Child is running... Child is running... Parent terminating child... Child terminated.
The child loops forever, printing once a second. After five prints the parent wakes from its sleep(5), sends SIGKILL with the child's PID, and then calls wait(NULL) to collect the corpse. That wait() matters: without it, the killed child would linger as a zombie, which is the next section.
One more rule from the slides: in UNIX, if the parent terminates, all its children are assigned `init` as their new parent. The children still have a parent to collect their status and execution statistics. That is UNIX's answer to the cascading termination policy: instead of killing orphans, adopt them.
Zombies and orphans
One is dead but not buried. The other is alive with no parent.
When a child exits, some process must `wait()` on it to read its exit code. Until that happens the exit code is kept in the process table. Reading it is called reaping the child. Between the moment a child exits and the moment it is reaped, the child is a zombie. A zombie has already released its memory and files. It occupies only a slot in the process table and takes no memory and no CPU. Its only job is to hold that exit status until the parent asks.
1/* Child becomes a zombie because the parent is sleeping2 when the child exits and has not called wait(). */3#include <stdlib.h>4#include <sys/types.h>5#include <unistd.h>6int main()7{8 // fork returns the child's pid in the parent9 pid_t child_pid = fork();1011 // Parent process12 if (child_pid > 0)13 sleep(50);1415 // Child process16 else17 exit(0);1819 return 0;20}
Output
(no output) For 50 seconds, `ps -el` shows the child with state Z and the label <defunct>. When the parent exits, init adopts and reaps the zombie.
An orphan is the mirror image. If a process exits while its children are still running, those children are orphans. Orphaned children are immediately adopted by `init` (pid 1). The slides also phrase it as: if the parent terminates without invoking wait(), the process is an orphan. init periodically calls wait(), so an orphan that later exits is reaped promptly and never becomes a long-lived zombie.
1/* Parent finishes execution while the child is running.2 The child becomes an orphan and is adopted by init. */3#include <stdio.h>4#include <sys/types.h>5#include <unistd.h>6int main()7{8 // Create a child process9 int pid = fork();1011 if (pid > 0)12 printf("in parent process");1314 else if (pid == 0)15 {16 sleep(30);17 printf("in child process");18 }1920 return 0;21}
Output
in parent process (the shell prompt returns; 30 seconds later, from the orphan now owned by init:) in child process
Zombie vs orphan
Zombie
- Child has exited, parent is still alive but has not called `wait()`.
- Dead, but its exit status is still in the process table.
- Uses no memory, no CPU, only a process-table entry.
- Fixed by the parent calling
wait(), or by the parent dying soinitreaps it. - Shows in
psas stateZ,<defunct>.
Orphan
- Parent has exited, child is still running.
- Alive and executing normally.
- Uses memory and CPU like any process.
- Adopted by `init` (pid 1) immediately, which will reap it when it exits.
- Shows in
pswith PPID 1.
Before the exam
The lines worth memorising, and the mistakes that lose marks.
Remember this
- 1Program = passive file on disk. Process = program in execution with a program counter and resources. A program becomes a process when the executable is loaded into memory.
- 2Memory: text (code), data (globals and statics, set up before
main), heap (malloc/free, grows up), stack (locals, freed when out of scope, grows down). Infinite recursion overflows the stack; heap meeting stack = collision, crash. - 3Seven states: new, ready, run, terminate, block/wait, suspend ready, suspend wait. Wait goes back to ready, never to run.
- 4Memory table: new, suspend ready, suspend wait in secondary memory; ready, run, wait in main memory; terminate in neither.
- 5Suspend wait: resource arrives, go to suspend ready; memory arrives, go to ready.
- 6PCB fields: state, program counter, CPU registers, scheduling info, memory-management info, accounting info, I/O status info.
- 7init is pid 1 and the root of the tree;
ps -ellists processes. The pid indexes the process's attributes in the kernel. - 8
fork()returns negative on failure, 0 to the child, child's PID to the parent. Child copies everything except the PID. - 9n sequential forks: 2^n processes, 2^n minus 1 children. Conditional forks: trace by hand.
- 10Each process has its own copy of every variable after
fork(). - 11
exec()replaces code, data, heap, stack in the same process; PID unchanged; code after a successfulexecnever runs. Six variants: execl, execle, execlp, execv, execve, execvp. - 12
wait()blocks until any child terminates and returns that child's PID;WEXITSTATUSgives the low 8 bits of theexit()value. - 13Termination frees memory, open files and I/O buffers. Parent may kill a child for resource overuse, task no longer needed, or because the parent is exiting (cascading termination).
- 14Zombie: exited child not yet reaped by
wait(); only a process-table entry. Orphan: running child whose parent exited; adopted by init.
Exam traps
- A program is not a process, because a program is a passive file on disk while a process is that program loaded into memory and executing with a program counter and resources.
- The stack is not freed when the program terminates, because stack space is released as soon as the variables go out of scope, which is when the function returns.
- Infinite recursion does not overflow the heap, because each call pushes a new frame on the stack, so it is the stack that overflows.
- A process whose I/O completes does not go straight to running, because it moves from wait to ready and must be scheduled again.
- Suspended processes are not in main memory, because suspend ready and suspend wait are swapped out to secondary memory when main memory is full.
- n forks do not create n children, because every fork doubles the process count, giving 2^n processes and 2^n minus 1 children.
- fork() does not return the child's PID to the child, because the child receives 0; only the parent receives the child's PID.
- Modifying a variable in the child does not change it in the parent, because fork duplicates the address space and each process owns a separate copy.
- exec() does not create a new process, because it loads a new program into the same process and keeps the same PID.
- A zombie does not consume memory or CPU, because it has already released everything except its process-table entry holding the exit status.
- An orphan is not a zombie, because an orphan is still running after its parent died and is adopted by init, while a zombie has exited and is waiting to be reaped.
Quiz yourself
One question at a time with instant feedback. Your best score is saved.
Which section of process memory contains the compiled program code?
Not quite
The text section holds the compiled program code, read in from non-volatile storage when the program is launched. Data holds globals and statics, heap holds dynamic allocations, stack holds locals.
Finished reading?
Mark this lecture as done
Take the quiz above first so your score is saved here.