- Writing a Linux-style Operating System From Scratch
- Chapter 2 — GDT, IDT, and Surviving Your First Kernel Crash
- Chapter 3 — Hardware Interrupts: PIC, PIT Timer, and Keyboard Input
- Chapter 4 — Reading the Memory Map and Building a Physical Page Allocator
- Chapter 20 — A Tiny Executable Format and User Program Loader
- Chapter 5 — Turning On Paging
- Chapter 6 — Building the First Kernel Heap
- Chapter 7 — A Real Virtual Memory Mapping Layer
- Chapter 8 – Moving the Heap onto Virtual Memory
- Chapter 9 — Cooperative Multitasking and Kernel Threads
- Chapter 10 — Timer-Driven Preemptive Multitasking
- Chapter 11 — Blocking Primitives, Sleep Queues, and Scheduler Hygiene
- Chapter 12 – Wait Queues and Blocking Keyboard Input
- Chapter 13 — Mutexes, Semaphores, and a Console Lock
- Chapter 14 — Terminal Line Discipline and a Kernel Monitor
- Chapter 15 — Command Tables, Argument Parsing, and Shift-Aware Keyboard Input
- Chapter 16 — Entering User Mode and Returning Through Syscalls
- Chapter 17 — Minimal Processes, User Memory Copying, and More Robust Syscalls
- Chapter 18 — File-Descriptor Syscalls and a Tiny User-Mode Console Program
- Chapter 19 — Per-Process Address Spaces and CR3 Switching
- Chapter 21 — Process Teardown and Address-Space Cleanup
- Chapter 23 — Building a Real User C Program and Embedding Its ELF
- Chapter 24 — User argc / argv and a Real Initial Stack
- Chapter 26 — Process Table, ps, runbg, and wait PID
- Chapter 27 — A Second User Program for Safe Background Execution
- Chapter 28 — Pattern-Based User Program Build System
- Chapter 29 — First Userland Runtime
- Chapter 30 — First User-Mode Shell
- Chapter 31 — SYS_EXEC, SYS_WAITPID, and Shell-Launched Programs
- Chapter 32 — Process Ownership, Waiting, and Job State
- Chapter 33 — Process Termination and Kill Checks
- Chapter 34 — First RAMFS and Core File APIs
- Chapter 35 — SYS_SEEK and Rewindable File Descriptors
- Chapter 36.5 – A Testing Detour and a Real Smoke Harness
- Chapter 37 — Directories, SYS_READDIR, and ls
Post Stastics
- This post has 3551 words.
- Estimated read time is 16.91 minute(s).
In Chapter 36, Toyix gained file metadata:
SYS_STAT
The shell could now do:
ush> stat /README stat: path=/README type=file size=... ush> stat /missing stat: could not stat /missing
Now we are ready for the next filesystem milestone:
directories
This chapter adds:
root directory / SYS_READDIR shell ls PATH
After this chapter:
ush> ls / file README file programs
For compatibility with the previous chapters, /programs will remain a regular text file for now:
ush> cat /programs demo counter shell fstest
The root directory / becomes the first actual directory.
1. What this chapter adds
Modify:
include/kernel/vfs.h kernel/vfs.c include/kernel/syscall.h kernel/syscall.c user/include/toyix_syscall.h user/shell.c kernel/program.c user/fstest.c tests/smoke.py README.md CHANGELOG.md index.md docs/roadmap.md
New syscall:
SYS_READDIR = 16
New user ABI structure:
typedef struct toyix_dirent {
toyix_u32 type;
char name[32];
} toyix_dirent_t;
New shell command:
ls [PATH]
2. Directory design for this chapter
The RAMFS now supports two node types:
regular file directory
Current RAMFS layout:
/ ├── README regular file, path /README └── programs regular file, path /programs
So:
ls /
prints:
file README file programs
But:
cat /programs
still prints the text file from earlier chapters.
Later we can turn /programs into a real directory.
For now, the goal is to add the directory mechanism without breaking the existing file tests.
3. New SYS_READDIR ABI
EAX = SYS_READDIR EBX = fd ECX = user pointer to toyix_dirent_t returns: EAX = 1 directory entry returned EAX = 0 end of directory EAX = 0xFFFFFFFF error
Example userland loop:
toyix_i32 fd = toyix_open("/", 0);
toyix_dirent_t ent;
for (;;) {
toyix_i32 rc = toyix_readdir(fd, &ent);
if (rc < 0) {
break;
}
if (rc == 0) {
break;
}
toyix_printf("%s\n", ent.name);
}
toyix_close(fd);
4. Update include/kernel/vfs.h
Replace it with this version:
// include/kernel/vfs.h
#ifndef TOYIX_KERNEL_VFS_H
#define TOYIX_KERNEL_VFS_H
#include <stdint.h>
#define VFS_OK 0
#define VFS_ERR_NOT_FOUND -1
#define VFS_ERR_INVALID -2
#define VFS_ERR_NO_MEMORY -3
#define VFS_ERR_NOT_SUPPORTED -4
#define TOYIX_SEEK_SET 0u
#define TOYIX_SEEK_CUR 1u
#define TOYIX_SEEK_END 2u
#define VFS_NODE_REGULAR 1u
#define VFS_NODE_DIRECTORY 2u
#define VFS_NAME_MAX 32u
typedef struct vfs_file vfs_file_t;
typedef struct vfs_stat {
uint32_t type;
uint32_t size;
} vfs_stat_t;
typedef struct vfs_dirent {
uint32_t type;
char name[VFS_NAME_MAX];
} vfs_dirent_t;
void vfs_init(void);
int vfs_open(const char *path, vfs_file_t **out_file);
int vfs_read(
vfs_file_t *file,
void *buffer,
uint32_t length,
uint32_t *out_read
);
int vfs_readdir(
vfs_file_t *file,
vfs_dirent_t *out_dirent,
uint32_t *out_has_entry
);
int vfs_seek(
vfs_file_t *file,
int32_t offset,
uint32_t whence,
uint32_t *out_position
);
uint32_t vfs_tell(vfs_file_t *file);
uint32_t vfs_size(vfs_file_t *file);
int vfs_stat(const char *path, vfs_stat_t *out_stat);
void vfs_close(vfs_file_t *file);
void vfs_test_once(void);
#endif
New pieces:
#define VFS_NAME_MAX 32u
typedef struct vfs_dirent {
uint32_t type;
char name[VFS_NAME_MAX];
} vfs_dirent_t;
and:
int vfs_readdir(
vfs_file_t *file,
vfs_dirent_t *out_dirent,
uint32_t *out_has_entry
);
5. Update RAMFS structures in kernel/vfs.c
Replace the old RAMFS node structure with directory-aware structures:
typedef struct ramfs_dir_entry {
const char *name;
const char *target_path;
} ramfs_dir_entry_t;
typedef struct ramfs_node {
const char *path;
uint32_t type;
const uint8_t *data;
uint32_t size;
const ramfs_dir_entry_t *entries;
uint32_t entry_count;
} ramfs_node_t;
The existing vfs_file object can remain small:
struct vfs_file {
const ramfs_node_t *node;
uint32_t offset;
};
For a regular file, offset means byte offset.
For a directory, offset means directory entry index.
That is simple and good enough for this stage.
6. Add root directory entries
In kernel/vfs.c, keep the existing file text:
static const uint8_t readme_text[] =
"Toyix RAMFS\n"
"This file lives inside the kernel image.\n"
"The first filesystem is read-only and memory-backed.\n";
static const uint8_t programs_text[] =
"demo\n"
"counter\n"
"shell\n";
Add root directory entries:
static const ramfs_dir_entry_t root_entries[] = {
{
.name = "README",
.target_path = "/README"
},
{
.name = "programs",
.target_path = "/programs"
}
};
Then replace the RAMFS node table with:
static const ramfs_node_t ramfs_nodes[] = {
{
.path = "/",
.type = VFS_NODE_DIRECTORY,
.data = 0,
.size = 2u,
.entries = root_entries,
.entry_count = 2u
},
{
.path = "/README",
.type = VFS_NODE_REGULAR,
.data = readme_text,
.size = sizeof(readme_text) - 1u,
.entries = 0,
.entry_count = 0
},
{
.path = "/programs",
.type = VFS_NODE_REGULAR,
.data = programs_text,
.size = sizeof(programs_text) - 1u,
.entries = 0,
.entry_count = 0
}
};
The VFS now has three nodes:
/ README programs
So vfs_init() will now print:
VFS: initialized RAMFS with 3 node(s)
Update the message in vfs_init() from:
console_writeln(" file(s)");
to:
console_writeln(" node(s)");
Full function:
void vfs_init(void) {
console_write("VFS: initialized RAMFS with ");
console_write_u32_dec(ramfs_node_count);
console_writeln(" node(s)");
}
7. Update vfs_read()
Directories should not be readable with read().
At the top of vfs_read(), after validation, add:
if (file->node->type != VFS_NODE_REGULAR) {
return VFS_ERR_NOT_SUPPORTED;
}
Full updated vfs_read():
int vfs_read(
vfs_file_t *file,
void *buffer,
uint32_t length,
uint32_t *out_read
) {
if (file == 0 || buffer == 0 || out_read == 0) {
return VFS_ERR_INVALID;
}
*out_read = 0;
if (file->node->type != VFS_NODE_REGULAR) {
return VFS_ERR_NOT_SUPPORTED;
}
if (length == 0) {
return VFS_OK;
}
if (file->offset >= file->node->size) {
return VFS_OK;
}
uint32_t remaining = file->node->size - file->offset;
uint32_t to_copy = length;
if (to_copy > remaining) {
to_copy = remaining;
}
memcpy(buffer, file->node->data + file->offset, to_copy);
file->offset += to_copy;
*out_read = to_copy;
return VFS_OK;
}
Now:
cat /
should fail with a read error.
That is acceptable until cat learns to reject directories based on stat.
8. Add vfs_readdir()
Add this after vfs_read():
int vfs_readdir(
vfs_file_t *file,
vfs_dirent_t *out_dirent,
uint32_t *out_has_entry
) {
if (file == 0 || out_dirent == 0 || out_has_entry == 0) {
return VFS_ERR_INVALID;
}
*out_has_entry = 0;
if (file->node->type != VFS_NODE_DIRECTORY) {
return VFS_ERR_NOT_SUPPORTED;
}
if (file->offset >= file->node->entry_count) {
return VFS_OK;
}
const ramfs_dir_entry_t *entry = &file->node->entries[file->offset];
const ramfs_node_t *target = ramfs_find(entry->target_path);
if (target == 0) {
return VFS_ERR_INVALID;
}
out_dirent->type = target->type;
kstrlcpy(out_dirent->name, entry->name, VFS_NAME_MAX);
file->offset++;
*out_has_entry = 1;
return VFS_OK;
}
This reads one directory entry at a time.
Return behavior:
VFS_OK + out_has_entry=1 entry returned VFS_OK + out_has_entry=0 end of directory error invalid fd or not a directory
9. Update vfs_seek() for directories
For this chapter, seeking on directories is useful because it lets userland rewind a directory stream.
Modify vfs_seek() so TOYIX_SEEK_END uses the right size:
uint32_t logical_size = 0;
if (file->node->type == VFS_NODE_DIRECTORY) {
logical_size = file->node->entry_count;
} else {
logical_size = file->node->size;
}
Then use logical_size in the TOYIX_SEEK_END case.
The relevant part becomes:
int64_t base = 0;
uint32_t logical_size = 0;
if (file->node->type == VFS_NODE_DIRECTORY) {
logical_size = file->node->entry_count;
} else {
logical_size = file->node->size;
}
switch (whence) {
case TOYIX_SEEK_SET:
base = 0;
break;
case TOYIX_SEEK_CUR:
base = (int64_t)file->offset;
break;
case TOYIX_SEEK_END:
base = (int64_t)logical_size;
break;
default:
return VFS_ERR_INVALID;
}
Seeking past the end of a directory is allowed, just like files.
A later readdir() will return EOF.
10. Update vfs_size()
For directories, return the number of entries.
Replace vfs_size() with:
uint32_t vfs_size(vfs_file_t *file) {
if (file == 0 || file->node == 0) {
return 0;
}
if (file->node->type == VFS_NODE_DIRECTORY) {
return file->node->entry_count;
}
return file->node->size;
}
11. Update vfs_stat()
vfs_stat() already copies:
out_stat->type = node->type; out_stat->size = node->size;
For directories, make size mean number of entries.
Replace that part with:
out_stat->type = node->type;
if (node->type == VFS_NODE_DIRECTORY) {
out_stat->size = node->entry_count;
} else {
out_stat->size = node->size;
}
Full function:
int vfs_stat(const char *path, vfs_stat_t *out_stat) {
if (path == 0 || out_stat == 0) {
return VFS_ERR_INVALID;
}
const ramfs_node_t *node = ramfs_find(path);
if (node == 0) {
return VFS_ERR_NOT_FOUND;
}
out_stat->type = node->type;
if (node->type == VFS_NODE_DIRECTORY) {
out_stat->size = node->entry_count;
} else {
out_stat->size = node->size;
}
return VFS_OK;
}
Now:
stat /
should report:
type=directory size=2
12. Expand vfs_test_once()
Add a root directory stat and readdir test.
Near the beginning, before /README stat, add:
vfs_stat_t root_stat;
if (vfs_stat("/", &root_stat) != VFS_OK) {
kernel_panic("VFS test could not stat /");
}
if (root_stat.type != VFS_NODE_DIRECTORY || root_stat.size != 2u) {
kernel_panic("VFS test received invalid root directory stat");
}
console_write("VFS test: / entries=");
console_write_u32_dec(root_stat.size);
console_writeln(" type=directory");
Then after the /README stat but before the file read tests, add:
vfs_file_t *dir = 0;
if (vfs_open("/", &dir) != VFS_OK || dir == 0) {
kernel_panic("VFS test could not open /");
}
vfs_dirent_t ent;
uint32_t has_entry = 0;
if (vfs_readdir(dir, &ent, &has_entry) != VFS_OK || !has_entry) {
kernel_panic("VFS test could not read first root entry");
}
console_write("VFS test: first root entry: ");
console_writeln(ent.name);
if (vfs_readdir(dir, &ent, &has_entry) != VFS_OK || !has_entry) {
kernel_panic("VFS test could not read second root entry");
}
console_write("VFS test: second root entry: ");
console_writeln(ent.name);
if (vfs_readdir(dir, &ent, &has_entry) != VFS_OK || has_entry) {
kernel_panic("VFS test root directory did not end");
}
vfs_close(dir);
Update the test banner:
console_writeln("VFS test: starting RAMFS directory/stat/seek test");
Update the final success line:
console_writeln("VFS test: RAMFS directory/stat/seek sanity check passed");
Expected new VFS test output:
VFS test: / entries=2 type=directory VFS test: /README size=... type=file VFS test: first root entry: README VFS test: second root entry: programs VFS test: RAMFS directory/stat/seek sanity check passed
13. Update syscall constants
Update both:
include/kernel/syscall.h user/include/toyix_syscall.h
Add:
#define SYS_READDIR 16u
The syscall list becomes:
#define SYS_PUTC 1u #define SYS_EXIT 2u #define SYS_WRITE 3u #define SYS_SLEEP 4u #define SYS_READ 5u #define SYS_EXEC 6u #define SYS_WAITPID 7u #define SYS_GETPID 8u #define SYS_GETPPID 9u #define SYS_PROCINFO 10u #define SYS_KILL 11u #define SYS_OPEN 12u #define SYS_CLOSE 13u #define SYS_SEEK 14u #define SYS_STAT 15u #define SYS_READDIR 16u
14. Add directory entry ABI structure
In include/kernel/syscall.h, add:
#define TOYIX_NAME_MAX 32u
near the file constants.
Then add:
typedef struct toyix_dirent {
uint32_t type;
char name[TOYIX_NAME_MAX];
} toyix_dirent_t;
The ABI structures section should now contain:
typedef struct toyix_stat {
uint32_t type;
uint32_t size;
} toyix_stat_t;
typedef struct toyix_dirent {
uint32_t type;
char name[TOYIX_NAME_MAX];
} toyix_dirent_t;
In user/include/toyix_syscall.h, add the same constant and structure:
#define TOYIX_NAME_MAX 32u
typedef struct toyix_dirent {
toyix_u32 type;
char name[TOYIX_NAME_MAX];
} toyix_dirent_t;
The struct layouts must match exactly.
15. Add user toyix_readdir() wrapper
In user/include/toyix_syscall.h, add:
static inline toyix_i32 toyix_readdir(
toyix_u32 fd,
toyix_dirent_t *dirent
) {
toyix_i32 result;
__asm__ volatile (
"int $0x80"
: "=a"(result)
: "a"(SYS_READDIR),
"b"(fd),
"c"(dirent)
: "memory"
);
return result;
}
Return behavior:
1 entry returned 0 end of directory -1 error
16. Add kernel SYS_READDIR
In kernel/syscall.c, add a type conversion helper if you do not already have one from SYS_STAT:
static uint32_t syscall_vfs_type_to_abi(uint32_t vfs_type) {
switch (vfs_type) {
case VFS_NODE_REGULAR:
return TOYIX_FILE_REGULAR;
case VFS_NODE_DIRECTORY:
return TOYIX_FILE_DIRECTORY;
default:
return 0;
}
}
If you already added this in Chapter 42, reuse it.
Now add:
static void syscall_readdir(interrupt_frame_t *frame) {
uint32_t fd = frame->ebx;
uintptr_t user_dirent = (uintptr_t)frame->ecx;
if (user_dirent == 0) {
frame->eax = 0xFFFFFFFFu;
return;
}
process_t *current = process_current();
if (current == 0) {
frame->eax = 0xFFFFFFFFu;
return;
}
vfs_file_t *file = process_fd_get(current, fd);
if (file == 0) {
frame->eax = 0xFFFFFFFFu;
return;
}
vfs_dirent_t kernel_ent;
uint32_t has_entry = 0;
if (vfs_readdir(file, &kernel_ent, &has_entry) != VFS_OK) {
frame->eax = 0xFFFFFFFFu;
return;
}
if (!has_entry) {
frame->eax = 0;
return;
}
toyix_dirent_t user_ent;
user_ent.type = syscall_vfs_type_to_abi(kernel_ent.type);
if (user_ent.type == 0) {
frame->eax = 0xFFFFFFFFu;
return;
}
kstrlcpy(user_ent.name, kernel_ent.name, TOYIX_NAME_MAX);
if (copy_to_user(
user_dirent,
&user_ent,
sizeof(user_ent)
) != USERCOPY_OK) {
frame->eax = 0xFFFFFFFFu;
return;
}
frame->eax = 1;
}
Make sure kernel/syscall.c includes the kernel string header if it does not already:
#include "kernel/string.h"
17. Update syscall handler
Add:
case SYS_READDIR:
syscall_readdir(frame);
syscall_finish_or_kill(frame);
return;
Place it near the other file syscalls:
case SYS_OPEN:
syscall_open(frame);
syscall_finish_or_kill(frame);
return;
case SYS_CLOSE:
syscall_close(frame);
syscall_finish_or_kill(frame);
return;
case SYS_SEEK:
syscall_seek(frame);
syscall_finish_or_kill(frame);
return;
case SYS_STAT:
syscall_stat(frame);
syscall_finish_or_kill(frame);
return;
case SYS_READDIR:
syscall_readdir(frame);
syscall_finish_or_kill(frame);
return;
18. Add shell ls command
Update user/shell.c.
First update help text.
Replace:
toyix_puts("commands: help, echo, args, cat, stat, run, runbg, jobs, wait, kill, exit");
with:
toyix_puts("commands: help, echo, args, cat, stat, ls, run, runbg, jobs, wait, kill, exit");
Add this helper:
static void print_dirent_type(toyix_u32 type) {
if (type == TOYIX_FILE_DIRECTORY) {
toyix_write_str("dir ");
} else if (type == TOYIX_FILE_REGULAR) {
toyix_write_str("file");
} else {
toyix_write_str("unk ");
}
}
Add the command:
static void cmd_ls(int argc, char **argv) {
const char *path = "/";
if (argc > 2) {
toyix_puts("usage: ls [PATH]");
return;
}
if (argc == 2) {
path = argv[1];
}
toyix_stat_t stat;
if (toyix_stat(path, &stat) != 0) {
toyix_printf("ls: could not stat %s\n", path);
return;
}
if (stat.type != TOYIX_FILE_DIRECTORY) {
toyix_printf("%s\n", path);
return;
}
toyix_i32 fd = toyix_open(path, 0);
if (fd < 0) {
toyix_printf("ls: could not open %s\n", path);
return;
}
toyix_dirent_t ent;
for (;;) {
toyix_i32 rc = toyix_readdir((toyix_u32)fd, &ent);
if (rc < 0) {
toyix_puts("ls: readdir error");
break;
}
if (rc == 0) {
break;
}
print_dirent_type(ent.type);
toyix_putchar(' ');
toyix_puts(ent.name);
}
toyix_close((toyix_u32)fd);
}
This version prints both type and name:
file README file programs
That is more useful than names alone and proves type propagation works.
Now add the dispatch branch after stat:
if (toyix_streq(cmd_argv[0], "ls")) {
cmd_ls(cmd_argc, cmd_argv);
continue;
}
The file command dispatch section becomes:
if (toyix_streq(cmd_argv[0], "cat")) {
cmd_cat(cmd_argc, cmd_argv);
continue;
}
if (toyix_streq(cmd_argv[0], "stat")) {
cmd_stat(cmd_argc, cmd_argv);
continue;
}
if (toyix_streq(cmd_argv[0], "ls")) {
cmd_ls(cmd_argc, cmd_argv);
continue;
}
19. Update shell test input
In kernel/program.c, add ls commands after the stat commands.
Change:
inject_text("cat /README\n");
inject_text("stat /\n");
inject_text("stat /README\n");
inject_text("stat /programs\n");
inject_text("stat /missing\n");
to:
inject_text("cat /README\n");
inject_text("stat /\n");
inject_text("stat /README\n");
inject_text("stat /programs\n");
inject_text("stat /missing\n");
inject_text("ls /\n");
inject_text("ls /README\n");
This tests:
stat directory stat file stat missing path ls directory ls regular file fallback
20. Expected shell output
New expected output:
ush> stat / stat: path=/ type=directory size=2 ush> ls / file README file programs ush> ls /README /README
The /README behavior is intentionally simple.
Since /README is not a directory, ls /README prints the path itself.
Later, we can make that output more like Unix ls -l.
21. Update tests/smoke.py
Since Chapter 36.5 moved assertions out of Makefile, Chapter 37 should extend the Python smoke harness instead of adding more shell grep chains.
Keep one design detail in mind while updating the test flow:
normal boots should stay interactive scripted shell input should run only under make test
That means the boot-time program_test_once() shell script should be compiled in only for smoke builds, while ordinary make iso and make run boots should go straight to a clean toyix> prompt.
Update the normal-boot expectations to check:
VFS: initialized RAMFS with 3 node(s) VFS test: / entries=2 type=directory VFS test: first root entry: README VFS test: second root entry: programs VFS test: RAMFS directory/stat/seek sanity check passed fstest: / type=directory size=2 fstest: first root entry: README fstest: second root entry: programs commands: help, echo, args, cat, stat, ls, run, runbg, jobs, wait, kill, exit stat: path=/ type=directory size=2 file README file programs /README
The full suite still runs through:
python3 tests/smoke.py
and the shell wrapper remains available as a compatibility entry point:
tests/smoke.sh
22. Interactive test
After boot:
toyix> run shell
Inside shell:
ush> ls /
Expected:
file README file programs
These file ... lines are output, not commands. There is no file built-in in Chapter 37.
Also remember that Chapter 37 still has:
absolute paths only no current working directory
So:
ush> ls /programs
prints:
/programs
because /programs is still a regular file, while:
ush> ls programs
prints:
ls: could not stat programs
because relative paths are not implemented yet.
ush> stat /
Expected:
stat: path=/ type=directory size=2
Then:
ush> ls /
Expected:
file README file programs
Then:
ush> cat /README
Expected:
Toyix RAMFS This file lives inside the kernel image. The first filesystem is read-only and memory-backed.
Try:
ush> ls /README
Expected:
/README
Try:
ush> ls /missing
Expected:
ls: could not stat /missing
23. Common failures
Failure: ls / says readdir error
Check that / is a directory node:
.type = VFS_NODE_DIRECTORY .entries = root_entries .entry_count = 2u
Also check that vfs_readdir() allows only:
file->node->type == VFS_NODE_DIRECTORY
If / accidentally has VFS_NODE_REGULAR, readdir will fail.
Failure: cat /README broke
Make sure /README is still a regular file node:
.path = "/README", .type = VFS_NODE_REGULAR, .data = readme_text, .size = sizeof(readme_text) - 1u
Do not accidentally make /README a directory.
Failure: cat / crashes
vfs_read() should reject directories:
if (file->node->type != VFS_NODE_REGULAR) {
return VFS_ERR_NOT_SUPPORTED;
}
The syscall should translate that into -1.
The shell cat should print:
cat: read error
not crash.
Failure: stat / says type=file
Check vfs_stat():
out_stat->type = node->type;
and the syscall type conversion:
VFS_NODE_DIRECTORY -> TOYIX_FILE_DIRECTORY
The shell’s file_type_name() should map:
TOYIX_FILE_DIRECTORY -> "directory"
Failure: directory entry names are garbage
Check the structure layouts.
Kernel ABI:
typedef struct toyix_dirent {
uint32_t type;
char name[TOYIX_NAME_MAX];
} toyix_dirent_t;
User ABI:
typedef struct toyix_dirent {
toyix_u32 type;
char name[TOYIX_NAME_MAX];
} toyix_dirent_t;
Also check that TOYIX_NAME_MAX is 32 in both headers.
Failure: readdir returns the same entry forever
Make sure vfs_readdir() increments:
file->offset++;
after copying the entry.
Failure: second ls / prints nothing
Each toyix_open("/") must create a new vfs_file_t with:
file->offset = 0;
If the directory offset is stored in the RAMFS node instead of the open file object, all opens share the same offset. That is wrong.
Offsets belong to open file objects, not nodes.
25. What this chapter achieved
Before this chapter:
RAMFS was path-addressable but not listable /programs was just a text file there was no directory reading syscall
After this chapter:
RAMFS has a real root directory VFS supports directory entries SYS_READDIR exposes directory entries to userland shell has ls PATH stat / reports directory metadata
This is a major filesystem milestone.
The Toyix shell can now discover files rather than only knowing hardcoded paths.
26. Design limitations
Still missing:
nested directories path normalization relative paths current working directory real directory file types under /programs directory creation write support mount points filesystem-backed exec
Also, ls is still simple.
It does not support:
ls -l hidden files sorting columns recursive listing
That is fine.
The important architecture is now present:
open directory ↓ readdir loop ↓ close directory
Next Chapter
Now that RAMFS can represent directories, the next natural step is to make /programs a real directory.
Instead of:
/programs regular text file
we can create:
/programs/ directory /programs/demo /programs/counter /programs/shell
At first, those program entries can be metadata-only pseudo-files.
Then later, filesystem-backed exec can use:
/programs/counter
to launch the embedded counter program.
That creates the bridge from:
embedded program registry
toward:
filesystem-backed execution
Resources
Closure
Chapter 37 gives Toyix its first real directory support and a simple ls path, which is the structural filesystem milestone needed before /programs can become a directory of its own.
Happy Coding!