Chapter 1 — The Linux Kernel: Referee Between Software and Hardware

What Is the Kernel?

The Linux kernel is a program. A large, complex, C program — but a program nonetheless. It is the first program loaded by the bootloader (GRUB, for example) and it stays running for the entire lifetime of the system.

Its job is to be the trusted intermediary between all software and all hardware. It manages:

The kernel is not an application you interact with directly. You interact with it through system calls, virtual filesystems, and the tools and libraries that wrap them.

Privilege Rings: Why Your Python Code Can’t Touch Hardware

Modern CPUs have a hardware mechanism called privilege rings (Intel calls them protection rings). Ring 0 is the most privileged; Ring 3 is least privileged.

Ring 0: Kernel — full hardware access, all CPU instructions
Ring 1: (unused on Linux)
Ring 2: (unused on Linux)
Ring 3: Userspace — restricted instructions, no direct hardware access

Linux uses only Ring 0 (kernel) and Ring 3 (userspace). When the CPU executes code in Ring 3, certain instructions are forbidden: directly reading/writing I/O ports, loading privileged memory descriptors, modifying interrupt tables. Any attempt raises a CPU exception and the kernel terminates the offending process.

This is why you cannot simply do this from Python:

# This does NOT work — you can't write to hardware ports directly
import ctypes
ctypes.c_uint8(0x80)  # writing to I/O port 0x80 would require ring 0

To cross into Ring 0, you use a system call.

System Calls: The Controlled Gate

A system call is a request from userspace to the kernel to perform a privileged operation. On x86-64, the syscall CPU instruction switches to Ring 0 and jumps to a kernel handler.

There are ~350 system calls in the Linux kernel. Python’s standard library wraps most of the ones you need. Here are common ones and their Python equivalents:

Syscall Python What It Does
open open() Open a file descriptor
read file.read() Read bytes from an fd
write file.write() Write bytes to an fd
ioctl fcntl.ioctl() Device-specific control
mmap mmap.mmap() Map memory or device
socket socket.socket() Create network socket
close file.close() Close file descriptor

You can watch system calls in real time with strace:

strace python3 -c "open('/dev/null')"

This will show you every syscall made, including the openat call for /dev/null. It’s one of the most useful debugging tools for understanding what’s really happening.

The Kernel’s Internal Structure

When a syscall enters the kernel, it travels through several subsystems depending on what it does:

syscall entry
    │
    ├── Virtual File System (VFS)
    │       │
    │       ├── ext4 / btrfs / tmpfs (real filesystems)
    │       ├── sysfs (/sys)
    │       ├── procfs (/proc)
    │       └── devtmpfs (/dev)
    │
    ├── Network stack (TCP/IP, sockets)
    │
    ├── Memory manager (allocate, map, protect)
    │
    └── Device drivers
            │
            ├── Block devices (disks)
            ├── Character devices (serial, GPIO)
            ├── Network devices (eth0, wlan0)
            └── Bus drivers (PCI, USB, I2C...)

The Virtual File System (VFS) is a key abstraction. It defines a uniform interface (open, read, write, ioctl) that all filesystems and device drivers implement. When you open /dev/ttyUSB0, the VFS routes your open() call to the USB serial driver. When you read /sys/class/net/eth0/speed, the VFS routes it to the network sysfs implementation. The API is the same; only the implementation changes.

The Boot Process in Brief

Understanding when the kernel takes over helps clarify its role:

  1. BIOS/UEFI: Firmware initializes CPU, memory, and finds a bootable device
  2. Bootloader (GRUB): Loads the kernel image (vmlinuz) into memory
  3. Kernel init: Kernel decompresses itself, initializes memory management, CPU features
  4. Device discovery: Kernel probes buses (PCI, USB), finds devices, loads matching drivers
  5. Init system (systemd): Kernel starts PID 1 (systemd), which starts all services
  6. Userspace: Your shell, your Python program

The “device discovery” step in phase 4 is where the kernel walks the PCI bus, finds all hardware, and loads the appropriate kernel module (driver) for each device. By the time you log in, all hardware is already mapped.

Kernel Modules: Loadable Drivers

The kernel does not need to be recompiled to support new hardware. Most drivers exist as kernel modules — object files (.ko files) that can be loaded and unloaded at runtime.

# List loaded modules
lsmod

# Load a module
sudo modprobe usbserial

# Unload a module
sudo modprobe -r usbserial

# Show module info
modinfo e1000e

Modules live in /lib/modules/$(uname -r)/. When hardware is detected, the kernel uses device IDs to match hardware to the right module and loads it automatically.

# See where modules for your kernel live
ls /lib/modules/$(uname -r)/kernel/drivers/

You will see subdirectories for net/, usb/, i2c/, pci/, char/, block/ — each containing drivers for that category of hardware.

The Kernel Version

The kernel version matters. Drivers are compiled for a specific kernel. Features and APIs change between versions.

uname -r      # e.g. 6.5.0-21-generic
uname -a      # full info: arch, hostname, date

When you see an issue with a driver or hardware, the kernel version is often the first thing to check.


Previous: Chapter 0 — Introduction

Next: Chapter 2 — Hardware Topology: PCI, USB, I2C, SPI

Back to Table of Contents