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.
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.
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.
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.
Understanding when the kernel takes over helps clarify its role:
vmlinuz) into memoryThe “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.
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 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