A deployment directory contains:
The service reads configuration through current, while the deployment tool changes which release that name selects.
Elsewhere, a backup tool wants two names to refer to exactly the same stored file without copying its bytes.
Both problems involve links, but they need different mechanisms:
A hard link refers directly to a file-system object; a symbolic link refers indirectly through a stored path.
That one distinction determines their inode numbers, link counts, cross-filesystem behavior, response to renames, and behavior after deletion.
Suppose report.txt refers to inode 8412.
A hard link named report.backup creates another directory entry that also refers to inode 8412:
Neither name is the original. Both are directory entries pointing at the same inode, and removing either one leaves the file reachable through the other.
A symbolic link named report.latest creates a new inode. Its contents are the path text report.txt:
A symlink stores a path as text, so resolution starts over from that string. If report.txt is renamed, the stored text still says report.txt and the link breaks.
The structures can be visualized together:
The hard-link path reaches the regular-file inode in one directory lookup. The symbolic-link path first reaches the symlink inode, reads its stored target string, and performs more path resolution.
The POSIX link() operation adds a new directory entry for an existing object:
The shell command ln provides the same basic operation:
Afterward, both names refer to the same inode:
A possible result is:
The inode number is the same, and the inode's link count has increased from one to two.
Creating the hard link does not duplicate the data. Both directory entries lead to the same metadata and logical byte sequence. Writing through either name changes the one underlying object:
The output is:
Metadata is shared too. Changing the mode, owner, or timestamps through one name changes inode metadata observed through the other. A hard link is not a lightweight copy with separate metadata.
People often call the first name the “original” and the later name the “link.” The file system does not preserve that hierarchy.
After:
the namespace contains two peer entries:
The inode does not store:
Removing report.txt does not promote report.backup or change what it means. It simply removes one entry and decrements the inode's link count.
report.backup continues to work because it directly names the same inode.
The word hard does not mean immutable. It describes a direct directory-entry reference rather than pathname-based indirection.
For ordinary files, the inode field st_nlink counts directory entries that directly refer to that inode.
When the count reaches zero, no pathname can newly resolve to the inode. The object can be reclaimed once the kernel also has no live open references to it.
An open file descriptor is not a hard link and does not increase st_nlink. The kernel tracks open references separately:
This distinction explains a deleted-but-open file:
The pathname is gone, but a process can continue using its already open descriptor. Storage cannot yet be reclaimed.
A hard link refers to an inode number within a particular file system. It cannot normally cross a file-system boundary.
The destination directory on file system B cannot contain a direct reference to an inode number owned by file system A. link() therefore fails, commonly with EXDEV.
Hard links also cannot refer to a nonexistent object. The existing inode must already be present when the new directory entry is created.
Ordinary users are generally not allowed to create hard links to directories. Unrestricted directory hard links could create cycles:
Cycles would break the simple tree model and make traversal, removal, and reference counting much harder to reason about. The file system manages special parent relationships itself instead of letting applications create arbitrary directory hard links.
Operating systems can impose additional restrictions on hard-link creation for security. For example, a user may be prevented from hard-linking another user's file even when directory access would otherwise permit adding an entry. The exact policy is system-specific.
Directory inodes also have link counts, but interpreting them as “number of user-created names” is misleading.
In a traditional Unix directory structure:
. entry refers to the directory itself... reference back to the parent.An otherwise empty, non-root directory therefore commonly starts with a link count of two. Each immediate subdirectory can increase the parent's count by one.
This does not mean projects contains only four entries or that four processes have it open.
Some modern file systems report directory link counts differently or use special values when exact counting would be expensive. Treat the traditional formula as an explanatory model, not a portable way to count subdirectories. Use directory enumeration when the entries themselves matter.
symlink() creates a new symbolic-link object containing target text:
The shell form is:
The resulting structure is:
The target text is stored exactly as a pathname string. The kernel does not need to resolve it when the link is created. This command can succeed even if releases/v42 does not yet exist.
Inspect the stored text without following it:
The output is:
When an operation follows current, the resolver interprets that stored path and continues toward the target.
A symbolic-link target can be relative or absolute.
This relative link:
stores:
The target is resolved relative to the directory containing the link, /srv/catalog, producing:
It is not resolved relative to whichever process happens to use the link.
This absolute link:
stores the complete absolute path. Following it restarts resolution from the process's root.
Relative links are often more portable when an entire directory tree can move:
Move catalog/ elsewhere as one tree, and the relative relationship can remain valid. An absolute target tied to /srv/catalog would still point at that old absolute location.
The correct target depends on the intended relationship. Relative links express a relationship within a tree; absolute links express a location from the namespace root.
The symbolic link and its target are separate objects.
The link has its own owner, timestamps, type, and link count. Its logical contents are the target path string.
Creating a symbolic link does not increment the target inode's hard-link count:
Most normal operations follow the link and act on the target. To inspect the link object itself:
lstat() reports metadata for the link rather than following the final component.readlink() returns the stored target text.unlink() removes the link's directory entry rather than removing the target.stat() normally follows the final symbolic link and reports the target's metadata.
Choosing the correct operation determines whether code observes the indirection object or the object reached through it.
A symbolic link stores a pathname, not a direct inode number. Its target can resolve into another mounted file system:
The resolver crosses the mount point in the usual way. The symbolic link itself remains an object in file system A.
This does not make the link independent of the namespace. The absolute target must make sense in the process's view. A container, restricted environment, or differently mounted host can interpret the same absolute target differently or fail to find it.
A relative target can also cross into another file system if following its components reaches a mount point.
Because the target need not exist during creation, a symbolic link can be dangling:
The link itself exists. lstat("current") and readlink("current") can succeed. An operation that follows it to the target fails, commonly with ENOENT.
Shell tests expose the distinction:
This is useful for links that intentionally refer to removable devices or targets created later. It is also a common source of confusion when ls -l shows a name but cat reports “No such file or directory.”
The failure can also occur because an intermediate component of the stored target is missing, not only because its final component is absent.
A hard link stays attached to an inode. A symbolic link repeats a pathname lookup each time it is followed.
Suppose:
Remove report.txt:
Now create a new file under the old name:
The symbolic link follows the name and now reaches the replacement object. The hard link remains attached to the old object.
This difference is central:
Hard links preserve object identity; symbolic links preserve a pathname relationship.
It is why symbolic links work well for names such as current, where the purpose is to select whichever release a path currently denotes. It is also why a symbolic link is not a stable handle to one inode.
The final component matters when a removal operation is applied.
Given:
this command:
removes the current directory entry and decrements the symbolic link inode's link count. It does not recursively follow the stored target and remove releases/v42.
The target remains:
This is different from opening current/file.txt, where path resolution follows the link before opening the file inside the target directory.
Command-line tools can have special rules for trailing slashes and recursive options, so destructive commands should use clearly resolved operands. At the system-call model, unlinking a symbolic-link entry acts on the link rather than its target.
Symbolic links can form cycles:
Following a would otherwise continue forever. The kernel limits symbolic-link expansions during one path-resolution operation. Exceeding that limit fails with ELOOP.
A link does not have to point directly back to itself to form a cycle. The loop can span directories and mix relative and absolute targets.
Recursive tools face a related choice. If a backup or search tool follows directory symlinks, it must avoid revisiting the same underlying directories indefinitely. Many tools do not follow directory symlinks by default for this reason.
The symlink objects in a loop remain valid objects that lstat() and readlink() can inspect. It is following the cycle that fails.
All three mechanisms can produce another pathname, but their structures differ.
| Property | Copy | Hard link | Symbolic link |
|---|---|---|---|
| Resulting inode | New inode | Same inode | New symlink inode |
| File data | Independent copied bytes | Shared underlying bytes | Stored target pathname |
| Metadata | Independent | Shared inode metadata | Link has its own metadata |
| Target must exist at creation | Source must exist | Yes | No |
| Can cross file systems | Yes, by copying data | No | Yes |
| Can normally be used with directories | Yes, through a recursive copy | Not through a user-created hard link | Yes |
| Survives removal of the first name | Yes | Yes | No, if that was its target path |
| Follows a replacement at the target name | No | No | Yes |
A copy is appropriate when the new file should evolve independently.
A hard link is appropriate when two names should denote exactly one object and shared modifications are intentional.
A symbolic link is appropriate when one namespace location should redirect to another pathname.
Choosing based only on apparent disk savings misses the much more important identity and lifetime semantics.
Loading simulation...
A deployment can keep immutable release directories:
The service uses /srv/catalog/current/config.json. Deployment changes current to select a release.
A safer switch creates a new link under a temporary name and renames it over current:
On GNU/Linux, mv -T treats current as the destination entry rather than as a directory to move into. A same-file-system rename provides atomic namespace replacement: readers resolve either the old link or the new one.
This does not by itself make newly written release contents durable against a sudden crash. Namespace atomicity and storage durability are separate properties.
Package managers and snapshot-like tools can use hard links when multiple names should share one unchanged inode. Modifying through any name modifies the shared object, so hard-link-based deduplication requires a clear immutability or copy-before-write policy at the application level.
An archive tool that encounters two paths with the same (device, inode) pair should decide whether to preserve their hard-link relationship. Blindly storing both as unrelated copies changes link counts and can consume more space after restoration.
For symbolic links, an archive should normally store the link's target text rather than copying the target contents unless dereferencing was explicitly requested.
Symbolic links make namespaces flexible, but they also make check-then-use code dangerous.
Consider a privileged service:
The check and open can reach different objects. A textual prefix such as /srv/uploads/ also does not prove the resolved target remains below that directory.
Useful defenses include:
O_NOFOLLOW can reject a symbolic link in the final component of an open() on systems that support it:
It does not automatically prevent symbolic links in intermediate components. Protecting an entire walk requires a constrained resolver or component-by-component strategy appropriate to the platform and threat model.
Security rules can also restrict hard links. Without such restrictions, a user could create another name for a sensitive object and keep that name after the object's expected pathname is removed. Modern systems commonly limit which files an unprivileged user may hard-link.
Create one regular file, one hard link, and one relative symbolic link:
Inspect the directory:
A simplified result is:
original.txt and hard.txt share inode 8412 and report link count two. symbolic.txt has inode 9231; its displayed size is the length of the stored target text in this example.
Compare metadata explicitly:
GNU stat inspects the link object by default and follows it with --dereference. The dereferenced symbolic path reaches the same target inode as original.txt, but the link object itself has a different inode.
Show that all three names currently reach the same bytes:
Remove the original name:
The hard link still works:
The symbolic link is now dangling:
Finally, create a replacement under the old target name:
The symbolic link prints version two because it resolves original.txt again. The hard link still shows the old inode's contents, including version one and shared update.
The following program uses lstat() to inspect the final pathname object. If that object is a symbolic link, it uses readlink() to show the stored target and stat() to inspect the currently resolved target.
Compile and run:
For the symbolic link, the first identity belongs to the symlink inode and the second belongs to the target inode. For the hard link, the program reports one ordinary final object; its identity matches the original inode.
readlink() returns a byte count and does not append a null terminator. The program reserves one byte and adds the terminator before printing the target as a C string.
The fixed target buffer is sufficient for Linux's normal symbolic-link target limit. A portable library intended for systems with different limits should resize dynamically when necessary.
A hard link is a directory entry that refers directly to an existing inode. All hard-link names are equal peers, share file contents and inode metadata, and contribute to the inode's link count. Hard links cannot normally cross file systems or target directories.
A symbolic link is a separate inode containing pathname text. Its target can be relative, absolute, nonexistent, a directory, or located on another file system. Following the link performs pathname resolution, while lstat() and readlink() inspect the link itself.
Removing one hard link leaves the inode reachable through its other names. Removing a symbolic link leaves its target untouched. If a symbolic link's target name is removed, the link dangles; if a new object later appears at that name, the same link reaches the replacement.
The central distinction is:
A hard link preserves identity by naming the same inode; a symbolic link preserves indirection by storing a path.
5 quizzes