-
Notifications
You must be signed in to change notification settings - Fork 69
File Systems
This page teaches you how MentOS manages files and directories on disk.
Important: All code references point to the actual MentOS source code. Explore these files in kernel/src/fs/ and kernel/inc/fs/ to understand how MentOS implements file systems.
Scenario: You write a file myfile.txt to disk. Then you turn off the computer. When you turn it back on, the file is still there.
Question: How does the computer remember where myfile.txt is? What's the structure on disk?
Answer: A filesystem - a way to organize data on disk into files and folders.
MentOS uses three layers (like a sandwich):
┌─────────────────────────────────────┐
│ User Programs (ring 3) │
│ printf("Hello"); │
│ read(fd, buf, 10); │
│ write(fd, "data", 4); │
├─────────────────────────────────────┤
│ VFS Layer (kernel) │
│ "I don't care HOW you store files" │
│ Just provide: open, read, write │
│ Works with ANY filesystem │
├─────────────────────────────────────┤
│ Filesystem Drivers │
│ EXT2: "Here's how WE do it" │
│ ProcFS: "And here's how WE do it" │
└─────────────────────────────────────┘
↓
Hard Disk
Key Idea: The VFS (Virtual File System) is a plug-and-play interface. Add a new filesystem? Just implement the VFS interface!
MentOS mounts exactly two filesystems at boot, both from kmain():
| Mount point | Filesystem | Backing store | Set up by |
|---|---|---|---|
/ |
EXT2 | the ATA disk /dev/hda
|
vfs_mount("ext2", "/", "/dev/hda") |
/proc |
procfs | nothing — generated on demand |
procfs_module_init() + mount |
The root image is built by the filesystem CMake target from the contents of the filesystem/
directory: mke2fs ... -b 4096 ... rootfs.img 32M, i.e. a 32 MiB EXT2 image with 4 KiB
blocks, handed to QEMU as an IDE drive. After mounting, fhs_initialize() creates the
standard Filesystem Hierarchy Standard directories.
procfs is a separate filesystem driver (kernel/src/fs/procfs.c), not a corner of EXT2.
Nothing under /proc is stored on disk.
The VFS is a layer of abstraction that says: "I don't care if you're using EXT2, FAT32, or a USB drive. You all must support the same interface."
Implementation: kernel/src/fs/vfs.c and kernel/inc/fs/vfs.h
Think of these as "handles" to files and directories. The three concepts below — file descriptor, inode, directory entry — are the standard Unix vocabulary, and they are the right way to reason about file I/O on any Unix-like system.
How MentOS maps onto that vocabulary. Linux gives each concept its own kernel structure:
struct inode(the on-disk object),struct dentry(a name in the tree) andstruct file(one open instance, with its own position). MentOS folds all three into a singlevfs_file_t(kernel/inc/fs/vfs_types.h), which carries the name, the inode numberino, the mode/uid/gid, and the read offsetf_posat once. Only the per-process descriptor is a separate structure (vfs_file_descriptor_t).So: learn the concepts as described below, but do not expect to find
struct inodeorstruct dentryin the MentOS tree — and be aware that becausef_poslives inside the sharedvfs_file_t, MentOS does not cleanly separate "the file" from "this open of the file".
When you do: fd = open("/home/user/file.txt", O_RDONLY)
The kernel creates a vfs_file_descriptor_t:
┌─────────────────────────────────┐
│ File Descriptor (fd = 3) │
├─────────────────────────────────┤
│ • Flags mask: O_RDONLY │
│ • Points to: vfs_file_t │
└─────────────────────────────────┘
↓
vfs_file_t (name, flags, f_pos, operations)
↓
Filesystem driver (EXT2 / procfs)
Real analogy: Like a bookmark in a book. The fd tells you WHERE YOU ARE in the file (position = page 5).
Two properties that matter in MentOS:
- A file descriptor is a per-process index, not a global identifier. Each
task_structcarries its ownfd_listarray, so fd 3 in one process and fd 3 in another are unrelated. This is whyfork()can duplicate descriptors (vfs_dup_task) without the two processes fighting over numbers. - The table is small and fixed:
MAX_OPEN_FDis 16 (kernel/inc/fs/vfs.h). There is no growable descriptor table, so a program that leaks descriptors will hit-EMFILEquickly. Remember that 0, 1 and 2 are already taken.
Every file/directory has an INODE that stores:
• File type (regular file, directory, symlink, device)
• Permissions (rwxrwxrwx)
• Owner (user ID, group ID)
• Size in bytes
• Timestamps (created, modified, accessed)
• WHERE ON DISK is the file data?
Inode does NOT store the filename!
Real analogy: Like a library card catalog entry. It describes WHAT a file is, but not its name or location.
Maps FILENAME to INODE:
"file.txt" → inode #12345
"folder/" → inode #12346
".." → inode #12340 (parent)
Real analogy: Like page 5 in the phone book: "Smith, John" → phone number "555-1234"
fd = open("/home/user/file.txt", O_RDONLY)
1. PARSE PATH: /home/user/file.txt
Start at root inode (inode #2)
│
├─ Directory lookup: "home"
│ └─ Read root inode → find "home" entry → get inode #1000
│
├─ Directory lookup: "user"
│ └─ Read inode #1000 → find "user" entry → get inode #1001
│
└─ Directory lookup: "file.txt"
└─ Read inode #1001 → find "file.txt" entry → get inode #1002
2. GET INODE METADATA from disk (inode #1002)
• Size: 150 bytes
• Permissions: rw-r--r--
• Owner: uid 1000
• Data blocks on disk: 256, 257, 258
3. CREATE FILE DESCRIPTOR
allocate fd = 3 (first free slot)
fd[3].position = 0
fd[3].inode = inode #1002
fd[3].flags = O_RDONLY
4. RETURN 3 to user
Result: When user calls read(3, buf, 10), kernel knows exactly which file to read!
ssize_t n = read(3, buf, 10); // Read 10 bytes from fd #31. Kernel looks up fd #3
→ Gets inode #1002
→ Knows position = 0
2. Kernel calculates which DISK BLOCK to read
• File position = 0
• Block size = 4096 bytes
• Need byte 0-9
→ Those are in block #256 on disk
3. Kernel reads block #256 from disk
→ Gets 4096 bytes
4. Kernel copies bytes 0-9 to user buffer
buf = "Hello file" ← user gets this
5. Kernel updates position
fd[3].position = 10
6. RETURN 10 (bytes read)
MentOS Implementation:
- Path parsing:
kernel/src/fs/namei.c(name to inode lookup) - File descriptor management:
kernel/src/fs/vfs.c - Read/write operations:
kernel/src/fs/read_write.candkernel/src/fs/vfs.c
EXT2 is a specific filesystem implementation. It says: "Here's HOW I organize data on this disk."
Implementation: kernel/src/fs/ext2.c
In MentOS the root image is created with a 4 KiB block size, so where the diagrams below say "block", read 4096 bytes.
Think of a disk like a filing cabinet:
┌─────────────────────────────────────────┐
│ Boot Sector (1 block) [How to boot OS] │
├─────────────────────────────────────────┤
│ Superblock (1 block) [Disk metadata] │
├─────────────────────────────────────────┤
│ Inode Bitmap (several blocks) │
│ [Which inodes are used? (bitmap)] │
├─────────────────────────────────────────┤
│ Block Bitmap (several blocks) │
│ [Which disk blocks are used?] │
├─────────────────────────────────────────┤
│ Inode Table (many blocks) │
│ [Inode #1, #2, #3, ... metadata] │
├─────────────────────────────────────────┤
│ Data Blocks (bulk of disk) │
│ [Actual file contents] │
└─────────────────────────────────────────┘
On disk, each inode is 128 bytes and contains:
struct ext2_inode {
uint16_t i_mode; // File type + permissions
uint16_t i_uid; // Owner user ID
uint32_t i_size; // File size in bytes
uint32_t i_atime; // Last access time
uint32_t i_ctime; // Creation time
uint32_t i_mtime; // Modification time
uint16_t i_gid; // Owner group ID
uint16_t i_links_count; // Hard link count
uint32_t i_blocks; // Disk blocks used
uint32_t i_flags; // Special flags
// MOST IMPORTANT: Where is the file data on disk?
uint32_t i_block[15]; // "Block pointers" (see next section)
};This is the clever part!
Each inode has 15 block pointers:
-
i_block[0-11]= Direct pointers (point directly to data blocks) -
i_block[12]= Indirect pointer (points to block of pointers) -
i_block[13]= Double indirect -
i_block[14]= Triple indirect
Example: Reading a small file (5KB)
File: myfile.txt (5KB = 1.25 blocks, rounded to 2 blocks)
Inode #42:
i_size = 5120
i_block[0] = 1000 ← Block 1000 on disk
i_block[1] = 1001 ← Block 1001 on disk
i_block[2-14] = 0 ← Unused
When reading bytes 0-5119:
1. Check inode #42
2. See that data is in blocks 1000-1001
3. Read block 1000 (4096 bytes) → get bytes 0-4095
4. Read block 1001 (1024 bytes) → get bytes 4096-5119
5. Done!
Example: Reading a huge file (5MB)
File: bigfile.bin (5MB)
Inode #50:
i_size = 5242880
i_block[0-11] = 2000, 2001, 2002, ... (12 direct blocks = 48KB)
i_block[12] = 5000 ← Indirect block!
When we need block #1000 in the file:
1. Calculate: block 1000 is beyond direct pointers
2. Read "indirect block" #5000 from disk
3. It contains: [3000, 3001, 3002, 3003, ...]
Each entry is a block pointer!
4. From that, get pointer to actual block
5. Read that block
6. Done!
Why this design?
- Small files: Fast! Use direct pointers
- Large files: Works! Use indirect pointers
- Huge files: Still works! Use double/triple indirect
Directories are just files that store directory entries:
Directory /home/user/ (stored as a file on disk)
Bytes: Data
────────────────────────────────────────────
0-3: 12345 (inode of "desktop")
4-255: "desktop\0" + padding
256-259: 12346 (inode of "documents")
260-383: "documents\0" + padding
384-387: 12347 (inode of "file.txt")
388-415: "file.txt\0" + padding
When kernel does ls /home/user/:
- Open inode of
/home/user/(it's a directory) - Read its data blocks (which contain directory entries)
- Parse entries: filename → inode → lookup inode metadata
- Print:
file.txt (12347, 5KB, rw-r--r--, Jan 15 14:30)
Location: kernel/src/fs/procfs.c
ProcFS is not on disk - it's generated on-the-fly by the kernel. It is mounted at
/proc by kmain() during boot (not by /bin/init), and it is a distinct filesystem type
registered with the VFS — it shares no code and no storage with the EXT2 root.
Examples of procfs entries in MentOS:
- `/proc/uptime`, `/proc/version`, `/proc/meminfo`, `/proc/stat` (see `kernel/src/io/proc_system.c`)
- `/proc/ipc/msg`, `/proc/ipc/sem`, `/proc/ipc/shm` (see `kernel/src/io/proc_ipc.c`)
- `/proc/feedback` (scheduler feedback, see `kernel/src/io/proc_feedback.c`)
- `/proc/video` (video info, see `kernel/src/io/proc_video.c`)
- `/proc/<pid>/cmdline`, `/proc/<pid>/stat` (see `kernel/src/io/proc_running.c`)
Example: When user reads /proc/1/stat:
- Kernel sees: "This is a procfs file"
- procfs routes the read to the proc-running callbacks
-
__procr_do_stat()formats the process data - User reads the formatted stat line
Why? Provides Unix-style interface to kernel data without storing anything on disk!
On a conventional Unix system, the first process gets descriptors 0/1/2 backed by a terminal device, and the shell later manages the controlling terminal and job control through it.
MentOS does something simpler. process_create_init() in kernel/src/process/process.c
calls vfs_open("/proc/video", ...) three times — once O_RDONLY, twice O_WRONLY — to
produce descriptors 0, 1 and 2:
vfs_file_t *vfs_stdin = vfs_open("/proc/video", O_RDONLY, 0);
vfs_file_t *vfs_stdout = vfs_open("/proc/video", O_WRONLY, 0);
vfs_file_t *vfs_stderr = vfs_open("/proc/video", O_WRONLY, 0);Every later process inherits these through fork(). Reading and writing fds 0/1/2 behaves the
way you expect, but there is no tty layer, no /dev/console, and no controlling-terminal
concept behind them — so do not go looking for one.
Location: kernel/src/fs/pipe.c
A pipe is a special "file" that two processes use to communicate:
cat bigfile.txt | grep "error"cat process grep process
│ │
├─ Write to pipe ─────┤
│ (data flows) ─────┤
├─ "line1" │ Read from pipe
├─ "error line" │ ← grep gets this
├─ "line3" │
└─ EOF │ Done reading
Implementation: kernel/src/fs/pipe.c - Creates a ring buffer and exposes it through the
ordinary VFS file operations.
Why a pipe lives in the filesystem layer. A pipe is two things at once, and it is worth seeing both views:
- As a communication abstraction, it is IPC: a unidirectional byte stream between processes, with blocking semantics and EOF when the write end closes. That view belongs on the IPC page.
-
As an implementation, it is a file.
pipe()returns two file descriptors, andread()andwrite()on them go through exactly the samevfs_read/vfs_writedispatch as a file on disk — which is why the code lives inkernel/src/fs/rather thankernel/src/ipc/.
That duality is not an accident of MentOS; it is the Unix "everything is a file" design.
Named pipes (FIFOs) are not complete.
kernel/src/fs/pipe.ccontains FIFO-related scaffolding — it setsS_IFIFOin file flags and reportsS_IFIFOfromstat— but no creation path was found:mkfifo()is not provided, andsys_mknodis not among the handlers registered bysyscall_init(). Treat anonymouspipe()as the supported mechanism, and named pipes as unfinished.
fd = open("/home/user/newfile.txt", O_CREAT | O_WRONLY, 0644);Behind the scenes:
- Parse path → navigate to
/home/user/directory inode - Call EXT2 driver: "Create new inode"
- Inode allocated → added to inode table on disk
- Add directory entry: "newfile.txt" → inode #12345
- Write inode and directory entry changes back to disk
- Return fd pointing to new inode
unlink("/home/user/oldfile.txt");Behind the scenes:
- Parse path → find inode #54321
- Remove directory entry "oldfile.txt"
- Mark inode as free in inode bitmap
- If no other hard links exist, free its data blocks
- Mark data blocks as free in block bitmap
- Write changes back to disk
mkdir("/home/user/newfolder", 0755);Behind the scenes:
- Create new inode (type=directory)
- Create directory entries within it:
- "." → self inode
- ".." → parent inode
- Add entry in parent directory
- Write to disk
Goal: Understand the MentOS filesystem structure.
Steps:
-
Boot MentOS:
make qemu
-
Explore directories:
/bin/ls / # Root directory /bin/ls /bin # Binaries /bin/ls /tmp # Temp files /bin/ls -l / # Long listing with metadata
-
Check file properties:
/bin/stat /bin/ls # File metadata /bin/stat / # Directory info
-
What do you notice?
- File sizes
- Timestamps
- Permissions (755, 644, etc.)
- Hard link counts
Goal: Write a program that creates, reads, and modifies files.
Program - userspace/bin/file_demo.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
printf("=== File Operations Demo ===\n\n");
// 1. CREATE and WRITE
printf("1. Creating file...\n");
int fd = open("/tmp/test.txt", O_CREAT | O_WRONLY, 0644);
if (fd < 0) {
perror("open");
return 1;
}
const char *text = "Hello from MentOS!\n";
write(fd, text, strlen(text));
close(fd);
printf(" File created: /tmp/test.txt\n");
// 2. READ
printf("\n2. Reading file...\n");
fd = open("/tmp/test.txt", O_RDONLY);
char buffer[100];
int n = read(fd, buffer, 100);
printf(" Read %d bytes: %s", n, buffer);
close(fd);
// 3. APPEND
printf("\n3. Appending to file...\n");
fd = open("/tmp/test.txt", O_APPEND | O_WRONLY);
const char *more = "This line was appended!\n";
write(fd, more, strlen(more));
close(fd);
// 4. READ AGAIN
printf("\n4. Reading file again...\n");
fd = open("/tmp/test.txt", O_RDONLY);
n = read(fd, buffer, 100);
printf(" Full file content:\n%s", buffer);
close(fd);
// 5. COPY
printf("\n5. Copying file...\n");
fd = open("/tmp/test.txt", O_RDONLY);
int fd2 = open("/tmp/test_copy.txt", O_CREAT | O_WRONLY, 0644);
while ((n = read(fd, buffer, 50)) > 0) {
write(fd2, buffer, n);
}
close(fd);
close(fd2);
printf(" File copied to /tmp/test_copy.txt\n");
return 0;
}Build and Run:
# Add to userspace/bin/CMakeLists.txt
# In MentOS:
/bin/file_demo
/bin/ls -l /tmp/test* # Verify files created
/bin/cat /tmp/test_copy.txtGoal: Create directories and list their contents.
Program - userspace/bin/dir_ops.c:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
int main() {
printf("=== Directory Operations ===\n\n");
// 1. CREATE DIRECTORY
printf("1. Creating directory /tmp/mydir...\n");
mkdir("/tmp/mydir", 0755);
// 2. CREATE FILES IN IT
printf("2. Creating files in directory...\n");
char path[100];
for (int i = 1; i <= 5; i++) {
snprintf(path, 100, "/tmp/mydir/file%d.txt", i);
int fd = open(path, O_CREAT | O_WRONLY, 0644);
write(fd, "content", 7);
close(fd);
printf(" Created %s\n", path);
}
// 3. LIST FILES
printf("\n3. Directory contents:\n");
system("/bin/ls -l /tmp/mydir");
// 4. CREATE SUBDIRECTORY
printf("\n4. Creating subdirectory...\n");
mkdir("/tmp/mydir/subdir", 0755);
printf(" Created /tmp/mydir/subdir\n");
// 5. FULL TREE
printf("\n5. Full tree structure:\n");
system("/bin/ls -lR /tmp/mydir");
return 0;
}Test:
/bin/dir_ops
/bin/ls -lR /tmp/mydirGoal: Learn about inodes and how hard links work.
Program - userspace/bin/hardlink_demo.c:
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
printf("=== Hard Links and Inodes ===\n\n");
// 1. CREATE FILE
printf("1. Creating original file...\n");
int fd = open("/tmp/original.txt", O_CREAT | O_WRONLY, 0644);
write(fd, "Original data", 13);
close(fd);
// Check inode
struct stat sb;
stat("/tmp/original.txt", &sb);
printf(" Inode: %lu\n", (unsigned long)sb.st_ino);
printf(" Link count: %lu\n", (unsigned long)sb.st_nlink);
printf(" Size: %lu bytes\n", (unsigned long)sb.st_size);
// 2. CREATE HARD LINK
printf("\n2. Creating hard link...\n");
link("/tmp/original.txt", "/tmp/hardlink.txt");
stat("/tmp/original.txt", &sb);
printf(" Original link count: %lu\n", (unsigned long)sb.st_nlink);
stat("/tmp/hardlink.txt", &sb);
printf(" Hard link inode: %lu\n", (unsigned long)sb.st_ino);
printf(" Hard link count: %lu\n", (unsigned long)sb.st_nlink);
// 3. MODIFY THROUGH LINK
printf("\n3. Modifying through hard link...\n");
fd = open("/tmp/hardlink.txt", O_APPEND | O_WRONLY);
write(fd, " - Modified!", 11);
close(fd);
// 4. READ ORIGINAL
printf("\n4. Reading original file:\n");
char buf[100];
fd = open("/tmp/original.txt", O_RDONLY);
int n = read(fd, buf, 100);
printf(" %.*s\n", n, buf);
close(fd);
// 5. DELETE LINK
printf("\n5. Deleting hard link...\n");
unlink("/tmp/hardlink.txt");
stat("/tmp/original.txt", &sb);
printf(" Final link count: %lu\n", (unsigned long)sb.st_nlink);
printf(" (Note: inode still exists until all links deleted)\n");
return 0;
}Test and Observe:
/bin/hardlink_demo
# Note: Both files share same inode!
# Modifying one affects the other
# File only deleted when ALL hard links removedGoal: Use system calls to interact with the filesystem directly.
Program - userspace/bin/rawio.c:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main() {
printf("=== Low-Level File I/O ===\n\n");
// 1. OPEN
printf("1. Opening file with O_RDWR...\n");
int fd = open("/tmp/rawtest.txt", O_CREAT | O_RDWR, 0644);
printf(" File descriptor: %d\n", fd);
// 2. WRITE at specific positions
printf("\n2. Writing at different positions...\n");
lseek(fd, 0, SEEK_SET);
write(fd, "START", 5);
printf(" Wrote 'START' at offset 0\n");
lseek(fd, 10, SEEK_SET);
write(fd, "MIDDLE", 6);
printf(" Wrote 'MIDDLE' at offset 10\n");
lseek(fd, 20, SEEK_SET);
write(fd, "END", 3);
printf(" Wrote 'END' at offset 20\n");
// 3. SEEK and READ
printf("\n3. Seeking and reading...\n");
lseek(fd, 0, SEEK_SET);
char buf[100];
int n = read(fd, buf, 100);
printf(" File content (%d bytes):\n", n);
for (int i = 0; i < n; i++) {
if (buf[i] == '\0') {
printf("[NULL]");
} else {
printf("%c", buf[i]);
}
}
printf("\n");
// 4. TRUNCATE
printf("\n4. Truncating file to 15 bytes...\n");
ftruncate(fd, 15);
lseek(fd, 0, SEEK_SET);
n = read(fd, buf, 100);
printf(" After truncate: %d bytes\n", n);
close(fd);
return 0;
}Explore:
-
lseek()for seeking to positions -
read()/write()at different offsets -
ftruncate()to change file size - File descriptors and offsets
Advanced Goal: Understand EXT2 structure on disk.
Steps:
- Examine EXT2 superblock and group descriptors
- Look at
kernel/src/fs/ext2.cimplementation - Write program to:
- Read superblock
- Enumerate block groups
- Count free inodes and blocks
- Navigate inode bitmap
Hints:
- The primary superblock always lives at byte offset 1024 from the start of the volume
(see the comment on
ext2_superblock_tinkernel/inc/fs/ext2.h). With the 1 KiB blocks of classic EXT2 that happens to be "block 1"; with MentOS's 4 KiB blocks it is inside block 0. Use the byte offset, not the block index. - Group descriptors follow the superblock
- Each group has inode bitmap and block bitmap
- Use direct disk I/O through
/dev/hda(careful!)
-
kernel/src/fs/vfs.c- VFS core operations -
kernel/src/fs/ext2.c- EXT2 filesystem implementation -
kernel/src/fs/namei.c- Path name resolution -
kernel/src/fs/procfs.c- Virtual procfs -
kernel/src/fs/pipe.c- Pipes for IPC -
kernel/inc/fs/vfs.h- VFS data structures -
kernel/inc/fs/ext2.h- EXT2 on-disk format -
lib/inc/fcntl.h- File control flags -
lib/inc/sys/stat.h- File metadata structures
- Architecture - Overall system structure
- Kernel - Kernel subsystems
- System Calls - File-related syscalls (open, read, write, mkdir, etc.)
- Userspace Programs - Programs that use files
Key takeaway: Files are just organized data on disk. The VFS abstracts the details, letting kernels support many filesystems. EXT2 shows how one filesystem organizes inodes and blocks to store everything efficiently!