Linux File System Internals: Complete Guide for Developers
Linux File System Internals: Complete Guide for Developers
What is this about?
In Linux, every file has two parts: the data (actual content) and the metadata (who owns it, how big it is, permissions, etc.). The metadata lives in a special structure called an inode. A directory is just a table that maps filenames to inode numbers. This page explains how all of that works.
Key Terms
Think of an inode as a ID card for a file. It stores everything about the file except its name. The name is stored separately in a directory.
An inode contains:
What the inode does NOT contain: the filename. The filename is stored only in the directory.
Below is a simplified view of how /etc/passwd is stored on disk.
| Name | Inode # |
|---|---|
| . | 2 |
| .. | 2 |
| etc | 7 |
| tmp | 5 |
| Name | Inode # |
|---|---|
| passwd | 6422 |
| group | 282 |
| Field | Value |
|---|---|
| type | regular file |
| perm | rw-r–r– |
| UID | root |
| links | 1 |
| data ptr | → block 0x3A |
A hard link means two different filenames pointing to the same inode. There is no original or copy — both names are equal owners of the same data.
When you run rm abc, the link count drops from 2 to 1. The file data is only deleted when the link count reaches 0.
Code Examples
#include <stdio.h>
#include <sys/stat.h>
int main() {
struct stat sb;
if (stat("/etc/passwd", &sb) == -1) {
perror("stat");
return 1;
}
printf("Inode number : %lu\n", (unsigned long)sb.st_ino);
printf("Hard link count: %lu\n", (unsigned long)sb.st_nlink);
printf("File size : %ld bytes\n", (long)sb.st_size);
printf("Owner UID : %d\n", sb.st_uid);
return 0;
}
Compile and run:
gcc -o stat_demo stat_demo.c
./stat_demo
Sample output:
Inode number : 6422
Hard link count: 1
File size : 2341 bytes
Owner UID : 0
#include <stdio.h>
#include <sys/stat.h>
int same_file(const char *path1, const char *path2) {
struct stat s1, s2;
if (stat(path1, &s1) == -1 || stat(path2, &s2) == -1)
return -1;
/* Same inode AND same device = same file */
return (s1.st_ino == s2.st_ino && s1.st_dev == s2.st_dev);
}
int main() {
/* Create hard link first: ln /tmp/a /tmp/b */
const char *f1 = "/tmp/a";
const char *f2 = "/tmp/b";
int result = same_file(f1, f2);
if (result == -1)
perror("stat failed");
else if (result)
printf("%s and %s point to the SAME inode\n", f1, f2);
else
printf("%s and %s are DIFFERENT files\n", f1, f2);
return 0;
}
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <string.h>
int main() {
DIR *dp = opendir(".");
struct dirent *entry;
struct stat sb;
char path[512];
if (!dp) { perror("opendir"); return 1; }
printf("%-30s %s\n", "Filename", "Inode#");
printf("%-30s %s\n", "--------", "------");
while ((entry = readdir(dp)) != NULL) {
snprintf(path, sizeof(path), "./%s", entry->d_name);
if (stat(path, &sb) == 0)
printf("%-30s %lu\n", entry->d_name, (unsigned long)sb.st_ino);
}
closedir(dp);
return 0;
}
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
void print_link_count(const char *path) {
struct stat sb;
stat(path, &sb);
printf("Link count of '%s': %lu\n", path, (unsigned long)sb.st_nlink);
}
int main() {
/* Create a file */
int fd = open("/tmp/demo_file", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, "hello", 5);
close(fd);
printf("--- After creating /tmp/demo_file ---\n");
print_link_count("/tmp/demo_file");
/* Create a hard link */
link("/tmp/demo_file", "/tmp/demo_link");
printf("--- After: link(demo_file, demo_link) ---\n");
print_link_count("/tmp/demo_file");
print_link_count("/tmp/demo_link");
/* Remove one link */
unlink("/tmp/demo_link");
printf("--- After unlink(demo_link) ---\n");
print_link_count("/tmp/demo_file");
/* Cleanup */
unlink("/tmp/demo_file");
return 0;
}
Output:
--- After creating /tmp/demo_file ---
Link count of '/tmp/demo_file': 1
--- After: link(demo_file, demo_link) ---
Link count of '/tmp/demo_file': 2
Link count of '/tmp/demo_link': 2
--- After unlink(demo_link) ---
Link count of '/tmp/demo_file': 1
Interview Questions
An inode is a data structure in the filesystem that stores all metadata about a file except its name. It stores file type, permissions, owner UID/GID, size, timestamps, link count, and pointers to data blocks.
Because a file can have multiple names (hard links). The filename lives in the directory entry. The inode is the file itself; directories just map names to inode numbers.
The directory entry is removed, the inode’s link count is decremented by 1. The actual data blocks are freed only when the link count reaches 0 AND there are no open file descriptors for that file.
No. Inode numbers are unique only within a single filesystem. A hard link in filesystem A cannot refer to an inode in filesystem B because the numbers would overlap.
Inode 0 means “unused entry” in a directory table. Inode 1 is reserved for recording bad blocks. So the kernel reserves inode 2 for the root directory as the well-known starting point for pathname resolution.
The directory entry is removed and the link count drops to 0, but the file is NOT immediately deleted. The kernel keeps the inode and data blocks alive because there are still open file descriptors. The file is deleted only when both processes close their file descriptors.
st_nlink is the hard link count. It is greater than 1 when more than one directory entry (filename) refers to the same inode. For directories it is at least 2 (the directory itself and its “.” entry).
Call stat() on both paths and compare st_ino (inode number) and st_dev (device number). If both fields are equal, both paths point to the same inode on the same device.
Continue Learning
Next: Symbolic Links — the flexible alternative to hard links
