Chapter 39 — Path-Based Program Launching with /programs

Chapter 39 — Path-Based Program Launching with /programs
This entry is part 36 of 38 in the series Writing A Linux Style Operating System From Scratch

Table of Contents

Writing A Linux Style Operating System From Scratch

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 9 — Cooperative Multitasking and Kernel Threads

Chapter 10 — Timer-Driven Preemptive Multitasking

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 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

Chapter 38 — Turning /programs into a Real Directory

Chapter 39 — Path-Based Program Launching with /programs

Chapter 40 — Shell Job References with %1

Post Stastics

  • This post has 2061 words.
  • Estimated read time is 9.81 minute(s).

Before reading the shell examples in this chapter, be explicit about the prompt transition:

toyix> run shell
ush>

toyix> is the kernel monitor.

ush> is the user shell.

So when this chapter shows commands like:

ush> run /programs/counter alpha beta
ush> runbg /programs/counter victim

those commands must be typed inside the user shell after toyix> run shell.

Two other constraints still apply in this chapter:

clear is a kernel monitor command, not a user-shell command
file paths in ush are still absolute-only

So cat README is still wrong, and cat /README is the correct form.
Likewise, cat / fails because / is a directory, not a regular file.

In Chapter 38, /programs became a real directory:

ush> ls /programs
file demo
file counter
file shell
file fstest

That gave Toyix a better filesystem shape, but program launching still lived in the older world:

ush> run counter alpha beta

The shell knew program names.

The filesystem knew /programs/counter.

Those were still two different namespaces.

Chapter 39 connects them.

After this chapter, the existing launch commands can accept either form:

ush> run counter alpha beta
ush> run /programs/counter alpha beta

Both launch the same embedded program.

We are still not loading ELF files from the filesystem yet. The kernel still launches from the embedded program registry. This chapter only teaches that registry-facing launch path how to recognize a narrow filesystem form:

/programs/<name>

That is enough to bridge the namespace we built in Chapter 38 to the execution path we built much earlier.


1. What this chapter adds

Modify:

include/kernel/program.h
kernel/program.c
tests/smoke.py
README.md
CHANGELOG.md
index.md
docs/roadmap.md

No new syscall number is needed.

No new shell built-in is needed.

No VFS node type changes are needed.

The shell already calls:

SYS_EXEC

with a string plus argv.

So the cleanest Chapter 39 design is:

teach the kernel launcher to accept either:
  counter
  /programs/counter

That means the shell commands stay stable:

run PROGRAM [ARGS...]
runbg PROGRAM [ARGS...]

and only the meaning of the PROGRAM operand grows.


2. Why this is the right Chapter 39

At this point in development, Toyix already has:

an embedded program registry
a user shell
SYS_EXEC
a real /programs directory

So the next literal step is not:

new ELF loading rules
new executable file metadata
new path search logic

It is simply:

accept /programs/<name> anywhere we already accept a registry program name

That keeps the system coherent.

The user sees a real filesystem path.

The kernel still launches through the same embedded program table.

Later chapters can add mode bits, current working directories, and general command lookup on top of this bridge.


3. Supported path rules

This chapter intentionally supports only a narrow path form.

Valid launch targets:

demo
counter
shell
fstest
/programs/demo
/programs/counter
/programs/shell
/programs/fstest

Invalid launch targets:

/programs
/programs/
/programs/missing
/programs/counter/extra
programs/counter
./counter
/README

That narrowness is important.

We are not adding a general path resolver yet.

We are only saying:

if the input begins with /programs/ and has exactly one final name component,
map that final component back to the embedded program registry

4. Update include/kernel/program.h

The public launcher API should reflect the broader input it now accepts.

Update the header so the names describe reality:

const embedded_program_t *program_find(const char *name);
const embedded_program_t *program_find_name_or_path(
    const char *name_or_path
);

process_t *program_create_process(
    const char *name_or_path,
    int argc,
    const char **argv
);

int program_run_background(
    const char *name_or_path,
    int argc,
    const char **argv,
    process_t **process_out
);

int program_run_foreground(
    const char *name_or_path,
    int argc,
    const char **argv,
    uint32_t *exit_code_out
);

The important change is semantic clarity.

Before this chapter, those functions expected:

plain registry names only

After this chapter, they accept:

either a plain registry name or a /programs/<name> path

5. Add a narrow resolver in kernel/program.c

Near the top of kernel/program.c, add a prefix constant:

#define PROGRAM_PATH_PREFIX "/programs/"
#define PROGRAM_PATH_PREFIX_LEN 10u

Then add a helper that recognizes only this exact shape:

static const char *program_path_suffix(const char *name_or_path) {
    if (name_or_path == 0) {
        return 0;
    }

    if (kstrncmp(
            name_or_path,
            PROGRAM_PATH_PREFIX,
            PROGRAM_PATH_PREFIX_LEN
        ) != 0) {
        return 0;
    }

    const char *name = name_or_path + PROGRAM_PATH_PREFIX_LEN;

    if (*name == '\0') {
        return 0;
    }

    while (*name != '\0') {
        if (*name == '/') {
            return 0;
        }

        ++name;
    }

    return name_or_path + PROGRAM_PATH_PREFIX_LEN;
}

This helper does three important things:

  1. It accepts only absolute /programs/... inputs.
  2. It rejects an empty tail like /programs/.
  3. It rejects nested paths like /programs/counter/extra.

So:

/programs/counter

returns:

counter

but:

/programs/counter/extra

returns failure.

That keeps Chapter 39 intentionally narrow and prevents accidental early path-walking behavior from leaking into the launcher.


6. Add program_find_name_or_path()

Keep the old plain-name lookup:

const embedded_program_t *program_find(const char *name);

That function is still useful and still correct.

Add a second helper on top of it:

const embedded_program_t *program_find_name_or_path(
    const char *name_or_path
) {
    const char *path_name = program_path_suffix(name_or_path);

    if (path_name != 0) {
        return program_find(path_name);
    }

    return program_find(name_or_path);
}

This is the core bridge for the chapter.

It means these two calls behave the same:

program_find_name_or_path("counter")
program_find_name_or_path("/programs/counter")

Both end up returning the embedded counter entry.

But the helper still rejects:

/programs
/programs/
/programs/missing
/programs/counter/extra

because those either fail the narrow path check or fail the final registry lookup.


7. Route all launch helpers through that resolver

Now update the launch path itself.

In program_create_process(), change:

const embedded_program_t *program = program_find(name);

to:

const embedded_program_t *program =
    program_find_name_or_path(name_or_path);

This one change automatically lifts the rest of the kernel launch path.

Because:

program_run_background()
program_run_foreground()
syscall_exec()
shell run
shell runbg

all eventually flow through program_create_process().

That is why Chapter 39 does not need a new syscall and does not need a new shell command.

The existing chain already exists:

shell run /programs/counter
  -> toyix_exec("/programs/counter", argv, argc)
  -> SYS_EXEC
  -> program_run_background(...)
  -> program_create_process(...)

We only needed to make the last kernel-side lookup step understand the new input form.


8. Preserve user-visible argv[0]

One subtle point matters here.

When a user types:

ush> run /programs/counter alpha beta

the program should still see:

argv[0] = /programs/counter

not:

argv[0] = counter

Why?

Because the kernel should normalize the launch target for lookup, but it should not silently rewrite the user’s argument vector unless that is architecturally required.

So the path is used for:

registry lookup

but the original argv is still passed through to the child.

That gives us a useful split:

internal process identity -> counter
user-facing argv[0]       -> /programs/counter

So the boot log will show:

Program: launching counter argc=3 ppid=...

while the child itself prints:

counter: argv[0]=/programs/counter

That is the correct Chapter 39 behavior.


9. Why the shell does not change in this chapter

Notice that user/shell.c does not need a code change for this milestone.

The shell already treats the first operand to run and runbg as an arbitrary string:

program_name = argv[1];
pid = toyix_exec(program_name, child_argv, (toyix_u32)child_argc);

That means the shell was already capable of passing:

counter
/programs/counter

The only missing piece was the kernel-side interpretation.

This is a good example of a chapter where the right design is not to add another layer of shell parsing just because a new user-visible behavior appears.

The shell was already general enough.

The kernel lookup was the narrow part.


10. Update the scripted smoke flow carefully

This chapter also needs a testing adjustment.

The old scripted shell sequence already launched:

run counter alpha beta
runbg counter victim
kill 5
wait 5

That sequence relied on the exact number of child launches that happened before the background counter.

If we had added:

run counter ...
run /programs/counter ...

then the later background process would no longer be PID 5, and the old injected:

kill 5
wait 5

would stop matching the intended child.

So instead of adding an extra launch, Chapter 39 should replace the foreground shell launch test with the path-based form:

run /programs/counter alpha beta

and then add a failure check that does not create a child:

run /programs/missing

This is the smooth version of the chapter because it tests the new feature while keeping the rest of the scripted flow stable.

That avoids repeating the Chapter 37-style mistake where documented and tested paths drifted apart.


11. Update program_test_once()

In kernel/program.c, change the injected shell command from:

inject_text("run counter alpha beta\n");

to:

inject_text("run /programs/counter alpha beta\n");

Then add a failing launch check before the existing background launch:

inject_text("run /programs/missing\n");
inject_text("runbg counter victim\n");

That gives the smoke harness three key Chapter 39 checks:

  1. A successful path-based foreground launch.
  2. A failed launch for a missing /programs path.
  3. No PID drift in the later runbg counter / kill 5 / wait 5 sequence.

That third point matters because the tests should validate the new chapter, not accidentally rewrite the meaning of an unrelated later assertion.


12. Update tests/smoke.py

Now update the expected shell output.

The success lines should include:

shell: run /programs/counter pid=
counter: argv[0]=/programs/counter
shell: /programs/counter exited code 4

And the failure path should include:

run: failed to launch /programs/missing

The Chapter 38 strings:

shell: run counter pid=
shell: counter exited code 4

should be replaced in the scripted shell portion because the new smoke flow is specifically checking path-based launch behavior now.

Also update the final success banner:

print("All Chapter 39 checks passed.")

That sounds small, but it matters when reading CI logs and release artifacts later.


13. What the user should see now

Inside the user shell:

ush> run /programs/counter alpha beta
Program: launching counter argc=3 ppid=...
shell: run /programs/counter pid=...
counter: argc=3
counter: argv[0]=/programs/counter
counter: argv[1]=alpha
counter: argv[2]=beta
counter: tick 1
counter: tick 2
counter: tick 3
shell: /programs/counter exited code 4

A missing path should fail cleanly:

ush> run /programs/missing
run: failed to launch /programs/missing

And plain registry launches should still work:

ush> run counter alpha beta

That backward compatibility is intentional.

Chapter 39 extends the interface; it does not break the earlier one.


14. Why this chapter still is not filesystem execution

It is important to say this literally.

After Chapter 39, Toyix still does not do this:

open /programs/counter
read an ELF image from that file
load that file as the executable

Instead it does this:

recognize /programs/counter
extract the tail name counter
look up counter in the embedded program registry
launch the embedded ELF image for counter

So the bridge is semantic, not yet storage-backed.

That is exactly the right scope for this chapter.

We already built the namespace in Chapter 38.

Chapter 39 lets the launch path speak that namespace.

Later chapters can make it more dynamic, more VFS-driven, and more Unix-like.


15. Files changed in this chapter

The final file list for this chapter should be:

include/kernel/program.h
kernel/program.c
tests/smoke.py
README.md
CHANGELOG.md
index.md
docs/roadmap.md
articles/chapter_39.md

That is another sign the chapter is now scoped correctly.

The implementation is mostly:

kernel launch-path normalization
scripted smoke updates
documentation alignment

not a broad shell rewrite and not a new filesystem execution subsystem.


Next Chapter

Now that Toyix can launch programs through /programs/<name>, the next natural step is to make shell job control less PID-centric and more shell-centric.

That means the next chapter can resume our regularly scheduled programming by teaching the shell how to refer to jobs by shell-visible handles instead of raw numeric PIDs.


Resources

Closure

Chapter 39 adds the first real bridge from the /programs filesystem namespace to Toyix program execution by letting the existing launch path accept /programs/<name> wherever it already accepted a registry name, while keeping the shell interface stable and the smoke flow aligned with the actual process lifecycle.

Happy Coding!

Writing A Linux Style Operating System From Scratch

Chapter 38 — Turning /programs into a Real Directory Chapter 40 — Shell Job References with %1

Leave a Reply

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