Chapter 37 — Directories, SYS_READDIR, and ls

Chapter 37 — Directories, SYS_READDIR, and ls
This entry is part 34 of 35 in the series Writing A Linux Style Operating System From Scratch

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!

Writing A Linux Style Operating System From Scratch

Chapter 36.5 – A Testing Detour and a Real Smoke Harness

Leave a Reply

Your email address will not be published. Required fields are marked *