Skip to main content

Linux Kernel Internals

The Linux kernel is the heart of the Linux operating system. It manages all hardware resources, provides essential abstractions (like processes, files, and memory), and enforces security and isolation. Understanding kernel internals is crucial for systems programming, performance optimization, and senior engineering interviews.
Why this matters in interviews: At companies like Google, Meta, and Amazon, systems-level questions separate senior candidates from mid-level ones. When an interviewer asks “what happens when you call read()?”, they want to hear you trace the path from user space through the syscall interface, into VFS, down to the device driver, and back. This chapter gives you that full picture.

What is a Kernel?

The kernel is the core part of any operating system. It runs in a privileged mode (kernel space) and has direct access to hardware. User applications run in user space and must interact with the kernel to perform any privileged operation (like reading a file or allocating memory). Think of the kernel as the general manager of a large hotel. Guests (user programs) never interact directly with the plumbing, electrical wiring, or HVAC systems (hardware). Instead, they call the front desk (system call interface), which dispatches staff (kernel subsystems) to handle requests safely. A guest cannot rewire their room — they must go through management. This separation protects every guest from every other guest, and protects the building itself from careless occupants.
The Linux kernel source tree is over 30 million lines of code as of kernel 6.x. About 70% of that is device drivers. The core kernel (scheduler, memory manager, VFS, networking stack) is surprisingly compact relative to the driver surface area.
Interview Frequency: High for systems roles
Key Topics: Kernel architecture, system calls, modules, boot process
Time to Master: 15-20 hours

Kernel Architecture

The Linux kernel uses a monolithic architecture: all core services (process management, memory, device drivers, networking) are part of a single binary, but it supports loadable modules for flexibility. The kernel sits between user applications and hardware, providing a safe and efficient interface. The word “monolithic” trips people up. It does not mean “inflexible” or “one giant function.” It means all kernel subsystems share a single address space and can call each other directly via function calls — no IPC overhead, no message serialization. Compare this to a microkernel (like Mach or QNX), where the filesystem, networking, and drivers each run in separate user-space processes and communicate via message passing. Linux chose speed over isolation; microkernels chose isolation over speed. In practice, Linux compensates with loadable kernel modules, which give you microkernel-like flexibility (load a driver at runtime) without microkernel overhead.
A senior engineer would say: “Linux is monolithic in address space but modular in design. The module system gives us runtime extensibility, and interfaces like VFS and netfilter provide clean subsystem boundaries. The trade-off is that a buggy driver can panic the entire kernel — which is why driver quality and code review standards in the kernel community are legendarily strict.”

User Space vs Kernel Space

User space is where regular applications run. Kernel space is reserved for the OS kernel and its extensions. This separation is enforced by the CPU’s privilege rings (Ring 3 for user space, Ring 0 for kernel space on x86). A user-space program literally cannot execute privileged instructions — the CPU will raise a General Protection Fault if it tries. This is not a software convention; it is a hardware-enforced boundary.

System Calls

User programs cannot access hardware directly. Instead, they use system calls (syscalls) to request services from the kernel. Every meaningful operation — reading a file, creating a process, allocating memory, opening a network socket — requires crossing the user/kernel boundary through a syscall. The kernel validates and executes these requests, ensuring security and stability. Think of a syscall as a bank teller window. You (user space) stand on one side of bulletproof glass. You slide a form through the slot (syscall arguments in CPU registers). The teller (kernel) verifies your identity and your request, performs the transaction in the vault (hardware), and slides the result back. You never touch the vault directly. The cost of this interaction is the context switch overhead — roughly 100-200 nanoseconds on modern hardware, which is why high-performance systems try to minimize syscall frequency (batch operations, io_uring, vDSO).
Common misconception: malloc() is NOT a system call. It is a C library function that manages a user-space heap. It only calls brk() or mmap() (the actual syscalls) when it needs more memory from the kernel. Most malloc() calls are satisfied entirely in user space from previously obtained memory. This distinction matters in interviews.

System Call Flow

Here is how a typical system call works, step by step:
  1. The application calls a library function (like read() in C).
  2. The glibc wrapper sets up the syscall number in rax and arguments in registers (rdi, rsi, rdx, etc.), then executes the syscall CPU instruction on x86_64.
  3. The CPU saves the user-space instruction pointer and stack pointer, switches to Ring 0 (kernel mode), and jumps to the syscall entry point (entry_SYSCALL_64).
  4. The kernel saves all user registers onto the kernel stack, looks up the handler in sys_call_table[rax], validates arguments, and performs the requested action.
  5. The kernel places the return value in rax, restores user registers, and executes sysret to switch back to Ring 3 (user mode).

System Call Implementation

The __user annotation is not just a comment — it is checked by the Sparse static analysis tool (make C=1). Any kernel code that dereferences a __user pointer directly instead of using copy_to_user() / copy_from_user() will trigger a Sparse warning. Directly dereferencing user pointers is a security vulnerability (TOCTOU races, NULL pointer dereference via mmap of address 0, and kernel information leaks).

Key System Calls

Modern Linux has approximately 450 syscalls (the exact number depends on architecture). Some of the most important ones, grouped by subsystem:
You can see all syscalls on your system with ausyscall --dump or by reading the kernel header unistd.h. The syscall table is architecture-specific — x86_64 and ARM64 have different numbering. This is why portable code uses glibc wrappers rather than raw syscall numbers.

Process Management

In Linux, every running program is represented by a task_struct in the kernel. This structure holds all information about the process or thread: its state, scheduling info, memory, open files, credentials, and more. A crucial insight that trips up many candidates: Linux does not distinguish between processes and threads at the scheduler level. Both are task_struct instances. The difference is what resources they share. A thread is simply a task_struct that shares its memory descriptor (mm_struct), file table, and signal handlers with its parent. The clone() system call lets you specify exactly which resources to share — fork() shares nothing, pthread_create() shares almost everything.
A senior engineer would say: “In Linux, a thread is just a process that shares its address space with another process. There is no separate ‘thread struct’ — the scheduler sees them identically. This is why Linux threading is so efficient compared to older Unix systems that had bolt-on thread libraries. It also means tools like top and ps can show threads and processes uniformly.”

Task Struct

The task_struct is the kernel’s internal data structure for tracking processes and threads. On a 64-bit kernel, a single task_struct is roughly 6-8 KB — and every process and thread on the system has one. Think of it as a personnel file in an HR department: it contains everything the organization needs to manage that employee — their schedule, their office assignment, their security clearance, their relationships to other employees.

Process States

Processes in Linux can be in various states: running, waiting, stopped, zombie, etc. The kernel manages transitions between these states as processes execute, wait for I/O, or terminate.
TASK_UNINTERRUPTIBLE is the state that creates the dreaded “D state” processes you see in ps output — processes that cannot be killed even with kill -9. They are waiting for I/O (typically disk or NFS) and the kernel refuses to interrupt them because doing so could corrupt filesystem data structures. If you see many D-state processes, it almost always means a storage problem (hung NFS mount, dying disk, overloaded SAN). This is the single most common “unkillable process” question in interviews.

Memory Management

Linux uses virtual memory to give each process the illusion of a private, contiguous address space. The kernel manages page tables, handles page faults, and allocates physical memory efficiently. The key insight to internalize: virtual memory is a lie, and that lie is the most powerful abstraction in computing. Every process believes it has 128 TB of contiguous memory starting at address 0. In reality, its “memory” is scattered across physical RAM pages, possibly compressed, possibly on swap, possibly not yet allocated at all. The kernel and MMU hardware maintain this illusion transparently — and the cost of maintaining it (page table walks, TLB misses) is one of the biggest performance factors in modern systems.

Address Space Layout

The address space of a process is divided into regions: code (text), data, heap, stack, and memory-mapped areas. The kernel enforces boundaries and permissions for each region, protecting processes from each other. On x86_64, only 48 bits of the 64-bit address space are used (256 TB total), split evenly between user space (lower 128 TB) and kernel space (upper 128 TB). The gap in the middle is intentionally unmapped — any pointer arithmetic that accidentally crosses from user to kernel space hits this gap and faults immediately.
ASLR (Address Space Layout Randomization) randomizes the base addresses of the stack, heap, mmap region, and even the executable itself on each execution. This is a critical security mitigation against buffer overflow exploits. Check it with cat /proc/sys/kernel/randomize_va_space (2 = full ASLR). Run cat /proc/self/maps twice — the addresses will differ each time.

Page Tables (4-Level)

Modern CPUs (like x86-64) use multi-level page tables to efficiently map virtual addresses to physical memory. The kernel walks these tables to resolve memory accesses and handles page faults when needed. Think of it like a library’s nested index system: to find a book, you first check the floor directory (PGD), then the section sign (PUD), then the shelf label (PMD), then the specific slot (PTE). Each level narrows the search by 512x. Why not use a flat table? A single-level page table for a 48-bit address space with 4KB pages would need 2^36 entries = 512 GB of RAM just for the table itself. Four levels of indirection mean we only allocate table pages for address ranges actually in use — a process using 100 MB of memory needs only a few KB of page tables.
TLB (Translation Lookaside Buffer) caches recent virtual-to-physical translations. A TLB miss forces the hardware to walk all 4 page table levels — roughly 4 memory accesses. On a modern CPU, TLB capacity is typically 1,536 entries for 4KB pages. TLB flushes (caused by context switches, mprotect(), or kernel page table updates) are one of the most significant hidden performance costs in systems programming.

Memory Allocation Layers

Memory allocation in Linux happens in layers, each solving a different problem. This layered design is a recurring pattern in the kernel — each layer provides an abstraction that simplifies the layer above it:
  • User programs call malloc() (implemented by glibc’s ptmalloc2, or alternatives like jemalloc/tcmalloc). This is purely user-space bookkeeping.
  • The C library requests large chunks from the kernel via brk() (extends the heap) or mmap() (maps a new anonymous region). These are the actual syscalls. glibc then sub-divides these chunks to satisfy individual malloc() calls.
  • The VM subsystem manages Virtual Memory Areas (VMAs, tracked as vm_area_struct), page tables, and demand paging. When a process first touches a page, the VM handles the page fault and allocates physical memory.
  • The slab allocator (SLUB) provides fast, cache-friendly allocation of fixed-size kernel objects (e.g., task_struct, inode, dentry). It pre-allocates slabs of identically-sized objects to avoid fragmentation.
  • The buddy allocator manages raw physical page frames in power-of-2 blocks (order 0 = 4KB, order 1 = 8KB, …, order 10 = 4MB). It is the foundation upon which everything else is built.
Performance insight: malloc() of 64 bytes typically costs ~20 nanoseconds (pure user-space). mmap() of a new page costs ~1-2 microseconds (syscall + kernel bookkeeping). A page fault on first access costs ~2-5 microseconds (allocate physical page, zero it, update page tables, flush TLB entry). Understanding these cost tiers is critical for performance-sensitive code.

Kernel Modules

Kernel modules are pieces of code that can be loaded into or removed from the running kernel at runtime. They extend kernel functionality (like device drivers, filesystems, or network protocols) without requiring a reboot. This is what makes Linux’s monolithic architecture practical — you get the performance benefits of a monolithic kernel with the flexibility of loading only what you need. Think of modules like plugins for a web browser. The browser (kernel) provides the core engine, and plugins (modules) add specific capabilities. You do not need to recompile Chrome to install an ad blocker; you do not need to recompile the kernel to add a new filesystem driver.
Security implication: A loaded kernel module runs with full kernel privileges (Ring 0). A malicious or buggy module can read any memory, intercept any syscall, or crash the entire system. This is why modern distros require module signing (CONFIG_MODULE_SIG), and production servers often disable module loading entirely after boot via kernel.modules_disabled=1 in sysctl.

Module Structure

Building a Module

Character Device Driver

Character devices are the most common type of device driver in Linux. They provide a stream-oriented interface (read/write byte sequences) as opposed to block devices (read/write fixed-size blocks). Examples: serial ports, terminals, /dev/null, /dev/random, GPU devices. The key abstraction is struct file_operations — a vtable of function pointers. When user space calls read() on /dev/mydev, the VFS dispatches to your .read function pointer.
Production pattern: Real drivers use container_of() extensively. If your device has private state (a buffer, a lock, configuration), embed the struct cdev inside your own struct and use container_of() in your file_operations to recover a pointer to your state from the struct inode *. This avoids global variables and supports multiple device instances.

Boot Process

The boot process takes a computer from “powered off” to “running your applications” through a carefully orchestrated chain of handoffs. Each stage initializes just enough hardware and software to load the next stage. Think of it like a relay race: the BIOS/UEFI hands off to GRUB, which hands off to the kernel, which hands off to initramfs, which hands off to systemd. At each baton pass, the system gains more capability.

Kernel Command Line

The kernel command line is passed by the bootloader and parsed during early initialization. These parameters control everything from which device to mount as root to debugging options. They are the primary “configuration file” for the kernel itself.
Emergency recovery trick: If your system will not boot, edit the GRUB command line (press e at the GRUB menu) and append init=/bin/bash. The kernel will drop you into a root shell without loading systemd or any services. From there you can fix /etc/fstab, reset passwords, or repair broken packages. This is the single most useful sysadmin recovery technique.

Kernel Debugging

Debugging kernel code is fundamentally different from user-space debugging. You cannot attach gdb to a running kernel easily (though KGDB exists). You cannot use printf and stderr. You cannot crash and restart quickly — a kernel bug often means a system reboot. The kernel’s debugging infrastructure has evolved to provide observability without these luxuries.

printk and Dynamic Debug

Rate limiting: In production, a tight loop calling pr_err() can flood the log buffer and cause performance issues. Use pr_err_ratelimited() to automatically suppress repeated messages. The printk_ratelimit() function defaults to 10 messages every 5 seconds.

/proc and /sys

These two virtual filesystems are your primary window into the running kernel. Neither occupies disk space — they are generated on-the-fly by the kernel in response to read() calls. /proc is the older, somewhat chaotic interface (process info mixed with system info). /sys (sysfs) is the newer, structured interface organized around the device model. Together, they expose thousands of kernel parameters and counters.
Debugging with /proc: When investigating a mysterious process, /proc/<pid>/maps shows you exactly what libraries it has loaded and where. /proc/<pid>/fd/ shows every open file and socket. /proc/<pid>/status tells you thread count, memory usage, and signal state. Master these three files and you can diagnose most process-level issues without installing any tools.

Tracing

Linux has a rich tracing infrastructure that lets you observe kernel behavior in real time. The evolution: printk (oldest, manual) -> ftrace (function-level, built-in) -> perf (sampling + events) -> eBPF/bpftrace (programmable, production-safe). Each tool has its niche.
A senior engineer would say: “For production debugging, I reach for bpftrace or BCC tools first. They use eBPF, which means the kernel’s verifier guarantees they cannot crash the system or corrupt memory. ftrace’s function tracer is too heavy for production, but its event tracing is fine. perf is the right tool when I need CPU profiling or PMU counter analysis.”

Interview Deep Dive Questions

Answer:1. Shell reads input:
2. Shell parses and finds executable:
3. fork() creates child process:
4. execve() replaces child with ls:
5. ls executes:
6. ls exits:
7. Shell reaps child:
Key system calls: read, fork, execve, openat, getdents64, write, exit_group, wait4
Answer:Problem: fork() creates complete copy of parent’s address space. With large processes, this would be slow and wasteful.Solution: Copy-on-Write
Implementation:
Benefits:
  • fork() is O(1) in page table size, not memory size
  • Pages never written are never copied
  • exec() after fork() doesn’t copy at all
Answer:Page fault types:
  1. Minor fault: Page in memory but not mapped
  2. Major fault: Page not in memory (disk I/O needed)
  3. Invalid fault: Access violation (segfault)
Handler flow:
Common scenarios:
Answer:Problem: Interrupt handlers must be fast, but some work takes time.Solution: Deferred work mechanisms
Comparison:Examples:
Answer:Futex = Fast Userspace muTEXGoal: Avoid syscall in the common (uncontended) case.
Kernel implementation:
Why it’s fast:
  • Uncontended: Just an atomic operation, no syscall
  • Hash table lookup in kernel is O(1)
  • Foundation for pthread_mutex, semaphores, condition variables
What interviewers are testing: Real-world debugging methodology that connects kernel internals knowledge to operational troubleshooting.Step 1 — Narrow the scope with top: Identify which processes have the highest system time. If it is a single process, the problem is likely in that process’s syscall pattern. If it is spread across many processes, the problem is system-wide (interrupt storm, lock contention, memory pressure).Step 2 — Identify the syscalls with strace -c -p <pid>: Get a summary of syscalls by count and time. Millions of futex() calls means lock contention. Heavy write() or read() means I/O. Churning mmap/munmap means pathological memory allocation.Step 3 — Trace kernel functions with perf top: See which kernel functions are consuming CPU. Common findings:
  • copy_to_user / copy_from_user — heavy I/O with small buffers (fix: batch reads/writes)
  • _raw_spin_lock — kernel lock contention (fix: reduce contention path)
  • page_fault — heavy allocation or working set exceeds RAM
  • tcp_sendmsg / tcp_recvmsg — network-bound workload
Step 4 — Check interrupts and context switches: cat /proc/interrupts for interrupt storms. vmstat 1 shows context switch rate — above 100K/sec usually indicates too many threads contending for CPUs or spinlock contention.Step 5 — Check I/O and memory pressure: iostat -x 1 for disk utilization. cat /proc/pressure/memory for PSI metrics. High kswapd CPU means the system is swapping — a memory sizing problem, not a kernel bug.The investigation follows a funnel: broad observation to narrow identification to root cause. This methodology matters more than any single tool.
What interviewers are testing: Understanding of containerization at the kernel level, beyond Docker commands.Namespaces provide isolation — each type gives a process its own view of a global resource:
  • PID namespace: Process sees itself as PID 1, cannot see host processes
  • NET namespace: Own network stack (interfaces, routing table, iptables rules)
  • MNT namespace: Own filesystem mount table
  • UTS namespace: Own hostname
  • USER namespace: UID mapping (appear as root inside, non-root outside)
Cgroups provide resource limiting — constrain how much of a resource a group of processes can use:
  • CPU: limits, shares, quotas (e.g., “at most 2 CPUs”)
  • Memory: hard limits, OOM behavior (e.g., “kill at 4GB”)
  • I/O: bandwidth and IOPS limits per device
  • PIDs: maximum process count (prevents fork bombs)
A container is simply a process running with namespace isolation and cgroup limits. docker run calls clone() with namespace flags, sets up cgroups, mounts overlayfs, and execs the entrypoint.Security implications: Namespaces provide isolation, not security. A process with CAP_SYS_ADMIN inside a container can escape to the host. Production containers need: seccomp profiles (syscall filtering), AppArmor or SELinux (mandatory access control), dropped capabilities, non-root users inside the container, and user namespaces. Kernel vulnerabilities that bypass namespace checks are regularly discovered — defense in depth is essential.

Kernel Exploration Commands

These commands are your toolkit for understanding what the kernel is doing on any Linux system. No installation required — they all use built-in interfaces.
PSI (Pressure Stall Information) was added in kernel 4.20 and is what Facebook uses internally to detect resource contention. It answers “what percentage of time are tasks waiting for this resource?” — a much more actionable metric than load average. If cat /proc/pressure/memory shows some avg10=25.00, it means tasks spent 25% of the last 10 seconds stalled on memory. This is the metric that triggers proactive OOM intervention in production.

End-to-End Walkthrough: read() on a TCP Socket

To connect the dots between subsystems, trace a single read() call in a typical server. This walkthrough is the kind of answer that impresses in a staff-level interview — it demonstrates that you understand how kernel subsystems compose, not just how each one works in isolation:
  1. User-space call:
    • Application thread calls read(fd, buf, n) on a TCP socket.
    • The C library issues the read system call (e.g., syscall(SYS_read, ...)).
  2. Syscall entry:
    • CPU executes syscall / svc instruction.
    • Control transfers to the kernel’s syscall entry (entry_SYSCALL_64 on x86-64).
    • The kernel locates the struct file for fd and dispatches to the socket’s file operations.
  3. VFS and socket layer:
    • The VFS read implementation calls into the socket layer (sock_read_iter).
    • This eventually calls the protocol-specific recvmsg implementation (e.g., tcp_recvmsg).
  4. TCP stack and receive queue:
    • If there is already data in the socket’s receive queue (filled by earlier packets), tcp_recvmsg copies it into buf and returns.
    • If not, it may sleep the process, putting it on a wait queue until more data arrives.
  5. Network device and driver:
    • Incoming packets trigger an interrupt on the NIC.
    • The driver’s interrupt handler schedules NAPI polling or other bottom-half work.
    • Packets are pulled from the NIC’s DMA ring into memory as sk_buff structures.
  6. Protocol processing:
    • The kernel’s networking stack parses headers, validates checksums, and places payload bytes into the appropriate socket’s receive buffers.
    • When enough data is available, it wakes up the sleeping read() caller.
  7. Copy to user and return:
    • tcp_recvmsg copies data from kernel buffers into the user-space buf using safe copy helpers.
    • The syscall returns to user space with the number of bytes read.
Throughout this path you touch:
  • Syscall machinery (entry_SYSCALL_*).
  • VFS (struct file, file operations).
  • Networking stack (TCP/IP implementation, sk_buff).
  • Scheduler and wait queues (sleep and wake-up of the thread).
  • Interrupt handling and drivers (NIC, NAPI, DMA).
When reading the rest of this course, try to anchor concepts back to this kind of end-to-end path.

Caveats and Common Pitfalls

The kernel is unforgiving in ways that user-space code is not. A subtle mistake compiles fine, passes light testing, and panics under production load three weeks later. The pitfalls below are the ones that bite engineers who treat the kernel like a slightly stricter version of user space.
Kernel-internals traps that catch even experienced engineers:
  1. Kernel internal API stability is not guaranteed — not even between minor releases. The user-space syscall ABI is sacred (Linus has shouted at people for breaking it), but internal APIs change constantly. A function signature that worked in 6.1 may have sprouted a new argument in 6.5, been renamed in 6.8, and split into three helpers in 6.10. Out-of-tree drivers and kernel modules that depend on internal APIs feel this acutely — every kernel upgrade is a porting exercise. NVIDIA’s proprietary drivers, ZFS-on-Linux, and many vendor drivers maintain large compatibility shim layers precisely because the kernel community refuses to freeze internal APIs to protect external code.
  2. GPL vs non-GPL symbol exports matter legally, not just technically. The kernel exports symbols with EXPORT_SYMBOL (any module can use) or EXPORT_SYMBOL_GPL (only GPL-licensed modules). Many critical interfaces — scheduler internals, RCU primitives, modern memory APIs — are GPL-only. If your module declares MODULE_LICENSE("Proprietary"), it cannot link against GPL-only symbols, and the kernel will refuse to load it. Worse, attempting to evade this with license-laundering tricks creates real legal exposure — the FSF and SFC have litigated this. If your driver needs GPL symbols, your driver must be GPL.
  3. preempt_disable() and local_irq_disable() leave the system in a fragile state. Forgetting to re-enable them is a classic kernel bug: the CPU stops scheduling other tasks (preempt) or stops handling interrupts (irq) until you restore them. A single early-return or unhandled error path that skips preempt_enable() can hang an entire CPU core. Always pair these with the matching restore call, and prefer the _irqsave / _irqrestore variants that capture and restore state automatically. The kernel’s lockdep checker can catch many of these, but not all.
  4. RCU usage requires strict reader/writer discipline. Read-Copy-Update is brilliant when used correctly and catastrophic when used wrong. Readers must call rcu_read_lock() / rcu_read_unlock() and must NOT sleep, block, or call synchronize_rcu() from within a critical section — doing so deadlocks. Writers must publish updates with rcu_assign_pointer() and free old data only after synchronize_rcu() returns. Mixing RCU with other primitives (taking a mutex inside an RCU read section that the writer also takes outside) creates deadlocks that only appear under specific timings. The CONFIG_PROVE_RCU debug option is mandatory for development — never write RCU code without it.
Solutions and patterns:
  • Pin to LTS kernels for production drivers. If you maintain an out-of-tree module, target Linux LTS releases (currently 6.1, 6.6, etc.) and accept that you will rebuild and re-test every two years. Avoid “we always use latest mainline” — the API churn will exhaust your team. The Linux Kernel Driver Project (drivers/staging) and upstreaming is the only sustainable long-term answer for most third-party drivers.
  • Use EXPORT_SYMBOL_GPL audit tooling. Before depending on an internal API, grep for EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL in the kernel source. If your module is not GPL, you cannot use GPL-only exports. Plan your architecture around this — often a GPL “shim layer” plus a non-GPL “logic layer” is the cleanest split, but get a lawyer to bless the boundary.
  • Always use local_irq_save / local_irq_restore instead of _disable / _enable. The save/restore variants capture the previous interrupt state and put it back atomically. The disable/enable variants assume the previous state and can corrupt nested locking. Same pattern for preempt: prefer get_cpu() / put_cpu() which handles preempt counting correctly.
  • Enable lockdep, KASAN, and PROVE_RCU during development. These debug options catch lock ordering bugs, use-after-free, and RCU misuse. They cost performance, so disable them in production, but every kernel module CI run should have them on. The fact that NVMe and io_uring shipped with so few correctness bugs is a direct result of this discipline at Facebook and Intel.
  • Prefer kvmalloc() over kmalloc() for variable-size allocations. kmalloc() requires physically contiguous memory, which can fail under fragmentation. kvmalloc() tries kmalloc() first and falls back to vmalloc(). This single change has prevented countless allocation failures in modern drivers.

Interview Deep-Dive

Strong Answer:At 95% memory usage, the kernel is under severe memory pressure but the OOM killer has not triggered because there is technically still reclaimable memory. Here is what is happening inside the kernel and how I would investigate:What the kernel is doing internally: The kernel’s page reclaim mechanism (kswapd) is running constantly, trying to free pages by scanning LRU (Least Recently Used) lists. It is evicting page cache pages (file-backed pages that can be re-read from disk), writing dirty pages back to disk, and possibly swapping anonymous pages to swap space. The system feels unresponsive because every malloc() or page fault now triggers direct reclaim — the allocating process itself must scan for freeable pages before its allocation can succeed. This adds latency of milliseconds to what should be nanosecond operations.Why OOM has not fired: The OOM killer is a last resort. The kernel tries increasingly aggressive reclaim first: kswapd background reclaim, then direct reclaim in the allocating process’s context, then compaction, then dropping caches. Only when __alloc_pages_slowpath() exhausts all options does it invoke the OOM killer. The threshold is “all reclaimable memory has been tried and allocation still fails,” not a simple percentage.Investigation steps:
  1. cat /proc/meminfo — check MemAvailable, Buffers, Cached, SwapFree, and critically Slab (kernel object caches can be huge). If Slab is 30GB on a 64GB machine, a kernel memory leak is likely (often caused by a dentry/inode cache explosion from a find traversing millions of files).
  2. cat /proc/pressure/memory — PSI metrics tell me what percentage of time tasks are stalled on memory. If full avg10 is above 50%, the system is effectively thrashing.
  3. slabtop — shows which kernel slab caches are consuming the most memory. Look for dentry, inode_cache, ext4_inode_cache growing unbounded.
  4. Per-process: smem -t or ps aux --sort=-%mem to identify the top consumers. Check /proc/<pid>/smaps_rollup for PSS (Proportional Set Size) — this accounts for shared libraries correctly.
  5. cat /proc/vmstat | grep -E 'pgfault|pgmajfault|pgscan|pgsteal' — if pgscand (direct reclaim scans) is climbing, processes are blocking on memory allocation. If pgmajfault is high, the system is page-faulting from disk (thrashing).
Immediate mitigation: If the service is leaking, restart it. If the system needs breathing room, echo 3 > /proc/sys/vm/drop_caches drops clean page cache and slab caches. For the longer term, configure cgroup memory limits so no single service can starve the system.Follow-up: How does the OOM killer decide which process to kill?The OOM killer scores every process using oom_score (visible at /proc/<pid>/oom_score). The score is based on memory consumption (RSS), adjusted by oom_score_adj (-1000 to +1000, set by the admin). A score of -1000 makes a process un-killable (used for critical infrastructure like sshd). The kernel selects the process with the highest score — the idea is to free the most memory with the least impact. In practice, this often kills the process you wanted it to kill, but not always. Kubernetes sets oom_score_adj based on QoS class: BestEffort pods get +1000 (killed first), Guaranteed pods get -997 (killed last).
Strong Answer:The problem: Some system calls are called millions of times per second but do not actually need kernel privileges. gettimeofday() is the classic example — it just reads a counter. But the standard syscall path costs 100-200 nanoseconds (register save, ring transition, handler dispatch, register restore). For a server calling gettimeofday() once per request at 1M requests/sec, that is 100-200 ms/sec of pure overhead.What vDSO is: The vDSO (virtual Dynamic Shared Object) is a small shared library that the kernel maps into every process’s address space. It looks like a regular .so file (visible in /proc/<pid>/maps as [vdso]), but the kernel owns it and keeps its data up to date. When you call gettimeofday(), glibc detects that a vDSO implementation is available and calls it as a normal function — no ring transition, no syscall instruction. The kernel maintains a shared memory page with the current time, and the vDSO function simply reads it.Which calls it accelerates on x86_64:
  • gettimeofday() — reads from a kernel-maintained time page. This is the biggest win.
  • clock_gettime() — same mechanism, supports CLOCK_MONOTONIC, CLOCK_REALTIME.
  • time() — trivially derived from the above.
  • getcpu() — reads the CPU number from a per-CPU segment register.
The related vsyscall page (deprecated, legacy) provided the same optimization but at fixed addresses, which was a security risk (no ASLR). The vDSO replaced it with a proper ASLR-compatible shared object.Limitations:
  1. Only read-only, non-privileged information can be served this way. You cannot vDSO-ify read() or write() because those require kernel arbitration of hardware.
  2. The kernel must update the shared data page on every timer tick. If the timer tick is 1ms (HZ=1000), clock resolution through vDSO is limited to 1ms. Hardware TSC (Time Stamp Counter) gets around this.
  3. clock_gettime(CLOCK_PROCESS_CPUTIME_ID) does NOT go through vDSO — it requires reading per-process counters that live in kernel space.
  4. If the kernel detects that the TSC is unreliable (unstable, or running at different rates on different CPUs), it falls back to a real syscall.
Practical impact: Benchmarks show gettimeofday() via vDSO takes approximately 20 nanoseconds versus approximately 200 nanoseconds via a real syscall. For a Redis-like server doing 500K ops/sec with timestamp logging, this saves roughly 90 ms/sec of CPU time — approximately 9% of one core.Follow-up: What is the vDSO equivalent for ARM64?ARM64 has the same concept but the implementation differs. The kernel maps a code page into user space that uses the mrs instruction to read the virtual counter register (CNTVCT_EL0) directly. The benefit is the same: avoid the svc instruction (ARM’s equivalent of syscall) for time-related functions. The kernel also exposes a data page with pre-computed time offsets so the user-space code can convert counter ticks to nanoseconds without a syscall.
Strong Answer:This is one of the most common kernel programming bugs. The error means the driver called a function that can sleep (block the calling thread) from a context where sleeping is forbidden — specifically, from atomic context (interrupt handler, softirq, or while holding a spinlock).Why sleeping is forbidden in atomic context: When the kernel is executing an interrupt handler, it has preempted whatever was running on that CPU. If the interrupt handler sleeps (calls schedule()), the CPU has nothing to run — the interrupted task cannot resume because the interrupt context is still on the stack, and the sleeping task cannot make progress. Result: deadlock or worse, the kernel detects this violation and panics.Similarly, sleeping while holding a spinlock means the lock is never released until you wake up, but other CPUs spinning on that lock waste 100% of their CPU time waiting. On a single-CPU system, if the spinlock holder sleeps, the system deadlocks because nothing can wake it up.Common culprits in driver code:
  • kmalloc(size, GFP_KERNEL) inside an interrupt handler. Fix: use GFP_ATOMIC (does not sleep, may fail if memory is tight).
  • mutex_lock() inside a spinlock-protected section. Fix: use spin_lock() consistently, or restructure so the mutex is taken outside the spinlock.
  • copy_to_user() / copy_from_user() inside a softirq or interrupt. These can page-fault, which requires sleeping. Fix: buffer the data and defer the copy to process context (workqueue).
  • msleep() or schedule_timeout() in a tasklet. Fix: use a workqueue instead, which runs in process context and CAN sleep.
How to find the bug: The stack trace in the crash log shows exactly which sleeping function was called and from where. The kernel’s might_sleep() debug check (enabled by CONFIG_DEBUG_ATOMIC_SLEEP) inserts checks at every potentially-sleeping function. Enable this during development and your test suite will catch these bugs even under light load.Prevention rules for kernel code:
  • In interrupt context: no sleeping, no GFP_KERNEL, no mutexes, no user-space access.
  • Under spinlock: same restrictions as interrupt context.
  • In workqueue / process context: anything goes — you can sleep, allocate, take mutexes.
  • When in doubt, check with in_atomic() or in_interrupt().
Follow-up: What is the difference between spin_lock(), spin_lock_bh(), and spin_lock_irqsave()?spin_lock() disables preemption on the local CPU but does NOT disable interrupts. Use it when the lock is only taken from process context. spin_lock_bh() also disables bottom halves (softirqs/tasklets), so use it when the lock is shared between process context and a softirq. spin_lock_irqsave() saves the interrupt state and disables interrupts, so use it when the lock is shared between process context and a hardware interrupt handler. Using a weaker variant than needed causes deadlocks; using a stronger variant than needed wastes performance by disabling interrupts unnecessarily.
Strong Answer Framework:
  1. Firmware (BIOS or UEFI). On power-on, the CPU starts in 16-bit real mode (BIOS) or 64-bit long mode (UEFI). The firmware initializes essential hardware (memory controller, basic I/O), runs POST, and decides what to boot. UEFI reads the EFI System Partition for a signed bootloader; BIOS reads the MBR. UEFI Secure Boot verifies the bootloader signature against keys in firmware — this is where many “kernel will not boot after upgrade” issues live.
  2. Bootloader (GRUB2 typically). GRUB lives in the EFI partition (or MBR). It reads its config (/boot/grub/grub.cfg), shows the menu, and loads the kernel image (vmlinuz) and initramfs into memory. GRUB also passes the kernel command line (root=/dev/sda2 ro quiet splash ...).
  3. Kernel decompression and early boot. vmlinuz is a self-extracting compressed image. The first thing it does is decompress itself, then jump to start_kernel() in init/main.c. This is where every architecture-specific path converges. Early init sets up the page tables, initializes the scheduler with PID 0 (the idle task), brings up other CPUs (SMP), and initializes essential subsystems (memory allocator, scheduler, timekeeping).
  4. initramfs (initial RAM filesystem). Before the real root filesystem is available, the kernel mounts an in-memory cpio archive provided by GRUB. This contains just enough drivers (storage, RAID, LVM, encryption) to find and mount the real root filesystem. The kernel runs /init from initramfs — typically a script or systemd-in-initramfs.
  5. Pivot to real root. Once the real root is mounted, the kernel calls switch_root (or pivot_root), which atomically replaces / with the real root and re-execs /sbin/init (which is usually a symlink to /lib/systemd/systemd).
  6. PID 1 (systemd). systemd reads its unit files, brings up targets in dependency order (sysinit, basic, multi-user, graphical), starts services, and the system is “up.” Failures here look like “boot hangs at [OK] Started …” with no progress.
Real-World Example: In 2021, Microsoft’s Patch Tuesday update broke booting on certain Lenovo and HP laptops with TPM 2.0 + BitLocker. The root cause was a UEFI variable size mismatch introduced by Windows update interacting with firmware — but the symptom was identical to a GRUB or initramfs failure. Engineers debugging this had to learn the full boot chain to even know whose code was at fault. Reference: KB5005076 (August 2021). The deeper lesson is that “Linux boots” is a chain of trust from firmware -> bootloader -> kernel -> initramfs -> systemd, and any link can break in ways that look identical from the boot screen.
Senior follow-up 1: “What is kexec and when would you use it?” kexec skips the BIOS/UEFI/bootloader stages and boots a new kernel directly from a running kernel. Used for fast reboots (no firmware POST), kernel crash dumps (kexec to a small kernel that dumps memory), and live kernel upgrades. The trade-off is that hardware does not get re-initialized, so any wedged device state persists.
Senior follow-up 2: “Why have an initramfs at all — why not just put everything in the kernel?” Modularity. The kernel image must work on any hardware; the initramfs is built per-system with the exact drivers needed. Without initramfs, the kernel would need every storage driver compiled in (massive image) or be re-compiled per system (impractical). initramfs lets a generic kernel adapt to specific hardware at boot.
Senior follow-up 3: “What is the difference between init=, rdinit=, and systemd.unit= on the kernel command line?” rdinit= overrides the initramfs init (default /init); init= overrides the post-pivot init (default /sbin/init). systemd.unit= tells systemd which target to boot into. Use init=/bin/bash to drop to a root shell when systemd is broken — a critical recovery technique.
Common Wrong Answers:
  1. “GRUB loads the kernel and the kernel just runs userspace.” Skips the entire decompression, early-init, initramfs, and pivot phases — which is where most boot failures actually occur. A senior engineer needs to know the full chain because the symptom of failure depends on the stage.
  2. “systemd is the kernel.” No — systemd is PID 1, a userspace process. The kernel is everything before that. Confusing the two leads to misdiagnosing whether a hang is a kernel issue, a systemd unit issue, or a service-level issue.
Further Reading:
  • Linux Documentation Project, “From Power Up to Bash Prompt” — the canonical free reference.
  • man 7 boot and man 7 bootup — official systemd boot sequence documentation.
  • Greg Kroah-Hartman, “Linux Kernel in a Nutshell” (free PDF) — chapter on boot covers kernel command line and initramfs in depth.
Strong Answer Framework:
  1. What RCU is. Read-Copy-Update is a synchronization primitive optimized for read-mostly workloads. Readers pay zero cost (no atomic operations, no cache-line bouncing); writers pay all the cost (copy, update, wait for grace period before freeing the old version). The “grace period” is the critical concept: after a writer publishes a new version, it must wait until every CPU has gone through a quiescent state (context switch or returned to userspace) before it can free the old version, because some reader might still be looking at it.
  2. When RCU is right. Read-mostly data structures where readers vastly outnumber writers and read latency matters more than memory: routing tables, DNS caches, task lists, security policy tables. The Linux kernel’s process list, network routing table, and namespace table all use RCU. Reads scale linearly across CPUs because there is no contention.
  3. When RCU is wrong. Write-heavy workloads (writers serialize against each other and pay grace-period cost), workloads where readers need to block or sleep (RCU read-side critical sections cannot sleep), workloads where memory is tight (the old version must live until the grace period ends — could be many milliseconds), and workloads where strong consistency is required (RCU readers may see slightly stale data).
  4. The mental model. Think of RCU as “publish to readers atomically, defer reclamation until all readers are done.” It is essentially a sophisticated form of garbage collection for kernel data structures.
  5. The classic alternative. A rwlock gives readers shared access and writers exclusive access, but readers still take a lock (cache-line bounce, atomic ops). RCU is faster for readers but harder to reason about. For new code, rwlock is often a safer first choice; reach for RCU when profiling shows reader-side contention.
Real-World Example: The Linux network routing table famously moved to RCU in the early 2.6 series (around 2005, commits by David Miller). Before that, every packet routing decision took a rwlock, which became a scalability bottleneck on multi-core systems. After the RCU conversion, packet forwarding scaled linearly with cores — a measured 2-3x improvement on 16-core systems. The flip side: kernel developers had to learn RCU semantics, and there were several bugs in the first year (use-after-free when a writer freed memory before all readers had finished). The CONFIG_PROVE_RCU machinery was added largely in response.
Senior follow-up 1: “What is a ‘sleepable RCU’ (SRCU) and why does it exist?” Standard RCU forbids readers from sleeping because the grace period detection assumes readers are bounded. SRCU lets readers sleep, at the cost of more expensive writer-side synchronization. Used for cases like notifier chains where callbacks may legitimately block.
Senior follow-up 2: “How does the kernel detect a grace period?” In classic RCU, every CPU passing through a quiescent state (context switch, idle, or userspace return) increments a counter. When all CPUs have advanced past the writer’s start point, the grace period is complete. Tree RCU (the modern implementation) uses a hierarchical structure to scale this to thousands of CPUs.
Senior follow-up 3: “Can you use RCU in user space?” Yes — the userspace RCU library (liburcu, by Mathieu Desnoyers) provides the same primitives outside the kernel. It is used in DPDK, LTTng, and high-performance network applications. The semantics are similar but the grace-period detection is different (it cannot rely on context switches the same way).
Common Wrong Answers:
  1. “RCU is a lock, just faster.” RCU is not a lock — it is a synchronization protocol. There is no “RCU lock” being acquired by readers; rcu_read_lock() is essentially a barrier that disables preemption. Calling RCU “a lock” leads to wrong mental models and incorrect usage (e.g., trying to nest mutexes inside RCU reads).
  2. “RCU readers always see the latest data.” No — RCU readers may see the old version if they grabbed a pointer before a writer published the new one. RCU is eventually consistent from the reader’s perspective. For strict consistency, use a different primitive.
Further Reading:
  • Paul McKenney, “What is RCU, Fundamentally?” (LWN three-part series) — definitive introduction.
  • Documentation/RCU/ in the Linux kernel source — the official, canonical reference.
  • “Userspace RCU” project documentation at liburcu.org — for using RCU patterns outside the kernel.
Strong Answer Framework:
  1. The traditional syscall path. User space invokes syscall(SYS_clock_gettime, ...) (or glibc wraps it). The CPU executes the syscall instruction, switches from Ring 3 to Ring 0, saves user registers, switches to the kernel stack, dispatches to the kernel’s sys_clock_gettime(), reads the requested clock source, copies the result back to user space, and returns. Total cost: roughly 100-200 nanoseconds depending on hardware.
  2. The vDSO path. The kernel maps a small shared object (linux-vdso.so.1) into every process’s address space. It looks like a normal library; glibc detects it and calls __vdso_clock_gettime() as a regular function. Inside the vDSO, the code reads a kernel-maintained data page that contains the current time, the clock source’s parameters (TSC frequency, offset), and a sequence counter. No ring transition, no syscall instruction. Total cost: roughly 15-25 nanoseconds.
  3. Why this matters. A high-throughput server logging a timestamp per request at 500K req/s spends 50-100 ms/sec on clock_gettime() via syscall, versus ~10 ms/sec via vDSO. That is roughly 1 core’s worth of CPU saved.
  4. What clocks are accelerated. CLOCK_REALTIME, CLOCK_MONOTONIC, CLOCK_REALTIME_COARSE, CLOCK_MONOTONIC_COARSE, and CLOCK_BOOTTIME (kernel 4.18+). Notably absent: CLOCK_PROCESS_CPUTIME_ID and CLOCK_THREAD_CPUTIME_ID, which require per-task accounting only the kernel can do.
  5. How concurrency works. The kernel writer updates the data page using a sequence-counter pattern: increment seqcount (now odd), write data, increment seqcount (now even). Readers in the vDSO read the seqcount before and after their read; if it changed or was odd, they retry. This is a wait-free read protocol with no locks.
Real-World Example: PostgreSQL added track_io_timing = on to log per-query I/O times; this caused a measurable performance regression on systems where the vDSO was disabled (some virtualized environments fall back to syscalls because TSC is not stable across vCPU migrations). The fix in modern Linux is the kvm-clock paravirtual clock source, which is also vDSO-accelerated. Reference: PostgreSQL mailing list discussions from 2017-2018 around pg_stat_statements and timing overhead.
Senior follow-up 1: “What happens if the TSC is unstable across CPUs?” The kernel detects this at boot (tsc: Marking TSC unstable) and falls back to a different clock source like HPET or kvm-clock. If the fallback clock cannot be safely read from user space (because it requires privileged hardware access), the vDSO clock_gettime() falls back to a real syscall.
Senior follow-up 2: “How is the vDSO different from vsyscall?” vsyscall was the older mechanism: a fixed page at a known address (0xffffffffff600000) that user space could call directly. It was deprecated because the fixed address defeated ASLR and the implementation supported only a few specific calls. The vDSO replaces it with a proper position-independent shared object that ASLR can randomize.
Senior follow-up 3: “Can you write your own vDSO functions?” Not portably — the vDSO is a kernel-controlled interface. But you can use the same technique: map a shared memory region into user processes that contains data you maintain from kernel space (or another process), and provide user-space functions that read it. This is essentially what shared-memory message queues like LMAX Disruptor do.
Common Wrong Answers:
  1. “vDSO is just a faster syscall.” Misleading — it is not a syscall at all. There is no ring transition, no syscall instruction, no kernel entry. It is a kernel-published library executed entirely in user space.
  2. “vDSO works for any syscall, just enable it.” No — only specific calls have vDSO implementations, and they all share the property that the kernel can publish their answer in advance. Calls that require kernel arbitration (read, write, open) cannot be vDSO-ified.
Further Reading:
  • man 7 vdso — official Linux documentation, very readable.
  • “Linux’s vsyscall() and vDSO” by Johan Petersson — explains the historical evolution.
  • LWN.net article “Architectural support for vDSO” by Andy Lutomirski — deep technical details.
Strong Answer:How CFS works: CFS models an “ideal” processor where every runnable task gets an equal share of CPU time simultaneously. Since a real CPU can only run one task at a time, CFS tracks how much CPU time each task has received using a metric called vruntime (virtual runtime). The task with the lowest vruntime gets to run next. The scheduler maintains runnable tasks in a red-black tree keyed by vruntime, so picking the next task is O(log n).Nice values (-20 to +19) affect the rate at which vruntime accumulates. A task at nice -20 accumulates vruntime slowly (gets more CPU), while nice +19 accumulates quickly (gets less). The weight ratio between extremes is approximately 88,000:1.CFS targets a scheduling period (typically 6ms for up to 8 tasks), divided proportionally by weight. With 4 equal-weight tasks, each gets approximately 1.5ms. The minimum granularity (typically 0.75ms) prevents excessive context switching even with many tasks.Solving the microservice vs batch problem:
  1. cgroup CPU controller (cpu.weight): Put microservices in a high-weight cgroup (e.g., cpu.weight=1000) and batch jobs in a low-weight cgroup (e.g., cpu.weight=100). Under contention, microservices get 10x more CPU. When microservices are idle, batch jobs use all available CPU — no waste.
  2. CPU bandwidth throttling (cpu.max): Set cpu.max = "200000 100000" on the batch cgroup, limiting it to 2 CPU cores maximum. This guarantees the batch job cannot starve microservices even in burst scenarios.
  3. CPU pinning (cpuset): Assign microservices to specific CPU cores (e.g., cores 0-3) and batch jobs to others (cores 4-7) using the cpuset cgroup controller. This eliminates cache pollution — the batch job’s working set cannot evict the microservice’s hot data from L1/L2 caches. The trade-off: less flexible under varying loads.
  4. SCHED_BATCH policy: Set batch jobs to SCHED_BATCH via chrt -b or sched_setscheduler(). CFS treats these tasks as non-interactive and avoids giving them the low-latency scheduling bonus that interactive tasks receive.
  5. For extreme latency requirements: Consider SCHED_DEADLINE for the microservice’s critical threads. This gives hard CPU bandwidth guarantees (e.g., “5ms of CPU every 10ms”) enforced by the kernel. The batch job literally cannot preempt a SCHED_DEADLINE task. However, misconfiguration can starve the system, so this requires careful capacity planning.
What I would actually deploy: In practice, I would combine approaches 1 and 3. Cgroup CPU weights for proportional sharing, plus cpuset isolation for cache performance. Kubernetes does this automatically: resource requests/limits map to cgroup controllers, and the kubelet’s CPU Manager can pin Guaranteed QoS pods to dedicated cores.Follow-up: What is the “CFS bandwidth throttle” problem in containers?When a container’s cgroup has a CPU quota (e.g., cpu.max = "100000 100000" = 1 CPU), CFS enforces this by throttling the cgroup once it exhausts its quota within the period. The problem: a multi-threaded application can exhaust its quota in a burst early in the period, then all its threads are throttled for the remainder. A 4-thread Go service with a 1-CPU limit can be throttled after just 25ms if all 4 threads run simultaneously for 25ms (4 * 25ms = 100ms quota consumed). This causes periodic latency spikes at the period boundary. The mitigation is to set quota proportionally to thread count, or use cpu.max.burst (added in kernel 5.14) to allow temporary bursts above quota.

Key Takeaways

Monolithic but Modular

Linux kernel is monolithic (single address space, direct function calls between subsystems) but supports loadable modules for runtime extensibility. Trade-off: speed over isolation.

System Call Interface

The SYSCALL instruction transitions from Ring 3 to Ring 0. Roughly 450 system calls in modern Linux, each with ~100-200ns overhead. vDSO eliminates this for hot-path calls like gettimeofday().

Memory Management

4-level page tables, buddy + SLUB allocators, copy-on-write fork(), demand paging. Virtual memory is the kernel’s most powerful abstraction — every process gets 128TB of private address space.

Boot Sequence

BIOS/UEFI -> GRUB -> Kernel decompression -> initramfs (load drivers, mount root) -> systemd (PID 1). Each stage initializes enough to load the next.

Debugging Toolkit

/proc and /sys for inspection, ftrace for function tracing, perf for profiling, bpftrace/eBPF for production-safe programmable tracing. PSI metrics for resource pressure.

Containers are Kernel Features

Containers = namespaces (isolation) + cgroups (resource limits) + seccomp (syscall filtering). Not VMs. Near-zero overhead. The kernel enforces all boundaries.

Next: Interview Preparation