Linux systems · 1 of 5

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.

Updated 2026-09-08
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.

Figure 1. The whole article on one page. Every branch is a section below; fold what you know, open what you do not.

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:

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"]
Figure 2. The layers. Only the arrow from the system call interface downward runs in privileged mode. Everything above it is user space.

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.

In the wild
  • Android is the Linux kernel with a different user space: Bionic instead of glibc, init instead 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 spaceUser space
CPU modeRing 0 (privileged)Ring 3
Can touchAll memory, all devices, page tablesIts own mapped memory only
A bug meansKernel panic, whole machineOne process dies with a signal
Gets in throughBootexec of a program
ExamplesScheduler, drivers, TCP, ext4bash, nginx, Postgres, your app

There are exactly three ways control passes from user mode to kernel mode:

  1. A system call. The program asks for something on purpose. This is the next section.
  2. An interrupt. A device or the timer needs attention. The program is paused, the kernel handles it, the program resumes without knowing.
  3. 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.

In the wild
  • 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
Figure 3. One write, end to end. The program's registers hold the request going in and the answer coming out. Steps 3 and 7 are the two mode switches.
  1. The program calls write in libc.
  2. libc puts the system call number in a register (rax on x86-64) and the arguments in others (rdi, rsi, rdx, r10, r8, r9).
  3. libc runs the syscall instruction. The CPU switches to ring 0 and jumps to the kernel's entry point. On ARM64 the instruction is svc and the number goes in x8.
  4. The kernel saves the program's registers, reads the number, and looks up the handler in the syscall table. Entry 1 is sys_write.
  5. 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.
  6. 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).
  7. The kernel puts the result in rax and returns to user mode. A success is a value of zero or more. A failure is a small negative number, the negated error code.
  8. libc sees the negative value, stores the positive code in errno, and returns -1 to 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

AreaSystem callsWhat they do
Filesopenat, read, write, close, stat, lseekEverything is a file descriptor: files, pipes, sockets, devices.
Processesfork, clone, execve, wait4, exit_group, killMake, replace, wait for and signal processes. Threads are clone with sharing flags.
Memorymmap, munmap, brk, mprotectMap pages into the address space. malloc is built on these.
Networksocket, bind, listen, accept, connect, sendto, recvfromCovered in the sockets article.
Waitingpoll, epoll_wait, futex, nanosleepSleep until something happens. futex is what mutexes use when they must block.
DevicesioctlThe 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.

In the wild
  • strace attaches to a process with ptrace and 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.
Watch out

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, EAGAIN are 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:

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.

In the wild
  • 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 print follows the same rule: line-buffered on a terminal, block-buffered into a pipe. python -u and PYTHONUNBUFFERED=1 exist 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. stdio does it, raw write does 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"]
Figure 4. An interrupt is split in two so the CPU is not blocked for long with interrupts disabled. The network stack's real work runs in the bottom half.

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.

In the wild
  • A busy NIC can raise hundreds of thousands of interrupts per second. Drivers switch to polling under load (NAPI), and /proc/interrupts shows the counts per CPU.
  • ksoftirqd is the kernel thread you see in top when 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 uptime is 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"]
Figure 5. The boot chain. Every step before PID 1 runs in kernel mode or before there is a kernel at all. From PID 1 on, it is ordinary processes.
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Real root. The initramfs mounts the real root filesystem and moves into it with switch_root, freeing the RAM copy.
  6. 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.

In the wild
  • An AWS EC2 instance boots this exact chain inside a VM; dmesg right after boot shows the kernel's part of it.
  • Android replaces the last step with its own init that reads .rc files 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 typeWhat it managesExample
.serviceA daemon or one-shot programnginx.service, docker.service
.socketA listening socket; the service starts on the first connectiondocker.socket, sshd.socket
.targetA named group of units, a milestone in bootmulti-user.target
.timerRuns a service on a schedule, like cronlogrotate.timer
.mountA filesystem mount, generated from /etc/fstabhome.mount
.sliceA cgroup subtree for resource accountingsystem.slice, user.slice
.deviceA device that udev founddev-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
Figure 6. Targets are milestones. Each one waits for the units grouped under it. 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.

In the wild
  • 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.socket listens on /run/docker.sock and starts dockerd on 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.
Watch out

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. journalctl reads 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 syscall instruction, and the kernel looks up the handler in a table.
  • A negative return from the kernel becomes -1 plus errno in libc. Read errno immediately.
  • 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; malloc and printf are 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. After orders them, Wants pulls 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/system stay 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 syscall instruction.
  • 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 -1 and sets errno.

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.

  • errno is only valid right after a failed call; the next failure overwrites it.
  • strace decodes the code into a name for you, such as ENOENT or EACCES.

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; strace never 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 malloc calls never enter the kernel at all.
  • On a terminal printf flushes 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.
  • ksoftirqd is 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_root replaces 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= or Wants= plus After=.
  • 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 stop kills the whole tree and why MemoryMax= is a real limit.
  • systemd-cgls shows the tree; systemctl status prints 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.