143 / 163 · C11 · 8 min
Interlude: Files and Directories
This chapter presents an original view of how an operating system virtualizes persistent devices as two complementary abstractions—files and directories—and how the classic UNIX interface of open, read, write and unlink hides inode numbers behind human-readable path names.
In this lesson
Files: Persistent Byte Streams Seen by the Kernel
To the kernel every file is merely a randomly accessible sequence of bytes whose true identity is an integer inode number. User programs almost never mention that number; they rely on directories to translate convenient names into it. The file system never guesses whether the bytes encode a picture, source code or music; it only promises that the data survive power loss.
How Directories Assemble the Whole Name Tree
The contents of a directory are a plain lookup table: a user-visible name on the left and an inode number on the right. Placing directories inside other directories grows a tree of any depth whose root is always a lone slash. An absolute path is simply the sequence of names obtained by walking that table from the root, so two files both named report.txt can coexist as long as they live in different directories.
The Small Integer Returned by open Is the Real Capability
Passing O_CREAT to open creates the file and yields a file descriptor that is private to the calling process. Every later read, write or lseek refers solely to that integer. The descriptor vanishes when the process exits or close is called. The operation that actually removes a name is unlink; it merely decrements a link count, and the data disappear only after the count hits zero and no process still holds the file open.
Mental Simplicity Gained from Uniform Naming
Once almost everything—regular files, devices, pipes and even some process information—appears under a single directory tree, a programmer only has to remember one set of open/read/write rules. The “everything is a file” convention hides low-level differences and lets scripts and tools treat wildly different objects in exactly the same way.
Pitfalls
- Treating the suffix after the last dot as a kernel-enforced type check rather than a mere convention
- Believing a file descriptor can be passed around like an ordinary integer to another process
- Expecting unlink to wipe disk blocks immediately, forgetting hard links and still-open descriptors
Run an example
Minimum C11 · complete program · Download .c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void) {
const char *path = "ostep_ch39_demo.tmp";
int fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
const char msg[] = "hello from files\n";
ssize_t n = write(fd, msg, sizeof(msg) - 1);
if (n != (ssize_t)(sizeof(msg) - 1)) {
perror("write");
close(fd);
unlink(path);
return 1;
}
if (lseek(fd, 0, SEEK_SET) < 0) {
perror("lseek");
close(fd);
unlink(path);
return 1;
}
char buf[32];
ssize_t r = read(fd, buf, sizeof(buf) - 1);
if (r < 0) {
perror("read");
close(fd);
unlink(path);
return 1;
}
buf[r] = '\0';
printf("wrote %zd bytes, read back: %s", n, buf);
close(fd);
if (unlink(path) != 0) {
perror("unlink");
return 1;
}
printf("unlinked successfully\n");
return 0;
}
Compile locally
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-39-files-directories.c -o example && ./exampleExpected result
wrote 17 bytes, read back: hello from files
unlinked successfully
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
If two distinct directory entries point at the same inode, does the file’s content vanish immediately after one unlink? Explain the reasoning.
Show a reference answer
No, it does not vanish at once. unlink only removes one name and decrements the link count. As long as the count stays above zero or any process still holds an open file descriptor, the data blocks remain and the other name can still reach the same content.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.