Once you truly understand the Linux filesystem — not just the commands, but the model — every other Linux concept becomes dramatically easier to learn.
Linux Zero to Hero · Episode 02 ·
In Episode 01 we introduced the Unix philosophy and got our first terminal session running. We mentioned one idea in passing that deserves its own episode: everything is a file.
It sounds like a cute slogan. It isn’t. It’s a design decision with profound consequences for how Linux works — and for how you work with Linux. The filesystem is not just where your documents live. It’s the interface to the kernel, to running processes, to hardware devices, to system configuration, and to network connections. All of it exposed as files you can read, write, and pipe through standard tools.
This episode builds the mental model that makes everything else click.
What a Filesystem Actually Is
Before we talk about Linux’s filesystem, let’s be precise about what a filesystem is in the first place.
A filesystem is the layer of software that manages how data is stored on and retrieved from a storage device. It defines:
- How files are named and organised into directories
- Where on the disk each file’s data actually lives
- What metadata is stored about each file (size, permissions, timestamps, ownership)
- How the operating system navigates from a filename to the actual bytes on disk
Without a filesystem, a disk is just a flat sequence of bytes with no structure. The filesystem gives it meaning.
Linux supports dozens of filesystem types: ext4 (the default for most Linux systems), xfs (popular for large-scale storage), btrfs (modern, with snapshots), tmpfs (in-memory), and more. But from a user perspective, they all look the same — a hierarchy of files and directories starting at /.
# What filesystem type is your root partition?
df -T /
# Filesystem Type 1K-blocks Used Available Use% Mounted on
# /dev/sda1 ext4 20511312 5242880 14221440 27% /
# All mounted filesystems
df -Th
# Filesystem Type Size Used Avail Use% Mounted on
# /dev/sda1 ext4 20G 5.0G 14G 27% /
# tmpfs tmpfs 3.9G 0 3.9G 0% /dev/shm
# /dev/sda2 ext4 50G 12G 35G 26% /home
The Inode: The Real Identity of a File
Here’s something most engineers don’t know: in Linux, a file’s name is not its identity. Its inode is.
An inode (index node) is a data structure stored on disk that contains everything about a file except its name and content:
- File type (regular file, directory, symlink, device…)
- Permissions (read, write, execute for owner, group, others)
- Owner (UID) and group (GID)
- File size
- Timestamps (created, modified, accessed)
- Number of hard links pointing to this inode
- Pointers to the actual data blocks on disk
The filename is just a label in a directory that points to an inode number. The file content lives in data blocks that the inode points to.
Directory entry: Inode: Data blocks:
"nginx.conf" → 142857 ┌─────────────────┐ ┌────────────────┐
│ inode 142857 │────▶ │ server { │
│ type: file │ │ listen 80; │
│ perms: 644 │ │ ... │
│ owner: root │ └────────────────┘
│ size: 2847 bytes│
│ links: 1 │
└─────────────────┘
# See inode numbers with ls -i
ls -i /etc/hosts
# 131073 /etc/hosts
# ↑ inode number
# See detailed inode information
stat /etc/hosts
# File: /etc/hosts
# Size: 221 Blocks: 8 IO Block: 4096 regular file
# Device: 802h/2050d Inode: 131073 Links: 1
# Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
# Access: 2024-01-15 09:23:41.000000000 +0000
# Modify: 2024-01-10 08:00:00.000000000 +0000
# Change: 2024-01-10 08:00:00.000000000 +0000
Why does the inode model matter? Because it explains hard links, symlinks, what happens when you rename a file, why deleting a file sometimes doesn’t free disk space, and how processes can keep a file open after it’s been deleted.
Hard Links vs Symbolic Links
Two types of links. Completely different in how they work.
Hard Links
A hard link is a second directory entry pointing to the same inode. Not a copy — the same underlying data. Both names are equally valid names for the same file.
# Create a hard link
echo "important config" > original.conf
ln original.conf hardlink.conf
# Both files show the same inode number
ls -i original.conf hardlink.conf
# 142858 hardlink.conf
# 142858 original.conf ← same inode!
# The link count is now 2
stat original.conf | grep Links
# Links: 2
# Editing one changes both (they're the same file)
echo "new line" >> hardlink.conf
cat original.conf
# important config
# new line
# Deleting one doesn't delete the data
rm original.conf
cat hardlink.conf
# important config
# new line ← data still exists, link count dropped to 1
Hard link rules: must be on the same filesystem, cannot link to directories (prevents circular loops), data is deleted only when the link count reaches zero.
Symbolic Links (Symlinks)
A symlink is a special file that contains a path to another file. Like a shortcut in Windows, but more powerful.
# Create a symlink
ln -s /etc/nginx/nginx.conf nginx.conf.link
# The link shows where it points
ls -la nginx.conf.link
# lrwxrwxrwx 1 ubuntu ubuntu 22 Jan 15 10:00 nginx.conf.link -> /etc/nginx/nginx.conf
# ↑ 'l' = symlink type
# Reading the symlink reads the target
cat nginx.conf.link # reads /etc/nginx/nginx.conf
# The symlink has a different inode than the target
ls -i nginx.conf.link /etc/nginx/nginx.conf
# 142860 nginx.conf.link
# 131075 /etc/nginx/nginx.conf ← different inodes
# A broken symlink (target doesn't exist) — still shows up but can't be read
ln -s /nonexistent/path broken.link
cat broken.link
# cat: broken.link: No such file or directory
ls -la broken.link
# lrwxrwxrwx 1 ubuntu ubuntu 19 Jan 15 10:01 broken.link -> /nonexistent/path
Symlinks are everywhere in Linux — they’re how /bin and /usr/bin point to the same place, how libraries maintain version compatibility, how /etc/alternatives manages multiple versions of the same tool.
# Real-world symlinks you'll encounter
ls -la /bin
# lrwxrwxrwx 1 root root 7 Jan 10 09:00 /bin -> usr/bin
ls -la /lib
# lrwxrwxrwx 1 root root 7 Jan 10 09:00 /lib -> usr/lib
# Python version management via symlinks
ls -la /usr/bin/python*
# lrwxrwxrwx 1 root root 9 Jan 10 09:00 /usr/bin/python3 -> python3.10
# -rwxr-xr-x 1 root root ... /usr/bin/python3.10
File Types in Linux
Linux has seven file types, all viewable with ls -la. The first character of the permissions string tells you the type:
ls -la /dev/ /tmp/ /etc/
# - regular file
# d directory
# l symbolic link
# c character device (reads/writes character by character — terminals, keyboards)
# b block device (reads/writes in blocks — disks, USB drives)
# p named pipe (FIFO)
# s socket (network/IPC communication)
# Regular file
-rw-r--r-- 1 root root 221 Jan 10 /etc/hosts
# Directory
drwxr-xr-x 2 root root 4096 Jan 10 /etc/nginx/
# Symbolic link
lrwxrwxrwx 1 root root 7 Jan 10 /bin -> usr/bin
# Block device (your hard disk)
brw-rw---- 1 root disk 8, 0 Jan 10 /dev/sda
# Character device (your terminal)
crw--w---- 1 ubuntu tty 136, 0 Jan 15 /dev/pts/0
# Named pipe
prw-r--r-- 1 root root 0 Jan 10 /run/systemd/initctl/fifo
# Socket
srw-rw-rw- 1 root root 0 Jan 10 /run/docker.sock
The socket /run/docker.sock is how docker CLI communicates with the Docker daemon — you write commands to a socket file, not a network port. Same principle as sending data over a network, but local. Everything is a file.
Understanding File Permissions
Every file in Linux has three permission sets and three permission types.
Three sets: owner (user), group, others (everyone else)
Three types: read (r), write (w), execute (x)
ls -la /etc/passwd
# -rw-r--r-- 1 root root 1823 Jan 10 /etc/passwd
# │││││││││
# ││││││││└── others: execute (-)
# │││││││└─── others: write (-)
# ││││││└──── others: read (r)
# │││││└───── group: execute (-)
# ││││└────── group: write (-)
# │││└─────── group: read (r)
# ││└──────── owner: execute (-)
# │└───────── owner: write (w)
# └────────── owner: read (r)
Octal Notation
Permissions are often expressed as three-digit octal numbers. Each digit represents one permission set. Each type is a bit: r=4, w=2, x=1.
r w x value
─ ─ ─
4+2+1 = 7 full permissions (rwx)
4+2+0 = 6 read and write (rw-)
4+0+1 = 5 read and execute (r-x)
4+0+0 = 4 read only (r--)
0+0+0 = 0 no permissions (---)
# Common permission patterns
chmod 644 file.conf # rw-r--r-- : owner rw, group r, others r (config files)
chmod 755 script.sh # rwxr-xr-x : owner rwx, group rx, others rx (executables)
chmod 600 private.key # rw------- : owner rw only (SSH keys, secrets)
chmod 777 shared/ # rwxrwxrwx : everyone full access (almost never do this)
chmod 700 ~/.ssh/ # rwx------ : owner only (SSH directory)
# Symbolic notation
chmod u+x script.sh # add execute for owner (u=user/owner)
chmod g-w file.txt # remove write for group
chmod o= file.txt # remove all permissions for others
chmod a+r file.txt # add read for all (a=all)
# Recursive
chmod -R 755 /var/www/html/
# Change owner
chown ubuntu file.txt
chown ubuntu:developers file.txt # change owner AND group
chown -R www-data:www-data /var/www/ # recursive
# Change group
chgrp developers project/
Execute Permission: It Means Different Things
For a regular file, execute (x) means you can run it as a program. For a directory, execute means you can enter it and access its contents — it's sometimes called the "search" or "traverse" permission.
# You need execute on a directory to cd into it
chmod 600 /tmp/testdir/ # remove execute from directory
cd /tmp/testdir/
# bash: cd: /tmp/testdir/: Permission denied
# You can have execute on a directory without read
# (you can cd in, but you can't ls to see what's there)
chmod 100 /tmp/secretdir/
cd /tmp/secretdir/ # works
ls # Permission denied
Special Permissions: setuid, setgid, sticky bit
Three additional permission bits that modify how execution works:
# setuid (s in owner execute position)
# File runs with the owner's privileges, not the caller's
# Used by: passwd, sudo, ping
ls -la /usr/bin/passwd
# -rwsr-xr-x 1 root root 68208 Jan 10 /usr/bin/passwd
# ↑ 's' instead of 'x' = setuid
# This is why regular users can change their own password
# /usr/bin/passwd runs as root, but only modifies the calling user's entry
chmod u+s /usr/bin/myprogram # set setuid
# setgid (s in group execute position)
# File runs with the group's privileges
# On a directory: new files inherit the directory's group
chmod g+s /shared/project/
# sticky bit (t in others execute position)
# On a directory: only the file owner can delete their own files
# Used on /tmp so users can't delete each other's temp files
ls -la / | grep tmp
# drwxrwxrwt 17 root root 4096 Jan 15 /tmp
# ↑ 't' = sticky bit
chmod +t /shared/uploads/ # set sticky bit
The /proc Filesystem: The Kernel’s Window
/proc is not a real filesystem on disk. It's a virtual filesystem that the kernel creates in memory and presents as files. It's how Linux exposes internal kernel and process state to userspace.
# System-wide information
cat /proc/version
# Linux version 5.15.0-91-generic (buildd@lgw01-amd64-060) (gcc version...) #101-Ubuntu SMP
cat /proc/uptime
# 86400.23 340128.91 ← seconds since boot, idle time
cat /proc/loadavg
# 0.52 0.58 0.59 2/312 14823 ← 1/5/15 min load avg, running/total processes, last PID
cat /proc/meminfo | head -10
# MemTotal: 16384000 kB
# MemFree: 8192000 kB
# MemAvailable: 10240000 kB
cat /proc/cpuinfo | grep -E "model name|cpu cores" | head -4
# model name : Intel(R) Xeon(R) CPU E5-2676 v3 @ 2.40GHz
# cpu cores : 4
# Per-process information — every running process has a directory
ls /proc/ | head -20
# 1 2 3 ... (numbered directories = process IDs)
# Information about a specific process (PID 1 = systemd/init)
ls /proc/1/
# cmdline cwd environ exe fd maps mem net root stat status
cat /proc/1/cmdline | tr '\0' ' '
# /sbin/init splash ← the command that started process 1
ls -la /proc/1/exe
# lrwxrwxrwx 1 root root 0 Jan 10 /proc/1/exe -> /lib/systemd/systemd
cat /proc/1/status | head -10
# Name: systemd
# State: S (sleeping)
# Tgid: 1
# Pid: 1
# PPid: 0
# VmRSS: 15360 kB ← resident memory in use
/proc is why tools like ps, top, and htop work — they read from /proc and format the output. There's no special system call required; it's just files.
The /sys Filesystem: Hardware and Kernel State
/sys (sysfs) is another virtual filesystem, introduced after /proc to provide a more structured view of hardware and kernel parameters.
# System hardware information
ls /sys/class/
# block bluetooth gpio input net power_supply thermal ...
# Network interfaces
ls /sys/class/net/
# eth0 lo wlan0
cat /sys/class/net/eth0/speed
# 1000 ← link speed in Mbps
cat /sys/class/net/eth0/operstate
# up
# CPU frequency and scaling
ls /sys/devices/system/cpu/cpu0/cpufreq/
# cpuinfo_cur_freq cpuinfo_max_freq scaling_governor...
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
# 3400000 ← 3.4 GHz in kHz
# Kernel tunable parameters via /proc/sys (writable!)
cat /proc/sys/net/ipv4/ip_forward
# 0 ← IP forwarding disabled
echo 1 > /proc/sys/net/ipv4/ip_forward # enable IP forwarding (requires root)
# This change is temporary — survives until reboot
# Permanent changes via sysctl
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0
sudo sysctl -w net.ipv4.ip_forward=1 # set temporarily
# Make permanent: add to /etc/sysctl.conf or /etc/sysctl.d/
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-ip-forward.conf
Mounting: Attaching Filesystems to the Tree
Linux has a single unified directory tree starting at /. Every disk, partition, network share, and virtual filesystem is mounted at a specific point in that tree — a directory where its contents become accessible.
# See all currently mounted filesystems
mount | column -t
# sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
# proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
# /dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro)
# /dev/sda2 on /home type ext4 (rw,relatime)
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev)
# Or the cleaner modern way
findmnt
# TARGET SOURCE FSTYPE OPTIONS
# / /dev/sda1 ext4 rw,relatime
# ├─/proc proc proc rw,nosuid,nodev
# ├─/sys sysfs sysfs rw,nosuid,nodev
# └─/home /dev/sda2 ext4 rw,relatime
# Mount a disk manually
sudo mkdir /mnt/data
sudo mount /dev/sdb1 /mnt/data
ls /mnt/data # contents of the disk now visible here
# Unmount
sudo umount /mnt/data
# Mount with specific options
sudo mount -o ro /dev/sdb1 /mnt/data # read-only
sudo mount -o remount,rw /mnt/data # remount read-write
sudo mount -t ext4 /dev/sdb1 /mnt/data # specify filesystem type
/etc/fstab: Permanent Mount Configuration
# The filesystem table — what gets mounted automatically at boot
cat /etc/fstab
# UUID=a1b2c3d4-... / ext4 defaults 0 1
# UUID=e5f6a7b8-... /home ext4 defaults 0 2
# UUID=c9d0e1f2-... swap swap sw 0 0
# tmpfs /tmp tmpfs defaults 0 0
# Fields: device mountpoint fstype options dump fsck-order
# After editing /etc/fstab, verify all entries are valid
sudo mount -a # mount everything in fstab that isn't already mounted
# If this returns no errors, your fstab is valid
⚠️ A broken /etc/fstab can prevent your system from booting. Always test with sudo mount -a before rebooting after editing it. Always use UUIDs (not /dev/sdX device names) in fstab — device names can change between boots depending on which disks are detected first.
Finding Files: The Three Essential Tools
find — Powerful, Exhaustive
# Find by name
find /etc -name "nginx.conf"
# /etc/nginx/nginx.conf
# Find by name pattern (case insensitive)
find /var/log -iname "*.log"
# Find by type
find /dev -type b # block devices only
find /home -type d # directories only
find /tmp -type l # symlinks only
# Find by size
find /var/log -size +100M # files larger than 100MB
find /tmp -size -1k # files smaller than 1KB
# Find by age
find /var/log -mtime +7 # modified more than 7 days ago
find /tmp -atime +1 # accessed more than 1 day ago
find /home -newer /etc/hosts # newer than /etc/hosts
# Find by permissions
find /etc -perm 644
find / -perm -4000 # setuid files (potential security concern)
# Find by owner
find /home -user alice
find /var -group www-data
# Execute a command on results
find /tmp -name "*.tmp" -delete # delete all .tmp files
find /var/log -name "*.log" -exec ls -lh {} \; # list each log file
find /home -name "*.py" -exec grep "import os" {} \; # search inside files
# Combine conditions
find /etc -name "*.conf" -size +10k -mtime -30 # large conf files changed recently
locate — Fast, Index-Based
# Update the index (run periodically by cron automatically)
sudo updatedb
# Find files by name — searches the index, extremely fast
locate nginx.conf
# /etc/nginx/nginx.conf
# /usr/share/doc/nginx/examples/nginx.conf
# Case insensitive
locate -i README
# Count results only
locate -c "*.log"
The trade-off: locate is fast but only knows about files that existed when the index was last updated. find is slow but always current.
which / whereis — Finding Executables
# Which version of a command will run?
which python3
# /usr/bin/python3
which -a python3 # show all versions in PATH
# /usr/bin/python3
# /usr/local/bin/python3
# Find binary, source, and manual page
whereis nginx
# nginx: /usr/sbin/nginx /etc/nginx /usr/share/man/man8/nginx.8.gz
Disk Usage: Finding What’s Taking Space
# Disk space by filesystem
df -h
# Filesystem Size Used Avail Use% Mounted on
# /dev/sda1 20G 5.0G 14G 27% /
# /dev/sda2 50G 12G 35G 26% /home
# Disk usage of a directory (recursive)
du -sh /var/log
# 2.3G /var/log
# Top-level breakdown
du -h --max-depth=1 /var
# 4.0K /var/backups
# 2.3G /var/log
# 145M /var/lib
# 2.5G /var
# Find the biggest files
du -ah /var/log | sort -rh | head -10
# 2.3G /var/log
# 1.1G /var/log/syslog
# 890M /var/log/auth.log
# Find directories over 1GB
du -h /var | grep -E "^[0-9.]+G"
Hands-On Lab: Exploring the Filesystem
# 1. Map the top-level structure
ls -la /
# Note the types (d, l, -) and which are symlinks
# 2. Explore the inode model
echo "test content" > /tmp/original.txt
ln /tmp/original.txt /tmp/hardlink.txt
ln -s /tmp/original.txt /tmp/symlink.txt
ls -lai /tmp/original.txt /tmp/hardlink.txt /tmp/symlink.txt
# Notice: original and hardlink share the same inode
# symlink has a different inode and shows the target with ->
stat /tmp/original.txt
# Check: Links: 2 (because of the hardlink)
rm /tmp/original.txt
cat /tmp/hardlink.txt # still works — data not deleted until link count = 0
cat /tmp/symlink.txt # broken! target is gone
# 3. Explore /proc for a running process
echo $$ # your current shell's PID
ls /proc/$$ # everything known about your shell
cat /proc/$$/cmdline | tr '\0' ' ' # what command is running
cat /proc/$$/status | grep -E "Name|State|VmRSS"
# 4. Examine permissions in detail
ls -la /etc/shadow # should be readable only by root
ls -la /usr/bin/passwd # notice the 's' in permissions (setuid)
ls -la /tmp # notice the 't' at the end (sticky bit)
# 5. Find interesting things on your system
find / -perm -4000 2>/dev/null | head -10 # all setuid files
# 2>/dev/null suppresses "Permission denied" errors
find /etc -name "*.conf" -size +5k 2>/dev/null # large config files
find /var/log -name "*.log" -mtime -1 # logs modified today
# 6. Check disk usage
df -h
du -sh /var/log /etc /usr/bin
du -ah /var/log 2>/dev/null | sort -rh | head -5
# 7. Trace a symlink chain
ls -la /bin
# Shows: /bin -> usr/bin
ls -la /usr/bin/python3
# May show: python3 -> python3.10
ls -la /usr/bin/python3.10
# The actual binary
# 8. Mount information
findmnt
mount | grep "type ext4" # show only ext4 mounts
cat /proc/mounts # raw kernel mount table
Quick Reference: Permissions Cheat Sheet
Octal Symbolic Meaning
───── ──────── ───────
777 rwxrwxrwx everyone full access (avoid)
755 rwxr-xr-x owner full, others read+execute (executables, dirs)
644 rw-r--r-- owner read+write, others read (config files, docs)
600 rw------- owner read+write only (private keys, secrets)
400 r-------- owner read only (very sensitive files)
700 rwx------ owner full, others nothing (private dirs)
750 rwxr-x--- owner full, group read+execute, others nothing
Special bits:
4000 setuid run as file owner
2000 setgid run as file group / inherit group in dirs
1000 sticky only owner can delete in shared dirs
What’s Next
In Episode 03, we cover Essential Linux Commands — the toolkit you’ll use every single day. Not a dry reference list, but the commands with real-world workflows: finding log entries, processing text, managing files efficiently, and the keyboard shortcuts that make the terminal feel fast instead of frustrating.
Episode → Topic
EP 01 Introduction to Linux
EP 02 ← You are here The Linux Filesystem
EP 03 Essential Linux Commands
EP 04 Users, Groups & Permissions
EP 05 Shell & Bash Scripting
EP 06 Text Processing Power Tools
Bonus Vim: The Editor That Lives in Every Server
EP 07 Processes & Job Control
EP 08 Linux Networking Fundamentals
EP 09 Storage & Disk Management
EP 10 Package Management
EP 11 Linux Security Hardening
EP 12 Performance Monitoring & Troubleshooting
Before the Next Article
Three exercises before moving on:
- Run stat on five different files in /etc. Notice what changes (size, timestamps, permissions) and what stays the same (structure). Find one file with a link count greater than 1 — what does that mean?
- Run find / -perm -4000 2>/dev/null to find all setuid files on your system. Look up what three of them do (man <command>). Understand why each one needs to run as root even when called by a regular user.
- Run du -ah /var/log 2>/dev/null | sort -rh | head -10. Identify the largest log file. Open it with tail -50 <filename> and read the last 50 lines. Get comfortable reading log output — you'll be doing it constantly.
The filesystem is the ground you walk on in Linux. Every command, every process, every network connection — they’re all mediated through files. Once you truly feel this model rather than just knowing it, you’ll find yourself navigating Linux with a clarity that surprises you. See you in Episode 03.