Operating-System Process Execution
The operating system is the program that runs other programs.
Learning Objectives
Distinguish program and process; describe process creation, identifiers and states (ready, running, blocked, terminated).
Explain scheduling, context switches, user and kernel mode, system calls and traps.
Use command-line arguments, the environment, standard streams, file descriptors and exit statuses.
Explain parent/child processes, fork/exec/wait on POSIX and CreateProcess on Windows.
Describe loading, the dynamic loader, start-up routines before main, global constructors, termination and cleanup handlers.
Use signals, pipes, shared memory, memory-mapped files and copy-on-write; understand isolation, resource limits and core dumps.
Plain-Language Explanation
The operating system is the program that runs other programs. To start yours it creates a process — a container with its own memory, an ID number, a list of open files and a current state — loads your executable into it, and lets the CPU run it for short time slices, switching between processes so quickly that they appear to run at once. Whenever your program needs something only the OS can do — read a file, print text, get more memory, start another program — it makes a system call: a controlled jump into the kernel and back.
Precise Technical Explanation
Program versus process A *program* is a passive file. A *process* is an active instance with an address space, one or more threads, kernel resources (file descriptors, signal handlers, credentials) and accounting data. The kernel tracks it in a process control block (Linux: struct task_struct).
Process creation overview POSIX splits creation in two: fork() duplicates the calling process; exec() replaces the duplicate's program image with a new one. Windows does both at once with CreateProcess. Modern code often uses posix_spawn (one call, no full duplicate).
Process identifiers and process states Each process has a PID and a parent PID (PPID). Threads within a process share the PID (on Linux each has its own TID).
Ready, running, blocked/waiting, terminated Ready — runnable, waiting for a CPU.
Running — executing on a CPU core.
Blocked — waiting for an event (disk I/O, network data, a mutex, a timer).
Terminated — finished; on POSIX it stays a *zombie* until the parent collects its status with wait.
The scheduler picks which ready thread runs on each core. Linux's default policy (CFS, replaced by EEVDF in kernel 6.6) shares CPU time fairly according to priority (nice); real-time policies (SCHED_FIFO, SCHED_RR) run strictly by priority. Preemption happens on timer interrupts and when higher-priority work becomes ready.
Switching from one thread to another saves the old thread's registers (including stack and program counter) into the kernel, restores the new one's, and — if the threads belong to different processes — switches the address space (page-table base register: CR3 on x86, TTBR0 on Arm). Direct cost: about a microsecond; indirect cost: cold caches and TLB entries.
User mode versus kernel mode CPUs run in privilege levels. Applications run in user mode (x86 ring 3, Arm EL0), where privileged instructions and kernel memory are forbidden. The kernel runs in kernel mode (ring 0, EL1). The only ways in are system calls, exceptions (page faults, division by zero) and interrupts.
System calls and traps A system call is a deliberate trap: the program puts a call number and arguments in registers and executes a special instruction (syscall on x86-64, svc #0 on AArch64, ecall on RISC-V). The CPU switches to kernel mode at a fixed entry point; the kernel validates the arguments, performs the work and returns. C library functions such as write, open and mmap are thin wrappers; printf and malloc call them only sometimes.
Command-line arguments and environment argc/argv are the standard C entry-point parameters; the environment (KEY=value strings) is reached through getenv or the non-standard third main parameter / environ. Both are copied onto the new process's stack by execve (Module 3).
Standard input/output/error Every process starts with three open streams: descriptor 0 (stdin), 1 (stdout), 2 (stderr). The shell connects them to the terminal, files (>, <) or pipes (|). In C, stdout is line-buffered on a terminal and fully buffered otherwise; stderr is unbuffered.
File descriptors/handles A file descriptor is a small integer indexing the process's table of open files, pipes, sockets and devices. Windows uses HANDLEs. Descriptors are inherited across fork and (unless O_CLOEXEC) across exec, and limited per process (RLIMIT_NOFILE).
Process exit status return n from main or exit(n) reports a status to the parent; only the low 8 bits are kept on POSIX. 0 means success. A process killed by a signal reports that instead; shells show it as 128 + signal number (139 = SIGSEGV, 134 = SIGABRT).
Parent/child processes The parent receives the child's PID from fork, can wait/waitpid for it, and is notified with SIGCHLD. Orphans are re-parented to init (PID 1) or a subreaper.
fork, exec and wait on POSIX Windows process creation concepts
CreateProcessW(application, command line, security attributes, inherit handles, flags, environment, directory, startup info, process information) creates a new process running a new image directly; there is no fork. Parameters arrive as one command-line string that the C runtime splits into argv. The parent waits with WaitForSingleObject and reads the status with GetExitCodeProcess.
Loading executables and shared libraries execve makes the kernel read the ELF headers, map the PT_LOAD segments, map the interpreter named in PT_INTERP (/lib64/ld-linux-x86-64.so.2), build the stack and jump to the interpreter's entry point.
Dynamic loader responsibilities The dynamic loader maps every NEEDED library (searching RPATH/RUNPATH, LD_LIBRARY_PATH, the cache in /etc/ld.so.cache, default directories), applies relocations, resolves symbols (now, or lazily through the PLT), runs library initialisers, and finally jumps to the program's entry point _start.
Signals and termination (POSIX overview) Signals are asynchronous notifications: SIGINT (Ctrl-C), SIGTERM (polite termination request), SIGKILL (cannot be caught), SIGSEGV, SIGABRT, SIGCHLD. A handler may only call *async-signal-safe* functions (write, _exit — not printf or malloc); the usual pattern is to set a volatile sig_atomic_t flag and handle it in the main loop.
Process isolation and permissions Separate address spaces mean one process cannot read another's memory; the kernel checks user IDs, capabilities, and mandatory access control (SELinux, AppArmor) for every privileged operation. Containers add namespaces and cgroups; sandboxes add seccomp syscall filters.
Pipes and inter-process communication A pipe is a one-way kernel buffer between two descriptors. Other IPC mechanisms: FIFOs (named pipes), UNIX-domain sockets, message queues, shared memory, signals; Windows has named pipes, ALPC and memory-mapped sections.
Two processes map the same physical pages (mmap(MAP_SHARED), shm_open, System V shmget; Windows CreateFileMapping). It is the fastest IPC — no copying — but needs synchronisation (process-shared mutexes, atomics, semaphores).
mmap maps a file's pages into the address space: reads fault pages in on demand, writes to a MAP_SHARED mapping go back to the file. Used for loading executables, databases (LMDB, SQLite's mmap mode) and large read-only data.
After fork, parent and child share all pages read-only; the first write to a page by either process triggers a page fault, and the kernel copies just that page. This makes fork cheap and is why the same virtual address shows different values in parent and child after a write.
Process resource limits getrlimit/setrlimit (ulimit in the shell) cap stack size, open files, address space, CPU time, core file size and more. cgroups limit groups of processes (memory, CPU, I/O).
Crash dumps and core dumps On a fatal signal the kernel can write a core dump — the process's memory and registers — if RLIMIT_CORE allows (ulimit -c unlimited); systemd-based systems route them to coredumpctl. Windows writes minidumps via Windows Error Reporting or MiniDumpWriteDump. Load the dump with the matching unstripped binary in a debugger to see every thread's stack.
Startup routines before main _start (from crt1.o) aligns the stack and calls __libc_start_main, which initialises the C library (stdio, locale, TLS, stack-protector canary), runs initialisers (.init_array: C++ global constructors and __attribute__((constructor)) functions), and calls main(argc, argv, envp).
Runtime initialization and global constructors C++ global objects are constructed before main in the order they appear *within one file*; the order *between files* is unspecified (the "static initialization order fiasco"). Use function-local statics for dependencies across files.
Program termination and cleanup handlers exit() (or returning from main) runs atexit handlers in reverse registration order, destroys C++ static objects, flushes and closes stdio streams, then calls _exit, which returns to the kernel without any user-space cleanup.
Normal exit versus abnormal termination Runs atexit handlers?
Flushes stdio?
return from main / exit() _exit() / _Exit() killed by SIGABRT fatal signal (SIGSEGV, SIGKILL) killed by the signal only at_quick_exit handlers
flowchart LR
N[new] --> R[ready]
R -- scheduled --> U[running]
U -- time slice expires / preempted --> R
U -- waits for I/O, lock, sleep --> B[blocked / waiting]
B -- event happens --> R
U -- exit --> T[terminated / zombie until reaped]pid_t pid = fork(); /* returns twice: 0 in the child, the child's PID in the parent */
if (pid == 0) { execlp("ls", "ls", "-l", (char *)NULL); _exit(127); } /* only returns on failure */
int status; waitpid(pid, &status, 0);
if (WIFEXITED(status)) printf("exit %d\n", WEXITSTATUS(status));Why It Matters in Real Systems
Services are supervised processes: they are started with arguments and environment, communicate through pipes and sockets, are told to stop with SIGTERM, and their exit statuses decide restarts. Shells, build tools, web servers (pre-fork models), databases and containers are all built on the primitives in this module.
Key Terms
Process ID / parent's process ID Saving one thread's CPU state and restoring another's Controlled entry into the kernel
Synchronous switch to kernel mode caused by an instruction Integer handle to an open file, pipe, socket or device fork / exec / wait
Duplicate / replace image / collect child status Terminated process whose status was not yet collected Asynchronous notification to a process Pages shared until written, then copied
Snapshot of a crashed process's memory and registers
Memory Layout and Execution Flow
flowchart TB
SH[shell: fork] --> C[child: execve ./program]
C --> K[kernel maps ELF + ld-linux, builds stack]
K --> L[ld-linux maps libc, relocates, runs initialisers]
L --> S[_start → __libc_start_main → constructors → main]
S --> E[main returns → atexit handlers → stdio flush → _exit]
E --> W[parent's waitpid collects the status]Syntax
int main(int argc, char *argv[]); /* standard C entry point */
pid_t fork(void); /* POSIX */
int execvp(const char *file, char *const argv[]);
pid_t waitpid(pid_t pid, int *status, int options);
int pipe(int fds[2]);
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t off);
int atexit(void (*fn)(void)); /* standard C */
void (*signal(int sig, void (*handler)(int)))(int); /* prefer sigaction() on POSIX */Complete Runnable Example
The specification's argument printer, run as ./m10_args --rate 500 "two words". argc/argv are standard C; how the shell splits the command line is platform-specific.
fork, exec, wait, copy-on-write, a pipe and shared memory in one POSIX program (Linux):
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("Argument count: %d\n", argc);
for (int i = 0; i < argc; ++i) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}Argument count: 4
argv[0] = ./m10_args
argv[1] = --rate
argv[2] = 500
argv[3] = two words#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
int counter = 100; /* will be copied on write */
int main(void)
{
int *shared = mmap(NULL, sizeof *shared, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0); /* truly shared page */
if (shared == MAP_FAILED) return 1;
*shared = 1;
int fds[2];
if (pipe(fds) != 0) return 1;
fflush(stdout); /* do not duplicate buffered output */
pid_t pid = fork();
if (pid < 0) return 1;
if (pid == 0) { /* child */
counter = 999; /* private copy: the page is copied now */
*shared = 2; /* shared page: parent sees it */
const char msg[] = "hello through the pipe";
close(fds[0]);
if (write(fds[1], msg, sizeof msg) != (ssize_t)sizeof msg) _exit(1);
close(fds[1]);
printf("child : counter=%d at %p\n", counter, (void *)&counter);
fflush(stdout);
_exit(7); /
…Line-by-Line Explanation
mmap(MAP_SHARED | MAP_ANONYMOUS) creates a page that stays shared across fork; ordinary memory becomes copy-on-write.
fflush(stdout) before fork prevents buffered text from being printed twice (both processes would inherit the buffer).
In the child, counter = 999 triggers a copy of that one page; &counter is the same *virtual* address in both processes, now backed by different physical pages.
The child writes to the pipe and exits with _exit(7) — not exit, which would run the parent's inherited atexit handlers and flush stdio again.
The parent reads the pipe, reaps the child with waitpid, decodes the status with WIFEXITED/WEXITSTATUS, then uses fork + execlp to run uname.
Expected Output
Output (one real run — addresses vary):
child : counter=999 at 0x64ec4b961010
parent: counter=100 at 0x64ec4b961010 (same address, different value)
parent: shared page now holds 2
parent: read 23 bytes: "hello through the pipe"
parent: child exited with status 7
Linux
parent: exec'd child exited with status 0Step-by-Step Execution Trace
mmap creates one shared anonymous page; pipe creates two descriptors.
fork duplicates the process: both continue after the call, with 0 in the child and the child's PID in the parent.
Child: write fault on counter's page → kernel copies the page; store 999 in the copy. Store 2 into the shared page. Write 23 bytes to the pipe. Print. _exit(7).
Parent: read blocks until the child writes, then returns the message. waitpid collects status 7.
Parent prints counter = 100 (its page was never written) and *shared = 2.
Second fork: the child calls execlp, replacing its entire image with /usr/bin/uname, which prints Linux and exits 0.
Interactive Visualization
The compilation pipeline explorer continues past the executable: select "loader" and "runtime" to see what the kernel and dynamic loader do before main.
Common Mistakes and Failure Modes
Not reaping children (zombies accumulate) or ignoring fork/exec failure.
Calling exit instead of _exit in a forked child, flushing duplicated buffers twice.
printf, malloc or locks inside signal handlers (not async-signal-safe).
Assuming a pipe write/read transfers the whole buffer in one call.
Leaking file descriptors into child processes (missing O_CLOEXEC).
Relying on the order of global constructors across files.
Using _exit or abort and expecting output to be flushed:
Start-up and shutdown order, made visible:
Output is redirected to a file, so stdout is fully buffered: exit flushes it and runs the handler; _exit and abort lose the buffered text. Status 134 is 128 + 6: killed by SIGABRT.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static volatile sig_atomic_t stop_requested;
__attribute__((constructor)) static void before_main(void) { puts("1. constructor runs before main"); }
__attribute__((destructor)) static void after_exit(void) { puts("6. destructor runs after the atexit handlers"); }
static void cleanup_a(void) { puts("5. atexit handler A (registered first, runs last)"); }
static void cleanup_b(void) { puts("4. atexit handler B (registered second, runs first)"); }
static void on_term(int sig) { (void)sig; stop_requested = 1; } /* async-signal-safe: just a flag */
int main(void)
{
puts("2. main starts");
atexit(cleanup_a);
atexit(cleanup_b);
struct sigaction sa = {0};
sa.sa_handler = on_term;
sigaction(SIGTERM, &sa, NULL);
raise(SIGTERM); /* deliver SIGTERM to ourselves */
printf("3. SIGTERM handled: stop_requested=%d, returning from main\n", (int)stop_requested);
return 0;
}1. constructor runs before main
2. main starts
3. SIGTERM handled: stop_requested=1, returning from main
4. atexit handler B (registered second, runs first)
5. atexit handler A (registered first, runs last)
6. destructor runs after the atexit handlerscat > kinds.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void handler(void) { printf("[atexit handler ran]"); }
int main(int argc, char *argv[])
{
(void)argc;
atexit(handler);
printf("buffered text "); /* no newline: still in the stdout buffer */
if (argv[1][0] == 'e') exit(0);
if (argv[1][0] == '_') _exit(0);
abort();
}
EOF
gcc -std=c17 -Wall kinds.c -o kinds
for k in exit _exit abort; do
s=0
bash -c './kinds "$1" > out.txt' _ "$k" 2>/dev/null || s=$?
printf '%-6s -> stdout "%s", status %d\n' "$k" "$(cat out.txt)" "$s"
doneexit -> stdout "buffered text [atexit handler ran]", status 0
_exit -> stdout "", status 0
abort -> stdout "", status 134Corrected / Safe Pattern
Prefer posix_spawn for "run a program" — it avoids duplicating the parent and works better on systems without overcommit.
pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; }
if (pid == 0) {
execvp(argv[0], argv);
perror("execvp"); /* only reached on failure */
_exit(127);
}
int status;
while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) return -1; }Debugging and Diagnostic Techniques
strace shows every system call — here the loader opening and mapping libc.so.6, the brk that starts the heap, and the single write behind puts:
Output (one real run; long addresses abbreviated to 0x…):
A process can inspect its own kernel-side state through /proc/self and getrlimit:
Descriptors 0–2 are pipes here because the build tool captured the program's output; in a terminal they point to /dev/pts/N.
Other tools: ps, top/htop, pstree, /proc/<pid>/{status,maps,fd,limits}, lsof, gdb -p <pid>, coredumpctl, Process Explorer and Process Monitor on Windows.
#include <stdio.h>
int main(void)
{
puts("hello, kernel");
return 0;
}strace -o trace.txt -e trace=execve,openat,mmap,brk,write,exit_group ./m10_hello > /dev/null
sed -E 's/0x[0-9a-f]{6,}/0x…/g' trace.txt
echo "--- syscall counts (strace -c) ---"
strace -c -o counts.txt ./m10_hello > /dev/null
awk 'NR>2 && NF>=5 && $NF!="total" {print $NF, $(NF-1)}' counts.txt | grep -v -- '------' | sort | tr '\n' ' '; echoexecve("./m10_hello", ["./m10_hello"], 0x… /* 22 vars */) = 0
brk(NULL) = 0x…
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x…
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
mmap(NULL, 22707, PROT_READ, MAP_PRIVATE, 3, 0) = 0x…
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
mmap(NULL, 2260560, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x…
mmap(0x…, 1658880, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x…
mmap(0x…, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x…) = 0x…
mmap(0x…, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x…) = 0x…
mmap(0x…, 52816, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x…
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x…
brk(NULL) = 0x…
brk(0x…) = 0x…
write(1, "hello, kernel\n", 14) = 14
exit_group(0) = ?
+++ exited with 0 +++
--- syscall counts (strace -c) ---
access 1 arch_prctl 1 brk 3 close 2 execve 1 getrandom 1 ioctl 1 mmap 8 mprotect 3 munmap 1
…#include <stdio.h>
#include <string.h>
#include <sys/resource.h>
#include <unistd.h>
int main(void)
{
FILE *f = fopen("/proc/self/status", "r");
char line[256];
while (f != NULL && fgets(line, sizeof line, f) != NULL) {
if (!strncmp(line, "Name:", 5) || !strncmp(line, "State:", 6) || !strncmp(line, "Pid:", 4) ||
!strncmp(line, "PPid:", 5) || !strncmp(line, "Threads:", 8) || !strncmp(line, "VmPeak:", 7) ||
!strncmp(line, "VmRSS:", 6)) {
fputs(line, stdout);
}
}
if (f != NULL) fclose(f);
struct { int resource; const char *name; } limits[] = {
{RLIMIT_NOFILE, "open files"}, {RLIMIT_STACK, "stack bytes"}, {RLIMIT_CORE, "core file bytes"}};
for (size_t i = 0; i < sizeof limits / sizeof limits[0]; ++i) {
struct rlimit rl;
getrlimit(limits[i].resource, &rl);
if (rl.rlim_cur == RLIM_INFINITY) printf("limit %-16s unlimited\n", limits[i].name);
else printf("limit %-16s %llu\n", limits[i].name, (unsigned long long)rl.rlim_cur);
}
for (int fd = 0; fd <= 2; ++fd) {
char path[32], target[256];
snprintf(path, sizeof path, "/proc/self/fd/%d", fd);
…Name: m10_status
State: R (running)
Pid: 52
PPid: 44
VmPeak: 2780 kB
VmRSS: 1360 kB
Threads: 1
limit open files 10240
limit stack bytes 8388608
limit core file bytes 0
fd 0 -> pipe:[7272]
fd 1 -> pipe:[7273]
fd 2 -> pipe:[7274]Performance Implications
A system call costs roughly 50–200 ns (more with speculative-execution mitigations); a process creation milliseconds; a context switch about a microsecond plus cache and TLB refill. Buffer I/O to reduce write calls (stdio does), batch work, use threads or thread pools rather than processes per request, and prefer posix_spawn/vfork semantics for spawning large processes.
Security and Safety Implications
Never pass untrusted strings to system() or a shell — use exec* with an argument vector. Drop privileges after start-up; close descriptors the child does not need; sanitise the environment (LD_PRELOAD, PATH) for privileged programs; set resource limits; restrict core dumps of processes holding secrets (prctl(PR_SET_DUMPABLE, 0)).
Platform and Language Caveats
fork, signals, mmap and /proc are POSIX/Linux; Windows has CreateProcess, structured exception handling, CreateFileMapping and the Toolhelp/WMI APIs.
macOS supports fork but many frameworks are not safe to use in a forked child without exec.
Java (ProcessBuilder), C# (Process.Start), Python (subprocess) and Node (child_process) wrap the platform APIs; they all ultimately call fork/exec/posix_spawn or CreateProcess.
Exit status is 8 bits on POSIX, 32 bits on Windows.
Real-World Application
systemd supervises services and restarts them by exit status; shells implement pipelines with pipe + fork + dup2 + exec; Chrome isolates sites in separate sandboxed processes; PostgreSQL forks a backend per connection and shares buffers through shared memory; Android's zygote forks pre-initialised app processes to exploit copy-on-write.
Best Practices Checklist
[ ] Check every fork, exec, pipe, read and write result.
[ ] Reap every child; handle EINTR.
[ ] Use _exit in forked children that do not exec.
[ ] Keep signal handlers to setting flags.
[ ] Open descriptors with O_CLOEXEC.
[ ] Never build shell command strings from untrusted input.
[ ] Set resource limits and enable core dumps for debugging (not for secret-holding processes).
Exercises
E1 · Beginner. What does the shell print after running a program that ends with return 300; and then echo $??
Answer: 44 — only the low 8 bits are kept (300 − 256).
E2 · Intermediate. After fork, the child writes to one global variable. How many pages are copied, and why is fork of a 2 GB process usually fast?
Answer: Only the page containing that variable (plus pages of the stack it touches). fork copies page tables and marks pages read-only; data pages are copied lazily on the first write.
E3 · Advanced challenge. Implement ls | wc -l in C with pipe, fork, dup2 and execlp. Which descriptors must each process close, and what happens if the parent forgets to close the pipe's write end?
Answer: Child 1: dup2(fd[1], 1), close both pipe ends, execlp("ls"…). Child 2: dup2(fd[0], 0), close both ends, execlp("wc", "wc", "-l"…). Parent: close both ends, wait for both. If any process (including the parent) keeps the write end open, wc never sees end-of-file and waits forever.
Quiz
? fork() returns 0 in...
+ the child process :: The parent receives the child's PID instead.
- the parent process :: The parent gets a positive PID.
- both processes when it fails :: Failure returns -1 in the parent only; no child exists.
- the init process :: fork affects only the caller and its new child.
? After fork, the child sets a global from 100 to 999. What does the parent read?
+ 100 :: Copy-on-write gave the child a private copy of the page.
- 999 :: Ordinary pages are not shared after fork.
- An undefined value :: The parent's memory is unchanged.
- It depends on which process runs first :: The pages are separate regardless of scheduling.
? Which run atexit handlers? (select all that apply)
+ return from main :: It is equivalent to calling exit.
+ exit(0) :: That is what exit does.
- _exit(0) :: _exit skips all user-space cleanup.
- abort() :: abort terminates via SIGABRT without handlers.
? A shell shows exit status 139. What happened?
- The program returned 139 from main. :: Possible in theory, but 139 conventionally means 128 + 11.
+ The process was killed by signal 11, SIGSEGV. :: Shells report signal deaths as 128 + signal number.
- The program ran out of file d
…Summary
The OS turns programs into processes with private address spaces, schedules them through ready/running/blocked states, and serves them through system calls. POSIX creates processes with fork + exec (copy-on-write makes fork cheap) and collects them with wait; Windows uses CreateProcess. Start-up code runs constructors before main; exit runs handlers and flushes stdio, _exit and fatal signals do not. Pipes, shared memory and mapped files connect processes.
References and Further Reading
Michael Kerrisk, *The Linux Programming Interface*, chapters 24–28 (process creation), 44 (pipes), 49 (memory mappings).
Remzi & Andrea Arpaci-Dusseau, *Operating Systems: Three Easy Pieces*, "Processes" and "Process API".
man 2 fork, man 2 execve, man 7 signal-safety, man 2 syscall.
Microsoft Docs, "Creating Processes".