Skip to main content
Be Bitwise

What to know before reading the Linux kernel source

The kernel uses a small bag of C tricks again and again. Learn the four or five idioms first and the source becomes readable instead of cryptic.

#linux-kernel#c#systems-programming

Most people open the kernel source the same way they read a paperback. Front cover, kernel/sched/core.c, scroll for ten minutes, close the tab. The code isn't hard, but it expects you to recognise a handful of C tricks that don't appear in textbooks. Learn those tricks first and the same file reads almost like ordinary C.

This post is the cheat sheet I wish someone had handed me. Five idioms, the directory layout, three good starting reads.

The C idioms you'll see on every page

The kernel is C99-ish, but with its own dialect built up since 1991. There are five things you should be able to spot at a glance.

Intrusive linked lists with container_of

The kernel almost never uses linked lists the way K&R does. Instead of a list of struct foo *, it embeds a struct list_head member inside struct foo and threads the list through that member. To get back from the embedded list_head to the enclosing foo, you call container_of(ptr, struct foo, list_member). That macro is offsetof arithmetic in disguise — it subtracts the field's offset within the struct from the pointer.

This pattern is everywhere. Once you see it, you'll notice every kobject, every struct file, every device — the whole kernel is held together with embedded list nodes and container_of to get back out.

Error pointers

Kernel functions that return a pointer don't return NULL on failure. They return an encoded errno cast to a pointer, packed into the high portion of the address space where no valid kernel pointer lives. So you'll see:

c
struct dentry *d = lookup_one_len(name, parent, len);
if (IS_ERR(d))
return PTR_ERR(d);

ERR_PTR(-EINVAL) packs the errno. IS_ERR() checks the magic range. PTR_ERR() unpacks back to a long. This neatly avoids needing both a return pointer and an out-parameter for the error code, and it's used by basically every function that returns a kernel object.

The macro forest

The kernel leans on macros heavily — for type-generic operations (min, max, swap), for boilerplate elimination (module_init, EXPORT_SYMBOL, DEFINE_PER_CPU), and for entire DSLs (the tracepoint machinery, the BPF helpers, the syscall declaration macros). The first time you see SYSCALL_DEFINE3(read, ...) it looks like nonsense. It expands to a function called sys_read with three typed arguments and various wrappers for ftrace and seccomp.

The trick is to skim macros, not unfold them in your head. Use a tool to do the unfolding when it matters.

Per-CPU and RCU patterns

DEFINE_PER_CPU(int, my_counter) allocates one slot per CPU. this_cpu_inc(my_counter) updates the local one without locks. RCU read-side critical sections sit between rcu_read_lock() and rcu_read_unlock(), with publishers using rcu_assign_pointer and synchronize_rcu. You don't need to understand RCU's internals to read code that uses it — just recognise that rcu_dereference(p) is "read a pointer the RCU way" and move on.

Implicit conventions on locking

If a function name ends in _locked, the caller already holds the relevant lock. If a function takes a struct foo * and the kernel-doc says "called with foo->lock held", you have to verify that yourself. There's no language-level help. When you read kernel code, you're constantly tracking which locks the current function assumes are held — sparse and lockdep do part of this work, but the discipline lives in your head.

Directory layout

The top-level tree looks like this:

arch/ holds CPU-specific code. Anything portable lives elsewhere. kernel/ has the scheduler, syscall entry tables, signals, time, locking primitives. mm/ is memory management. fs/ contains the VFS layer plus every filesystem implementation. drivers/ is by far the largest tree and the one you'll touch least if you're learning. net/ holds the network stack. include/linux/ is the kernel-internal headers; include/uapi/ is what userspace sees.

When you grep, restrict to the relevant subtree. git grep from the kernel root works but is slow on first run; cscope and ctags both index the tree well, and the Bootlin Elixir cross-referencer is the fastest way to follow a function across versions without checking out the source at all.

Build setup

Read-only browsing doesn't need a build. But once you want to follow macros properly, you'll want a configured tree. make defconfig && make -j$(nproc) prepare scripts is enough to generate the autoconf headers that most of the source assumes exist. After that, cscope -bqkR builds a cross-reference database in a few minutes. Inside vim or emacs, jump-to-definition starts working.

Three places to start

Syscall implementations are the most approachable code in the tree. They have well-defined inputs (the syscall arguments), well-defined outputs (a return value or -errno), and they sit at a clean boundary.

  1. sys_getpid() in kernel/sys.c. Two lines of real code. Reads current (a per-CPU pointer to the running task) and returns its PID. Useful as a sanity check that you can find anything at all.

  2. sys_read() entry in fs/read_write.c. Walk it down through vfs_read, see how the kernel resolves an fd to a struct file, then dispatches to f_op->read_iter. You'll meet container_of, error pointers, and the file_operations vtable pattern in one short trip.

  3. A simple ioctl in a small driver — drivers/char/mem.c is a fine target. Pick the /dev/null ioctl path. Driver code looks intimidating until you realise most of it is the same vtable plumbing wrapped around a few specific operations.

That's the whole onboarding. Skim the idioms, set up cscope or use Elixir, start with a syscall. The kernel wasn't written to obstruct you — it was written by people who got tired of typing the same boilerplate, and once you see the shorthand it stops looking weird.

Where this fits in

Module 22 (OS & Kernel) covers the conceptual ground — process model, virtual memory, the syscall interface — that makes the source make sense. Module 50 (Kernel Security Mechanisms) goes into SMEP, SMAP, KASLR, and the rest of what gets in your way once you start poking at the kernel from a security angle.