Linux Under the Hood: What Nobody Teaches Beginners.
We often say “Linux” when we mean Ubuntu, CentOS, or Debian. But from an engineering standpoint, that’s not quite right. Linux is the kernel. A piece of code that manages hardware. Everything we see on screen, everything we interact with — it’s just a collection of programs running on top of that kernel.
In this article, we’ll break down how it all works. Let’s start from the very bottom.
Step 1. Press the power button
Current flows. The CPU wakes up — but at this moment it’s a dumb piece of silicon. It doesn’t know what Linux is. It doesn’t know what files are. It doesn’t even know how much RAM it has. It simply looks for the first instruction at a hardcoded address.
The first thing to start is the motherboard firmware — BIOS or UEFI. Its job is to wake up the hardware. It runs POST (Power-On Self-Test): checks if memory is present, whether the GPU works, whether the CPU initialized. Then it polls devices and looks for something to boot from.
Step 2. The bootloader — GRUB
Let’s say BIOS found a hard drive. But here’s the catch — BIOS can’t read file systems on its own. It doesn’t know what ext4 or XFS is. It can’t just open a file. So it reads the very first 512 bytes of the disk (the MBR) or looks inside a special EFI partition. And that’s where the bootloader lives.
In the Linux world, it’s almost always GRUB 2.
GRUB is a small but smart program. Its one superpower — it knows how to read file systems. It looks inside the /boot partition, where two critically important files live, without which nothing happens.
vmlinuz — this is Linux itself. The kernel, compressed into a binary file. The z at the end stands for zip — it’s compressed.
initramfs — a small temporary archive. More on that in a moment.
GRUB takes the kernel, decompresses it directly into RAM, and hands over control. That’s it. From this point, BIOS goes off to have a smoke, the bootloader dies. Now there’s only one boss in the system — the kernel.
Power button
|
v
BIOS/UEFI
(POST, device scan)
|
v
GRUB 2
(reads /boot, loads kernel)
|
v
vmlinuz (kernel)
|
v
initramfs
(temporary disk with drivers)
|
v
switch_root
(switch to the real disk)
|
v
systemd (PID 1)
|
v
SSH, Nginx, Postgres...
Step 3. The chicken-and-egg problem
And this is where the kernel hits a classic engineering problem.
The kernel is loaded into memory. Its job is to mount the main disk to start the system. But to mount the disk, it needs drivers — for the file system and the disk controller. And where do the drivers live? Exactly — on the very disk we can’t read yet, because we have no drivers.
A vicious cycle.
That’s exactly why GRUB loaded the second file — initramfs. It’s a small temporary archive that unpacks into RAM as a virtual disk. Inside is a mini Linux with a minimal set of drivers. The kernel mounts initramfs as a temporary root, loads the necessary drivers for the real hardware (RAID controllers, NVMe, LVM), finds the real disk — and performs a trick called switch_root, swapping the root filesystem. The temporary disk is discarded from memory and replaced with the real one.
Note: you may come across the term pivot_root — that’s an older mechanism. Modern systems with initramfs use switch_root.
Practical tip: if your server hangs during boot with a kernel panic error — the kernel got stuck right here. Either it couldn’t find initramfs, or the required driver for the disk wasn’t inside it.
Step 4. The kernel — three core tasks
The bootloader has done its job, drivers are loaded — we’re now in kernel space. From the CPU architecture perspective, we’re in protection ring Ring 0. Everything is permitted here: any CPU instruction, any memory address.
Linux is a monolithic kernel, not a collection of separate services. The GPU driver, TCP/IP network stack, ext4 filesystem driver — all of it cooks in one giant pot, in a single address space. This gives insane performance because there’s no overhead for inter-component communication. But there’s a risk: if a buggy driver crashes, it takes the entire server down with it into kernel panic.
The kernel has three core tasks.
1. Memory management
The kernel’s main job here is to lie. It lies to our programs.
When Nginx or Python requests memory, it thinks it’s the only process in the system. From its perspective, everything looks like a clean, continuous strip of addresses. In reality, physical memory is fragmented and data is scattered chaotically.
The kernel maintains a huge virtual memory table: “virtual address X for the Nginx process actually lives at physical cell Y.” This gives two things: isolation (one process physically cannot touch another’s memory) and swap (if memory runs low, the kernel silently offloads some data to disk).
But if memory runs completely dry and swap is full too — the OOM Killer shows up. And the name is no accident. It’s literally a hitman hired by the kernel. It wakes up, scans the process table, finds whoever is eating the most memory at the lowest priority — and puts a bullet in its head. In Kubernetes, this happens constantly when pod limits are misconfigured.
2. Process scheduler
Let’s say our server has 4 cores, but 500 processes are running. How do they all work simultaneously?
They don’t. It’s an illusion.
The kernel uses preemptive multitasking. It lets a process run for a few milliseconds — a so-called time slice — then forcibly stops it. This is called a context switch: the kernel saves the CPU register state for the current task, loads the state for the next one, and hits play. This happens thousands of times per second.
If you see a high load average in monitoring but CPU usage isn’t at 100% — the problem most likely has nothing to do with the CPU at all. Load average counts not only actively running processes, but also those blocked waiting for I/O — stuck until a disk or network responds. This is the D state in ps output (uninterruptible sleep). The CPU sits idle while the server feels slow. You can diagnose this with iostat or vmstat — if wa (I/O wait) is high, that’s your culprit.
Context switching is a separate story. It shows up in vmstat as a high cs value. If the system is genuinely burning resources switching between thousands of processes — CPU will be loaded and cs will spike.
3. Hardware abstraction
The kernel acts as a translator. Programs in user space don’t know the hardware. Say there’s a Python program that writes a word to a file. But Python doesn’t know where it’s writing — to a Samsung SSD, an old hard drive, or a network share. Every device has its own protocol, its own voltages, its own controller commands.
The kernel provides a universal interface. The program says “write to this file.” The kernel driver translates that into electrical signals. That’s why our code runs the same way on any hardware.
Step 5. System calls — the only door into the kernel
Between user mode, where our programs live, and kernel mode stands a concrete wall. The CPU physically forbids user space code from accessing hardware. If a program tries to directly access hardware — the CPU will immediately stop it. No access granted.
But there’s one single armored door in that wall. It’s called a system call (syscall). It’s a strictly regulated API. The kernel says: “I won’t let you near the disk directly, but if you ask nicely through a special function — I’ll do it for you.”
Let’s look at an example. We write
print("Hello World")
in Python — and a whole chain of events unfolds:
- The Python interpreter calls the C standard library (glibc).
- The library forms a write syscall.
- It places arguments in CPU registers and generates a software interrupt.
- The CPU stops the program, switches mode to Ring 0, and hands control to the kernel.
- The kernel checks permissions and outputs the text to screen.
- Control returns to the program.
There are around 300–400 system calls in Linux. The key ones everything is built on: fork, exec, open, close, read, write, socket, connect.
Why does this matter? Because programs lie. Logs can be empty, errors can be useless. But a program can’t lie to the kernel. It has to make syscalls to do anything at all.
That’s where strace comes in — the ultimate debugging tool. It’s a wiretap for syscalls. Run it on a process and see everything: what it tried to do, what it got back, where it got stuck. strace is an X-ray. If you can read its output, no bug is a mystery.
Step 6. The first process — systemd
The kernel has loaded, initialized memory and drivers. But the server is still empty — no SSH, no console, no network. To bring the system to life, the kernel needs to launch the very first process in user space. It searches for an executable in several paths:
/sbin/init
/etc/init
/bin/init
/bin/sh
It runs the first one it finds. This process gets PID 1 — its unique identifier. Every other process in the system — PID 2, 3, 4, 10000 — will be a descendant of this one. If PID 1 dies — the kernel decides the system is broken, crashes into kernel panic, and the server halts.
In modern Linux, systemd plays the role of PID 1.
Back in the SysV init days, processes started in strict sequence: first network, then disk, then database. Slow. systemd works like a dependency manager — it builds a directed graph and launches everything in parallel, maxing out the CPU during boot. SSH doesn’t depend on anything? Start it immediately. Postgres depends on disk? It waits.
When you type
systemctl start nginx
— you’re sending a command to systemd over a socket. systemd checks permissions, looks at dependencies, and executes the fork syscall (creates a copy of itself) + exec (replaces that copy with the Nginx binary). That’s how a web server process is born — a child of systemd.
Step 7. The filesystem — “everything is a file”
Processes are running, but they need to read configs, write logs, save data. We move up to the next abstraction layer — the filesystem.
The core Unix philosophy: everything is a file. But let’s look at this not as a slogan, but as an engineering decision.
Windows has drive letters C:, D:, E: — physical device separation. Linux takes a different approach. There’s a single directory tree that always starts at the root — the / symbol. A process doesn’t care how many disks exist. It only sees the tree.
Inside the kernel there’s a layer called VFS (Virtual File System) — a universal interface. Mount an SSD at /boot. Mount a network disk at /mnt/share. Mount RAM at /tmp. To a program, it all looks the same — it just writes to paths, and VFS figures out where to send the bytes: over a SATA cable or over the network to another server.
/ ← root
├── boot/ ← SSD with kernel
├── etc/ ← config files
├── home/ ← home directories
│ └── user/
├── proc/ ← virtual (kernel data)
├── sys/ ← virtual (kernel settings)
├── tmp/ ← RAM
├── usr/
│ ├── bin/ ← program binaries
│ └── lib/ ← shared libraries
├── var/
│ └── log/ ← logs
└── mnt/
└── share/ ← network disk
What is a file physically?
For us, a file is photo.jpg. But for the kernel, the name means nothing. Just bytes for human convenience. For the kernel, a file is an inode (index node). Every file in the system is just a number.
An inode stores all metadata except the name: owner, permissions, file size, timestamps, and most importantly — pointers to the disk sectors where the actual data lives.
So what is a filename? The name lives in a directory. Technically a directory is also a file, but inside it there’s a simple two-column table: filename → inode number. When we open a file, the kernel reads the directory, finds the inode number, checks permissions — and only then starts reading data.
Directory /home/user/
┌─────────────┬──────────┐
│ Name │ inode # │
├─────────────┼──────────┤
│ photo.jpg │ 42017 │
│ notes.txt │ 42031 │
│ script.sh │ 42089 │
└─────────────┴──────────┘
│
▼
inode #42017
┌─────────────────────┐
│ owner: uid 1000 │
│ perms: 644 │
│ size: 3.2 MB │
│ modified: 2024-03-01│
│ data → [sectors] │
└─────────────────────┘
/proc and /sys — the kernel as files
The Linux developers thought: why limit ourselves to disks? Let’s expose the kernel itself as files.
Enter /proc and /sys. Files there are 0 bytes in size. Nothing on disk. It’s an illusion — but a direct interface to kernel data structures in RAM. Read /proc/cpuinfo — the kernel polls the CPU and generates the response text on the fly.
And it works both ways. Want to enable IP forwarding and turn your server into a router? No need to hunt for a checkbox in any control panel. Just write 1 to the right file:
echo 1 > /proc/sys/net/ipv4/ip_forward
The kernel intercepts that write and changes a variable in its own code. That’s what “everything is a file” really means — a universal API for managing the system.
Step 8. Processes, stdin/stdout, and pipes
A process is an isolated container in memory with its own PID, variables, and resources. But a process in a vacuum is useless. It needs to receive data and return results.
When the kernel launches any process, it automatically opens three communication channels — three files:
Descriptor Name Default attachment
0 stdin (standard input) keyboard
1 stdout (standard output) screen/terminal
2 stderr (standard error) screen/terminal
Why separate stdout and stderr? To tell signal from noise. If a program runs and throws errors, you can tell the shell: write useful data (descriptor 1) to a file, and throw errors (descriptor 2) into /dev/null — the black hole:
command > data.txt 2>/dev/null
The pipe | — Linux’s greatest trick
The | symbol is a mechanism for inter-process communication. When you write:
cat access.log | grep "ERROR"
The kernel launches cat and grep simultaneously. For cat, it replaces stdout: instead of the screen — a special memory buffer (the pipe). For grep, it replaces stdin: instead of the keyboard — that same buffer.
The genius of it: cat thinks it’s writing to the screen. grep thinks it’s reading from the keyboard. They don’t know each other exists. The kernel just rewired the connections.
cat access.log grep "ERROR"
┌───────────┐ ┌───────────┐
│ │ stdout │ │
│ cat ├──────────►│ grep │──► screen
│ │ pipe │ │
└───────────┘ (buffer) └───────────┘
thinks it's thinks it's
writing to screen reading from keyboard
This lets you build pipelines of any length. Five dumb little programs connected by pipes become a powerful analytics tool. That’s the Unix philosophy: write programs that do one thing well — and make them work together through text streams.
Step 9. Signals — how processes get a nudge
Processes don’t live in complete isolation. The kernel and other processes can send them messages. These are called signals — short asynchronous notifications: “hey, time to die” or “re-read your config.”
When you press Ctrl+C in a terminal — you’re not “closing” the program. You’re sending it a SIGINT signal. The program can catch it and exit cleanly. Or ignore it.
Key signals to know:
SIGTERM — a polite request to exit. The program can catch it, save state, and shut down.
SIGKILL — an order that cannot be ignored. The kernel kills the process immediately, with no chance to save anything. This is what OOM Killer uses.
SIGHUP — historically meant “terminal lost.” Today daemons use it as a signal to reload their config without restarting.
SIGINT — keyboard interrupt (Ctrl+C).
SIGSTOP — freeze the process. Also cannot be ignored.
You can send a signal manually with the
kill
command — despite the name, it can send any signal:
kill -SIGTERM 1234 # politely ask process 1234 to exit
kill -SIGKILL 1234 # kill without mercy
kill -SIGHUP 1234 # ask it to reload config
Important: SIGKILL is a last resort. The process has no time to save anything, close connections, or flush data. Always try SIGTERM first — only use SIGKILL if it doesn’t work.
Step 10. File permissions
In Linux, security isn’t antivirus magic. It’s checking bits in the inode.
The kernel doesn’t know words. It only knows numbers. Your username is translated into a UID (User ID), your group into a GID (Group ID). The mapping lives in plain text files /etc/passwd and /etc/group.
Every inode stores: “this file is owned by UID X, group Y.” And alongside that, three sets of permissions: user (owner), group, others (everyone else). Each set has three actions:
read (4) — read the file
write (2) — modify the file
execute (1) — run it as a process
These aren’t arbitrary numbers — they’re a bitmask: read = 100, write = 010, execute = 001.
When you run
chmod 755 file
:
7 = 4+2+1 — owner can do everything
5 = 4+0+1 — group can read and execute, but not write
5 = 4+0+1 — same for everyone else
One important detail: the x flag on a directory doesn’t mean execute — it means permission to enter it. If you can read the directory but can’t enter it, you’ll see the file list but can’t read their contents.
Root and sudo
In Linux there’s one user for whom the kernel disables permission checks — that’s root (UID 0). When a process with UID 0 opens a file, the kernel doesn’t check the inode — it just opens it. Root can read /etc/shadow with password hashes, kill any process, format a disk on a live system.
That’s why running as root is bad practice. For administration, use sudo — a utility with the SUID bit that temporarily elevates your UID to 0, runs one command, and critically — writes everything to an audit log. Security without losing control.
Step 11. Package manager and libraries
On Windows people are used to: find a site, download .exe, click Next, and every program drags its own libraries along. Linux takes a different approach — centralized repositories. These are curated warehouses of binary packages maintained by your distro developers.
When you run
apt install nginx
— it’s not just a download. The package manager unpacks the package according to the FHS standard: binaries go to /usr/bin or /usr/sbin, configs to /etc, service file to /lib/systemd/system. And most importantly — it checks dependencies.
Why does this matter? Because programs in Linux are almost always dynamically linked. The Nginx binary is tiny — it contains no encryption or compression code. At startup it simply tells the kernel: “I need this library.” Those libraries are shared — one copy on the system. The kernel loads the library into memory once, and Nginx, the SSH client, and your Python script all get a reference to that same memory region. It saves enormous amounts of RAM.
But library versions must match. When that house of cards collapses — it’s called Dependency Hell. That’s exactly why you need a package manager, not manual file copying.
Step 12. Namespaces and cgroups — what Docker is really made of
In the conclusion we’ll say “there’s no such thing as Docker.” But let’s unpack what that means.
The Linux kernel provides two mechanisms that together give us containers.
Namespaces — environment isolation
A namespace is a mechanism that tells a process: “you don’t see the whole world — only your slice of it.”
There are several namespace types:
PID namespace — the process thinks it’s PID 1. It can’t see processes outside.
NET namespace — the process has its own network interface, its own IP, its own firewall rules.
MNT namespace — the process sees its own filesystem tree.
UTS namespace — the process thinks it has its own hostname.
USER namespace — the process thinks it’s root — but only inside its own namespace.
When Docker creates a container — it just creates a set of namespaces and launches a process inside them. The process thinks it’s alone on the server. In reality, hundreds of equally “lonely” processes are running right beside it.
Host (Linux kernel)
┌─────────────────────────────────────┐
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Container 1 │ │ Container 2 │ │
│ │ (namespaces)│ │ (namespaces)│ │
│ │ PID: 1 │ │ PID: 1 │ │
│ │ IP: 10.0.1 │ │ IP: 10.0.2 │ │
│ │ cgroups: │ │ cgroups: │ │
│ │ 512MB RAM │ │ 1 CPU │ │
│ │ 1 CPU │ │ 2 CPU │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────┘
Cgroups — resource limits
Namespaces give isolation. But what if a process inside its namespace decides to eat all the server’s memory?
That’s where cgroups (control groups) come in. This is a kernel mechanism that lets you limit how many resources a process or group of processes can use:
memory.limit_in_bytes — max RAM
cpu.shares — share of CPU time
blkio.weight — disk I/O priority
cgroups are exactly what you configure in Kubernetes when you write resources.limits in a pod manifest. Kubernetes tells the kernel: “this process can use no more than 512MB of memory.” If it exceeds that — OOM Killer arrives.
So Docker is just a convenient wrapper around namespaces and cgroups. No magic. Any process in Linux can use these mechanisms — Docker just automates and packages it into a friendly interface.
Conclusion: look at Linux like an engineer
We’ve traveled from cold silicon to a running web server. Now your head should hold not a jumble of commands, but a clear vertical structure:
- Hardware — dumb silicon woken up by BIOS
- GRUB — bootloader found and unpacked the kernel
- Kernel — the dictator that lies to programs about memory, slices CPU time, manages drivers
- System calls — the only door between programs and the kernel
- systemd (PID 1) — builds the entire user space environment
- Filesystem — a single tree where every file is a number with permissions
- Processes and pipes — isolated containers connected by data streams
- Signals — how the kernel and processes communicate
- Permissions — bits in the inode that the kernel checks on every syscall
- Packages and libraries — a managed dependency ecosystem
- Namespaces and cgroups — the isolation and resource limits that make up any container
Keep this picture in your head and the magic disappears. What’s left is clean, dry logic.
See a Permission Denied error? That’s not “Linux complaining.” That’s the kernel calling open(), comparing your UID against the inode bits, and returning an error code.
Server slowing down, high load average? That’s not “the CPU is tired.” The scheduler queue got too long and the system is burning resources on context switching.
Someone asks you about Docker? At its core it’s namespaces and cgroups — kernel features available to any process. Docker is a convenient wrapper around those mechanisms. Understand the kernel — understand containers.
A beginner memorizes commands. An engineer understands how bytes flow. You can Google a command in 5 seconds. You can only earn architectural understanding through practice.
