-
Notifications
You must be signed in to change notification settings - Fork 63
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!
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:
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).
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
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.
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!
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 buffer that connects two file descriptors
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:
- Superblock at block 1
- Group descriptors follow 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!
Directory Listing (getdents)