Skip to content

Inodes and Links

An inode is the on-disk record of a file: its type, permissions, owner, size, timestamps and the location of its data. A file name is only a directory entry pointing to an inode, which explains hard links, why mv is instant, and why deleting a file does not always free space.

Track: Core · Interview weight: High


Must-Know Facts

Fact Value Verify with
Inode holds Type, mode, UID, GID, size, timestamps, link count, block pointers stat <file>
Inode does not hold The file name ls -i
Directory A table of name to inode number ls -li
Hard link Another name for the same inode; same filesystem only; not for directories ln a b
Symbolic link A small file containing a path; can cross filesystems and point to directories ln -s a b
Link count Number of names for an inode; a new directory starts at 2 stat -c %h <file>
Deletion rm removes a name; data is freed at link count 0 and no open descriptors lsof +L1
Inode exhaustion "No space left on device" with free blocks df -i
mtime Content changed stat -c %y
ctime Inode changed (content, mode, owner, links); cannot be set by touch stat -c %z
atime Last read; relatime updates it at most once a day findmnt -o OPTIONS /
Root inode 2 on ext4 (XFS uses a different number) stat -c %i /

What stat Shows

echo "config v1" > app.conf
stat app.conf

Output:

  File: app.conf
  Size: 10          Blocks: 8          IO Block: 4096   regular file
Device: 253,0   Inode: 130338      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/laborant)   Gid: ( 1001/laborant)
Access: 2026-09-16 13:56:56.634849637 +0000
Modify: 2026-09-16 13:56:56.634849637 +0000
Change: 2026-09-16 13:56:56.634849637 +0000
 Birth: 2026-09-16 13:56:56.634849637 +0000

Blocks: 8 counts 512-byte units: a 10-byte file occupies one 4096-byte filesystem block. Birth is the creation time, available on ext4, xfs and btrfs with recent stat.


ln app.conf hard.conf
ln -s app.conf soft.conf
ls -li
echo "config v2" >> hard.conf
cat app.conf

Output:

total 8
130338 -rw-rw-r-- 2 laborant laborant 10 Sep 16 13:56 app.conf
130338 -rw-rw-r-- 2 laborant laborant 10 Sep 16 13:56 hard.conf
130339 lrwxrwxrwx 1 laborant laborant  8 Sep 16 13:56 soft.conf -> app.conf
config v1
config v2

app.conf and hard.conf share inode 130338 and a link count of 2; neither is "the original". The symlink has its own inode and a size of 8, the length of the text app.conf.

Removing the first name leaves the data reachable through the hard link and breaks the symlink:

rm app.conf
ls -li
cat hard.conf
cat soft.conf
echo "rc=$?"

Output:

total 4
130338 -rw-rw-r-- 1 laborant laborant 20 Sep 16 13:56 hard.conf
130339 lrwxrwxrwx 1 laborant laborant  8 Sep 16 13:56 soft.conf -> app.conf
config v1
config v2
cat: soft.conf: No such file or directory
rc=1
Hard link Symbolic link
Points to An inode A path
Own inode No Yes
Across filesystems No Yes
To a directory No Yes
Target deleted Data stays while any link remains Link breaks (dangling)
Permissions shown The file's lrwxrwxrwx; the target's mode applies
Typical use Backups with shared blocks (rsync --link-dest) Version switching, /etc/alternatives, usrmerge

The two restrictions of hard links produce these errors:

ln ~/links /tmp/dirlink
ln hard.conf /dev/shm/hard.conf

Output:

ln: /home/laborant/links: hard link not allowed for directory
ln: failed to create hard link '/dev/shm/hard.conf' => 'hard.conf': Invalid cross-device link

Hard links to directories are refused because they could create loops that find, du and backups would follow forever.


ln -s /etc/nonexistent broken
find . -xtype l
readlink broken
readlink -f soft.conf
ln -sfn /opt/app-2.0 current
ls -l current

Output:

./soft.conf
./broken
/etc/nonexistent
/home/laborant/links/app.conf
lrwxrwxrwx 1 laborant laborant 12 Sep 16 13:56 current -> /opt/app-2.0

find -xtype l lists dangling links. ln -sfn replaces an existing link in one step, which is how release directories switch versions (current -> releases/2.0); without -n, ln would create the new link inside the old target directory.

A relative symlink is relative to the link's directory

ln -s app.conf /etc/app/current.conf points to /etc/app/app.conf, not to app.conf in the directory where the command ran. Use an absolute target, or ln -sr to compute the relative path.


A directory has one link from its parent and one from its own . entry. Each subdirectory adds one more through its ...

ls -ld ~/links; mkdir sub; ls -ld ~/links

Output:

drwxrwxr-x 2 laborant laborant 4096 Sep 16 13:56 /home/laborant/links
drwxrwxr-x 3 laborant laborant 4096 Sep 16 13:56 /home/laborant/links

Timestamps

touch -d '2026-01-01' times.txt
stat --printf 'atime %x\nmtime %y\nctime %z\n' times.txt
sleep 2
chmod 640 times.txt
cat times.txt
stat --printf 'atime %x\nmtime %y\nctime %z\n' times.txt

Output:

atime 2026-01-01 00:00:00.000000000 +0000
mtime 2026-01-01 00:00:00.000000000 +0000
ctime 2026-09-16 13:57:06.306620314 +0000
atime 2026-09-16 13:57:08.310614223 +0000
mtime 2026-01-01 00:00:00.000000000 +0000
ctime 2026-09-16 13:57:08.310614223 +0000

touch -d set atime and mtime but not ctime. chmod changed ctime only, and cat updated atime because it was older than mtime (the relatime rule).

Timestamp Updated when Shown by
mtime (modify) Content is written ls -l, find -mtime
ctime (change) Content or inode metadata changes ls -lc, find -ctime
atime (access) File is read, subject to mount options ls -lu, find -atime
btime (birth) File is created stat

ctime is not creation time

ctime is the last inode change. Backup tools and intrusion checks use it because touch can fake mtime and atime but not ctime.


Inode Numbers and Usage

ls -i /usr/bin/ls /bin/ls
stat -c '%i %n' / /proc /sys /home
df -i /

Output:

1777 /bin/ls
1777 /usr/bin/ls
2 /
1 /proc
1 /sys
783 /home
Filesystem      Inodes IUsed   IFree IUse% Mounted on
/dev/root      5171200 51427 5119773    1% /

Inode numbers are unique only within one filesystem, so /proc and /sys can both have inode 1. ext4 fixes the inode count at mkfs time; millions of small files (mail queues, session files, container layers) can exhaust inodes while df -h still shows free space.


Deleted but Open Files

Space is released only when the last name and the last open descriptor are gone.

dd if=/dev/zero of=big.log bs=1M count=500 status=none
df -h / | tail -1
sleep 300 < big.log &
pid=$!
rm big.log
df -h / | tail -1
lsof -a -p "$pid" +L1
ls -l /proc/$pid/fd/0
: > /proc/$pid/fd/0
df -h / | tail -1
kill $pid

Output:

/dev/root        79G  2.3G   73G   4% /
/dev/root        79G  2.3G   73G   4% /
COMMAND  PID     USER   FD   TYPE DEVICE  SIZE/OFF NLINK   NODE NAME
sleep   4109 laborant    0r   REG  253,0 524288000     0 130345 /home/laborant/links/big.log (deleted)
lr-x------ 1 laborant laborant 64 Sep 16 13:57 /proc/4109/fd/0 -> /home/laborant/links/big.log (deleted)
/dev/root        79G  1.8G   73G   3% /

NLINK 0 with (deleted) identifies the file. Truncating it through /proc/<pid>/fd/<n> frees the space without restarting the process; restarting the process also works.


Common Errors

Cause: hard links cannot span filesystems (EXDEV).

Fix: use ln -s, or copy the file.

Cause: directories cannot have extra hard links.

Fix: use a symbolic link or a bind mount.

No space left on device with free space in df -h

Cause: the filesystem has no free inodes.

Fix: df -i to confirm, then find directories with many files: sudo du --inodes -x / | sort -n | tail.


Interview Checkpoints

L1: What is an inode, and what does it not contain?

Say first: the on-disk metadata record of a file (type, mode, owner, size, timestamps, block pointers); it does not contain the name, which lives in the directory.

Proof: stat file; ls -i shows the inode number next to the name.

Follow-up: Given that, what does mv change on the same filesystem?

L1: What is the difference between a hard link and a soft link?

Say first: a hard link is another name for the same inode; a symlink is a separate file that stores a path.

Proof: ls -li shows the same inode and a link count of 2 for hard links, and a different inode with -> for symlinks.

Follow-up: What happens to each when the original name is deleted?

L1: What is the difference between mtime, ctime and atime?

Say first: mtime changes with content, ctime with any inode change including permissions, atime with reads.

Proof: chmod changes only ctime in stat output.

Follow-up: Why can touch not set ctime?

L2: Find all broken symlinks under /etc.

Say first: find -xtype l matches links whose target does not exist.

Proof: sudo find /etc -xtype l

Follow-up: How do you show where one of them points? (readlink.)

L2: Switch a current symlink from release 1.0 to 2.0 without a gap.

Say first: replace the link atomically.

Proof: ln -sfn /opt/app/releases/2.0 /opt/app/current, or create a temporary link and mv -T it over the old one.

Follow-up: Why is mv -T the strictly atomic version? (rename() is atomic; ln -sf unlinks first.)

L2: Find every name for a file that has several hard links.

Say first: search by inode number on the same filesystem.

Proof: find / -xdev -samefile /path/to/file or find / -xdev -inum <n>

Follow-up: Why is -xdev required?

L3: df shows the disk 100 percent full, but du finds far less data.

Say first: look for deleted files still held open, then for data hidden under a mount point.

Proof: sudo lsof +L1 lists (deleted) files with their size; truncating /proc/<pid>/fd/<n> or restarting the process frees the space.

Follow-up: How can data be hidden under a mount point, and how do you check it? (Bind-mount / elsewhere and run du.)

L3: Writes fail with No space left on device, but df -h shows 40 percent used.

Say first: check inode usage.

Proof: df -i shows IUse% 100%; du --inodes finds the directory with millions of small files.

Follow-up: How do you avoid it on a new filesystem? (mkfs.ext4 -N or -i, or XFS with dynamic inode allocation.)

L4: What happens on disk when you run rm on a file?

Say first: rm calls unlink(), which removes the directory entry and decrements the inode's link count; the kernel frees the inode and blocks only when the count is 0 and no process has the file open.

Proof: strace -e trace=unlinkat rm f; lsof +L1 shows files with link count 0 still in use.

Don't say: "rm overwrites the data."


Captured on Rocky Linux 10.2 and Ubuntu 24.04.4 LTS (iximiuz Labs microVMs, kernel 6.1.167), 2026-09.