Project05 - Octox User Programs and System Calls¶
Due Mon May 11th by 11:59pm in your Project05 GitHub repo
Links¶
Tests: https://github.com/USF-CS631-S26/tests
Background¶
You have just walked through the UNIX system call interface in the
UNIX System Calls lecture
and the nine ex_* example programs in src/user/bin/. This project is the
follow-on: write your own Octox user programs that use those same syscalls
to build small versions of familiar UNIX tools.
You will write four new user programs and modify the Octox shell to add a
history builtin. Each program is a .rs file under src/user/bin/ plus
a matching [[bin]] entry in src/user/Cargo.toml. The
Octox Guide walks through how to add a user
program from scratch if you have not done so before.
Setup¶
Clone the Octox repo into your Project05 GitHub repo. Build it once to confirm your toolchain works:
$ cd octox
$ cargo build --target riscv64gc-unknown-none-elf
$ cargo run --target riscv64gc-unknown-none-elf
You should land at the $ shell prompt. Ctrl-A x exits QEMU.
Requirements¶
- Write four new user programs:
seq,tail,diff, andpipecount. - Modify
src/user/bin/sh.rsto add ahistorybuiltin. - Each new program must have a matching
[[bin]]entry insrc/user/Cargo.tomlso the build system picks it up. The file system strips the leading_so the binary appears at/bin/<name>. - All programs must produce output matching the automated tests exactly.
- Use the
ulib::syssyscalls and helpers (File,BufReader,Command, etc.) — do not pull in additional crates.
Section 1: seq N¶
Print the integers 1 through N, one per line. If N is 0 the program
produces no output. The argument is a non-negative decimal integer.
Examples:
Hints: Look at src/user/bin/echo.rs for argv parsing and
src/user/bin/ex_args.rs from the lecture for the simplest possible
write-loop pattern. The println! macro from ulib ultimately calls
sys::write(STDOUT_FILENO, ...).
Section 2: tail [-n N] FILE¶
Print the last N lines of FILE. The default for N is 10. If the
file has fewer than N lines, print the entire file.
Examples:
$ seq 12 > /t
$ tail /t
3
4
5
6
7
8
9
10
11
12
$ tail -n 3 /t
10
11
12
$ tail -n 1 /t
12
$ tail -n 20 /t
1
2
3
4
5
6
7
8
9
10
11
12
Hints: Mirror src/user/bin/head.rs. Use BufReader::new(File::open(path))
and either read_line or the lines() iterator from ulib::io::BufRead.
The simplest implementation reads every line into a Vec<String> then
prints the suffix.
Section 3: diff FILE1 FILE2¶
Read both files line-by-line and emit a record for every line index where the two files differ. The record format is:
If one file has more lines than the other, treat the missing lines as
empty. When a side is empty, print < or > alone (no trailing space).
If the two files are identical, produce no output.
Examples:
$ seq 3 > /a
$ seq 3 > /b
$ diff /a /b
$ seq 3 > /a
$ seq 5 > /b
$ diff /a /b
line 4
<
---
> 4
line 5
<
---
> 5
$ seq 5 > /a
$ seq 3 > /b
$ diff /a /b
line 4
< 4
---
>
line 5
< 5
---
>
Hints: Open both files, read all lines from each into a Vec<String>,
then iterate up to the longer of the two. The pattern from head.rs /
tail.rs carries over — you are doing it twice.
Section 4: pipecount CMD1... : CMD2...¶
Run CMD1 | CMD2, but with the parent process sitting in the middle
counting bytes:
After both children exit, print pipecount: N bytes to your stdout where
N is the total number of bytes that flowed from CMD1 into the parent
(equivalently, the bytes the parent forwarded into CMD2).
The two commands are separated by a single literal : argument. Each side
may have its own arguments. Examples:
$ pipecount seq 5 : wc
l=5, w=5, c=10
pipecount: 10 bytes
$ seq 5 > /a
$ pipecount cat /a : wc
l=5, w=5, c=10
pipecount: 10 bytes
Required syscalls (use these directly from ulib::sys):
sys::pipe— create the two pipessys::fork— twice, once per childsys::dup2— wire each child's stdout/stdin to the right pipe endsys::close— close every pipe end you do not use, in every processsys::exec— start each child programsys::read/sys::write— forward bytes through the parentsys::wait— twice, to reap both children
Hints: src/user/bin/ex_pipe2.rs is the recipe for a 1-pipe pipeline
(ls | wc). Your pipecount is the same idea with two pipes — the
parent sits between them, looping read from pipe1[0] and write into
pipe2[1]. After CMD1 closes its write end, your read on pipe1[0]
returns Ok(0) (EOF) and you must close(pipe2[1]) so CMD2 sees its own
EOF on stdin. Forgetting any close will deadlock the pipeline.
To exec a command from a vector of args, build the path
"/bin/<name>" and pass the original &[&str] argv directly to
sys::exec. (Octox's mkfs strips the leading _, so _seq lands at
/bin/seq.)
Section 5: Add a history builtin to sh¶
Modify src/user/bin/sh.rs so that the shell records every non-empty
command line the user enters and provides a history builtin that prints
those lines, numbered starting at 1, in entry order. The history
command itself counts as an entry — it appears as the most recent line in
its own output.
Example:
Hints: The existing shell already special-cases cd, export, and
exit (around line 49 of sh.rs). Add history alongside them. Track
entries in a local Vec<String> declared just before the main loop and
push to it after each read_line. Only the parent process should print
the history (so it does not run inside a piped subprocess) — guard with
the existing if num == 0 check used by the other builtins.
Building and Running¶
In your project repo:
# build everything (kernel + user programs)
$ cargo build --target riscv64gc-unknown-none-elf
# boot the OS in QEMU; lands at the $ prompt
$ cargo run --target riscv64gc-unknown-none-elf
# exit QEMU
Ctrl-A x
To run a single command end-to-end (the way the autograder tests it),
use runoctox.py:
Each argument to runoctox.py is one shell command. Tests that need
fixture files chain them in a single invocation:
Tips and Pitfalls¶
println!needsprint!in scope. Add both to youruseline:use ulib::{print, println, ...};. The macro expands to aprint!call.- Read until EOF.
sys::readmay return fewer bytes than the buffer holds.Ok(0)is the only EOF signal. Loop until you see it. (ex_countin the lecture is the canonical example.) - Close every pipe end you don't use. This is the most common source
of "my pipeline hangs forever" bugs. The parent in
pipecountholds both ends of both pipes afterpipe(); if you forget to closepipe2[1]after EOF, CMD2 will wait for input that never comes. execdoes not return on success. If you reach the line aftersys::exec, exec failed. Always follow it withsys::exit(1)(or a panic) so the path is well-defined.- The shell does not handle quotes. Tokens are split on whitespace only. Do not write tests that depend on quoted strings as arguments.
Grading¶
Tests: https://github.com/USF-CS631-S26/tests
Grading is based on automated tests (100 points total):
| Tests | Points | Description |
|---|---|---|
| seq-01, seq-02 | 10 | Basic seq cases (5 each) |
| tail-01..03 | 15 | Default N, custom N, N larger than file (5 each) |
| diff-01..03 | 15 | Identical, /a longer, /b longer (5 each) |
| pipecount-01..03 | 30 | seq through wc, cat through wc, larger seq (10 each) |
| history-01, -02 | 30 | Basic history, longer history (15 each) |
| Total | 100 |
Code Quality¶
Code quality deductions may be applied and can be earned back. We are looking for:
- Consistent spacing and indentation
- Consistent naming and commenting
- No commented-out ("dead") code
- No redundant or overly complicated code
- A clean repo, that is no build products, extra files, etc.