OPERATING SYSTEMS / 7. SYSTEM CALLS

System Calls — The Bridge Between User & Kernel

Every interaction with hardware goes through exactly this interface


EXPLANATION

A system call (syscall) is the fundamental mechanism by which a user-space program requests a service from the kernel. It is the only legitimate way for user code (Ring 3) to perform privileged operations.

Why syscalls exist: user code cannot directly touch hardware (NIC, disk, keyboard). If it could, any program could corrupt another program's data, read other processes' passwords, or crash the whole system. The CPU's privilege levels enforce this. Only kernel code (Ring 0) can access hardware.

How a syscall works mechanically (x86-64):
① User code puts the syscall number in register RAX
   (RAX=1 = write, RAX=0 = read, RAX=2 = open, RAX=57 = fork…)
② Arguments go in RDI, RSI, RDX, R10, R8, R9
③ User code executes the SYSCALL instruction
④ CPU switches to Ring 0, jumps to kernel's syscall handler
⑤ Kernel validates arguments (checks pointers are in user space, etc.)
⑥ Kernel performs the operation (writes to file, allocates memory, etc.)
⑦ Kernel puts return value in RAX
⑧ CPU switches back to Ring 3, execution continues after SYSCALL

The most important Linux syscalls (know these):
• open(path, flags, mode) → returns file descriptor
• read(fd, buf, count) → reads bytes from fd
• write(fd, buf, count) → writes bytes to fd
• close(fd) → releases file descriptor
• fork() → creates a child process (copy of parent)
• exec(path, argv, envp) → replaces process image with a new program
• waitpid(pid) → parent waits for child to finish
• exit(status) → terminates the current process
• mmap(addr, len, prot, flags, fd, offset) → maps memory into address space
• munmap(addr, len) → unmaps memory
• socket(domain, type, protocol) → creates a network socket
• connect(fd, addr, addrlen) → TCP connect
• send(fd, buf, len, flags) → send data on socket
• kill(pid, signal) → send a signal to a process
• clone(flags) → fork with fine-grained sharing control (basis of threads)
• getpid() → returns current PID
• stat(path, buf) → get file metadata

strace: the ultimate debugging tool — it intercepts and prints every syscall a program makes. "strace python myscript.py" shows you every file it opens, every network call, every memory allocation — the complete conversation between your program and the kernel.

The cost of syscalls: each syscall requires a privilege level switch, TLB considerations, and kernel validation. A syscall takes ~100-300 ns. This is why read()/write() with small buffers in a loop is slow — each call has overhead. Buffered I/O (stdio in C, Python's file objects) batches many small reads/writes into fewer, larger syscalls.

VDSO (Virtual Dynamic Shared Object): some syscalls that just read kernel data (gettimeofday, clock_gettime) are mapped into user space — no ring switch needed. Makes clock reads extremely fast.

DIAGRAM

SYSCALL MECHANISM:
  User Space (Ring 3)          Kernel Space (Ring 0)
  ─────────────────────        ─────────────────────
  int fd = open("file", O_RDONLY)
       ↓
  RAX = 2 (open syscall #)
  RDI = ptr to "file"
  RSI = O_RDONLY flag
       ↓
  SYSCALL instruction
       ─────────────────────────→ CPU switches to Ring 0
                                  Validate args
                                  Find/create file in VFS
                                  Allocate file descriptor
                                  Return fd in RAX
       ←───────────────────────── CPU switches back to Ring 3
  fd = (value from RAX)
  (could be 3, 4, 5, ...)

  STRACE OUTPUT (running ls):
  execve("/bin/ls", ["ls"], envp)    = 0
  openat(AT_FDCWD, "/etc/ld.so.cache") = 3
  read(3, "<7f>ELF...", 832) = 832
  mmap(NULL, 2220368, PROT_READ, ..) = 0x7f2a...
  write(1, "file1  file2  dir1
", 20) = 20
  exit_group(0)

CODE

PYTHON
1import ctypes
2import os
3import sys
4
5# ── strace equivalent: trace syscalls ─────────────────
6# Run this in a shell:
7# strace -c python your_script.py (summary of syscalls made)
8# strace -e trace=file python ... (only file-related syscalls)
9# strace -p <PID> (attach to running process)
10
11# ── Python → syscall mapping ───────────────────────────
12print("=== Python stdlib syscalls ===")
13print("open() openat()")
14print("read() read()")
15print("write() write()")
16print("os.fork() clone()")
17print("threading clone() with CLONE_THREAD flag")
18print("socket() socket()")
19print("connect() connect()")
20print("malloc/new brk() or mmap()")
21
22# ── Direct syscalls via ctypes ─────────────────────────
23# On Linux we can call syscalls directly through libc
24libc = ctypes.CDLL(None)
25
26# getpid() syscall (no ring switch via VDSO on modern Linux)
27pid = libc.getpid()
28print(f"
29direct getpid(): {pid}")
30
31# write() syscall: write to stdout (fd=1)
32msg = b"Direct write() syscall to stdout!
33"
34libc.write(1, msg, len(msg))
35
36# ── Measure syscall cost ───────────────────────────────
37import time
38
39N = 100_000
40start = time.perf_counter()
41for _ in range(N):
42 os.getpid() # very cheap — VDSO, no ring switch
43elapsed = (time.perf_counter() - start) * 1e9 / N
44print(f"getpid() (VDSO): {elapsed:.0f} ns/call")
45
46start = time.perf_counter()
47for _ in range(N):
48 os.stat("/dev/null") # requires actual kernel trip
49elapsed = (time.perf_counter() - start) * 1e9 / N
50print(f"stat() (real syscall): {elapsed:.0f} ns/call")
51
52# ── File syscall sequence demo ─────────────────────────
53print("
54=== What open/write/close actually do ===")
55# This is the raw sequence — Python's open() does all this for you
56fd = os.open("/tmp/syscall_demo.txt", os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o644)
57n_written = os.write(fd, b"Syscalls are the bedrock of everything.
58")
59os.fsync(fd) # syscall: fdatasync() — wait for disk write
60os.close(fd) # syscall: close()
61print(f"Wrote {n_written} bytes via raw syscalls")
62
63# Verify
64with open("/tmp/syscall_demo.txt") as f:
65 print(f"Read back: {f.read().strip()}")
← PREV6. File Systems & I/ONEXT →8. Full Picture — Hardware to App