OPERATING SYSTEMS / 8. FULL PICTURE — HARDWARE TO APP

The Full Picture — Tracing One Execution

Following python hello.py from shell command to printed output, at every layer


EXPLANATION

Let's trace exactly what happens when you type "python hello.py" in your terminal, all the way from keystrokes to the printed output. Every layer we've covered plays a role.

The journey of "python hello.py":

HARDWARE → KERNEL (Input):
① Each key press generates an electrical signal → keyboard controller sends interrupt (IRQ1)
② CPU's interrupt controller (APIC) raises interrupt
③ CPU pauses current work → saves registers → jumps to interrupt handler in kernel
④ Keyboard driver reads scancode from I/O port 0x60 → translates to ASCII
⑤ Kernel puts character in terminal's input buffer
⑥ Shell (bash) is blocked in read() syscall waiting for input — kernel wakes it up

SHELL → OS (Process creation):
⑦ Shell reads the line "python hello.py"
⑧ Shell parses: command = "python", argument = "hello.py"
⑨ Shell calls fork() → kernel creates a child process (copies page tables, fd table)
⑩ Child process calls exec("python", ["python", "hello.py"]) syscall
⑪ Kernel: replaces child's address space with python interpreter binary
⑫ Kernel reads python binary from /usr/bin/python (disk → page cache → address space)
⑬ Dynamic linker (/lib/ld-linux.so) maps shared libraries (libc, libpython…)

PYTHON INTERPRETER:
⑭ Python starts — initializes itself, sets up GC, imports sys
⑮ Opens hello.py: open() → VFS → ext4 → disk read → page cache
⑯ Compiles hello.py to bytecode (CPython's front end)
⑰ Python VM executes bytecode instruction by instruction:
   • LOAD_GLOBAL (print) → looks up in globals dict
   • LOAD_CONST ("Hello, World!") → loads string object
   • CALL_FUNCTION → calls print()

KERNEL → HARDWARE (Output):
⑱ print() calls sys.stdout.write("Hello, World!")
⑲ Python's file object buffers the string
⑳ Calls write(fd=1, buf, len) syscall
㉑ Kernel receives write() → checks fd 1 is a tty
㉒ Terminal driver receives bytes → processes escape codes if any
㉓ Characters go to the framebuffer (GPU memory) via display driver
㉔ GPU sends signals to monitor → pixels light up
㉕ You see "Hello, World!" on screen

Process exit:
⑧ Python calls exit(0) syscall
㉖ Kernel: closes all file descriptors, frees address space, stores exit code
㉗ Sends SIGCHLD to parent (shell)
㉘ Shell calls waitpid() → gets exit code 0 → displays prompt again

This entire sequence — from keystroke to display — takes about 50–200 milliseconds. Most of that time is I/O (reading the file, disk access). The actual computation is microseconds. Now you understand every nanosecond of it.

DIAGRAM

"python hello.py" — complete trace:

  Keyboard HW → IRQ → Kernel ISR → Terminal Driver
                                          ↓
                                    Shell reads input
                                          ↓
                                    fork() → clone child process
                                          ↓
                                    exec() → load python binary
                                          ↓
                              Kernel: map ELF into VM
                              Dynamic linker: load .so files
                                          ↓
                              Python interpreter starts
                              open("hello.py") → read bytecode
                                          ↓
                              Execute: LOAD, CALL print()
                                          ↓
                              write(fd=1, "Hello, World!
")
                                          ↓
                              Kernel: tty driver receives bytes
                                          ↓
                              Framebuffer update → GPU → Monitor
                                          ↓
                              YOU SEE: Hello, World!

  TIMING (approximate):
  Keystroke → kernel:         ~0.1 ms
  fork() + exec():            ~5 ms
  Python startup:             ~30 ms
  File read + bytecode:       ~5 ms
  print() → write() syscall:  ~0.01 ms
  Screen update:              ~16 ms (1 frame at 60Hz)

CODE

PYTHON
1import os, sys, time, subprocess, dis, tracemalloc
2
3# ── 1. Trace the full Python startup ──────────────────
4print("=== Python Startup Info ===")
5print(f"Executable: {sys.executable}")
6print(f"Version: {sys.version.split()[0]}")
7print(f"PID: {os.getpid()}")
8print(f"Platform: {sys.platform}")
9print(f"Default encoding: {sys.getdefaultencoding()}")
10
11# ── 2. What did Python open to start? (Linux) ─────────
12print("
13=== Files Python has open ===")
14fd_path = f"/proc/{os.getpid()}/fd"
15try:
16 for fd in sorted(os.listdir(fd_path), key=int):
17 try:
18 link = os.readlink(f"{fd_path}/{fd}")
19 print(f" fd={fd}: {link}")
20 except:
21 pass
22except PermissionError:
23 pass
24
25# ── 3. Bytecode = what Python actually executes ────────
26print("
27=== Bytecode for hello.py equivalent ===")
28code = compile('print("Hello, World!")', "hello.py", "exec")
29dis.dis(code)
30
31# ── 4. The write() syscall path ───────────────────────
32print("
33=== Tracing write() ===")
34import io
35buf = io.BytesIO()
36start = time.perf_counter_ns()
37buf.write(b"Hello") # in-memory, no syscall
38t1 = time.perf_counter_ns()
39sys.stdout.write("Hello
40") # goes through Python buffer
41t2 = time.perf_counter_ns()
42sys.stdout.flush() # forces write() syscall
43t3 = time.perf_counter_ns()
44print(f"
45ByteIO write (no syscall): {t1-start:,} ns")
46print(f"stdout.write (buffered): {t2-t1:,} ns")
47print(f"stdout.flush (syscall): {t3-t2:,} ns")
48
49# ── 5. The fork+exec lifecycle ─────────────────────────
50print("
51=== fork() + exec() demo ===")
52start = time.perf_counter()
53result = subprocess.run(
54 [sys.executable, "-c", "print('Hello from child process!')"],
55 capture_output=True, text=True
56)
57elapsed = time.perf_counter() - start
58print(f"Child output: {result.stdout.strip()}")
59print(f"Exit code: {result.returncode}")
60print(f"fork+exec+run: {elapsed*1000:.1f} ms")
61
62# ── 6. Memory from start to now ────────────────────────
63tracemalloc.start()
64# ... your program runs ...
65snapshot = tracemalloc.take_snapshot()
66stats = snapshot.statistics("lineno")
67total_kb = sum(s.size for s in stats) / 1024
68print(f"
69Total Python memory allocated: {total_kb:.1f} KB")
70print(f"Largest allocations:")
71for s in stats[:3]:
72 print(f" {s}")
← PREV7. System Calls