{
let n = states.len();
(0..n)
.map(|off| (self.next + off) % n)
.find(|&i| states[i] == ProcState::Runnable)
.map(|i| { self.next = (i + 1) % n; i })
}
```
`sched.rs:20` — consider `n` offsets, wrap from where we left off, take the
first runnable, remember to resume after it.
**Lazy:** `map` computes one index per `find` request; `find` stops on success.
No intermediate list, no allocation — the compiled code is a hand-written loop.
---
## The Line This Session Aims At
```rust
static mut PROCS: [Proc; NPROC] = [const { Proc::new() }; NPROC]; // proc.rs:65
pub const NPROC: usize = 64; // param.rs:7
```
Sixty-four slots, decided when the kernel is compiled. Never sixty-five.
In ordinary Rust this looks like a beginner's mistake — surely a growable
Vec<Proc> is better? It is not, for three reasons that generalize.
---
## Boot Order Decides It
flowchart TD
A["_entry: machine boots\nPROCS already exists in .bss"] --> B["uart::init()\nwe can print"]
B --> C["kalloc::init()\na PAGE allocator, 4096-byte chunks"]
C --> D["vm::kvminithart()\nthe MMU comes on"]
D --> E["proc::init()\nwalk PROCS, mark slots Unused"]
E --> F["trap::init(), fs init"]
F --> G["38k: kheap registers the\nglobal allocator — NOW Vec works"]
style A fill:#e8f5e9,stroke:#00543c
style G fill:#fff3cd,stroke:#FDBB30
---
## Reason 1: No Allocator Yet
- `PROCS` is used at `proc::init()` — the fourth line of `kinit` (`main.rs:87`)
- `Vec` calls the **global allocator**; rv6 has none until exercise 38k (`kheap.rs:40`)
- `34k_processes` has no `kheap.rs`; `38k_semaphores/main.rs:18` is the first `extern crate alloc`
This is the shape of every boot. kalloc::init() walks physical memory to
build the free list — it is what makes allocation possible, so it cannot
allocate. Anything needed earlier must be a fixed-size static.
---
## Reason 2: The Fault Path Must Not Allocate
The process table is touched from the trap handler: timer interrupt, page
fault, system call. The rule there is absolute.
- Allocation can **fail** — a page-fault handler has nowhere to report failure
- Allocation takes **unbounded time** — you cannot wait with interrupts disabled
- Allocation **takes locks** — deadlock against the code you interrupted
Linux fights this with `GFP_ATOMIC` and a ban on sleeping allocators in
interrupt context. A fixed array simply **cannot break the rule**.
---
## Reason 3: A Hard Limit Fails Honestly
**Array:** `allocproc` scans for `Unused`, returns null if full
(`proc.rs:107`, `:134`) → `fork` returns `-1` → the machine keeps
running. Testable: fill the table, check that the 65th `fork` fails.
**Vec:** the limit is "until memory runs out" — failure arrives late, at an
unrelated allocation, possibly on the trap path. You cannot test it.
"We might need more than N" is answered with a compile-time constant and an
error, not with growth.
---
## How Everyone Else Does It
- **xv6**: `struct proc proc[NPROC];`, `#define NPROC 64` — identical, decades older
- **Linux**: `task_struct` from a slab cache, *but* `pid_max`, `RLIMIT_NPROC`, `file-max`, and statically reserved per-CPU interrupt stacks
- **Power of Ten** (JPL/NASA), rule 3: no dynamic allocation after initialization
- **MISRA C**: `malloc` banned outright
- **seL4**: no kernel heap at all
The difference is where the boundary sits, not whether one exists.
---
## Sizing the Constant
| Field | Bytes |
|---|---|
| `state` | 1 |
| `pid`, `kstack`, `xstate`, 3 pointers | 8 each |
| `context` (14 saved registers, `swtch.rs:7`) | 112 |
| `ofile` — `[File; 16]`, `File` is 24 bytes | 384 |
| `name` — `[u8; 16]` | 16 |
| **`size_of::()`** | **568** |
`64 x 568 = 36,352` bytes = 35.5 KiB of `.bss`, reserved whether one process
runs or sixty-four.
---
## rv6's Static Bounds
| Constant | Value | Where | Bounds |
|---|---|---|---|
| `NPROC` | 64 | `param.rs:7` | processes in the system |
| `NOFILE` | 16 | `file.rs:19` | open files per process |
| `NINODE` | 64 | `fs.rs:5` | files in the filesystem |
| `NDIRENT` | 16 | `fs.rs:6` | entries per directory |
| `NAMELEN` | 14 | `fs.rs:7` | bytes in a filename |
| `FILESIZE` | 128 | `fs.rs:8` | bytes in one file |
| `BUF_LEN` | 256 | `console.rs:8` | buffered keystrokes |
Each has a defined failure: `-1`, `FsError::DirFull`, or a dropped keystroke.
---
## The Lowest-Free-Slot Rule
```text
ofile: fd 0 fd 1 fd 2 fd 3 fd 4 ... fd 15
+--------+--------+--------+--------+--------+ +------+
|Console |Console |Console | None | None | ... | None |
+--------+--------+--------+--------+--------+ +------+
stdin stdout stderr ^
open() returns 3: the lowest free index
```
- `allocproc` (`proc.rs:108`) and `fdalloc` (`syscall.rs:298`) are the same scan
- For fds it is the **Unix contract**: `open` returns the lowest unused descriptor
- That is why `cmd > file` = "close fd 1, then open" — you write this in **exercise 50k**
---
## Where `Vec` Does Belong
`Vec` is not bad; it is a tool with a prerequisite.
- **Module 1, host commands, `cargo test`**: use it freely — the OS allocator is already running
- **Kernel, exercise 38k onward**: legal, under two standing rules
- allocate at initialization, never on the trap path
- know your allocator: `KernelHeap::alloc` (`kheap.rs:23`) serves one 4096-byte page per allocation and refuses anything larger
---
## Summary
1. `[T; N]` **is** the data, `Vec` **owns** it, `&[T]` **points at** it
2. A slice is a fat pointer: address + length, 16 bytes, no ownership
3. Take `&[T]` / `&mut [T]` in signatures — one function, every table size
4. Bounds checks are ~3 instructions, usually optimized away
5. Check untrusted indices at the **boundary** (`fd >= NOFILE`)
6. Iterator adapters are lazy and allocate nothing; `collect` is the exception
7. `PROCS: [Proc; NPROC]` — no allocator yet, no allocation on the fault path, honest failure
8. Kernels bound resources statically: a constant and an error, not growth
**Next:** exercise `06r_collections` — build the miniature process table.