esh served its purpose. It got QRV to a shell prompt, proved the IPC
stack, and ran through two months of bring-up. But it has no variables, no
conditionals, no scripting. What QRV has needed for a while — and now has
— is a real shell.
v0.26 lands mksh, the MirBSD Korn Shell (R59, 2025-04-26), ported from scratch in a single day. It runs interactively, handles history with cursor keys, shows the current directory in the prompt, and runs sequences of commands without exiting:
MIRBSD KSH QRV-1 (mksh R59 base)
[/]# ls
bin dev proc rd
[/]# cd /disk2/bin
[/disk2/bin]# ls -la
[/disk2/bin]# ./syscall_testing
...
=== Results: 89 tests, 89 passed, 0 failed ===
[/disk2/bin]#
The [/disk2/bin]# prompt is live: mksh re-evaluates $PWD on every
prompt redraw. The cd you just ran is reflected immediately.
This matters beyond the convenience. A real shell means the init script
that lives in modpkg.cpio — the script that starts every daemon at boot
— can now have conditionals, variables, functions, and proper error
handling. That is the foundation of a BSD-style rc system: hardware
probing, conditional driver loading, different boot scenarios from a single
init script. The modest CPIO image suddenly has real expressive power.
What the Port Required
mksh is about 37,000 lines of C. The upstream codebase carries three
decades of platform accretion: EBCDIC support, OS/2, z/OS, Windows, AIX,
Solaris, multiple editor modes (emacs, vi, gmacs), a German-language
boolean type (Wahr/Ja/Nee/isWahr), and a 3,147-line monolithic
sh.h that aggregates everything from every platform the shell has ever
run on.
None of that goes into QRV. The port started with explicit disciplines:
Apache-2.0 relicensing (enabled by the MirOS sublicensing right, with the
original MirOS text preserved), K&R formatting, no fork() by design,
poll() instead of select() as a QRV project rule, and every line
earning its place.
sh.h: from 3,147 lines to 244
The original sh.h was replaced with a 244-line foundation containing
only what the .c files actually use. Everything else was extracted into
topical headers: env.h, shf.h, cclass.h, var.h, lex.h,
proto.h, tree.h, msgs.h. Each domain in its own file, with a
one-line rationale for every declaration that remains.
German booleans
931 substitutions, automated:
Wahr → bool (296 hits)
Ja → true (292)
Nee → false (309)
isWahr → ((bool)) (34)
These were fine in mksh. They have no place in QRV.
The line editor
mksh's edit.c is 5,832 lines: emacs mode, vi mode, gmacs mode, kill
rings, undo, configurable bind tables, modal editing, vi tab-complete
heuristics. QRV needs exactly none of that. A fresh line editor was written
from scratch — 553 lines — covering the keystrokes that actually matter:
^A/^E/^B/^F home / end / back / forward char
^P/^N + arrows previous / next history
^R reverse incremental history search
^K/^U/^W kill-to-eol / kill-line / kill-word-back
^L redraw
DEL/^H/^D backspace / delete / EOF
ESC [A/B/C/D cursor keys
Net: −5,279 lines of code the QRV shell will never need.
No fork()
QRV has no fork() by design. The original exchild() in jobs.c was
135 lines of fork-then-child-or-parent bookkeeping. The replacement is
25 lines: run the AST in this process, update job bookkeeping. External
commands go through posix_spawn() + waitpid() at the TEXEC site in
exec.c. The shell stays resident throughout.
This was also the source of v0.26's last bug. The fork-removal rewrite
unconditionally passed XEXEC to execute(). In upstream mksh, XEXEC
means "I'm a forked child — call unwind(LEXIT) when done." With no
child, that unwind exited the shell after the first external command. Found
and fixed: the test is simple in retrospect — run ls, then run pwd,
and check whether the shell is still alive.
libc foundations
The port surfaced three missing pieces in QRV's libc, all added as part of this release:
setjmp/longjmp. <setjmp.h> had been declared but never implemented.
The RISC-V assembly saves callee-saved integer registers (ra, sp,
s0–s11), mirroring the layout of the kernel's existing xfer-fault
recovery path. Nine new tests: round-trips, the longjmp(env, 0) → 1
POSIX rule, callee-saved values surviving across call boundaries.
posix_spawn / posix_spawnp. The kernel side (_PROC_POSIX_SPAWN) and
the spawnattr_* / file_actions_* helpers already existed. The entry
point symbols were simply missing. Adapted from openqnx's reference
implementation with the Adaptive Partitioning wire format removed (it was
removed from QRV's taskman in an earlier release).
Signal API. POSIX signals implemented on top of QRV's existing
pulse model. A per-process signal table in libc; the system thread that
every QRV process already has translates _PULSE_CODE_SIGNAL pulses into
sigaction-registered handlers. sigaction, sigprocmask, signal,
raise, and kill all work. 19 new tests, all passing. raise(SIGUSR1)
runs the handler synchronously before returning; kill(getpid(), SIGUSR2)
round-trips through taskman → pulse → system thread → handler.
The test suite grew from 61 to 89 tests across this release: 9 for setjmp/longjmp, 19 for signals. All 89 pass.
qrvfs Format v2
One more thing that landed quietly: the filesystem got a format upgrade. qrvfs v1 used 9 direct + 1 single-indirect block, capping file size at ~2 MiB. That was fine for a test image. It is not fine for anything approaching real use.
v2 repartitions the same 10 address slots: 7 direct + single + double +
triple indirect. Maximum file size: ~512 GiB. mkfs-qrv was rewritten
around a recursive walk_indirect() that allocates index blocks lazily.
A 1.1 GiB test file round-trips byte-perfect, exercising the triple
indirect path.
v2 superblocks are incompatible with v1. qrvfs_init() rejects v1 images
with a clear error.
What Comes Next
Background jobs (posix_spawn with detach rather than waitpid), poll()
with real readiness probing (wiring _IO_NOTIFY through the resmgr layer),
and getppid() / umask() / ioctl() / tcflush() — the eight libc
stubs that currently back the shell's less-used features. Then the init
script gains real conditionals, and the rc system starts to take shape.
No comments:
Post a Comment