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.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.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.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).System Call Flow
Here is how a typical system call works, step by step:- The application calls a library function (like
read()in C). - The glibc wrapper sets up the syscall number in
raxand arguments in registers (rdi,rsi,rdx, etc.), then executes thesyscallCPU instruction on x86_64. - 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). - 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. - The kernel places the return value in
rax, restores user registers, and executessysretto switch back to Ring 3 (user mode).
System Call Implementation
Key System Calls
Modern Linux has approximately 450 syscalls (the exact number depends on architecture). Some of the most important ones, grouped by subsystem: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 atask_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.
Task Struct
Thetask_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.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.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.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) ormmap()(maps a new anonymous region). These are the actual syscalls. glibc then sub-divides these chunks to satisfy individualmalloc()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.
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.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.
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.Kernel Debugging
Debugging kernel code is fundamentally different from user-space debugging. You cannot attachgdb 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
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 toread() 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.
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.
Interview Deep Dive Questions
Q1: Walk through what happens when you type 'ls' in a terminal
Q1: Walk through what happens when you type 'ls' in a terminal
Q2: Explain copy-on-write (COW) in fork()
Q2: Explain copy-on-write (COW) in fork()
- 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
Q3: How does the kernel handle a page fault?
Q3: How does the kernel handle a page fault?
- Minor fault: Page in memory but not mapped
- Major fault: Page not in memory (disk I/O needed)
- Invalid fault: Access violation (segfault)
Q4: Explain the difference between softirq, tasklet, and workqueue
Q4: Explain the difference between softirq, tasklet, and workqueue
Q5: How does the kernel implement futexes?
Q5: How does the kernel implement futexes?
- Uncontended: Just an atomic operation, no syscall
- Hash table lookup in kernel is O(1)
- Foundation for pthread_mutex, semaphores, condition variables
Q6: A production server shows 70%+ system CPU time. Walk through your investigation.
Q6: A production server shows 70%+ system CPU time. Walk through your investigation.
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 RAMtcp_sendmsg/tcp_recvmsg— network-bound workload
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.Q7: Explain how namespaces and cgroups work together to create containers. What are the security implications?
Q7: Explain how namespaces and cgroups work together to create containers. What are the security implications?
- 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)
- 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)
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.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:
- User-space call:
- Application thread calls
read(fd, buf, n)on a TCP socket. - The C library issues the
readsystem call (e.g.,syscall(SYS_read, ...)).
- Application thread calls
- Syscall entry:
- CPU executes
syscall/svcinstruction. - Control transfers to the kernel’s syscall entry (
entry_SYSCALL_64on x86-64). - The kernel locates the
struct fileforfdand dispatches to the socket’s file operations.
- CPU executes
- VFS and socket layer:
- The VFS
readimplementation calls into the socket layer (sock_read_iter). - This eventually calls the protocol-specific
recvmsgimplementation (e.g.,tcp_recvmsg).
- The VFS
- TCP stack and receive queue:
- If there is already data in the socket’s receive queue (filled by earlier packets),
tcp_recvmsgcopies it intobufand returns. - If not, it may sleep the process, putting it on a wait queue until more data arrives.
- If there is already data in the socket’s receive queue (filled by earlier packets),
- 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_buffstructures.
- 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.
- Copy to user and return:
tcp_recvmsgcopies data from kernel buffers into the user-spacebufusing safe copy helpers.- The syscall returns to user space with the number of bytes read.
- 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).
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.Interview Deep-Dive
Your team is debugging a production outage where a critical service is consuming 95% of system memory. The OOM killer has not fired yet, but the system is nearly unresponsive. Walk me through what is happening inside the kernel and how you would investigate.
Your team is debugging a production outage where a critical service is consuming 95% of system memory. The OOM killer has not fired yet, but the system is nearly unresponsive. Walk me through what is happening inside the kernel and how you would investigate.
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:cat /proc/meminfo— checkMemAvailable,Buffers,Cached,SwapFree, and criticallySlab(kernel object caches can be huge). IfSlabis 30GB on a 64GB machine, a kernel memory leak is likely (often caused by a dentry/inode cache explosion from afindtraversing millions of files).cat /proc/pressure/memory— PSI metrics tell me what percentage of time tasks are stalled on memory. Iffull avg10is above 50%, the system is effectively thrashing.slabtop— shows which kernel slab caches are consuming the most memory. Look fordentry,inode_cache,ext4_inode_cachegrowing unbounded.- Per-process:
smem -torps aux --sort=-%memto identify the top consumers. Check/proc/<pid>/smaps_rollupfor PSS (Proportional Set Size) — this accounts for shared libraries correctly. cat /proc/vmstat | grep -E 'pgfault|pgmajfault|pgscan|pgsteal'— ifpgscand(direct reclaim scans) is climbing, processes are blocking on memory allocation. Ifpgmajfaultis high, the system is page-faulting from disk (thrashing).
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).You are writing a high-performance network server. An engineer proposes using the vDSO to eliminate system call overhead. Explain what the vDSO is, which calls it accelerates, and what its limitations are.
You are writing a high-performance network server. An engineer proposes using the vDSO to eliminate system call overhead. Explain what the vDSO is, which calls it accelerates, and what its limitations are.
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.
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:- Only read-only, non-privileged information can be served this way. You cannot vDSO-ify
read()orwrite()because those require kernel arbitration of hardware. - 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.
clock_gettime(CLOCK_PROCESS_CPUTIME_ID)does NOT go through vDSO — it requires reading per-process counters that live in kernel space.- 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.
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.A kernel module developer reports that their driver works perfectly in testing but causes sporadic crashes in production under high load. The crash log shows 'BUG: sleeping function called from invalid context.' Explain what this means at the kernel level and how to fix it.
A kernel module developer reports that their driver works perfectly in testing but causes sporadic crashes in production under high load. The crash log shows 'BUG: sleeping function called from invalid context.' Explain what this means at the kernel level and how to fix it.
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: useGFP_ATOMIC(does not sleep, may fail if memory is tight).mutex_lock()inside a spinlock-protected section. Fix: usespin_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()orschedule_timeout()in a tasklet. Fix: use a workqueue instead, which runs in process context and CAN sleep.
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()orin_interrupt().
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.Walk me through how Linux boots from GRUB to userspace init. What does each stage actually do, and where can it fail?
Walk me through how Linux boots from GRUB to userspace init. What does each stage actually do, and where can it fail?
- 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.
- 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) andinitramfsinto memory. GRUB also passes the kernel command line (root=/dev/sda2 ro quiet splash ...). - Kernel decompression and early boot.
vmlinuzis a self-extracting compressed image. The first thing it does is decompress itself, then jump tostart_kernel()ininit/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). - 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
/initfrom initramfs — typically a script or systemd-in-initramfs. - Pivot to real root. Once the real root is mounted, the kernel calls
switch_root(orpivot_root), which atomically replaces/with the real root and re-execs/sbin/init(which is usually a symlink to/lib/systemd/systemd). - 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.
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.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.- “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.
- “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.
- Linux Documentation Project, “From Power Up to Bash Prompt” — the canonical free reference.
man 7 bootandman 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.
Explain RCU (Read-Copy-Update). When is it the right primitive, and when is it the wrong one?
Explain RCU (Read-Copy-Update). When is it the right primitive, and when is it the wrong one?
- 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.
- 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.
- 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).
- 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.
- The classic alternative. A
rwlockgives 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,rwlockis often a safer first choice; reach for RCU when profiling shows reader-side contention.
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.- “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). - “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.
- 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.
What is the difference between a syscall and a vDSO call for clock_gettime? Walk me through both paths.
What is the difference between a syscall and a vDSO call for clock_gettime? Walk me through both paths.
- The traditional syscall path. User space invokes
syscall(SYS_clock_gettime, ...)(or glibc wraps it). The CPU executes thesyscallinstruction, switches from Ring 3 to Ring 0, saves user registers, switches to the kernel stack, dispatches to the kernel’ssys_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. - 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. - 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. - What clocks are accelerated.
CLOCK_REALTIME,CLOCK_MONOTONIC,CLOCK_REALTIME_COARSE,CLOCK_MONOTONIC_COARSE, andCLOCK_BOOTTIME(kernel 4.18+). Notably absent:CLOCK_PROCESS_CPUTIME_IDandCLOCK_THREAD_CPUTIME_ID, which require per-task accounting only the kernel can do. - 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.
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.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.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.- “vDSO is just a faster syscall.” Misleading — it is not a syscall at all. There is no ring transition, no
syscallinstruction, no kernel entry. It is a kernel-published library executed entirely in user space. - “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.
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.
Explain how the Linux scheduler's Completely Fair Scheduler (CFS) works. Your team is running latency-sensitive microservices alongside batch ML training jobs on the same host. How would you use kernel scheduling features to prevent the batch jobs from degrading microservice latency?
Explain how the Linux scheduler's Completely Fair Scheduler (CFS) works. Your team is running latency-sensitive microservices alongside batch ML training jobs on the same host. How would you use kernel scheduling features to prevent the batch jobs from degrading microservice latency?
- 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.
-
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. - 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.
-
SCHED_BATCH policy: Set batch jobs to
SCHED_BATCHviachrt -borsched_setscheduler(). CFS treats these tasks as non-interactive and avoids giving them the low-latency scheduling bonus that interactive tasks receive. -
For extreme latency requirements: Consider
SCHED_DEADLINEfor 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.
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
System Call Interface
Memory Management
Boot Sequence
Debugging Toolkit
Containers are Kernel Features
Next: Interview Preparation →