Operating-System Foundations

The operating system is the referee that gives every program a turn on the CPU.

Learning Objectives

Explain programs, processes, address spaces, resources, process states and lifecycles.

Explain threads, their stacks and thread-local storage; user-mode versus kernel threads; cores and hardware threads.

Describe context switching, time slicing, preemptive and cooperative scheduling, priorities and policies.

Create, join, detach and terminate threads and processes; understand isolation, IPC (pipes, named pipes, shared memory, sockets, signals), handles and file descriptors, resource inheritance, pools, and portability.

Plain-Language Explanation

The operating system is the referee that gives every program a turn on the CPU. A process is a running program in its own sealed room: its own memory, its own open files. A thread is one worker inside that room; several threads share the room and everything in it. The OS keeps a list of who is ready to work and switches between them many times per second — so quickly that they seem to run at once. When a process needs to talk to another process, it must use an official channel — a pipe, a socket, shared memory — because the walls between rooms are real.

Precise Technical Explanation

Program versus process A program is an executable file. A process is an instance of it being executed: an address space, one or more threads, and kernel resources (file descriptors, sockets, signal handlers, credentials).

Process address spaces and resources Each process has a private virtual address space (code, data, heap, stacks, mapped libraries) and a table of resources. One process cannot read another's memory unless they explicitly share it.

Process states and lifecycle New → ready → running ⇄ (blocked/waiting) → terminated. A terminated POSIX process stays a *zombie* until its parent collects its exit status with wait.

A thread is an independent flow of control within a process: its own program counter, registers and stack; shared code, heap, globals and file descriptors.

Thread versus process shared with sibling threads shared variables (needs synchronization) a crash usually kills the whole process a crash kills only that process User-mode and kernel-mode threads

Kernel threads are scheduled by the OS (Linux, Windows, macOS threads; one-to-one with pthread/std::thread). User-mode threads (green threads, fibers, goroutines, Java virtual threads) are scheduled by a runtime onto a smaller number of kernel threads (M:N). User-mode switching is cheaper, but a blocking system call can stall the carrier kernel thread unless the runtime handles it.

Thread stacks and thread-local storage Each thread has its own stack (Linux default 8 MiB virtual; Windows 1 MiB; configurable at creation). Thread-local storage gives each thread its own copy of a variable (_Thread_local/thread_local, threading.local, ThreadLocal<T>).

CPU cores and hardware threads A core executes instructions; with simultaneous multithreading (Intel Hyper-Threading, AMD SMT) one core presents two hardware threads that share execution units. The build machine has 4 cores × 2 = 8 hardware threads. Two hardware threads on one core are not twice as fast as one.

Switching the CPU from one thread to another saves registers, stack pointer and program counter, and restores the next thread's. Switching between processes also switches page tables. Direct cost: roughly 1–5 µs; indirect cost: cold caches and TLBs.

Scheduling and time slicing The scheduler chooses which ready thread runs on each core. Time slicing gives each thread a quantum (milliseconds) before it may be preempted so others get a turn.

The OS may interrupt a running thread at any time (timer interrupt, higher-priority thread becoming ready). This is why any two instructions of different threads can interleave.

Threads (or tasks) give up the CPU only voluntarily. Classic Mac OS and Windows 3.1 worked this way; today it is used inside runtimes (event loops, coroutines) and some embedded schedulers.

Priorities and scheduling policies Linux: SCHED_OTHER (fair sharing with nice values), SCHED_FIFO/SCHED_RR (real-time, strict priority), SCHED_DEADLINE. Windows: 32 priority levels with dynamic boosts. Priorities do not guarantee order and can cause starvation and priority inversion (Module 5).

Thread creation and termination POSIX pthread_create / pthread_exit / return from the start routine; C++ std::thread; Java Thread.start(); Python threading.Thread.start(). A thread should end by returning; forcibly killing threads is dangerous (Module 14).

Process creation and termination POSIX fork + exec (or posix_spawn); Windows CreateProcess; language wrappers (subprocess, ProcessBuilder, child_process). A process ends with exit/return from main or a fatal signal.

Join and detach concepts Joining waits for a thread to finish and collects its result. Detaching gives up that right: the thread runs independently and its resources are released when it ends. A joinable thread that is never joined leaks resources (and in C++ destroying a joinable std::thread calls std::terminate).

Separate address spaces, permissions and resource limits mean one process's bug cannot corrupt another's memory. Browsers and servers use this deliberately (site isolation, pre-fork workers).

Inter-process communication (IPC) Pipes, named pipes, sockets, shared memory, message queues, signals, files, and higher-level protocols (gRPC, D-Bus).

Pipes and named pipes A pipe is a one-way kernel byte buffer between related processes (pipe(), the shell's |). A named pipe (FIFO on POSIX, \\.\pipe\name on Windows) has a name, so unrelated processes can connect.

Processes map the same physical pages (shm_open + mmap, Windows file mappings). Fastest IPC, but requires process-shared synchronization.

Bidirectional endpoints for local (UNIX-domain) or network (TCP/UDP) communication — the basis of client/server concurrency.

Signals and notifications POSIX signals (SIGINT, SIGTERM, SIGCHLD) asynchronously interrupt a process; handlers may only use async-signal-safe functions. In multithreaded programs signals go to an arbitrary thread that does not block them; a common pattern is a dedicated thread calling sigwait. Windows uses console control events and other notification mechanisms instead.

Handles and file descriptors Small integers (POSIX) or opaque HANDLEs (Windows) referring to kernel objects: files, sockets, pipes, events, threads, processes. All threads of a process share the descriptor table.

Child processes inherit open descriptors (unless close-on-exec), environment, working directory and signal dispositions. After fork in a multithreaded program only the calling thread exists in the child — locks held by other threads stay locked forever, so the child should call only async-signal-safe functions until exec.

Process pools and worker pools Creating processes or threads per task is expensive; pools keep workers alive and feed them tasks from a queue (Modules 15–16).

OS differences and portability POSIX threads are not part of standard C (C11 has optional <threads.h>), and are not native on Windows. Thread stack sizes, priority ranges, signal handling and process creation differ across Linux, macOS and Windows. Portable languages (C++, Java, Go, Rust,.NET) hide much of this behind their own APIs.

Why It Matters in Real Systems

Choosing threads or processes decides failure isolation, memory use, communication cost and scalability. Understanding scheduling explains latency spikes, priority inversion and why "it ran in order on my machine" proves nothing.

Key Terms

Running program with a private address space and resources Flow of control within a process; shares its memory Kernel / user thread Scheduled by the OS / by a runtime SMT execution context on a core

Saving one thread's CPU state and loading another's Quantum a thread may run before preemption

Wait for completion / release the thread to run independently File descriptor / handle Reference to a kernel object

Diagram or Execution Timeline

flowchart LR
  N[new] --> R[ready]
  R -- scheduler dispatches --> U[running]
  U -- time slice ends / preempted --> R
  U -- waits for I/O, lock, sleep --> B[blocked]
  B -- event occurs --> R
  U -- returns / exits --> T[terminated]

Syntax and API Overview

The specification's POSIX threads example. POSIX threads are not part of standard C; they are a POSIX API available on Linux, macOS and other Unix-like systems (Windows needs a compatibility layer). Build with -pthread.

pthread_create(&thread, attributes, start_routine, argument) starts worker(argument) in a new thread and returns 0 on success (an error number otherwise — not errno). pthread_join(thread, &result) blocks until the thread ends. Because the join happens before the second puts, "Worker running" is guaranteed to appear before "Worker finished".

#include <pthread.h>
#include <stdio.h>

static void *worker(void *arg) {
    (void)arg;
    puts("Worker running");
    return NULL;
}

int main(void) {
    pthread_t thread;

    if (pthread_create(&thread, NULL, worker, NULL) != 0) {
        return 1;
    }

    if (pthread_join(thread, NULL) != 0) {
        return 1;
    }

    puts("Worker finished");
    return 0;
}
Worker running
Worker finished

Complete Code Example

Threads share memory; processes do not. One C program (Linux) runs the same increment in a thread and in a forked child, passes an argument and returns a result through pthread_join, and reads the kernel's context-switch counters:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

static int shared_value = 100;
static _Thread_local int per_thread = 0;

struct Job { int id; int add; };

static void *worker(void *arg)
{
    struct Job *job = arg;                       /* argument passed by pointer */
    shared_value += job->add;                    /* same memory as main (safe here: main is waiting in join) */
    per_thread = job->id;                        /* this thread's own copy */
    int *result = malloc(sizeof *result);        /* result handed back through pthread_join */
    if (result) *result = job->id * 10;
    printf("thread %d: shared_value=%d per_thread=%d\n", job->id, shared_value, per_thread);
    return result;
}

static void print_switches(const char *who)
{
    FILE *f = fopen("/proc/self/status", "r");
    char line[128];
    while (f && fgets(line, sizeof line, f)) {
        if (strstr(line, "ctxt_switches")) printf("%s %s", who, line);
    }
    if (f) fclose(f);
}

int main(void)
{
    struct Job job = { .id = 1, .add = 5 };
    pthread_t t;
    if (pthread_create(&t, NULL, worker, &job) != 0) return 1;
    void *re
…

Line-by-Line Explanation

pthread_create(&t, NULL, worker, &job) passes a pointer to job; the struct stays valid because main waits for the thread.

The worker modifies shared_value — the *same* variable main sees — and its own per_thread copy.

The result is heap-allocated so it outlives the thread; pthread_join delivers the pointer, and main frees it.

After fork, the child has a *copy* of memory: its write never reaches the parent.

/proc/self/status reports voluntary context switches (the thread blocked — e.g. waiting in join or for I/O) and involuntary ones (preempted).

Output

Output (one real run — the context-switch counts vary):

thread 1: shared_value=105 per_thread=1
main after join: shared_value=105, main's per_thread=0, result=10
child process: shared_value=1105
parent after child exits: shared_value=105 (the child changed its own copy)
online CPUs: 8
main voluntary_ctxt_switches:	12
main nonvoluntary_ctxt_switches:	0

Interleaving or State Trace

(created, ready) blocked in pthread_join runs: shared_value 100 → 105, prints resumes, prints 105 copy of memory with 105 blocked in waitpid

105 → 1105 in its own copy, prints, exits

Correctness and Safety Analysis

The worker's write to shared_value is safe only because main does not touch it until after pthread_join, and pthread_join guarantees that everything the thread did *happens before* the join returns. If main read shared_value while the worker ran, that would be a data race (undefined behavior in C — Module 3).

Common Mistakes and Misconceptions

Passing the address of a loop variable or a local that goes out of scope before the thread reads it.

Treating the return value of pthread_create as errno-style (−1); it returns the error number.

Forgetting -pthread (link errors or subtly wrong code generation on some systems).

Never joining or detaching threads (resource leak).

Calling non-async-signal-safe functions in a child after fork from a multithreaded program.

Failure Modes and Debugging Steps

Thread creation fails with EAGAIN when limits are reached (ulimit -u, memory for stacks).

Hung program: a thread blocked forever (inspect with gdb thread apply all bt, /proc/<pid>/task/*/stack, jstack, dotnet-stack).

Zombie processes: a parent never called wait.

Performance and Scalability Implications

Thousands of OS threads are feasible; millions are not (stack memory, scheduler overhead). Thread creation costs tens of microseconds; reuse threads with pools. Context switches and cache misses dominate when there are far more runnable threads than cores.

Security Considerations

Process boundaries are a security boundary; threads are not. Inherited descriptors can leak capabilities to child processes — use close-on-exec. Signal handlers that do too much are a source of exploitable bugs.

Platform and Runtime Caveats

/proc, fork and _Thread_local behavior shown are Linux/glibc; Windows uses CreateThread/_beginthreadex, CreateProcess and __declspec(thread)/thread_local.

Default stack sizes and maximum thread counts differ by OS and configuration.

Real-World Applications

Chrome's multi-process architecture, PostgreSQL's process-per-connection, nginx's worker processes with event loops, Java servers' thread pools, Android's main thread plus workers.

Best Practices

Check every creation and join result.

Give each thread a clear owner that joins it.

Pass data by value or via objects whose lifetime clearly exceeds the thread's use.

Prefer processes for isolation, threads for shared-memory parallelism, pools for repeated work.

Use close-on-exec for descriptors.

Exercises

E1 · Beginner. What would happen if main returned immediately after pthread_create without joining?

Answer: Returning from main calls exit, which terminates the whole process — including the worker, possibly before it prints anything.

E2 · Intermediate. Explain why the child's shared_value started at 105 rather than 100.

Answer: fork copies the parent's memory at the moment of the call; by then the worker thread (already joined) had changed it to 105.

E3 · Advanced challenge. Start 4 threads, each passed a *different* integer argument, without allocating, and without the classic "all threads see the last loop value" bug.

Answer: Store the arguments in an array that outlives the threads (int ids[4]) and pass &ids[i], or pass the integer itself cast through intptr_t ((void *)(intptr_t)i). Do not pass &i of the loop variable, which changes before the threads read it.

Quiz

? What do threads of the same process share?
+ The heap, global variables and open file descriptors :: They live in one address space.
- Their stacks :: Each thread has its own stack (though other threads can reach it through pointers).
- Their program counters :: Each thread has its own registers.
- Nothing; threads are isolated like processes :: That describes processes.

? pthread_join(t, &ret) guarantees that...
+ the thread has finished and its writes are visible to the joining thread :: Join establishes happens-before.
- all other threads have finished :: Only t.
- the thread's result is non-NULL :: The thread may return NULL.
- no data race can happen anywhere in the program :: Join orders only this thread's completion.

? Which are forms of inter-process communication? (select all that apply)
+ Pipes :: One-way kernel buffers.
+ Shared memory mappings :: Same physical pages mapped in both processes.
+ Sockets :: Local or network endpoints.
- A global variable :: Globals are per process after fork.

Summary

Processes isolate; threads share. The OS scheduler preempts threads at arbitrary points, switches contexts and enforces policies and priorities. Threads are created, joined or detached; processes are forked/spawned and waited for. IPC — pipes, sockets, shared memory, signals — connects processes. POSIX threads are a platform API, not standard C.

Further Reading

Michael Kerrisk, *The Linux Programming Interface*, chapters 29–33 (threads).

man 7 pthreads, man 2 clone, man 7 sched.

Microsoft Docs, "Processes and Threads".

Arpaci-Dusseau, *OSTEP*, "Scheduling" chapters.