Skip to content

Using P32W Unix

P32W Unix is the P32W's own multi-user operating system (D22; the design is os-design.md, the kernel kernel/README.md). It is a small Unix in the style of V6/V7: several users at once, one on each serial port and one at the video console, each process in its own protected address space, a Unix filesystem on the CompactFlash card, and the classic commands.

Running it

make -C emulator                      # the emulators
make -C software disk                 # the C library, the commands, the kernel, software/build/disk.img
python3 assembler/p32asm.py software/monitor/monitor.asm -o monitor.bin
emulator/p32emu -d software/build/disk.img monitor.bin

tools/p32w does the last two lines for you (and builds what is missing). At the monitor's > prompt type b: the monitor loads the kernel from the card and the kernel boots. Your terminal is UART 0 (the console); ^] stops the emulator. When idle, the emulated machine waits in real time.

A second user gets UART 1 over TCP:

emulator/p32emu -T 2323 -d software/build/disk.img monitor.bin
python3 tools/p32term.py              # in another window: login: ...
tools/p32term.py sends each key at once, as the console does (so q in top, vi and ^C work) and leaves with ^]. Plain nc localhost 2323 also connects, but the Mac then holds each line until Return, and ^C ends nc. (For scripts: -A FILE feeds UART 1 and -a FILE records it; -w feeds input a line at a time, each after a prompt — software/tests/run_session.sh.)

On the real machine: write software/build/disk.img to a CF card (dd if=software/build/disk.img of=/dev/diskN bs=1m), boot, b.

Logging in

User Password Home
root root /
ken p32 /usr/ken
guest (none) /usr/guest

Change a password with passwd. su becomes root (with root's password). who lists who is logged in; id / whoami show who you are. Log out with exit (or ^D); init starts a new login: on that terminal.

Shut down as root with shutdown [-r] [now | +minutes] [message]: it warns both terminals (each minute, with the message), stops the logins, ends every process, writes everything to the disk and marks the filesystem clean, then halts — or with -r restarts the machine into the monitor (type b to boot again). halt and reboot do the same at once, without the warnings. (Switching off without it is like any Unix without sync: the next boot warns that the filesystem was not unmounted cleanly.)

The shell

sh reads a command per line:

cmd arg ... run a command (found on $PATH: /bin, /usr/bin)
cmd < in > out, >> out redirection
a | b | c a pipeline
a ; b, a & a list; run in the background (the shell prints its pid)
'...', "...", \c quoting ($ is expanded inside "...")
$NAME, ${NAME}, $?, $$, $! variables, the last status, the shell's pid, the last background pid
$0–$9, $#, $* a script's name and arguments (sh script args)
NAME=value set a variable (all variables are passed to commands)
`command` its output, in place (split into words unless in "...")
* ? [...] wildcards (in the last part of a path)
if list; then list; elif list; then list; else list; fi conditions: a command's exit status (test / [)
while list; do list; done, for NAME in words; do list; done loops; break, continue
cd dir, exit [n], unset NAME, : built-ins

More, as in the Bourne shell (man sh): functions (name() { list; }, return), case word in pattern) list ;; esac, here-documents (<<END), subshells ( list ) and groups { list; }, redirection of any descriptor (2> err, 2>&1, >&2), set, shift, read, eval, ., exec, wait, umask, NAME=value cmd for one command, #! scripts, and /etc/profile plus $HOME/.profile for login shells.

A construct may span lines: the shell prompts > until it is complete. test (also [ ... ]) checks files (-f -d -e -r -w -x -s), strings (-z -n = !=) and numbers (-eq -ne -lt -le -gt -ge); expr does arithmetic and comparisons (n=`expr $n + 1`).

^C interrupts the running command, ^\ quits it, ^U erases the line, backspace / DEL erase a character, ^D ends input.

Editing and history (at a terminal):

Key or command What it does
← → (or ^B ^F) move along the line; typing inserts at the cursor
^A, ^E (Home, End) go to the start / end of the line
backspace, ^D, Delete delete before / at the cursor (^D on an empty line ends input)
^K, ^U, ^W delete to the end, the whole line, the word before the cursor
↑ ↓ (or ^P ^N) step through earlier commands
^L redraw the line; ^C abandons it
Tab complete a command name (first word) or a file name; Tab again lists the choices
history list the last 100 commands, numbered (works in pipelines: history \| grep cc)
!!, !n, !-n, !text run the last command again, number n, the n-th last, the last starting with text (the expanded line is shown first)

History is kept per user in $HOME/.sh_history, so it survives logging out. A ! followed by a space or = (as in test a != b) is left alone, as is anything inside '...'.

Commands

ls [-1ACFRSadhilnrst] cat cp mv rm [-rf] ln mkdir rmdir pwd touch chmod chown find du ed grep [-vnci] wc [-lwc] sort [-rn] uniq [-c] head [-n] tail [-n] tee more od [-c] cmp echo basename dirname test / [ expr fsck ps kill [-sig] sleep date stty uname [-a] who id whoami su passwd login sync true false; ps [-u user] [-t tty] [-f]; /etc/adduser [-p] [-u uid] [-g group] [-c comment] name (root: a passwd line, the next free uid from 100, group staff, a home /usr/name of their own — -p makes it private, 700; then passwd name); time command (real, user and system time), cycles command (the same, with the measuring gate up around it: man cycles, D85), uptime (with the load average); top [-d secs] [-n count] [-b] (the busiest processes, redrawn; keys q, k kill, n how many, s delay); kill -l, kill -HUP pid (names or numbers); shutdown, reboot, halt. ps shows each process's CPU time.

Text tools: sed (scripts, -n -e -f -i, s/// with groups, ranges, hold space), tr, cut, paste, join, comm, xargs, diff (-u -c -e -r, and more; its output works with patch and ed), bc (an arbitrary-precision calculator; bc -l for the math library), and awk (POSIX awk: fields, arrays, functions, printf, getline, pipes). Each has a man page; man -k word finds commands by subject.

Compiler tools: yacc (LALR(1) parsers, with -ly) and lex (scanners, with -ll); their output compiles with P32W's own cc.

Lua: lua (Lua 5.4: 32-bit integers, double floats, the string, table, math, io, os, coroutine and utf8 libraries, a REPL) and luac (compile to bytecode). require looks in /usr/lib/lua/5.4 and the current directory. Both are built on the host, since the native cc can't yet hold Lua's largest files in memory.

Users: write user and wall send to terminals, mesg y|n allows or refuses them (terminals are group tty; write and wall are setgid tty); mail user sends and mail reads (/usr/spool/mail, setgid mail; login says "You have mail."). Root has chgrp and /etc/mknod.

Administration (root): df (any user), mount / umount, /etc/mkfs, /etc/fsck [-y] (repairs; after repairing / the machine restarts), su [-] [user] [-c cmd], at and cron (/etc/crontab).

Time zones: the clock keeps UTC; programs show local time by TZ (a POSIX rule, e.g. TZ=PST8PDT,M3.2.0,M11.1.0, TZ=CET-1CEST,M3.5.0,M10.5.0/3) or, without it, the first line of /etc/TZ (echo PST8PDT,M3.2.0,M11.1.0 > /etc/TZ sets it for everyone); neither: UTC. date names the zone; date -u shows and sets UTC; ls -l, who, uptime, top, zip's DOS times and Kermit's dates are local; tar keeps UTC, as its format says.

Archives (D25), exchangeable with the Mac: tar c|t|x[vzf] (POSIX ustar; z through gzip), gzip / gunzip / zcat (-cdfkqtv1..9), zip [-rq0..9] archive files (adds or replaces entries) and unzip [-lqtoan] [-d dir] archive [names]. Modes, owners and modification times travel with the files. On the Mac, make tar archives with COPYFILE_DISABLE=1 tar ... (or the ._name resource-fork files come along); unzip leaves out Finder's __MACOSX/ entries unless -a.

Moving files with Kermit

kermit (D25) moves files over a terminal line: kermit -s file ... sends, kermit -r receives, and kermit -x makes P32W a server that the other side drives entirely. Options: -a name (as), -T (a text file: CRLF on the line, LF here), -e length (longest packet, default 1000), -w window (default 3), -l /dev/tty1 (use the other line), -q. Long packets, sliding windows, CRC checks and attribute packets (type, size, modification time) are negotiated with the other Kermit; a lost or damaged packet is sent again.

From the Mac, with C-Kermit (brew install c-kermit):

  • In the emulator, start p32emu ... -T 5000 (the second terminal on TCP); in C-Kermit: set host /raw-socket localhost 5000, then connect.
  • On the hardware: set line /dev/cu.usbserial-…, set speed 115200, set flow none, set carrier-watch off, then connect.

Log in, type kermit -x, and escape back to C-Kermit (Ctrl-\ then c). Then: send file (or send *.c), get name (wildcards * ?), remote dir, remote cd /tmp, remote delete name, remote type name, and finish to end the server (you are back at the shell). C-Kermit decides text or binary by the file name and says so in its attribute packet; P32W stores text with LF line ends. Times: P32W keeps UTC and C-Kermit sends local time, so received files are off by the time zone until P32W has one.

Later, and regularly

/etc/cron (started by /etc/rc) runs /etc/crontab's lines — minute hour day month weekday command, fields *, n, a-b, a,b, steps /n, local time — as root, each minute. at HH:MM [MM/DD] or at now + N minutes|hours|days queues the commands on its input to run once, as you, in the current directory, output appended to $HOME/at.out; atq lists your jobs, atrm job removes one. sh -c 'commands' runs a command line.

More filesystems

/etc/mkfs special nblocks [ninodes] makes an empty filesystem on a block device; /etc/mount special dir mounts it, /etc/umount special|dir unmounts it (not while a file or a current directory on it is in use); /etc/mount lists them (/etc/mtab); df shows the space on each; /etc/fsck [special] checks one. /dev/ram is a RAM disk of 256 blocks (empty after a reboot): /etc/mkfs /dev/ram 256; /etc/mount /dev/ram /mnt. Links cannot cross filesystems (EXDEV); cp the file instead. Halt, reboot and shutdown write out and mark clean every mounted filesystem.

Debugging

db program [args] runs a program under the debugger, stopped before its first instruction: b where (a function, sym+off or 0xADDR), c, s [n] (step instructions), n [n] (over calls), f (finish the function), bt, r (registers), set REG VALUE, x where [n], p sym, l [where] [n] (disassemble), bl / d [n] (list / delete breakpoints), sym [prefix], q; an empty line repeats s, n or l. It works at the machine level, with the names from the a.out's symbol table (ld writes one; ld -s leaves it out). Underneath: ptrace (system call 26) — a traced program stops at every signal and after exec, and wait reports the stop (WIFSTOPPED).

Programming tools (M7)

cc [-c | -S | -E] [-o OUT] [-I DIR] [-D NAME[=VAL]] [-w] files... is the C compiler (the language of cc/README.md: C89 without floating point, long long, bit-fields or struct arguments and results): cc -o prog prog.c compiles, assembles and links with /usr/lib/libc.a; headers are in /usr/include. Underneath: as [-o file.o] file.s (the syntax of the host's p32asm.py), ld [-o prog] /usr/lib/crt0.o file.o ... /usr/lib/libc.a, ar lib.a file.o ... (or V7 keys: ar r|q|t|x|d[v] lib.a [files]). All four write exactly what the host tools write, and cc compiles itself. Each .c file is compiled in a process of its own, which holds the file's tokens after preprocessing (28 bytes each) and its largest function's tree. Lua's biggest file, lvm.c (53,000 tokens, one function of 35,000), takes 3 MB of a process's 8 MiB (D33, D34); split much bigger programs into several files.

sh lists: a ; b, a & b, a && b (b if a succeeded), a || b (b if it failed); NAME=value and export NAME=value set variables.

Without C-Kermit, tools/pykermit.py --tcp 5000 send|get|dir|finish ... does the same against a P32W server (the tests use it).

vi [file] is the screen editor, for a VT100-style terminal (the Mac's Terminal or nc; LINES / COLUMNS set its size, 24 × 80): counts; h j k l and the arrows, w b e 0 ^ $ G gg H M L, ^F ^B ^D ^U, f t F T ; ,, %; x X r J ~; d c y with a motion or doubled, D C Y p P; i a I A o O R; u (undo), . (repeat); /re ?re n N; ZZ; :w :q :q! :wq :x :e file :r file :N, :[range]s/re/text/[g], :[range]d, :!cmd (ranges N,M . $ %; regular expressions . * ^ $ [...]). Long lines scroll sideways instead of wrapping.

ed is the editor (as on V6: no screen needed): a appends lines until a line with ., p / n print (,p the whole file), d deletes, s/old/new/g substitutes, w writes, q quits; addresses are line numbers, ., $, /pattern/. See the head of software/cmd/ed.c.

Writing programs

On P32W Unix itself: cc prog.c (see man cc; cc -h lists the options), then ./a.out. The C library is Unix-shaped (stdio, string, stdlib, unistd, fcntl, signal, dirent, pwd...; man 3 intro), the system calls are in section 2 (man 2 intro), and db debugs the result.

On the host, the same language is compiled with cc/p32cc.py (C89 without floating point; cc/README.md). To make a program part of the system, add it to software/cmd/, to CMDS in software/Makefile and to software/root.manifest, and make disk.

The system's sources and make

/usr/src holds the sources of P32W Unix, each directory with a makefile for make (man make):

Directory make make install (as root)
/usr/src/cmd rebuilds the commands whose source changed (make ls: one) copies each rebuilt one that differs into /bin or /etc
/usr/src/cc rebuilds the C compiler (make test: it compiles itself, identically) /bin/cc
/usr/src/libc rebuilds libc.a and crt0.o /usr/lib

The disk comes with everything already built there, so a first make says there is nothing to do and later ones rebuild only what was edited. make in /usr/src/kernel builds the kernel, unix.bin, byte for byte the host's (D86), but it has no make install yet: only the host can put a kernel on the disk's boot area (tools/p32fs.py --kernel). The group staff (ken) may build in /usr/src. For your own programs, make hello builds hello from hello.c with no makefile at all.

The manual

man name shows a command's page (a page at a time, through more); man 2 name and man 3 name the system call or library function of that name; man -k word lists the pages about it; man intro explains the sections: 1 commands, 2 system calls, 3 the C library, 5 file formats, 8 administration. The pages are plain text in /usr/man (the source is software/man/; tools/mancheck.py checks their layout). Every command also answers --help (alone) with its page: ls --help is man ls.

The date and time

The I/O card's real-time clock (D23) keeps the time with the power off; P32W Unix reads it at boot and keeps UTC. date prints it; as root, date 2610141230 sets it to 2026-10-14 12:30 ([cc]yymmddhhmm[.ss], as in Unix V7) — the clock is set too. In the emulator the clock follows the Mac's (-R SECONDS fixes a start time, as the tests do).

Devices

Block devices (through the buffer cache): /dev/disk, the filesystem area of the CF card (root only). Character devices: /dev/tty0–/dev/tty3 (the UARTs), /dev/tty4 (the video card's screen and keyboard, D53), /dev/null, /dev/mem (root), /dev/port (reading gives the input port, writing sets the output port), /dev/rdisk (the whole card, raw; root only).