- 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
Post Stastics
- This post has 2812 words.
- Estimated read time is 13.39 minute(s).
In Chapter 26, we added:
process table PID lookup ps runbg wait PID
That gave us a real background process path.
But our only user program, demo, waits for terminal input:
user>
That makes it awkward for runbg, because a background process can compete with the monitor for keyboard input.
This chapter adds a second embedded user program:
counter
counter does not read from stdin. It prints its arguments, counts a few times, sleeps between messages, and exits.
That gives us a safe program for testing:
toyix> runbg counter alpha beta toyix> ps toyix> wait 2
The milestone output will look like:
Program registry: registered 2 embedded program(s) Program test: starting background counter test Program: launching counter argc=3 Process: created pid=1 name=counter counter: argc=3 counter: argv[0]=counter counter: argv[1]=alpha counter: argv[2]=beta counter: tick 1 counter: tick 2 counter: tick 3 Syscall: process counter pid=1 exited code 4 Process: destroyed pid=1 name=counter Program test: background counter cleanup sanity check passed
1. What this chapter adds
Add:
user/ └── counter.c
Modify:
kernel/program.c Makefile tests/smoke.sh
No kernel syscall changes are needed.
This chapter mostly expands the userland build pipeline from:
one embedded program
to:
multiple embedded programs
2. Add user/counter.c
// user/counter.c
#include "toyix_syscall.h"
static toyix_u32 str_len(const char *text) {
toyix_u32 len = 0;
while (text[len] != '\0') {
len++;
}
return len;
}
static void write_str(const char *text) {
toyix_write(FD_STDOUT, text, str_len(text));
}
static void write_uint(toyix_u32 value) {
char buffer[11];
toyix_u32 index = 0;
if (value == 0) {
write_str("0");
return;
}
while (value > 0 && index < sizeof(buffer)) {
buffer[index++] = (char)('0' + (value % 10u));
value /= 10u;
}
while (index > 0) {
char ch = buffer[--index];
toyix_write(FD_STDOUT, &ch, 1);
}
}
int main(int argc, char **argv) {
write_str("counter: argc=");
write_uint((toyix_u32)argc);
write_str("\n");
for (int i = 0; i < argc; ++i) {
write_str("counter: argv[");
write_uint((toyix_u32)i);
write_str("]=");
write_str(argv[i]);
write_str("\n");
}
for (toyix_u32 i = 1; i <= 3; ++i) {
write_str("counter: tick ");
write_uint(i);
write_str("\n");
toyix_sleep(2);
}
return 4;
}
This program exits with code 4.
That gives the kernel test an easy value to verify.
3. Why this program is background-safe
demo does this:
toyix_read(FD_STDIN, buffer, sizeof(buffer));
counter does not.
It only uses:
SYS_WRITE SYS_SLEEP SYS_EXIT
That means counter can run in the background without stealing terminal input from the monitor.
This is exactly the kind of program we need for runbg.
4. Update the Makefile for multiple user ELFs
Chapter 23 added a single user build path for:
user/demo.c
Now we need to build and embed two programs:
user/demo.c user/counter.c
There are two ways to do this.
The clean future path is a pattern-based user-program build system.
For this chapter, we will keep it explicit so it is easy to read and debug.
5. Add counter objects to OBJS
Find the kernel OBJS list and add:
build/user/counter_elf_blob.o
The end of the object list should now look like this:
build/drivers/console/serial.o \
build/drivers/console/vga_text.o \
build/drivers/input/keyboard.o \
build/user/demo_elf_blob.o \
build/user/counter_elf_blob.o
Order does not matter much here, but keeping all embedded user blobs together is clearer.
6. Add Makefile rules for counter
Keep the existing demo rules.
Add these after the demo user build rules:
build/user/counter.o: user/counter.c user/include/toyix_syscall.h | build/user
$(CC) $(USER_CFLAGS) -c $< -o $@
build/user/counter.elf: build/user/crt0.o build/user/counter.o user/linker.ld | build/user
$(CC) $(USER_LDFLAGS) build/user/crt0.o build/user/counter.o -o $@
build/user/counter_elf_blob.o: build/user/counter.elf | build/user
$(OBJCOPY) \
-I binary \
-O elf32-i386 \
-B i386 \
--rename-section .data=.rodata.usercounter,alloc,load,readonly,data,contents \
--redefine-sym _binary_build_user_counter_elf_start=user_counter_elf_start \
--redefine-sym _binary_build_user_counter_elf_end=user_counter_elf_end \
$< $@
Now the build generates:
build/user/demo.elf build/user/counter.elf
and embeds both into the kernel.
7. Avoid map-file collision
In Chapter 23, USER_LDFLAGS probably included this:
-Wl,-Map,build/user/demo.map
That is fine for one user program, but now both demo.elf and counter.elf would write the same map file.
Change USER_LDFLAGS to remove the fixed map file:
USER_LDFLAGS := \
-nostdlib \
-ffreestanding \
-m32 \
-Wl,-T,user/linker.ld \
-Wl,--build-id=none
Then add program-specific map files in each link rule.
Update demo.elf:
build/user/demo.elf: build/user/crt0.o build/user/demo.o user/linker.ld | build/user
$(CC) $(USER_LDFLAGS) -Wl,-Map,build/user/demo.map build/user/crt0.o build/user/demo.o -o $@
Update counter.elf:
build/user/counter.elf: build/user/crt0.o build/user/counter.o user/linker.ld | build/user
$(CC) $(USER_LDFLAGS) -Wl,-Map,build/user/counter.map build/user/crt0.o build/user/counter.o -o $@
Now each user program gets its own map file.
8. Inspect both user ELFs
After building, run:
make build/user/demo.elf make build/user/counter.elf i686-elf-readelf -h build/user/demo.elf i686-elf-readelf -l build/user/demo.elf i686-elf-readelf -h build/user/counter.elf i686-elf-readelf -l build/user/counter.elf
Both should show:
Class: ELF32 Data: 2's complement, little endian Type: EXEC Machine: Intel 80386 Entry point address: 0x40100000
Both should have at least one LOAD segment beginning at or near:
0x40100000
9. Update kernel/program.c
Add the new embedded symbols:
extern const uint8_t user_demo_elf_start[]; extern const uint8_t user_demo_elf_end[]; extern const uint8_t user_counter_elf_start[]; extern const uint8_t user_counter_elf_end[];
Then update the program registry.
Replace:
static const embedded_program_t programs[] = {
{
.name = "demo",
.description = "compiled user-mode demo program",
.image_start = user_demo_elf_start,
.image_end = user_demo_elf_end
}
};
with:
static const embedded_program_t programs[] = {
{
.name = "demo",
.description = "interactive stdin/stdout demo",
.image_start = user_demo_elf_start,
.image_end = user_demo_elf_end
},
{
.name = "counter",
.description = "background-safe counter demo",
.image_start = user_counter_elf_start,
.image_end = user_counter_elf_end
}
};
Now programs will list two entries:
demo - interactive stdin/stdout demo counter - background-safe counter demo
10. Replace program_test_once()
In Chapter 26, program_test_once() used demo and injected keyboard input.
Now we want the dedicated background test to use counter.
Replace the function with this version:
void program_test_once(void) {
console_writeln("Program test: starting background counter test");
static const char *argv[] = {
"counter",
"alpha",
"beta"
};
process_t *process = 0;
int rc = program_run_background(
"counter",
3,
argv,
&process
);
if (rc != 0 || process == 0) {
kernel_panic("program test could not launch counter");
}
uint32_t pid = process->pid;
console_write("Program test: background pid=");
console_write_u32_dec(pid);
console_putc('\n');
process_list();
process_t *found = process_find(pid);
if (found != process) {
kernel_panic("program test could not find background process by PID");
}
uint32_t exit_code = process_wait(process);
if (exit_code != 4) {
kernel_panic("program test received wrong counter exit code");
}
process_list();
process_destroy(process);
console_writeln("Program test: background counter cleanup sanity check passed");
}
Notice what is gone:
keyboard_debug_inject_char(...)
The test no longer needs synthetic keyboard input.
That is the whole point of adding counter.
11. Remove unused include from kernel/program.c
If program.c no longer uses keyboard injection, remove:
#include "drivers/input/keyboard.h"
The top of kernel/program.c should now look like:
#include <stddef.h> #include <stdint.h> #include "kernel/console.h" #include "kernel/elf_loader.h" #include "kernel/panic.h" #include "kernel/process.h" #include "kernel/program.h" #include "kernel/string.h" #include "kernel/thread.h"
You may still need kernel/thread.h because program_test_once() waits on the process through process_wait(), which itself sleeps, but if program.c does not directly call thread functions anymore, you can remove it too.
In the version above, program.c does not directly call thread_sleep_ticks(), so this include can also be removed unless another function needs it:
#include "kernel/thread.h"
A minimal include set is:
#include <stddef.h> #include <stdint.h> #include "kernel/console.h" #include "kernel/elf_loader.h" #include "kernel/panic.h" #include "kernel/process.h" #include "kernel/program.h" #include "kernel/string.h"
12. Update monitor behavior
No required monitor code changes are needed.
The existing commands automatically benefit:
toyix> programs
now shows:
Embedded programs: demo - interactive stdin/stdout demo counter - background-safe counter demo
And runbg can now be used safely:
toyix> runbg counter A B runbg: started counter pid=2 toyix> ps PID STATE EXIT NAME 2 running - counter toyix> wait 2 counter: argc=3 counter: argv[0]=counter counter: argv[1]=A counter: argv[2]=B counter: tick 1 counter: tick 2 counter: tick 3 Syscall: process counter pid=2 exited code 4 wait: pid 2 exited code 4
Depending on timing, some counter output may appear before or after the ps prompt. That is expected because it runs concurrently.
13. Update Makefile greps
Replace:
grep -q "Program registry: registered 1 embedded program(s)" build/test.log
with:
grep -q "Program registry: registered 2 embedded program(s)" build/test.log
Replace the program descriptions:
grep -q "demo - compiled user-mode demo program" build/test.log
with:
grep -q "demo - interactive stdin/stdout demo" build/test.log grep -q "counter - background-safe counter demo" build/test.log
Replace the Chapter 26 test lines:
grep -q "Program test: starting background process table test" build/test.log grep -q "Program test: background process table cleanup sanity check passed" build/test.log grep -q "Program: launching demo argc=3" build/test.log grep -q "Process: created pid=1 name=demo" build/test.log grep -q "argv\\[0\\]=demo" build/test.log grep -q "echo: toyix" build/test.log grep -q "Syscall: process demo pid=1 exited code 9" build/test.log grep -q "Process: destroyed pid=1 name=demo" build/test.log
with:
grep -q "Program test: starting background counter test" build/test.log grep -q "Program: launching counter argc=3" build/test.log grep -q "Process: created pid=1 name=counter" build/test.log grep -q "counter: argc=3" build/test.log grep -q "counter: argv\\[0\\]=counter" build/test.log grep -q "counter: argv\\[1\\]=alpha" build/test.log grep -q "counter: argv\\[2\\]=beta" build/test.log grep -q "counter: tick 1" build/test.log grep -q "counter: tick 2" build/test.log grep -q "counter: tick 3" build/test.log grep -q "Syscall: process counter pid=1 exited code 4" build/test.log grep -q "Process: destroyed pid=1 name=counter" build/test.log grep -q "Program test: background counter cleanup sanity check passed" build/test.log
The updated process/program test block should look like this:
grep -q "Address space: kernel address space registered" build/test.log
grep -q "Process: process table initialized" build/test.log
grep -q "Program registry: registered 2 embedded program(s)" build/test.log
grep -q "Embedded programs:" build/test.log
grep -q "demo - interactive stdin/stdout demo" build/test.log
grep -q "counter - background-safe counter demo" build/test.log
grep -q "usage: runbg PROGRAM" build/test.log
grep -q "usage: wait PID" build/test.log
grep -q "Program test: starting background counter test" build/test.log
grep -q "Address space: created process page directory" build/test.log
grep -q "ELF32: loaded PT_LOAD vaddr=0x40100000" build/test.log
grep -q "ELF32: entry=0x40100000" build/test.log
grep -q "Process: initial stack argc=3" build/test.log
grep -q "Program: launching counter argc=3" build/test.log
grep -q "Process: created pid=1 name=counter" build/test.log
grep -q "Program test: background pid=1" build/test.log
grep -q "PID STATE" build/test.log
grep -q "counter: argc=3" build/test.log
grep -q "counter: argv\\[0\\]=counter" build/test.log
grep -q "counter: argv\\[1\\]=alpha" build/test.log
grep -q "counter: argv\\[2\\]=beta" build/test.log
grep -q "counter: tick 1" build/test.log
grep -q "counter: tick 2" build/test.log
grep -q "counter: tick 3" build/test.log
grep -q "Syscall: process counter pid=1 exited code 4" build/test.log
grep -q "Address space: destroyed process page directory" build/test.log
grep -q "Process: destroyed pid=1 name=counter" build/test.log
grep -q "Program test: background counter cleanup sanity check passed" build/test.log
Update the final success message:
@echo "Boot, memory, heap, sync, monitor, process table, and background counter smoke test passed."
14. Update tests/smoke.sh
No structural change is needed.
#!/usr/bin/env bash set -euo pipefail make clean make test make test-exception make test-page-fault echo "All Chapter 27 checks passed."
15. Expected boot output
A successful boot should include:
Program registry: registered 2 embedded program(s) ... Embedded programs: demo - interactive stdin/stdout demo counter - background-safe counter demo ... Program test: starting background counter test Address space: created process page directory ELF32: loaded PT_LOAD vaddr=0x40100000 ... ELF32: entry=0x40100000 Process: initial stack argc=3 esp=0x6FFFF... Program: launching counter argc=3 Thread: created counter id=... Process: created pid=1 name=counter Program test: background pid=1 PID STATE EXIT NAME 1 running - counter counter: argc=3 counter: argv[0]=counter counter: argv[1]=alpha counter: argv[2]=beta counter: tick 1 counter: tick 2 counter: tick 3 Syscall: process counter pid=1 exited code 4 Threads: reaping zombie counter id=... PID STATE EXIT NAME 1 exited 4 counter Address space: destroyed process page directory, user pages=... tables=... Process: destroyed pid=1 name=counter Program test: background counter cleanup sanity check passed
The exact interleaving may vary slightly because counter is scheduled as a real process.
16. Interactive test
After boot, try:
toyix> programs
Expected:
Embedded programs: demo - interactive stdin/stdout demo counter - background-safe counter demo
Then:
toyix> runbg counter one two
Expected:
Program: launching counter argc=3 Process: created pid=... runbg: started counter pid=...
Then:
toyix> ps
Depending on timing, the process may be running or already exited:
PID STATE EXIT NAME 2 running - counter
or:
PID STATE EXIT NAME 2 exited 4 counter
Then:
toyix> wait 2
Expected:
wait: pid 2 exited code 4 Process: destroyed pid=2 name=counter
17. Common failures
Failure: undefined user_counter_elf_start
Check the objcopy rule for counter.
The rule must include:
--redefine-sym _binary_build_user_counter_elf_start=user_counter_elf_start --redefine-sym _binary_build_user_counter_elf_end=user_counter_elf_end
Verify with:
i686-elf-nm build/user/counter_elf_blob.o
You should see:
user_counter_elf_start user_counter_elf_end
Failure: registry still says one program
Check that the registry array has two entries and that program_count is computed from the array:
static const uint32_t program_count =
sizeof(programs) / sizeof(programs[0]);
Do not hardcode:
static const uint32_t program_count = 1;
Failure: runbg counter says unknown program
Check the registry name:
.name = "counter"
and make sure program_find() uses kstrcmp() correctly:
if (kstrcmp(programs[i].name, name) == 0)
Failure: counter exits with wrong code
The user program should return:
return 4;
crt0.S should turn main()’s return value into SYS_EXIT:
call main mov %eax, %ebx mov $2, %eax int $0x80
Failure: counter runs but wait PID hangs
Likely causes:
process_exit_current() did not mark process->exited process_wait() is waiting on the wrong process pointer process table has stale PID/pointer
Check:
process->exited = 1; process->state = PROCESS_EXITED;
inside process_exit_current().
18. What this chapter achieved
Before this chapter:
only one embedded user program runbg existed but the only program read stdin
After this chapter:
two embedded user programs demo = interactive stdin/stdout test counter = background-safe process test runbg counter works without keyboard contention ps/wait can manage real background work
This makes the monitor much more useful.
19. Design limitations
The user-program build system is still explicit and repetitive.
We now have two sets of similar rules:
demo.o demo.elf demo_elf_blob.o counter.o counter.elf counter_elf_blob.o
That is okay for two programs, but it will get ugly with five or ten.
A later chapter should introduce pattern rules or a USER_PROGRAMS variable.
Other limitations remain:
no filesystem exec no PATH no job control no process ownership tree no kill no nonblocking wait no stdout serialization beyond console lock
But now the process table has a background-safe workload.
20. Commit this chapter
After tests pass:
git status git add . git commit -m "Add background-safe counter user program"
21. Next chapter
The next cleanup should be the user-program build system.
Instead of hand-writing rules for every program, we can introduce:
USER_PROGRAMS := demo counter
and generate:
build/user/<name>.o build/user/<name>.elf build/user/<name>_elf_blob.o
That will make adding new user programs much easier before we build a small shell-like user program or start filesystem work.
22. Resources
23. Closure
Chapter 27 gives Toyix a practical background-safe workload. The kernel now embeds two compiled user programs, can launch counter without fighting the monitor for stdin, and can verify background launch, PID lookup, wait, exit status, and cleanup through a deterministic process test path.
Happy Coding!