Kernel, user space and system calls
What the kernel is, why applications live in a separate world, how a system call crosses between them, and how the machine gets from power-on to a running systemd.
On this page
Every program on Linux is a guest. It cannot touch the disk, the network card, or another program's memory. It has to ask. This page is about who it asks, how the asking works, and how the thing it asks came to be running in the first place.
The map
Read this first when short on time. Every branch is a section below.
The kernel: one program that owns the hardware
The kernel is a single program that is loaded first and stays in memory the whole time the machine is on. It owns the CPU, the memory and every device. Everything else runs on top of it and has to ask it for things.
"Linux" strictly means only this program. Ubuntu, Fedora, Alpine and Android are the same kernel with different user space around it: a C library, a shell, an init system, package tools. That is why a Docker image built on Debian runs on a Fedora host. The image brings its own user space; the kernel underneath is the host's.
Inside, the kernel is a set of subsystems that share one address space:
- The scheduler decides which task runs on which CPU, and for how long.
- Memory management hands out pages and keeps every process's view of memory separate.
- The VFS (virtual filesystem) turns
openandreadinto calls on ext4, XFS, tmpfs or NFS. - The network stack owns sockets, TCP, IP and routing.
- Device drivers talk to the actual hardware.
flowchart TD
A["Applications: shell, nginx, Chrome, your code"] --> L["C library: glibc, musl, Bionic"]
L --> S["System call interface"]
S --> K
subgraph K["Kernel"]
direction LR
K1["Scheduler"] ~~~ K2["Memory"] ~~~ K3["VFS"] ~~~ K4["Network"]
end
K --> D["Device drivers"]
D --> H["Hardware: CPU, RAM, disk, NIC"]
Linux is a monolithic kernel: drivers and filesystems run inside the kernel itself, not as separate programs. But it loads code on demand. A driver can be a module, a .ko file that modprobe pulls in when the hardware shows up. lsmod lists what is loaded right now. Once loaded, a module is kernel code with full power; a bug in it can take the whole machine down.
- Android is the Linux kernel with a different user space: Bionic instead of glibc,
initinstead of systemd, and the app framework on top. The app launch article starts from that kernel. - Cloud VMs on AWS or GCP each boot their own kernel. A container does not; it shares the host's.
- Docker images ship no kernel at all, which is why an image is small and why it cannot run a different kernel version than the host.
- Kernel
- The one program with full control of the hardware. Loaded first, never exits.
- Distribution
- A kernel plus a chosen user space: libc, shell, init, packages.
- Module
- A piece of kernel code loaded at runtime, usually a driver or a filesystem.
Kernel space and user space: two modes on one CPU
The CPU has a privilege level, and the kernel runs at the highest one. User programs run at the lowest. That single hardware fact is what keeps a buggy program from wrecking the machine.
On x86 the levels are called rings. The kernel runs in ring 0 and can do anything: touch any memory, talk to any device, change the page tables. Applications run in ring 3. If a ring 3 program tries a privileged instruction, such as turning off interrupts or writing to a device register, the CPU refuses and jumps into the kernel, which usually kills the program. ARM has the same idea with different names (EL1 and EL0).
Memory is split the same way. Every process gets its own address space, and the kernel's memory is mapped into the top of each one but marked as not accessible from user mode. So a process cannot read another process's memory, and it cannot read the kernel's either. The page tables enforce this, and only the kernel can change the page tables. The memory article goes through how those tables work.
| Kernel space | User space | |
|---|---|---|
| CPU mode | Ring 0 (privileged) | Ring 3 |
| Can touch | All memory, all devices, page tables | Its own mapped memory only |
| A bug means | Kernel panic, whole machine | One process dies with a signal |
| Gets in through | Boot | exec of a program |
| Examples | Scheduler, drivers, TCP, ext4 | bash, nginx, Postgres, your app |
There are exactly three ways control passes from user mode to kernel mode:
- A system call. The program asks for something on purpose. This is the next section.
- An interrupt. A device or the timer needs attention. The program is paused, the kernel handles it, the program resumes without knowing.
- An exception. The program did something the CPU could not complete: touched an unmapped page, divided by zero, ran a privileged instruction. The kernel decides whether to fix it (a page fault) or kill the process (a segmentation fault).
Each crossing is a mode switch: the CPU saves where the program was, jumps to a kernel entry point, and later restores the program. It is cheap but not free, which is why the number of system calls a program makes matters for performance.
- Chrome runs each site in its own process. A crashed renderer takes down one tab, not the browser, because the kernel keeps processes apart.
- Docker containers all run on one kernel in ring 0. The isolation between them is user-space bookkeeping in the kernel (namespaces and cgroups), not a second CPU mode. Firecracker and gVisor exist for people who want more than that.
- Meltdown (2018) was a CPU bug that let ring 3 code read kernel memory through the cache. The fix, page table isolation, unmaps most of the kernel while user code runs, and made every system call slower.
- Ring 0 / ring 3
- The x86 names for privileged and unprivileged CPU mode. The kernel is ring 0.
- Mode switch
- The CPU moving from user mode to kernel mode or back. Happens on every system call, interrupt and exception.
- Context switch
- Different thing: the kernel switching the CPU from one process to another. Includes swapping the page tables.
System calls: the only door into the kernel
A system call is how a user program asks the kernel to do something it cannot do itself: open a file, send a packet, start a process, allocate memory. There are over 400 of them on x86-64, and every one has a number.
You almost never make one directly. You call a function in the C library, and the library makes the call for you. Here is what happens when a program writes five bytes to standard output:
sequenceDiagram autonumber participant P as Program participant L as libc participant K as Kernel participant F as Filesystem P->>L: write(1, "hello", 5) L->>L: number 1 in rax, args in rdi rsi rdx L->>K: syscall instruction (mode switch) K->>K: look up entry 1 in the syscall table K->>F: sys_write: find fd 1, copy bytes in F-->>K: 5 bytes accepted K-->>L: rax = 5, return to user mode L-->>P: 5
- The program calls
writein libc. - libc puts the system call number in a register (
raxon x86-64) and the arguments in others (rdi,rsi,rdx,r10,r8,r9). - libc runs the
syscallinstruction. The CPU switches to ring 0 and jumps to the kernel's entry point. On ARM64 the instruction issvcand the number goes inx8. - The kernel saves the program's registers, reads the number, and looks up the handler in the syscall table. Entry 1 is
sys_write. - The handler checks the arguments. Every pointer from user space is treated as untrusted: the kernel copies data across with
copy_from_user, which fails cleanly instead of crashing if the address is bad. - The work happens: find file descriptor 1 in the process's table, hand the bytes to whatever is behind it (a terminal, a pipe, a file in the page cache).
- The kernel puts the result in
raxand returns to user mode. A success is a value of zero or more. A failure is a small negative number, the negated error code. - libc sees the negative value, stores the positive code in
errno, and returns-1to the program.
The C looks like this. Everything in the sequence above is hidden inside the one call to write.
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
int main(void) {
ssize_t n = write(1, "hello", 5); // fd 1 is standard output
if (n < 0) {
perror("write"); // prints the message for errno
return 1;
}
return 0;
}
strace shows the crossings and nothing else. Run it on the program above and the interesting line is the one in the middle:
$ strace ./hello
execve("./hello", ["./hello"], 0x7ffd...) = 0
brk(NULL) = 0x5555...
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...
...
write(1, "hello", 5) = 5
exit_group(0) = ?
The lines before write are the dynamic loader setting up libc. Even the smallest program makes a few dozen system calls before reaching main.
The ones worth knowing by name
| Area | System calls | What they do |
|---|---|---|
| Files | openat, read, write, close, stat, lseek | Everything is a file descriptor: files, pipes, sockets, devices. |
| Processes | fork, clone, execve, wait4, exit_group, kill | Make, replace, wait for and signal processes. Threads are clone with sharing flags. |
| Memory | mmap, munmap, brk, mprotect | Map pages into the address space. malloc is built on these. |
| Network | socket, bind, listen, accept, connect, sendto, recvfrom | Covered in the sockets article. |
| Waiting | poll, epoll_wait, futex, nanosleep | Sleep until something happens. futex is what mutexes use when they must block. |
| Devices | ioctl | The catch-all for "do this device-specific thing". |
What a system call costs
A bare system call is a few hundred nanoseconds: two mode switches, the register save and restore, and the table lookup. After the Meltdown mitigations it is more, because the kernel's page tables are swapped in and out too. That is small next to a disk read, but a program doing millions of tiny reads pays for every one. This is why buffered I/O exists, and why high-performance servers batch work per call.
A few calls are so common that the kernel avoids the trap entirely. clock_gettime, gettimeofday, time and getcpu are served by the vDSO, a small page of kernel-provided code mapped into every process. libc calls it like an ordinary function, it reads a shared memory page the kernel keeps up to date, and no mode switch happens. strace does not show these at all.
- strace attaches to a process with
ptraceand stops it at every system call. It is the first tool to reach for when a program hangs or fails with an odd error; the tracing article shows how to use it. - The Go runtime makes system calls directly instead of through libc, which is why a Go binary can be fully static with no libc at all.
- io_uring is a newer interface where a program puts many requests in a shared ring buffer and the kernel picks them up, so a busy server can do thousands of I/O operations per system call.
A system call that returns -1 tells you almost nothing on its own. The reason is in errno, and errno is overwritten by the next call that fails. Read it immediately, or use perror and strerror. In strace output the error name is printed for you, as in = -1 ENOENT (No such file or directory).
- System call
- A request from user space to the kernel, identified by a number and made with a special CPU instruction.
- Syscall table
- The kernel's array mapping call numbers to handler functions.
- errno
- A per-thread variable in libc holding the error code of the last failed call.
ENOENT,EACCES,EAGAINare values of it. - vDSO
- Virtual dynamic shared object. Kernel code mapped into each process so a few calls, mostly about time, need no mode switch.
The C library: the wrapper you actually call
The C library sits between every program and the kernel. It wraps each system call in a normal function, and it adds a lot that is not a system call at all.
On most distributions it is glibc. Alpine uses musl, which is smaller and simpler, and Android uses Bionic. Every other language runtime sits on one of these, or, like Go, reimplements the thin layer itself. A program compiled against glibc will not run on an Alpine image, which is the usual reason a container "works on my machine" and not in production.
The library does three jobs:
- Wrapping.
open,read,write,forkand friends are one-line wrappers that set registers, runsyscall, and turn a negative return intoerrno. - Buffering.
printfandfwritecollect bytes in a buffer and callwriteonly when it fills up, or at a newline if stdout is a terminal, or at exit. That turns a thousand tiny writes into one system call. - Everything else.
malloc,strlen,qsort,getaddrinfo, locale handling. None of these are system calls on their own.mallocasks the kernel for big chunks withbrkormmapand carves them up in user space.
The buffering catches people. Print a line with printf and then crash, and the line never appears, because it was still in the buffer. Pipe a program's output into another command and the lines come out late and in blocks, because stdout is no longer a terminal so it is fully buffered. fflush(stdout), setvbuf, or writing to stderr (which is unbuffered) are the fixes. strace makes it visible: you see one write of 4096 bytes instead of a hundred small ones.
- Alpine images are small because musl is small, and they occasionally break software that assumes glibc behaviour in DNS lookups or locale handling.
- Python's
printfollows the same rule: line-buffered on a terminal, block-buffered into a pipe.python -uandPYTHONUNBUFFERED=1exist for container logs for this reason. - Android's Bionic is a from-scratch libc built to be small and to start fast, because every app process pays for its startup.
- glibc
- The GNU C library, the default on Debian, Ubuntu, Fedora and RHEL.
- musl
- A small, strict C library used by Alpine Linux and for static binaries.
- Buffered I/O
- Collecting output in memory and writing it in large chunks.
stdiodoes it, rawwritedoes not.
Interrupts: how the kernel takes the CPU back
A system call is the program handing control to the kernel. An interrupt is the kernel taking it. A device raises a signal on a wire, the CPU stops what it was doing mid-instruction, and jumps into a kernel handler.
flowchart TD A["Device raises an IRQ: packet arrived, disk finished, timer tick"] --> B["CPU pauses the running task, enters the kernel"] B --> C["Top half: acknowledge the device, grab the data, return in microseconds"] C --> D["Bottom half: softirq or workqueue does the slow part later"] D --> E["Wake the process that was waiting on this"] E --> F["Scheduler picks who runs next, returns to user mode"]
Handlers are kept short because while one runs, that CPU cannot take another interrupt. So the handler does the minimum, the top half, and queues the rest as a softirq or a work item, the bottom half. For a network card, the top half notes that packets are in the ring buffer and the bottom half walks them up through IP and TCP.
The most important interrupt is the timer. It fires every few milliseconds on each CPU, and the handler gives the scheduler a chance to decide the current task has run long enough. Without it, a program in an infinite loop would own the CPU forever. This is what makes the kernel preemptive: a task is paused whether it likes it or not. Modern kernels also switch the tick off on an idle CPU to save power.
- A busy NIC can raise hundreds of thousands of interrupts per second. Drivers switch to polling under load (NAPI), and
/proc/interruptsshows the counts per CPU. - ksoftirqd is the kernel thread you see in
topwhen softirq work is piling up faster than the bottom half can drain it, often on a server under a packet flood. - The load average reported by
uptimeis recomputed every five seconds by code that the timer tick drives.
- IRQ
- Interrupt request. A numbered line a device uses to get the CPU's attention.
- Softirq
- Deferred kernel work that runs soon after the interrupt handler, with interrupts enabled again.
- Preemption
- The kernel pausing a running task on its own decision, not the task's.
Boot: from firmware to the first process
Booting is a chain of programs, each one just capable enough to load the next. It ends when the kernel starts the first user space process.
flowchart TD A["Firmware (UEFI): test hardware, find a boot entry on the disk"] --> B["Bootloader (GRUB, systemd-boot): pick a kernel, load it and the initramfs into RAM"] B --> C["Kernel: decompress, set up page tables, start CPUs, probe devices, load drivers"] C --> D["initramfs: a tiny root filesystem in RAM. Its init loads the modules needed to reach the real disk"] D --> E["Mount the real root, switch to it"] E --> F["Run /sbin/init as PID 1: systemd"] F --> G["systemd starts services, mounts, the login screen"]
- Firmware. UEFI (or the old BIOS) initialises the hardware, reads its boot entries from NVRAM, and runs the bootloader from the EFI system partition. Cloud VMs and the Raspberry Pi have their own firmware that does the same job.
- Bootloader. GRUB or systemd-boot shows a menu, loads the compressed kernel image (
vmlinuz) and the initramfs into memory, passes the kernel command line (root=,quiet,ro), and jumps to the kernel. - Kernel. It decompresses itself, sets up memory and page tables, brings up the other CPUs, and probes for devices. This is the wall of text on a console boot.
- initramfs. A small root filesystem packed as a cpio archive. The kernel unpacks it into RAM and runs its
/init. That script loads the driver modules needed to reach the real root disk, unlocks encrypted volumes, assembles RAID. Without it the kernel would need every possible disk driver built in. - Real root. The initramfs mounts the real root filesystem and moves into it with
switch_root, freeing the RAM copy. - PID 1. The kernel execs
/sbin/init, which is a symlink to systemd on most distributions. This is the first and only process the kernel ever starts by itself. Every other process is a descendant of it. If PID 1 exits, the kernel panics.
Two tasks exist alongside PID 1 from the start: kthreadd (PID 2), the parent of every kernel thread, and the idle task (PID 0). They are why ps shows names in square brackets like [kworker/0:1]. Those are kernel-space tasks with no user space program.
- An AWS EC2 instance boots this exact chain inside a VM;
dmesgright after boot shows the kernel's part of it. - Android replaces the last step with its own
initthat reads.rcfiles and starts Zygote, described in the app launch article. - Docker containers skip all of this. The host kernel is already up; a container start is just a process being created with a different view of the system.
- UEFI
- The firmware standard that replaced BIOS. Finds and runs the bootloader.
- initramfs
- A small filesystem in RAM used during boot to load the drivers needed to reach the real root disk.
- PID 1
- The first user space process. The ancestor of all others. systemd on most systems.
systemd: PID 1 and everything it supervises
systemd is the init system on nearly every current distribution. As PID 1 it starts every service, keeps them running, and is the parent every orphaned process ends up under. It also runs the logging daemon, mounts filesystems, and manages timers, devices and login sessions.
Units
Everything systemd manages is a unit, described by a small text file. The type is the file's suffix.
| Unit type | What it manages | Example |
|---|---|---|
.service | A daemon or one-shot program | nginx.service, docker.service |
.socket | A listening socket; the service starts on the first connection | docker.socket, sshd.socket |
.target | A named group of units, a milestone in boot | multi-user.target |
.timer | Runs a service on a schedule, like cron | logrotate.timer |
.mount | A filesystem mount, generated from /etc/fstab | home.mount |
.slice | A cgroup subtree for resource accounting | system.slice, user.slice |
.device | A device that udev found | dev-sda1.device |
A service file says what to run and how to treat it:
[Unit]
Description=My API server
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=api
ExecStart=/opt/api/bin/server --port 8080
Restart=on-failure
RestartSec=2
MemoryMax=512M
Environment=RUST_LOG=info
[Install]
WantedBy=multi-user.target
Two kinds of relationship matter. After and Before are about order: start me after that. Wants and Requires are about pulling in: if I start, start that too (and with Requires, fail if it fails). They are independent. A unit that Wants another without After starts both at the same time. The [Install] section only says which target should pull this unit in when it is enabled.
Targets and the boot order
A target is a checkpoint. Boot is systemd working its way to default.target, which is a symlink to multi-user.target on a server or graphical.target on a desktop. The targets chain, and services hang off them.
flowchart TD S["sysinit.target: mounts, udev, journald, swap"] --> B["basic.target: sockets, timers, paths"] B --> M["multi-user.target: sshd, nginx, docker, cron"] M --> G["graphical.target: display manager, login screen"] D["default.target"] -. symlink .-> M
systemctl get-default shows which is the goal.Services start in parallel wherever the ordering allows it, which is why systemd boots faster than the old shell-script init. systemd-analyze blame lists what took longest, and systemd-analyze critical-chain shows the path that set the boot time.
Supervision
systemd puts each service in its own cgroup, so it knows exactly which processes belong to it even if the service forks a hundred times. That is how systemctl stop reliably kills everything, and how MemoryMax= and CPUQuota= in a unit file turn into real limits. The containers article covers cgroups in full.
It restarts services that die (Restart=on-failure), and it can hold a socket open on a service's behalf and only start the service when the first client connects (socket activation). Logs go to journald, which captures stdout and stderr of every service into a binary, indexed journal.
systemctl status nginx # running? PID, cgroup, recent log lines
systemctl start|stop|restart nginx
systemctl enable --now nginx # start at boot and start now
systemctl list-units --type=service --state=failed
systemctl cat nginx # the unit file plus drop-ins
systemctl edit nginx # add an override in /etc/systemd/system/nginx.service.d/
systemctl daemon-reload # after editing unit files by hand
journalctl -u nginx -f # follow one service's log
journalctl -b -p err # errors since this boot
systemd-analyze blame # what slowed the boot
Unit files that ship with a package live in /usr/lib/systemd/system/. Your changes go in /etc/systemd/system/, either as a whole file that shadows the packaged one or as a drop-in directory that overrides a few lines. That is the same split as everything else under /usr and /etc, explained in the filesystems article.
- Ubuntu, Debian, Fedora, RHEL, Arch all boot with systemd. Alpine uses OpenRC and Void uses runit, which is one reason those are popular for small containers.
- Docker itself is a systemd service on the host, and it uses socket activation:
docker.socketlistens on/run/docker.sockand startsdockerdon the first command. - Inside a container there is usually no systemd. The application is PID 1, which has consequences for signals that the processes article covers.
Type=simple means systemd considers the service started the moment the process is forked, not when it is ready. A unit that depends on it with After= may start before the daemon is listening. Use Type=notify with sd_notify, or a .socket unit, when readiness matters.
- Unit
- One thing systemd manages: a service, socket, mount, timer, target.
- Target
- A unit that groups others. Used as boot milestones like
multi-user.target. - journald
- systemd's log collector.
journalctlreads it. - Socket activation
- systemd holds the listening socket and starts the service on the first connection.
Recap
- The kernel is one program that owns the hardware. A distribution is that kernel plus a user space around it.
- The CPU runs the kernel in a privileged mode and applications in an unprivileged one. Page tables, which only the kernel can change, keep every process's memory separate.
- Control passes to the kernel in three ways: a system call, an interrupt, or an exception.
- A system call is a numbered request. libc puts the number and arguments in registers, runs the
syscallinstruction, and the kernel looks up the handler in a table. - A negative return from the kernel becomes
-1pluserrnoin libc. Readerrnoimmediately. - A system call costs a few hundred nanoseconds. Buffering and batching exist to make fewer of them. The vDSO makes time lookups free.
- libc wraps the calls and adds buffering and everything else;
mallocandprintfare library code, not system calls. - Interrupts let devices and the timer take the CPU back. Handlers are split into a fast top half and a deferred bottom half.
- Boot is firmware, then bootloader, then kernel, then initramfs, then the real root, then PID 1.
- PID 1 is systemd. Every process descends from it, and it panics the kernel if it exits.
- systemd manages units.
Afterorders them,Wantspulls them in, targets are milestones, and each service lives in its own cgroup. - Your unit changes go in
/etc/systemd/system; the packaged ones in/usr/lib/systemd/systemstay untouched.
Questions
Try answering each one out loud before opening it. Lead with the one-line answer, then a couple of points, then one extra detail.
What is the difference between the kernel and the operating system?
The kernel is the one privileged program that owns the hardware; the operating system, or distribution, is that kernel plus the user space around it: libc, shell, init, package manager.
- Ubuntu, Alpine and Android share the Linux kernel and differ almost entirely in user space.
- A container image ships user space only, which is why it runs on any host with a compatible kernel.
This is also why a Debian image cannot use a kernel feature the Fedora host's kernel lacks; the kernel is always the host's.
What separates kernel space from user space, physically?
A CPU privilege level: the kernel runs in ring 0 and can execute any instruction and touch any memory, applications run in ring 3 and are limited to their own mapped pages.
- The page tables enforce the memory split, and only ring 0 code can change page tables.
- A privileged instruction from ring 3 traps into the kernel, which typically kills the process.
Kernel memory is mapped into the top of every process's address space but marked inaccessible from user mode, which is what lets a system call be handled without changing address space.
Name the three ways execution moves from user mode into the kernel.
A system call, an interrupt, or an exception.
- System call: the program asks on purpose, with the
syscallinstruction. - Interrupt: a device or the timer demands attention; the program is paused and resumed without knowing.
- Exception: the program did something the CPU could not finish, such as touching an unmapped page.
A page fault is an exception the kernel usually fixes silently; a segmentation fault is one it cannot, so it sends the process SIGSEGV.
Walk through what happens when a program calls write.
libc puts the syscall number in rax and the arguments in rdi, rsi, rdx, runs the syscall instruction, the CPU switches to ring 0, the kernel looks up sys_write in the syscall table, copies the bytes from user memory, hands them to whatever is behind the file descriptor, puts the result in rax and returns to user mode.
- Every user pointer is copied with
copy_from_user, which fails cleanly on a bad address instead of crashing the kernel. - A negative return is an error code; libc turns it into
-1and setserrno.
The whole round trip is a few hundred nanoseconds, so a program making millions of one-byte writes is paying mostly for mode switches.
How does the kernel report an error from a system call, and how does that become errno?
The kernel returns a small negative number, the negated error code, in the return register; the libc wrapper spots it, stores the positive code in the thread-local errno, and returns -1.
errnois only valid right after a failed call; the next failure overwrites it.stracedecodes the code into a name for you, such asENOENTorEACCES.
Go and Rust do the same decoding in their own runtimes because they call the kernel without libc.
What is the vDSO and why does it exist?
A small page of kernel-provided code mapped into every process so that a few very frequent calls, mostly clock_gettime and gettimeofday, can be answered in user space without a mode switch.
- The kernel keeps a shared page of timing data up to date, and the vDSO function reads it.
- libc calls it like an ordinary function;
stracenever sees it.
Programs that timestamp every request, such as tracing libraries, would otherwise spend a noticeable share of their time in the kernel.
Is malloc a system call? Is printf?
Neither. Both are C library code; malloc asks the kernel for large regions with brk or mmap and carves them up itself, and printf formats into a buffer and eventually calls write.
- Most
malloccalls never enter the kernel at all. - On a terminal
printfflushes at every newline; into a pipe or file it flushes only when the buffer fills.
That buffering is why output can vanish when a program crashes and why PYTHONUNBUFFERED=1 exists for container logs.
Why does the same binary run on Ubuntu but fail on an Alpine image?
The binary is dynamically linked against glibc, and Alpine ships musl, a different C library with a different loader path and symbol set.
- The kernel interface is identical; what differs is the user space library the program expects.
- Fixes are a static build, a glibc compatibility package, or building the binary on Alpine.
Go binaries avoid the problem because the Go runtime makes system calls directly and needs no libc at all.
What is the difference between a mode switch and a context switch?
A mode switch is the CPU moving between user and kernel mode inside the same process; a context switch is the kernel replacing one process with another on the CPU.
- Every system call and interrupt is a mode switch. Only some of them end in a context switch.
- A context switch also swaps page tables and flushes cached translations, so it costs more.
vmstat reports context switches per second in its cs column; a very high number often means threads bouncing on locks.
Why are interrupt handlers split into a top half and a bottom half?
Because while a handler runs, that CPU cannot take another interrupt, so the handler does only the urgent minimum and defers the rest to a softirq or workqueue that runs with interrupts enabled.
- For a network card the top half notes that packets arrived; the bottom half pushes them through IP and TCP.
ksoftirqdis the per-CPU thread that runs deferred work when there is too much of it.
Under packet floods drivers switch to polling so the CPU is not interrupted for every packet.
What makes the kernel preemptive, and why does that matter?
The timer interrupt fires every few milliseconds on each CPU and gives the scheduler a chance to pause the current task, so a program cannot hold the CPU by refusing to yield.
- Without it, an infinite loop in one process would freeze everything else on that CPU.
- The scheduler also runs when a task blocks on I/O or a lock, which is the cooperative path.
Idle CPUs turn the tick off entirely to save power, which is why a laptop's interrupt count drops when nothing is running.
Describe the boot sequence from power-on to a login prompt.
Firmware runs the bootloader, the bootloader loads the kernel and initramfs, the kernel initialises hardware and runs the initramfs init, which loads the drivers to reach the real root and switches to it, and the kernel then execs PID 1, systemd, which starts everything else.
- The initramfs exists so the kernel does not need every disk, RAID and encryption driver built in.
- systemd works towards
default.target, starting units in parallel where the ordering allows.
If PID 1 ever exits the kernel panics, which is why init systems are written to never crash.
What is the initramfs for?
It is a small root filesystem in RAM whose only job is to load the modules and do the setup needed to mount the real root, such as storage drivers, RAID assembly or disk decryption.
- It is a cpio archive the bootloader loads next to the kernel.
- Once the real root is mounted,
switch_rootreplaces it and frees the memory.
A machine that boots to an "unable to mount root" panic usually has an initramfs that is missing the driver for its disk controller.
In a systemd unit, what is the difference between After= and Requires=?
After= only sets the order, so this unit starts once the other has started; Requires= pulls the other unit in and fails this unit if that one fails, but says nothing about order.
- You usually want both:
Requires=orWants=plusAfter=. Wants=is the soft form: pull it in, but carry on if it fails.
With Type=simple, "started" means forked, not ready; use Type=notify when the dependent unit actually needs the daemon listening.
How does systemd know which processes belong to a service?
It starts every service in its own cgroup, and the kernel keeps every descendant in that cgroup no matter how often the service forks.
- That is why
systemctl stopkills the whole tree and whyMemoryMax=is a real limit. systemd-cglsshows the tree;systemctl statusprints the cgroup path.
Old init scripts tracked a PID file instead and routinely lost track of daemons that double-forked.
What is socket activation?
systemd creates and listens on a service's socket itself and only starts the service when the first client connects, handing over the already-open socket.
- Boot gets faster because services start on demand and in any order.
- Clients never see a connection refused during a restart, because systemd keeps the socket open.
Docker on a fresh host works this way: docker.socket is listening and the first docker command starts dockerd.