`. `exec` takes a full path; no PATH search.
Between fork and exec is where a shell does setup work: open files, dup2, close, setuid, ...
---
## 6. `ex_redir` — Rewire Your Own Stdout
```rust
match sys::fork().expect("fork") {
0 => {
// --- child ---
let fd = sys::open(path,
omode::WRONLY | omode::CREATE | omode::TRUNC).expect("open");
// Redirect stdout. After this call, fd 1 refers to the file.
sys::dup2(fd, STDOUT_FILENO).expect("dup2");
sys::close(fd).expect("close");
println!("hello from child: my stdout is redirected");
sys::exit(0);
}
_ => {
let mut status: i32 = 0;
sys::wait(&mut status).expect("wait");
println!("parent: child finished; my stdout is still the terminal");
}
}
```
---
## `dup2` Picture
Before `dup2(fd, 1)`:
```
fd 0 ─► terminal
fd 1 ─► terminal
fd 2 ─► terminal
fd 3 ─► /tmp/r ← from open()
```
After `dup2(fd, 1)` then `close(fd)`:
```
fd 0 ─► terminal
fd 1 ─► /tmp/r ← writes now land in the file
fd 2 ─► terminal
```
dup2(src, dst) atomically closes dst and makes it an alias for src.
---
## 7. `ex_redir2` — fds Survive `exec`
```rust
match sys::fork().expect("fork") {
0 => {
let fd = sys::open(path,
omode::WRONLY | omode::CREATE | omode::TRUNC).expect("open");
sys::dup2(fd, STDOUT_FILENO).expect("dup2");
sys::close(fd).expect("close");
// fd 1 is already the file. The execed program writes to
// stdout "normally" and never knows it is being redirected.
let argv = ["echo", "hello", "from", "exec"];
sys::exec("/bin/echo", &argv, None).expect("exec");
sys::exit(1);
}
_ => {
let mut status: i32 = 0;
sys::wait(&mut status).expect("wait");
}
}
```
---
## How a Shell Does `cmd > file`
```
fork()
if child:
fd = open(file, WRONLY | CREATE | TRUNC)
dup2(fd, 1)
close(fd)
exec(cmd, argv)
else:
wait()
```
- The program named by `cmd` never knows it's being redirected.
- Works for *any* program — no cooperation needed.
- Same pattern with `dup2(fd, 0)` implements `cmd < file`.
---
## 8. `ex_pipe` — Bytes Between Processes
```rust
let mut p = [0usize; 2];
sys::pipe(&mut p).expect("pipe");
let (read_fd, write_fd) = (p[0], p[1]);
match sys::fork().expect("fork") {
0 => {
// --- child: writer ---
sys::close(read_fd).expect("close");
sys::write(write_fd, b"hello from child\n").expect("write");
sys::close(write_fd).expect("close");
sys::exit(0);
}
_ => {
// --- parent: reader ---
sys::close(write_fd).expect("close");
let mut buf = [0u8; 64];
let n = sys::read(read_fd, &mut buf).expect("read");
sys::close(read_fd).expect("close");
sys::write(STDOUT_FILENO, &buf[..n]).expect("write");
let mut st = 0i32;
sys::wait(&mut st).expect("wait");
}
}
```
---
## The Pipe Invariant
After `pipe` + `fork`, **both** processes hold **both** ends.
read returns EOF on a pipe only when every open write-end fd has been closed. If the reader forgets to close its own copy of the write end, it will read its own EOF — never — and deadlock.
Convention: each side closes the end it does not use, immediately after `fork`.
---
## 9. `ex_pipe2` — `ls | wc` (1/2)
```rust
let mut p = [0usize; 2];
sys::pipe(&mut p).expect("pipe");
let (read_fd, write_fd) = (p[0], p[1]);
// --- child 1: producer (ls) ---
match sys::fork().expect("fork") {
0 => {
sys::dup2(write_fd, STDOUT_FILENO).expect("dup2");
sys::close(read_fd).expect("close");
sys::close(write_fd).expect("close");
let argv = ["ls"];
sys::exec("/bin/ls", &argv, None).expect("exec");
sys::exit(1);
}
_ => {}
}
```
---
## `ex_pipe2` — `ls | wc` (2/2)
```rust
// --- child 2: consumer (wc) ---
match sys::fork().expect("fork") {
0 => {
sys::dup2(read_fd, STDIN_FILENO).expect("dup2");
sys::close(read_fd).expect("close");
sys::close(write_fd).expect("close");
let argv = ["wc"];
sys::exec("/bin/wc", &argv, None).expect("exec");
sys::exit(1);
}
_ => {}
}
// --- parent: CLOSE BOTH ENDS or wc hangs forever ---
sys::close(read_fd).expect("close");
sys::close(write_fd).expect("close");
let mut status: i32 = 0;
sys::wait(&mut status).expect("wait 1");
sys::wait(&mut status).expect("wait 2");
```
---
## `ls | wc` Layout
flowchart LR
P1["child1
ls"] -- "stdout = write_fd" --> PIPE["pipe
buffer"]
PIPE -- "read_fd = stdin" --> P2["child2
wc"]
PARENT["parent
(closes both ends,
waits twice)"] -.-> P1
PARENT -.-> P2
Each child `dup2`'s one pipe end onto stdin or stdout *before* `exec`, so `ls` and `wc` run as if they had a normal terminal on the other side.
---
## Why the Parent Must Close Both
If any process still holds the write end open, wc's final read will block forever waiting for EOF.
- The parent called `pipe` *before* the children existed, so the parent holds both ends too.
- Each child closes its own extras as part of setup.
- The parent must close the ends it held before the children's closes can "count."
---
## Building a Shell from 7 Syscalls
| Shell feature | Syscall recipe |
| Run a command | fork → child exec; parent wait |
| Background | fork → exec; parent does not wait |
cmd > file | fork → open + dup2(fd,1) + close → exec |
cmd < file | fork → open + dup2(fd,0) + close → exec |
a | b | pipe; fork×2; each child dup2s one end; parent closes both, wait×2 |
The Octox shell is built from exactly this toolkit (see `src/user/bin/sh.rs`).
---
## Key Takeaways
- A **syscall** is a function whose body runs in kernel mode; reached via `ecall`.
- **Everything is a file descriptor** — `read`/`write` work uniformly.
- **`fork` returns twice**; parent and child have independent memory.
- **`exec` does not return** on success; fds survive it.
- **`dup2`** is how redirection works — *atomically* rewire an fd.
- **Pipes + fork + dup2** build pipelines.
- **Close every unused fd.** Pipe EOF depends on it.
---
## Further Reading
- The nine programs: `src/user/bin/ex_*.rs` in the [Octox repo](https://github.com/USF-CS631-S26/octox)
- [Octox Guide](../guides/octox-guide.md) — architecture, build, adding a user program
- xv6 book, ch. 1 (OS Interfaces), ch. 8 (File System)
- On Linux: `man 2 fork`, `man 2 execve`, `man 2 open`, `man 2 pipe`, `man 2 dup2`