Chapter 39 — Path-Based Program Launching with /programs

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

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

Leave a Reply

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