In this lecture we stop speaking in pure abstraction and inspect a real operating system. We trace two end-to-end Linux walkthroughs, one device-oriented and one network-oriented, and use /proc, /sys, /dev, strace, dmesg, ip, and ss to recognize the semester's abstractions in a living system.
Lecture Date
š April 29, 2026
Standard
I/O and Networking
Topics Covered
Linux WalkthroughsVFS and Device Nodes/proc, /sys, /devSyscall TracingObservability Tools
Over the last several lectures we built a chain of increasingly general communication abstractions:
Drivers and the U-bend ā A process's read() descends through a syscall, dispatch, driver code, and hardware interaction, then returns through interrupts and deferred work
IPC ā Local communication mechanisms range from lightweight pipes to network-like Unix domain sockets, each making different tradeoffs about data movement, structure, and coordination
Packets and layering ā Once communication leaves the machine, data must travel as self-describing packets through a layered protocol stack
Transport and sockets ā The OS compresses packet-level complexity into a programmer-facing abstraction backed by different contract choices (UDP and TCP)
Waiting behavior ā Blocking, nonblocking, and readiness-based I/O give programs control over how they experience asynchronous reality
Each of these abstractions was introduced in isolation, as a design idea with a clean definition and a small example. But real operating systems do not run one abstraction at a time. They run all of them at once, cooperating on every meaningful action a user takes.
Today we stop speaking purely in abstractions and start tracing them inside a real operating system: Linux.
Today's Agenda
Linux as Interacting Abstractions ā Framing the lecture around recognition rather than subsystem coverage
Setting Up ā The Multipass VM as our inspection environment
Walkthrough 1: Process to Device ā Tracing a device-oriented action from user space into the kernel and back
Walkthrough 2: Process to Network ā Tracing a socket-oriented action through the network stack
Linux's Windows Into Itself ā /proc, /sys, and /dev as structured views into kernel state
Extending the Kernel Without Rebooting ā Kernel modules, lsmod, modinfo, and the bridge to contribution
Observing the Real System ā Using ps, strace, dmesg, ip, and ss as evidence
What You Should Now Recognize ā Mapping the semester's abstractions to their Linux counterparts
Looking Forward ā From recognition to safe contribution
Linux as a System of Interacting Abstractions
Linux is not interesting because it is famous. Linux is interesting because it makes the course's abstractions concrete. Process management, virtual memory, drivers, filesystems, and networking all cooperate inside one real system ā the same real system running on most of the world's servers, most of the world's phones, and most of the world's cloud infrastructure.
This lecture is not a survey of Linux subsystems. It is a guided tour through two end-to-end actions ā one device-oriented and one network-oriented ā that let familiar course concepts reappear naturally as the action travels through the system. The goal is recognition: you should be able to watch something happen in Linux and say "I know what that is. That is the driver dispatch we saw in LN19. That is the transport layer from LN22. That is the blocking behavior from our scheduling lectures."
Course Abstractions in Linux
Each concept connects to observable Linux surfaces. Filter by walkthrough.
š¤ Key Framing: The question driving this lecture is not "what is Linux?" It is "where do the abstractions from this course show up when a user actually does something in Linux?"
Setting Up: The Lecture VM
To make today's walkthroughs concrete and reproducible, we will use a lightweight Linux virtual machine running through Multipass. Multipass provides a terminal-first Ubuntu instance backed by a headless QEMU VM ā no GUI overhead, no complex setup, and it works the same on macOS (Intel and Apple Silicon), Windows, and Linux hosts.
Why a Virtual Machine?
In LN19, we established that kernel-space code is dangerous ā "code is lava." A bug in a driver or kernel module does not crash an application; it crashes the entire machine. You cannot safely experiment with strace, kallsyms, or kernel modules on the same machine you use for homework and email.
A virtual machine solves this. A VM is a complete operating system ā its own kernel, its own memory, its own /dev and /proc ā running inside a sandbox managed by a hypervisor. The hypervisor is a thin software layer that allocates real hardware resources (CPU time, RAM, virtual disk) to the VM while keeping it isolated from your host. If the VM's kernel panics, your host is unaffected. If you load a buggy module, only the VM crashes. You delete it and start fresh.
This is not emulation ā the VM runs real Linux kernel code on real CPU instructions. The hypervisor just ensures the VM cannot reach outside its sandbox. When you type multipass shell ln23, your terminal opens a connection to the VM's shell over a local socket. Your keystrokes flow in, the VM's output flows back, and the experience feels identical to a local terminal ā but every command executes inside the isolated guest OS.
Your Machine(macOS / Windows / Linux)
Terminal
$multipass launch --name ln23 lts
$multipass shell ln23
Your keystrokes and screen output are forwarded through a local socket to the VM.
stdin stdout stderr
stdin / stdout / stderr
Hypervisor(Multipass / QEMU)
Allocates CPU, memory, and virtual hardware for the VM. Isolates it from the host.
All lecture commands run here ā inside the VM, not on your host.
This architecture is exactly what makes the next lecture possible: we will compile and load our own kernel module inside a VM, knowing that any mistake is contained. The VM is our lab bench ā we can inspect the kernel, modify it, and break it, all without risk to the machine we depend on.
Setting Up Your VM
Every command in this lecture is meant to be run inside a Linux VM. Before continuing, complete the following setup. The Addendum at the bottom of this page contains additional detail and the full script contents if you need them.
1. Install Multipass from multipass.run if you haven't already.
2. Launch and enter the VM (run these on your host machine):
4. Create the demo scripts by following Addendum Step 5 at the bottom of this page. Once done, verify:
tree ~/ln23-demo/
You should see device-read.sh, device-trace.sh, and the various inspect-*.sh scripts.
š Note: All commands from this point forward are run inside the VM, in the ~/ln23-demo/ directory. Run cd ~/ln23-demo now before continuing.
Walkthrough 1: Process to Device
Our first walkthrough traces a process interacting with a device-like resource in Linux. We will read from /dev/urandom ā a pseudo-device that returns random bytes. It is not real hardware, but it exercises the exact same kernel path: a process opens a device node, calls read(), crosses the syscall boundary, enters a driver-like code path, and receives data.
The Action
š„ļø Run this in your VM:
ddif=/dev/urandom bs=16 count=1 2>/dev/null | xxd
dd copies data between files ā here it reads 16 bytes (bs=16 count=1) from the input file (if=) /dev/urandom. The 2>/dev/null suppresses dd's status output. The pipe | sends those bytes to xxd, which displays raw data in hexadecimal so we can see what came back.
You should see one line of hex output ā 16 random bytes:
strace intercepts every system call a program makes and prints it to the terminal. The -e flag filters to only the syscalls we care about ā openat, read, write, and close. Everything after the flags is the command being traced.
You will see a lot of startup noise ā the dynamic linker opening shared libraries, locale files being loaded. Scroll past those. The lines that matter are near the end of the output:
This output reveals the U-bend in action. The process calls openat() to open /dev/urandom ā that is the left tip of the U, descending through the syscall boundary. The kernel resolves the path through VFS, finds the device node, and locates the driver registered for this major/minor number. Then read() enters the driver's code, which generates random bytes and copies them into the process's buffer. The process receives data and calls write() to send it to stdout. Finally, close() releases the file descriptor. Notice dd's own status message (1+0 records in...) also appears as a write(2, ...) syscall ā even the program's informational output crosses the syscall boundary.
š” Connection to LN19: This is exactly the U-bend. The process called read(). The kernel dispatched to a driver. The driver produced data. The result ascended back through the syscall boundary to the process. Every step we diagrammed in the abstract is visible here through strace.
š Cleaner output: The raw strace output includes a lot of startup noise from the dynamic linker. Run ./device-trace.sh to see the same trace with only the relevant syscalls ā it filters out everything except the openat, read, write, and close calls that matter.
Where the Abstractions Appear
Course Abstraction
Where It Appears in This Walkthrough
Syscall boundary (LN19)
openat() and read() cross from user space into kernel space
VFS and device dispatch (LN18)
The kernel resolves /dev/urandom through VFS to the correct driver
Major/minor numbers (LN18)
The device node encodes which driver handles this device
Driver code (LN19)
The random number generator runs in kernel context
File descriptor (LN18)
dd holds an fd that represents the open device
Blocking / synchronous appearance (LN18)
read() returns when the data is ready ā the process experiences a simple sequential call
Inspecting the Device Node
š„ļø Run this in your VM:
ls -la /dev/urandom
ls -la lists a file with full details: permissions, owner, group, and ā for device nodes ā the major and minor numbers instead of a file size.
The c at the start means character device. Where you would normally see a file size, you instead see 1, 9 ā the major and minor numbers. These are the dispatch key the kernel uses to find the right driver ā the same major/minor system we studied in LN18.
š„ļø Run this in your VM:
cat /proc/devices | head -20
cat prints a file's contents. /proc/devices is a virtual file the kernel generates listing all registered device drivers and their major numbers. head -20 shows only the first 20 lines to keep the output manageable.
Look for major number 1 in the output ā it maps to mem, the driver that handles /dev/urandom, /dev/null, /dev/zero, and other memory-related pseudo-devices. This is how the kernel knows that device node 1, 9 should be handled by the memory driver.
š Cleaner output: Run ./device-read.sh to see the device read, device node details, and major/minor mapping all in one labeled output.
š Key Point: Nothing in this walkthrough is new theory. Every step maps directly onto concepts from earlier lectures. Linux is where those concepts stop being diagrams and start being commands.
Following the Dispatch to the Driver
We now know that /dev/urandom is character device 1, 9 and that major 1 maps to the mem driver. But we can go further ā the kernel provides enough information to trace from the device node all the way to the actual source code of the function that runs when you call read() on this file.
š„ļø Run this in your VM:
cat /sys/devices/virtual/mem/urandom/uevent
/sys/devices/ is where the kernel exposes its internal device tree. The uevent file shows the kernel's own record of what this device is ā its major/minor numbers and the name it registered under.
You should see:
MAJOR=1
MINOR=9
DEVNAME=urandom
DEVMODE=0666
This confirms the mapping: the kernel itself says this device is 1:9 and is named urandom. The mode 0666 means any process can read from or write to it ā no special permissions needed.
Next, we can look at the kernel's symbol table to find the entry point:
š„ļø Run this in your VM:
sudo grep urandom_fops /proc/kallsyms
/proc/kallsyms lists every symbol (function, struct, variable) in the running kernel, with its memory address. urandom_fops is the file_operations struct for /dev/urandom ā the table of function pointers the kernel uses to dispatch open(), read(), write(), and close() calls on this device.
You should see output like:
ffff800081998fd8 D urandom_fops
The D means this is a data symbol in the global (initialized) data section ā it is a struct, not a function. This struct is the dispatch table from LN19: it contains the function pointers the kernel follows when your read() syscall reaches the driver.
š„ļø Run this in your VM:
sudo grep urandom_read_iter /proc/kallsyms
You should see:
ffff800080e27908 t urandom_read_iter
The t means this is a text symbol ā executable code in the kernel. This is the actual function the kernel calls when you read() from /dev/urandom. The lowercase t means it has file-local scope (static), which makes sense ā only the dispatch table should call it directly.
Seeing the Source
This VM runs kernel 6.8. The urandom_fops struct and the urandom_read_iter function live in drivers/char/random.c in the Linux source tree. Here is the actual dispatch table from that file:
Look at .read_iter = urandom_read_iter ā this is the exact connection. When dd calls read() on /dev/urandom:
The syscall traps into the kernel (read() ā vfs_read())
VFS follows fd 3 ā inode ā urandom_fops
It calls urandom_fops.read_iter, which is urandom_read_iter()
That function generates random bytes and copies them back to user space
This is the same dispatch pattern we studied in LN19 when we looked at how drivers are structured ā but now you are seeing it in a real, running system with a real production driver. The file_operations struct is not an abstract diagram; it is a real C struct at address ffff800081998fd8 in this kernel's memory.
Inside urandom_read_iter()
We can go one level deeper. Here is the function that urandom_fops.read_iter points to ā the code that actually runs when you read() from /dev/urandom:
This function is surprisingly short. It does exactly two things:
Checks readiness ā if the kernel's cryptographic random number generator has not yet gathered enough entropy (e.g., very early in boot), it warns. For a normally running system, crng_ready() is true and this check is skipped entirely.
Dispatches to get_random_bytes_user() ā the function that actually generates and delivers the random bytes.
Here is get_random_bytes_user(), where the real work happens:
staticssize_tget_random_bytes_user(struct iov_iter *iter)
{
u32 chacha_state[CHACHA_STATE_WORDS];
u8 block[CHACHA_BLOCK_SIZE];
size_t ret = 0, copied;
if (unlikely(!iov_iter_count(iter)))
return0;
crng_make_state(chacha_state, (u8 *)&chacha_state[4], CHACHA_KEY_SIZE);
if (iov_iter_count(iter) <= CHACHA_KEY_SIZE) {
ret = copy_to_iter(&chacha_state[4], CHACHA_KEY_SIZE, iter);
goto out_zero_chacha;
}
for (;;) {
chacha20_block(chacha_state, block);
if (unlikely(chacha_state[12] == 0))
++chacha_state[13];
copied = copy_to_iter(block, sizeof(block), iter);
ret += copied;
if (!iov_iter_count(iter) || copied != sizeof(block))
break;
/* ... periodic rescheduling for large reads ... */
}
memzero_explicit(block, sizeof(block));
out_zero_chacha:
memzero_explicit(chacha_state, sizeof(chacha_state));
return ret ? ret : -EFAULT;
}
Follow this through for our dd read of 16 bytes:
crng_make_state() ā initializes ChaCha20 cipher state from the kernel's entropy pool. This is the cryptographic core: ChaCha20 is a stream cipher that expands a small seed into an arbitrary-length stream of unpredictable bytes.
Short-path check ā CHACHA_KEY_SIZE is 32. Our request is 16 bytes, which is <= 32, so the function takes the short path: it copies the random bytes directly from the ChaCha state with copy_to_iter() and jumps to cleanup. No loop, no block generation ā just a single copy.
copy_to_iter() ā this is the kernel's iterator-based version of copy_to_user(). It copies 16 bytes from the kernel's ChaCha state buffer into dd's user-space buffer. This is where data crosses back over the user/kernel boundary.
memzero_explicit() ā the function erases the ChaCha state from memory immediately after use. Even if an attacker later reads kernel memory, they cannot reconstruct the random bytes that were generated. This is called forward secrecy, and it is a concrete example of the defensive kernel programming we discussed in LN19 ā "code is lava" means cleaning up after yourself, even for data structures that will go out of scope.
š Key insight: The entire invisible interior of the U-bend ā from read() entering the kernel to read() returning 16 ā is just these four operations: set up ChaCha20 state, generate bytes, copy to user, zero the key material. Every step either produces the data or protects the system. Nothing is wasted.
Overview
The full U-bend
dd reads 16 bytes from /dev/urandom. The left column shows what strace reveals ā the syscalls crossing the user/kernel boundary. The right column shows what happens invisibly inside the kernel between those crossings. Each kernel node corresponds to a real function you can find in drivers/char/random.c.
Overview
Walkthrough 2: Process to Network
Our second walkthrough traces a process communicating over the network. We will use a simple client/server exchange over TCP on the loopback interface ā one process listens, another connects and sends a message.
š This walkthrough requires two terminal sessions in the VM. Open a second host terminal and run multipass shell ln23 so you have two independent shells inside the VM. We will call them Terminal 1 (the server) and Terminal 2 (the client). Make sure both are in the ~/ln23-demo/ directory.
The Action
š„ļø Terminal 1 ā Run this to start the server:
nc -l 4000
nc (netcat) is a simple networking utility. The -l flag tells it to listen for incoming connections. 4000 is the port number. This turns nc into a minimal TCP server that waits for a client and prints whatever it receives.
The server will appear to hang ā that is correct. It is blocking on accept(), waiting for a client. Leave it running.
š„ļø Terminal 2 ā Run this to send a message:
echo"GET_TEMP" | nc 127.0.0.1 4000
echo prints the string "GET_TEMP". The pipe sends it to nc, which connects to 127.0.0.1 (the loopback address ā this same machine) on port 4000 and transmits the data over TCP.
Switch back to Terminal 1. You should see GET_TEMP printed ā the server received the message. This is our temperature-service example from LN22, stripped down to the simplest possible form.
What We Can Observe
Now let's repeat the exchange, but trace the syscalls. The server has already exited, so we start fresh.
Same strace as before, but now filtering for socket-related syscalls: socket, bind, listen, accept, read, write, and close. The command being traced is the nc server.
You will immediately see the setup syscalls appear, then the server will block:
The output reveals the socket lifecycle we studied in LN22. The process calls socket() to create an endpoint. It calls bind() to attach to port 4000. It calls listen() to mark the socket as accepting connections. Then it blocks on accept() ā sleeping until a client arrives. When the client connects, accept() returns with a new file descriptor (4) for the connected conversation (the well-known port discovery pattern). The server then calls read() on that new descriptor and receives the client's message.
On the client side, you can trace the other half of the exchange. Start the server again first:
The sh -c '...' wraps the piped command so strace can trace the entire pipeline. The -f flag tells strace to follow forks ā without it, we only see the parent shell's syscalls, not the nc child process where the actual networking happens. We filter for the client-side syscalls: socket, connect, write, and close.
Look past the parent shell's bookkeeping (SIGCHLD, inherited close calls) for the lines coming from the child PID ā that is nc itself:
The child process calls socket(), then connect() to initiate the TCP handshake with the server. The EINPROGRESS is normal ā nc opens the socket non-blocking, so the kernel returns immediately while the handshake completes in the background. Once connected, write() sends the data. The kernel handles the rest ā packetization, transport headers, loopback delivery.
ā ļø Why -f matters: Without -f, you'll only see the shell waiting on its child (SIGCHLD) and closing inherited file descriptors. The interesting calls happen in the forked nc process, which strace ignores by default. This is a common gotcha when tracing anything launched through sh -c, bash -c, or a pipeline.
š” Connection to LN22: Every socket operation we defined ā socket(), bind(), listen(), accept(), connect(), read(), write(), close() ā appears directly in the strace output. The abstraction is not a simplification for the lecture; it is the actual API the program uses.
Inspecting the Network State
Start the server one more time so we can inspect it while it's listening:
š„ļø Terminal 1:
nc -l 4000
š„ļø Terminal 2 ā Inspect the socket state before the client connects:
ss -tlnp
ss is Linux's socket statistics tool (successor to netstat). The flags: -t for TCP sockets, -l for listening sockets only, -n for numeric output (don't resolve hostnames), -p to show the process that owns each socket.
Look for a line with *:4000 or 0.0.0.0:4000 in the output ā that is our server's listening socket. This shows the listening socket on port 4000, the process that owns it, and its state. After the client connects, running ss -tnp (without -l) reveals the established connection with its full 4-tuple ā the same multiplexing story from LN22, made visible.
š„ļø Terminal 2 ā Inspect the loopback interface:
ip addr show lo
ip is Linux's network configuration tool. addr show lists IP addresses assigned to interfaces. lo specifies the loopback interface, which is the virtual network interface a machine uses to talk to itself.
You should see:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
inet 127.0.0.1/8 scope host lo
inet6 ::1/128 scope host
This reveals the loopback interface ā the "network" our packets are traveling over. Even on localhost, the full network stack runs: the kernel still builds packets, applies transport headers, and delivers through the protocol layers. The loopback interface simply short-circuits the NIC ā the same IPC mirror insight from LN21.
š Cleaner output: Run ./inspect-net.sh to see the loopback interface, listening sockets, and established connections in one labeled output. Run it while the server is listening in Terminal 1 to see the socket state.
You can now stop the server in Terminal 1 with Ctrl+C.
Where the Abstractions Appear
Course Abstraction
Where It Appears in This Walkthrough
Socket as handle (LN22)
socket() returns a file descriptor for the endpoint
Ports and multiplexing (LN22)
bind() to port 4000, ss shows the 4-tuple
Well-known port / accept() (LN22)
Server listens on a known port; each client gets its own connected socket
TCP handshake (LN22)
connect() triggers the three-way handshake before data flows
Transport layer (LN22)
The kernel adds TCP headers, manages sequencing and acknowledgments
Packet path (LN21)
Even over loopback, the kernel builds and processes packets through the stack
IPC mirror (LN21)
Loopback communication uses the full network API but skips the physical wire
Blocking I/O (LN22)
accept() blocks until a client arrives ā the server sleeps and wakes on an event
Overview
Full socket lifecycle
A minimal TCP exchange over loopback: nc listens on port 4000, another nc connects and sends "GET_TEMP". The full socket lifecycle from LN22 plays out ā observable through strace and ss.
Overview
Linux's Windows Into Itself
Both walkthroughs used specific Linux surfaces to inspect what the kernel is doing. Those surfaces are not random files ā they are structured interfaces the kernel exposes so that user-space programs can observe and interact with kernel state.
/proc ā Process and Kernel Status
Every running process has a directory in /proc named by its PID. Inside that directory, files like status, maps, fd/, and cmdline reveal the process's state, memory layout, open file descriptors, and command-line arguments.
š„ļø Run these in your VM:
ls /proc/self/fd
cat /proc/self/status | head -15
/proc/self is a magic symlink that always points to the /proc directory of whichever process reads it. fd/ is a subdirectory containing a numbered entry for each open file descriptor. status is a virtual file the kernel fills with the process's name, PID, state, memory usage, and more.
The first command shows the file descriptors currently open ā including 0 (stdin), 1 (stdout), and 2 (stderr). The second shows something like:
Name: cat
State: R (running)
Pid: 3520
PPid: 3518
Notice the Name field says cat, not bash ā that is because /proc/self resolves to whichever process reads it, and here cat is the process doing the reading. This is itself a mini-lesson in how the kernel tracks per-process state.
š Cleaner output: Run ./inspect-proc.sh to see file descriptors, process status, and system memory info in one labeled output.
š” Connection to LN9: We introduced processes as the OS's unit of isolation and management. /proc is where Linux lets you peek inside that management layer without needing kernel source code.
/sys ā Structured Subsystem Information
Where /proc is organized around processes, /sys is organized around the kernel's device and subsystem model. You can find information about block devices, network interfaces, PCI devices, and more.
š„ļø Run these in your VM:
ls /sys/class/net/
cat /sys/class/net/lo/mtu
/sys/class/net/ groups all network interfaces by class. Each subdirectory is an interface (e.g., lo, enp0s1). The mtu file inside each contains a single number ā the Maximum Transmission Unit in bytes ā that you can read like a regular file.
The first command lists the network interfaces the kernel knows about (you should see enp0s1 and lo). The second prints 65536 ā the MTU of the loopback interface, which is large because loopback does not traverse a real wire. This is a direct view into the link-layer configuration we discussed in LN21.
š Cleaner output: Run ./inspect-sys.sh to see network interfaces, loopback MTU, and block devices in one labeled output.
/dev ā Device Endpoints
We already used /dev/urandom in the first walkthrough. The /dev directory contains device nodes ā special files that represent endpoints for device communication. Each node encodes a major and minor number that the kernel uses to dispatch operations to the correct driver.
š„ļø Run this in your VM:
ls -la /dev/urandom /dev/null /dev/zero
Lists three common device nodes side by side. /dev/null discards everything written to it. /dev/zero produces an infinite stream of zero bytes. /dev/urandom produces random bytes. All three are character devices with distinct major/minor numbers routing to different drivers.
All three share major number 1 (the mem driver) but have different minor numbers ā 3, 9, and 5. The kernel uses the minor number to dispatch to the correct behavior within the same driver. These are all character devices accessible through the same open()/read()/write() interface.
š Cleaner output: Run ./inspect-dev.sh to see common device nodes and a broader device listing in one labeled output.
Three Windows, Three Perspectives
Surface
Organized By
What It Reveals
Example
/proc
Process (PID)
Per-process state, kernel-wide counters
/proc/self/status, /proc/meminfo
/sys
Subsystem / device hierarchy
Device attributes, driver bindings, bus topology
/sys/class/net/lo/mtu
/dev
Device node (major/minor)
Communication endpoints for device I/O
/dev/urandom, /dev/null
š Key Point: These are not just directories with random files. They are the kernel's way of making its internal abstractions visible without requiring user-space programs to read kernel source or parse binary structures. Every file you read in /proc, /sys, or /dev is the kernel generating an answer to your question on demand.
Extending the Kernel Without Rebooting ā Kernel Modules
Everything we have traced so far ā the driver behind /dev/urandom, the network stack handling our TCP exchange, even the virtual filesystems /proc and /sys ā lives inside the running kernel. But Linux does not force all of this code to be compiled in at build time. Much of it is loaded on demand as kernel modules.
This is a direct consequence of the design we studied in LN19. The kernel provides a stable internal interface (the file_operations struct, registration functions, and the VFS dispatch layer). A module plugs into that interface. When you load a module, the kernel links it into its address space and calls its initialization function. When you unload it, the kernel calls its cleanup function and removes it. The machine never stops running.
Why Modules Matter
Modules solve a practical problem: the Linux kernel supports thousands of devices, filesystems, and protocols, but no single machine needs all of them. Rather than shipping a monolithic kernel binary containing everything, Linux ships a small core kernel with a large collection of modules. The system loads only the modules it actually needs ā when a USB device is plugged in, when a filesystem is mounted, when a network protocol is requested.
This has a deeper implication for us. The entire "code is lava" discussion from LN19 ā privilege, reentrancy, the danger of bugs in kernel space ā applies to every module. A buggy module can crash the entire system, corrupt data, or open security holes. There is no sandbox separating module code from the rest of the kernel. This is exactly why safe kernel-space programming matters and why Rust's entry into the Linux kernel (which we will see in the next lecture) is significant.
Observing Modules
š„ļø Run this in your VM:
lsmod | head -20
lsmod lists all currently loaded kernel modules. Each line shows the module name, its size in memory, and how many other modules depend on it. head -20 limits the output to the first 20 entries.
You should see a table like:
Module Size Used by
tcp_diag 12288 0
inet_diag 28672 1 tcp_diag
tls 159744 0
binfmt_misc 28672 1
Each of these is a piece of kernel code that was loaded after the kernel itself started ā filesystem drivers, network protocols, diagnostics modules, and more.
š„ļø Run this in your VM:
modinfo e1000 | head -6
modinfo prints detailed information about a specific kernel module ā its description, author, license, parameters it accepts, and which devices it supports. e1000 is Intel's Gigabit Ethernet driver, one of the most common network drivers. We pipe through head -6 to see just the key metadata.
Notice the license field ā Linux requires modules to declare their license, and the kernel logs a "taint" warning when a non-GPL module is loaded. This is the community's way of flagging when kernel-space code cannot be inspected or audited.
š„ļø Run this in your VM:
ls /lib/modules/$(uname -r)/kernel/drivers/net/ | head -20
uname -r prints the running kernel version. /lib/modules/<version>/kernel/drivers/net/ is where compiled network driver modules are stored on disk as .ko (kernel object) files. This command lists the network drivers available for loading.
The modules directory on disk mirrors the kernel's subsystem structure. There are subdirectories for drivers/, fs/, net/, crypto/, and more ā each containing the .ko files that can be loaded on demand.
š Cleaner output: Run ./inspect-modules.sh to see loaded modules and available network drivers in one labeled output.
The Module Lifecycle
Operation
Command
What Happens
List loaded modules
lsmod
Shows all modules currently in kernel memory
Inspect a module
modinfo <name>
Shows metadata, parameters, and dependencies
Load a module
sudo modprobe <name>
Loads the module and any dependencies it requires
Unload a module
sudo modprobe -r <name>
Removes the module from kernel memory (if nothing depends on it)
š” Connection to LN19: A kernel module is the tangible artifact behind the "driver" abstraction we studied. When we said the kernel "dispatches to a driver," this is how the driver got there ā it was loaded as a module that registered itself with the kernel's dispatch infrastructure. The U-bend does not just cross into kernel space; it crosses into specific module code that may not have even been present when the machine booted. We saw exactly this dispatch in Walkthrough 1: urandom_fops is the file_operations struct that the mem driver registered, and urandom_read_iter is the function pointer the kernel follows when you call read(). A loadable module works the same way ā it registers its own file_operations struct, and the kernel's dispatch layer treats it identically to built-in code.
š¤ Looking Ahead: In the next lecture, we will write a kernel module ourselves ā in Rust. The module system is what makes this possible: we can compile our code, load it into a running kernel, test it, and unload it, all without rebooting. Understanding how modules fit into the kernel's architecture is the bridge between observing Linux and contributing to it.
Observing the Real System
Both walkthroughs relied on a small set of Linux tools. These tools are not trivia to memorize ā they are the standard vocabulary for inspecting a running Linux system. Each one gives visibility into a different layer of the abstractions we have studied.
The Core Toolkit
Tool
What It Reveals
Course Connection
ps
Running processes, their states, PIDs, resource usage
Process management (LN9)
strace
Syscalls a process makes in real time
Syscall boundary, U-bend entry/exit (LN19)
dmesg
Kernel log messages, including driver and hardware events
Socket state ā listening, established, 4-tuples, ports
Transport, sockets, multiplexing (LN22)
Putting Them Together
The power of these tools is not in any single command. It is in combining them to build a picture of what the system is doing across layers.
For example, if you start nc -l 4000 in one terminal, you could inspect it from another using:
š„ļø Try any of these in your VM (with a server running in another terminal):
ps aux | grep nc
strace -p <pid> -e read,write
ss -tnp | grep 4000
dmesg | tail
ps aux lists all running processes with details (user, PID, CPU/memory usage, command). Piping through grep nc filters for lines containing "nc." strace -p <pid> attaches to an already-running process by its PID rather than launching a new one ā replace <pid> with the PID from the ps output. ss -tnp shows TCP sockets (-t) with numeric output (-n) and owning process (-p). dmesg prints the kernel's ring buffer ā its internal log ā and tail shows the most recent entries.
Each tool reveals a different slice of the same underlying action. ps shows the process exists and is running. strace shows what kernel services the process is requesting. ss shows the transport-layer state of its sockets. dmesg shows what the kernel itself has to say about recent events. Together, they turn the abstract U-bend into observable evidence.
š Key Point: Understanding the OS includes knowing how to inspect it, not just how to theorize about it. These tools are how working systems engineers verify what the kernel is actually doing rather than guessing.
What You Should Now Recognize
After this course, you should be able to look at a Linux system and recognize the following:
What You See in Linux
What You Learned in This Course
A process in ps output
The OS's unit of protection and scheduling (LN9)
A syscall in strace
The controlled crossing from user space to kernel space (LN19)
A device node in /dev
A handle dispatched through VFS to a driver (LN18, LN19)
A pipe connecting two commands
IPC through a kernel-managed buffer (LN20)
A socket visible in ss
A transport endpoint backed by the full network stack (LN22)
A listening port
Discovery through well-known ports, multiplexing through 4-tuples (LN22)
An entry in /proc
The kernel exposing process state through a virtual filesystem (LN9)
An MTU value in /sys
Link-layer configuration for the network interface (LN21)
A kernel message in dmesg
Driver or subsystem activity in kernel space (LN19)
A blocked process
A process sleeping until an event ā the same waiting we saw in I/O and scheduling (LN18, LN10)
This is the payoff. None of these are new Linux facts to memorize. They are course concepts made visible in a real system.
š¤ The Recognition Test: If you can watch strace output and narrate what the kernel is doing at each syscall ā which abstraction is being invoked, which boundary is being crossed, which subsystem is handling the request ā then you understand what this course taught. Linux is just the place where you prove it.
Looking Forward
This lecture traced familiar abstractions inside a living operating system. Processes, syscalls, drivers, sockets, transport, buffering, and waiting behavior all appeared exactly where the course predicted they would. The abstractions are not simplified teaching models ā they are the actual design of the system.
That raises one final question. If Linux is built from all of these cooperating abstractions, and if the code that implements them runs with maximum privilege in kernel space ā the same "code is lava" environment we studied in LN19 ā then how do real engineers safely contribute to functionality at this level?
The answer involves the language this course has been teaching all semester. The next lecture explores how Rust is changing the safety story for kernel and driver development, and what it looks like to participate in a system like Linux rather than just observe it.
Summary
Linux is not a separate topic ā it is where the semester's abstractions become concrete
The lecture traces two end-to-end walkthroughs rather than surveying subsystems
Walkthrough 1 (device): reading from /dev/urandom reveals the full U-bend ā syscall, VFS dispatch, driver code, data return
Walkthrough 2 (network): a TCP client/server exchange reveals sockets, ports, the transport layer, and the packet path ā all observable through strace and ss
/proc, /sys, and /dev are three structured windows the kernel provides into its own state, each organized differently
Kernel modules allow Linux to extend its functionality at runtime without rebooting ā drivers, filesystems, and protocols are loaded on demand, running with full kernel privilege
ps, strace, dmesg, ip, and ss form a core toolkit for observing Linux from user space
Every observable behavior in the walkthroughs maps directly onto a concept from earlier in the course
The next lecture moves from recognition to participation: safely contributing to Linux through Rust
š Lecture Notes
Key Definitions:
Term
Definition
Multipass
A tool for launching lightweight Ubuntu VMs from the command line, providing a full Linux kernel for inspection
procfs (/proc)
A virtual filesystem exposing per-process information and kernel status as readable files
sysfs (/sys)
A virtual filesystem exposing structured subsystem, device, and driver information
Kernel Module
A compiled object file that can be loaded into a running kernel to add drivers, filesystems, or protocols without rebooting
VFS
The kernel's abstraction layer providing a uniform file-like interface regardless of what sits behind each path
Syscall Tracing
Intercepting and recording every system call a process makes to observe the user/kernel boundary
Linux Observation Toolkit:
Tool
Layer It Reveals
What to Look For
ps
Process management
Process state, PID, resource usage
strace
Syscall boundary
Which kernel services the process requests
dmesg
Kernel-space events
Driver messages, hardware events, kernel warnings
ip
Network interfaces
Addresses, MTU, routing configuration
ss
Transport / sockets
Listening ports, established connections, 4-tuples
This section contains everything you need to reproduce the lecture walkthroughs on your own machine. You can follow these steps after class, or set up ahead of time and follow along live.
Step 1: Install Multipass
Download and install Multipass from multipass.run. It works on macOS (Intel and Apple Silicon), Windows, and Linux. Once installed, the multipass command should be available in your terminal.
Step 2: Launch the VM
multipass launch --name ln23 lts
This creates a new virtual machine named ln23 running the latest Ubuntu LTS release. The download happens once; subsequent launches reuse the cached image.
Step 3: Enter the VM
multipass shell ln23
Opens a shell session inside the ln23 VM. Everything from here on runs inside the VM, not on your host machine.
Installs the observability tools used throughout the lecture. strace for syscall tracing, iproute2 for ip and ss, procps for ps, netcat-openbsd for nc, tree for directory visualization, build-essential for compilation tools, and kmod for lsmod/modinfo/modprobe.
Step 5: Create the Demo Directory
Run the following commands inside the VM to create the demo directory with all files used during the lecture:
This script does more than reread the device ā it walks the entire dispatch chain from device node to driver source code, automating the manual sleuthing we did in the lecture body. Pass any character device path (defaults to /dev/urandom).
cat > ~/ln23-demo/device-read.sh << 'SCRIPT'#!/bin/bash# Driver dispatch tracer: device node -> major:minor -> driver name# -> sysfs entry -> file_operations -> read entry point -> kernel source URL
DEVICE="${1:-/dev/urandom}"echo"============================================================"echo" DRIVER DISPATCH TRACER for $DEVICE"echo"============================================================"echo""echo"[1/7] Read 16 bytes from $DEVICE (the user-space view)"ddif="$DEVICE" bs=16 count=1 2>/dev/null | xxd
echo""echo"[2/7] Inspect the device node ā extract major:minor"
LS_OUT=$(ls -la "$DEVICE")
echo" $LS_OUT"
MAJOR=$(echo"$LS_OUT" | awk '{print $5}' | tr -d ',')
MINOR=$(echo"$LS_OUT" | awk '{print $6}')
echo" -> major=$MAJOR minor=$MINOR"echo""echo"[3/7] Look up the driver name in /proc/devices (major -> name)"
DRIVER=$(awk -v m="$MAJOR"'$1==m {print $2}' /proc/devices)
echo" major $MAJOR -> driver group: '$DRIVER'"echo""echo"[4/7] Confirm via sysfs (/sys/dev/char/$MAJOR:$MINOR/uevent)"if [ -r "/sys/dev/char/$MAJOR:$MINOR/uevent" ]; then
sed 's/^/ /'"/sys/dev/char/$MAJOR:$MINOR/uevent"
DEVNAME=$(awk -F= '/^DEVNAME=/{print $2}'"/sys/dev/char/$MAJOR:$MINOR/uevent")
elseecho" (sysfs entry not present)"
DEVNAME=""fiecho""echo"[5/7] Find the file_operations dispatch table in /proc/kallsyms"echo" (requires sudo ā kernel symbols are root-readable)"
SYMS=$(sudo grep -E "(${DEVNAME}|${DRIVER})_fops" /proc/kallsyms 2>/dev/null | head -5)
if [ -n "$SYMS" ]; thenecho"$SYMS" | sed 's/^/ /'elseecho" (no matching *_fops symbol found)"fiecho""echo"[6/7] Find the read entry point (the function strace cannot see)"
READ_FN=$(sudo grep -E " ${DEVNAME}_read_iter| ${DRIVER}_read_iter" /proc/kallsyms 2>/dev/null | head -3)
if [ -n "$READ_FN" ]; thenecho"$READ_FN" | sed 's/^/ /'elseecho" (no matching *_read_iter symbol ā driver may use .read instead)"fiecho""echo"[7/7] Jump to the source code for THIS kernel"
KVER=$(uname -r | cut -d- -f1 | sed 's/\.0$//')
echo" kernel: $(uname -r) -> upstream tag: v$KVER"echo" drivers/char/random.c:"echo" https://github.com/torvalds/linux/blob/v$KVER/drivers/char/random.c"echo""echo"============================================================"echo" You have just traced the U-bend from device node to source."echo"============================================================"
SCRIPT
chmod +x ~/ln23-demo/device-read.sh
Running ./device-read.sh (or ./device-read.sh /dev/random) compresses the entire "Following the Dispatch to the Driver" section into a single labeled run. Every step is something you can do by hand ā the script just chains them so the link from 1, 9 in ls -la all the way to a clickable kernel source URL is impossible to miss.
This script runs the same strace from the lecture but annotates each surviving line with the U-bend stage it represents, so the trace reads as a narrative of one trip through the kernel.
cat > ~/ln23-demo/device-trace.sh << 'SCRIPT'#!/bin/bashecho"=== Tracing syscalls for dd reading /dev/urandom ==="echo"(Filtered to the interesting calls and annotated with U-bend stages)"echo""
strace -e openat,read,write,close -o /dev/stdout ddif=/dev/urandom bs=16 count=1 2>/dev/null \
| grep -aE 'urandom|read\(0|write\(1|write\(2|close\(0|close\(1|close\(2|exited' \
| sed \
-e 's|^\(openat.*urandom.*\)|\1 <-- LEFT TIP: enter kernel via VFS, get fd|' \
-e 's|^\(read(0,.*\)|\1 <-- BOTTOM: urandom_read_iter() runs in kernel|' \
-e 's|^\(write(1,.*\)|\1 <-- RIGHT TIP: bytes delivered back to user space (stdout)|' \
-e 's|^\(write(2,.*\)|\1 <-- dd progress message (also a syscall to stderr)|' \
-e 's|^\(close(0).*\)|\1 <-- release stdin fd|' \
-e 's|^\(close(1).*\)|\1 <-- release stdout fd|' \
-e 's|^\(close(2).*\)|\1 <-- release stderr fd|'
SCRIPT
chmod +x ~/ln23-demo/device-trace.sh
The sed post-processing tags each surviving syscall with which part of the U-bend it occupies. After running, you can read the trace top to bottom as a guided tour: enter the kernel, run the driver, return, clean up.
inspect-proc.sh ā Exploring /proc
cat > ~/ln23-demo/inspect-proc.sh << 'SCRIPT'#!/bin/bashecho"=== Open file descriptors for this shell ==="ls /proc/self/fd
echo""echo"=== Process status ==="cat /proc/self/status | head -15
echo""echo"=== System memory info ==="cat /proc/meminfo | head -10
SCRIPT
chmod +x ~/ln23-demo/inspect-proc.sh
cat > ~/ln23-demo/inspect-net.sh << 'SCRIPT'#!/bin/bashecho"=== Loopback interface ==="
ip addr show lo
echo""echo"=== Listening TCP sockets ==="
ss -tlnp
echo""echo"=== All established TCP connections ==="
ss -tnp
SCRIPT
chmod +x ~/ln23-demo/inspect-net.sh
Verifying the Setup
Once all scripts are created, verify the demo directory:
Each script is self-contained and prints labeled output, so you can run them in any order. For the network walkthrough, open two terminal sessions into the VM (multipass shell ln23 in two host terminals) ā the lecture body provides the commands to run directly.