- 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
- 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 985 words.
- Estimated read time is 4.69 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> runbg counter victim ush> jobs ush> kill %1 ush> wait %1
those commands must be typed inside the user shell after toyix> run shell.
Chapter 39 made /programs/<name> usable as a launch target. That fixed the namespace bridge between the filesystem and the embedded program registry, but one brittle test problem remained:
kill 5 wait 5
Those commands only work if every earlier step in the boot script creates exactly the same number of processes.
That is too fragile for a chapter series that keeps growing.
Chapter 40 fixes that by giving the shell its own job references:
%1 %2 %3
The shell still launches the same background children.
The kernel still tracks the same PIDs.
But the shell now gives each background job a local handle that can be used in jobs, kill, and wait.
1. What this chapter adds
Modify:
user/shell.c kernel/program.c tests/smoke.py README.md CHANGELOG.md index.md docs/roadmap.md
No syscall changes are needed.
No kernel scheduler changes are needed.
No VFS changes are needed.
This is a shell-facing workflow improvement built on top of the background-job table that already exists.
2. Why this chapter exists
Toyix already had two related but separate ideas:
kernel PID shell background job
A PID is a kernel identity.
A shell job reference is a user-facing convenience.
Before this chapter, the shell exposed only the kernel PID:
shell: runbg counter pid=5
That meant the test script had to remember 5 and assume nothing else changed before that step.
Chapter 40 makes the shell record a local job number as well:
shell: runbg counter pid=5 job=%1
That lets the shell refer to the same job in a stable way even if process IDs shift later.
3. Supported job references
This chapter supports both of these forms:
5 %1
5 means “use PID 5 directly.”
%1 means “look up shell job 1 and use the PID associated with it.”
Plain PIDs are still supported for compatibility.
The new %N syntax is a shell-local alias, not a kernel feature.
That distinction matters:
kernel PID -> actual process identity shell job %N -> stable shell handle for a background child
4. Shell job table changes
In user/shell.c, each job entry now stores a shell job ID in addition to the PID:
typedef struct shell_job {
toyix_u32 job_id;
toyix_u32 pid;
int active;
char name[SHELL_JOB_NAME_MAX];
} shell_job_t;
The shell keeps a small counter for new job IDs:
static toyix_u32 shell_next_job_id = 1;
That means the first background job in a shell session becomes %1, the next becomes %2, and so on.
The counter is shell-local. It resets when the shell starts.
That is exactly what we want.
5. Parsing %N
The shell needs one small helper to resolve either form:
kill 5 kill %1 wait 5 wait %1
The helper checks whether the argument begins with %.
If it does, the shell looks up the matching job ID in its table and uses that job’s PID.
If it does not, the shell parses the value as a PID directly.
That gives us a narrow and predictable rule:
%N -> job lookup N -> PID lookup
The kernel never sees %1.
Only the shell does.
6. Updated shell commands
runbg now prints the shell job reference when a launch succeeds:
shell: runbg counter pid=5 job=%1
jobs now shows the job reference for each active entry:
shell jobs: %1 pid=5 parent=<shell pid> name=counter state=running
kill accepts either a PID or a job reference:
ush> kill %1 shell: kill requested job=%1 pid=5
wait does the same:
ush> wait %1 shell: wait job=%1 pid=5 name=counter code=128
Plain PID form still works too:
ush> kill 5 ush> wait 5
That compatibility keeps the shell usable for direct debugging while the new job references make scripted tests more robust.
7. Test flow update
The Chapter 40 test sequence in kernel/program.c now uses %1 for the background job:
runbg counter victim jobs kill %1 jobs wait %1 jobs
That change removes the dependency on a hardcoded PID for the shell job-control part of the boot script.
The rest of the scripted flow stays the same.
That is deliberate.
The goal is to test the new shell job reference feature without destabilizing unrelated chapter checks.
8. What the user should see
Inside the user shell:
ush> runbg counter victim shell: runbg counter pid=5 job=%1 ush> jobs shell jobs: %1 pid=5 parent=<shell pid> name=counter state=running ush> kill %1 shell: kill requested job=%1 pid=5 ush> wait %1 shell: wait job=%1 pid=5 name=counter code=128
The local shell job number is the stable reference.
The PID remains visible for debugging.
Both are useful, and both are now shown.
9. What this is not
This chapter does not add:
process groups job control signals foreground/background terminal control shell pipelines shell command history
It is only the first shell-local job reference layer.
That keeps the scope small and makes the chapter easy to verify.
10. Files changed in this chapter
The final file set for this chapter should be:
user/shell.c kernel/program.c tests/smoke.py README.md CHANGELOG.md index.md docs/roadmap.md articles/chapter_40.md
That is a good sign that the chapter stays focused on shell-facing behavior rather than sprawling into unrelated subsystems.
Next Chapter
Now that the shell has stable job references, the next natural step is to keep tightening the program-launch path around the embedded registry and filesystem namespace.
That means Chapter 41 can move on to dynamic /programs behavior driven by the registry instead of static launch tables.
Resources
Closure
Chapter 40 gives Toyix shell-local %N job references so background work can be managed by stable shell handles instead of brittle hardcoded PIDs, while keeping the underlying kernel process model unchanged.
Happy Coding!