COMP 3150Operating Systems

01 Introduction

In the earliest computers, only one program ran at a time. It had the machine entirely to itself: the whole address space, every register, every device. Modern systems are radically different. A laptop runs a browser, a music player, a code editor, a chat client, a backup daemon, and dozens of system services, all apparently at once. The abstraction that makes this possible is the process.

A process is a program in execution. It is the unit of work that the operating system schedules, the unit of isolation that the memory manager protects, and the unit of ownership that the file system tracks. Almost every mechanism in the rest of this course — threads, scheduling, synchronization, memory management — is defined in terms of processes. Getting the concept precisely right now makes everything that follows easier.

This lesson establishes the process concept carefully. The next four lessons in the unit build on it: states, control structures, scheduling, and context switching. By the end of the unit you will have a complete picture of the abstraction that every modern operating system is organised around.

Prerequisites from earlier units

This lesson assumes you have completed the course VM setup (Lesson 1.0) and have a working Linux environment with the toolchain from Lesson 2.5. The interactive activity and worked example in this lesson run inside the VM, so make sure it is running before you begin.

02 Learning objectives

By the end of this lesson you will be able to

  • Define a process.
  • Differentiate processes and programs.
  • Explain process execution.
  • Identify process resources.
  • Analyse process lifecycle events.

This lesson contributes to U3.1 and to course learning outcomes CLO2 and CLO6.

03 Required reading

  • OSTEP — Chapter 4, The Abstraction: The Process, and Chapter 5, Interlude: Process API. The clearest introduction to the process abstraction and the fork/exec/wait API that Linux implements.
  • Operating System Concepts — Chapter 3, Sections 3.1–3.3 (process concept, process states, and process control block). The classical taxonomy of what a process is and what the kernel tracks about it.
  • Linux man pages: man 2 fork, man 2 execve, man 2 wait, man 2 exit, man 5 proc, man 1 ps. These document the exact behaviour of the system calls and the process interface.
  • Love, R. — Linux Kernel Development, 3rd ed., Chapter 3 (Process Management). How Linux implements the process abstraction; useful for connecting the textbook to the real system.

Reading strategy

Read OSTEP Chapter 4 first for the abstraction, then Chapter 5 for the fork/exec/wait API. Read Silberschatz Sections 3.1–3.3 for the formal taxonomy. Keep the man pages open while working through the worked example — the fork(2) page in particular has subtle details that the textbook glosses over.

04 Core content

4.1 From program to process

A program is a passive artefact: an executable file on disk, containing machine instructions and static data. It does not run, does not consume CPU, and does not own any memory beyond the space it occupies on storage.

A process is that program in execution. It has a program counter that advances, a stack that grows and shrinks, a heap that allocates and frees, registers that change values, and file descriptors that reference open files. Where a program is a noun, a process is a verb.

A process is a program in execution — an active entity with a program counter, a set of registers, and an address space, together with whatever resources the operating system has granted it.

Two consequences follow immediately from this definition. First, the same program can give rise to many processes at once. Running two copies of gcc means two separate processes, each with its own program counter, its own address space, and its own open files. Neither knows the other exists (unless they deliberately share something). Second, a single process may execute code from several different programs over its lifetime — through the exec family of system calls — but only one at a time.

This distinction is worth restating with an example. On a Linux system, the shell itself is a process. When you type ls, the shell asks the kernel to create a new process that runs the ls program. The new process is not the shell, and it is not the same as the ls file on disk. It is a running instance of that file, with its own resources and its own lifetime. When the program finishes, the process ends and its resources are reclaimed.

4.2 The memory image of a process

From the process's own point of view, memory is a large, private, contiguous address space. In reality, the operating system maps portions of that address space onto physical memory, files, or nothing at all. But the abstraction is what programs see, and it has a conventional layout that is worth understanding precisely.

The memory layout of a process A vertical diagram showing the conventional layout of a process address space: at the top, command-line arguments and environment; then the stack growing downward; a large unmapped gap; then the heap growing upward; then initialised and uninitialised data; at the bottom, read-only executable code. THE ADDRESS SPACE OF A PROCESS high addresses low addresses command-line arguments · environment strings e.g. argv[0]="ls", argv[1]="/home", PATH=..., HOME=... stack local variables · function arguments · return addresses · saved registers grows downward (toward lower addresses) with each function call ↓ unmapped gap (mapped on demand, may be large) heap dynamically allocated memory — malloc(), new, etc. grows upward (toward higher addresses) as allocation proceeds ↑ initialised + uninitialised data (BSS) global and static variables — those with initial values are loaded from the file text — executable code (read-only) loaded from the executable file, shared between processes running the same program
Figure 3.1 — The conventional layout of a process address space. The heap grows upward and the stack grows downward; the space between them is the process's available dynamic memory.

The five regions each have a distinct purpose:

Text segment
The executable code. Read-only in almost all modern systems; a write to the text segment causes a fault. Because it is read-only, the same physical pages can be shared between processes running the same program — the mechanism behind shared libraries and the reason a hundred instances of bash do not consume a hundred copies of the binary.
Data segment
Global and static variables that have an initial value in the source code. Loaded from the executable file at process start.
BSS segment
Global and static variables without an initial value. The kernel zero-fills this region at load time; it is not stored in the executable file, which is why uninitialised globals cost no disk space. The name is historical: "Block Started by Symbol" from the IBM 7094 assembler.
Heap
Dynamically allocated memory. Grows upward as malloc and its equivalents request more space. Managed by the C library's allocator, which asks the kernel for additional pages only when necessary.
Stack
Local variables, function arguments, return addresses, and saved registers. Grows downward as function calls nest more deeply. Automatically managed by the compiler — every function entry and exit adjusts the stack pointer.

The layout is a convention, not a kernel requirement. The kernel gives each process a private virtual address space and lets the loader lay out the segments; the specific addresses vary depending on the executable format, the architecture, and the state of ASLR. What matters for this course is that the regions are conceptually distinct and that each behaves differently under growth, sharing, and protection.

4.3 The machine state

The address space is only half of what a process is. The other half is the machine state: the values of the CPU registers at any given moment.

  • Program counter (PC, or rip on x86-64). The address of the next instruction to execute.
  • Stack pointer (SP, or rsp). The current position in the process's stack, which changes with every function call and return.
  • Frame pointer (FP, or rbp). A stable reference to the current stack frame, used by debuggers and by some compilers for stack unwinding.
  • General-purpose registers. Temporary values used by the currently executing code. The precise set depends on the architecture: x86-64 has sixteen, ARM64 has thirty-one.
  • Status register (flags). Condition codes (zero, carry, overflow), interrupt mask, and the current privilege level.

Together, the address space and the machine state are the process's context. If the operating system saves the context of one process and restores the context of another, the second process resumes exactly where it left off — as if nothing had happened. This is the trick that underlies multiprogramming, and it is examined in detail in Lesson 3.5.

4.4 What resources a process owns

Beyond memory and registers, each process holds a collection of operating-system resources. The set is worth cataloguing because it defines what the kernel must track — and because each element will come up again later in the course.

Resources owned by a process
Resource Description Covered in
Process ID (PID) A unique integer identifying the process within the system. This lesson
Address space The virtual memory mappings the process can access. Module 3
File descriptors Handles to open files, pipes, sockets, and devices. Module 4
Signal handlers Functions or dispositions associated with asynchronous signals. This unit
Credentials Real, effective, and saved user and group IDs. Module 5
Working directory The current directory used to resolve relative paths. Unit 2
Resource limits Maximum CPU time, file size, memory, open files, and so on. Module 4
Environment The set of name-value strings inherited from the parent. This lesson

On Linux, most of this information is visible through the /proc/<pid> pseudo-filesystem. The directory contains a status file with the process's credentials and state, a maps file describing memory regions, an fd subdirectory listing open file descriptors, and many others. The interactive activity at the end of this lesson will have you inspect several of them.

4.5 Process creation and termination

Processes do not appear spontaneously. On a UNIX-like system, every process except the very first is created by another process, called its parent. The creating operation is fork.

fork: the creation primitive

fork is unusual among system calls because it returns twice: once in the parent and once in the child. Both processes resume from the same point in the code, but the return value distinguishes them. The parent receives the child's PID; the child receives zero. This asymmetry lets a single piece of code branch into parent behaviour and child behaviour.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        printf("child  · pid = %d, parent = %d\n", getpid(), getppid());
    } else {
        printf("parent · pid = %d, child = %d\n", getpid(), pid);
    }
    return 0;
}

The two printf lines may appear in either order. The scheduler does not guarantee that the parent runs first after fork returns; either process may be dispatched next. This non-determinism is a recurring theme in concurrent programming and one you will see again in Unit 6.

Copy-on-write makes fork efficient

Early UNIX implementations physically copied the entire address space on every fork. That is obviously wasteful — the child usually calls exec immediately afterward, discarding everything that was copied. Modern Linux uses copy-on-write: the parent's pages are marked read-only and shared with the child, and a page is copied only when either process attempts to write to it. Programs that fork and then exec without modifying much memory pay almost nothing.

exec: replacing the program

After fork, the parent has three options: continue running alongside the child, wait for the child to finish using wait, or terminate independently. The child, if it wants to run a different program, typically calls one of the exec family of system calls, which replaces the child's address space with that of a new program.

#include <unistd.h>
#include <stdio.h>

int main(void) {
    /* On success, execve never returns — the process image is replaced */
    char *args[] = { "ls", "-l", "/home", NULL };
    execvp("ls", args);

    /* If we reach this line, exec failed */
    perror("execvp");
    return 1;
}

The combination fork + exec is how a shell runs a command. The shell forks a child, and the child immediately replaces its image with the requested program. The shell itself continues running and waits for the child to finish.

exit and wait: termination and reaping

A process terminates when it calls exit, returns from main, or is killed by a signal. On termination the kernel releases the process's resources. The process's exit status is held briefly for the parent to collect through wait; a process that has terminated but whose status has not yet been collected is called a zombie.

The wait call serves two purposes. It blocks the parent until a child terminates, and it reaps the child's entry from the kernel's process table. Without a wait, a terminated child would remain a zombie indefinitely, consuming a small amount of kernel memory. A parent that creates many children without ever reaping them can exhaust the process table and prevent new processes from being created — a real failure mode worth knowing about.

4.6 Lifecycle events

Summarising, a process passes through a predictable sequence of significant events:

  1. Creation A parent calls fork. The kernel allocates a PID, an address space, and a process control block (PCB). The process enters the New state.
  2. Admission Resources are confirmed. The process is moved to the Ready queue, where it waits for the scheduler to run it.
  3. Execution The scheduler dispatches the process; it runs, blocks on I/O, is preempted, and repeats as the OS decides. The next four lessons examine each of these in turn.
  4. Replacement (optional) A child process may call exec to replace its own image with a new program, inheriting the same PID.
  5. Waiting A parent that needs its child's result blocks in wait until the child terminates.
  6. Termination The process exits, normally or via a signal. Its resources are released; its exit status is held for the parent.
  7. Reaping The parent collects the exit status. The process's entry is finally removed from the kernel's tables.

Each of these events corresponds to a state transition that the kernel tracks. Lesson 3.2 introduces the formal state model; the events above are the "why" behind the states.

4.7 The first process

There is one process on a Linux system that was not created by fork. At boot, the kernel creates PID 1 — the init process — directly. Its responsibilities are to initialise the system, start the services the system needs, and (through the modern init systems) become the ancestor of every other process that will ever run. If PID 1 terminates, the kernel panics: no other process can take over the tree.

On modern Linux distributions, PID 1 is usually systemd. On older or minimal systems it might be sysvinit, upstart, or a shell. The specific program is less important than the role: PID 1 is the root of the process tree, the ancestor of everything.

You can inspect this tree with pstree or ps -ejH. The hierarchy that appears shows the parent-child relationships between all processes currently running. Every process other than PID 1 has a parent — and the chain always leads back to PID 1.

4.8 Why processes are the fundamental abstraction

It is worth stepping back to ask why the process is so central. Several properties make it the right unit of organisation:

  • Isolation. Each process has its own address space, so a bug in one cannot corrupt another. The kernel enforces this on every memory access.
  • Ownership. Every resource — file descriptors, memory, CPU time — is owned by a process. This makes resource accounting, limits, and cleanup tractable.
  • Schedulability. The CPU runs one process at a time; the scheduler's job is to choose which. The process is the natural unit for that decision.
  • Failure containment. When a process crashes, the kernel can terminate it cleanly without affecting other processes. This is what keeps a bug in one application from taking down the whole system.
  • Composability. Processes can be combined through pipes and sockets, each running independently, which is the foundation of the UNIX programming model.

Threads, which we will study in Unit 4, relax the isolation property to gain performance. But the process remains the fundamental unit — threads live inside processes, sharing their resources and their fate.

4.9 The process API in one place

For reference, the essential process-management system calls on Linux:

Core process-management system calls
Call Purpose Notes
fork Create a new process Returns 0 in child, child PID in parent, −1 on error
execve Replace the current process image Never returns on success; the process is a "new" program with the same PID
wait, waitpid Wait for a child to terminate Blocks the parent; reaps the child's process-table entry
exit, _exit Terminate the process Runs atexit handlers, flushes stdio, then enters the kernel
getpid, getppid Get the process or parent PID Simple, fast, always successful
kill Send a signal to a process Named for its most common use but can send any signal
clone Create a process or thread (Linux-specific) Generalisation of fork; used by pthreads under the hood

We will return to these calls repeatedly. For now, the goal is to recognise them and to understand that they are the interface between user space and the kernel's process manager.

05 Worked example

To make the process concept concrete, trace what happens when a shell runs a simple command.

ls /home at the shell prompt

  1. The shell reads a line. It is already a process — likely with PID 4321, say — with its own address space, file descriptors, and open file handles.
  2. The shell calls fork. The kernel duplicates the shell's address space (using copy-on-write, so no physical pages are copied until a write occurs). A new PCB is created, a new PID is assigned — say 4322 — and the child process enters the Ready queue.
  3. Both processes resume from the fork return. The parent's copy of fork returns 4322 (the child's PID). The child's copy returns 0.
  4. The child branches. Because fork returned 0, the child knows it is the child. It calls execve("/bin/ls", ["ls", "/home"], environ).
  5. The kernel performs the exec. The child's old address space is discarded. A new address space is created from the /bin/ls executable: text segment from the file, data segment from the file, zeroed BSS, a fresh stack, a fresh heap. The PID remains 4322 — the process is the same, only its image has changed.
  6. ls runs. It opens /home, reads the directory entries, and writes them to its standard output — which is still the terminal, since file descriptors survived exec.
  7. ls exits. The kernel marks the process as terminated, retains the exit status for the parent, and releases most resources. The process is now a zombie.
  8. The parent collects. The shell calls wait4, retrieves the status, and reaps the zombie. The process entry is finally removed.
  9. The shell prompts again. It loops back to read the next command.

Why the fork/exec split matters

Because fork and exec are separate, the child has a chance to modify its environment between the two calls — change directory, redirect file descriptors, set resource limits, drop privileges. This is how the shell implements redirection and pipelines, and how privileged programs drop root privileges before executing untrusted code. A single "spawn" system call could not offer this flexibility.

Following the example in your VM

You can observe this sequence directly on your course VM. In a terminal:

# See the shell's PID
$ echo $$
4321

# Run ls under a trace of the fork/exec/wait family
$ strace -f -e trace=clone,execve,wait4 ls /home

# The output will show clone() (the underlying call fork uses),
# execve() for /bin/ls, and eventually wait4() in the parent.

The clone system call is what fork uses on Linux; fork is a thin wrapper around it. Seeing it in the trace is a direct confirmation of the process model.

06 Interactive activity

Activity · explore processes on a live Linux system

Open a terminal in your course VM and work through the following. Record what you see at each step — the goal is to connect the concepts of this lesson to a running system.

  1. See your own process. Run echo $$. The number printed is the PID of your current shell. Note it down.
  2. See the process tree. Run pstree -p | head -30. Find your shell's PID in the tree and trace back to PID 1. What is PID 1 on your system? (Modern Ubuntu uses systemd.)
  3. Inspect the process's memory layout. Run cat /proc/$$/maps | head -20. Each line describes one region of the shell's address space. Can you identify the text, heap, stack, and shared-library mappings?
  4. Inspect open files. Run ls -l /proc/$$/fd. What file descriptors does your shell currently have open? Why are 0, 1, and 2 there?
  5. Watch a process start and stop. In one terminal, run sleep 300 &. Note the PID it prints. In the same terminal, run ps -p <PID> -o pid,ppid,stat,cmd. What state is the process in? Then run kill <PID> and check again.
  6. Create a child. Run bash -c 'echo parent=$$; (echo child=$BASHPID)'. Which PID is the parent, which is the child?
  7. Observe a zombie. Write a small C program that forks a child, lets the child exit immediately, and has the parent sleep without calling wait. Compile it with the course flags, run it, and while it is sleeping, run ps aux | grep Z in another terminal. What does the state column show?
  8. Trace the fork/exec cycle. Run strace -f -e trace=clone,execve,wait4 bash -c 'ls /tmp'. Identify the clone call (fork), the execve call (exec), and the wait4 call (wait). This is the full lifecycle of a short-lived command.

Reflect: for each item, identify which of the process resources listed in section 4.4 you were observing. Which resource was hardest to see directly, and why?

07 Knowledge check

Check your understanding

Question 1

A user runs the same program twice. Are the two executions the same process, the same program, both, or neither? Explain.

Show answer
They are the same program but two different processes. The program is the executable file on disk — a passive set of instructions. Each execution creates a distinct process with its own PID, address space, register state, and file descriptors. Neither process is aware of the other; a write by one does not affect the other unless they explicitly share a resource (for example, a file they both have open in append mode, or a shared-memory segment).

Question 2

In the layout of a process address space, why do the heap and stack grow toward each other rather than away?

Show answer
So that the large unused space in the middle can serve either purpose. A program with heavy dynamic allocation but shallow recursion gets more heap; a program with deep recursion but little allocation gets more stack. By placing the two growing regions back-to-back and pointing them at each other, the kernel lets the workload decide where the boundary falls. The alternative — reserving fixed maximum sizes for each — would waste address space in every program.

Question 3

What is a zombie process, and why does the kernel keep one around?

Show answer
A zombie is a process that has terminated but whose exit status has not yet been collected by its parent. The kernel keeps a minimal entry in its process table — containing the PID and exit status — so that when the parent eventually calls wait, the status is available. Once reaped, the entry disappears. Zombies consume a small amount of kernel memory but no CPU or address space; a parent that never reaps its children can accumulate them until the process table is exhausted and new processes cannot be created.

Question 4

Explain the purpose of copy-on-write in fork, and why it makes the fork/exec pattern efficient.

Show answer
Copy-on-write marks the parent's writable pages as read-only and shares them with the child. Neither process can write to the shared pages, so both observe the same content. When either process tries to write, the kernel copies just that page and updates the writer's page table. This is efficient because most children immediately call exec, which discards the child's entire address space anyway — with copy-on-write, the kernel never had to copy it. Programs that fork without exec (a web server forking a worker, for example) also benefit, because pages that neither process modifies remain shared indefinitely.

Question 5

Why is the process tree always rooted at PID 1, and what happens if PID 1 terminates?

Show answer
PID 1 is created directly by the kernel at boot, before any other process exists. Every subsequent process is created by a fork from an existing process, so the parent-child relationships form a tree whose root is PID 1. If PID 1 terminates, the kernel panics: there is no process left to take over the tree, and the system's assumptions about process ancestry are broken. This is why init systems are carefully written and why PID 1 is protected from most signals — a signal that would ordinarily terminate a process is ignored by PID 1 unless it has installed a handler.

08 Unit discussion

Discussion prompt

The fork/exec pair is the standard way to create a process on UNIX-like systems. Windows, by contrast, offers a single CreateProcess call that both creates the process and loads the program. Meanwhile, some modern language runtimes and container environments use clone directly, and others (such as WebAssembly runtimes) discard the fork model entirely in favour of instantiating an isolated module.

Argue for or against the UNIX approach. Consider: the ability to modify the child's environment between fork and exec; the cost of duplicating an address space that will be immediately discarded; the conceptual simplicity for programmers; and the way each model handles the case of "run a program, but with one small tweak to the environment."

Post your response to the Unit 3 discussion forum and reply to at least two classmates. Supports CLO2 and CLO8.

09 Practice exercise

This ungraded exercise consolidates the lesson and prepares you for Lab 2. Aim to spend 45–60 minutes on it.

  1. Write and run a fork program. Compile and run the fork example from section 4.5. Run it five times. Does the output order change between runs? Why or why not?
  2. Add a loop. Modify the program so that both parent and child print their PID five times each. How many distinct lines do you see? How many would you see if you called fork twice in a row?
  3. Create a zombie. Write a program in which the parent forks a child, the child exits immediately, and the parent sleeps for 30 seconds without calling wait. In another terminal, run ps -ef | grep Z while the parent sleeps. What do you see? Then modify the program to call wait and confirm the zombie disappears.
  4. Trace the exec. Run strace -f -e trace=execve bash -c 'echo hello'. Which execve calls appear? What does the trace tell you about how the shell runs a builtin versus an external program?
  5. Inspect /proc. Explore the /proc/self directory of your shell. Which of the resources from the table in section 4.4 can you find? Open status, maps, and the fd subdirectory. Describe what each shows.
  6. Write a small shell. Write a C program that reads a line from standard input, splits it into a command and arguments, forks a child, and uses execvp in the child to run the command. The parent should wait for the child and then loop to read the next line. This is the essence of a shell, and it is the seed of the process-management exercises in Lab 2.
  7. Reflect. In your own words, describe the difference between a process and a program. Give one example of two processes running the same program, and one example of a single process that has run code from multiple programs over its lifetime.

10 Summary

  • A program is a passive file; a process is that program in execution, with its own address space, registers, and resources.
  • The address space has five conventional regions: text, data, BSS, heap (growing upward), and stack (growing downward).
  • The machine state — program counter, stack pointer, general registers, status register — is the other half of the process. Together with the address space, it constitutes the process context.
  • A process owns many resources beyond memory: PID, file descriptors, signal handlers, credentials, working directory, resource limits, and environment.
  • On UNIX-like systems, processes are created by fork, typically followed by exec, and terminate through exit. A terminated process whose status has not been collected is a zombie.
  • Copy-on-write makes fork efficient by deferring the copy of the address space until either process writes.
  • All processes form a tree rooted at PID 1, which is created directly by the kernel at boot.

Looking ahead

Lesson 3.2 examines the states a process occupies during its life and the transitions between them. The lifecycle events from section 4.6 correspond directly to the state transitions you will see next.

11 References

  • Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapters 4 and 5. Available online at ostep.org.
  • Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 3.
  • Love, R. Linux Kernel Development, 3rd ed., Chapter 3 (Process Management). Addison-Wesley, 2010.
  • Linux man-pages: man 2 fork, man 2 execve, man 2 wait, man 2 exit, man 5 proc, man 1 ps, man 1 pstree, man 1 strace.
  • Bach, M. J. The Design of the UNIX Operating System, Chapter 6. Prentice Hall, 1986. (Classical reference for the process model.)
👤