Working with Files · about 40 min

Inodes, hard links and symbolic links

Why this matters

Links explain a whole family of puzzles: why deleting a huge log file did not free any disk space, why /bin/ls and /usr/bin/ls are the same file, how servers switch to a new software release in one instant step, and what "file names" really are. They also finish the picture of the filesystem you started building in module 2.

Names and inodes, again

Recall the model: a directory is a table of name → inode number; the inode holds the file's metadata and points to its data. The name is not part of the file. That has a striking consequence: nothing stops two directory entries from naming the same inode.

 data/                                inode 16500
 ┌──────────────┬───────┐            ┌──────────────────────┐     ┌──────────────────────┐
 │ original.txt │ 16500 ├──────┬────▶│ regular file, 0644    │────▶│ the original content │
 │ twin.txt     │ 16500 ├──────┘     │ links: 2              │     └──────────────────────┘
 │ shortcut.txt │ 16501 ├──┐         └──────────────────────┘
 └──────────────┴───────┘  │         inode 16501
                           └────────▶┌──────────────────────┐
                                     │ symbolic link         │  content: "original.txt"
                                     └──────────────────────┘

ln TARGET NAME creates a hard link: a new directory entry for an existing inode.

Open the shell for this lesson and try:

$ cd data
$ ls -li
16500 -rw-r--r--. 1 learner learner 21 Nov  1 15:00 original.txt
$ ln original.txt twin.txt
$ ls -li
16500 -rw-r--r--. 2 learner learner 21 Nov  1 15:00 original.txt
16500 -rw-r--r--. 2 learner learner 21 Nov  1 15:00 twin.txt

Same inode number, and the link count column went from 1 to 2. Both names are equal; there is no original and no copy. Change the content through one name and you see it through the other, because there is only one file:

$ echo more >> twin.txt
$ cat original.txt
the original content
more

Deleting is unlinking

This is why the system call behind rm is called unlink. It removes one name and decrements the link count. The kernel frees the inode and its data blocks only when both:

  1. the link count is 0, and
  2. no process still has the file open.

The second rule explains a classic puzzle: a disk is full because of a huge log file, you rm it, and df shows no space freed. The program writing the log still has it open, so the data lives on (nameless) until that program closes it or exits. Restarting the program frees the space.

ln -s TARGET NAME creates a symbolic link (symlink, soft link): a small file of its own, with its own inode, whose content is just a path. Whenever a program uses the link, the kernel reads that path and continues resolving from there.

$ ln -s original.txt shortcut.txt
$ ls -li
16500 -rw-r--r--. 2 learner learner 26 Nov  1 15:00 original.txt
16501 lrwxrwxrwx. 1 learner learner 12 Nov  1 15:02 shortcut.txt -> original.txt
16500 -rw-r--r--. 2 learner learner 26 Nov  1 15:00 twin.txt
$ cat shortcut.txt
the original content
more
$ readlink shortcut.txt
original.txt

Note the l type, the arrow, and the size: 12 bytes, the length of the text original.txt. The permissions of a symlink are always shown as rwxrwxrwx and do not matter; the target's permissions are what count.

Symlinks can do what hard links cannot: point at directories and across filesystems. This system uses them itself:

$ ls -l /bin
lrwxrwxrwx. 1 root root 7 Jan  1  2025 /bin -> usr/bin

The target text is interpreted relative to the directory containing the link, not to your current directory when you created it. This is the most common symlink mistake:

$ cd /work
$ ln -s data/original.txt data/oops.txt
$ cat data/oops.txt
cat: data/oops.txt: No such file or directory
$ readlink data/oops.txt
data/original.txt

The link lives in data/ and points to data/original.txt relative to data/, which means /work/data/data/original.txt. Either cd into the link's directory first, or use an absolute target, or use ln -sr (GNU: compute the relative path for you).

Since a symlink is only a path, its target can disappear or never exist. The link stays, now dangling (broken). ls -l still shows it; using it fails:

$ ls -l app/current
lrwxrwxrwx. 1 learner learner 13 Nov  1 15:05 app/current -> releases/v1.2
$ cat app/current/VERSION
cat: app/current/VERSION: No such file or directory

With colors on, ls shows dangling links in red. test -e (later module) is false for a dangling link because it follows the link; test -L checks for the link itself.

ln -s refuses if the name exists (File exists). -f removes the old name first. But if the existing name is a symlink to a directory, ln treats it as that directory and creates the new link inside it; -n says "treat a symlink to a directory as a plain name". So the idiom to repoint a link is:

$ ln -sfn releases/v1.4 app/current

This is how many deployment tools switch releases: the new version is unpacked beside the old, then one link is repointed. (For a truly atomic switch, tools create the new link under a temporary name and mv -T it over the old one, because rename is atomic.)

Hard or symbolic?

Hard linkSymbolic link
What it isanother name for the same inodea small file containing a path
Own inodenoyes
Directoriesnoyes
Across filesystemsnoyes
Target deletedstill works (it is the file)dangling
Shown by ls -llink count above 1l type and ->

In everyday work you will create symlinks far more often. Hard links appear in backup tools (which hard-link unchanged files between snapshots to save space) and in the link count of ls -l.

Common mistakes

Recap

Going further: stat -c '%h %n' /usr/bin/* | sort -rn | head shows the programs with the most hard links. Then try ls -l /etc/alternatives | head: Debian's system of symlinks for choosing default programs (the editor, pager and awk commands are chosen this way).

Exercises

Exercise 1 · quiz

Two names, one file

You run these commands:

echo hello > a.txt
ln a.txt b.txt
rm a.txt
cat b.txt

What happens at the last line?

Answer
  • It prints hello.

ln a.txt b.txt creates a hard link: a second directory entry pointing at the same inode. The two names are completely equal; neither is "the original". rm a.txt removes one name and drops the link count from 2 to 1. The inode and its data are freed only when the count reaches 0 (and no process has the file open), so b.txt still works.

Exercise 4 · task in a shell

Fix a dangling link

/work/app holds a deployment layout that many real servers use:

app/
├── releases/
│   ├── v1.3/
│   └── v1.4/
└── current -> releases/v1.2     (broken: v1.2 was deleted)

app/current is a symbolic link that should always point at the release in use. It is dangling. Make app/current point to releases/v1.4 instead (a relative target, exactly releases/v1.4), without creating anything inside the release directories.

Hint: ln -s refuses to replace an existing name unless you add -f, and when the existing link points at a directory you also need -n so ln does not create the link inside it. Here the old link is broken, so -sf would do, but -sfn is the habit to build.

Hint 1

ls -l app shows where current points. The target text is relative to the link's own directory.

Hint 2

From inside app: ln -sfn releases/v1.4 current

Reference solution
cd app
ln -sfn releases/v1.4 current