Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e3deb5390 | ||
|
|
d650ff433b | ||
|
|
2aef583aad | ||
|
|
b3785343f8 | ||
|
|
b411836478 | ||
|
|
8d836c66b2 | ||
|
|
dee3affa3b | ||
|
|
020aa6113c | ||
|
|
b3f72cd95e | ||
|
|
5d4d318ebf | ||
|
|
a9f8cb7e0f | ||
|
|
ed81f9a07a | ||
|
|
6e78bb0d5e | ||
|
|
1334f446cc | ||
|
|
f94d19ae47 | ||
|
|
59aff881ac | ||
|
|
42008c26c0 | ||
|
|
cb3d28e3d6 | ||
|
|
2556a955b3 | ||
|
|
655c7d8d01 | ||
|
|
5843503033 | ||
|
|
d7ad3d09f7 | ||
|
|
7d2eca8271 | ||
|
|
afb2e084af | ||
|
|
9c729edac1 | ||
|
|
2651ff2169 | ||
|
|
96389ada8f | ||
|
|
a9d2e14004 | ||
|
|
d437e42b49 | ||
|
|
3676036762 | ||
|
|
4ad49ed5f2 | ||
|
|
438e7af141 | ||
|
|
73dcea7496 | ||
|
|
4743df8c4a | ||
|
|
54b25708c3 | ||
|
|
3113eff7fe | ||
|
|
c77841ce3d | ||
|
|
393bf674f5 | ||
|
|
f4d7f9100d | ||
|
|
2372806862 | ||
|
|
fd9d392416 | ||
|
|
c65bfa3fa3 | ||
|
|
f6c4b6377f | ||
|
|
c6d90e69fa | ||
|
|
cb9b06d46f | ||
|
|
38c89b1a9a | ||
|
|
d8e8ae604f | ||
|
|
5ea03c4cf5 | ||
|
|
b15fb2fdaa | ||
|
|
c79b1d6d6d | ||
|
|
d2c70c9129 |
209 changed files with 29821 additions and 5027 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -36,17 +36,15 @@ dist/
|
|||
|
||||
# Build output under projects/ -- these are programs that run on SchOS, and
|
||||
# their binaries were tracked by accident.
|
||||
projects/schcom/schcom
|
||||
projects/schcom/schcom-demo
|
||||
|
||||
# A built ISO dropped next to the site for download.html. 62 MB; not source.
|
||||
docs/Website/releases/
|
||||
|
||||
# Kernel modules, ever. docs/plan/ldls.md rule 1: LDLS ships the layer and
|
||||
# never the thing it loads, and that applies to this tree as well as to an
|
||||
# image. The loader's test input is built from ldls/test/refmod.c, which is
|
||||
# ours; a .ko here would be someone else's code, with its own provenance, in
|
||||
# a tree that is otherwise all first-party or attributed in NOTICE.
|
||||
# Kernel modules are build artefacts, not source: they are staged into the
|
||||
# image by `make ldls-r8169` / `make ldls-stage` from a Linux build and ship
|
||||
# in the ISO (docs/architecture/ldls.md rule 1), but a binary does not belong
|
||||
# in git. The loader's test input is built from ldls/test/refmod.c, which is
|
||||
# ours.
|
||||
*.ko
|
||||
*.ko.xz
|
||||
|
||||
|
|
|
|||
216
Makefile
216
Makefile
|
|
@ -26,7 +26,7 @@ KERN_CORE = $(addprefix $(BUILD)/kernel/core/, \
|
|||
KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
|
||||
bio.o blkdev.o cdev.o device.o disk.o fb.o ide.o input.o irq.o kbd.o keymap.o mouse.o \
|
||||
pci.o pciuser.o text.o ttf.o uart.o gfx.o vt.o ramdisk.o procfs.o \
|
||||
dbg.o netuser.o audio.o dmi.o)
|
||||
dbg.o netuser.o audio.o dmi.o pty.o kdev.o)
|
||||
|
||||
# Drivers for hardware that has to be *found* rather than assumed. Anything
|
||||
# discovered by walking a bus belongs here; the fixed-address legacy devices
|
||||
|
|
@ -38,17 +38,34 @@ KERN_DEV = $(addprefix $(BUILD)/kernel/dev/, \
|
|||
# already eight files with four called usb_something, and each driver added made
|
||||
# the directory harder to read. The compile rule uses `mkdir -p $(dir $@)`, so
|
||||
# the depth costs nothing.
|
||||
# Which of them are compiled in is config/drivers.conf: y or n per driver, y by
|
||||
# default, overridable on the command line (make DRV_E1000=n). Only the drivers
|
||||
# that nothing else calls can be left out; the rest are always linked.
|
||||
-include config/drivers.conf
|
||||
DRV_LIST = VGA_PCI VIRTIO_GPU E1000 RTL8139 R8169 AC97 AHCI NVME
|
||||
DRV_DEFAULT_R8169 = n
|
||||
$(foreach d,$(DRV_LIST),$(eval DRV_$(d) ?= $(if $(DRV_DEFAULT_$(d)),$(DRV_DEFAULT_$(d)),y)))
|
||||
$(foreach d,$(DRV_LIST),$(if $(filter y n,$(DRV_$(d))),,$(error config/drivers.conf: DRV_$(d) must be y or n, not '$(DRV_$(d))')))
|
||||
DRV_DEFS = $(foreach d,$(DRV_LIST),$(if $(filter y,$(DRV_$(d))),-DCONFIG_DRV_$(d)=1))
|
||||
# Rewritten only when the choice changes, so registry.o is rebuilt then and not otherwise.
|
||||
DRV_STAMP = $(BUILD)/kernel/drivers/.config
|
||||
$(DRV_STAMP): FORCE
|
||||
@mkdir -p $(dir $@)
|
||||
@echo '$(DRV_DEFS)' | cmp -s - $@ || echo '$(DRV_DEFS)' > $@
|
||||
$(BUILD)/kernel/drivers/registry.o: $(DRV_STAMP)
|
||||
$(BUILD)/kernel/drivers/registry.o: CFLAGS += $(DRV_DEFS)
|
||||
|
||||
KERN_DRIVERS = $(BUILD)/kernel/drivers/registry.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_pci.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/vga/vga_fb.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/gpu/virtio_gpu/virtio_gpu.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/e1000/e1000.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/r8169/r8169.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/audio/ac97.o
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_VGA_PCI)),$(BUILD)/kernel/drivers/gpu/vga/vga_pci.o)
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_VIRTIO_GPU)),$(BUILD)/kernel/drivers/gpu/virtio_gpu/virtio_gpu.o)
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_E1000)),$(BUILD)/kernel/drivers/net/e1000/e1000.o)
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_R8169)),$(BUILD)/kernel/drivers/net/r8169/r8169.o)
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_RTL8139)),$(BUILD)/kernel/drivers/net/rtl8139/rtl8139.o)
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_AC97)),$(BUILD)/kernel/drivers/audio/ac97.o)
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/bus/xhci/xhci.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/ahci/ahci.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/storage/nvme/nvme.o
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_AHCI)),$(BUILD)/kernel/drivers/storage/ahci/ahci.o)
|
||||
KERN_DRIVERS += $(if $(filter y,$(DRV_NVME)),$(BUILD)/kernel/drivers/storage/nvme/nvme.o)
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/core/usb.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hid/usb_hid.o
|
||||
KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o
|
||||
|
|
@ -115,8 +132,11 @@ KERN_ZSTD = $(addprefix $(BUILD)/zstd/, \
|
|||
decompress/zstd_decompress_block.o)
|
||||
KERN_ZSTD += $(BUILD)/kernel/scff/scff.o
|
||||
|
||||
# GMMOS, what runs after a panic, and its micro drivers. See include/gmmos/gmmos.h.
|
||||
KERN_GMMOS = $(addprefix $(BUILD)/kernel/gmmos/, gmmos.o hidkeys.o ps2.o i2c.o xhci.o)
|
||||
|
||||
KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \
|
||||
proc.o)
|
||||
proc.o mapname.o)
|
||||
|
||||
KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \
|
||||
syscall.o sys_proc.o sys_fs.o sys_mem.o sys_net.o \
|
||||
|
|
@ -126,7 +146,8 @@ KERN_SYS = $(addprefix $(BUILD)/kernel/sys/, \
|
|||
KERN_ARCH += $(BUILD)/kernel/arch/x86_64/vectors.o
|
||||
|
||||
KERNOBJS = $(KERN_ARCH) $(KERN_CORE) $(KERN_DEV) $(KERN_DRIVERS) \
|
||||
$(KERN_FS) $(KERN_NET) $(KERN_ACPI) $(KERN_ZSTD) $(KERN_PROC) $(KERN_SYS)
|
||||
$(KERN_FS) $(KERN_NET) $(KERN_ACPI) $(KERN_ZSTD) $(KERN_GMMOS) \
|
||||
$(KERN_PROC) $(KERN_SYS)
|
||||
# entry.o must be first in link order; remove it from KERNOBJS for the rule
|
||||
KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
|
||||
|
||||
|
|
@ -140,7 +161,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS))
|
|||
# Third-party software does NOT go here — see ports/.
|
||||
# -----------------------------------------------------------------------
|
||||
ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o)
|
||||
CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello evtest xready locktest
|
||||
CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello evtest xready locktest su passwd
|
||||
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
@ -524,17 +545,24 @@ endif
|
|||
#
|
||||
# Evaluated when the Makefile is read, so the wipe happens before anything is
|
||||
# compiled against the wrong setting.
|
||||
#
|
||||
# Only the two objects that read RAMDISK_ROOT are dropped: kernel/proc/proc.c
|
||||
# (ROOTDEV is used nowhere else) and kernel/dev/ramdisk.c. The whole of
|
||||
# build/kernel, build/lib and build/cmd used to go, which made every `make iso`
|
||||
# recompile the kernel and every command, rebuild the root image from the new
|
||||
# binaries and compress it again at zstd -19 -- minutes, for a one-line change.
|
||||
# If another file ever starts to use ROOTDEV or RAMDISK_ROOT, add it here.
|
||||
RAMROOT_STATE := $(if $(RAMROOT),on,off)
|
||||
RAMROOT_STAMP := $(BUILD)/ramroot.state
|
||||
$(shell mkdir -p $(BUILD); \
|
||||
if [ "$$(cat $(RAMROOT_STAMP) 2>/dev/null)" != "$(RAMROOT_STATE)" ]; then \
|
||||
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd; \
|
||||
rm -f $(BUILD)/kernel/proc/proc.o $(BUILD)/kernel/dev/ramdisk.o; \
|
||||
printf '%s' '$(RAMROOT_STATE)' > $(RAMROOT_STAMP); \
|
||||
fi)
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# The root filesystem is an Alpine root that apk manages, with SchOS installed
|
||||
# into it as the package schos-base -- see docs/plan/alpine.md and "The
|
||||
# into it as the package schos-base -- see docs/architecture/packages.md and "The
|
||||
# apk-managed root" below. Both images are made from it: schfs.img, which
|
||||
# `make qemu` boots, and the ramdisk inside the kernel, which `make iso`
|
||||
# boots. sysroot/ is what schos-base is packaged from, not a root of its own.
|
||||
|
|
@ -625,7 +653,7 @@ UEFI_CFLAGS = $(UEFI_DEBUG_FLAGS) --target=x86_64-unknown-windows -ffreestanding
|
|||
# Hence the check and the message. It is worth the four lines.
|
||||
$(BUILD)/uefi/BOOTX64.EFI: boot/uefi/main.c boot/uefi/efi.h include/arch/schboot.h \
|
||||
boot/common/ui.c boot/common/ui.h \
|
||||
include/gfx/logo.h include/gfx/theme.h include/gfx/font.h
|
||||
include/gfx/logo.h include/gfx/theme.h include/gfx/font.h include/gfx/nvfont.h
|
||||
@mkdir -p $(BUILD)/uefi
|
||||
@command -v clang >/dev/null 2>&1 || { \
|
||||
echo ""; \
|
||||
|
|
@ -734,7 +762,6 @@ qemu-uefi: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
|
|||
# silently reused for the other, leaving ROOTDEV pointing at the wrong device.
|
||||
# Dropping the compiled objects first makes the switch always correct.
|
||||
ramroot:
|
||||
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
|
||||
$(MAKE) RAMROOT=1
|
||||
|
||||
cmd: $(CMDBINS)
|
||||
|
|
@ -752,7 +779,7 @@ ports:
|
|||
#
|
||||
# Staged into $(SYSROOT) so the next image build picks them up; run
|
||||
# `make projects && make` to get them onto a disk.
|
||||
PROJECTS ?= schcom
|
||||
PROJECTS ?=
|
||||
|
||||
projects:
|
||||
@test -d $(MUSLPREFIX)/include || { \
|
||||
|
|
@ -777,7 +804,7 @@ projects:
|
|||
# CFLAGS and make cannot see that.
|
||||
# -----------------------------------------------------------------------
|
||||
iso:
|
||||
rm -rf $(BUILD)/kernel $(BUILD)/lib $(BUILD)/cmd
|
||||
@$(MAKE) --no-print-directory ldls-r8169-if-present
|
||||
$(MAKE) RAMROOT=1 schos.iso
|
||||
|
||||
# One El Torito entry, pointing at a FAT image holding the loader and kernel.
|
||||
|
|
@ -852,13 +879,13 @@ $(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
|
|||
$(BUILD)/fs-ram.img: $(MKSCHFS) $(ROOT_STAMP)
|
||||
@mkdir -p $(BUILD)
|
||||
@rm -f $@
|
||||
@used=$$($(MKSCHFS) $@ --size 262144 --label schos \
|
||||
@used=$$($(MKSCHFS) $@ --size 262144 --label schos --owners $(AROOT_DIR)/owners \
|
||||
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) | \
|
||||
sed -n 's/^ *\([0-9][0-9]*\) of [0-9][0-9]* blocks used.*/\1/p'); \
|
||||
test -n "$$used" || { echo "fs-ram.img: mkschfs did not say how much it used" >&2; exit 1; }; \
|
||||
rm -f $@; \
|
||||
$(MKSCHFS) $@ --size $$((used + used / 8 + $(RAMDISK_SLACK))) --label schos \
|
||||
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE)
|
||||
--owners $(AROOT_DIR)/owners $(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE)
|
||||
|
||||
# The image goes into the kernel as SCFF (include/fs/scff.h): 128 KB chunks,
|
||||
# each its own zstd frame, unpacked by the kernel the first time a block in it
|
||||
|
|
@ -963,6 +990,10 @@ $(BUILD)/kernel/dev/%.o: kernel/dev/%.c
|
|||
@mkdir -p $(BUILD)/kernel/dev
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(BUILD)/kernel/gmmos/%.o: kernel/gmmos/%.c
|
||||
@mkdir -p $(BUILD)/kernel/gmmos
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
# `$(dir $@)` rather than a fixed path: drivers are grouped into one directory
|
||||
# per device class (gpu/, net/, bus/, usb/), so the output directory varies with
|
||||
# the target and a hardcoded mkdir only creates the top one.
|
||||
|
|
@ -1089,22 +1120,30 @@ $(BUILD)/lib/%.o: lib/%.S
|
|||
# own -- a specific rule beats the pattern one below. ldls/ko.c, btf.c and
|
||||
# stub.c are freestanding and build unchanged for either side, which is the
|
||||
# whole point of having written them that way.
|
||||
# A Linux module is compiled with an 8-byte-aligned stack and, being kernel
|
||||
# code, never sets %al for a variadic call -- so it arrives in this code with
|
||||
# the stack off by eight and %al garbage. Any function here that takes `...`
|
||||
# then opens with `movaps %xmm0, -0xb0(%rbp)`, which faults on a misaligned
|
||||
# address: that was the general-protection fault in ldlsd on real hardware, in
|
||||
# the log function, the moment the first driver message was printed.
|
||||
# -mstackrealign makes every function re-align its own stack on entry.
|
||||
LDLS_ALIGN = -mstackrealign
|
||||
LDLS_GUEST = $(addprefix $(BUILD)/ldls/guest/, ko.o btf.o stub.o) \
|
||||
$(addprefix $(BUILD)/ldls/guest/kpi/, kpi.o pci.o io.o net.o skb.o string.o work.o thunk.o)
|
||||
$(addprefix $(BUILD)/ldls/guest/kpi/, kpi.o pci.o io.o net.o skb.o string.o work.o thunk.o base.o)
|
||||
|
||||
$(BUILD)/ldls/guest/%.o: ldls/%.c ldls/ko.h ldls/btf.h ldls/stub.h
|
||||
@mkdir -p $(BUILD)/ldls/guest
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
$(CC) $(CFLAGS) $(LDLS_ALIGN) -c $< -o $@
|
||||
|
||||
# The KPI -- the Linux symbols, implemented. Guest-only: it reaches /dev/pci
|
||||
# and /dev/netdev, which exist on SchOS and nowhere else.
|
||||
$(BUILD)/ldls/guest/kpi/%.o: ldls/kpi/%.c ldls/kpi/kpi.h
|
||||
$(BUILD)/ldls/guest/kpi/%.o: ldls/kpi/%.c ldls/kpi/kpi.h ldls/kpi/link_modes.inc
|
||||
@mkdir -p $(BUILD)/ldls/guest/kpi
|
||||
$(CC) $(CFLAGS) -I./ldls -c $< -o $@
|
||||
$(CC) $(CFLAGS) $(LDLS_ALIGN) -I./ldls -c $< -o $@
|
||||
|
||||
$(BUILD)/cmd/ldlsd: cmd/ldlsd/main.c $(ULIB) $(LDLS_GUEST)
|
||||
@mkdir -p $(BUILD)/cmd
|
||||
$(CC) $(CFLAGS) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o
|
||||
$(CC) $(CFLAGS) $(LDLS_ALIGN) -I./ldls -c $< -o $(BUILD)/cmd/ldlsd.o
|
||||
$(LD) $(LDFLAGS) $(LD_LOWBASE) -e _start -Ttext 0x4000 -o $@ \
|
||||
$(BUILD)/cmd/ldlsd.o $(LDLS_GUEST) $(ULIB)
|
||||
$(OBJCOPY) -S $@
|
||||
|
|
@ -1135,6 +1174,22 @@ $(BUILD)/cmd/xready: cmd/xready/main.c $(MUSLPREFIX)/lib/libc.so $(LINUXHDR)
|
|||
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/xready.o \
|
||||
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
|
||||
|
||||
# su and passwd: musl, for getpwnam and crypt, and static -- they run setuid root,
|
||||
# so there is no loader or library path to be led astray.
|
||||
$(BUILD)/cmd/su: cmd/su/main.c $(MUSLPREFIX)/lib/libc.so
|
||||
@mkdir -p $(BUILD)/cmd
|
||||
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/su.o
|
||||
$(DYNCC) -static -no-pie -nostdlib -o $@ \
|
||||
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/su.o \
|
||||
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
|
||||
|
||||
$(BUILD)/cmd/passwd: cmd/passwd/main.c $(MUSLPREFIX)/lib/libc.so
|
||||
@mkdir -p $(BUILD)/cmd
|
||||
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/passwd.o
|
||||
$(DYNCC) -static -no-pie -nostdlib -o $@ \
|
||||
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/passwd.o \
|
||||
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
|
||||
|
||||
$(BUILD)/cmd/locktest: cmd/locktest/main.c $(MUSLPREFIX)/lib/libc.so
|
||||
@mkdir -p $(BUILD)/cmd
|
||||
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/locktest.o
|
||||
|
|
@ -1308,7 +1363,7 @@ crashqr:
|
|||
$(PYTHON) tools/python/crashqr.py
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# LDLS -- the Linux Driver Layer. docs/plan/ldls.md.
|
||||
# LDLS -- the Linux Driver Layer. docs/architecture/ldls.md.
|
||||
#
|
||||
# ldls/ko.c is freestanding and builds on both sides: here as part of a host
|
||||
# tool, and later as part of the SchOS-side host. Same discipline as SchFS,
|
||||
|
|
@ -1333,7 +1388,7 @@ $(LDLS_RUN): ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c \
|
|||
$(HOSTCC) $(HOSTWARN) -O1 -fno-strict-aliasing -o $@ \
|
||||
ldls/host/run.c ldls/ko.c ldls/stub.c ldls/btf.c
|
||||
|
||||
# The test object, compiled and never linked. Ours, because docs/plan/ldls.md
|
||||
# The test object, compiled and never linked. Ours, because docs/architecture/ldls.md
|
||||
# rule 1 means no .ko is committed here -- see the header of refmod.c.
|
||||
#
|
||||
# -fno-pic matters: a PIC object routes its data through the GOT, and a GOT is
|
||||
|
|
@ -1356,13 +1411,12 @@ ldls: $(LDLS_INSPECT) $(LDLS_RUN) $(LDLS_TYPES)
|
|||
#
|
||||
# make ldls-stage KO=/path/to/8139too.ko BTF=/sys/kernel/btf/vmlinux
|
||||
#
|
||||
# This is the model docs/plan/ldls.md describes, carried out on the machine
|
||||
# that is doing the testing: the user obtains a module and loads it on their
|
||||
# own machine. Deliberately **not** a dependency of `all`, so an ordinary build
|
||||
# never picks it up, and `.gitignore` refuses `*.ko` so it cannot be committed
|
||||
# by accident. An image built after this has somebody else's GPL code in it and
|
||||
# must not be redistributed -- which is exactly what licensing.md §6 says about
|
||||
# the combination, and why it is one command you have to type on purpose.
|
||||
# For trying any module, not just the one that ships. Deliberately **not** a
|
||||
# dependency of `all`, so an ordinary build never picks it up, and `.gitignore`
|
||||
# refuses `*.ko` so a binary cannot be committed by accident. An image with a
|
||||
# module in it is a distribution of that module's code, under its own licence
|
||||
# (docs/licensing.md §6) -- fine for the ones SchOS ships (r8169, below), and
|
||||
# a thing to check for any other.
|
||||
ldls-stage:
|
||||
@test -n "$(KO)" || { echo "usage: make ldls-stage KO=<module.ko> [BTF=<vmlinux-btf>]"; exit 1; }
|
||||
@mkdir -p $(SYSROOT)/lib/modules
|
||||
|
|
@ -1370,6 +1424,55 @@ ldls-stage:
|
|||
@test -z "$(BTF)" || cp "$(BTF)" $(SYSROOT)/lib/modules/btf
|
||||
@echo "staged into $(SYSROOT)/lib/modules -- this image is now yours alone"
|
||||
|
||||
# Linux's r8169 and what it needs, from the kernel this machine runs, for
|
||||
# etc/schinit/ldls-r8169.job. The module set is r8169's dependency closure less
|
||||
# what LDLS answers itself (led-class, of_mdio, fwnode_mdio, fixed_phy), in load
|
||||
# order; the firmware is the RTL8168/8111 PHY patches. These are the modules and
|
||||
# firmware SchOS ships in its ISO (docs/licensing.md §6, NOTICE): run this before
|
||||
# `make iso` for an image that has the laptop's network.
|
||||
LDLS_KDIR ?= /run/current-system/kernel-modules/lib/modules/$(shell uname -r)/kernel
|
||||
LDLS_FWDIR ?= /run/current-system/firmware
|
||||
LDLS_R8169 = drivers/net/phy/mdio-bus drivers/net/phy/libphy \
|
||||
drivers/net/phy/mdio_devres drivers/net/phy/realtek/realtek \
|
||||
drivers/net/ethernet/realtek/r8169
|
||||
# Every file is replaced only if its contents differ. sysroot.stamp records the
|
||||
# mtime of each staged file, `make iso` runs this every time, and rewriting the
|
||||
# same bytes moved every mtime -- which rebuilt the root image and compressed it
|
||||
# at zstd -19 (a minute) after a one-line kernel change.
|
||||
|
||||
ldls-r8169:
|
||||
@mkdir -p $(SYSROOT)/lib/modules $(SYSROOT)/lib/firmware/rtl_nic
|
||||
@set -e; list=; for m in $(LDLS_R8169); do \
|
||||
f=$$(ls $(LDLS_KDIR)/$$m.ko* | head -1); n=$$(basename $$m).ko; \
|
||||
t=$(SYSROOT)/lib/modules/$$n; \
|
||||
case $$f in *.xz) xz -dc $$f;; *.zst) zstd -qdc $$f;; *) cat $$f;; esac > $$t.new; \
|
||||
if cmp -s $$t.new $$t; then rm -f $$t.new; else mv -f $$t.new $$t; fi; \
|
||||
list=$$list$${list:+,}/lib/modules/$$n; done; \
|
||||
if ! cmp -s /sys/kernel/btf/vmlinux $(SYSROOT)/lib/modules/btf; then \
|
||||
cp /sys/kernel/btf/vmlinux $(SYSROOT)/lib/modules/btf; fi; \
|
||||
printf '#!/bin/sh\nmkdir -p /var/log\nexec /bin/ldlsd %s /lib/modules/btf > /var/log/ldls-r8169.log 2>&1\n' \
|
||||
"$$list" > $(SYSROOT)/lib/modules/ldls-r8169.sh.new; \
|
||||
if cmp -s $(SYSROOT)/lib/modules/ldls-r8169.sh.new $(SYSROOT)/lib/modules/ldls-r8169.sh; then \
|
||||
rm -f $(SYSROOT)/lib/modules/ldls-r8169.sh.new; \
|
||||
else mv -f $(SYSROOT)/lib/modules/ldls-r8169.sh.new $(SYSROOT)/lib/modules/ldls-r8169.sh; fi
|
||||
@for f in $(LDLS_FWDIR)/rtl_nic/rtl8168*.fw*; do \
|
||||
n=$$(basename $$f); case $$n in \
|
||||
*.zst) out=$${n%.zst}; zstd -qdc $$f > $(SYSROOT)/lib/firmware/rtl_nic/$$out.new;; \
|
||||
*.xz) out=$${n%.xz}; xz -dc $$f > $(SYSROOT)/lib/firmware/rtl_nic/$$out.new;; \
|
||||
*) out=$$n; cp $$f $(SYSROOT)/lib/firmware/rtl_nic/$$out.new;; esac; \
|
||||
d=$(SYSROOT)/lib/firmware/rtl_nic/$$out; \
|
||||
if cmp -s $$d.new $$d; then rm -f $$d.new; else mv -f $$d.new $$d; fi; done
|
||||
@echo "staged Linux r8169 ($(shell uname -r)) into $(SYSROOT)"
|
||||
|
||||
# `make iso` runs this first: the ISO ships Linux's r8169 set (NOTICE), so stage it whenever
|
||||
# this machine has the modules and the BTF to stage it from, and say so when it does not.
|
||||
ldls-r8169-if-present:
|
||||
@if ls $(LDLS_KDIR)/drivers/net/ethernet/realtek/r8169.ko* >/dev/null 2>&1 && [ -r /sys/kernel/btf/vmlinux ]; then \
|
||||
$(MAKE) --no-print-directory ldls-r8169; \
|
||||
else \
|
||||
echo "iso: no Linux r8169 modules or BTF on this machine ($(LDLS_KDIR)) -- this ISO will not carry them"; \
|
||||
fi
|
||||
|
||||
ldls-unstage:
|
||||
rm -rf $(SYSROOT)/lib/modules
|
||||
@echo "removed"
|
||||
|
|
@ -1423,19 +1526,27 @@ LOGO_ASCII = docs/branding/Ascii/ascii_50.txt
|
|||
# proves the same thing far less clearly.
|
||||
SRV_FILES = $(wildcard srv/*)
|
||||
|
||||
# cp -p, not cp: sysroot.stamp lists every staged file with its mtime, and the
|
||||
# Alpine root, the root image and its zstd -19 compression are all rebuilt when
|
||||
# it changes. A plain cp gave every copy a new mtime, so every `make iso`
|
||||
# rebuilt all of them -- over a minute of CPU for no change at all.
|
||||
etc-install: $(SCHINIT_JOBS) $(ETC_PLAIN) $(ETC_ZSH) $(LOGO_ASCII) $(SRV_FILES)
|
||||
@mkdir -p $(SYSROOT)/etc/schinit $(SYSROOT)/etc/zsh $(SYSROOT)/usr/share/schos $(SYSROOT)/srv
|
||||
@cp $(ETC_PLAIN) $(SYSROOT)/etc/
|
||||
@cp $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
|
||||
@cp $(ETC_ZSH) $(SYSROOT)/etc/zsh/
|
||||
@cp $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
|
||||
@cp $(SRV_FILES) $(SYSROOT)/srv/
|
||||
@cp -p $(ETC_PLAIN) $(SYSROOT)/etc/
|
||||
@cp -p $(SCHINIT_JOBS) $(SYSROOT)/etc/schinit/
|
||||
@cp -p $(ETC_ZSH) $(SYSROOT)/etc/zsh/
|
||||
@cp -p $(LOGO_ASCII) $(SYSROOT)/usr/share/schos/logo.txt
|
||||
@cp -p $(SRV_FILES) $(SYSROOT)/srv/
|
||||
@# The device-name databases, where lspci and lsusb look for them: the
|
||||
@# hwdata package's directory. They were at /usr/share/usb.ids, which no
|
||||
@# tool searches, so lsusb printed numbers only.
|
||||
@mkdir -p $(SYSROOT)/usr/share/hwdata
|
||||
@rm -f $(SYSROOT)/usr/share/usb.ids
|
||||
@for f in usb.ids pci.ids; do [ -f dist/$$f ] && cp dist/$$f $(SYSROOT)/usr/share/hwdata/; done; true
|
||||
@for f in usb.ids pci.ids; do [ -f dist/$$f ] && cp -p dist/$$f $(SYSROOT)/usr/share/hwdata/; done; true
|
||||
@# The licences travel with the image: SchOS's, xv6's, and NOTICE, which carries the
|
||||
@# third-party ones -- including Linux's r8169 modules, whose source offer it records.
|
||||
@mkdir -p $(SYSROOT)/usr/share/licenses/schos
|
||||
@cp -p LICENSE LICENSE.MIT NOTICE $(SYSROOT)/usr/share/licenses/schos/
|
||||
@echo "installed $(words $(ETC_PLAIN)) /etc file(s), $(words $(ETC_ZSH)) zsh file(s) and $(words $(SCHINIT_JOBS)) schinit manifest(s)"
|
||||
|
||||
# The mount points. Created here rather than by init, because a mkdir on a
|
||||
|
|
@ -1549,15 +1660,6 @@ $(BUILD)/fs.img: $(MKFS) $(BUILD)/sysroot.stamp
|
|||
# it gets worse -- because stage2 loads the kernel by PIO one sector at a time,
|
||||
# and under KVM every one of those port accesses is a VM exit, where under TCG
|
||||
# it is cheap.
|
||||
# Schcom, end to end, checked from outside the machine.
|
||||
#
|
||||
# Boots, starts the compositor and a client, and then screendumps the
|
||||
# framebuffer and asserts the pixels on the host. That last part is the whole
|
||||
# value: the compositor saying "I composited it" proves nothing, and a
|
||||
# screenshot nobody inspects proves less. Needs `make projects` first.
|
||||
schcom-test: esp $(BUILD)/schfs.img
|
||||
sh tools/posix/schcom_test.sh
|
||||
|
||||
# The firmware and the disks, in one place. Every run target below boots the
|
||||
# same way, because there is only one way to boot now: OVMF, the staged ESP as
|
||||
# the first disk, the SchFS volume as the second. Keeping the argument list in
|
||||
|
|
@ -1626,7 +1728,7 @@ SCHFS_BLOCKS ?= 262144
|
|||
$(BUILD)/schfs.img: $(MKSCHFS) $(ROOT_STAMP)
|
||||
@mkdir -p $(BUILD)
|
||||
@rm -f $@
|
||||
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(ROOT_TREE)
|
||||
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --owners $(AROOT_DIR)/owners --sysroot $(ROOT_TREE)
|
||||
|
||||
# --- The apk-managed root ------------------------------------------------------
|
||||
#
|
||||
|
|
@ -1693,6 +1795,13 @@ LOGO_PX = 128
|
|||
logo:
|
||||
$(PYTHON) tools/python/mklogo.py "$(LOGO_SRC)" include/gfx/logo.h $(LOGO_PX)
|
||||
|
||||
# include/gfx/nvfont.h: the boot manager's typeface (Instrument Sans, from
|
||||
# docs/Website/niveus/fonts), rasterised once and committed, same arrangement as
|
||||
# logo.h. Needs Pillow, so it is run by hand: nix-shell -p 'python3.withPackages
|
||||
# (p: [p.pillow])' --run "make nvfont"
|
||||
nvfont:
|
||||
$(PYTHON) tools/python/mknvfont.py docs/Website/niveus/fonts/static include/gfx/nvfont.h
|
||||
|
||||
# The USB vendor/product name database, from linux-usb.org.
|
||||
#
|
||||
# Dual-licensed GPL-2.0-or-later **or** 3-clause BSD; SchOS takes the BSD
|
||||
|
|
@ -1754,10 +1863,10 @@ qrtest: $(QRTEST)
|
|||
qrcheck: $(QRTEST)
|
||||
$(PYTHON) tools/python/qrcheck.py
|
||||
|
||||
.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm schcom-test \
|
||||
.PHONY: disk all kernel ramroot cmd ports projects sysroot-install qemu qemu-console qemu-kvm \
|
||||
schfs-test schfs-objtest schfs-image-test schfs-check kmem-test mkschfs \
|
||||
schfs-fsck crashshot qemu-schfs iso qemu-iso clean distclean \
|
||||
logo qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi
|
||||
logo nvfont qrtest qrcheck crashqr usb-ids pci-ids hwdata ovmf esp uefi
|
||||
.PRECIOUS: $(BUILD)/%.o
|
||||
|
||||
# 'clean' removes generated output only. dist/ (upstream tarballs) survives,
|
||||
|
|
@ -1781,3 +1890,6 @@ distclean: clean
|
|||
# build output: Alpine's usr/lib/sysctl.d is a directory, and including it
|
||||
# stopped every make with "Is a directory".
|
||||
-include $(shell find $(BUILD) \( -path $(BUILD)/aroot -o -path $(BUILD)/iso -o -path $(BUILD)/ports \) -prune -o -type f -name '*.d' -print 2>/dev/null)
|
||||
|
||||
FORCE:
|
||||
.PHONY: FORCE ldls-r8169-if-present
|
||||
|
|
|
|||
30
NOTICE
30
NOTICE
|
|
@ -191,6 +191,36 @@ the whole library on the host to write it. zstd is dual-licensed; SchOS takes
|
|||
it under the BSD licence above. The glue written for SchOS (kernel/scff/) is
|
||||
GPL-2.0-or-later and is not part of zstd; zstd's own sources are unmodified.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Linux kernel drivers (r8169 and its modules) GPL-2.0-only
|
||||
--------------------------------------------------------------------------------
|
||||
Copyright (c) the Linux kernel contributors
|
||||
|
||||
The ISO carries these Linux kernel modules, unmodified, as built for the
|
||||
kernel version that `make ldls-r8169` took them from (see
|
||||
/var/log/ldls-r8169.log on a running system for the version):
|
||||
|
||||
drivers/net/phy/mdio-bus.ko drivers/net/phy/libphy.ko
|
||||
drivers/net/phy/mdio_devres.ko drivers/net/phy/realtek/realtek.ko
|
||||
drivers/net/ethernet/realtek/r8169.ko
|
||||
|
||||
They are loaded into an ordinary process by ldlsd (ldls/, cmd/ldlsd), which is
|
||||
GPL-2.0-or-later and part of SchOS, and not linked into the kernel. The
|
||||
licence of the modules is the GNU General Public License, version 2 only; the
|
||||
corresponding source is the same release of the Linux kernel, available from
|
||||
https://www.kernel.org/ . The BTF type data staged beside them
|
||||
(/lib/modules/btf) is generated from that kernel build.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Realtek RTL8168/8111 PHY firmware (rtl_nic/rtl8168*.fw) Realtek / linux-firmware
|
||||
--------------------------------------------------------------------------------
|
||||
Copyright (c) Realtek Semiconductor Corp.
|
||||
|
||||
Redistributed unmodified from the linux-firmware collection, under the terms
|
||||
in its WHENCE file and the Realtek licence there: redistribution in binary
|
||||
form is permitted provided the copyright notice is reproduced. Staged by
|
||||
`make ldls-r8169` into /lib/firmware/rtl_nic and read by the PHY driver.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Intel 8254x device identifiers BSD 3-Clause
|
||||
--------------------------------------------------------------------------------
|
||||
|
|
|
|||
196
README.md
196
README.md
|
|
@ -4,48 +4,50 @@
|
|||
|
||||
# SchOS
|
||||
|
||||
**A 64-bit operating system, written from the metal up.**
|
||||
**A 64-bit operating system, written from the metal up — and it runs Linux's software.**
|
||||
|
||||
SMP kernel · copy-on-write filesystem · Linux syscall ABI · Alpine packages · X11 on real hardware
|
||||
SMP kernel · copy-on-write filesystem · Linux syscall ABI · Alpine packages · Xorg · Linux's own drivers
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
SchOS began as [xv6](https://github.com/mit-pdos/xv6-public) and is now mostly
|
||||
something else. The scheduler, the memory manager and the trap path have been
|
||||
rewritten for x86-64 and for more than one processor; the filesystem has been
|
||||
replaced outright; the syscall table speaks Linux numbering, so real Linux
|
||||
software runs on it unmodified — statically or dynamically linked, straight
|
||||
out of Alpine's package repositories.
|
||||
SchOS is a hobby operating system built by one person. It began as
|
||||
[xv6](https://github.com/mit-pdos/xv6-public) and almost nothing of that is left:
|
||||
the scheduler, the memory manager, the trap path and every driver have been
|
||||
rewritten for x86-64 and for more than one processor, and the filesystem is a
|
||||
different one. The syscall table speaks Linux's numbering, so **real Linux
|
||||
programs run on it unmodified** — statically or dynamically linked, straight out
|
||||
of Alpine's package repositories — and a desktop (Xorg and Window Maker) and a
|
||||
web browser (NetSurf) come up on the framebuffer.
|
||||
|
||||
It is a hobby system built by one person, and it is honest about that: what
|
||||
works below has been *run*, not inferred, and the gaps are listed as plainly as
|
||||
the features.
|
||||
It is honest about being a hobby system: what is listed below has been *run*,
|
||||
not inferred, and [the gaps](#not-there-yet) are listed as plainly as the
|
||||
features. It boots from a USB stick on three different real machines.
|
||||
|
||||
## In numbers
|
||||
|
||||
<!-- stats:begin -->
|
||||
| | |
|
||||
|---|---|
|
||||
| **First-party code** | 61,367 lines, plus 21,128 lines of comments (26%) in 272 files |
|
||||
| **Kernel** (`kernel/`, `include/`) | 45,207 lines of code, 16,111 of comments |
|
||||
| **Syscalls** | 208, of which 184 on Linux numbering |
|
||||
| **First-party code** | 72,766 lines, plus 23,670 lines of comments (25%) in 307 files |
|
||||
| **Kernel** (`kernel/`, `include/`) | 51,893 lines of code, 17,213 of comments |
|
||||
| **Syscalls** | 215, of which 193 on Linux numbering |
|
||||
| **Drivers** | 15 in the kernel |
|
||||
| **First-party programs** | 29 in `cmd/` |
|
||||
| **Alpine packages in the root** | 88, from 11 asked for in `ports/alpine/world` |
|
||||
| **History** | 253 commits since 2026-08-09 (76 days) |
|
||||
| **Alpine packages in the root** | 179, from 29 asked for in `ports/alpine/world` |
|
||||
| **History** | 292 commits since 2026-08-09 (78 days) |
|
||||
|
||||
<details>
|
||||
<summary>Lines by language</summary>
|
||||
|
||||
| Language | Code | Comments |
|
||||
|---|--:|--:|
|
||||
| C | 56,580 | 19,427 |
|
||||
| C | 67,615 | 21,904 |
|
||||
| Assembly | 1,750 | 336 |
|
||||
| PowerShell | 1,362 | 638 |
|
||||
| Python | 759 | 147 |
|
||||
| Shell | 716 | 469 |
|
||||
| Python | 943 | 150 |
|
||||
| Shell | 896 | 531 |
|
||||
| Perl | 129 | 45 |
|
||||
| Linker script | 71 | 66 |
|
||||
|
||||
|
|
@ -55,78 +57,74 @@ the features.
|
|||
<code>ports/</code> and generated tables are not counted.</sub>
|
||||
<!-- stats:end -->
|
||||
|
||||
## The pieces
|
||||
## What it is made of
|
||||
|
||||
| Layer | Name | What it is |
|
||||
|---|---|---|
|
||||
| System | **SchOS** | The distribution — kernel, userland, tooling |
|
||||
| Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS |
|
||||
| Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS and Linux's syscall ABI |
|
||||
| Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem |
|
||||
| Image format | **SCFF** | The SchOS Compressed File Format the live root ships in |
|
||||
| API | **SchAPI** | System programming interface — the stable surface |
|
||||
| Compositor | **SchCom** | Display compositor *(planned)* |
|
||||
| Window manager | **SchWM** | *(planned)* |
|
||||
| Desktop | **SchDE** | *(planned)* |
|
||||
| Packages | **apk** | Alpine's package manager manages the root; SchOS itself is the package `schos-base` |
|
||||
| Desktop | **Xorg + Window Maker** | Alpine's, on the framebuffer |
|
||||
| Crash screen | **PSOGM / GMMOS** | The Purple Screen of Guru Meditation, and the micro OS that runs under it |
|
||||
| API | **SchAPI** | The long-term native interface — *designed, not built* |
|
||||
|
||||
`uname -s` reports `SchIFT`, the way every Unix reports its kernel rather than
|
||||
its distribution.
|
||||
|
||||
## What works
|
||||
|
||||
**Kernel** — Long mode, 4-level paging, preemptive multitasking. **SMP** up to
|
||||
64 processors, enumerated from the ACPI MADT. ACPI through
|
||||
[uACPI](https://github.com/uACPI/uACPI): interrupt routing from `_PRT`, the
|
||||
power button, clean power-off and reset. Memory sized from the E820/UEFI map.
|
||||
**Kernel** — Long mode, 4-level paging, preemptive multitasking, a fair-share
|
||||
scheduler with `nice`. **SMP** up to 64 processors, enumerated from the ACPI
|
||||
MADT. **Threads** (`clone`, `futex`, TLB shootdowns), **demand paging** for the
|
||||
huge reservations browsers make, and dynamic linking. ACPI through
|
||||
[uACPI](https://github.com/uACPI/uACPI): interrupt routing, the power button,
|
||||
clean power-off and reset, batteries and AC through a laptop's embedded
|
||||
controller, and deep idle states. Memory sized from the firmware's map, up to 256 GB.
|
||||
|
||||
**Filesystem** — **SchFS** is the root: a copy-on-write B-tree with a
|
||||
uberblock ring, so a crash leaves either the old tree or the new one and there
|
||||
is nothing to repair. The live image ships as **[SCFF](docs/architecture/scff.md)**:
|
||||
the 358 MB root compresses to 102 MB with zstd and is unpacked a chunk at a
|
||||
time, the first time something reads it. `/proc` has `/proc/<pid>/fd`,
|
||||
`/proc/self` and the rest that `ps`, `top` and Xorg read.
|
||||
**Filesystem** — **SchFS** is the root: a copy-on-write B-tree with an uberblock
|
||||
ring, so a crash leaves either the old tree or the new one and there is nothing
|
||||
to repair. The live image ships as **[SCFF](docs/architecture/scff.md)**: the
|
||||
root compresses with zstd to about a third of its size and is unpacked a chunk at
|
||||
a time, the first time something reads it. `/proc`, `/sys` and `/dev` are
|
||||
synthesised, in Linux's layouts, because that is what the programs read.
|
||||
|
||||
**Processes** — fork, exec, wait, pipes, signals, sessions and job control,
|
||||
`mmap` (anonymous and file-backed), POSIX shared memory, `memfd`, Unix domain
|
||||
sockets with descriptor passing (`SCM_RIGHTS`), `poll`, `epoll`, `eventfd`,
|
||||
`timerfd`, `signalfd`. **Dynamic linking**: `exec` loads `ld-musl`, so
|
||||
binaries come from Alpine as they are.
|
||||
**Linux software** — `fork`, `clone`, `exec`, pipes, signals, sessions and job
|
||||
control, `mmap`, POSIX and `memfd` shared memory, Unix sockets with descriptor and
|
||||
credential passing, `poll`, `epoll`, `eventfd`, `timerfd`, `signalfd`, `inotify`,
|
||||
pseudo-terminals. The root is managed by Alpine's **apk**: `ports/alpine/world`
|
||||
says what is installed, `world.lock` pins it, and `apk add` fetches more over HTTPS.
|
||||
|
||||
**Packages** — the root is managed by Alpine's **apk**. `ports/alpine/world`
|
||||
says what is installed; the build resolves it once into `world.lock` and every
|
||||
image after that is reproducible. SchOS itself is one more package in it.
|
||||
**Desktop** — **Xorg with Window Maker**, `xterm`, the classic X clients, SDL games
|
||||
and the NetSurf browser, on the framebuffer (`/dev/fb0`, Linux fbdev) with input
|
||||
as Linux evdev. Twelve virtual terminals, switched with Ctrl+Alt+F1–F12, with X on
|
||||
a terminal of its own. Sound through ALSA (over OSS and an AC'97 driver).
|
||||
|
||||
**Graphics** — **Xorg with Window Maker**, preinstalled, on the framebuffer
|
||||
(`/dev/fb0`, Linux fbdev). Six virtual terminals, switched with
|
||||
Ctrl+Alt+F1–F6 or Alt+F1–F6, with X on a terminal of its own; Ctrl+Alt+Backspace
|
||||
ends the session. Input as Linux evdev on `/dev/input/event*`.
|
||||
|
||||
**Devices** — PCI with a bus-agnostic device model at `/proc/devices`.
|
||||
**Drivers** — written from the specifications, and **Linux's own, unmodified,
|
||||
through [LDLS](docs/architecture/ldls.md)**: a `.ko` runs in an ordinary process
|
||||
and reaches the hardware through `/dev/pci`, so a crashed driver costs an interface,
|
||||
not the machine. The laptop's Realtek NIC is driven that way.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Storage | AHCI (SATA), NVMe, IDE, USB mass storage |
|
||||
| Network | Intel e1000, Realtek r8169 and RTL8139 |
|
||||
| Network | Intel e1000, Realtek RTL8139 and r8169 (the latter also as Linux's driver in a process) |
|
||||
| USB | xHCI, hubs, HID keyboards and mice |
|
||||
| Input | PS/2, I2C-HID laptop touchpads (DesignWare I2C, found through ACPI) |
|
||||
| Input | PS/2, I2C-HID laptop touchpads and keyboards (DesignWare I2C, found through ACPI) |
|
||||
| Display | UEFI GOP framebuffer, virtio-gpu, Bochs/stdvga |
|
||||
| Audio | AC'97 |
|
||||
|
||||
**Networking** — ARP, IPv4, ICMP, UDP, TCP and DHCP on lwIP. DNS, `wget`
|
||||
over **HTTPS**, `ping`, and an `httpd` serving a directory back.
|
||||
**Networking** — ARP, IPv4, ICMP, UDP, TCP and DHCP on lwIP; DNS, `wget` and `curl`
|
||||
over **HTTPS**, `ping`, `httpd`, netlink for `getifaddrs`.
|
||||
|
||||
**Userland** — musl, toybox, mksh and zsh, plus whatever Alpine has: `ls`,
|
||||
`ps`, `top`, `awk`, `diff`, `xterm`, `fastfetch` behave the way you expect,
|
||||
because they are the real programs.
|
||||
**When it dies** — PSOGM, the Purple Screen of Guru Meditation: stop code, fault
|
||||
address, call chain, and the log as QR codes. Under it runs **GMMOS**, a micro OS with
|
||||
its own keyboard drivers, so a dead machine can still show its log, take a few
|
||||
commands, and restart or power off.
|
||||
|
||||
**Text** — a TrueType rasteriser (`glyf` outlines, composite glyphs, cmap
|
||||
format 4). JetBrains Mono for the terminal, Roboto for the desktop to come.
|
||||
|
||||
**When it dies** — PSOGM, the Purple Screen of Guru Meditation: stop code,
|
||||
fault address, call chain, and a QR code containing a text copy of the whole
|
||||
report.
|
||||
|
||||
**On real hardware** — booted on an AMD B550 desktop (Ryzen 5 4600G), an ASUS
|
||||
TUF A15 laptop and an Ivy Bridge HP all-in-one, from a USB stick.
|
||||
**On real hardware** — booted from a USB stick on an AMD B550 desktop (Ryzen 5
|
||||
4600G), an ASUS TUF A15 laptop and an Ivy Bridge HP all-in-one.
|
||||
|
||||
## Quick start
|
||||
|
||||
|
|
@ -134,19 +132,19 @@ TUF A15 laptop and an Ivy Bridge HP all-in-one, from a USB stick.
|
|||
make # kernel + UEFI loader + root filesystem
|
||||
make qemu # build it and boot it
|
||||
make iso # a bootable ISO for VMware or real hardware
|
||||
./write.sh sdX # write that ISO to a USB stick (Linux; it checks what it is overwriting)
|
||||
```
|
||||
|
||||
The ISO is a hybrid image: write it to a USB stick with
|
||||
`dd if=schos.iso of=/dev/sdX bs=4M` (or Rufus/Etcher in DD mode) and boot it
|
||||
on any UEFI machine.
|
||||
The ISO is a hybrid image: it boots on any UEFI machine from a stick written with
|
||||
`write.sh`, `dd if=schos.iso of=/dev/sdX bs=4M`, or Rufus/Etcher in DD mode. **It runs
|
||||
from RAM and touches none of the machine's disks.** Log in on the console, and type
|
||||
`startx` for the desktop.
|
||||
|
||||
Builds on **Linux, macOS, FreeBSD/OpenBSD/NetBSD and Windows** (Git Bash). The
|
||||
toolchain is clang + lld everywhere — it is a cross-compiler by construction, so
|
||||
one LLVM install targets the bare-metal kernel, the 16-bit boot code and musl
|
||||
userland from any host. See **[docs/guides/building.md](docs/guides/building.md)** for
|
||||
prerequisites per platform, and
|
||||
**[docs/history/moving-to-linux.md](docs/history/moving-to-linux.md)** for moving a checkout
|
||||
between machines.
|
||||
userland from any host; `nix-shell` supplies all of it. See
|
||||
**[docs/guides/building.md](docs/guides/building.md)** for prerequisites per platform.
|
||||
|
||||
## Testing
|
||||
|
||||
|
|
@ -160,28 +158,35 @@ tools/windows/bootcheck.ps1 -Tty -Ps # terminal behaviour, process enumeration
|
|||
make schfs-check # SchFS B-tree, object and image suites
|
||||
```
|
||||
|
||||
`bootcheck` is PowerShell because `pwsh` runs on every supported host.
|
||||
|
||||
The numbers at the top are measured, not typed: `python3 tools/python/stats.py`
|
||||
rewrites them here and on the website.
|
||||
`bootcheck` is PowerShell because `pwsh` runs on every supported host. Several
|
||||
programs in `cmd/` (`socktest`, `memfdtest`, `xready`, …) exist to be run *on* the
|
||||
system and report which of their own checks failed. See
|
||||
**[docs/guides/testing.md](docs/guides/testing.md)** for what each proves. The numbers at
|
||||
the top are measured, not typed: `python3 tools/python/stats.py` rewrites them here
|
||||
and on the website.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
kernel/ arch/ core/ dev/ drivers/ fs/ net/ proc/ sys/
|
||||
kernel/ arch/ core/ dev/ drivers/ fs/ net/ proc/ sys/ acpi/ gmmos/ scff/
|
||||
fs/schfs/ the filesystem -- same source the host tools compile
|
||||
ldls/ the Linux driver layer: the .ko loader, BTF reader, Linux symbols
|
||||
include/ kernel and userland headers, by subsystem (arch/ core/ fs/ dev/ ...)
|
||||
boot/ uefi/ (PE loader) common/ (shared loader UI)
|
||||
lib/ cmd/ first-party userland runtime and programs
|
||||
ports/ third-party software built from source
|
||||
etc/ what the image starts with: schinit's job manifests, motd, ...
|
||||
ports/ third-party software built from source; ports/alpine is the apk root
|
||||
tools/ windows/ (.ps1) posix/ (.sh) python/ (.py) and .c/.pl
|
||||
projects/ things that run ON SchOS (an installer, SchFS for Linux: both frozen)
|
||||
docs/ architecture/ (what it is) plan/ (what's next)
|
||||
guides/ (how to work on it) history/ (why it's like this)
|
||||
Website/ (the site, and the docs reader)
|
||||
```
|
||||
|
||||
**[docs/README.md](docs/README.md)** is the index. If you are reading the tree
|
||||
for the first time, [docs/architecture/README.md](docs/architecture/README.md)
|
||||
is the system layer by layer.
|
||||
is the system layer by layer, and [docs/plan/1.0.md](docs/plan/1.0.md) says
|
||||
where it stands against its finish line.
|
||||
|
||||
One rule worth knowing: **the kernel and the host tools compile the same SchFS
|
||||
source**. `mkschfs` cannot disagree with the kernel about the on-disk format,
|
||||
|
|
@ -193,19 +198,24 @@ Another: **syscalls are declared in exactly one place**,
|
|||
|
||||
## Not there yet
|
||||
|
||||
- **No threads.** `clone` makes processes, not threads (`CLONE_VM` is
|
||||
refused), so multi-threaded programs run on one thread or not at all. Xorg
|
||||
runs with `-dumbSched` for this reason.
|
||||
- **No udev.** Xorg is told its input devices by a config file instead of
|
||||
finding them; `/sys` and the device-event socket are what is missing.
|
||||
- **No installer, and nothing persists.** The live image runs from RAM and
|
||||
writes nothing to the machine's disks, so what you install or save is gone at
|
||||
reboot. This is the one thing between the system and its 1.0 definition.
|
||||
- **No GPU acceleration.** Xorg draws in software; the plan is Linux's `amdgpu`
|
||||
through LDLS ([docs/plan/gpu.md](docs/plan/gpu.md)). There is no IOMMU either,
|
||||
so a driver in a process is isolated against its bugs, not against malice.
|
||||
- **udev sees the kernel's own devices only.** eudev runs and Xorg finds its input devices
|
||||
through it; PCI and USB devices are not yet in the `/sys/devices` tree.
|
||||
- **WebKit browsers do not start.** NetSurf does; Epiphany's web process dies at
|
||||
start-up, and it has not been run to ground.
|
||||
- **USB 2.0 controllers (EHCI)** have no driver. On older machines the ports
|
||||
wired only to EHCI do nothing; xHCI ports work.
|
||||
- **Touchpads run in mouse mode**: pointer, click and tap, but no two-finger
|
||||
scroll yet.
|
||||
- **No page cache**, so writable `MAP_SHARED` of a file is refused rather than
|
||||
faked. SysV shared memory is missing too.
|
||||
- **No installer.** SchOS runs live from the USB stick; nothing is written to
|
||||
the machine's disks.
|
||||
faked, and `mprotect` is accepted and ignored (nothing has NX).
|
||||
- **No Wi-Fi, no HDA audio**, and the Realtek NIC on the laptop has reached the
|
||||
point of finding its PHY; whether it carries traffic there is not confirmed.
|
||||
- SchFS has no Linux driver yet; the plan is on-disk format version first, then
|
||||
FUSE, then a kernel module.
|
||||
- CFF/OpenType outlines and TrueType hinting are deliberately out of scope.
|
||||
|
|
@ -215,14 +225,16 @@ Another: **syscalls are declared in exactly one place**,
|
|||
**GPL-2.0-or-later**, copyright Nexoniarz. See [LICENSE](LICENSE).
|
||||
|
||||
The kernel derives from xv6, which is MIT — see [LICENSE.MIT](LICENSE.MIT).
|
||||
Third-party code redistributed in a built image (musl, toybox, mksh, zsh, kilo,
|
||||
the fonts) keeps its own licence; [NOTICE](NOTICE) carries the attributions.
|
||||
See [docs/licensing.md](docs/licensing.md).
|
||||
Third-party code redistributed in a built image (musl, toybox, mksh, zstd, the
|
||||
fonts, and Alpine's packages, each under its own licence) keeps that licence;
|
||||
[NOTICE](NOTICE) carries the attributions. See [docs/licensing.md](docs/licensing.md).
|
||||
|
||||
Drivers may be adapted from BSD sources and, since the move to the GPL, from
|
||||
Linux — GPL-2.0-only code combines with GPL-2.0-or-later, and the result ships
|
||||
under GPLv2. Best of all is still the public specification — xHCI, virtio,
|
||||
AHCI, e1000 — which carries no licence on what you write from it.
|
||||
under GPLv2. Linux's own drivers run unmodified through LDLS, and the ISO ships the
|
||||
Realtek `r8169` set and its firmware; their licences and source are in [NOTICE](NOTICE). Best of
|
||||
all is still the public specification — xHCI, virtio, AHCI, e1000 — which carries
|
||||
no licence on what you write from it.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
760
boot/common/ui.c
760
boot/common/ui.c
|
|
@ -13,6 +13,7 @@
|
|||
#include "../../include/gfx/theme.h"
|
||||
#include "../../include/gfx/font.h"
|
||||
#include "../../include/gfx/logo.h"
|
||||
#include "../../include/gfx/nvfont.h"
|
||||
|
||||
/* "Draw the glyph, leave the background alone." Not a colour anyone can mean:
|
||||
* the palette is 24-bit, so the high byte is free for a sentinel. */
|
||||
|
|
@ -21,6 +22,25 @@
|
|||
static struct ui_fb FB;
|
||||
static int ACTIVE;
|
||||
|
||||
/* What the mark is blended against: the splash ground, or a window body. */
|
||||
static ui_u32 LOGO_BG = SCH_BG;
|
||||
|
||||
/* The boot manager's scale and faces. 2 on a screen big enough that a 15 px
|
||||
* face would be a speck; the faces are rasterised at both sizes, not doubled. */
|
||||
static int SC = 1;
|
||||
static const struct nvfont *F_BODY, *F_SMALL, *F_TITLE;
|
||||
static int CUR_ON; /* the pointer is drawn right now */
|
||||
|
||||
static void
|
||||
ui_set_scale(void)
|
||||
{
|
||||
SC = (FB.height >= 1400 || FB.width >= 2400) ? 2 : 1;
|
||||
F_BODY = SC == 2 ? &nvfont_body2 : &nvfont_body1;
|
||||
F_SMALL = SC == 2 ? &nvfont_small2 : &nvfont_small1;
|
||||
F_TITLE = SC == 2 ? &nvfont_title2 : &nvfont_title1;
|
||||
CUR_ON = 0; /* a new framebuffer holds no pointer */
|
||||
}
|
||||
|
||||
/* Progress state. Kept here rather than passed around because the caller ticks
|
||||
* it from inside a read loop that has no business carrying UI state. */
|
||||
static ui_u32 PROG_TOTAL, PROG_DONE;
|
||||
|
|
@ -101,6 +121,7 @@ ui_init(const struct ui_fb *fb)
|
|||
if (!FB.stride)
|
||||
FB.stride = FB.width * 4;
|
||||
ACTIVE = 1;
|
||||
ui_set_scale();
|
||||
}
|
||||
|
||||
int ui_active(void) { return ACTIVE; }
|
||||
|
|
@ -236,7 +257,7 @@ ui_logo(int y, int w)
|
|||
|
||||
/* Coverage blends the shaded metal against the ground, so the mark
|
||||
* has soft edges and a lit face at the same time. */
|
||||
put(x0 + dx, y + dy, mix(SCH_BG, shade, (int)cov, 15));
|
||||
put(x0 + dx, y + dy, mix(LOGO_BG, shade, (int)cov, 15));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -244,121 +265,555 @@ ui_logo(int y, int w)
|
|||
/* Where the bar goes. Derived, not stored, so nothing can set it and then have
|
||||
* the screen change underneath it.
|
||||
*
|
||||
* The bar is exactly the mark's width and sits directly beneath it: the two are
|
||||
* one object, and a bar wider than the mark it belongs to reads as a separate
|
||||
* thing that happens to be nearby.
|
||||
* The track is wider than the mark and sits beneath it: a mark with a recessed
|
||||
* track and one line of type, centred, on the same graphite the boot manager
|
||||
* uses. Laid out from the middle outwards so it reads the same at 800x600 and
|
||||
* at 4K. S() is defined further down with the rest of the Niveus code, so the
|
||||
* scale is applied through SC directly.
|
||||
*/
|
||||
static void
|
||||
bar_geometry(void)
|
||||
{
|
||||
PROG_W = ui_mark_width();
|
||||
PROG_H = BAR_H;
|
||||
PROG_W = 220 * SC;
|
||||
PROG_H = 6 * SC;
|
||||
PROG_X = ((int)FB.width - PROG_W) / 2;
|
||||
|
||||
int mark_h = (LOGO_H * PROG_W) / LOGO_W;
|
||||
int group_h = mark_h + MARK_GAP + PROG_H;
|
||||
int mw = ui_mark_width() * SC;
|
||||
if (mw > LOGO_W) mw = LOGO_W;
|
||||
int mark_h = (LOGO_H * mw) / LOGO_W;
|
||||
int gap = 34 * SC;
|
||||
int group_h = mark_h + gap + PROG_H;
|
||||
int top = ((int)FB.height - group_h) / 2;
|
||||
PROG_Y = top + mark_h + MARK_GAP;
|
||||
PROG_Y = top + mark_h + gap;
|
||||
}
|
||||
|
||||
static ui_u32 bg_at(int y);
|
||||
static void nv_backdrop(void);
|
||||
static int nv_width(const struct nvfont *f, const char *s);
|
||||
static void nv_text(const struct nvfont *f, int x, int base, const char *s,
|
||||
ui_u32 fg, ui_u32 bgtop, ui_u32 bgbot, int y0, int h);
|
||||
static void rrect(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot,
|
||||
ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh);
|
||||
|
||||
void
|
||||
ui_splash(const char *subtitle)
|
||||
{
|
||||
if (!ACTIVE)
|
||||
return;
|
||||
|
||||
ui_clear();
|
||||
nv_backdrop();
|
||||
bar_geometry();
|
||||
|
||||
int w = PROG_W;
|
||||
int w = ui_mark_width() * SC;
|
||||
if (w > LOGO_W) w = LOGO_W;
|
||||
int mark_h = (LOGO_H * w) / LOGO_W;
|
||||
int top = PROG_Y - MARK_GAP - mark_h;
|
||||
int top = PROG_Y - 34 * SC - mark_h;
|
||||
|
||||
ui_u32 saved = LOGO_BG;
|
||||
LOGO_BG = bg_at(top + mark_h / 2);
|
||||
ui_logo(top, w);
|
||||
LOGO_BG = saved;
|
||||
|
||||
if (subtitle)
|
||||
ui_text_centered(PROG_Y + PROG_H + SUB_GAP, subtitle, SCH_DIM);
|
||||
ui_progress_label(subtitle);
|
||||
}
|
||||
|
||||
/* --- panels ---------------------------------------------------------------
|
||||
/* --- Niveus -----------------------------------------------------------------
|
||||
*
|
||||
* A panel is a plate: a slightly lighter ground than the screen, one lighter
|
||||
* row along the top and a darker border, which is the whole of the bevel. Two
|
||||
* rows of shading do more for the impression of a raised surface than any
|
||||
* amount of gradient inside it, and cost nothing to draw.
|
||||
* The boot manager is drawn in the same language as the rest of the system
|
||||
* (docs/Website/niveus): windows are solid, controls are
|
||||
* bevelled keys with light falling across them, fields are wells cut into the
|
||||
* surface, and the ground under all of it is warm graphite rather than blue.
|
||||
*
|
||||
* Every shape here is a rounded rectangle with a vertical gradient, which is
|
||||
* all that skeuomorphism needs at this size. Corners are anti-aliased by
|
||||
* supersampling the corner pixels only, and the AA blends against a colour the
|
||||
* caller *names*, never against what is in the framebuffer: reading the
|
||||
* framebuffer back is uncached on real hardware and ruinously slow.
|
||||
*
|
||||
* The typeface is Instrument Sans, rasterised at build time (nvfont.h).
|
||||
*/
|
||||
static void
|
||||
panel(int x, int y, int w, int h)
|
||||
|
||||
#define NV_BG_TOP 0x1B1B1Eu /* the ground, light at the top */
|
||||
#define NV_BG_BOT 0x0B0B0Du
|
||||
#define NV_WIN_BODY 0x232326u
|
||||
#define NV_WIN_EDGE 0x050507u
|
||||
#define NV_TITLE_TOP 0x45444Au
|
||||
#define NV_TITLE_BOT 0x2C2B30u
|
||||
#define NV_HILITE 0x5E5D64u /* the lit upper edge of a raised thing */
|
||||
#define NV_KEY_TOP 0x3F3E44u
|
||||
#define NV_KEY_BOT 0x2D2C32u
|
||||
#define NV_KEY_EDGE 0x111113u
|
||||
#define NV_KEY_SHADOW 0x141416u
|
||||
#define NV_WELL_TOP 0x101012u
|
||||
#define NV_WELL_BOT 0x171719u
|
||||
#define NV_WELL_EDGE 0x0A0A0Bu
|
||||
#define NV_WELL_LIP 0x3A393Eu /* light catching the lower lip of a well */
|
||||
#define NV_TXT 0xECEAE5u
|
||||
#define NV_TXT2 0xA9A7A1u
|
||||
#define NV_TXT3 0x74726Du
|
||||
|
||||
#define S(n) ((n) * SC)
|
||||
#define WHITE 0xFFFFFFu
|
||||
#define BLACK 0x000000u
|
||||
|
||||
static ui_u32
|
||||
bg_at(int y)
|
||||
{
|
||||
ui_fill_rect(x, y, w, h, PANEL_BG);
|
||||
ui_fill_rect(x, y, w, 1, PANEL_TOP); /* lit top edge */
|
||||
ui_fill_rect(x, y + h - 1, w, 1, METAL_EDGE); /* shadowed bottom */
|
||||
ui_fill_rect(x, y, 1, h, METAL_EDGE);
|
||||
ui_fill_rect(x + w - 1, y, 1, h, METAL_EDGE);
|
||||
return mix(NV_BG_TOP, NV_BG_BOT, y, (int)FB.height > 1 ? (int)FB.height - 1 : 1);
|
||||
}
|
||||
|
||||
/* A heading in metal: the same shading the mark uses, applied per row of the
|
||||
* glyph cell, so a title looks struck out of the same material. */
|
||||
static void
|
||||
metal_text_centered(int y, const char *s)
|
||||
nv_backdrop(void)
|
||||
{
|
||||
int len = slen(s);
|
||||
int x = ((int)FB.width - len * GLYPH_W) / 2;
|
||||
for (int y = 0; y < (int)FB.height; y++)
|
||||
ui_fill_rect(0, y, (int)FB.width, 1, bg_at(y));
|
||||
}
|
||||
|
||||
for (int row = 0; row < GLYPH_H; row++) {
|
||||
ui_u32 shade = metal(row, GLYPH_H);
|
||||
for (int i = 0; i < len; i++) {
|
||||
ui_u8 bits = font_data[(ui_u8)s[i]][row];
|
||||
for (int col = 0; col < GLYPH_W; col++)
|
||||
if (bits & (0x80 >> col))
|
||||
put(x + i * GLYPH_W + col, y + row, shade);
|
||||
static int
|
||||
nv_glyph_index(unsigned c)
|
||||
{
|
||||
if (c < NVFONT_FIRST || c > NVFONT_LAST)
|
||||
c = '?';
|
||||
return (int)(c - NVFONT_FIRST);
|
||||
}
|
||||
|
||||
static int
|
||||
nv_width(const struct nvfont *f, const char *s)
|
||||
{
|
||||
int w = 0;
|
||||
for (; *s && *s != '\n'; s++)
|
||||
w += f->g[nv_glyph_index((ui_u8)*s)].adv;
|
||||
return (w + 32) >> 6;
|
||||
}
|
||||
|
||||
static int
|
||||
nv_height(const struct nvfont *f)
|
||||
{
|
||||
return f->ascent + f->descent;
|
||||
}
|
||||
|
||||
/* Draw `s` (up to a newline) with its origin on baseline `base`. The colour
|
||||
* under it is a vertical gradient from bgtop at row y0 to bgbot at y0+h. */
|
||||
static void
|
||||
nv_text(const struct nvfont *f, int x, int base, const char *s, ui_u32 fg,
|
||||
ui_u32 bgtop, ui_u32 bgbot, int y0, int h)
|
||||
{
|
||||
int pen = 0; /* 1/64 px: the face's own spacing */
|
||||
for (; *s && *s != '\n'; s++) {
|
||||
const struct nvglyph *g = &f->g[nv_glyph_index((ui_u8)*s)];
|
||||
const ui_u8 *bits = f->bits + g->off;
|
||||
int gx = x + ((pen + 32) >> 6);
|
||||
for (int r = 0; r < g->h; r++) {
|
||||
int py = base + g->oy + r;
|
||||
ui_u32 bg = mix(bgtop, bgbot, py - y0, h > 1 ? h - 1 : 1);
|
||||
for (int c = 0; c < g->w; c++) {
|
||||
ui_u8 a = bits[r * g->w + c];
|
||||
if (a)
|
||||
put(gx + g->ox + c, py, mix(bg, fg, a, 255));
|
||||
}
|
||||
}
|
||||
pen += g->adv;
|
||||
}
|
||||
}
|
||||
|
||||
/* How much of the pixel at (dx,dy) from a corner's centre lies inside a circle
|
||||
* of radius r, in sixteenths. */
|
||||
static int
|
||||
corner_cov(int dx, int dy, int r)
|
||||
{
|
||||
int n = 0, R = r * 8;
|
||||
for (int j = 0; j < 4; j++)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int sx = dx * 8 + 2 * i + 1, sy = dy * 8 + 2 * j + 1;
|
||||
if (sx * sx + sy * sy <= R * R)
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static void
|
||||
rrect(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot,
|
||||
ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh)
|
||||
{
|
||||
if (w <= 0 || h <= 0)
|
||||
return;
|
||||
if (r * 2 > h) r = h / 2;
|
||||
if (r * 2 > w) r = w / 2;
|
||||
|
||||
for (int rr = 0; rr < h; rr++) {
|
||||
int yy = y + rr;
|
||||
ui_u32 col = mix(top, bot, rr, h > 1 ? h - 1 : 1);
|
||||
int cy, corner = 1;
|
||||
if (rr < r) cy = y + r;
|
||||
else if (rr >= h - r) cy = y + h - r;
|
||||
else corner = 0;
|
||||
|
||||
if (!corner) {
|
||||
ui_fill_rect(x, yy, w, 1, col);
|
||||
continue;
|
||||
}
|
||||
ui_u32 bg = mix(bgtop, bgbot, yy - bgy, bgh > 1 ? bgh - 1 : 1);
|
||||
ui_fill_rect(x + r, yy, w - 2 * r, 1, col);
|
||||
for (int k = 0; k < r; k++) {
|
||||
int dy = yy - cy;
|
||||
int cl = corner_cov(k - r, dy, r);
|
||||
int cr = corner_cov(k, dy, r);
|
||||
if (cl) put(x + k, yy, mix(bg, col, cl, 16));
|
||||
if (cr) put(x + w - r + k, yy, mix(bg, col, cr, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Panel metrics.
|
||||
*
|
||||
* TITLE_H is the whole title band -- the padding above the heading, the
|
||||
* heading itself, and the air beneath it before the rule. It was 34 while the
|
||||
* heading alone needs 18 + 16 = 34, so the rule at TITLE_H - 6 was drawn
|
||||
* *through* the lettering and the first row started on the heading's baseline.
|
||||
* Derived from its parts now, so it cannot drift from them again.
|
||||
*/
|
||||
#define ROW_H 24
|
||||
#define PANEL_PAD 18
|
||||
#define TITLE_GAP 14 /* heading to the rule */
|
||||
#define TITLE_H (PANEL_PAD + GLYPH_H + TITLE_GAP) /* = 48 */
|
||||
#define ROW_LIFT ((ROW_H - GLYPH_H) / 2) /* highlight overhang */
|
||||
/* A raised object: an edge, a gradient face, and a bright line just inside the
|
||||
* top where the light lands. */
|
||||
static void
|
||||
bevel(int x, int y, int w, int h, int r, ui_u32 top, ui_u32 bot, ui_u32 edge,
|
||||
ui_u32 hi, ui_u32 bgtop, ui_u32 bgbot, int bgy, int bgh)
|
||||
{
|
||||
rrect(x, y, w, h, r, edge, edge, bgtop, bgbot, bgy, bgh);
|
||||
rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r > SC ? r - SC : r,
|
||||
top, bot, edge, edge, 0, 1);
|
||||
if (hi)
|
||||
ui_fill_rect(x + r, y + SC, w - 2 * r, SC, hi);
|
||||
}
|
||||
|
||||
/* A recess: the same thing with the light reversed. Darker at the top where the
|
||||
* rim shades it, with a pale lip along the bottom edge. */
|
||||
static void
|
||||
well(int x, int y, int w, int h, ui_u32 bgcol)
|
||||
{
|
||||
int r = S(8);
|
||||
bevel(x, y, w, h, r, NV_WELL_TOP, NV_WELL_BOT, NV_WELL_EDGE, 0, bgcol, bgcol, 0, 1);
|
||||
ui_fill_rect(x + r, y + h - SC, w - 2 * r, SC, NV_WELL_LIP);
|
||||
}
|
||||
|
||||
/* The soft shadow beneath a window, as stacked translucent layers laid down
|
||||
* from the outermost in, each one blended against the backdrop's own gradient. */
|
||||
static void
|
||||
shadow(int x, int y, int w, int h, int r)
|
||||
{
|
||||
int spread = S(16), dy = S(8);
|
||||
for (int i = spread; i > 0; i -= SC) {
|
||||
int t = spread - i;
|
||||
int a = 60 * t * t / (spread * spread); /* percent black */
|
||||
int ty = y - i + dy, th = h + 2 * i;
|
||||
ui_u32 top = mix(bg_at(ty), BLACK, a, 100);
|
||||
ui_u32 bot = mix(bg_at(ty + th), BLACK, a, 100);
|
||||
rrect(x - i, ty, w + 2 * i, th, r + i, top, bot,
|
||||
bg_at(ty), bg_at(ty + th), ty, th);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- the pointer ----------------------------------------------------------- */
|
||||
|
||||
#define CUR_W 12
|
||||
#define CUR_H 19
|
||||
static const char *const CUR_SHAPE[CUR_H] = {
|
||||
"X ",
|
||||
"XX ",
|
||||
"X.X ",
|
||||
"X..X ",
|
||||
"X...X ",
|
||||
"X....X ",
|
||||
"X.....X ",
|
||||
"X......X ",
|
||||
"X.......X ",
|
||||
"X........X ",
|
||||
"X.........X ",
|
||||
"X......XXXXX",
|
||||
"X...X..X ",
|
||||
"X..X X..X ",
|
||||
"X.X X..X ",
|
||||
"XX X..X ",
|
||||
"X X..X ",
|
||||
" X..X ",
|
||||
" XXX ",
|
||||
};
|
||||
static ui_u32 CUR_SAVE[CUR_W * 2 * CUR_H * 2];
|
||||
static int CUR_X, CUR_Y, CUR_WANT;
|
||||
|
||||
static void
|
||||
panel_frame(const char *title, int rows, const char *hint,
|
||||
int *out_x, int *out_y, int *out_w)
|
||||
cursor_hide(void)
|
||||
{
|
||||
int w = (int)FB.width / 2;
|
||||
if (w < 360) w = 360;
|
||||
if (w > (int)FB.width - 60) w = (int)FB.width - 60;
|
||||
if (!CUR_ON || !ACTIVE)
|
||||
return;
|
||||
int w = CUR_W * SC, h = CUR_H * SC;
|
||||
for (int r = 0; r < h; r++)
|
||||
for (int c = 0; c < w; c++) {
|
||||
int px = CUR_X + c, py = CUR_Y + r;
|
||||
if (px >= 0 && py >= 0 && (ui_u32)px < FB.width && (ui_u32)py < FB.height)
|
||||
*(ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4) =
|
||||
CUR_SAVE[r * w + c];
|
||||
}
|
||||
CUR_ON = 0;
|
||||
}
|
||||
|
||||
int h = TITLE_H + rows * ROW_H + PANEL_PAD * 2;
|
||||
int x = ((int)FB.width - w) / 2;
|
||||
int y = ((int)FB.height - h) / 2;
|
||||
static void
|
||||
cursor_show(void)
|
||||
{
|
||||
if (!CUR_WANT || CUR_ON || !ACTIVE)
|
||||
return;
|
||||
int w = CUR_W * SC, h = CUR_H * SC;
|
||||
for (int r = 0; r < h; r++)
|
||||
for (int c = 0; c < w; c++) {
|
||||
int px = CUR_X + c, py = CUR_Y + r;
|
||||
if (px < 0 || py < 0 || (ui_u32)px >= FB.width || (ui_u32)py >= FB.height)
|
||||
continue;
|
||||
ui_u32 *p = (ui_u32 *)(FB.base + (ui_u64)py * FB.stride + (ui_u64)px * 4);
|
||||
CUR_SAVE[r * w + c] = *p;
|
||||
char ch = CUR_SHAPE[r / SC][c / SC];
|
||||
if (ch == 'X') *p = BLACK;
|
||||
else if (ch == '.') *p = WHITE;
|
||||
}
|
||||
CUR_ON = 1;
|
||||
}
|
||||
|
||||
ui_clear();
|
||||
void
|
||||
ui_cursor(int x, int y, int show)
|
||||
{
|
||||
cursor_hide();
|
||||
CUR_X = x; CUR_Y = y; CUR_WANT = show;
|
||||
cursor_show();
|
||||
}
|
||||
|
||||
/* The mark, small, above the plate: it is the same screen, not a new one. */
|
||||
int mw = ui_mark_width() * 2 / 3;
|
||||
int mh = (LOGO_H * mw) / LOGO_W;
|
||||
if (y - mh - 24 > 0)
|
||||
ui_logo(y - mh - 24, mw);
|
||||
/* --- window chrome --------------------------------------------------------- */
|
||||
|
||||
panel(x, y, w, h);
|
||||
metal_text_centered(y + PANEL_PAD, title);
|
||||
ui_fill_rect(x + PANEL_PAD, y + PANEL_PAD + GLYPH_H + TITLE_GAP / 2,
|
||||
w - PANEL_PAD * 2, 1, METAL_EDGE);
|
||||
#define TB_H S(34)
|
||||
#define BODY_PAD S(18)
|
||||
#define HINT_H S(36)
|
||||
|
||||
if (hint)
|
||||
ui_text_centered(y + h + 16, hint, SCH_DIM);
|
||||
static void
|
||||
window(int x, int y, int w, int h, const char *title)
|
||||
{
|
||||
int r = S(12), tb = TB_H;
|
||||
|
||||
*out_x = x; *out_y = y + TITLE_H; *out_w = w;
|
||||
shadow(x, y, w, h, r);
|
||||
rrect(x, y, w, h, r, NV_WIN_EDGE, NV_WIN_EDGE, bg_at(y), bg_at(y + h), y, h);
|
||||
rrect(x + SC, y + SC, w - 2 * SC, h - 2 * SC, r - SC, NV_WIN_BODY, NV_WIN_BODY,
|
||||
NV_WIN_EDGE, NV_WIN_EDGE, 0, 1);
|
||||
|
||||
/* The titlebar is rounded on top only: draw it tall enough that its bottom
|
||||
* corners fall below the line, then lay the body colour over the excess. */
|
||||
rrect(x + SC, y + SC, w - 2 * SC, tb + r, r - SC, NV_TITLE_TOP, NV_TITLE_BOT,
|
||||
NV_WIN_EDGE, NV_WIN_EDGE, 0, 1);
|
||||
ui_fill_rect(x + SC, y + SC + tb, w - 2 * SC, r, NV_WIN_BODY);
|
||||
ui_fill_rect(x + SC, y + SC + tb - SC, w - 2 * SC, SC, NV_WIN_EDGE);
|
||||
ui_fill_rect(x + r, y + SC, w - 2 * r, SC, NV_HILITE);
|
||||
|
||||
int tw = nv_width(F_BODY, title);
|
||||
int base = y + SC + (tb - nv_height(F_BODY)) / 2 + F_BODY->ascent;
|
||||
nv_text(F_BODY, x + (w - tw) / 2, base, title, NV_TXT2,
|
||||
NV_TITLE_TOP, NV_TITLE_BOT, y + SC, tb);
|
||||
}
|
||||
|
||||
static void
|
||||
footer(int x, int y, int w, int h, const char *hint)
|
||||
{
|
||||
if (!hint)
|
||||
return;
|
||||
int fy = y + h - HINT_H;
|
||||
ui_fill_rect(x + S(14), fy, w - S(28), SC, 0x1A1A1Cu);
|
||||
ui_fill_rect(x + S(14), fy + SC, w - S(28), SC, 0x2E2E32u);
|
||||
int tw = nv_width(F_SMALL, hint);
|
||||
int base = fy + (HINT_H - nv_height(F_SMALL)) / 2 + F_SMALL->ascent + SC;
|
||||
nv_text(F_SMALL, x + (w - tw) / 2, base, hint, NV_TXT3,
|
||||
NV_WIN_BODY, NV_WIN_BODY, fy, HINT_H);
|
||||
}
|
||||
|
||||
/* --- keys ------------------------------------------------------------------ */
|
||||
|
||||
#define MAXKEYS 40
|
||||
|
||||
static struct { int x, y, w, h; } KEY[MAXKEYS];
|
||||
|
||||
static struct {
|
||||
const char *title, *hint;
|
||||
const char *const *items;
|
||||
int n, sel, scroll, vis, kind; /* kind: 0 a list of keys, 1 a panel */
|
||||
int has_detail, kh, gap, logo;
|
||||
int wx, wy, ww, wh;
|
||||
int lx, ly, lw; /* the list's box */
|
||||
int ypos[MAXKEYS];
|
||||
} M;
|
||||
|
||||
static int
|
||||
has_nl(const char *s)
|
||||
{
|
||||
for (; *s; s++)
|
||||
if (*s == '\n')
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *
|
||||
label_of(const char *s)
|
||||
{
|
||||
return s[0] == 1 ? s + 1 : s;
|
||||
}
|
||||
|
||||
static const char *
|
||||
detail_of(const char *s)
|
||||
{
|
||||
for (; *s; s++)
|
||||
if (*s == '\n')
|
||||
return s + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
paint_key(int i, int sel)
|
||||
{
|
||||
if (i < M.scroll || i >= M.scroll + M.vis || i >= MAXKEYS)
|
||||
return;
|
||||
|
||||
int x = KEY[i].x, y = KEY[i].y, w = KEY[i].w, h = KEY[i].h, r = S(8);
|
||||
const char *item = M.items[i];
|
||||
const char *label = label_of(item), *detail = detail_of(item);
|
||||
|
||||
/* Wipe the ring/shadow a previous state left, then draw this one. */
|
||||
ui_fill_rect(x - S(3), y - S(3), w + S(6), h + S(7), NV_WIN_BODY);
|
||||
|
||||
ui_u32 top, bot, edge, hi, fg, fg2;
|
||||
if (sel) {
|
||||
ui_u32 ring = mix(NV_WIN_BODY, SCH_ACCENT, 70, 100);
|
||||
rrect(x - S(2), y - S(2), w + S(4), h + S(4), r + S(2), ring, ring,
|
||||
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
|
||||
top = mix(SCH_ACCENT, WHITE, 20, 100);
|
||||
bot = mix(SCH_ACCENT, BLACK, 24, 100);
|
||||
edge = mix(SCH_ACCENT, BLACK, 62, 100);
|
||||
hi = mix(top, WHITE, 45, 100);
|
||||
fg = WHITE; fg2 = 0xE2DBFFu;
|
||||
} else {
|
||||
rrect(x, y + SC, w, h, r, NV_KEY_SHADOW, NV_KEY_SHADOW,
|
||||
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
|
||||
top = NV_KEY_TOP; bot = NV_KEY_BOT; edge = NV_KEY_EDGE; hi = NV_HILITE;
|
||||
fg = NV_TXT; fg2 = NV_TXT3;
|
||||
}
|
||||
bevel(x, y, w, h, r, top, bot, edge, hi, NV_WIN_BODY, NV_WIN_BODY, 0, 1);
|
||||
|
||||
int ty0 = y + SC, th = h - 2 * SC, tx = x + S(18);
|
||||
if (detail) {
|
||||
int lh = nv_height(F_BODY), dh = nv_height(F_SMALL);
|
||||
int total = lh + S(2) + dh;
|
||||
int t = y + (h - total) / 2;
|
||||
nv_text(F_BODY, tx, t + F_BODY->ascent, label, fg, top, bot, ty0, th);
|
||||
nv_text(F_SMALL, tx, t + lh + S(2) + F_SMALL->ascent, detail,
|
||||
sel ? fg2 : NV_TXT2, top, bot, ty0, th);
|
||||
} else {
|
||||
int base = y + (h - nv_height(F_BODY)) / 2 + F_BODY->ascent;
|
||||
nv_text(F_BODY, tx, base, label, fg, top, bot, ty0, th);
|
||||
}
|
||||
|
||||
if (M.kind == 0 && M.n <= 9 && M.vis == M.n) {
|
||||
char num[2] = { (char)('1' + i), 0 };
|
||||
int nw = nv_width(F_SMALL, num);
|
||||
int base = y + (h - nv_height(F_SMALL)) / 2 + F_SMALL->ascent;
|
||||
nv_text(F_SMALL, x + w - S(18) - nw, base, num, sel ? fg2 : NV_TXT3,
|
||||
top, bot, ty0, th);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
menu_layout(void)
|
||||
{
|
||||
int n = M.n > MAXKEYS ? MAXKEYS : M.n;
|
||||
M.n = n;
|
||||
M.has_detail = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (has_nl(M.items[i]))
|
||||
M.has_detail = 1;
|
||||
M.gap = S(8);
|
||||
M.kh = M.has_detail ? S(50) : S(34);
|
||||
M.ww = S(520);
|
||||
if (M.ww > (int)FB.width - S(24))
|
||||
M.ww = (int)FB.width - S(24);
|
||||
|
||||
int maxh = (int)FB.height - S(24);
|
||||
int chrome = TB_H + BODY_PAD * 2 + HINT_H + 2 * SC;
|
||||
int logoh = S(56) + S(16);
|
||||
int full;
|
||||
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
int y = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (pass == 0 && i && M.items[i][0] == 1)
|
||||
y += S(14);
|
||||
M.ypos[i] = y;
|
||||
y += M.kh + M.gap;
|
||||
}
|
||||
full = n ? y - M.gap : 0;
|
||||
if (pass == 0 && chrome + full <= maxh)
|
||||
break;
|
||||
if (pass == 0 && chrome + logoh + full <= maxh)
|
||||
break;
|
||||
}
|
||||
|
||||
M.logo = 1;
|
||||
M.vis = n;
|
||||
int list_h = full;
|
||||
if (chrome + logoh + full > maxh)
|
||||
M.logo = 0;
|
||||
if (chrome + full > maxh) {
|
||||
int avail = maxh - chrome;
|
||||
M.vis = (avail + M.gap) / (M.kh + M.gap);
|
||||
if (M.vis < 1) M.vis = 1;
|
||||
if (M.vis > n) M.vis = n;
|
||||
list_h = M.vis * M.kh + (M.vis - 1) * M.gap;
|
||||
}
|
||||
M.wh = chrome + (M.logo ? logoh : 0) + list_h;
|
||||
M.wx = ((int)FB.width - M.ww) / 2;
|
||||
M.wy = ((int)FB.height - M.wh) / 2;
|
||||
if (M.wy < 0) M.wy = 0;
|
||||
M.lx = M.wx + S(20);
|
||||
M.lw = M.ww - S(40) - (M.vis < n ? S(14) : 0);
|
||||
M.ly = M.wy + SC + TB_H + BODY_PAD + (M.logo ? logoh : 0);
|
||||
|
||||
if (M.sel < M.scroll) M.scroll = M.sel;
|
||||
if (M.sel >= M.scroll + M.vis) M.scroll = M.sel - M.vis + 1;
|
||||
if (M.scroll < 0) M.scroll = 0;
|
||||
if (M.scroll > n - M.vis) M.scroll = n - M.vis;
|
||||
if (M.scroll < 0) M.scroll = 0;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
KEY[i].x = M.lx;
|
||||
KEY[i].w = M.lw;
|
||||
KEY[i].h = M.kh;
|
||||
KEY[i].y = M.ly + M.ypos[i] - M.ypos[M.scroll];
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
menu_paint(void)
|
||||
{
|
||||
cursor_hide();
|
||||
nv_backdrop();
|
||||
window(M.wx, M.wy, M.ww, M.wh, M.title);
|
||||
|
||||
if (M.logo) {
|
||||
ui_u32 saved = LOGO_BG;
|
||||
LOGO_BG = NV_WIN_BODY;
|
||||
ui_logo(M.wy + SC + TB_H + BODY_PAD, S(56));
|
||||
LOGO_BG = saved;
|
||||
}
|
||||
|
||||
for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++)
|
||||
paint_key(i, i == M.sel);
|
||||
|
||||
if (M.vis < M.n) {
|
||||
int tx = M.lx + M.lw + S(8), tw = S(4);
|
||||
int th = M.vis * M.kh + (M.vis - 1) * M.gap;
|
||||
rrect(tx, M.ly, tw, th, tw / 2, NV_WELL_TOP, NV_WELL_BOT,
|
||||
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
|
||||
int thumb = th * M.vis / M.n;
|
||||
if (thumb < S(16)) thumb = S(16);
|
||||
int ty = M.ly + (th - thumb) * M.scroll / (M.n - M.vis);
|
||||
rrect(tx, ty, tw, thumb, tw / 2, NV_KEY_TOP, NV_KEY_BOT,
|
||||
NV_WIN_BODY, NV_WIN_BODY, 0, 1);
|
||||
}
|
||||
|
||||
footer(M.wx, M.wy, M.ww, M.wh, M.hint);
|
||||
cursor_show();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -367,46 +822,129 @@ ui_menu(const char *title, const char *const *items, int n, int selected,
|
|||
{
|
||||
if (!ACTIVE)
|
||||
return;
|
||||
M.title = title; M.items = items; M.n = n; M.sel = selected; M.hint = hint;
|
||||
M.scroll = 0; M.kind = 0;
|
||||
menu_layout();
|
||||
menu_paint();
|
||||
}
|
||||
|
||||
int x, y, w;
|
||||
panel_frame(title, n, hint, &x, &y, &w);
|
||||
void
|
||||
ui_menu_select(int selected)
|
||||
{
|
||||
if (!ACTIVE || M.kind != 0 || selected < 0 || selected >= M.n || selected == M.sel)
|
||||
return;
|
||||
int old = M.sel;
|
||||
M.sel = selected;
|
||||
if (selected < M.scroll || selected >= M.scroll + M.vis) {
|
||||
menu_layout();
|
||||
menu_paint();
|
||||
return;
|
||||
}
|
||||
cursor_hide();
|
||||
paint_key(old, 0);
|
||||
paint_key(selected, 1);
|
||||
cursor_show();
|
||||
}
|
||||
|
||||
int
|
||||
ui_menu_hit(int x, int y)
|
||||
{
|
||||
if (!ACTIVE)
|
||||
return -1;
|
||||
for (int i = M.scroll; i < M.scroll + M.vis && i < M.n; i++)
|
||||
if (x >= KEY[i].x && x < KEY[i].x + KEY[i].w &&
|
||||
y >= KEY[i].y && y < KEY[i].y + KEY[i].h)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* A panel: label/value rows in a well, and one button underneath. With no
|
||||
* values, the labels are a message and are centred. */
|
||||
static void
|
||||
box(const char *title, const char *const *labels, const char *const *values,
|
||||
int n, const char *button, const char *hint)
|
||||
{
|
||||
if (!ACTIVE)
|
||||
return;
|
||||
int rowh = S(28), wpad = S(12);
|
||||
int well_h = n * rowh + 2 * wpad;
|
||||
int btn_h = S(34), btn_w = S(180);
|
||||
|
||||
M.title = title; M.hint = hint; M.kind = 1;
|
||||
M.items = &button; M.n = 1; M.sel = 0; M.scroll = 0; M.vis = 1;
|
||||
M.ww = S(520);
|
||||
if (M.ww > (int)FB.width - S(24))
|
||||
M.ww = (int)FB.width - S(24);
|
||||
M.wh = TB_H + 2 * SC + BODY_PAD + well_h + S(18) + btn_h + BODY_PAD + HINT_H;
|
||||
M.wx = ((int)FB.width - M.ww) / 2;
|
||||
M.wy = ((int)FB.height - M.wh) / 2;
|
||||
if (M.wy < 0) M.wy = 0;
|
||||
|
||||
cursor_hide();
|
||||
nv_backdrop();
|
||||
window(M.wx, M.wy, M.ww, M.wh, title);
|
||||
|
||||
int wx = M.wx + S(20), ww = M.ww - S(40);
|
||||
int wy = M.wy + SC + TB_H + BODY_PAD;
|
||||
well(wx, wy, ww, well_h, NV_WIN_BODY);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
int ry = y + i * ROW_H;
|
||||
|
||||
if (i == selected) {
|
||||
/* The selected row is the only lit thing on the plate. Filling it
|
||||
* with the accent and putting the label in the ground colour reads
|
||||
* at a glance; an outline does not, at this size. */
|
||||
/* Centred on the glyph box: (ROW_H - GLYPH_H) / 2 above it, the
|
||||
* same below. Off-by-a-few here is what makes a highlight look
|
||||
* like it is sitting on the text rather than around it. */
|
||||
ui_fill_rect(x + PANEL_PAD / 2, ry - ROW_LIFT, w - PANEL_PAD, ROW_H,
|
||||
SCH_ACCENT);
|
||||
ui_text(x + PANEL_PAD, ry, items[i], PANEL_BG, SCH_BG_NONE);
|
||||
int ry = wy + wpad + i * rowh;
|
||||
int base = ry + (rowh - nv_height(F_BODY)) / 2 + F_BODY->ascent;
|
||||
if (i)
|
||||
ui_fill_rect(wx + S(14), ry, ww - S(28), SC, 0x1E1E21u);
|
||||
if (values) {
|
||||
nv_text(F_BODY, wx + S(16), base, labels[i], NV_TXT2,
|
||||
NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
|
||||
int vw = nv_width(F_BODY, values[i]);
|
||||
nv_text(F_BODY, wx + ww - S(16) - vw, base, values[i], NV_TXT,
|
||||
NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
|
||||
} else {
|
||||
ui_text(x + PANEL_PAD, ry, items[i], SCH_FG, SCH_BG_NONE);
|
||||
int lw = nv_width(F_BODY, labels[i]);
|
||||
nv_text(F_BODY, wx + (ww - lw) / 2, base, labels[i], NV_TXT,
|
||||
NV_WELL_TOP, NV_WELL_BOT, wy, well_h);
|
||||
}
|
||||
}
|
||||
|
||||
KEY[0].w = btn_w; KEY[0].h = btn_h;
|
||||
KEY[0].x = M.wx + (M.ww - btn_w) / 2;
|
||||
KEY[0].y = wy + well_h + S(18);
|
||||
M.has_detail = 0;
|
||||
paint_key(0, 1);
|
||||
footer(M.wx, M.wy, M.ww, M.wh, hint);
|
||||
cursor_show();
|
||||
}
|
||||
|
||||
void
|
||||
ui_panel(const char *title, const char *const *labels,
|
||||
const char *const *values, int n, const char *hint)
|
||||
{
|
||||
if (!ACTIVE)
|
||||
return;
|
||||
box(title, labels, values, n, "Back", hint);
|
||||
}
|
||||
|
||||
int x, y, w;
|
||||
panel_frame(title, n, hint, &x, &y, &w);
|
||||
void
|
||||
ui_notice(const char *title, const char *const *lines, int n,
|
||||
const char *button, const char *hint)
|
||||
{
|
||||
box(title, lines, 0, n, button, hint);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
int ry = y + i * ROW_H;
|
||||
ui_text(x + PANEL_PAD, ry, labels[i], SCH_DIM, SCH_BG_NONE);
|
||||
/* The track is a recess, the fill is the accent, lit from above -- the same two
|
||||
* materials as a well and a selected key in the boot manager. The fill is
|
||||
* redrawn from the left edge on every tick rather than extended, because its
|
||||
* rounded end moves; the bar is small enough that this costs nothing. */
|
||||
static void
|
||||
progress_paint(int px)
|
||||
{
|
||||
int r = PROG_H / 2;
|
||||
ui_u32 bg = bg_at(PROG_Y + PROG_H / 2);
|
||||
|
||||
/* Values right-aligned, so the column reads down rather than ragged. */
|
||||
int vw = slen(values[i]) * GLYPH_W;
|
||||
ui_text(x + w - PANEL_PAD - vw, ry, values[i], SCH_FG, SCH_BG_NONE);
|
||||
rrect(PROG_X, PROG_Y, PROG_W, PROG_H, r, 0x0A0A0Bu, 0x151517u, bg, bg, 0, 1);
|
||||
if (px >= PROG_H) {
|
||||
ui_u32 top = mix(SCH_ACCENT, 0xFFFFFFu, 28, 100);
|
||||
ui_u32 bot = mix(SCH_ACCENT, 0x000000u, 24, 100);
|
||||
rrect(PROG_X + 1, PROG_Y + 1, px - 2, PROG_H - 2, r - 1, top, bot,
|
||||
0x0A0A0Bu, 0x151517u, PROG_Y, PROG_H);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,10 +960,7 @@ ui_progress_begin(const char *label, ui_u32 total)
|
|||
PROG_LAST_PX = 0;
|
||||
PROG_ACTIVE = 1;
|
||||
|
||||
/* The unfilled track: a recess, not a colour. Drawn once -- repainting it
|
||||
* on every tick is what makes a progress bar flicker. */
|
||||
ui_fill_rect(PROG_X, PROG_Y, PROG_W, PROG_H, 0x1B1F28u);
|
||||
ui_fill_rect(PROG_X, PROG_Y, PROG_W, 1, 0x101319u);
|
||||
progress_paint(0);
|
||||
if (label)
|
||||
ui_progress_label(label);
|
||||
}
|
||||
|
|
@ -436,11 +971,16 @@ ui_progress_label(const char *label)
|
|||
if (!ACTIVE || !label)
|
||||
return;
|
||||
|
||||
/* Clear the line first. A shorter label after a longer one would otherwise
|
||||
* leave the tail of the old one behind. */
|
||||
int y = PROG_Y + PROG_H + SUB_GAP;
|
||||
ui_fill_rect(0, y, (int)FB.width, GLYPH_H, SCH_BG);
|
||||
ui_text_centered(y, label, SCH_DIM);
|
||||
/* Repaint the backdrop under the line first. A shorter label after a longer
|
||||
* one would otherwise leave the tail of the old one behind. */
|
||||
int h = nv_height(F_BODY) + 4 * SC;
|
||||
int y = PROG_Y + PROG_H + 18 * SC;
|
||||
for (int r = 0; r < h; r++)
|
||||
ui_fill_rect(0, y + r, (int)FB.width, 1, bg_at(y + r));
|
||||
|
||||
int tw = nv_width(F_BODY, label);
|
||||
nv_text(F_BODY, ((int)FB.width - tw) / 2, y + F_BODY->ascent, label, NV_TXT3,
|
||||
bg_at(y), bg_at(y + h), y, h);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
@ -454,16 +994,8 @@ ui_progress_tick(ui_u32 done)
|
|||
PROG_DONE = done;
|
||||
|
||||
int px = (int)(((ui_u64)PROG_DONE * (ui_u64)PROG_W) / PROG_TOTAL);
|
||||
|
||||
/* Only the newly filled part. Filling from zero each time is O(width) per
|
||||
* tick and visibly slower than the thing it is reporting on. */
|
||||
if (px > PROG_LAST_PX) {
|
||||
/* The fill is metal too, shaded across the bar's height, so it looks
|
||||
* like the same material as the mark rather than a coloured rectangle
|
||||
* that happens to be underneath it. */
|
||||
for (int r = 0; r < PROG_H; r++)
|
||||
ui_fill_rect(PROG_X + PROG_LAST_PX, PROG_Y + r,
|
||||
px - PROG_LAST_PX, 1, metal(r, PROG_H));
|
||||
progress_paint(px);
|
||||
PROG_LAST_PX = px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,21 @@ void ui_splash(const char *subtitle);
|
|||
void ui_menu(const char *title, const char *const *items, int n, int selected,
|
||||
const char *hint);
|
||||
|
||||
/* Repaint only the keys that changed when the highlight moves (a full paint
|
||||
* redraws the screen). Returns without effect on a panel. */
|
||||
void ui_menu_select(int selected);
|
||||
|
||||
/* Which key is under a point, or -1. A panel has one key, index 0. */
|
||||
int ui_menu_hit(int x, int y);
|
||||
|
||||
/* The pointer: drawn on top of everything, with what is under it saved and put
|
||||
* back. Menus hide it while they repaint and show it again afterwards. */
|
||||
void ui_cursor(int x, int y, int show);
|
||||
|
||||
/* A message in the same window as the panels, with one button. */
|
||||
void ui_notice(const char *title, const char *const *lines, int n,
|
||||
const char *button, const char *hint);
|
||||
|
||||
/* A panel of label/value rows -- the system information screen. */
|
||||
void ui_panel(const char *title, const char *const *labels,
|
||||
const char *const *values, int n, const char *hint);
|
||||
|
|
|
|||
|
|
@ -27,3 +27,11 @@ What this will need, in rough order:
|
|||
|
||||
The image goes at `EFI/BOOT/BOOTX64.EFI` on a FAT ESP, which also means the
|
||||
ISO carries a single El Torito EFI entry.
|
||||
|
||||
## The boot manager
|
||||
|
||||
`F8` (or `o` on a serial line) opens a Niveus-styled boot manager, drawn by
|
||||
`../common/ui.c` in Instrument Sans (`include/gfx/nvfont.h`, regenerate with
|
||||
`make nvfont`). It takes keyboard and pointer, lists other systems it finds on
|
||||
the disks and can chain-load them, and has working Firmware Setup, Restart and
|
||||
Shut Down. See [docs/architecture/boot.md](../../docs/architecture/boot.md#the-boot-manager).
|
||||
|
|
|
|||
|
|
@ -153,17 +153,22 @@ typedef struct {
|
|||
void *LocateDevicePath;
|
||||
void *InstallConfigurationTable;
|
||||
|
||||
void *LoadImage;
|
||||
void *StartImage;
|
||||
EFI_STATUS (*LoadImage)(BOOLEAN BootPolicy, EFI_HANDLE ParentImage,
|
||||
void *DevicePath, void *SourceBuffer,
|
||||
UINTN SourceSize, EFI_HANDLE *ImageHandle);
|
||||
EFI_STATUS (*StartImage)(EFI_HANDLE ImageHandle, UINTN *ExitDataSize,
|
||||
CHAR16 **ExitData);
|
||||
void *Exit;
|
||||
void *UnloadImage;
|
||||
EFI_STATUS (*UnloadImage)(EFI_HANDLE ImageHandle);
|
||||
EFI_STATUS (*ExitBootServices)(EFI_HANDLE ImageHandle, UINTN MapKey);
|
||||
|
||||
void *GetNextMonotonicCount;
|
||||
EFI_STATUS (*Stall)(UINTN Microseconds);
|
||||
void *SetWatchdogTimer;
|
||||
|
||||
void *ConnectController;
|
||||
/* Recursive = TRUE asks for every driver that will bind, down the tree. */
|
||||
EFI_STATUS (*ConnectController)(EFI_HANDLE Controller, EFI_HANDLE *DriverImage,
|
||||
void *RemainingDevicePath, BOOLEAN Recursive);
|
||||
void *DisconnectController;
|
||||
|
||||
void *OpenProtocol;
|
||||
|
|
@ -181,6 +186,43 @@ typedef struct {
|
|||
void **Interface);
|
||||
} EFI_BOOT_SERVICES;
|
||||
|
||||
// --- runtime services -----------------------------------------------------
|
||||
//
|
||||
// Only the two things the boot manager needs: variables, to ask the firmware to
|
||||
// show its own setup on the next start, and ResetSystem, to restart or power
|
||||
// off. The slots before them are spelled out as padding so the offsets hold.
|
||||
|
||||
#define EfiResetCold 0
|
||||
#define EfiResetShutdown 2
|
||||
|
||||
#define EFI_VARIABLE_NON_VOLATILE 0x1
|
||||
#define EFI_VARIABLE_BOOTSERVICE_ACCESS 0x2
|
||||
#define EFI_VARIABLE_RUNTIME_ACCESS 0x4
|
||||
|
||||
#define EFI_GLOBAL_VARIABLE_GUID \
|
||||
{ 0x8BE4DF61, 0x93CA, 0x11D2, { 0xAA, 0x0D, 0x00, 0xE0, 0x98, 0x03, 0x2B, 0x8C } }
|
||||
|
||||
/* OsIndications bit 0: "boot into the firmware's own user interface". */
|
||||
#define EFI_OS_INDICATIONS_BOOT_TO_FW_UI 0x1ULL
|
||||
|
||||
typedef struct {
|
||||
char Hdr[24];
|
||||
void *GetTime;
|
||||
void *SetTime;
|
||||
void *GetWakeupTime;
|
||||
void *SetWakeupTime;
|
||||
void *SetVirtualAddressMap;
|
||||
void *ConvertPointer;
|
||||
EFI_STATUS (*GetVariable)(CHAR16 *Name, EFI_GUID *Vendor, UINT32 *Attributes,
|
||||
UINTN *DataSize, void *Data);
|
||||
void *GetNextVariableName;
|
||||
EFI_STATUS (*SetVariable)(CHAR16 *Name, EFI_GUID *Vendor, UINT32 Attributes,
|
||||
UINTN DataSize, void *Data);
|
||||
void *GetNextHighMonotonicCount;
|
||||
void (*ResetSystem)(UINTN ResetType, EFI_STATUS Status, UINTN DataSize,
|
||||
void *ResetData);
|
||||
} EFI_RUNTIME_SERVICES;
|
||||
|
||||
typedef struct {
|
||||
char Hdr[24];
|
||||
CHAR16 *FirmwareVendor;
|
||||
|
|
@ -191,7 +233,7 @@ typedef struct {
|
|||
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
|
||||
EFI_HANDLE StandardErrorHandle;
|
||||
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
|
||||
void *RuntimeServices;
|
||||
EFI_RUNTIME_SERVICES *RuntimeServices;
|
||||
EFI_BOOT_SERVICES *BootServices;
|
||||
UINTN NumberOfTableEntries;
|
||||
void *ConfigurationTable;
|
||||
|
|
@ -348,4 +390,42 @@ typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL {
|
|||
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
|
||||
} EFI_GRAPHICS_OUTPUT_PROTOCOL;
|
||||
|
||||
// --- pointers -------------------------------------------------------------
|
||||
//
|
||||
// Two protocols, because firmware offers whichever the device is: a mouse or
|
||||
// touchpad reports movement (SIMPLE_POINTER), a tablet -- QEMU's, and most
|
||||
// touchscreens -- reports where it is (ABSOLUTE_POINTER). Both are polled.
|
||||
|
||||
#define EFI_SIMPLE_POINTER_PROTOCOL_GUID \
|
||||
{ 0x31878C87, 0x0B75, 0x11D5, { 0x9A, 0x4F, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0x4D } }
|
||||
#define EFI_ABSOLUTE_POINTER_PROTOCOL_GUID \
|
||||
{ 0x8D59D32B, 0xC655, 0x4AE9, { 0x9B, 0x15, 0xF2, 0x59, 0x04, 0x99, 0x2A, 0x43 } }
|
||||
|
||||
#define EFI_NOT_READY ((EFI_STATUS)(0x8000000000000000ULL | 6))
|
||||
|
||||
typedef struct { int RelativeMovementX, RelativeMovementY, RelativeMovementZ;
|
||||
BOOLEAN LeftButton, RightButton; } EFI_SIMPLE_POINTER_STATE;
|
||||
typedef struct { UINT64 ResolutionX, ResolutionY, ResolutionZ;
|
||||
BOOLEAN LeftButton, RightButton; } EFI_SIMPLE_POINTER_MODE;
|
||||
typedef struct EFI_SIMPLE_POINTER_PROTOCOL {
|
||||
EFI_STATUS (*Reset)(struct EFI_SIMPLE_POINTER_PROTOCOL *This, BOOLEAN Extended);
|
||||
EFI_STATUS (*GetState)(struct EFI_SIMPLE_POINTER_PROTOCOL *This,
|
||||
EFI_SIMPLE_POINTER_STATE *State);
|
||||
void *WaitForInput;
|
||||
EFI_SIMPLE_POINTER_MODE *Mode;
|
||||
} EFI_SIMPLE_POINTER_PROTOCOL;
|
||||
|
||||
typedef struct { UINT64 CurrentX, CurrentY, CurrentZ; UINT32 ActiveButtons; }
|
||||
EFI_ABSOLUTE_POINTER_STATE;
|
||||
typedef struct { UINT64 AbsoluteMinX, AbsoluteMinY, AbsoluteMinZ,
|
||||
AbsoluteMaxX, AbsoluteMaxY, AbsoluteMaxZ;
|
||||
UINT32 Attributes; } EFI_ABSOLUTE_POINTER_MODE;
|
||||
typedef struct EFI_ABSOLUTE_POINTER_PROTOCOL {
|
||||
EFI_STATUS (*Reset)(struct EFI_ABSOLUTE_POINTER_PROTOCOL *This, BOOLEAN Extended);
|
||||
EFI_STATUS (*GetState)(struct EFI_ABSOLUTE_POINTER_PROTOCOL *This,
|
||||
EFI_ABSOLUTE_POINTER_STATE *State);
|
||||
void *WaitForInput;
|
||||
EFI_ABSOLUTE_POINTER_MODE *Mode;
|
||||
} EFI_ABSOLUTE_POINTER_PROTOCOL;
|
||||
|
||||
#endif /* SCHOS_EFI_H */
|
||||
|
|
|
|||
703
boot/uefi/main.c
703
boot/uefi/main.c
|
|
@ -81,6 +81,8 @@ struct elf64_phdr {
|
|||
#define PT_LOAD 1
|
||||
|
||||
static EFI_SYSTEM_TABLE *ST;
|
||||
static EFI_HANDLE SELF; /* this image */
|
||||
static EFI_HANDLE BOOT_VOL; /* the volume it was loaded from */
|
||||
static EFI_BOOT_SERVICES *BS;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -183,7 +185,20 @@ serial_item(const char *const *items, int i, int sel, int numbered)
|
|||
} else if (numbered) {
|
||||
serial_puts(" "); /* keep the column */
|
||||
}
|
||||
serial_puts(items[i]);
|
||||
/* The label, and its detail in brackets. Items carry both, with a newline
|
||||
* between and a leading \001 where a group begins: the screen uses those
|
||||
* for layout and a serial line has no use for either. */
|
||||
const char *t = items[i];
|
||||
if (t[0] == 1)
|
||||
t++;
|
||||
for (; *t && *t != '\n'; t++)
|
||||
serial_putc(*t);
|
||||
if (*t == '\n') {
|
||||
serial_puts(" (");
|
||||
for (t++; *t; t++)
|
||||
serial_putc(*t);
|
||||
serial_putc(')');
|
||||
}
|
||||
serial_puts("\n");
|
||||
}
|
||||
|
||||
|
|
@ -360,6 +375,7 @@ open_boot_volume(EFI_HANDLE image)
|
|||
// is not a thing to do by accident.
|
||||
st = BS->HandleProtocol(image, &li_guid, (void **)&li);
|
||||
if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st);
|
||||
BOOT_VOL = li->DeviceHandle;
|
||||
self_base = (UINT64)(UINTN)li->ImageBase;
|
||||
self_size = li->ImageSize;
|
||||
|
||||
|
|
@ -904,6 +920,237 @@ u64_to_dec(UINT64 v, char *out, int cap)
|
|||
out[i] = 0;
|
||||
}
|
||||
|
||||
/* --- pointers, and one place to wait for input ------------------------------
|
||||
*
|
||||
* The boot manager is operated by keyboard, mouse, touchpad or touchscreen, and
|
||||
* every screen waits through here so that all of them behave the same way.
|
||||
*
|
||||
* Both pointer protocols are polled rather than waited on: the loop is already
|
||||
* polling the keyboard, and an event per device would mean a wait-for-any call
|
||||
* that most firmware implements badly. A machine with no pointer simply never
|
||||
* shows a cursor.
|
||||
*/
|
||||
#define MAX_PTR 4
|
||||
static EFI_SIMPLE_POINTER_PROTOCOL *PTR_REL[MAX_PTR];
|
||||
static EFI_ABSOLUTE_POINTER_PROTOCOL *PTR_ABS[MAX_PTR];
|
||||
static int PTR_NREL, PTR_NABS;
|
||||
static int PTR_X, PTR_Y, PTR_DOWN;
|
||||
static int PTR_RESCANS, PTR_TICKS;
|
||||
static int PTR_ACTIVE; /* a real event has arrived; until then there is no cursor */
|
||||
static void u64_to_dec(UINT64 v, char *out, int cap);
|
||||
|
||||
#define EV_NONE 0
|
||||
#define EV_KEY 1
|
||||
#define EV_HOVER 2 /* the pointer moved onto a key: *item */
|
||||
#define EV_CLICK 3 /* a key was clicked: *item */
|
||||
|
||||
#define PE_MOVED 1
|
||||
#define PE_DOWN 2
|
||||
#define PE_UP 4 /* wheel */
|
||||
#define PE_DOWNW 8
|
||||
|
||||
/* Bind every driver to every controller.
|
||||
*
|
||||
* The firmware connects only what it needs to boot: the devices that carry a
|
||||
* console, and the disks. A USB mouse or tablet is neither, so on most
|
||||
* machines its driver has not been started and no pointer protocol exists yet.
|
||||
* Doing it here, when the menu opens, costs a second or so on a real machine
|
||||
* and nothing on a boot that never shows a menu. */
|
||||
static void
|
||||
connect_all(void)
|
||||
{
|
||||
EFI_HANDLE *h = 0;
|
||||
UINTN n = 0;
|
||||
|
||||
if (EFI_ERROR(BS->LocateHandleBuffer(0 /* AllHandles */, 0, 0, &n, &h)) || !h)
|
||||
return;
|
||||
for (UINTN i = 0; i < n; i++)
|
||||
BS->ConnectController(h[i], 0, 0, 1);
|
||||
BS->FreePool(h);
|
||||
}
|
||||
|
||||
static void
|
||||
pointers_find(void)
|
||||
{
|
||||
EFI_GUID g1 = EFI_SIMPLE_POINTER_PROTOCOL_GUID;
|
||||
EFI_GUID g2 = EFI_ABSOLUTE_POINTER_PROTOCOL_GUID;
|
||||
EFI_HANDLE *h = 0;
|
||||
UINTN n = 0;
|
||||
|
||||
if (!EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &g1, 0, &n, &h)) && h) {
|
||||
for (UINTN i = 0; i < n && PTR_NREL < MAX_PTR; i++) {
|
||||
void *p = 0;
|
||||
if (!EFI_ERROR(BS->HandleProtocol(h[i], &g1, &p)) && p) {
|
||||
EFI_SIMPLE_POINTER_PROTOCOL *sp = p;
|
||||
int seen = 0;
|
||||
for (int k = 0; k < PTR_NREL; k++)
|
||||
if (PTR_REL[k] == sp) seen = 1;
|
||||
if (!seen) {
|
||||
sp->Reset(sp, 0);
|
||||
PTR_REL[PTR_NREL++] = sp;
|
||||
}
|
||||
}
|
||||
}
|
||||
BS->FreePool(h);
|
||||
}
|
||||
h = 0; n = 0;
|
||||
if (!EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &g2, 0, &n, &h)) && h) {
|
||||
for (UINTN i = 0; i < n && PTR_NABS < MAX_PTR; i++) {
|
||||
void *p = 0;
|
||||
if (!EFI_ERROR(BS->HandleProtocol(h[i], &g2, &p)) && p) {
|
||||
EFI_ABSOLUTE_POINTER_PROTOCOL *ap = p;
|
||||
int seen = 0;
|
||||
for (int k = 0; k < PTR_NABS; k++)
|
||||
if (PTR_ABS[k] == ap) seen = 1;
|
||||
if (!seen) {
|
||||
ap->Reset(ap, 0);
|
||||
PTR_ABS[PTR_NABS++] = ap;
|
||||
}
|
||||
}
|
||||
}
|
||||
BS->FreePool(h);
|
||||
}
|
||||
if (PTR_X == 0 && PTR_Y == 0)
|
||||
PTR_X = PTR_Y = -1; /* centred on first use, when the size is known */
|
||||
}
|
||||
|
||||
static int
|
||||
pointer_poll(void)
|
||||
{
|
||||
int ev = 0, w = (int)ui_width(), h = (int)ui_height();
|
||||
int down = PTR_DOWN;
|
||||
|
||||
for (int i = 0; i < PTR_NREL; i++) {
|
||||
EFI_SIMPLE_POINTER_STATE st;
|
||||
if (EFI_ERROR(PTR_REL[i]->GetState(PTR_REL[i], &st)))
|
||||
continue;
|
||||
int dx = st.RelativeMovementX, dy = st.RelativeMovementY;
|
||||
/* A little acceleration: the counts are the device's, not pixels, and
|
||||
* a fast flick has to be able to cross a 4K panel. */
|
||||
if (dx > 6 || dx < -6) dx *= 2;
|
||||
if (dy > 6 || dy < -6) dy *= 2;
|
||||
if (dx || dy) {
|
||||
PTR_X += dx; PTR_Y += dy;
|
||||
ev |= PE_MOVED;
|
||||
}
|
||||
if (st.RelativeMovementZ)
|
||||
ev |= st.RelativeMovementZ < 0 ? PE_UP : PE_DOWNW;
|
||||
down = st.LeftButton;
|
||||
}
|
||||
for (int i = 0; i < PTR_NABS; i++) {
|
||||
EFI_ABSOLUTE_POINTER_STATE st;
|
||||
EFI_ABSOLUTE_POINTER_MODE *m = PTR_ABS[i]->Mode;
|
||||
if (EFI_ERROR(PTR_ABS[i]->GetState(PTR_ABS[i], &st)) || !m)
|
||||
continue;
|
||||
UINT64 rx = m->AbsoluteMaxX - m->AbsoluteMinX;
|
||||
UINT64 ry = m->AbsoluteMaxY - m->AbsoluteMinY;
|
||||
if (rx && ry) {
|
||||
PTR_X = (int)(((st.CurrentX - m->AbsoluteMinX) * (UINT64)(w - 1)) / rx);
|
||||
PTR_Y = (int)(((st.CurrentY - m->AbsoluteMinY) * (UINT64)(h - 1)) / ry);
|
||||
ev |= PE_MOVED;
|
||||
}
|
||||
down = (int)(st.ActiveButtons & 1);
|
||||
}
|
||||
|
||||
if (PTR_X < 0) PTR_X = 0;
|
||||
if (PTR_Y < 0) PTR_Y = 0;
|
||||
if (PTR_X >= w) PTR_X = w - 1;
|
||||
if (PTR_Y >= h) PTR_Y = h - 1;
|
||||
if (down && !PTR_DOWN)
|
||||
ev |= PE_DOWN;
|
||||
PTR_DOWN = down;
|
||||
return ev;
|
||||
}
|
||||
|
||||
/* Wait for a key, a click on a key, or the pointer moving onto one.
|
||||
*
|
||||
* The cursor is shown whenever a pointer device is present: hiding it until
|
||||
* something moved left a machine whose pointer sends no events to the
|
||||
* loader with nothing on screen to say a pointer was even looked for.
|
||||
*
|
||||
* `budget` is a countdown in milliseconds that survives across calls: a
|
||||
* caller that returns here after a hover continues the same ten seconds
|
||||
* instead of being given a fresh ten. NULL waits for ever.
|
||||
*/
|
||||
static int
|
||||
wait_event(EFI_INPUT_KEY *key, int *item, UINTN *budget)
|
||||
{
|
||||
int have = PTR_NREL + PTR_NABS;
|
||||
int last = -2;
|
||||
|
||||
if (PTR_X < 0) {
|
||||
PTR_X = (int)ui_width() / 2;
|
||||
PTR_Y = (int)ui_height() / 2;
|
||||
}
|
||||
if (have)
|
||||
PTR_ACTIVE = 1;
|
||||
ui_cursor(PTR_X, PTR_Y, PTR_ACTIVE);
|
||||
for (;;) {
|
||||
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, key)))
|
||||
return EV_KEY;
|
||||
/* USB devices appear some time after their controller is connected,
|
||||
* and the menu is already on screen by then: look again, twice a
|
||||
* second for the first ten. */
|
||||
if (PTR_RESCANS < 20 && ++PTR_TICKS >= 250) {
|
||||
PTR_TICKS = 0;
|
||||
PTR_RESCANS++;
|
||||
pointers_find();
|
||||
have = PTR_NREL + PTR_NABS;
|
||||
if (have && !PTR_ACTIVE) {
|
||||
PTR_ACTIVE = 1;
|
||||
ui_cursor(PTR_X, PTR_Y, 1);
|
||||
}
|
||||
}
|
||||
if (have) {
|
||||
int e = pointer_poll();
|
||||
if ((e & (PE_MOVED | PE_DOWN)) && !PTR_ACTIVE)
|
||||
PTR_ACTIVE = 1;
|
||||
if (e & PE_MOVED || ((e & PE_DOWN) && PTR_ACTIVE))
|
||||
ui_cursor(PTR_X, PTR_Y, PTR_ACTIVE);
|
||||
if (e & (PE_UP | PE_DOWNW)) {
|
||||
key->UnicodeChar = 0;
|
||||
key->ScanCode = (e & PE_UP) ? 0x01 : 0x02;
|
||||
return EV_KEY;
|
||||
}
|
||||
int hit = ui_menu_hit(PTR_X, PTR_Y);
|
||||
if ((e & PE_DOWN) && hit >= 0) {
|
||||
*item = hit;
|
||||
return EV_CLICK;
|
||||
}
|
||||
if ((e & PE_MOVED) && hit >= 0 && hit != last) {
|
||||
last = hit;
|
||||
*item = hit;
|
||||
return EV_HOVER;
|
||||
}
|
||||
if (hit < 0)
|
||||
last = -2;
|
||||
}
|
||||
if (budget) {
|
||||
if (*budget < 2)
|
||||
return EV_NONE;
|
||||
*budget -= 2;
|
||||
}
|
||||
BS->Stall(2000);
|
||||
}
|
||||
}
|
||||
|
||||
/* A message and a button. Any key or a click dismisses it. */
|
||||
static void
|
||||
notice(const char *title, const char *const *lines, int n, const char *button,
|
||||
const char *hint)
|
||||
{
|
||||
EFI_INPUT_KEY key;
|
||||
int item = 0;
|
||||
|
||||
ui_notice(title, lines, n, button, hint);
|
||||
mirrors_show();
|
||||
for (;;) {
|
||||
int e = wait_event(&key, &item, 0);
|
||||
if (e == EV_KEY || e == EV_CLICK)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap)
|
||||
{
|
||||
|
|
@ -916,6 +1163,8 @@ res_row(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, char *out, int cap)
|
|||
for (char *p = wa; *p && i < cap - 1; p++) out[i++] = *p;
|
||||
if (i < cap - 1) out[i++] = 'x';
|
||||
for (char *p = ha; *p && i < cap - 1; p++) out[i++] = *p;
|
||||
for (const char *p = "\nChange the screen size"; *p && i < cap - 1; p++)
|
||||
out[i++] = *p;
|
||||
out[i] = 0;
|
||||
}
|
||||
|
||||
|
|
@ -1038,16 +1287,16 @@ res_read_number(const char *prompt)
|
|||
shown[i] = 0;
|
||||
|
||||
const char *rows[1] = { shown };
|
||||
ui_menu("Custom resolution", rows, 1, 0, "digits, Enter to accept");
|
||||
ui_notice("Custom resolution", rows, 1, "Accept", "digits, Enter to accept");
|
||||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
for (;;) {
|
||||
if (ST->ConIn &&
|
||||
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||
break;
|
||||
BS->Stall(10000);
|
||||
}
|
||||
int item = 0, e;
|
||||
do
|
||||
e = wait_event(&key, &item, 0);
|
||||
while (e == EV_HOVER);
|
||||
if (e == EV_CLICK)
|
||||
return v;
|
||||
if (key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n')
|
||||
return v;
|
||||
if (key.UnicodeChar >= L'0' && key.UnicodeChar <= L'9')
|
||||
|
|
@ -1069,6 +1318,47 @@ res_read_number(const char *prompt)
|
|||
* cannot prove a mode is right, but they can prove one is wrong, and a wrong
|
||||
* one is refused here, in the menu, where there is still a screen to say so on.
|
||||
*/
|
||||
/* "Keep this resolution?", drawn in the new mode, and Enter within ten
|
||||
* seconds to keep it -- the question every desktop asks after a mode change,
|
||||
* for the same reason. The numbers the firmware reports for a mode are no
|
||||
* proof the screen shows it: AMD's GOP on a Ryzen APU and on the ASUS TUF
|
||||
* "sets" the smaller modes without leaving the panel's native timing, and
|
||||
* reports a row length that is the mode's rather than the panel's. Every
|
||||
* check above passes and the picture comes out as three or four squashed
|
||||
* copies side by side. Nobody can read the question on a screen like that,
|
||||
* so nobody presses Enter, and the old mode comes back on its own. */
|
||||
static int
|
||||
mode_confirmed(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||
{
|
||||
struct ui_fb uifb;
|
||||
EFI_INPUT_KEY key;
|
||||
const char *msg[2] = { "Keep this resolution?",
|
||||
"If you can read this, press Enter" };
|
||||
|
||||
uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase;
|
||||
uifb.width = gop->Mode->Info->HorizontalResolution;
|
||||
uifb.height = gop->Mode->Info->VerticalResolution;
|
||||
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
|
||||
ui_init(&uifb);
|
||||
ui_notice("Resolution", msg, 2, "Keep", "going back in 10 seconds");
|
||||
|
||||
if (ST->ConIn)
|
||||
while (!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||
; /* the key that chose the mode is not an answer */
|
||||
UINTN left = 10000; /* one countdown, however many hovers */
|
||||
for (;;) {
|
||||
int item = 0;
|
||||
int e = wait_event(&key, &item, &left);
|
||||
if (e == EV_NONE)
|
||||
break;
|
||||
if (e == EV_CLICK)
|
||||
return 1;
|
||||
if (e == EV_KEY)
|
||||
return key.UnicodeChar == L'\r' || key.UnicodeChar == L'\n';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode)
|
||||
{
|
||||
|
|
@ -1087,10 +1377,11 @@ mode_set_checked(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, UINT32 mode)
|
|||
got->VerticalResolution == want->VerticalResolution &&
|
||||
got->PixelsPerScanLine >= got->HorizontalResolution &&
|
||||
gop->Mode->FrameBufferBase &&
|
||||
gop->Mode->FrameBufferSize >= need)
|
||||
gop->Mode->FrameBufferSize >= need &&
|
||||
mode_confirmed(gop))
|
||||
return 1;
|
||||
|
||||
describe_gop(gop, L"schos: mode does not add up, going back:");
|
||||
describe_gop(gop, L"schos: mode not kept, going back:");
|
||||
gop->SetMode(gop, was);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1100,21 +1391,14 @@ static void
|
|||
res_refused(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||
{
|
||||
struct ui_fb uifb;
|
||||
EFI_INPUT_KEY key;
|
||||
const char *msg[1] = { "The display refused that size -- keeping this one" };
|
||||
const char *msg[1] = { "Not kept -- back to the size before" };
|
||||
|
||||
uifb.base = (ui_u8 *)(UINTN)gop->Mode->FrameBufferBase;
|
||||
uifb.width = gop->Mode->Info->HorizontalResolution;
|
||||
uifb.height = gop->Mode->Info->VerticalResolution;
|
||||
uifb.stride = gop->Mode->Info->PixelsPerScanLine * 4;
|
||||
ui_init(&uifb);
|
||||
ui_menu("Resolution", msg, 1, 0, "any key to go back");
|
||||
mirrors_show();
|
||||
for (;;) {
|
||||
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||
break;
|
||||
BS->Stall(10000);
|
||||
}
|
||||
notice("Resolution", msg, 1, "OK", "any key to go back");
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -1122,6 +1406,8 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
{
|
||||
int sel = 0;
|
||||
int echoed = -1;
|
||||
static const char hint[] =
|
||||
"W/S or arrows to choose, Enter to apply, Esc or Q to go back";
|
||||
|
||||
for (;;) {
|
||||
struct ui_fb uifb;
|
||||
|
|
@ -1138,41 +1424,53 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
items[i] = RES_TEXT[i];
|
||||
|
||||
if (sel > RES_N) sel = RES_N;
|
||||
ui_menu("Resolution", items, RES_N + 1, sel,
|
||||
"W/S to choose, Enter to apply, Esc or Q to go back");
|
||||
ui_menu("Resolution", items, RES_N + 1, sel, hint);
|
||||
|
||||
if (echoed < 0) {
|
||||
serial_menu("Resolution", items, RES_N + 1, sel,
|
||||
"W/S to choose, Enter to apply, Esc or Q to go back", 0);
|
||||
} else if (echoed != sel) {
|
||||
serial_item(items, sel, sel, 0);
|
||||
}
|
||||
if (echoed < 0)
|
||||
serial_menu("Resolution", items, RES_N + 1, sel, hint, 0);
|
||||
echoed = sel;
|
||||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
for (;;) {
|
||||
if (ST->ConIn &&
|
||||
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||
break;
|
||||
BS->Stall(10000);
|
||||
}
|
||||
EFI_INPUT_KEY key;
|
||||
int item = -1, act = 0;
|
||||
int e = wait_event(&key, &item, 0);
|
||||
|
||||
if (e == EV_HOVER || e == EV_CLICK) {
|
||||
if (item != sel) {
|
||||
sel = item;
|
||||
ui_menu_select(sel);
|
||||
serial_item(items, sel, sel, 0);
|
||||
mirrors_show();
|
||||
}
|
||||
act = e == EV_CLICK;
|
||||
} else {
|
||||
CHAR16 c = key.UnicodeChar;
|
||||
/* Q as well as Esc, for the same reason f8_window() accepts
|
||||
* 'o' as well as F8: on a serial line a lone ESC byte is
|
||||
* indistinguishable from the start of an escape sequence, so
|
||||
* the terminal driver holds it back waiting for the rest and
|
||||
* it never arrives as a key. Esc is the obvious thing to
|
||||
* press on a screen and unreachable on a cable. */
|
||||
if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q')
|
||||
return;
|
||||
int to = sel;
|
||||
if (key.ScanCode == 0x01 || c == L'w' || c == L'W')
|
||||
to = (sel + RES_N) % (RES_N + 1);
|
||||
else if (key.ScanCode == 0x02 || c == L's' || c == L'S')
|
||||
to = (sel + 1) % (RES_N + 1);
|
||||
else if (c == L'\r' || c == L'\n')
|
||||
act = 1;
|
||||
if (to != sel) {
|
||||
sel = to;
|
||||
ui_menu_select(sel);
|
||||
serial_item(items, sel, sel, 0);
|
||||
mirrors_show();
|
||||
}
|
||||
}
|
||||
if (!act)
|
||||
continue;
|
||||
|
||||
CHAR16 c = key.UnicodeChar;
|
||||
/* Q as well as Esc, for the same reason f8_window() accepts 'o' as
|
||||
* well as F8: on a serial line a lone ESC byte is indistinguishable
|
||||
* from the start of an escape sequence, so the terminal driver holds
|
||||
* it back waiting for the rest and it never arrives as a key. Esc is
|
||||
* the obvious thing to press on a screen and unreachable on a cable. */
|
||||
if (key.ScanCode == 0x17 || c == 27 || c == L'q' || c == L'Q')
|
||||
return;
|
||||
if (key.ScanCode == 0x01 || c == L'w' || c == L'W') {
|
||||
sel = (sel + RES_N) % (RES_N + 1); continue;
|
||||
}
|
||||
if (key.ScanCode == 0x02 || c == L's' || c == L'S') {
|
||||
sel = (sel + 1) % (RES_N + 1); continue;
|
||||
}
|
||||
if (c == L'\r' || c == L'\n') {
|
||||
if (sel < RES_N) {
|
||||
if (!mode_set_checked(gop, RES_MODE[sel])) {
|
||||
res_refused(gop);
|
||||
|
|
@ -1206,14 +1504,7 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
/* Nothing matched. Saying so beats silently keeping the old mode
|
||||
* and letting the user wonder whether the key registered. */
|
||||
const char *msg[1] = { "No such mode -- keeping this one" };
|
||||
ui_menu("Resolution", msg, 1, 0, "any key to go back");
|
||||
mirrors_show();
|
||||
for (;;) {
|
||||
if (ST->ConIn &&
|
||||
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||
break;
|
||||
BS->Stall(10000);
|
||||
}
|
||||
notice("Resolution", msg, 1, "OK", "any key to go back");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1469,19 +1760,205 @@ sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
int item = 0;
|
||||
for (;;) {
|
||||
if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key)))
|
||||
int e = wait_event(&key, &item, 0);
|
||||
if (e == EV_KEY || e == EV_CLICK)
|
||||
return;
|
||||
BS->Stall(10000);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- other systems ------------------------------------------------------------
|
||||
*
|
||||
* A boot manager that can only boot itself is a menu with one item. Other
|
||||
* operating systems on this machine leave their loader at a known path on an
|
||||
* EFI system partition, and chain-loading one is the firmware's own
|
||||
* LoadImage and StartImage: no ExitBootServices of ours is involved, and if the
|
||||
* other loader gives control back the menu is still here.
|
||||
*
|
||||
* Only paths that are well known are probed. Walking every directory of every
|
||||
* volume would find more and would also find things that are not an operating
|
||||
* system, and a boot menu that lists a firmware updater is not an improvement.
|
||||
*/
|
||||
#define MAX_OTHERS 6
|
||||
|
||||
static const struct { const CHAR16 *path; const char *name; } KNOWN_LOADERS[] = {
|
||||
{ L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi", "Windows Boot Manager" },
|
||||
{ L"\\EFI\\ubuntu\\shimx64.efi", "Ubuntu" },
|
||||
{ L"\\EFI\\debian\\shimx64.efi", "Debian" },
|
||||
{ L"\\EFI\\fedora\\shimx64.efi", "Fedora" },
|
||||
{ L"\\EFI\\arch\\grubx64.efi", "Arch Linux" },
|
||||
{ L"\\EFI\\opensuse\\grubx64.efi", "openSUSE" },
|
||||
{ L"\\EFI\\systemd\\systemd-bootx64.efi", "systemd-boot" },
|
||||
{ L"\\EFI\\refind\\refind_x64.efi", "rEFInd" },
|
||||
{ L"\\EFI\\BOOT\\BOOTX64.EFI", "Another EFI loader" },
|
||||
};
|
||||
|
||||
static struct {
|
||||
EFI_HANDLE vol;
|
||||
const CHAR16 *path;
|
||||
char text[96]; /* "name\ndetail", as the menu wants it */
|
||||
} OTHERS[MAX_OTHERS];
|
||||
static int OTHER_N;
|
||||
|
||||
static void
|
||||
others_find(void)
|
||||
{
|
||||
EFI_GUID fs_guid = EFI_SIMPLE_FILE_SYSTEM_PROTOCOL_GUID;
|
||||
EFI_HANDLE *h = 0;
|
||||
UINTN n = 0;
|
||||
|
||||
OTHER_N = 0;
|
||||
if (EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &fs_guid, 0, &n, &h)) || !h)
|
||||
return;
|
||||
|
||||
for (UINTN i = 0; i < n && OTHER_N < MAX_OTHERS; i++) {
|
||||
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs = 0;
|
||||
EFI_FILE_PROTOCOL *root = 0;
|
||||
if (EFI_ERROR(BS->HandleProtocol(h[i], &fs_guid, (void **)&fs)) || !fs)
|
||||
continue;
|
||||
if (EFI_ERROR(fs->OpenVolume(fs, &root)) || !root)
|
||||
continue;
|
||||
|
||||
for (UINTN k = 0; k < sizeof KNOWN_LOADERS / sizeof KNOWN_LOADERS[0] &&
|
||||
OTHER_N < MAX_OTHERS; k++) {
|
||||
/* Our own volume is probed too -- SchOS shares the ESP with
|
||||
* Windows on a laptop -- but the fallback path there is this
|
||||
* loader, and offering to start ourselves again is no help. */
|
||||
if (h[i] == BOOT_VOL && k == sizeof KNOWN_LOADERS / sizeof KNOWN_LOADERS[0] - 1)
|
||||
continue;
|
||||
EFI_FILE_PROTOCOL *f = 0;
|
||||
if (EFI_ERROR(root->Open(root, &f, (CHAR16 *)KNOWN_LOADERS[k].path,
|
||||
EFI_FILE_MODE_READ, 0)) || !f)
|
||||
continue;
|
||||
f->Close(f);
|
||||
|
||||
int o = OTHER_N++;
|
||||
OTHERS[o].vol = h[i];
|
||||
OTHERS[o].path = KNOWN_LOADERS[k].path;
|
||||
int c = 0;
|
||||
for (const char *q = KNOWN_LOADERS[k].name; *q && c < 40; q++)
|
||||
OTHERS[o].text[c++] = *q;
|
||||
OTHERS[o].text[c++] = '\n';
|
||||
for (const char *q = "Start it from "; *q; q++)
|
||||
OTHERS[o].text[c++] = *q;
|
||||
char pn[48];
|
||||
narrow(KNOWN_LOADERS[k].path, pn, sizeof pn);
|
||||
for (const char *q = pn; *q && c < (int)sizeof OTHERS[o].text - 1; q++)
|
||||
OTHERS[o].text[c++] = *q;
|
||||
OTHERS[o].text[c] = 0;
|
||||
}
|
||||
root->Close(root);
|
||||
}
|
||||
BS->FreePool(h);
|
||||
}
|
||||
|
||||
/* Start another loader. The device path for LoadImage is the volume's own path
|
||||
* with one more node on the end naming the file: copy the volume's nodes up to
|
||||
* its terminator, append a file-path node, and terminate again. */
|
||||
static void
|
||||
chain_load(int o)
|
||||
{
|
||||
EFI_GUID dp_guid = EFI_DEVICE_PATH_PROTOCOL_GUID;
|
||||
static UINT8 dp[512];
|
||||
UINT8 *vol = 0;
|
||||
UINTN len = 0;
|
||||
|
||||
if (EFI_ERROR(BS->HandleProtocol(OTHERS[o].vol, &dp_guid, (void **)&vol)) || !vol)
|
||||
return;
|
||||
|
||||
for (;;) {
|
||||
UINTN nl = (UINTN)vol[len + 2] | ((UINTN)vol[len + 3] << 8);
|
||||
if (nl < 4 || len + nl > sizeof dp - 64)
|
||||
return;
|
||||
if (vol[len] == 0x7F && vol[len + 1] == 0xFF) /* End of Entire Path */
|
||||
break;
|
||||
len += nl;
|
||||
}
|
||||
copy(dp, vol, len);
|
||||
|
||||
UINTN chars = 0;
|
||||
while (OTHERS[o].path[chars])
|
||||
chars++;
|
||||
UINTN nl = 4 + (chars + 1) * 2;
|
||||
if (len + nl + 4 > sizeof dp)
|
||||
return;
|
||||
dp[len] = 4; dp[len + 1] = 4; /* media, file path */
|
||||
dp[len + 2] = (UINT8)nl; dp[len + 3] = (UINT8)(nl >> 8);
|
||||
copy(dp + len + 4, OTHERS[o].path, (chars + 1) * 2);
|
||||
len += nl;
|
||||
dp[len] = 0x7F; dp[len + 1] = 0xFF; dp[len + 2] = 4; dp[len + 3] = 0;
|
||||
|
||||
EFI_HANDLE img = 0;
|
||||
if (EFI_ERROR(BS->LoadImage(0, SELF, dp, 0, 0, &img)) || !img) {
|
||||
const char *msg[1] = { "The firmware would not load it." };
|
||||
notice("Start another system", msg, 1, "OK", "any key to go back");
|
||||
return;
|
||||
}
|
||||
BS->StartImage(img, 0, 0);
|
||||
BS->UnloadImage(img);
|
||||
}
|
||||
|
||||
/* Ask the firmware to open its own setup on the next start, then restart.
|
||||
*
|
||||
* This used to halt with "reset to reach firmware setup", which is the screen
|
||||
* of a loader that never learned the protocol. The firmware publishes a
|
||||
* variable for exactly this: OsIndicationsSupported says what it can do and
|
||||
* OsIndications is where the request goes. Not every firmware has it, and the
|
||||
* ones that do not are told so rather than restarted for nothing. */
|
||||
static void
|
||||
firmware_setup(void)
|
||||
{
|
||||
EFI_RUNTIME_SERVICES *rt = ST->RuntimeServices;
|
||||
EFI_GUID gv = EFI_GLOBAL_VARIABLE_GUID;
|
||||
UINT64 supported = 0, cur = 0;
|
||||
UINTN sz = sizeof supported;
|
||||
UINT32 attr = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS |
|
||||
EFI_VARIABLE_RUNTIME_ACCESS;
|
||||
|
||||
if (EFI_ERROR(rt->GetVariable(L"OsIndicationsSupported", &gv, 0, &sz,
|
||||
&supported)) ||
|
||||
!(supported & EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) {
|
||||
const char *msg[2] = { "This firmware cannot be asked to open its",
|
||||
"setup. Use its own key when powering on." };
|
||||
notice("Firmware Setup", msg, 2, "OK", "any key to go back");
|
||||
return;
|
||||
}
|
||||
|
||||
UINT32 a = 0;
|
||||
sz = sizeof cur;
|
||||
if (EFI_ERROR(rt->GetVariable(L"OsIndications", &gv, &a, &sz, &cur)))
|
||||
cur = 0;
|
||||
cur |= EFI_OS_INDICATIONS_BOOT_TO_FW_UI;
|
||||
if (EFI_ERROR(rt->SetVariable(L"OsIndications", &gv, attr, sizeof cur, &cur))) {
|
||||
const char *msg[1] = { "The firmware refused the request." };
|
||||
notice("Firmware Setup", msg, 1, "OK", "any key to go back");
|
||||
return;
|
||||
}
|
||||
rt->ResetSystem(EfiResetCold, 0, 0, 0);
|
||||
for (;;) __asm__ volatile("hlt");
|
||||
}
|
||||
|
||||
static void
|
||||
power(UINTN how)
|
||||
{
|
||||
ST->RuntimeServices->ResetSystem(how, 0, 0, 0);
|
||||
for (;;) __asm__ volatile("hlt");
|
||||
}
|
||||
|
||||
enum { A_BOOT, A_VERBOSE, A_OTHER, A_DISPLAY, A_INFO, A_FIRMWARE, A_RESTART,
|
||||
A_SHUTDOWN, A_DEBUG };
|
||||
|
||||
static int
|
||||
boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||
{
|
||||
int selected = 0;
|
||||
int echoed = -1; // the serial copy: header once, then moves
|
||||
char resbuf[48];
|
||||
char resbuf[96];
|
||||
|
||||
connect_all();
|
||||
pointers_find();
|
||||
others_find();
|
||||
|
||||
for (;;) {
|
||||
/* Re-read the framebuffer every pass: choosing a resolution moves it,
|
||||
|
|
@ -1496,78 +1973,97 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
|
||||
res_row(gop, resbuf, sizeof resbuf);
|
||||
|
||||
// Six entries in a debug build, five in a release one.
|
||||
//
|
||||
// The debugger entry is compiled out rather than hidden, because a
|
||||
// menu item that is present and refuses is an invitation to work out
|
||||
// how to make it not refuse. An ISO built for anybody else has no way
|
||||
// to turn a network debugger on, because the code that would set the
|
||||
// flag is not in it.
|
||||
const char *items[6];
|
||||
int nitems = 5;
|
||||
items[0] = "Boot SchOS";
|
||||
items[1] = "Boot SchOS (verbose)";
|
||||
items[2] = resbuf;
|
||||
items[3] = "System Information";
|
||||
items[4] = "Reboot to firmware";
|
||||
//
|
||||
// An item is a label, then a newline and a line of detail; a leading
|
||||
// \001 starts a new group, drawn with a gap above it. `what` says what
|
||||
// each row does, because the number of rows is not fixed: other
|
||||
// systems found on the disks are listed after the two ways of
|
||||
// starting this one.
|
||||
const char *items[8 + MAX_OTHERS];
|
||||
int what[8 + MAX_OTHERS], arg[8 + MAX_OTHERS];
|
||||
int nitems = 0;
|
||||
#define ITEM(text, a, x) do { items[nitems] = (text); what[nitems] = (a); arg[nitems] = (x); nitems++; } while (0)
|
||||
ITEM("Boot SchOS\nStart the system", A_BOOT, 0);
|
||||
ITEM("Boot SchOS (verbose)\nShow kernel messages while starting", A_VERBOSE, 0);
|
||||
for (int o = 0; o < OTHER_N; o++)
|
||||
ITEM(OTHERS[o].text, A_OTHER, o);
|
||||
ITEM(resbuf, A_DISPLAY, 0);
|
||||
ITEM("System Information\nFirmware, processor and memory", A_INFO, 0);
|
||||
ITEM("\001Firmware Setup\nRestart into the UEFI settings", A_FIRMWARE, 0);
|
||||
ITEM("Restart\nReboot the machine", A_RESTART, 0);
|
||||
ITEM("Shut Down\nPower the machine off", A_SHUTDOWN, 0);
|
||||
#ifdef SCHOS_DEBUG
|
||||
items[5] = DEBUG_BOOT ? "Remote debugging: on (schdbgd, port 4444)"
|
||||
: "Remote debugging: off";
|
||||
nitems = 6;
|
||||
ITEM(DEBUG_BOOT ? "Remote debugging: on\nschdbgd, port 4444"
|
||||
: "Remote debugging: off\nschdbgd, port 4444", A_DEBUG, 0);
|
||||
#endif
|
||||
const char *hint = "W/S or arrows to choose, Enter to select - number keys jump";
|
||||
if (selected >= nitems) selected = 0;
|
||||
|
||||
ui_menu("Boot Manager", items, nitems, selected,
|
||||
nitems == 6 ? "W/S or 1-6 to choose, Enter to select"
|
||||
: "W/S or 1-5 to choose, Enter to select");
|
||||
ui_menu("SchOS Boot Manager", items, nitems, selected, hint);
|
||||
|
||||
// The same menu on the serial line: the whole thing once, then one
|
||||
// line per *move*. Reprinting on every keypress would be correct and
|
||||
// unreadable -- a serial terminal has no cursor to paint over, so an
|
||||
// unrecognised key would scroll the selection past again for nothing.
|
||||
if (echoed < 0) {
|
||||
serial_menu("Boot Manager", items, nitems, selected,
|
||||
nitems == 6 ? "W/S or 1-6 to choose, Enter to select"
|
||||
: "W/S or 1-5 to choose, Enter to select", 1);
|
||||
} else if (echoed != selected) {
|
||||
serial_item(items, selected, selected, 1);
|
||||
serial_menu("Boot Manager", items, nitems, selected, hint, 1);
|
||||
}
|
||||
echoed = selected;
|
||||
mirrors_show();
|
||||
|
||||
EFI_INPUT_KEY key;
|
||||
CHAR16 c = 0;
|
||||
UINT16 scan = 0;
|
||||
for (;;) {
|
||||
if (ST->ConIn &&
|
||||
!EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) {
|
||||
c = key.UnicodeChar; scan = key.ScanCode; break;
|
||||
EFI_INPUT_KEY key;
|
||||
int item = -1, act = 0;
|
||||
int e = wait_event(&key, &item, 0);
|
||||
int to = selected;
|
||||
|
||||
if (e == EV_HOVER || e == EV_CLICK) {
|
||||
to = item;
|
||||
act = e == EV_CLICK;
|
||||
} else {
|
||||
CHAR16 c = key.UnicodeChar;
|
||||
if (key.ScanCode == 0x01 || c == L'w' || c == L'W')
|
||||
to = (selected + nitems - 1) % nitems;
|
||||
else if (key.ScanCode == 0x02 || c == L's' || c == L'S')
|
||||
to = (selected + 1) % nitems;
|
||||
else if (c >= L'1' && c <= L'9' && (int)(c - L'1') < nitems)
|
||||
to = (int)(c - L'1');
|
||||
else if (c == L'\r' || c == L'\n')
|
||||
act = 1;
|
||||
}
|
||||
BS->Stall(10000);
|
||||
}
|
||||
|
||||
if (scan == 0x01) { selected = (selected + nitems - 1) % nitems; continue; }
|
||||
if (scan == 0x02) { selected = (selected + 1) % nitems; continue; }
|
||||
if (c == L'w' || c == L'W') { selected = (selected + nitems - 1) % nitems; continue; }
|
||||
if (c == L's' || c == L'S') { selected = (selected + 1) % 5; continue; }
|
||||
if (c >= L'1' && c <= L'0' + nitems) { selected = (int)(c - L'1'); continue; }
|
||||
if (to != selected) {
|
||||
selected = to;
|
||||
ui_menu_select(selected);
|
||||
serial_item(items, selected, selected, 1);
|
||||
echoed = selected;
|
||||
mirrors_show();
|
||||
}
|
||||
if (!act)
|
||||
continue;
|
||||
|
||||
if (c == L'\r' || c == L'\n') {
|
||||
if (selected == 0) return 0;
|
||||
if (selected == 1) return 1;
|
||||
if (selected == 2) { res_choose(gop); continue; }
|
||||
if (selected == 3) { sys_info(gop); continue; }
|
||||
switch (what[selected]) {
|
||||
case A_BOOT: return 0;
|
||||
case A_VERBOSE: return 1;
|
||||
case A_OTHER: chain_load(arg[selected]); break;
|
||||
case A_DISPLAY: res_choose(gop); break;
|
||||
case A_INFO: sys_info(gop); break;
|
||||
case A_FIRMWARE: firmware_setup(); break;
|
||||
case A_RESTART: power(EfiResetCold); break;
|
||||
case A_SHUTDOWN: power(EfiResetShutdown); break;
|
||||
#ifdef SCHOS_DEBUG
|
||||
// A toggle rather than an action: picking it changes what the
|
||||
// entry says and leaves you in the menu, so the state you booted
|
||||
// with is the state you can see before you boot.
|
||||
if (selected == 5) { DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; continue; }
|
||||
case A_DEBUG: DEBUG_BOOT = !DEBUG_BOOT; echoed = -1; break;
|
||||
#endif
|
||||
if (selected == 4) {
|
||||
ui_clear();
|
||||
ui_text_centered((int)ui_height() / 2, "Halted -- reset to reach firmware setup",
|
||||
0xB9BEC7u);
|
||||
for (;;) __asm__ volatile("hlt");
|
||||
}
|
||||
break; // repaint: the thing that ran may have changed the screen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2146,6 +2642,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
|||
{
|
||||
ST = systab;
|
||||
BS = systab->BootServices;
|
||||
SELF = image;
|
||||
|
||||
print(L"SchOS UEFI loader\r\n");
|
||||
|
||||
|
|
|
|||
|
|
@ -19,13 +19,14 @@
|
|||
// guest cannot detect this: a key the host swallowed is indistinguishable from
|
||||
// a key nobody pressed. So it is a setting.
|
||||
//
|
||||
// chvt -c altgr+F AltGr+F1..F6 (the default)
|
||||
// chvt -c ctrl+shift+F Ctrl+Shift+F1..F6
|
||||
// chvt -c alt+1 Alt+1..6
|
||||
// chvt -c ctrl+alt+F Ctrl+Alt+F1..F12 (the default)
|
||||
// chvt -c altgr+F AltGr+F1..F12
|
||||
// chvt -c ctrl+shift+F Ctrl+Shift+F1..F12
|
||||
// chvt -c alt+1 Alt+1..9
|
||||
// chvt -c off no chord at all
|
||||
//
|
||||
// Whatever the modifiers are, they also bind minus and equals to the previous
|
||||
// and next terminal in use -- AltGr+- and AltGr+= by default.
|
||||
// and next terminal in use -- Ctrl+Alt+- and Ctrl+Alt+= by default.
|
||||
|
||||
#include "core/types.h"
|
||||
#include "uapi/user.h"
|
||||
|
|
|
|||
380
cmd/ldlsd/main.c
380
cmd/ldlsd/main.c
|
|
@ -9,10 +9,10 @@
|
|||
//
|
||||
// ldlsd <module.ko> [vmlinux-btf]
|
||||
//
|
||||
// **Neither file ships with SchOS.** Both are the user's: a module they
|
||||
// obtained, and the BTF of the kernel it was built for. docs/licensing.md §6
|
||||
// is the whole argument, and `.gitignore` refusing `*.ko` is how it is kept
|
||||
// mechanically rather than by intention.
|
||||
// The module and the BTF of the kernel it was built for come from a Linux build
|
||||
// (`make ldls-stage`, `make ldls-r8169`). SchOS ships Linux's r8169 set in its
|
||||
// ISO; docs/licensing.md §6 has the licensing, and `.gitignore` refusing `*.ko`
|
||||
// only keeps binaries out of git.
|
||||
//
|
||||
// What it does *not* yet do is implement any of the symbols the driver asks
|
||||
// for -- every one is still a stub that reports itself and returns zero, which
|
||||
|
|
@ -185,6 +185,32 @@ ldls_stub(long idx, unsigned long *args)
|
|||
|
||||
struct rctx { struct ko *k; struct stubs *s; };
|
||||
|
||||
// Modules loaded before the one being relocated. A module set is loaded in
|
||||
// dependency order -- libphy before r8169, ttm before amdgpu -- and what an
|
||||
// earlier one defines is what a later one imports from it.
|
||||
#define MAXMOD 32
|
||||
static struct ko *g_mods[MAXMOD];
|
||||
static int g_nmods;
|
||||
|
||||
static unsigned long long
|
||||
find_global(struct ko *k, const char *want)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < k->nsym; i++) {
|
||||
if ((k->sym[i].st_info >> 4) == 0) // STB_LOCAL: not for others
|
||||
continue;
|
||||
if (k->sym[i].st_shndx == KO_SHN_UNDEF ||
|
||||
k->sym[i].st_shndx >= (unsigned short)k->nsh)
|
||||
continue;
|
||||
if (!k->sec[k->sym[i].st_shndx].alloc)
|
||||
continue;
|
||||
if (!streq(k->str + k->sym[i].st_name, want))
|
||||
continue;
|
||||
return k->sec[k->sym[i].st_shndx].addr + k->sym[i].st_value;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The KPI first, a logging stub second.
|
||||
//
|
||||
// An unimplemented symbol still resolves -- to something that reports itself
|
||||
|
|
@ -208,6 +234,14 @@ resolve(void *ctx, const char *name, unsigned long long *out)
|
|||
return 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < g_nmods; i++) {
|
||||
unsigned long long a = find_global(g_mods[i], name);
|
||||
if (a) {
|
||||
*out = a;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < r->k->nundef; i++)
|
||||
if (streq(r->k->undef[i], name)) {
|
||||
*out = stub_addr(r->s, i);
|
||||
|
|
@ -230,7 +264,7 @@ resolve(void *ctx, const char *name, unsigned long long *out)
|
|||
// bargain as the logging stubs, and the same caveat -- see the memcpy note in
|
||||
// docs/architecture/ldls.md.
|
||||
|
||||
#define PERCPU_SLOTS 8
|
||||
#define PERCPU_SLOTS 16
|
||||
#define PERCPU_SIZE (16 * 1024)
|
||||
|
||||
static char percpu_mem[PERCPU_SLOTS][PERCPU_SIZE];
|
||||
|
|
@ -260,6 +294,14 @@ percpu_for(void *ctx, const char *name)
|
|||
*(unsigned long long *)percpu_mem[i] =
|
||||
(unsigned long long)(void *)(percpu_mem[i] + 64);
|
||||
puts_(" per-CPU "); puts_(name); puts_(" -- a task of zeroes\n");
|
||||
} else if (streq(name, "__preempt_count")) {
|
||||
// x86 keeps PREEMPT_NEED_RESCHED inverted in this word, so that
|
||||
// `decl %gs:__preempt_count; je resched` is one instruction. Zero
|
||||
// means "count zero *and* a reschedule wanted", and every
|
||||
// preempt_enable() then called preempt_schedule -- see the note on
|
||||
// __SCT__preempt_schedule in ldls/kpi/base.c for what that cost.
|
||||
*(unsigned int *)percpu_mem[i] = 0x80000000u;
|
||||
puts_(" per-CPU "); puts_(name); puts_(" -- PREEMPT_ENABLED\n");
|
||||
} else {
|
||||
puts_(" per-CPU "); puts_(name); puts_(" -- 16K of zeroes\n");
|
||||
}
|
||||
|
|
@ -284,6 +326,33 @@ find_symbol(struct ko *k, const char *want)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// "r8169: rtl_open.cold+0x4f" for an address in a loaded module, so that a
|
||||
// photograph of the screen is enough to find the line. Modules are named by
|
||||
// the file they were loaded from, with the directory and ".ko" dropped.
|
||||
static const char *g_modname[MAXMOD];
|
||||
|
||||
static void
|
||||
where(unsigned long addr)
|
||||
{
|
||||
for (int m = 0; m < g_nmods; m++) {
|
||||
struct ko *k = g_mods[m];
|
||||
for (int i = 0; i < k->nsym; i++) {
|
||||
const struct ko_sym *s = &k->sym[i];
|
||||
if (s->st_shndx == KO_SHN_UNDEF || s->st_shndx >= (unsigned short)k->nsh ||
|
||||
!k->sec[s->st_shndx].alloc || s->st_size == 0 || (s->st_info & 0xf) != 2)
|
||||
continue;
|
||||
unsigned long long a = k->sec[s->st_shndx].addr + s->st_value;
|
||||
if (addr >= a && addr < a + s->st_size) {
|
||||
puts_(g_modname[m]); puts_(": ");
|
||||
puts_(k->str + s->st_name); puts_("+");
|
||||
puthex(addr - a);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
puthex(addr);
|
||||
}
|
||||
|
||||
// --- reading and writing a struct at an offset BTF gave us --------------------
|
||||
|
||||
static unsigned int
|
||||
|
|
@ -430,6 +499,9 @@ kpi_layout_from_btf(struct btf *b)
|
|||
GET(nd, "dev_addr", kpi_l.nd_dev_addr);
|
||||
GET(nd, "perm_addr", kpi_l.nd_perm_addr);
|
||||
GET(nd, "addr_len", kpi_l.nd_addr_len);
|
||||
// Optional: older kernels do not have it, and 0 is how that is said.
|
||||
if (btf_member_off(b, nd, "priv_len", &off, &size) == 0)
|
||||
kpi_l.nd_priv_len = off;
|
||||
GET(nd, "mc", kpi_l.nd_mc);
|
||||
GET(nd, "uc", kpi_l.nd_uc);
|
||||
GET(nd, "_tx", kpi_l.nd_tx);
|
||||
|
|
@ -445,8 +517,42 @@ kpi_layout_from_btf(struct btf *b)
|
|||
GET(pd, "dev", kpi_l.pd_dev);
|
||||
GET(dv, "kobj", kpi_l.dev_kobj);
|
||||
GET(ko, "name", kpi_l.kobj_name);
|
||||
kpi_l.dev_kobj_ok = 1;
|
||||
}
|
||||
}
|
||||
// The driver core and struct firmware. Optional: a driver that never
|
||||
// registers a bus or loads firmware does not need them, so a BTF without
|
||||
// one of these turns the feature off rather than refusing to run.
|
||||
{
|
||||
int dv = btf_find(b, "device", BTF_KIND_STRUCT);
|
||||
int dd = btf_find(b, "device_driver", BTF_KIND_STRUCT);
|
||||
int bt = btf_find(b, "bus_type", BTF_KIND_STRUCT);
|
||||
int fw = btf_find(b, "firmware", BTF_KIND_STRUCT);
|
||||
int ok = dv && dd && bt;
|
||||
#define OPT(id, name, field) \
|
||||
do { if (btf_member_off(b, id, name, &off, &size) < 0) ok = 0; \
|
||||
else field = off; } while (0)
|
||||
if (ok) {
|
||||
OPT(dv, "bus", kpi_l.dev_bus);
|
||||
OPT(dv, "driver", kpi_l.dev_driver);
|
||||
OPT(dd, "bus", kpi_l.drv_bus);
|
||||
OPT(dd, "probe", kpi_l.drv_probe);
|
||||
OPT(dd, "name", kpi_l.drv_name);
|
||||
OPT(bt, "match", kpi_l.bus_match);
|
||||
OPT(bt, "probe", kpi_l.bus_probe);
|
||||
}
|
||||
kpi_l.core_ok = ok;
|
||||
if (fw) {
|
||||
ok = 1;
|
||||
OPT(fw, "size", kpi_l.fw_size);
|
||||
OPT(fw, "data", kpi_l.fw_data);
|
||||
kpi_l.fw_size_of = btf_type_size(b, fw);
|
||||
kpi_l.fw_ok = ok;
|
||||
}
|
||||
#undef OPT
|
||||
puts_(" kpi driver core "); puts_(kpi_l.core_ok ? "on" : "off");
|
||||
puts_(", firmware "); puts_(kpi_l.fw_ok ? "on" : "off"); puts_("\n");
|
||||
}
|
||||
{
|
||||
int nq = btf_find(b, "netdev_queue", BTF_KIND_STRUCT);
|
||||
if (!nq) { puts_("ldlsd: no struct netdev_queue\n"); return -1; }
|
||||
|
|
@ -682,8 +788,11 @@ drive_probe(const char *btfpath)
|
|||
{
|
||||
struct pciuser_info in;
|
||||
|
||||
// No device: not a failure worth restarting for. schinit's
|
||||
// KeepAlive would otherwise start this again every few seconds on
|
||||
// every machine without the NIC. See SCHINIT_EXIT_NORESTART.
|
||||
if (claim_for(idtab, id_size, id_vendor, id_device, &in) < 0)
|
||||
return;
|
||||
exit_group(78);
|
||||
|
||||
pdev = (unsigned long)getmem(dev_size);
|
||||
if (!pdev) { puts_("ldlsd: no memory for a pci_dev\n"); return; }
|
||||
|
|
@ -714,6 +823,26 @@ drive_probe(const char *btfpath)
|
|||
(unsigned char)(q >> (i * 8));
|
||||
}
|
||||
|
||||
// pdev->bus. pci_domain_nr() and pci_dev_id() are inlines that
|
||||
// dereference it -- r8169 names its MDIO bus with both -- and a null
|
||||
// one is a fault at the first line of MDIO setup.
|
||||
{
|
||||
unsigned int pd_bus = lay(&b, "pci_dev", "bus");
|
||||
unsigned int pb_num = lay(&b, "pci_bus", "number");
|
||||
unsigned int pb_sys = lay(&b, "pci_bus", "sysdata");
|
||||
unsigned int pb_size = lay(&b, "pci_bus", 0);
|
||||
if (pd_bus != 0xffffffffu && pb_num != 0xffffffffu &&
|
||||
pb_sys != 0xffffffffu && pb_size != 0xffffffffu) {
|
||||
unsigned long bus = (unsigned long)getmem(pb_size);
|
||||
unsigned long sys = (unsigned long)getmem(4096);
|
||||
if (bus && sys) {
|
||||
((unsigned char *)bus)[pb_num] = (unsigned char)in.bus;
|
||||
bcopy_((void *)(bus + pb_sys), &sys, 8);
|
||||
bcopy_((void *)(pdev + pd_bus), &bus, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wr16(pdev, dev_vendor, in.vendor);
|
||||
wr16(pdev, dev_device, in.device);
|
||||
wr32(pdev, dev_devfn, (unsigned int)((in.slot << 3) | in.func));
|
||||
|
|
@ -795,21 +924,145 @@ drive_probe(const char *btfpath)
|
|||
}
|
||||
}
|
||||
|
||||
// --- main ----------------------------------------------------------------------
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
// Read the BTF and fill in the KPI's layouts, before any module runs.
|
||||
static void
|
||||
early_layout(const char *btfpath)
|
||||
{
|
||||
struct ko k;
|
||||
static struct btf b;
|
||||
unsigned long long blen;
|
||||
unsigned char *bimg = slurp(btfpath, &blen);
|
||||
long nt;
|
||||
unsigned int *boff;
|
||||
|
||||
if (!bimg) return;
|
||||
nt = btf_count(bimg, blen);
|
||||
if (nt < 0) return;
|
||||
boff = getmem(((unsigned long long)nt + 2) * 4);
|
||||
if (!boff || btf_open(&b, bimg, blen, boff, (unsigned int)nt + 2) < 0)
|
||||
return;
|
||||
{
|
||||
extern unsigned int kpi_netdev_size;
|
||||
int nd = btf_find(&b, "net_device", BTF_KIND_STRUCT);
|
||||
kpi_netdev_size = nd ? btf_type_size(&b, nd) : 0;
|
||||
}
|
||||
kpi_layout_from_btf(&b);
|
||||
}
|
||||
|
||||
// One module: read, laid out, relocated, its %gs references fixed, and its
|
||||
// init_module called. Kept loaded for the ones after it to link against.
|
||||
static void
|
||||
load_module(const char *path)
|
||||
{
|
||||
struct ko *k;
|
||||
struct ko_section *sec;
|
||||
struct stubs stubs;
|
||||
struct stubs *stubs;
|
||||
struct rctx rc;
|
||||
unsigned char *img, *mem, *code;
|
||||
unsigned long long len, span, entry;
|
||||
int nsec, i;
|
||||
|
||||
k = getmem(sizeof(*k));
|
||||
stubs = getmem(sizeof(*stubs));
|
||||
if (!k || !stubs) { puts_("ldlsd: out of memory\n"); exit(); }
|
||||
|
||||
img = slurp(path, &len);
|
||||
if (!img) { puts_("ldlsd: cannot read "); puts_(path); puts_("\n"); exit(); }
|
||||
|
||||
nsec = ko_sections_needed(img, len);
|
||||
if (nsec < 0) {
|
||||
puts_("ldlsd: that is not an ELF64 relocatable object\n");
|
||||
exit();
|
||||
}
|
||||
sec = getmem((unsigned long long)(nsec + 1) * sizeof(struct ko_section));
|
||||
if (!sec) { puts_("ldlsd: out of memory\n"); exit(); }
|
||||
|
||||
if (ko_open(k, img, len, sec, nsec) < 0) {
|
||||
puts_("ldlsd: "); puts_(ko_err(k)); puts_("\n");
|
||||
exit();
|
||||
}
|
||||
|
||||
puts_(path); puts_("\n");
|
||||
puts_(" module ");
|
||||
puts_(ko_name(k) ? ko_name(k) : "(not a kernel module)"); puts_("\n");
|
||||
puts_(" license ");
|
||||
puts_(ko_modinfo(k, "license") ? ko_modinfo(k, "license")
|
||||
: "(none declared)"); puts_("\n");
|
||||
puts_(" wants "); putn(k->nundef); puts_(" symbols\n");
|
||||
|
||||
span = ko_span(k);
|
||||
mem = getmem(span ? span : 4096);
|
||||
code = getmem((unsigned long long)(k->nundef + 1) * STUB_SIZE);
|
||||
if (!mem || !code) { puts_("ldlsd: out of memory\n"); exit(); }
|
||||
|
||||
stub_build_from(stubs, code, k->nundef, g_count);
|
||||
for (i = 0; i < k->nundef; i++) {
|
||||
g_names[g_count + i] = k->undef[i];
|
||||
if (stub_is_bare(k->undef[i]))
|
||||
stub_make_bare(code + (unsigned long long)i * STUB_SIZE);
|
||||
}
|
||||
g_count += k->nundef;
|
||||
k_nundef_global = g_count;
|
||||
|
||||
if (ko_layout(k, mem, span, (unsigned long long)mem) < 0) {
|
||||
puts_("ldlsd: layout: "); puts_(ko_err(k)); puts_("\n");
|
||||
exit();
|
||||
}
|
||||
|
||||
rc.k = k; rc.s = stubs;
|
||||
if (ko_relocate(k, resolve, &rc) < 0) {
|
||||
puts_("ldlsd: relocate: "); puts_(ko_err(k)); puts_("\n");
|
||||
exit();
|
||||
}
|
||||
// The stack canary is fetched through %gs, which belongs to the kernel
|
||||
// here. See ko_defuse_gs().
|
||||
{
|
||||
int g = ko_defuse_gs(k, "__ref_stack_chk_guard");
|
||||
if (g < 0) g = 0;
|
||||
if (g == 0) g = ko_defuse_gs(k, "__stack_chk_guard");
|
||||
if (g > 0) { puts_(" defused "); putn(g); puts_(" %gs stack-guard load(s)\n"); }
|
||||
}
|
||||
// Whatever %gs references are left name real per-CPU variables, and those
|
||||
// cannot be defused -- the driver dereferences what it reads.
|
||||
{
|
||||
int p = ko_fixup_percpu(k, percpu_for, 0);
|
||||
if (p < 0) {
|
||||
puts_("ldlsd: per-CPU: "); puts_(ko_err(k)); puts_("\n");
|
||||
exit();
|
||||
}
|
||||
if (p > 0) { puts_(" rehomed "); putn(p); puts_(" %gs per-CPU reference(s)\n"); }
|
||||
}
|
||||
|
||||
puts_(" loaded "); putn(span); puts_(" bytes at "); puthex((unsigned long long)mem);
|
||||
puts_("\n");
|
||||
|
||||
entry = find_symbol(k, "init_module");
|
||||
if (g_nmods < MAXMOD) {
|
||||
const char *b = path, *q;
|
||||
for (q = path; *q; q++)
|
||||
if (*q == '/') b = q + 1;
|
||||
g_modname[g_nmods] = b;
|
||||
g_mods[g_nmods++] = k;
|
||||
kpi_where = where;
|
||||
}
|
||||
if (!entry) { puts_(" no init_module -- nothing to call\n"); return; }
|
||||
|
||||
puts_("\n calling init_module\n\n");
|
||||
{
|
||||
int (*init)(void) = (int (*)(void))(void *)entry;
|
||||
int r = init();
|
||||
puts_("\n init_module returned "); putn(r);
|
||||
puts_(" after "); putn(g_calls); puts_(" stub call(s)\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// --- main ----------------------------------------------------------------------
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
if (argc < 2) {
|
||||
puts_("usage: ldlsd <module.ko> [vmlinux-btf] [bus:slot.func]\n");
|
||||
puts_("usage: ldlsd <module.ko>[,<module.ko>...] [vmlinux-btf] [bus:slot.func]\n");
|
||||
puts_(" neither file ships with SchOS; both are yours.\n");
|
||||
exit();
|
||||
}
|
||||
|
|
@ -837,85 +1090,32 @@ main(int argc, char *argv[])
|
|||
puthexn(sel_slot, 2); puts_("."); puthexn(sel_func, 1); puts_("\n");
|
||||
}
|
||||
|
||||
img = slurp(argv[1], &len);
|
||||
if (!img) { puts_("ldlsd: cannot read "); puts_(argv[1]); puts_("\n"); exit(); }
|
||||
|
||||
nsec = ko_sections_needed(img, len);
|
||||
if (nsec < 0) {
|
||||
puts_("ldlsd: that is not an ELF64 relocatable object\n");
|
||||
exit();
|
||||
}
|
||||
sec = getmem((unsigned long long)(nsec + 1) * sizeof(struct ko_section));
|
||||
if (!sec) { puts_("ldlsd: out of memory\n"); exit(); }
|
||||
|
||||
if (ko_open(&k, img, len, sec, nsec) < 0) {
|
||||
puts_("ldlsd: "); puts_(ko_err(&k)); puts_("\n");
|
||||
exit();
|
||||
}
|
||||
|
||||
puts_(argv[1]); puts_("\n");
|
||||
puts_(" module ");
|
||||
puts_(ko_name(&k) ? ko_name(&k) : "(not a kernel module)"); puts_("\n");
|
||||
puts_(" license ");
|
||||
puts_(ko_modinfo(&k, "license") ? ko_modinfo(&k, "license")
|
||||
: "(none declared)"); puts_("\n");
|
||||
puts_(" wants "); putn(k.nundef); puts_(" symbols\n");
|
||||
|
||||
span = ko_span(&k);
|
||||
mem = getmem(span ? span : 4096);
|
||||
code = getmem((unsigned long long)(k.nundef + 1) * STUB_SIZE);
|
||||
if (!mem || !code) { puts_("ldlsd: out of memory\n"); exit(); }
|
||||
|
||||
stub_build(&stubs, code, k.nundef);
|
||||
g_names = k.undef;
|
||||
g_count = k.nundef;
|
||||
k_nundef_global = k.nundef;
|
||||
kpi.netfd = -1;
|
||||
for (i = 0; i < k.nundef; i++)
|
||||
if (stub_is_bare(k.undef[i]))
|
||||
stub_make_bare(code + (unsigned long long)i * STUB_SIZE);
|
||||
|
||||
if (ko_layout(&k, mem, span, (unsigned long long)mem) < 0) {
|
||||
puts_("ldlsd: layout: "); puts_(ko_err(&k)); puts_("\n");
|
||||
exit();
|
||||
}
|
||||
|
||||
rc.k = &k; rc.s = &stubs;
|
||||
if (ko_relocate(&k, resolve, &rc) < 0) {
|
||||
puts_("ldlsd: relocate: "); puts_(ko_err(&k)); puts_("\n");
|
||||
exit();
|
||||
}
|
||||
// The stack canary is fetched through %gs, which belongs to the kernel
|
||||
// here. See ko_defuse_gs().
|
||||
// argv[1] is one module, or a set in dependency order separated by
|
||||
// commas: libphy.ko,realtek.ko,r8169.ko. Each is loaded, relocated
|
||||
// against the KPI and the ones before it, and initialised before the next.
|
||||
{
|
||||
int g = ko_defuse_gs(&k, "__ref_stack_chk_guard");
|
||||
if (g < 0) g = 0;
|
||||
if (g == 0) g = ko_defuse_gs(&k, "__stack_chk_guard");
|
||||
if (g > 0) { puts_(" defused "); putn(g); puts_(" %gs stack-guard load(s)\n"); }
|
||||
}
|
||||
// Whatever %gs references are left name real per-CPU variables, and those
|
||||
// cannot be defused -- the driver dereferences what it reads.
|
||||
{
|
||||
int p = ko_fixup_percpu(&k, percpu_for, 0);
|
||||
if (p < 0) {
|
||||
puts_("ldlsd: per-CPU: "); puts_(ko_err(&k)); puts_("\n");
|
||||
exit();
|
||||
static char list[4096];
|
||||
char *p = list, *name;
|
||||
int n = 0;
|
||||
while (argv[1][n] && n < (int)sizeof(list) - 1) { list[n] = argv[1][n]; n++; }
|
||||
list[n] = 0;
|
||||
// The layouts first: init_module is where a module registers its
|
||||
// drivers, and the driver core needs struct offsets to do that.
|
||||
if (argc > 2)
|
||||
early_layout(argv[2]);
|
||||
g_names = getmem(sizeof(char *) * (unsigned long long)MAXMOD * KO_MAXUNDEF);
|
||||
if (!g_names) { puts_("ldlsd: out of memory\n"); exit(); }
|
||||
kpi.netfd = -1;
|
||||
for (;;) {
|
||||
name = p;
|
||||
while (*p && *p != ',') p++;
|
||||
int last = *p == 0;
|
||||
*p = 0;
|
||||
if (*name)
|
||||
load_module(name);
|
||||
if (last) break;
|
||||
p++;
|
||||
}
|
||||
if (p > 0) { puts_(" rehomed "); putn(p); puts_(" %gs per-CPU reference(s)\n"); }
|
||||
}
|
||||
|
||||
puts_(" loaded "); putn(span); puts_(" bytes at "); puthex((unsigned long long)mem);
|
||||
puts_("\n");
|
||||
|
||||
entry = find_symbol(&k, "init_module");
|
||||
if (!entry) { puts_(" no init_module -- nothing to call\n"); exit(); }
|
||||
|
||||
puts_("\n calling init_module\n\n");
|
||||
{
|
||||
int (*init)(void) = (int (*)(void))(void *)entry;
|
||||
int r = init();
|
||||
puts_("\n init_module returned "); putn(r);
|
||||
puts_(" after "); putn(g_calls); puts_(" stub call(s)\n");
|
||||
}
|
||||
|
||||
if (argc > 2 && g_pci_driver)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// memfdtest -- two processes map the same memfd, one writes, the other sees it.
|
||||
//
|
||||
// That sentence is docs/plan/foundations.md's own acceptance criterion for
|
||||
// That sentence is docs/architecture/kernel.md's own acceptance criterion for
|
||||
// item 2, and it is the check a permissive implementation cannot pass: an
|
||||
// mmap that quietly returns private zeroed pages satisfies every return code
|
||||
// here and fails the one assertion that matters, because the reader is a
|
||||
|
|
|
|||
178
cmd/passwd/main.c
Normal file
178
cmd/passwd/main.c
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// passwd [USER]
|
||||
//
|
||||
// Change a password. Setuid root, like su and for the same reason a program of
|
||||
// its own: it has to write /etc/shadow, which no one else can.
|
||||
//
|
||||
// Anyone may change their own password after proving they know the old one;
|
||||
// root may change anyone's without it. The new one is stored as a SHA-512
|
||||
// crypt hash with a fresh random salt, and the file is replaced by rename, so
|
||||
// a crash leaves the old shadow or the new one and never half of each.
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <crypt.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <shadow.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <termios.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int
|
||||
read_password(const char *prompt, char *buf, size_t n)
|
||||
{
|
||||
int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
|
||||
struct termios t, old;
|
||||
size_t i = 0;
|
||||
char c;
|
||||
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (tcgetattr(fd, &old) == 0) {
|
||||
t = old;
|
||||
t.c_lflag &= ~(ECHO | ECHONL);
|
||||
tcsetattr(fd, TCSAFLUSH, &t);
|
||||
}
|
||||
if (write(fd, prompt, strlen(prompt)) < 0) {}
|
||||
while (read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
|
||||
if (i + 1 < n)
|
||||
buf[i++] = c;
|
||||
buf[i] = 0;
|
||||
tcsetattr(fd, TCSAFLUSH, &old);
|
||||
if (write(fd, "\n", 1) < 0) {}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
make_salt(char *out)
|
||||
{
|
||||
static const char a[] =
|
||||
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
unsigned char r[16];
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
|
||||
if (fd < 0 || read(fd, r, sizeof r) != (ssize_t)sizeof r) {
|
||||
fprintf(stderr, "passwd: no random numbers\n");
|
||||
exit(1);
|
||||
}
|
||||
close(fd);
|
||||
strcpy(out, "$6$");
|
||||
for (int i = 0; i < 16; i++)
|
||||
out[3 + i] = a[r[i] & 63];
|
||||
out[19] = '$';
|
||||
out[20] = 0;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
uid_t uid = getuid();
|
||||
struct passwd *me = getpwuid(uid), *pw;
|
||||
// getpwuid and getpwnam share one static buffer: keep the name we were
|
||||
// given, not a pointer into it.
|
||||
char *user = argc > 1 ? argv[1] : (me ? strdup(me->pw_name) : 0);
|
||||
char oldp[256], p1[256], p2[256], salt[24], *hash;
|
||||
FILE *in, *out;
|
||||
char line[1024], tmp[] = "/etc/shadow.new";
|
||||
long days = (long)(time(0) / 86400);
|
||||
int found = 0;
|
||||
|
||||
if (!user || !(pw = getpwnam(user))) {
|
||||
fprintf(stderr, "passwd: unknown user\n");
|
||||
return 1;
|
||||
}
|
||||
{
|
||||
static struct passwd copy;
|
||||
copy = *pw;
|
||||
copy.pw_name = strdup(pw->pw_name);
|
||||
pw = ©
|
||||
}
|
||||
if (geteuid() != 0) {
|
||||
fprintf(stderr, "passwd: must be setuid root\n");
|
||||
return 1;
|
||||
}
|
||||
if (uid != 0 && pw->pw_uid != uid) {
|
||||
fprintf(stderr, "passwd: you may only change your own password\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (uid != 0) {
|
||||
struct spwd *sp = getspnam(pw->pw_name);
|
||||
const char *cur = sp ? sp->sp_pwdp : "";
|
||||
if (cur[0]) {
|
||||
if (read_password("Current password: ", oldp, sizeof oldp) < 0)
|
||||
return 1;
|
||||
char *e = crypt(oldp, cur);
|
||||
memset(oldp, 0, sizeof oldp);
|
||||
if (!e || strcmp(e, cur) != 0) {
|
||||
sleep(2);
|
||||
fprintf(stderr, "passwd: incorrect password\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (read_password("New password: ", p1, sizeof p1) < 0 ||
|
||||
read_password("Retype new password: ", p2, sizeof p2) < 0)
|
||||
return 1;
|
||||
if (strcmp(p1, p2) != 0) {
|
||||
fprintf(stderr, "passwd: the passwords do not match\n");
|
||||
return 1;
|
||||
}
|
||||
make_salt(salt);
|
||||
// An empty password stays empty: crypt of "" is a hash, and the user asked
|
||||
// for no password at all.
|
||||
hash = p1[0] ? crypt(p1, salt) : "";
|
||||
if (!hash) {
|
||||
fprintf(stderr, "passwd: cannot hash\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!(in = fopen("/etc/shadow", "r"))) {
|
||||
fprintf(stderr, "passwd: /etc/shadow: %s\n", strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
umask(0077);
|
||||
if (!(out = fopen(tmp, "w"))) {
|
||||
fprintf(stderr, "passwd: %s: %s\n", tmp, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
while (fgets(line, sizeof line, in)) {
|
||||
size_t n = strlen(pw->pw_name);
|
||||
if (strncmp(line, pw->pw_name, n) == 0 && line[n] == ':') {
|
||||
// name:hash:lastchg:min:max:warn:inactive:expire:reserved
|
||||
char *f = strchr(line + n + 1, ':');
|
||||
fprintf(out, "%s:%s:%ld%s", pw->pw_name, hash, days,
|
||||
f && (f = strchr(f + 1, ':')) ? f : ":0:::::\n");
|
||||
found = 1;
|
||||
} else {
|
||||
fputs(line, out);
|
||||
}
|
||||
}
|
||||
fclose(in);
|
||||
if (!found)
|
||||
fprintf(out, "%s:%s:%ld:0:::::\n", pw->pw_name, hash, days);
|
||||
memset(p1, 0, sizeof p1);
|
||||
memset(p2, 0, sizeof p2);
|
||||
|
||||
struct group *g = getgrnam("shadow");
|
||||
fchmod(fileno(out), 0640);
|
||||
if (g)
|
||||
fchown(fileno(out), 0, g->gr_gid);
|
||||
if (fclose(out) != 0 || rename(tmp, "/etc/shadow") < 0) {
|
||||
fprintf(stderr, "passwd: could not write /etc/shadow: %s\n", strerror(errno));
|
||||
unlink(tmp);
|
||||
return 1;
|
||||
}
|
||||
printf("password updated\n");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// pcitest -- prove that a driver could live out here.
|
||||
//
|
||||
// docs/plan/hybrid.md mechanisms 2 and 4, exercised end to end from a process:
|
||||
// docs/architecture/devices.md mechanisms 2 and 4, exercised end to end from a process:
|
||||
// enumerate the bus, claim a device, map its registers, read them, and get
|
||||
// memory the device could be pointed at.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// worth recording because it is not "ours is better". toybox's ping reads its
|
||||
// replies with recvmsg(2) and asks for the TTL through an IP_RECVTTL control
|
||||
// message, and SchOS has neither: no recvmsg, no ancillary data. Both are owed
|
||||
// -- recvmsg is item 1 of docs/plan/foundations.md, where it is wanted for
|
||||
// -- recvmsg is item 1 of docs/architecture/kernel.md, where it is wanted for
|
||||
// SCM_RIGHTS -- and neither should be rushed to make one applet run.
|
||||
//
|
||||
// So this uses sendto/recvfrom, which the kernel does have, and proves the ICMP
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// schmode -- change the display mode.
|
||||
//
|
||||
// docs/plan/hybrid.md milestone H3. The interesting thing about this program
|
||||
// docs/architecture/devices.md milestone H3. The interesting thing about this program
|
||||
// is how little it is: it opens a socket to `vgpud` and writes a line. No
|
||||
// syscall it makes has heard of a GPU, no kernel code runs that knows what a
|
||||
// scanout is, and if the driver on the other end gets the sequence wrong the
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ strneq_(const char *a, const char *b, int n)
|
|||
// ---------------------------------------------------------------------------
|
||||
// 8. SCM_RIGHTS: a descriptor crossing between processes.
|
||||
//
|
||||
// docs/plan/foundations.md item 1, docs/plan/hybrid.md mechanism 1. The thing
|
||||
// docs/architecture/kernel.md item 1, docs/architecture/devices.md mechanism 1. The thing
|
||||
// being proved is not that recvmsg returns a number -- it is that the number
|
||||
// names the *same open file* the other process had, which is why every check
|
||||
// below moves data through the descriptor rather than inspecting it.
|
||||
|
|
@ -763,7 +763,7 @@ test_fdpass_nodata(void)
|
|||
// ---------------------------------------------------------------------------
|
||||
// 15. A bound socket is a file.
|
||||
//
|
||||
// docs/plan/foundations.md item 3, in its own words: bind() a socket, then
|
||||
// docs/architecture/kernel.md item 3, in its own words: bind() a socket, then
|
||||
// stat() and `ls -l` the path and see an `s`, then unlink() it.
|
||||
//
|
||||
// The reason it matters is not tidiness. A Wayland client finds its compositor
|
||||
|
|
|
|||
221
cmd/su/main.c
Normal file
221
cmd/su/main.c
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// su [-|-l] [-p] [-c COMMAND] [USER]
|
||||
//
|
||||
// Run a shell, or one command, as another user -- root by default. Setuid root,
|
||||
// and a program of its own rather than a toybox applet: toybox is one binary
|
||||
// under every name, and a setuid copy of it can be asked for any of its other
|
||||
// applets by naming them, all as root.
|
||||
//
|
||||
// The rules are the ones su has always had, kept short:
|
||||
//
|
||||
// - root may become anyone, with no password;
|
||||
// - anyone else is asked for the target's password, and gets three tries;
|
||||
// - becoming root takes membership in `wheel` first, whatever the password
|
||||
// is -- an account with no password is otherwise a door for every user.
|
||||
//
|
||||
// -, -l a login shell: a clean environment, the target's home, argv[0] "-sh"
|
||||
// -p keep the environment (HOME, SHELL, USER and LOGNAME are still set
|
||||
// unless the target is root)
|
||||
// -c CMD run CMD with the shell's -c rather than an interactive shell
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <crypt.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <pwd.h>
|
||||
#include <shadow.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int
|
||||
in_wheel(uid_t uid, gid_t gid)
|
||||
{
|
||||
struct group *g = getgrnam("wheel");
|
||||
struct passwd *pw = getpwuid(uid);
|
||||
|
||||
if (!g)
|
||||
return 0;
|
||||
if (gid == g->gr_gid)
|
||||
return 1;
|
||||
for (char **m = g->gr_mem; pw && m && *m; m++)
|
||||
if (strcmp(*m, pw->pw_name) == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A line from the terminal with echo off. The password is read from the
|
||||
// controlling terminal, not stdin, so a script cannot feed it.
|
||||
static int
|
||||
read_password(const char *prompt, char *buf, size_t n)
|
||||
{
|
||||
int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
|
||||
struct termios t, old;
|
||||
size_t i = 0;
|
||||
char c;
|
||||
|
||||
if (fd < 0)
|
||||
return -1;
|
||||
if (tcgetattr(fd, &old) == 0) {
|
||||
t = old;
|
||||
t.c_lflag &= ~(ECHO | ECHONL);
|
||||
tcsetattr(fd, TCSAFLUSH, &t);
|
||||
}
|
||||
if (write(fd, prompt, strlen(prompt)) < 0) {}
|
||||
while (read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
|
||||
if (i + 1 < n)
|
||||
buf[i++] = c;
|
||||
buf[i] = 0;
|
||||
tcsetattr(fd, TCSAFLUSH, &old);
|
||||
if (write(fd, "\n", 1) < 0) {}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Does `pw` accept this password? An empty hash accepts anything, a locked one
|
||||
// ("*", "!...") nothing.
|
||||
static int
|
||||
password_ok(const struct passwd *pw, const char *pass)
|
||||
{
|
||||
const char *hash = pw->pw_passwd;
|
||||
struct spwd *sp;
|
||||
char *enc;
|
||||
|
||||
if (hash[0] == 'x' && hash[1] == 0) {
|
||||
sp = getspnam(pw->pw_name);
|
||||
if (!sp)
|
||||
return 0;
|
||||
hash = sp->sp_pwdp;
|
||||
}
|
||||
if (hash[0] == 0)
|
||||
return 1;
|
||||
if (hash[0] == '*' || hash[0] == '!')
|
||||
return 0;
|
||||
enc = crypt(pass, hash);
|
||||
return enc && strcmp(enc, hash) == 0;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
const char *target = "root", *cmd = 0;
|
||||
int login = 0, keep = 0, i;
|
||||
struct passwd *pw, *me;
|
||||
uid_t uid = getuid();
|
||||
char pass[256];
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "-") || !strcmp(argv[i], "-l"))
|
||||
login = 1;
|
||||
else if (!strcmp(argv[i], "-p"))
|
||||
keep = 1;
|
||||
else if (!strcmp(argv[i], "-c") && i + 1 < argc)
|
||||
cmd = argv[++i];
|
||||
else if (argv[i][0] == '-' && argv[i][1]) {
|
||||
fprintf(stderr, "usage: su [-|-l] [-p] [-c COMMAND] [USER]\n");
|
||||
return 2;
|
||||
} else
|
||||
target = argv[i];
|
||||
}
|
||||
|
||||
if (geteuid() != 0) {
|
||||
fprintf(stderr, "su: must be setuid root\n");
|
||||
return 1;
|
||||
}
|
||||
// getpwnam and getpwuid return the same static buffer, so the second call
|
||||
// overwrites the first answer. Keep our own copy of the one that matters.
|
||||
if (!(pw = getpwnam(target))) {
|
||||
fprintf(stderr, "su: unknown user %s\n", target);
|
||||
return 1;
|
||||
}
|
||||
{
|
||||
static struct passwd copy;
|
||||
copy = *pw;
|
||||
copy.pw_name = strdup(pw->pw_name);
|
||||
copy.pw_passwd = strdup(pw->pw_passwd);
|
||||
copy.pw_dir = strdup(pw->pw_dir);
|
||||
copy.pw_shell = strdup(pw->pw_shell);
|
||||
pw = ©
|
||||
}
|
||||
me = getpwuid(uid);
|
||||
|
||||
if (uid != 0) {
|
||||
int tries, ok = 0;
|
||||
|
||||
if (pw->pw_uid == 0 && !in_wheel(uid, getgid())) {
|
||||
fprintf(stderr, "su: %s is not in the wheel group\n",
|
||||
me ? me->pw_name : "you");
|
||||
return 1;
|
||||
}
|
||||
for (tries = 0; tries < 3 && !ok; tries++) {
|
||||
if (pw->pw_passwd[0] == 0 ||
|
||||
(pw->pw_passwd[0] == 'x' && getspnam(pw->pw_name) &&
|
||||
getspnam(pw->pw_name)->sp_pwdp[0] == 0)) {
|
||||
ok = 1;
|
||||
break;
|
||||
}
|
||||
if (read_password("Password: ", pass, sizeof pass) < 0) {
|
||||
fprintf(stderr, "su: no terminal\n");
|
||||
return 1;
|
||||
}
|
||||
ok = password_ok(pw, pass);
|
||||
memset(pass, 0, sizeof pass);
|
||||
if (!ok) {
|
||||
sleep(2);
|
||||
fprintf(stderr, "su: incorrect password\n");
|
||||
}
|
||||
}
|
||||
if (!ok)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (initgroups(pw->pw_name, pw->pw_gid) < 0 ||
|
||||
setgid(pw->pw_gid) < 0 || setuid(pw->pw_uid) < 0) {
|
||||
fprintf(stderr, "su: cannot become %s: %s\n", pw->pw_name, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
{
|
||||
const char *shell = pw->pw_shell && *pw->pw_shell ? pw->pw_shell : "/bin/sh";
|
||||
char *sh_argv[5];
|
||||
int n = 0;
|
||||
char arg0[256];
|
||||
|
||||
if (login) {
|
||||
clearenv();
|
||||
setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1);
|
||||
if (getenv("TERM") == 0)
|
||||
setenv("TERM", "linux", 1);
|
||||
}
|
||||
if (login || !keep || pw->pw_uid != 0) {
|
||||
setenv("HOME", pw->pw_dir, 1);
|
||||
setenv("SHELL", shell, 1);
|
||||
setenv("USER", pw->pw_name, 1);
|
||||
setenv("LOGNAME", pw->pw_name, 1);
|
||||
}
|
||||
if (login && chdir(pw->pw_dir) < 0)
|
||||
chdir("/");
|
||||
|
||||
if (login) {
|
||||
const char *base = strrchr(shell, '/');
|
||||
snprintf(arg0, sizeof arg0, "-%s", base ? base + 1 : shell);
|
||||
sh_argv[n++] = arg0;
|
||||
} else {
|
||||
const char *base = strrchr(shell, '/');
|
||||
sh_argv[n++] = (char *)(base ? base + 1 : shell);
|
||||
}
|
||||
if (cmd) {
|
||||
sh_argv[n++] = "-c";
|
||||
sh_argv[n++] = (char *)cmd;
|
||||
}
|
||||
sh_argv[n] = 0;
|
||||
execv(shell, sh_argv);
|
||||
fprintf(stderr, "su: %s: %s\n", shell, strerror(errno));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// vgpud -- the virtio-gpu driver, outside the kernel.
|
||||
//
|
||||
// docs/plan/hybrid.md's first eviction. The same device the kernel drives in
|
||||
// docs/architecture/devices.md's first eviction. The same device the kernel drives in
|
||||
// kernel/drivers/gpu/virtio_gpu/, driven instead by a process that asks the
|
||||
// kernel for four things and nothing else:
|
||||
//
|
||||
|
|
@ -648,7 +648,7 @@ present(void)
|
|||
//
|
||||
// This used to be a test card: three colour bands, four gradients, a
|
||||
// checkerboard, and this instance's pid in binary along the bottom edge. That
|
||||
// was scaffolding for hybrid.md's H2 and H3 -- a screenshot had to answer "did
|
||||
// was scaffolding for the out-of-kernel driver work -- a screenshot had to answer "did
|
||||
// a *userspace process* put this here", and "did the driver come back" needed
|
||||
// a second run to look different from a first that never stopped. Both
|
||||
// milestones are closed, and what was left was a garish test pattern on a
|
||||
|
|
@ -866,7 +866,7 @@ register_panic_fb(void)
|
|||
|
||||
// --- the modeset --------------------------------------------------------------
|
||||
//
|
||||
// docs/plan/hybrid.md milestone H3: *"a modeset API, which is the thing an
|
||||
// docs/architecture/devices.md milestone H3: *"a modeset API, which is the thing an
|
||||
// out-of-kernel GPU driver makes affordable and an in-kernel one never got."*
|
||||
//
|
||||
// The claim is worth testing rather than repeating, so here is the whole of
|
||||
|
|
@ -944,7 +944,7 @@ set_mode(int w, int h)
|
|||
//
|
||||
// The name is a *name*, not a path. AF_UNIX sockets in this kernel live in a
|
||||
// table keyed by string and never touch the filesystem -- that is item 3 of
|
||||
// docs/plan/foundations.md and it is not built. Nothing here depends on which
|
||||
// docs/architecture/kernel.md and it is not built. Nothing here depends on which
|
||||
// it is, and the day it becomes a real path this keeps working.
|
||||
#define VGPUD_SOCKET "/run/vgpud"
|
||||
|
||||
|
|
|
|||
45
config/drivers.conf
Normal file
45
config/drivers.conf
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Which drivers are compiled into the kernel.
|
||||
#
|
||||
# y compiled in, and enabled: it is offered every device it can drive
|
||||
# n not compiled in: the code is not linked and the kernel never knows it
|
||||
#
|
||||
# Everything is y unless it says otherwise. Change a line, or override one for
|
||||
# a single build:
|
||||
#
|
||||
# make DRV_E1000=n DRV_NVME=n
|
||||
#
|
||||
# A changed value rebuilds only kernel/drivers/registry.o and the link.
|
||||
#
|
||||
# These are the drivers that can be left out cleanly: nothing else in the kernel
|
||||
# calls them, they are only offered a device by the registry
|
||||
# (kernel/drivers/registry.c). The others cannot be, because the rest of the
|
||||
# kernel calls into them directly, and are not listed:
|
||||
#
|
||||
# vga_fb the boot framebuffer: the console and the crash screen draw on it
|
||||
# xhci, usb core, usb hid / storage / hub
|
||||
# the keyboard and the root disk can arrive over USB, bio.c and
|
||||
# the hotplug thread call in, and so does the I2C-HID touchpad
|
||||
# PS/2, IDE, the 16550 UART
|
||||
# fixed-address legacy devices (kernel/dev/)
|
||||
#
|
||||
# Linux's own drivers, run in a process by ldlsd, are not here: they are files in
|
||||
# the image (make ldls-r8169), not code in the kernel.
|
||||
|
||||
# --- display --------------------------------------------------------------
|
||||
DRV_VGA_PCI = y # the stdvga/Bochs card (the framebuffer driver stays regardless)
|
||||
DRV_VIRTIO_GPU = y # QEMU's virtio-gpu: mode setting and an explicit flush
|
||||
|
||||
# --- network ----------------------------------------------------------------
|
||||
DRV_E1000 = y # Intel 8254x and 8257x
|
||||
DRV_RTL8139 = y # Realtek 8139 (the one Realtek part QEMU emulates)
|
||||
# The native RTL8168/8111 driver. Off by default because the laptop's NIC is
|
||||
# driven by Linux's r8169 under LDLS (etc/schinit/ldls-r8169.job), and a driver
|
||||
# here would claim the device first. Turn it on for a machine without that job.
|
||||
DRV_R8169 = n
|
||||
|
||||
# --- storage ----------------------------------------------------------------
|
||||
DRV_AHCI = y # SATA
|
||||
DRV_NVME = y # NVMe SSDs
|
||||
|
||||
# --- audio ------------------------------------------------------------------
|
||||
DRV_AC97 = y # Intel AC'97 (QEMU's and old boards')
|
||||
|
|
@ -72,29 +72,36 @@ good chance it is in there.
|
|||
| | |
|
||||
|---|---|
|
||||
| [README](architecture/README.md) | The system, layer by layer. Read first. |
|
||||
| [boot.md](architecture/boot.md) | Firmware to the first userspace process. |
|
||||
| [kernel.md](architecture/kernel.md) | SchIFT: memory, processes, scheduling, traps, signals, SMP. |
|
||||
| [boot.md](architecture/boot.md) | Firmware to the first userspace process, and where the loader puts the kernel. |
|
||||
| [kernel.md](architecture/kernel.md) | SchIFT: memory, processes and threads, traps, signals, SMP. |
|
||||
| [scheduler.md](architecture/scheduler.md) | Fair share by virtual runtime, slices, and `nice`. |
|
||||
| [syscalls.md](architecture/syscalls.md) | The call table, and the Linux personality that lets real software run. |
|
||||
| [filesystem.md](architecture/filesystem.md) | The VFS, the buffer cache, procfs, mounts. |
|
||||
| [filesystem.md](architecture/filesystem.md) | The VFS, the buffer cache, procfs, sysfs, devtmpfs, mounts. |
|
||||
| [schfs.md](architecture/schfs.md) | SchFS: the copy-on-write root filesystem. |
|
||||
| [devices.md](architecture/devices.md) | The device model, driver binding, the class vtables. |
|
||||
| [scff.md](architecture/scff.md) | SCFF: the compressed image the live root ships as. |
|
||||
| [fat.md](architecture/fat.md) | FAT12/16/32 with long names: USB sticks, SD cards, the ESP. |
|
||||
| [devices.md](architecture/devices.md) | The device model, driver binding, the class vtables, and drivers that live in a process. |
|
||||
| [ldls.md](architecture/ldls.md) | Running Linux's drivers: a `.ko` in a process, reaching hardware through `/dev/pci`. |
|
||||
| [usb.md](architecture/usb.md) | xHCI, the USB core, and the class drivers. |
|
||||
| [graphics.md](architecture/graphics.md) | Framebuffer, text rendering, console, and the crash screen. |
|
||||
| [terminals.md](architecture/terminals.md) | The six virtual terminals, and how one is switched to. |
|
||||
| [networking.md](architecture/networking.md) | Drivers, lwIP, sockets — Unix and INET. |
|
||||
| [graphics.md](architecture/graphics.md) | Framebuffer, `/dev/fb0`, text rendering, the console, PSOGM and GMMOS. |
|
||||
| [terminals.md](architecture/terminals.md) | The twelve virtual terminals, ptys, and handing one to a display server. |
|
||||
| [audio.md](architecture/audio.md) | `/dev/dsp`, AudioEngine, AC'97, and ALSA over OSS. |
|
||||
| [networking.md](architecture/networking.md) | NIC drivers, lwIP, sockets — Unix, INET and netlink. |
|
||||
| [init.md](architecture/init.md) | `schinit`, PID 1 as a role, and the job manifests. |
|
||||
| [packages.md](architecture/packages.md) | The apk root: Alpine's packages, and SchOS as one of them. |
|
||||
| [desktop.md](architecture/desktop.md) | Xorg on the framebuffer, Window Maker, and what they stand on. |
|
||||
| [debugging.md](architecture/debugging.md) | Symbols, a log that outlives the kernel, and asking the kernel a question. |
|
||||
|
||||
## plan/ — what SchOS will be
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| [README](plan/README.md) | The three current decisions and the order they happen in. |
|
||||
| [1.0.md](plan/1.0.md) | **The finish line, and the freeze.** What 1.0 contains, what is deferred, and the register of every half-built thing in the tree. |
|
||||
| [foundations.md](plan/foundations.md) | The kernel work everything else is blocked on. |
|
||||
| [wayland.md](plan/wayland.md) | The GUI stack, and what gets deleted to make room. |
|
||||
| [hybrid.md](plan/hybrid.md) | Moving drivers out of the kernel. |
|
||||
| [README](plan/README.md) | What is here, what used to be, and the order. |
|
||||
| [1.0.md](plan/1.0.md) | **The finish line, and the freeze.** What 1.0 contains, where it stands, and the register of every half-built thing in the tree. |
|
||||
| [roadmap.md](plan/roadmap.md) | Everything else scoped and still open. |
|
||||
| [gpu.md](plan/gpu.md) | Linux's `amdgpu` through LDLS, and `/dev/dri` served by a process. |
|
||||
| [finch.md](plan/finch.md) | The author's language for SchOS's own programs. |
|
||||
| [schapi.md](plan/schapi.md) | The long-term API layering, and the compatibility promise. |
|
||||
| [roadmap.md](plan/roadmap.md) | Everything else still open. |
|
||||
|
||||
## guides/ — how to work on it
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ is a token. A hex value in that file means the system is missing something, and
|
|||
the answer is to add it to Niveus rather than to work around it here.
|
||||
|
||||
What that replaced was a 21 KB `style.css` that transcribed
|
||||
[`include/gfx/theme.h`](../../include/theme.h) by hand — palette, type scale,
|
||||
[`include/gfx/theme.h`](../../include/gfx/theme.h) by hand — palette, type scale,
|
||||
shadows, the lot. The intent was right and the mechanism was a copy: two files
|
||||
that had to be kept in step by remembering to. Now the whole identity is one
|
||||
declaration, in the `<head>` of each page:
|
||||
|
|
@ -70,22 +70,16 @@ rm -rf docs/Website/niveus && cp -r docs/niveus docs/Website/niveus
|
|||
|
||||
## The download page
|
||||
|
||||
`download.html` links to `releases/schos.iso`, a path relative to this
|
||||
directory. **Nothing puts a file there** — the ISO is 62 MB and does not belong
|
||||
in the repository — so the link is dead until a build is dropped next to the
|
||||
site:
|
||||
`download.html` links to the ISO attached to the release on the project's Forgejo
|
||||
(`https://pandor.pl/git/nexoniarz/SchOS/releases/download/v0.1.0/schos-v0.1.0.iso`). The ISO is
|
||||
hundreds of megabytes and does not belong in the repository, so nothing is stored under
|
||||
`docs/Website/`; publishing a release is `tools/posix/release.sh vX.Y.Z`, and **when the tag
|
||||
changes, change the link and the version beside it** (only the newest release keeps its ISO).
|
||||
|
||||
```sh
|
||||
make iso
|
||||
mkdir -p docs/Website/releases && cp schos.iso docs/Website/releases/
|
||||
```
|
||||
|
||||
The page is written so that this is the only thing that needs doing, and so
|
||||
that nothing on it is a promise the tree cannot keep. In particular the support
|
||||
table says **one real machine, under test** rather than listing hardware,
|
||||
because one Chromebox is the entire bare-metal record and a compatibility list
|
||||
would be fiction. When that changes, change the table; do not soften the
|
||||
wording.
|
||||
The page is written so that nothing on it is a promise the tree cannot keep. In
|
||||
particular the support table lists the three machines SchOS has booted on and nothing else,
|
||||
because that is the entire bare-metal record and a compatibility list would be fiction. When
|
||||
that changes, change the table; do not soften the wording.
|
||||
|
||||
The `dd` command is split across two lines on purpose. It was one long line
|
||||
first, and the card clipped it at `conv=` — a destructive command that can
|
||||
|
|
|
|||
|
|
@ -30,14 +30,19 @@
|
|||
["architecture/syscalls.md", "Syscalls"],
|
||||
["architecture/filesystem.md", "The VFS"],
|
||||
["architecture/schfs.md", "SchFS"],
|
||||
["architecture/scff.md", "SCFF"],
|
||||
["architecture/fat.md", "FAT"],
|
||||
["architecture/terminals.md", "Virtual terminals"],
|
||||
["architecture/devices.md", "Device model"],
|
||||
["architecture/usb.md", "USB"],
|
||||
["architecture/udev.md", "udev"],
|
||||
["architecture/users.md", "Users and permissions"],
|
||||
["architecture/graphics.md", "Graphics"],
|
||||
["architecture/audio.md", "Audio"],
|
||||
["architecture/networking.md", "Networking"],
|
||||
["architecture/init.md", "init"],
|
||||
["architecture/packages.md", "Packages (apk)"],
|
||||
["architecture/desktop.md", "The desktop (Xorg)"],
|
||||
["architecture/ldls.md", "Linux drivers (LDLS)"],
|
||||
["architecture/debugging.md", "Debugging"]
|
||||
]},
|
||||
|
|
@ -51,9 +56,8 @@
|
|||
{ group: "Plan", items: [
|
||||
["plan/README.md", "What plan/ is for"],
|
||||
["plan/roadmap.md", "Roadmap"],
|
||||
["plan/foundations.md", "Foundations"],
|
||||
["plan/wayland.md", "Wayland"],
|
||||
["plan/hybrid.md", "Hybrid kernel"],
|
||||
["plan/gpu.md", "The GPU"],
|
||||
["plan/finch.md", "Finch"],
|
||||
["plan/schapi.md", "SchAPI"]
|
||||
]},
|
||||
{ group: "History", items: [
|
||||
|
|
|
|||
|
|
@ -66,33 +66,34 @@
|
|||
<p class="nv-prose">
|
||||
SchOS ships as one bootable ISO. It boots on UEFI, runs entirely from
|
||||
RAM, and touches nothing on the machine unless you tell it to — there is
|
||||
no installer yet, and that is the point of trying it this way.
|
||||
no installer yet, and that is the point of trying it this way. It comes up with
|
||||
Xorg, Window Maker and Alpine's package manager.
|
||||
</p>
|
||||
|
||||
<div class="nv-cluster">
|
||||
<a class="nv-button nv-button--primary nv-button--lg" href="releases/schos.iso" download>Download the ISO</a>
|
||||
<a class="nv-button nv-button--primary nv-button--lg" href="https://pandor.pl/git/nexoniarz/SchOS/releases/download/v0.1.0/schos-v0.1.0.iso" download>Download the ISO</a>
|
||||
<a class="nv-button nv-button--lg" href="#build">Build it yourself</a>
|
||||
</div>
|
||||
|
||||
<p class="nv-caption">
|
||||
<code class="nv-code">schos.iso</code> · about 62 MB · UEFI x86-64 ·
|
||||
<code class="nv-code">schos.iso</code> · v0.1.0 · a few hundred MB · UEFI x86-64 ·
|
||||
nothing is written to your disks
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- The support table. Deliberately not a compatibility list: one Chromebox
|
||||
is the entire bare-metal record, and a longer table would be fiction. -->
|
||||
<!-- The support table. Deliberately not a compatibility list: three machines
|
||||
are the entire bare-metal record, and a longer table would be fiction. -->
|
||||
<article class="nv-card nv-card--elevated">
|
||||
<div class="nv-card__header">
|
||||
<div>
|
||||
<p class="nv-overline nv-text-tertiary">Before you do</p>
|
||||
<h2 class="nv-card__title">This has booted on exactly one real machine</h2>
|
||||
<h2 class="nv-card__title">This has booted on three real machines</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nv-card__body nv-text-secondary">
|
||||
Everything below QEMU is honest reporting rather than a compatibility
|
||||
list. SchOS drives real hardware, and it has been run on a very small
|
||||
number of it.
|
||||
number of it — from a USB stick, each time.
|
||||
</div>
|
||||
<div class="nv-card__body">
|
||||
<div class="nv-table-frame">
|
||||
|
|
@ -103,8 +104,16 @@
|
|||
<td class="nv-table__actions"><span class="nv-badge nv-badge--success">boots</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Asus Chromebox <span class="nv-text-tertiary">Guado CN62</span></td>
|
||||
<td class="nv-table__actions"><span class="nv-badge nv-badge--warning">under test</span></td>
|
||||
<td>AMD B550 desktop <span class="nv-text-tertiary">Ryzen 5 4600G</span></td>
|
||||
<td class="nv-table__actions"><span class="nv-badge nv-badge--success">boots</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ASUS TUF A15 laptop <span class="nv-text-tertiary">Ryzen, RTL8168H</span></td>
|
||||
<td class="nv-table__actions"><span class="nv-badge nv-badge--warning">boots, network under test</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>HP all-in-one <span class="nv-text-tertiary">Ivy Bridge, 2012</span></td>
|
||||
<td class="nv-table__actions"><span class="nv-badge nv-badge--success">boots</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Anything else</td>
|
||||
|
|
@ -231,11 +240,10 @@
|
|||
<span class="nv-overline nv-text-tertiary">Step three</span>
|
||||
<h3 class="nv-steps__title">A shell</h3>
|
||||
<p class="nv-steps__text">
|
||||
<code class="nv-code">schinit</code> starts one on the console and a
|
||||
second on <code class="nv-code">tty2</code>.
|
||||
<code class="nv-code">mksh</code> is <code class="nv-code">/bin/sh</code>,
|
||||
with <code class="nv-code">zsh</code> beside it and the real toybox
|
||||
userland behind both.
|
||||
<code class="nv-code">schinit</code> starts one on the console and one on
|
||||
each of the next five terminals. <code class="nv-code">zsh</code> is the login shell,
|
||||
<code class="nv-code">/bin/sh</code> is busybox's, and <code class="nv-code">mksh</code> is
|
||||
the static one recovery uses. Type <code class="nv-code">startx</code> for the desktop.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -243,9 +251,9 @@
|
|||
<span class="nv-steps__marker">4</span>
|
||||
<div class="nv-steps__body">
|
||||
<span class="nv-overline nv-text-tertiary">Step four</span>
|
||||
<h3 class="nv-steps__title">Six terminals</h3>
|
||||
<h3 class="nv-steps__title">Twelve terminals</h3>
|
||||
<p class="nv-steps__text">
|
||||
<code class="nv-code">AltGr+F1</code>…<code class="nv-code">F6</code>
|
||||
<code class="nv-code">Ctrl+Alt+F1</code>…<code class="nv-code">F12</code>
|
||||
switches between them — the one chord a Linux host does not take for
|
||||
itself — or <code class="nv-code">chvt N</code> from a shell.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -347,7 +347,7 @@
|
|||
</div>
|
||||
<div class="nv-card__body nv-text-secondary">
|
||||
Six of them, each with its own cell grid, scrollback and foreground
|
||||
job — switched with <code class="nv-code">AltGr+F1</code>…<code class="nv-code">F6</code>,
|
||||
job — switched with <code class="nv-code">Ctrl+Alt+F1</code>…<code class="nv-code">F12</code>,
|
||||
which is the one chord a Linux host does not take for itself. The console
|
||||
draws to whichever framebuffer is present, from the kernel's first line.
|
||||
</div>
|
||||
|
|
@ -398,13 +398,13 @@
|
|||
|
||||
<article class="nv-card">
|
||||
<div class="nv-card__header">
|
||||
<h3 class="nv-card__title"><span class="nv-tile nv-tile--sm" style="--nv-tile-tint: var(--nv-color-warning)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="13" height="10" rx="1.5"/><rect x="8" y="10" width="13" height="10" rx="1.5"/></svg></span>Schcom</h3>
|
||||
<span class="nv-badge nv-badge--warning">in progress</span>
|
||||
<h3 class="nv-card__title"><span class="nv-tile nv-tile--sm" ><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="13" height="10" rx="1.5"/><rect x="8" y="10" width="13" height="10" rx="1.5"/></svg></span>The desktop</h3>
|
||||
<span class="nv-badge nv-badge--success">runs</span>
|
||||
</div>
|
||||
<div class="nv-card__body nv-text-secondary">
|
||||
The compositor that will own the display. v0 runs, and descriptor
|
||||
passing — the one thing a Wayland client cannot do without — now
|
||||
exists. What a buffer still needs is a shareable memory object.
|
||||
Xorg on the framebuffer with Window Maker, <code class="nv-code">xterm</code> and a few X clients;
|
||||
NetSurf for the web. No acceleration yet — the GPU is a plan, and the plan is Linux's own
|
||||
<code class="nv-code">amdgpu</code> through LDLS.
|
||||
</div>
|
||||
</article>
|
||||
|
||||
|
|
@ -415,7 +415,7 @@
|
|||
</div>
|
||||
<div class="nv-card__body nv-text-secondary">
|
||||
The native programming interface — specified, not built, and deliberately
|
||||
so. It should be designed <i>from</i> the compositor's experience rather
|
||||
so. It should be designed <i>from</i> the desktop's experience rather
|
||||
than before it.
|
||||
</div>
|
||||
</article>
|
||||
|
|
@ -549,19 +549,19 @@
|
|||
<dl class="nv-stat-group">
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">lines of code in <code class="nv-code">kernel/</code> and <code class="nv-code">include/</code></dt>
|
||||
<dd class="nv-stat__value">45,207</dd>
|
||||
<dd class="nv-stat__value">51,893</dd>
|
||||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">lines of comments alongside them</dt>
|
||||
<dd class="nv-stat__value">16,111</dd>
|
||||
<dd class="nv-stat__value">17,213</dd>
|
||||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">first-party lines in all</dt>
|
||||
<dd class="nv-stat__value">61,367</dd>
|
||||
<dd class="nv-stat__value">72,766</dd>
|
||||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">syscalls, Linux numbering</dt>
|
||||
<dd class="nv-stat__value">184</dd>
|
||||
<dd class="nv-stat__value">193</dd>
|
||||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">drivers in the kernel</dt>
|
||||
|
|
@ -569,15 +569,15 @@
|
|||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">Alpine packages in the live root</dt>
|
||||
<dd class="nv-stat__value">88</dd>
|
||||
<dd class="nv-stat__value">179</dd>
|
||||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">commits</dt>
|
||||
<dd class="nv-stat__value">253</dd>
|
||||
<dd class="nv-stat__value">292</dd>
|
||||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">days since the first one</dt>
|
||||
<dd class="nv-stat__value">76</dd>
|
||||
<dd class="nv-stat__value">78</dd>
|
||||
</div>
|
||||
<div class="nv-stat">
|
||||
<dt class="nv-stat__label">patches carried against fastfetch</dt>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ its distribution.
|
|||
| Kernel | **SchIFT** | Multi-core x86-64 kernel with a modular VFS |
|
||||
| Filesystem | **SchFS** | Transactional, copy-on-write, the root filesystem |
|
||||
| API | **SchAPI** | The native programming interface — *designed, not built* |
|
||||
| Compositor | **Schcom** | Owns the display; v0 runs |
|
||||
| Packages | **apk** | Alpine's package manager manages the root; SchOS itself is the package `schos-base` |
|
||||
|
||||
Six virtual terminals sit under all of it -- `/dev/tty1`..`tty6`, AltGr+F1..F6
|
||||
or `chvt N` --
|
||||
each with its own screen, line discipline and foreground process group. See
|
||||
Twelve virtual terminals sit under all of it -- `/dev/tty1`..`tty12`, Ctrl+Alt+F1..F12
|
||||
or `chvt N` -- each with its own screen, line discipline and foreground process group. See
|
||||
[terminals.md](terminals.md).
|
||||
|
||||
---
|
||||
|
|
@ -29,9 +28,11 @@ each with its own screen, line discipline and foreground process group. See
|
|||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ toybox kilo mksh zsh musl first-party cmd/ │ userland
|
||||
│ Xorg + Window Maker · Alpine's packages (apk) │ desktop, userland
|
||||
│ toybox mksh musl first-party cmd/ │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ vgpud — a driver that is an ordinary process │
|
||||
│ vgpud, ldlsd — drivers that are ordinary │
|
||||
│ processes (and Linux's r8169 inside ldlsd) │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ schinit — PID 1, job manifests, supervision │
|
||||
├─────────────────────────────────────────────────┤
|
||||
|
|
@ -45,7 +46,8 @@ each with its own screen, line discipline and foreground process group. See
|
|||
├─────────────────────────────────────────────────┤
|
||||
│ /dev/pci — MMIO, DMA and IRQs to a process │ ← the eviction seam
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ drivers: xHCI e1000 virtio-gpu VBE IDE PS/2│
|
||||
│ drivers: xHCI AHCI NVMe e1000 8139 r8169 AC97 │
|
||||
│ virtio-gpu VBE IDE PS/2 I2C-HID │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ scheduler │ paging │ traps │ IRQ │ SMP │ DMA │ SchIFT core
|
||||
└─────────────────────────────────────────────────┘
|
||||
|
|
@ -59,7 +61,8 @@ never names its consumer and a consumer never names a driver. And at
|
|||
**`/dev/pci`**, a driver stops having to be in the kernel at all: a process is
|
||||
handed a device's registers, DMA memory and interrupts, and the in-kernel driver
|
||||
that held it releases it cleanly. `vgpud` is the virtio-gpu driver living out
|
||||
there today. See [../plan/hybrid.md](../plan/hybrid.md).
|
||||
there today, and `ldlsd` runs Linux's own drivers the same way. See
|
||||
[devices.md](devices.md) and [ldls.md](ldls.md).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -91,9 +94,14 @@ Read in this order if you are new:
|
|||
| [fat.md](fat.md) | FAT12/16/32 with long names: USB sticks, SD cards, the ESP. |
|
||||
| [devices.md](devices.md) | Buses, classes, binding, and the vtables consumers talk to. |
|
||||
| [usb.md](usb.md) | xHCI, the USB core, and the three class drivers. |
|
||||
| [graphics.md](graphics.md) | Framebuffer, text rendering, the console, and PSOGM. |
|
||||
| [terminals.md](terminals.md) | The six virtual terminals, and the chord that switches them. |
|
||||
| [networking.md](networking.md) | NIC drivers, lwIP, and both socket families. |
|
||||
| [users.md](users.md) | Accounts, login, and the permissions the kernel enforces. |
|
||||
| [udev.md](udev.md) | eudev on SchOS: the device table, the `/sys` tree, uevents, datagram sockets. |
|
||||
| [graphics.md](graphics.md) | Framebuffer, `/dev/fb0`, text rendering, the console, PSOGM and GMMOS. |
|
||||
| [desktop.md](desktop.md) | Xorg on the framebuffer, Window Maker, and what they stand on. |
|
||||
| [packages.md](packages.md) | The apk root: Alpine's packages, and SchOS as one of them. |
|
||||
| [scff.md](scff.md) | The compressed image the live root ships as. |
|
||||
| [terminals.md](terminals.md) | The twelve virtual terminals, ptys, and handing one to a display server. |
|
||||
| [networking.md](networking.md) | NIC drivers, lwIP, and the socket families. |
|
||||
| [audio.md](audio.md) | `/dev/dsp`, AudioEngine, and the AC'97 driver under it. |
|
||||
| [init.md](init.md) | `schinit`, PID 1 as a role, and the job manifests. |
|
||||
| [debugging.md](debugging.md) | Symbols, a log that outlives the kernel, and asking the kernel a question. |
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ until the speaker has caught up*.
|
|||
**It is one stream.** Mixing several programs into one device is a real feature
|
||||
with a real design behind it, and building it before anything had played a
|
||||
single sound would be exactly the mistake [../plan/1.0.md](../plan/1.0.md)
|
||||
keeps a register for: work that exists and is connected to nothing. The second
|
||||
keeps a register against: work that exists and is connected to nothing. The second
|
||||
program that wants to play at the same time is what should cause it to be
|
||||
written.
|
||||
|
||||
|
|
@ -84,6 +84,25 @@ Both register files are **I/O ports**, not MMIO — which is why nothing in the
|
|||
driver maps anything. `bar[n].is_io` is what says so, and a driver that
|
||||
`ioremap`ed these would be mapping a port number as though it were an address.
|
||||
|
||||
### Through ALSA, for programs that expect it
|
||||
|
||||
SDL 3 has no OSS backend and SchOS has no ALSA *kernel* side, so sound from SDL games
|
||||
and anything else that speaks ALSA goes **SDL → alsa-lib → alsa-plugins' OSS plugin →
|
||||
`/dev/dsp`**. `alsa-lib` and `alsa-plugins` are in the Alpine `world`, and
|
||||
`/etc/asound.conf` (from `schos-base`) makes the default device a `plug` onto the OSS
|
||||
device at 16-bit stereo 48 kHz, which is what every SchOS audio driver plays.
|
||||
|
||||
Running that path found two bugs underneath it:
|
||||
|
||||
- **AC'97:** resetting the PCM-out box also clears the descriptor-list address and nothing
|
||||
set it again, so every stream after the first stop played a "list" at physical address 0
|
||||
— silence — and ALSA resets before each stream. If the engine halts at the end of the
|
||||
queue, the run bit is set again.
|
||||
- **`/dev/dsp` was missing the ioctls the plugin's ring-and-pointer model uses:**
|
||||
`SETFRAGMENT`, `GETFMTS`, `GETCAPS`, `SETTRIGGER`, `GETOPTR`, `GETODELAY`, and `poll()`,
|
||||
writable once a whole fragment fits (ALSA spun at 76% CPU otherwise). `speaker-test` now
|
||||
plays in real time at 0% CPU, verified in a recording of QEMU's AC'97.
|
||||
|
||||
## How it is tested
|
||||
|
||||
QEMU's `wav` audio backend writes what the guest plays to a file, so the test
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ because there are none.** Everything the jump needs is computed before.
|
|||
### The hand-off block
|
||||
|
||||
One page of low memory at `0x30000`, defined once in
|
||||
[`include/arch/schboot.h`](../../include/schboot.h) and included by both the loader
|
||||
[`include/arch/schboot.h`](../../include/arch/schboot.h) and included by both the loader
|
||||
and `entry.S` so the two cannot disagree.
|
||||
|
||||
| Offset | What |
|
||||
|
|
@ -131,6 +131,42 @@ when the early console is set up. So a low framebuffer is attached exactly as
|
|||
before, and a high one is attached a few dozen instructions later, immediately
|
||||
after `kvmalloc()` — still thousands of lines before anything else happens.
|
||||
|
||||
### Where the kernel goes, and how it gets there
|
||||
|
||||
The kernel is linked at physical **16 MB** (`KERNPHYS` in `memlayout.h`, repeated in
|
||||
`kernel.ld`, which must agree) and is about 34 MB with the ramdisk inside it. From the
|
||||
old 1 MB it ran across 8 MB, where OVMF keeps ACPI NVS memory that outlives the firmware,
|
||||
so it was overwritten on every boot, after `ExitBootServices`. Firmware leaves its
|
||||
leftovers low; above 16 MB is where a machine is most reliably empty. (The 15 MB below
|
||||
the kernel is not given to the page allocator yet.)
|
||||
|
||||
**Every free page of that range is claimed first.** A segment lying wholly on claimed
|
||||
pages is read straight home; one that is not is read into a buffer elsewhere and
|
||||
**copied home after `ExitBootServices`**, when boot-services memory is ours. An HP
|
||||
all-in-one from 2012 used to freeze at the start of the loading bar because the loader
|
||||
said "could not reserve" and read the kernel there anyway, over the firmware's own memory
|
||||
with the firmware still running on it. Memory under the kernel that outlives the firmware
|
||||
(ACPI NVS, runtime services) is named, and so is the loader or its stack sitting in the
|
||||
range. Segments are read a megabyte at a time, so the bar moves through the ramdisk and
|
||||
shows where a machine stopped.
|
||||
|
||||
The copy and the jump themselves run from **one page below the kernel's lowest address**,
|
||||
allocated as loader code before the firmware exits: a short position-independent routine,
|
||||
the copy list and a stack, switching to the loader's page tables and its own stack before
|
||||
it copies anything. Doing the copy from code the firmware had placed wherever it liked put
|
||||
some of it inside the kernel's range on that machine: a triple fault and a reset. The
|
||||
routine is `sysv_abi`, because UEFI code is the Microsoft convention and the first try loaded
|
||||
the framebuffer address into `rsp`.
|
||||
|
||||
**No 1 GB pages, no problem.** Intel's desktop CPUs before Haswell have none, and there the
|
||||
PS bit in a PDPT entry is reserved: the first access after the switch is a page fault that
|
||||
cannot be handled either. Both the loader and the kernel check CPUID `0x80000001` EDX bit 26;
|
||||
without it the low 4 GB is four page directories of 2 MB pages (placed below the kernel, with
|
||||
the staged segments kept below 4 GB), and the kernel gives each gigabyte of its direct map
|
||||
and of `ioremap()` a page directory of its own. Neither KVM nor QEMU enforces the reserved
|
||||
bit, so the fault could not be reproduced here; with the fallback forced on, the kernel
|
||||
boots at 2 and 8 GB and both CPUs start.
|
||||
|
||||
### The loader jumped out from under itself
|
||||
|
||||
A machine with a lot of RAM froze at "Starting the kernel" — the splash still
|
||||
|
|
@ -294,9 +330,64 @@ Dropping back to protected mode to re-enter through the 32-bit multiboot path
|
|||
is a mode switch whose failure mode is a triple fault with no console. The
|
||||
32-bit entry is still present in `entry.S` and now has no caller.
|
||||
|
||||
### The boot manager
|
||||
|
||||
F8 (or `o` on a serial line) opens the boot manager. It used to be a text list
|
||||
on a dark plate; it is now drawn in the Niveus language the site is drawn in
|
||||
(`docs/Website/niveus`): a window with a title bar, keys with a lit upper edge
|
||||
and a shadow beneath, panels cut into the surface as wells, a graphite ground
|
||||
that darkens towards the bottom, and the mark above the list.
|
||||
|
||||
What makes that possible in a loader is that nothing in it is large:
|
||||
|
||||
- **Shapes** are rounded rectangles with a vertical gradient. Corners are
|
||||
anti-aliased by supersampling only the corner pixels, against a colour the
|
||||
caller names. Nothing reads the framebuffer back, because that memory is
|
||||
uncached on real hardware and a read per pixel is the difference between a
|
||||
menu that appears and one that crawls. (The one exception is the pointer,
|
||||
which saves sixteen by nineteen pixels.)
|
||||
- **Type** is Instrument Sans, the face Niveus ships. A TrueType rasteriser
|
||||
does not belong in a loader, so `tools/python/mknvfont.py` renders it once
|
||||
into `include/gfx/nvfont.h` as 8-bit coverage bitmaps with fractional
|
||||
advances, at the size for ordinary screens and at twice that for screens 1400
|
||||
pixels tall or more. Rasterising at the real size is what keeps it crisp;
|
||||
doubling pixels does not.
|
||||
- **Layout** is computed from the item list, not drawn from fixed positions: a
|
||||
list taller than the screen drops the mark, then scrolls with a bar.
|
||||
|
||||
An entry is a label, a newline and a line of detail. The entries are:
|
||||
|
||||
| Entry | Does |
|
||||
|---|---|
|
||||
| Boot SchOS / (verbose) | the two ways of starting this system |
|
||||
| other systems | one entry per loader found at a well-known path on any volume: Windows Boot Manager, Ubuntu, Debian, Fedora, Arch, openSUSE, systemd-boot, rEFInd, and any other `\EFI\BOOT\BOOTX64.EFI` that is not this one. Chosen, they are started with the firmware's own `LoadImage`/`StartImage`, and if the other loader returns, the menu is still here |
|
||||
| Resolution | the list of modes the firmware offers, or a typed size, with the "keep this?" confirmation |
|
||||
| System Information | firmware, UEFI revision, processor, memory, display, ACPI |
|
||||
| Firmware Setup | sets `OsIndications` and restarts into the firmware's own settings; on firmware that does not support it, says so instead of restarting for nothing |
|
||||
| Restart, Shut Down | `ResetSystem` |
|
||||
| Remote debugging | `make DEBUG=1` builds only |
|
||||
|
||||
Keyboard, mouse, touchpad and touchscreen all drive it: both UEFI pointer
|
||||
protocols are polled (a mouse reports movement, a tablet reports position),
|
||||
hover moves the highlight, a click activates, and the wheel scrolls. All input
|
||||
goes through one function, `wait_event()`, so every screen behaves the same way.
|
||||
The cursor stays hidden until the pointer has actually done something, because
|
||||
the firmware's console layer installs a pointer protocol whether or not a mouse
|
||||
is attached. USB devices appear some time after their controller is connected
|
||||
and the menu is already up by then, so the loader connects every controller when
|
||||
the menu opens and looks for pointers again twice a second for ten seconds.
|
||||
|
||||
The "keep this resolution?" confirmation counts ten seconds down once, however
|
||||
many times the pointer moves over the button: a person with a garbled screen
|
||||
waving a mouse around must not be able to keep postponing the revert.
|
||||
|
||||
**Not verified:** hover and click were exercised with synthetic events. Under
|
||||
QEMU and OVMF a USB mouse or tablet never delivered an event to the loader, so
|
||||
the pointer path has not been seen working on a device -- real hardware first.
|
||||
|
||||
### The menu is on the serial line too
|
||||
|
||||
The three F8 screens paint into the framebuffer, which is the right place for
|
||||
The screens paint into the framebuffer, which is the right place for
|
||||
them on a machine with a screen and nowhere at all on one driven over a serial
|
||||
cable — which is how this loader is usually driven. `f8_window()` already went
|
||||
out of its way to support that, accepting `o` as well as F8 because *"a serial
|
||||
|
|
@ -315,7 +406,7 @@ its text across the menu that was just painted there.
|
|||
Two details that are not cosmetic:
|
||||
|
||||
- **The item number is shown only where it is a key you can press.** The boot
|
||||
menu takes 1–5. The resolution list takes only W/S and Enter and is longer
|
||||
menu takes 1–9. The resolution list takes only W/S and Enter and is longer
|
||||
than nine entries, so numbering it printed `:`, `;`, `<` past the ninth — a
|
||||
character nobody can type, offering to select something.
|
||||
- **Q goes back as well as Esc.** On a serial line a lone `ESC` byte is
|
||||
|
|
@ -610,7 +701,9 @@ in the one process that runs first.
|
|||
|
||||
## What it does not do
|
||||
|
||||
- **No BIOS path.** A machine without UEFI does not boot SchOS.
|
||||
- **No BIOS path.** A machine without UEFI does not boot SchOS. The ISO carries a real EFI
|
||||
system partition (MBR type `0xEF`, FAT16 by cluster count, the type old firmware reads best),
|
||||
so a stick written with `dd` or `write.sh` boots on strict firmware as well as on lenient.
|
||||
- **The quiet boot hides a slow one.** With nothing printing between the
|
||||
loader and the greeting, a boot that stalls for ten seconds looks the same as
|
||||
one that does not. F8 is the answer and it has to be pressed *before* the
|
||||
|
|
@ -621,9 +714,11 @@ in the one process that runs first.
|
|||
with its partitions, but nothing mounts it. See [usb.md](usb.md).
|
||||
- **NVMe and AHCI poll for completion**, one command at a time per disk. That
|
||||
is enough to boot and run from them, not to be fast.
|
||||
- **`make ramroot` is the only way to boot with no data disk.** It links a
|
||||
filesystem image into the kernel binary, which costs boot time and carries a
|
||||
licensing constraint — see [../licensing.md](../licensing.md) §4.
|
||||
- **The live ISO is a ramdisk root.** `make iso` builds the root as a SchFS image,
|
||||
compresses it ([scff.md](scff.md)) and links it into the kernel; nothing is written to
|
||||
the machine's disks, and everything written to the root is gone at reboot. There is
|
||||
no installer, and no kernel that finds a root on a disk by SchFS's GPT type as its
|
||||
*default*. `make ramroot` is the same without compression.
|
||||
- **The 32-bit multiboot entry is dead code.** It is left in place deliberately:
|
||||
deleting kernel entry code is its own piece of work with its own way of
|
||||
failing silently.
|
||||
|
|
|
|||
|
|
@ -29,10 +29,10 @@ asked for one. That is the right default and it has one bad consequence —
|
|||
to enable it.** A laptop with no serial port that stops before it can draw has
|
||||
nowhere to say why, and `schinitd enable` needs a prompt that does not exist.
|
||||
|
||||
So a build made with `make DEBUG=1` has a sixth entry in the F8 menu:
|
||||
So a build made with `make DEBUG=1` has one more entry at the end of the F8 menu:
|
||||
|
||||
```
|
||||
> 6. Remote debugging: off
|
||||
> Remote debugging: off
|
||||
```
|
||||
|
||||
Picking it toggles the entry, booting carries the choice through
|
||||
|
|
@ -179,6 +179,10 @@ part of the boot is blank for the part that is hardest to debug any other way.
|
|||
|
||||
---
|
||||
|
||||
A panic is not the end of the log. GMMOS ([graphics.md](graphics.md)) keeps running
|
||||
under the report, can show this log on screen or as QR codes a phone can read, and
|
||||
has a small terminal for looking around a machine whose kernel is dead.
|
||||
|
||||
## Asking a question
|
||||
|
||||
`/dev/dbg` is a conversation: write a line, read the answer.
|
||||
|
|
@ -493,10 +497,16 @@ something that reports nonsense.
|
|||
- **One conversation at a time.** The answer buffer is global, so two programs
|
||||
asking at once get one answer each in some order — never half of the other's,
|
||||
but not two independent sessions either.
|
||||
- **No userspace symbolization.** The kernel names kernel symbols. A fault in a
|
||||
process still reports a bare address, and resolving it is a host-side job.
|
||||
- **The stack walk stops where the frame pointer stops.** `getcallerpcs()` walks
|
||||
`rbp` chains, so a trap frame in the middle of a chain ends the walk.
|
||||
- **No userspace symbolization.** The kernel names kernel symbols. A process
|
||||
killed by a fault is told which *file and offset* it died in (from
|
||||
`/proc/<pid>/maps`' records), which is enough to resolve with the file's own
|
||||
symbols on the host; a bare address is all there is for anonymous memory.
|
||||
- **The stack walk stops where the frame pointer stops, and it will not follow a
|
||||
bad one.** `getstackpcs()` walks `rbp` chains, and each link must go up the same
|
||||
stack by less than `KSTACKSIZE`: a chain can end at the syscall entry with a *user*
|
||||
`rbp` that a program uses as an ordinary register (`-1024` in one game), and
|
||||
dereferencing that under the syscall tracer once panicked the kernel inside
|
||||
`acquire()`. A trap frame in the middle of a chain ends the walk.
|
||||
- **`sysrq g` costs you log history.** Its output goes through `cprintf`, so
|
||||
sixteen kilobytes of views push the same amount out of a 128 KB ring.
|
||||
- **No tracing.** No static tracepoints, no event ring, no counters. "What
|
||||
|
|
|
|||
79
docs/architecture/desktop.md
Normal file
79
docs/architecture/desktop.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# The desktop: Xorg on the framebuffer
|
||||
|
||||
The desktop is X11, on the framebuffer: Alpine's `xorg-server` with the `fbdev` video
|
||||
driver and the `evdev` input driver, Window Maker as the window manager, and `xterm`.
|
||||
`startx` runs `wmaker`. Nothing here is written by SchOS except the kernel interfaces it
|
||||
stands on and four small files in `tools/posix/mkaroot.sh`.
|
||||
|
||||
**Xorg is the desktop for now, not for ever.** SchOS will not write its own compositor (the one it had, Schcom, was deleted).
|
||||
What comes next is whichever of XLibre (the Xorg fork), Xorg or a Wayland compositor from Alpine suits the machine; the groundwork
|
||||
Wayland needs — udev, `/sys/devices`, libinput, a DRM device — is in [../plan/roadmap.md](../plan/roadmap.md) §5.
|
||||
|
||||
## Why X and not Wayland
|
||||
|
||||
Every Wayland compositor worth running — labwc, and everything built on wlroots — needs
|
||||
DRM/KMS, libinput, udev and libseat. DRM/KMS is the largest of them by far: a kernel graphics
|
||||
subsystem, and in practice a great deal taken from Linux. Xorg runs on a plain framebuffer
|
||||
through `fbdev`, which SchOS already had. That is the shorter road to a desktop by a long way,
|
||||
and it brings the window managers with it. udev, libinput and libseat are now in the image
|
||||
([udev.md](udev.md)); DRM/KMS is the part still missing, and it is the GPU plan
|
||||
([../plan/gpu.md](../plan/gpu.md)).
|
||||
|
||||
## What it stands on
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **fbdev** | `/dev/fb0` speaks Linux's fbdev ([graphics.md](graphics.md#devfb0-and-how-xorg-draws)) |
|
||||
| **evdev** | `/dev/input/event0` (every keyboard) and `event1` (every mouse and touchpad) with Linux's queries, LED writes and key/button/relative-axis bits |
|
||||
| **VTs** | `/dev/tty0`, `VT_SETMODE(VT_PROCESS)`, `KD_GRAPHICS`, the release/acquire handshake ([terminals.md](terminals.md)) |
|
||||
| **dlopen** | Xorg loads its drivers as modules, so `exec` does dynamic linking ([kernel.md](kernel.md)) |
|
||||
| **sysfs** | `/sys/bus/pci/devices/*/{boot_vga,enable,graphics/fb0}`, which libpciaccess reads |
|
||||
| **ptys** | `xterm` runs its shell on a pseudo-terminal |
|
||||
| **Unix sockets** | the X11 socket, abstract names, `SO_PEERCRED`, `flock` |
|
||||
| **headers** | `<linux/*.h>` in the build sysroot, as `ports/linux-headers` |
|
||||
|
||||
`cmd/xready` checks every one of the kernel calls Xorg makes before it draws anything — the
|
||||
evdev queries, the fbdev calls, the console calls, and the `/sys` files — one at a time, on a
|
||||
terminal of its own, and puts everything back. The calls were written against what the
|
||||
programs are documented to do; each step past it found one that was not, which is what running
|
||||
the real program is for.
|
||||
|
||||
## The four files
|
||||
|
||||
- **`/root/.xserverrc`** starts `X` on the VT `startx` was started from (startx works that
|
||||
out only when `uname -s` says Linux, and SchOS's says SchIFT). It used to add **`-dumbSched`**,
|
||||
Xorg's switch for reading input on the main loop, because Xorg normally reads input on a
|
||||
thread of its own and, when SchOS had no threads, the server crashed the first time a client
|
||||
disconnected, in `pthread_join` on a thread that was never made. Threads exist now and the switch
|
||||
is gone: under QEMU, X starts with Window Maker, `xclock` and `xeyes` connect and disconnect, an
|
||||
`xterm` runs and exits, and the server stays up.
|
||||
- **`/root/.xinitrc`** is `exec wmaker`. Without it xinit falls back to twm and an xterm, which
|
||||
are not in the image.
|
||||
- **`/etc/X11/xorg.conf.d/10-schos.conf`** holds what the defaults do not give: Ctrl+Alt+Backspace
|
||||
enabled, the keyboard options, and the framebuffer driver. Input devices are *found*, not
|
||||
named: Xorg asks libudev, which reads what `udevd` wrote to `/run/udev` ([udev.md](udev.md)).
|
||||
- **`/etc/asound.conf`** is for sound, not X ([audio.md](audio.md)).
|
||||
|
||||
## Using it
|
||||
|
||||
Twelve virtual terminals, switched with Ctrl+Alt+F1–F12 — on the console and under X
|
||||
alike, because X owns the keyboard while it runs and switches only with that chord. `startx` on
|
||||
a terminal runs the desktop there; Ctrl+Alt+Backspace ends the session and gives the terminal
|
||||
back. Window Maker, `xterm`, `xeyes`, `xclock`, `xcalc`, DejaVu fonts; SDL 2 games
|
||||
(`lbreakouthd`) and NetSurf, a GTK 3 browser, run in it.
|
||||
|
||||
Touchpads that sit on an I2C bus (most laptops) are driven by `kernel/acpi/i2c_hid.c`, found
|
||||
through ACPI, and reach X as a mouse: pointer, click and tap.
|
||||
|
||||
## What it does not do
|
||||
|
||||
- **No acceleration.** Software rendering to the framebuffer: no DRM, no glamor, no DRI, no
|
||||
OpenGL beyond what software gives. GPU drivers are a plan ([../plan/gpu.md](../plan/gpu.md)).
|
||||
- **udev covers the kernel's own devices only.** Input, terminals, memory devices, the
|
||||
framebuffer and disks are announced; PCI and USB devices are in `/sys/bus/*/devices` but are
|
||||
not yet in the `/sys/devices` tree or on the uevent socket ([udev.md](udev.md)).
|
||||
- **Touchpads are mice**: no two-finger scroll, no gestures, no palm rejection.
|
||||
- **No resolution change from inside X.** The mode is the console's; `schmode` changes the
|
||||
`vgpud` head only, and the loader's menu is where a real panel's mode is chosen.
|
||||
- **Runs as an ordinary user** where there is one: `startx` works by group permission alone
|
||||
([users.md](users.md)); the image has only root; X itself is still a single seat with no logind or seatd.
|
||||
|
|
@ -4,7 +4,61 @@ How hardware gets found, claimed and used, and what the rest of the kernel sees
|
|||
of it.
|
||||
|
||||
Writing a driver is [../guides/adding-a-driver.md](../guides/adding-a-driver.md).
|
||||
Moving drivers out of the kernel entirely is [../plan/hybrid.md](../plan/hybrid.md).
|
||||
Drivers that live in a process rather than in the kernel are described below
|
||||
(*Drivers that are not in the kernel*), and the foreign ones in [ldls.md](ldls.md).
|
||||
|
||||
---
|
||||
|
||||
## Which drivers are compiled in
|
||||
|
||||
`config/drivers.conf` lists the drivers that can be left out of a kernel, `y` (compiled in and offered every device it can
|
||||
drive) or `n` (not linked at all), all `y` unless a line says otherwise — the one exception is the native `r8169`, `n`
|
||||
because Linux's runs in its place under LDLS. Override one for a build with `make DRV_E1000=n DRV_NVME=n`. A changed
|
||||
choice rebuilds only `registry.o` and the link. The list is short on purpose: a driver is optional only if nothing else in
|
||||
the kernel calls it (display: `vga_pci`, `virtio_gpu`; network: `e1000`, `rtl8139`, `r8169`; storage: `ahci`, `nvme`;
|
||||
audio: `ac97`). `vga_fb`, the USB stack and the legacy PS/2, IDE and UART devices are always there, because the console, the
|
||||
keyboard and the root disk can depend on them. A kernel with every optional driver off builds and links; whether such a
|
||||
kernel is useful on any particular machine is the person who turned them off to say.
|
||||
|
||||
## What stays in the kernel, and what may leave
|
||||
|
||||
"Hybrid" is used to mean everything from "a monolith with a nice device model" to
|
||||
"a microkernel that gave up", so the line is worth drawing exactly.
|
||||
|
||||
**Stays in, permanently:** scheduling, address spaces, the trap and syscall path;
|
||||
IPC and shared memory; the VFS and the **root** filesystem; IRQ demultiplexing; and
|
||||
the console and PSOGM. That last one is not a size compromise but an invariant: the
|
||||
crash screen must work when everything else is dead, so it cannot depend on anything
|
||||
that could *be* dead. A crash screen served by a userspace process is absent in
|
||||
exactly the case it exists for.
|
||||
|
||||
**May leave, and should:** GPU drivers, non-root filesystems, NICs after the first,
|
||||
and anything written by someone else.
|
||||
|
||||
> **Anything needed to reach a shell stays in. Anything discovered at runtime,
|
||||
> replaceable, or plausibly written by someone else goes out.**
|
||||
>
|
||||
> **The kernel keeps a minimal driver for everything on the path to a shell, and
|
||||
> userspace owns the good one.** Break that and the compositor cannot start because
|
||||
> the display driver is a process that needs a compositor.
|
||||
|
||||
### USB stays in the kernel, and why
|
||||
|
||||
It was on the list to leave, and the premise was wrong. USB is not a peripheral; it
|
||||
is the bus peripherals arrive on, structurally the same kind of thing as PCI, and
|
||||
nobody has proposed evicting PCI. The keyboard and the root disk that arrive over it
|
||||
are on the path to a shell, so the stack that finds them is part of that path. Even
|
||||
with a minimal in-kernel fallback written, the good driver and the minimal one would
|
||||
be the same driver, and an eviction that leaves an identical copy behind has moved
|
||||
nothing. A second, quieter difference: `/dev/pci` hands over one PCI *function*, one
|
||||
owner and nothing shared, while a USB device sits behind a controller other devices
|
||||
also sit behind — "the xHCI controller" includes the keyboard. What the eviction was
|
||||
for, third-party drivers living outside the kernel, survives and belongs to
|
||||
[ldls.md](ldls.md).
|
||||
|
||||
The root filesystem stays in the kernel for the same reason (SchFS cannot be
|
||||
restarted, and a bug in it is fatal), and so does the first NIC, because the network
|
||||
thread and lwIP are in the kernel.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -148,7 +202,7 @@ bitmap, and loses them when it releases or exits. Granting it is not optional
|
|||
a driver's `outl` is an instruction compiled into it, not a call anything can
|
||||
intercept — and granting it *coarsely* is not acceptable, because `IOPL=3` is
|
||||
one flag that hands over the interrupt controller and the disk along with the
|
||||
NIC. See [../../include/ioperm.h](../../include/ioperm.h).
|
||||
NIC. See [../../include/arch/ioperm.h](../../include/arch/ioperm.h).
|
||||
|
||||
### Handing a device over
|
||||
|
||||
|
|
@ -184,8 +238,47 @@ client — and that is worth a sentence here because it is the thing the device
|
|||
model makes possible rather than the thing it does. A modeset is five commands
|
||||
to the device. In the kernel those five commands are also a reason to be
|
||||
nervous, because a wrong one takes the machine; in a process the worst case is
|
||||
that `schinit` starts another one. See
|
||||
[../plan/hybrid.md](../plan/hybrid.md), "A modeset, from a process".
|
||||
that `schinit` starts another one. That is why the in-kernel driver never grew
|
||||
one — not difficulty, exposure.
|
||||
|
||||
```
|
||||
# schmode
|
||||
1280x800 current
|
||||
1280x800 maximum, framebuffer 4000 KB
|
||||
# schmode 1024 768
|
||||
1024x768 ok
|
||||
```
|
||||
|
||||
The scanout really resizes, and nothing in the kernel changed for it: `schmode`
|
||||
writes a line to a socket and `vgpud` sends five commands to the device. The
|
||||
framebuffer is allocated **once, at the largest mode**, and every smaller mode
|
||||
reuses it, because `PCIIOC_DMA` has no free — a driver's blocks come back when it
|
||||
exits and not before — so a modeset that allocated would spend the arena and the
|
||||
failure would look like a GPU bug rather than like counting. A larger mode is
|
||||
refused, and says why. The control socket speaks lines of text and has no buffer
|
||||
submission, damage or vblank, which a compositor would need and nothing has asked
|
||||
for.
|
||||
|
||||
### A panic has to reach the screen
|
||||
|
||||
Handing a device away costs something that was not on the list. The kernel panics,
|
||||
paints the report into the console framebuffer, and halts — and the display it gave
|
||||
to a process keeps showing whatever that process last put there. On a machine with
|
||||
two heads, one says the kernel is dead and the other shows a shell prompt. There is
|
||||
no scheduling after a panic, so the process holding the device is never going to run
|
||||
again, and the kernel let go of the device deliberately.
|
||||
|
||||
So the driver says in advance where it is scanning out, with `PCIIOC_PANICFB`
|
||||
(`include/dev/pciuser.h`), and the kernel: (1) paints the report into that memory;
|
||||
(2) asks the *kernel* driver the device was taken from to put it on screen —
|
||||
`panic_present` in `struct pci_driver`; (3) which resets the device and rebuilds one
|
||||
queue from **static** memory — no allocation, no locks, no sleeping, because a
|
||||
corrupted heap is one of the ways a machine gets here. The registration is checked
|
||||
like the promise it is: the range must lie inside a block that process got from
|
||||
`PCIIOC_DMA` on that device, or it would be a way to nominate any physical address
|
||||
to be overwritten at panic. It costs 20 KB of static memory in the virtio-gpu driver,
|
||||
paid whether or not the machine ever panics; allocating it on demand would be
|
||||
cheaper and would not work.
|
||||
|
||||
### Taking it back
|
||||
|
||||
|
|
@ -222,8 +315,9 @@ enforces it.
|
|||
Coming back is a job, not a kernel feature. `etc/schinit/vgpud.job` carries
|
||||
`KeepAlive = yes`, so killing the driver is the entire test: `schinitd` starts
|
||||
another one, it claims the device, and the pid of whichever process drew the
|
||||
frame is legible in the frame itself. See
|
||||
[../plan/hybrid.md](../plan/hybrid.md), "The driver came back".
|
||||
frame is legible in the frame itself: read off the scanout across four generations of
|
||||
the driver it said 4, 8, 10, 12, and the DMA arena handed back the same addresses
|
||||
every time.
|
||||
|
||||
### What this is not
|
||||
|
||||
|
|
@ -231,8 +325,7 @@ frame is legible in the frame itself. See
|
|||
buffer can point the device at any physical address. This isolates a driver
|
||||
against its own *bugs* and not against its own *intent* — a crashed driver that
|
||||
restarts instead of taking the machine down is the entire benefit, and it is
|
||||
worth having on those terms. [../plan/hybrid.md](../plan/hybrid.md) says so at
|
||||
length.
|
||||
worth having on those terms. VT-d/AMD-Vi is its own project.
|
||||
|
||||
**One device per process.** A driver drives one thing; if you need two, run two.
|
||||
|
||||
|
|
@ -240,29 +333,22 @@ length.
|
|||
|
||||
## Linux drivers
|
||||
|
||||
**Decided 2026-08-08.** Linux drivers are not shipped with SchOS and never
|
||||
will be. The user may install one on their own machine, through a device
|
||||
manager or equivalent, and SchOS will load it.
|
||||
[ldls.md](ldls.md) is how SchOS uses them: a Linux `.ko` runs in a process and
|
||||
reaches the hardware through `/dev/pci`. The licensing position, stated exactly
|
||||
because it is easy to get wrong in both directions:
|
||||
|
||||
The licensing reasoning matters and is worth stating exactly, because it is
|
||||
easy to get wrong in both directions:
|
||||
- **Distribution is what triggers the GPL.** SchOS is GPL-2.0-or-later, so shipping a
|
||||
GPL-2.0-only driver in an image is *lawful* — see [../licensing.md](../licensing.md) §6 —
|
||||
and the owner has decided it is wanted (code and firmware blobs). The ISO ships Linux's
|
||||
r8169 set. The repository holds no binary: `.gitignore` refuses `*.ko`, and
|
||||
`make ldls-r8169` stages the modules from a Linux build, where the matching BTF is.
|
||||
- **Implementing a loader is not copying.** Writing support for a module format and
|
||||
a driver ABI from published documentation produces our own work; the layouts come
|
||||
from the kernel's BTF at load time on the user's machine, so the source holds none
|
||||
of Linux's.
|
||||
|
||||
- **Distribution is what triggers the GPL.** SchOS is GPL-2.0-or-later, so
|
||||
shipping a GPL-2.0-only driver in an image is now *possible* — see
|
||||
[../licensing.md](../licensing.md) §6 — but LDLS still does not, by policy. A
|
||||
user obtaining a driver themselves and loading it on their own machine is
|
||||
their act, not ours, and is not a distribution by us.
|
||||
- **Implementing a loader is not copying.** Writing support for a module format
|
||||
and a driver ABI from published documentation produces our own work. It is a
|
||||
clean-room implementation, and it stays one only if it is written from
|
||||
documentation rather than transcribed from Linux source. Reading Linux to
|
||||
understand *hardware* is fine; translating its code is not, whatever the
|
||||
identifiers say.
|
||||
|
||||
So the rule for this subsystem is the same as everywhere else in the tree: the
|
||||
loader is ours and GPL-2.0-or-later, what it loads is the user's business, and
|
||||
no third-party module enters an image we build. `RAMDISK_EXCLUDE` and `NOTPORTS` are the
|
||||
existing mechanisms for keeping that true mechanically rather than by intention.
|
||||
The loader is ours and GPL-2.0-or-later; what it loads is Linux's, unmodified, with its own
|
||||
licence and source recorded in `NOTICE`.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -371,12 +457,13 @@ the console's minors to the console and the rest to their own functions. But
|
|||
- **No hot-plug.** Devices are registered at boot and never removed; the
|
||||
`in_use` flag is the hook a hot-unplug path would need, and nothing sets it
|
||||
for that purpose.
|
||||
- **`NDEVICE` is 32**, compile-time, like every other table in the kernel.
|
||||
- **`NDEVICE` is 128**, compile-time, like every other table in the kernel.
|
||||
- **In-kernel drivers are fixed at link time.** There is no module loader, and
|
||||
nothing can be added to the kernel after the build. A driver *outside* the
|
||||
kernel is an ordinary program and can be started whenever — but it reaches its
|
||||
device through `/dev/pci` and publishes nothing to the class vtables, so the
|
||||
console cannot be handed to one yet.
|
||||
kernel is an ordinary program and can be started whenever — it reaches its
|
||||
device through `/dev/pci`, and a NIC driver can publish itself to the network
|
||||
class through `/dev/netdev` (LDLS does); a display driver publishes nothing to
|
||||
`display_ops`, so the console cannot be handed to one.
|
||||
- **The kernel does not rebind its own driver.** `PCIIOC_RELEASE`, or the process
|
||||
exiting, leaves the interrupt line masked and the in-kernel driver unbound —
|
||||
the console stays wherever the release put it. A *supervisor* can start a
|
||||
|
|
|
|||
|
|
@ -199,11 +199,14 @@ provides — reads and writes never reach it, because `fileread`/`filewrite` see
|
|||
It is read-only: `dirlink` and `ialloc` answer `EROFS` rather than pretending.
|
||||
A node exists because the kernel has the device, or it does not exist.
|
||||
|
||||
### What is there
|
||||
### What is there in `/dev`
|
||||
|
||||
`console`, `null`, `keyboard`, `zero`, `full`, `random`, `urandom`, `tty`,
|
||||
`stdin`, `stdout`, `stderr`, `fb0`, `mouse`, `hda`, `hdb`, and `input/event0`
|
||||
and `input/event1` in one subdirectory.
|
||||
`console`, `null`, `zero`, `full`, `random`, `urandom`, `tty`, `tty0` and
|
||||
`tty1`..`tty12`, `stdin`, `stdout`, `stderr`, `keyboard`, `mouse`, `fb0`, `dsp`,
|
||||
`ptmx` and a `pts/` directory of pseudo-terminal slaves, `hda`, `hdb`,
|
||||
`input/event0`.., and the SchOS-specific `pci` (a device's registers, DMA and
|
||||
interrupts, for a driver in a process), `netdev` (a network interface for a
|
||||
process), `dbg` and `dbgro` (the kernel debugger's views).
|
||||
|
||||
Every disk and partition the block layer registered is there too, under the
|
||||
name it was given (`sata0`, `sata0p2`, `nvme0p1`, `usb0p1`): major 8, minor its
|
||||
|
|
@ -218,11 +221,18 @@ nothing behind it fails with `ENXIO`, which is a better answer than a missing
|
|||
path — and with no node at all, `echo x > /dev/hdb` silently creates an ordinary
|
||||
file of that name and appears to work.
|
||||
|
||||
### What is there
|
||||
### What is there in `/proc`
|
||||
|
||||
Per-process directories, plus the global files: `meminfo`, `uptime`, `cpuinfo`,
|
||||
`version`, `stat`, `loadavg`, `mounts`, `filesystems`, `schfs`, `pci`,
|
||||
`interrupts`, `devices`, `netif`, and `sysrq`.
|
||||
`/proc/self`, and one directory per process holding `stat`, `status`, `cmdline`,
|
||||
`statm`, `comm`, `schedstat`, `maps`, `exe` (a symlink), `fd/` (a symlink per open
|
||||
descriptor) and `task/` (one directory per thread). `maps` lists the file
|
||||
mappings, and a process killed by a fault is told which file and offset it died
|
||||
in.
|
||||
|
||||
The global files: `meminfo`, `uptime`, `cpuinfo`, `version`, `stat`, `loadavg`,
|
||||
`mounts`, `filesystems`, `partitions`, `schfs`, `pci`, `interrupts`, `devices`,
|
||||
`netif`, `resolv.conf`, `slab`, `fsstat`, `kmsg`, `strace`, `bootmode`,
|
||||
`bootdebug`, `quiet`, `verbose`, `schinit` and `sysrq`.
|
||||
|
||||
**/proc is read-only except `sysrq`**, which is the one deliberate write target,
|
||||
and the code says so in one place. Marking every procfs inode read-only once
|
||||
|
|
@ -242,13 +252,15 @@ mounted by init with `mount("sysfs", "/sys", "sysfs", 0, 0)`.
|
|||
|
||||
| path | contents |
|
||||
|------|----------|
|
||||
| `/sys/bus/pci/devices/0000:BB:DD.F/` | `vendor device class revision subsystem_vendor subsystem_device irq resource config uevent` |
|
||||
| `/sys/bus/usb/devices/B-P[.P...]/` | `idVendor idProduct bcdDevice busnum devnum speed bDeviceClass bDeviceSubClass bDeviceProtocol bNumConfigurations uevent` |
|
||||
| `/sys/devices/pci0000:00/0000:BB:DD.F/` (and `/sys/bus/pci/devices/` links to it) | `vendor device class revision subsystem_vendor subsystem_device irq resource config uevent subsystem driver` |
|
||||
| `/sys/devices/pci0000:00/<xHCI>/usbB/B-P[.P...]/` (and `/sys/bus/usb/devices/` links) | `idVendor idProduct bcdDevice busnum devnum speed bDeviceClass bDeviceSubClass bDeviceProtocol bNumConfigurations uevent subsystem driver` |
|
||||
| `/sys/firmware/acpi/tables/` | each ACPI table, raw; a repeated signature is numbered from 1 (`SSDT1`, `SSDT2`) |
|
||||
| `/sys/firmware/dmi/tables/` | `smbios_entry_point` and `DMI`, raw |
|
||||
| `/sys/firmware/memmap/N/` | `start end type`, the firmware's map as the loader handed it over |
|
||||
| `/sys/firmware/efi/` | present when UEFI booted the kernel; empty |
|
||||
| `/sys/class/dmi/id/` | `sys_vendor product_name product_version board_name bios_version`, and the rest Linux has, except serials and the UUID |
|
||||
| `/sys/class/power_supply/` | `BAT0` (`capacity`, `status`, `energy_*`/`charge_*`, `voltage_now`, `cycle_count`, ...) and `AC` (`online`), from ACPI — the battery and the adapter are read through the laptop's embedded controller |
|
||||
| `/sys/bus/pci/devices/…/` | also `boot_vga`, `enable` and `graphics/fb0`, which libpciaccess (and so Xorg) reads |
|
||||
|
||||
**Linux's layout because everything that reads hardware reads it from there.**
|
||||
toybox's `lspci` and `lsusb` walk `/sys/bus/*/devices` and parse each `uevent`
|
||||
|
|
@ -263,9 +275,18 @@ at the moment it is read, so a stick shows up in `/sys/bus/usb/devices` as soon
|
|||
as it is attached. Text files report size 0, as `/proc`'s do; binary ones (the
|
||||
tables, `config`) report their real length, since tools copy them by it.
|
||||
|
||||
What is missing, compared with Linux: `/sys/devices` and its symlinks (the
|
||||
`bus/*/devices` entries are directories rather than links into it),
|
||||
`/sys/class` beyond `dmi`, `/sys/block`, and anything writable. PCI `config` is
|
||||
The kernel's own devices (input, tty, mem, graphics, block) have the Linux tree:
|
||||
`/sys/devices/virtual/<class>/<name>` with `dev`, `uevent` and a `subsystem`
|
||||
link, `/sys/class/<class>/<name>` and `/sys/dev/{char,block}/MAJ:MIN` as symlinks
|
||||
into it, and input capabilities; `uevent` is writable, and a write is announced
|
||||
on the uevent netlink socket ([udev.md](udev.md)).
|
||||
|
||||
PCI and USB devices are in that tree too, under `/sys/devices/pci0000:00`, with
|
||||
the `bus/*/devices` entries as links into it and `add`/`remove` events for USB
|
||||
hot-plug ([udev.md](udev.md)).
|
||||
|
||||
What is missing, compared with Linux: PCI bridges as parents, USB interfaces,
|
||||
`/sys/class` entries for most devices, `/sys/block`, and most of what is writable. PCI `config` is
|
||||
256 bytes, because config space is read through port `0xCF8`, which reaches
|
||||
no further.
|
||||
|
||||
|
|
@ -376,8 +397,6 @@ through.
|
|||
- **No page cache.** `mmap` of a file reads the whole thing in at map time, and
|
||||
writable `MAP_SHARED` of a file is refused rather than faked. This is the
|
||||
single largest gap in the layer.
|
||||
- **No symlinks in procfs**, which is why `/proc/<pid>/exe` is a regular file
|
||||
and `ps -o comm` prints `[init]`.
|
||||
- **`rename()` does not move directories**, and will not replace one. `EISDIR`
|
||||
either way.
|
||||
- **No named pipes.** `mkfifo()` fails with `EPERM`, Linux's answer for a
|
||||
|
|
@ -385,19 +404,13 @@ through.
|
|||
device, because `mknod()` read a Linux program's `(mode, dev)` as the native
|
||||
`(major, minor)`: `ls -l` showed `crw-rw-rw- 438, 0` where a `p` belongs. The
|
||||
zsh port was configured with `zsh_cv_sys_fifo=yes` on the strength of that.
|
||||
- **No `/dev/fd`, and no `/proc/self`.** A process cannot reach its own open
|
||||
descriptors by path. `<(cmd)` needs one or the other — zsh now knows it has
|
||||
neither and says so.
|
||||
- **No `AF_UNIX` socket inodes.** Socket names live in a table in
|
||||
`kernel/net/unix.c`, so `bind()` creates nothing that can be `stat()`ed or
|
||||
`unlink()`ed.
|
||||
- **No `/sys/devices` and no uevents**, so nothing can be told that a device
|
||||
appeared; Xorg is given its input devices by a config file instead.
|
||||
- **No unified inode cache across filesystems** beyond the single `itable`.
|
||||
- **A known, un-root-caused hang:** heavy `ls -lR` use across many commands in
|
||||
one long shell session can eventually make a *later*, unrelated directory
|
||||
hang; the identical command succeeds on a fresh boot. It is not inode or file
|
||||
table exhaustion — both fail cleanly and neither fired. It smells like a lock
|
||||
or refcount leak in the `*at()` paths or in toybox's own fd handling. The next
|
||||
step is instrumenting `itable`/`ftable` occupancy across a long session to see
|
||||
what is climbing.
|
||||
- **`sys_delete`/`sys_show`/`sys_showstat`** are orphaned entries from the old
|
||||
v1 filesystem's recycle bin. Nothing reaches them and they die with `fs.c`.
|
||||
- **One long-standing hang was never found.** Heavy `ls -lR` use across many
|
||||
commands in one long shell session once made a *later*, unrelated directory
|
||||
hang; the identical command succeeded on a fresh boot. It was instrumented and
|
||||
hunted and could not be reproduced, which is recorded in
|
||||
[../history/kernel.md](../history/kernel.md) rather than forgotten.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Graphics, text and the console
|
||||
|
||||
Four things share this area and they are deliberately separate: the kernel's own
|
||||
drawing surface, the compositor's display interface, the text-rendering layer,
|
||||
and the crash screen.
|
||||
Five things share this area and they are deliberately separate: the kernel's own
|
||||
drawing surface, the `display` class a display server could use, the text-rendering
|
||||
layer, the crash screen — and `/dev/fb0`, which is how Xorg reaches the screen.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -11,13 +11,13 @@ and the crash screen.
|
|||
This distinction causes confusion every time somebody meets it, so it is the
|
||||
first thing here.
|
||||
|
||||
| | [`gfx.h`](../../include/gfx.h) | [`display.h`](../../include/display.h) |
|
||||
| | [`gfx.h`](../../include/gfx/gfx.h) | [`display.h`](../../include/gfx/display.h) |
|
||||
|---|---|---|
|
||||
| **What** | The kernel's own drawing surface | The compositor's interface |
|
||||
| **What** | The kernel's own drawing surface | A frame interface for a display server |
|
||||
| **About** | Pixels and text | *Frames* |
|
||||
| **Operations** | `draw_pixel`, `fill_rect`, `putc`, `text_at` | `acquire`, `present`, `wait_vblank` |
|
||||
| **Buffering** | None. Direct writes to the scanout. | Double-buffered |
|
||||
| **Users** | The console and PSOGM | Schcom |
|
||||
| **Users** | The console, PSOGM, GMMOS | None today — Xorg draws through `/dev/fb0` |
|
||||
| **Must work when** | **Nothing else in the system does** | Userspace is healthy |
|
||||
|
||||
That last row is the reason they are not one interface. The console and the
|
||||
|
|
@ -67,7 +67,7 @@ and `vgpud` mirroring it see what is on the screen.
|
|||
`struct display_ops`, found with `device_find_class(DEVCLASS_DISPLAY)`.
|
||||
|
||||
`acquire()` allocates the back buffer **on first call, never at boot** — it is
|
||||
around two megabytes at 800×600, and a machine whose compositor never starts
|
||||
around two megabytes at 800×600, and a machine whose display server never starts
|
||||
should not pay that. A driver that cannot double-buffer returns 0, and the
|
||||
caller draws directly and accepts tearing rather than not drawing at all.
|
||||
`wait_vblank` is optional for the same reason.
|
||||
|
|
@ -133,7 +133,7 @@ A machine with both a stdvga and a virtio-gpu has two displays, and `vgpud` —
|
|||
the out-of-kernel driver, `cmd/vgpud/main.c` — owns the second one. For a long
|
||||
time it drew a test card there: colour bands, gradients, a checkerboard, and
|
||||
its own pid in binary along the bottom. That was scaffolding for
|
||||
[../plan/hybrid.md](../plan/hybrid.md)'s H2 and H3, where a screenshot had to
|
||||
the out-of-kernel driver work ([devices.md](devices.md)), where a screenshot had to
|
||||
answer *did a userspace process put this here* and *did the driver come back*.
|
||||
|
||||
Both of those are closed, so the test card became the Shift mark. Which was
|
||||
|
|
@ -223,9 +223,8 @@ shows the top-left corner, which is where a terminal puts everything worth
|
|||
seeing. Scaling is the alternative and it is the wrong one: text resampled to a
|
||||
non-integer ratio is text you cannot read.
|
||||
|
||||
**This is not the endpoint.** A compositor (Gate 3) will want this head for
|
||||
itself, and when there is one, the mirror goes away. Until then the honest
|
||||
thing for a working display to show is the system running on it.
|
||||
A display server that wants the head for itself can have it; until one does, the
|
||||
honest thing for a working display to show is the system running on it.
|
||||
|
||||
### The F8 resolution is the head's resolution
|
||||
|
||||
|
|
@ -263,9 +262,26 @@ not apply: two `vgpud`s cannot coexist, because the first holds the PCI claim
|
|||
and the second is refused the device long before it reaches `bind()`. The claim
|
||||
is the mutual exclusion; the socket node never was.
|
||||
|
||||
## /dev/fb0, and how Xorg draws
|
||||
|
||||
Xorg runs on the framebuffer through its `fbdev` driver, so `/dev/fb0` speaks Linux's
|
||||
fbdev: `FBIOGET_FSCREENINFO`, `FBIOGET_VSCREENINFO`, `FBIOPUT_VSCREENINFO` (accepted
|
||||
when it asks for the mode already in force), `FBIOPAN_DISPLAY`, `FBIOBLANK`,
|
||||
`FBIOPUTCMAP`, and `mmap` of the physical framebuffer. `FBIOGET_VSCREENINFO` reports
|
||||
nominal timings, as Linux's `efifb` does, instead of zeros — a zero pixel clock made
|
||||
fbdev compute a 0 Hz RandR mode. Pixel layout comes from the same table the kernel
|
||||
draws with, so a colour a program builds is the colour the kernel would.
|
||||
|
||||
Once a program maps the framebuffer the kernel has no way to know when it draws, so
|
||||
`fb_mapped()` marks the screen as no longer the text console's and `fbclose()` puts it
|
||||
back. A terminal switched to `KD_GRAPHICS` is not repainted ([terminals.md](terminals.md)),
|
||||
and the `VT_PROCESS` handshake tells the server before it loses the screen. `cmd/xready`
|
||||
checks every one of these calls before Xorg is started; the picture itself is the proof
|
||||
beyond that.
|
||||
|
||||
## Text
|
||||
|
||||
[`include/gfx/text.h`](../../include/text.h) + `kernel/dev/text.c`.
|
||||
[`include/gfx/text.h`](../../include/gfx/text.h) + `kernel/dev/text.c`.
|
||||
|
||||
**The font was never the problem — the character-cell grid was.** The console
|
||||
draws text by indexing `font_data[256][16]` and stepping the cursor eight
|
||||
|
|
@ -537,24 +553,63 @@ channel that works when the machine will not come back at all.
|
|||
machine whose network stack is dead**, and a phone camera is an out-of-band
|
||||
channel needing no driver on our side.
|
||||
|
||||
It encodes the **report itself**, not a URL — it reaches nothing. Deliberately
|
||||
small in three ways: byte mode only, EC level L only (the screen is a clean,
|
||||
evenly lit, high-contrast source; the damage QR's stronger levels protect
|
||||
against does not apply), and versions 1–6 only, stopping short of version 7's
|
||||
extra version-information blocks and version 10's wider count field.
|
||||
It encodes the **report itself**, not a URL — it reaches nothing. Byte mode only
|
||||
and EC level L only (the screen is a clean, evenly lit, high-contrast source; the
|
||||
damage QR's stronger levels protect against does not apply), but **every version,
|
||||
1 to 40**: version 40 at level L holds 2,953 bytes, which is what lets GMMOS put the
|
||||
kernel log on screen as a series of codes. Checked against the `segno` library module
|
||||
for module at every version.
|
||||
|
||||
134 bytes is a large crash report. If a payload ever needs more, the honest fix
|
||||
is to shorten the report.
|
||||
It draws on the report unconditionally rather than on a keypress, which is better:
|
||||
it costs nothing, and requiring input would mean the one case where input is least
|
||||
trustworthy is also the case where the report is hardest to retrieve. Verified by
|
||||
`make crashshot && make crashqr`, which decodes the symbol out of the captured
|
||||
framebuffer.
|
||||
|
||||
It draws unconditionally rather than on a keypress, which is better: it costs
|
||||
nothing, and requiring input would mean the one case where input is least
|
||||
trustworthy is also the case where the report is hardest to retrieve. Verified
|
||||
by `make crashshot && make crashqr`, which decodes the symbol out of the
|
||||
captured framebuffer.
|
||||
### Every processor stops
|
||||
|
||||
A panic used to stop only the processor it happened on; the rest carried on with a
|
||||
dead kernel, drew over the report, and kept the fans loud until somebody held the power
|
||||
button. The panicking processor now sends every other one an **NMI**, which arrives even
|
||||
with interrupts off, and `trap()` halts a processor that takes one while `panic_stop` is
|
||||
set — first thing, before the canary check, because that stack may be what broke. NMI has
|
||||
its own per-CPU stack (IST 1): for a few instructions at the top of `syscall_entry` the
|
||||
live stack is the user's, at ring 0, and a fault pushing the frame there would be a double
|
||||
fault on the same stack and a reset instead of a report. A second processor that panics
|
||||
while one is reporting halts.
|
||||
|
||||
## GMMOS — the Guru Meditation Micro OS
|
||||
|
||||
The report used to be the last thing the kernel did: a picture, blinking keyboard lights,
|
||||
and a machine that needed its power button. GMMOS keeps the report and goes on living
|
||||
under it (`kernel/gmmos/`, about 2,000 lines). The real kernel stays dead — nothing of it is
|
||||
called again that takes a lock, sleeps, allocates or waits for an interrupt. GMMOS has its
|
||||
own: the framebuffer, a clock read from the TSC, and keyboards it polls itself through
|
||||
**micro drivers**, cut-down copies of the real ones that do only what a keyboard needs.
|
||||
|
||||
| Key | |
|
||||
|---|---|
|
||||
| (nothing) | a status line counts down **three minutes** to power-off; any key cancels it |
|
||||
| **Space** | the last 300 lines of this boot's log as **QR codes**, a page per symbol, numbered so a phone can take them in order; `+` / `-` trade module size for capacity |
|
||||
| **L** | the log on screen, scrolled with the arrows |
|
||||
| **T** | a terminal: `why`, `log`, `grep`, `ps`, `cpus`, `pci`, `mem` (page tables walked first, so a bad address cannot fault), `uptime`, `restart`, `poweroff` |
|
||||
| **R R** / **P P** | restart / power off — twice, so a stray key does neither |
|
||||
|
||||
The micro drivers are **PS/2**, **I2C-HID** (the laptop's own keyboard, over a DesignWare
|
||||
controller — the live driver hands over what it found at boot, because AML cannot run on a
|
||||
dead kernel) and **xHCI** (USB keyboards: every controller is halted, reset and enumerated
|
||||
from nothing, root ports and USB 2 hubs up to two deep, a boot-protocol keyboard, no
|
||||
interrupts, a static 256 KB pool). GMMOS probes all of them and polls every one that answers.
|
||||
|
||||
Every buffer is static. A panic inside GMMOS, or a second one in the report, is reported
|
||||
and then only blinks; GMMOS does not re-enter itself. Checked under QEMU: every screen, two
|
||||
QR pages read off a screenshot with `zbar` and decoded to the log text exactly, restart
|
||||
reboots, and left alone it powers off at three minutes. What has been seen on real hardware
|
||||
is the report and the keyboards the laptops have; the xHCI path is the least exercised.
|
||||
|
||||
## One palette
|
||||
|
||||
[`include/gfx/theme.h`](../../include/theme.h). There were three unrelated designs
|
||||
[`include/gfx/theme.h`](../../include/gfx/theme.h). There were three unrelated designs
|
||||
and the seams showed: the splash was deep purple with gold text deliberately
|
||||
matching the *panic* screen, the console was cream with near-black text, and so
|
||||
a healthy boot was dressed as a crash and then abruptly became an amber
|
||||
|
|
@ -571,16 +626,20 @@ makes it mean something.
|
|||
|
||||
## What it does not do
|
||||
|
||||
- **The console still has its cell grid.** Cells are now a Unicode codepoint
|
||||
drawn by the TrueType rasteriser rather than a CP437 byte drawn from a bitmap,
|
||||
and the cell size follows the font's measured advance — but it is still a
|
||||
fixed grid, and the text layer's proportional path is unused by it.
|
||||
- **No mode setting.** Resolution is whatever the loader asked for. There is no
|
||||
`/dev/dri`-style device and no way to change it at runtime.
|
||||
- **No double buffering in practice.** `display_ops.acquire` is specified and
|
||||
the compositor needs it; virtio-gpu binds but exposes no modeset API, and
|
||||
- **The console still has its cell grid.** Cells are a Unicode codepoint drawn by the
|
||||
TrueType rasteriser, and the cell size follows the font's measured advance (the font
|
||||
scales to about 45 rows, 16 to 40 px per em, on whatever mode the firmware booted
|
||||
in) — but it is a fixed grid, and the text layer's proportional path is unused by it.
|
||||
- **Mode setting is the loader's and `vgpud`'s.** On real hardware resolution is whatever
|
||||
the loader chose from its menu — which now asks "Keep this resolution?" and falls back
|
||||
by itself, because AMD's GOP "sets" smaller modes without leaving the panel's native
|
||||
timing and the screen showed three or four squashed copies side by side. There is no
|
||||
`/dev/dri`, no KMS, and Xorg runs at the mode the console is in.
|
||||
- **No double buffering in practice.** `display_ops.acquire` is specified and nothing
|
||||
uses it; virtio-gpu exposes a modeset API on `vgpud`'s socket and nothing more, and
|
||||
`wait_vblank` is unimplemented everywhere.
|
||||
- **No hardware acceleration of any kind**, and none is planned before 2D blit.
|
||||
- **No hardware acceleration of any kind**: Xorg draws in software to the framebuffer
|
||||
(no glamor, no DRI), and GPU drivers are a plan, not a feature ([../plan/gpu.md](../plan/gpu.md)).
|
||||
- **No CFF/Type 2 charstrings**, so OTF fonts do not render — that is a separate
|
||||
`font_ops` provider and a bytecode interpreter, not an extension of the `glyf`
|
||||
path. TrueType hinting is deliberately out of scope.
|
||||
|
|
@ -593,6 +652,6 @@ makes it mean something.
|
|||
- **No composite point-matching.** Components positioned by matching a point in
|
||||
the base rather than by an offset are placed at the origin. Fonts that use it
|
||||
are rare and none in the tree do.
|
||||
- **PSOGM renders and stops.** The interactive crash mode described in
|
||||
[../plan/roadmap.md](../plan/roadmap.md) — walk the stack, read another
|
||||
process's memory, dump page tables — is designed and not built.
|
||||
- **GMMOS cannot look at a live process by name.** The "interactive crash mode" is built as far
|
||||
as a dead machine allows — log, a terminal, `ps`, `cpus`, `pci`, memory by physical walk — but
|
||||
the scheduler is gone, so there is no running a debugger against a process.
|
||||
|
|
|
|||
|
|
@ -11,18 +11,27 @@ driver or a protocol stack: memory, processes, scheduling, traps and signals.
|
|||
One page table per process; the kernel is mapped into all of them at
|
||||
`KERNBASE = 0xFFFF800000000000`, so a trap does not switch `CR3`.
|
||||
|
||||
`kvmalloc()` maps physical 0–4 GB as four 1 GB pages, with the top one marked
|
||||
uncached — that is where PCI BARs and the framebuffer live. This is why 3 GB is
|
||||
the most memory the kernel can treat as RAM, and it is a deliberate simplicity:
|
||||
a direct map with a fixed offset makes `P2V`/`V2P` arithmetic rather than a
|
||||
lookup.
|
||||
`kvmalloc()` maps as many 1 GB pages of physical memory as the machine has RAM
|
||||
(up to `PHYSTOP_MAX`, 256 GB), always at least the low four. The 3–4 GB slot stays
|
||||
uncached because that is where the PCI hole is; everything above 4 GB is RAM, so
|
||||
those are cached, and a device BAR up there still gets its own uncached window
|
||||
from `ioremap()`, which takes the entries the direct map did not. On a CPU
|
||||
without 1 GB pages (Intel's desktop parts before Haswell), each gigabyte is a
|
||||
page directory of 2 MB pages instead — the same reach, one extra page of table
|
||||
per gigabyte. A direct map with a fixed offset makes `P2V`/`V2P` arithmetic
|
||||
rather than a lookup.
|
||||
|
||||
`kalloc()` hands out one 4 KB page at a time from a free list. **There is no
|
||||
slab or pool allocator**, and the consequence is visible throughout the tree:
|
||||
every subsystem that needs many small objects has invented its own fixed-size
|
||||
table — `NPROC` processes, `NSOCK` sockets, `NDEVICE` devices, `NSHM` segments.
|
||||
That is why the limits are compile-time constants rather than a design decision
|
||||
about each one.
|
||||
How much RAM there is comes from the firmware's E820/UEFI map, as a *list* of
|
||||
usable regions, not one run ending at a top address (OVMF drops an ACPI NVS
|
||||
region at 8 MB, which used to hide everything above it). The kernel itself is
|
||||
linked at 16 MB, clear of what firmware keeps low. `kalloc` holds never-used
|
||||
memory as ranges, so a large machine costs nothing until its memory is used.
|
||||
|
||||
`kalloc()` hands out one 4 KB page at a time from a free list. Objects smaller
|
||||
than a page come from the slab caches below; objects larger than one do not have
|
||||
a home yet, so the biggest tables (`NPROC` processes, `NSOCK` sockets, `NDEVICE`
|
||||
devices, `NSHM` segments) are still fixed-size arrays, which is why their limits
|
||||
are compile-time constants rather than a design decision about each one.
|
||||
|
||||
The free list carries a canary, checked at the top of both `kalloc()` and
|
||||
`kfree()`, so a wild write into it panics at the point it is touched rather than
|
||||
|
|
@ -150,8 +159,8 @@ pages, reference counted, mapped through a VMA with `no_free` set so that
|
|||
`memfd_create(2)` is the same machinery reached by **descriptor** instead of by
|
||||
key. That difference is the whole point and the reason it exists on Linux at
|
||||
all: two processes share memory by passing the fd over a socket, so neither has
|
||||
to agree on a path or race to invent a key. It is what a Wayland buffer crosses
|
||||
on, and it is why descriptor passing (`SCM_RIGHTS`) had to be built first.
|
||||
to agree on a path or race to invent a key. It is how a display server and its client
|
||||
share a buffer, and it is why descriptor passing (`SCM_RIGHTS`) had to be built first.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
|
|
@ -171,10 +180,46 @@ version of something this kernel cannot do:
|
|||
mapper. Here a freed page would simply stay mapped to memory somebody else
|
||||
now owns.
|
||||
|
||||
Writable `MAP_SHARED` of a **real file** is still `ENODEV`. That needs a page
|
||||
cache, it is a separate project, and nothing in 1.0 asks for it — an anonymous
|
||||
object has no such contract to break, which is exactly why `memfd` is the
|
||||
narrow answer.
|
||||
Writable `MAP_SHARED` of a **real file** is still refused. That needs a page
|
||||
cache, it is a separate project, and nothing that runs today asks for it — an
|
||||
anonymous object has no such contract to break, which is exactly why `memfd` is
|
||||
the narrow answer.
|
||||
|
||||
### Demand paging
|
||||
|
||||
Anonymous memory can be recorded without being backed: the page appears,
|
||||
zeroed, the first time it is touched — by the program, through `trap()`, or by
|
||||
the kernel on its behalf, through `uva2ka()`. `mmap` does this for mappings of
|
||||
1 MB or more, for `MAP_NORESERVE`, and for `PROT_NONE`. The reason is WebKit,
|
||||
which reserves 4 GB of address space at start for its JIT and heaps and touches
|
||||
a sliver of it; backing all of it at `mmap` time failed with `ENOMEM` and the
|
||||
browser aborted. Go, Java and Firefox reserve the same way.
|
||||
|
||||
`vm_fault()` takes a spinlock and nothing that sleeps, so `uva2ka()` may call it
|
||||
from anywhere; the lock keeps two threads of one process from both mapping the
|
||||
same page. `fork` and `munmap` skip the holes, so a page nobody touched is one
|
||||
they never see, and a child that touches it reads zeroes — what it would have
|
||||
read in the parent.
|
||||
|
||||
File-backed mappings are **not** lazy: `mmap` of a file still reads the range in
|
||||
at map time.
|
||||
|
||||
### TLB shootdowns
|
||||
|
||||
Unmapping from an address space that threads on other processors are running in
|
||||
interrupts those processors and waits until each has reloaded `CR3`
|
||||
(`tlb_shootdown()` in `vm.c`; the vector is one of the MSI lines, which `trap()`
|
||||
already dispatches with an EOI). Without it, when one thread unmapped a range and
|
||||
`mmap` handed the same addresses straight back, a thread on another CPU went on
|
||||
reading and writing the *old* pages through its TLB: stores vanished and loads
|
||||
were stale. musl's allocator noticed first — a group's metadata no longer
|
||||
pointing back at it — and Epiphany, forty threads on four processors, died at a
|
||||
different address on every run.
|
||||
|
||||
Only one shootdown runs at a time, and it is taken without disabling interrupts:
|
||||
two processors each waiting with interrupts off for the other to answer would
|
||||
wait for ever. On a single processor, or if no vector could be allocated, it
|
||||
does nothing (and says so at boot).
|
||||
|
||||
### What the fixed tables actually cost
|
||||
|
||||
|
|
@ -184,31 +229,69 @@ partly true:
|
|||
|
||||
| | size | table | |
|
||||
|---|---|---|---|
|
||||
| `struct proc` | 11744 | `NPROC` 128 | larger than a page; 1 KB of it is FPU state |
|
||||
| `struct socket` | 4936 | `NSOCK` 128 | larger than a page |
|
||||
| `struct proc` | 1984 | `NPROC` 128 | 254 KB in all; the address space, descriptors and signal handlers live in their own objects now, shared by threads |
|
||||
| `struct mm` | 8232 | one per process, 128 | 1 MB in all, almost all of it the 256-entry VMA table |
|
||||
| `struct socket` | about 4900 | `NSOCK` 128 | larger than a page |
|
||||
| `struct device` | 120 | `NDEVICE` 128 | 15 KB, never hit |
|
||||
| `struct buf` | 200 | `NBUF` 256 to start | grows from a slab, data from `kalloc`, up to RAM/32 |
|
||||
| `struct inode` | 296 | none | grows from a slab; `NINODE` 200 is only how many are made up front |
|
||||
| `struct file` | 64 | none | grows from a slab; `NFILE` 512 is only the static pool |
|
||||
| `struct inode` | about 460 | none | grows from a slab; `NINODE` 200 is only how many are made up front |
|
||||
| `struct file` | about 88 | none | grows from a slab; `NFILE` 512 is only the static pool |
|
||||
|
||||
The two largest cannot go in a page-backed slab at all. Freeing them needs a
|
||||
contiguous multi-page allocator, which is a separate thing and is not written.
|
||||
The buffer header shrank to 200 bytes when its 4 KB of data moved out to a page
|
||||
of its own, which is what let the cache grow at all.
|
||||
(Sizes from `nm -S` on a build of the tree, not from a header comment.)
|
||||
|
||||
`struct proc` shrank by a factor of six when threads arrived, because the page
|
||||
tables, VMAs, descriptor table and signal dispositions moved out of it into
|
||||
objects a thread group shares. `struct socket` is still too large for a
|
||||
page-backed slab; freeing it needs a contiguous multi-page allocator, which is a
|
||||
separate thing and is not written. The buffer header shrank to 200 bytes when its
|
||||
4 KB of data moved out to a page of its own, which is what let the cache grow at
|
||||
all.
|
||||
|
||||
## Processes
|
||||
|
||||
`struct proc` in [`include/core/proc.h`](../../include/proc.h). A fixed table of
|
||||
`NPROC` entries, allocated by linear scan.
|
||||
`struct proc` in [`include/core/proc.h`](../../include/core/proc.h). A fixed table of
|
||||
`NPROC` entries, allocated by linear scan. A thread is a `struct proc` too.
|
||||
|
||||
**The field order at the top of the struct is fixed by assembly.** `trapasm.S`
|
||||
hard-codes `fs_base` at offset `0x18`, and there is a `_Static_assert` in
|
||||
`proc.c` that fails the build if it moves. Anything new goes at the end.
|
||||
|
||||
Each process carries: page directory and size, kernel stack, open file table,
|
||||
cwd inode *and* cwd path string, signal handlers and masks, VMA table for
|
||||
`mmap`, POSIX identity (pgid, sid, and all six of real/effective/saved uid and
|
||||
gid), and the Linux-ABI syscall map.
|
||||
Each process carries: a kernel stack, cwd inode *and* cwd path string, signal
|
||||
masks, POSIX identity (pgid, sid, and all six of real/effective/saved uid and
|
||||
gid), the Linux-ABI syscall map, and its x87/SSE registers (saved on a switch;
|
||||
the kernel itself stays off them). Three things it *points to* rather than
|
||||
owns, because a thread group shares them:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `struct mm` | the address space: page tables, heap top, the VMA table, which file each span came from, and a lock so one thread at a time changes the mappings |
|
||||
| `struct files` | the descriptor table and its `FD_CLOEXEC` flags |
|
||||
| `struct sighand` | signal dispositions — what `sigaction()` sets |
|
||||
|
||||
Each is reference counted. `clone()` shares what its flags say and `fork()`
|
||||
copies all three.
|
||||
|
||||
### Threads
|
||||
|
||||
`clone()` takes `pthread_create`'s flags (`CLONE_VM`, `FILES`, `SIGHAND`,
|
||||
`THREAD`, `SETTLS`, `PARENT_SETTID`, `CHILD_CLEARTID`) and `posix_spawn`'s
|
||||
(`VM`, `VFORK`, a stack). What follows from that:
|
||||
|
||||
- **futex** — `WAIT`, `WAIT_BITSET` with timeouts, `WAKE`, `REQUEUE`,
|
||||
`CMP_REQUEUE`, keyed by the word's *kernel* address so processes that map the
|
||||
same page meet. `set_tid_address` keeps the word that exit zeroes and wakes,
|
||||
which is what `pthread_join` waits on.
|
||||
- **Exit.** `exit` ends the calling thread; `exit_group`, a fatal signal, or the
|
||||
leader exiting ends them all. `reapd` frees exited threads, and the leader is
|
||||
reaped by its parent once the rest are gone. `getpid` is the group, `gettid`
|
||||
the thread.
|
||||
- **Signals.** Stops and resumes reach every thread; other process-directed
|
||||
signals go to the leader.
|
||||
- **`exec`** kills the other threads and unshares the dispositions.
|
||||
- **`/proc`** lists processes, `task/` lists threads; `comm`, `Threads:` and
|
||||
`Tgid` are there.
|
||||
|
||||
The first program to need all of this was GTK's, then WebKit's.
|
||||
|
||||
### Slot reuse
|
||||
|
||||
|
|
@ -232,12 +315,15 @@ exactly as a syscall would. The network thread is the current user.
|
|||
|
||||
## Scheduling
|
||||
|
||||
Round-robin over the process table, one scheduler per CPU, each with its own
|
||||
context. `swtch()` saves callee-saved registers and switches stacks; `sched()`
|
||||
returns to the scheduler and asserts four invariants on the way — holding
|
||||
`ptable.lock`, `ncli == 1`, not `RUNNING`, interrupts off.
|
||||
Fair share by virtual runtime, one scheduler per CPU, each with its own context
|
||||
— the rule, the guards and `nice` are in [scheduler.md](scheduler.md).
|
||||
`swtch()` saves callee-saved registers and switches stacks; `sched()` returns to
|
||||
the scheduler and asserts four invariants on the way — holding `ptable.lock`,
|
||||
`ncli == 1`, not `RUNNING`, interrupts off.
|
||||
|
||||
When a full pass finds nothing runnable the CPU halts rather than spinning.
|
||||
When nothing is runnable the CPU goes idle rather than spinning: into ACPI's
|
||||
`_CST` C2 state (CC6 on Zen), by port read or `MWAIT`, where the APIC timer keeps
|
||||
running through it, and otherwise a plain `hlt`.
|
||||
|
||||
**Locking is a single `ptable.lock` over the whole table.** Not per-process: a
|
||||
scheduling operation touches two processes (the one leaving and the one
|
||||
|
|
@ -309,9 +395,23 @@ mksh's job control could not make progress until they existed.
|
|||
|
||||
## What userspace gets
|
||||
|
||||
`fork`, `exec`, `wait`/`waitpid`/`wait4` with `WNOHANG` and real pid selection,
|
||||
pipes, signals, `mmap` (anonymous and file-backed), POSIX shared memory, Unix
|
||||
domain sockets, `poll`/`select`/`epoll`, `eventfd`, `timerfd`, `signalfd`.
|
||||
`fork`, `clone` (threads), `exec`, `wait`/`waitpid`/`wait4` with `WNOHANG` and
|
||||
real pid selection, pipes, signals, `mmap` (anonymous and file-backed), POSIX
|
||||
shared memory, `memfd`, Unix domain sockets, `poll`/`select`/`epoll`, `eventfd`,
|
||||
`timerfd`, `signalfd`, `inotify`, `futex`.
|
||||
|
||||
### Dynamic linking
|
||||
|
||||
`exec` handles `ET_EXEC` and `ET_DYN`. A dynamically linked program names an
|
||||
interpreter in `PT_INTERP` — on musl, `/lib/ld-musl-x86_64.so.1`, which is libc
|
||||
under another name — and the kernel's whole part is to load that as well and
|
||||
start it instead of the program. The interpreter finds the program through the
|
||||
auxiliary vector (`AT_PHDR`, `AT_ENTRY`), maps libc and whatever else it needs
|
||||
with `mmap()`, relocates, and jumps to `AT_ENTRY` itself. A position-independent
|
||||
program goes where a fixed one would (`0x400000`); the interpreter goes at the
|
||||
start of the `mmap` area (`0x40000000`) and `mmap` carries on above it, so the
|
||||
two never meet. This is what lets Alpine's binaries run unmodified, and
|
||||
`dlopen` works, so Xorg loads its drivers as modules.
|
||||
|
||||
`mmap` uses a bump allocator (`proc->mmap_next`, starting at `0x40000000`) so
|
||||
every mapping gets its own disjoint, exactly-sized range. It previously assigned
|
||||
|
|
@ -329,24 +429,29 @@ allocation over 8 MB mapped straight into a neighbouring live mapping.
|
|||
loop reports 97%, `sleep 2` reports 0% — but kernel time needs TSC deltas
|
||||
closed and reopened around every `sched()`, which is scheduler accounting
|
||||
rather than sampling, and is not written.
|
||||
|
||||
- **No slab allocator.** One page at a time, and every subsystem's fixed-size
|
||||
table is a consequence. This is the single change that would most improve the
|
||||
rest of the kernel.
|
||||
- **No demand paging and no page cache.** `mmap` of a file *reads the whole
|
||||
thing in* at map time. Writable `MAP_SHARED` of a file is therefore refused
|
||||
outright rather than faked — see [../plan/foundations.md](../plan/foundations.md).
|
||||
- **No dynamic linking.** `exec` handles `ET_EXEC` only: no `PT_INTERP`, no
|
||||
`ET_DYN`, no shared libraries, no `dlopen`.
|
||||
- **No threads.** `clone` exists but does not share an address space;
|
||||
`gettid` returns the pid.
|
||||
- **Permissions are tracked but not enforced.** uid/gid are maintained honestly
|
||||
so `getuid()`/`setuid()` stop lying, and nothing consults them on access.
|
||||
- **`struct socket` is too big for a slab**, and every subsystem's fixed-size
|
||||
table (`NPROC`, `NSOCK`, `NDEVICE`, `NSHM`) is still a compile-time constant.
|
||||
Slabs exist for the objects that fit in a page.
|
||||
- **No page cache.** `mmap` of a file *reads the range in* at map time, and
|
||||
writable `MAP_SHARED` of a file is refused outright rather than faked. Demand
|
||||
paging covers anonymous memory only.
|
||||
- **Root is `euid == 0` and nothing finer.** No capabilities; file access, `kill`, `exec`
|
||||
and the privileged calls are enforced as [users.md](users.md) says.
|
||||
- **`mprotect` is accepted and not enforced** (`sys_mprotect` returns 0). RELRO is not read-only,
|
||||
a `PROT_NONE` guard does not fault, and nothing has `NX` (`EFER.NXE` is never set), so every
|
||||
mapping is executable.
|
||||
- **`poll`, `select` and `epoll` wait a tick at a time** (10 ms at `HZ=100`) unless something
|
||||
that can make a descriptor ready calls `poll_kick()` — which the pipe, socket and event paths
|
||||
do. busybox's `wget` polls before every read and downloaded at 85 KB/s, a segment per tick,
|
||||
until the network path started kicking.
|
||||
- **No swap**, and no memory pressure handling of any kind. `kalloc` failing is
|
||||
reported up as `-ENOMEM` and that is the whole policy.
|
||||
- **Nested signal delivery is not supported.** `in_signal` guards against
|
||||
re-entering a handler while one is running.
|
||||
- **Scheduling is fair-share on one global run queue.** Priorities and
|
||||
fairness accounting exist now ([scheduler.md](scheduler.md)); per-CPU run
|
||||
queues, work stealing and CPU affinity do not, despite `sched_setaffinity`
|
||||
existing.
|
||||
- **One global run queue.** Fair share and `nice` exist; per-CPU run queues, work
|
||||
stealing and CPU affinity do not, and `sched_setaffinity` is accepted and
|
||||
ignored.
|
||||
- **No IOMMU**, so DMA-capable devices (and the driver processes that program
|
||||
them) are trusted.
|
||||
- **256 GB is the most RAM** the kernel uses; the rest is ignored, with a line
|
||||
saying so.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ drivers, and LDLS is what does that.
|
|||
|
||||
It works, and on more than one driver. `8139too` and `e1000` — unmodified GPL
|
||||
kernel modules — load into a SchOS process, drive their hardware through
|
||||
`/dev/pci`, and carry packets:
|
||||
`/dev/pci`, and carry packets (both under QEMU). A third, Linux's `r8169`, is
|
||||
the NIC driver on the ASUS TUF A15 laptop, and it is the first that needs a
|
||||
*set* of modules and not one; it is described under *A set of modules*. It has
|
||||
registered its interface and read its PHY on that laptop; whether it carries
|
||||
traffic there has not been confirmed yet.
|
||||
|
||||
```
|
||||
# ldlsd /lib/modules/8139too.ko /lib/modules/btf &
|
||||
|
|
@ -46,18 +50,25 @@ Every one was a call that succeeded, returned zero, and did nothing.
|
|||
|
||||
Five, and everything else is an implementation of them.
|
||||
|
||||
### 1. Nothing GPL ever enters an image we build
|
||||
### 1. Linux's drivers ship in the image
|
||||
|
||||
The driver is **the user's**: obtained by them, loaded on their machine. SchOS
|
||||
ships the layer and never the thing it loads. `.gitignore` refuses `*.ko`,
|
||||
`make ldls-stage` is the one command that puts a module into a local image and
|
||||
is deliberately not part of `all`, and `make ldls-unstage` takes it out again.
|
||||
SchOS is GPL-2.0-or-later, and GPL-2.0-only code combines with it, so there is no
|
||||
licensing reason to keep Linux's drivers out of an image. They are in: the **r8169 set**
|
||||
(`mdio-bus`, `libphy`, `mdio_devres`, the Realtek PHY driver and `r8169`, unmodified) and
|
||||
the RTL8168 PHY firmware are what the ISO carries for the laptop's network, with their
|
||||
licence and corresponding source recorded in `NOTICE`
|
||||
([../licensing.md](../licensing.md) §6).
|
||||
|
||||
[../licensing.md](../licensing.md) §6 is the whole argument. The short form is
|
||||
that the combination happens in a user process on the user's machine, so no
|
||||
combined work is distributed by anyone. Since SchOS moved to GPL-2.0-or-later
|
||||
that is no longer the only way it *could* be done — it is the way it is done,
|
||||
because a module we never ship is a module we never have to answer for.
|
||||
(An earlier version of this rule said the opposite — that LDLS would ship the layer and never
|
||||
the thing it loads. That was written when SchOS was Apache-2.0 and the combination could not
|
||||
be distributed; it stopped being true with the move to the GPL, and the owner has said Linux
|
||||
code and blobs may be shipped.)
|
||||
|
||||
What is left is mechanical. The repository holds no `*.ko` — a binary does not belong in git, so
|
||||
`.gitignore` refuses them — and `make ldls-r8169` (before `make iso`) stages the set from a
|
||||
Linux build, together with its BTF, because a module, its dependencies and the BTF that
|
||||
describes them have to come from the same build. `make ldls-stage KO=…` does the same for any
|
||||
other module, which is then a distribution of *that* module's code, under its own licence.
|
||||
|
||||
### 2. It runs in a process, never in the kernel
|
||||
|
||||
|
|
@ -186,6 +197,55 @@ the object has to live where a signed 32-bit absolute address reaches. A
|
|||
process's mmap area starts at `0x40000000`, which is inside that window; on the
|
||||
host harness `MAP_32BIT` reaches it from the other end.
|
||||
|
||||
### A set of modules — what r8169 needed
|
||||
|
||||
Linux's `r8169` is not one module. It needs `mdio-bus`, `libphy`, `mdio_devres`,
|
||||
the Realtek PHY driver, the NIC itself, the RTL PHY firmware, and the BTF of the
|
||||
kernel they came from. `make ldls-r8169` stages all of that from the running
|
||||
kernel; `etc/schinit/ldls-r8169.job` starts `ldlsd` on it at boot, with its output
|
||||
in `/var/log/ldls-r8169.log`. The job has `KeepAlive = no`: a driver that crashes
|
||||
on real hardware would otherwise be restarted into the same crash every few seconds.
|
||||
|
||||
What that added to `ldlsd`:
|
||||
|
||||
- **A comma-separated module set**, loaded in dependency order, each linking
|
||||
against the ones before it, with the BTF read before any `init_module` runs.
|
||||
A library module (`libphy`) just exports symbols to the next; the PCI `probe`
|
||||
runs once, at the end, for whichever module registered a `pci_driver`.
|
||||
- **A driver core**: bus match, driver probe, so the PHY driver binds to the PHY
|
||||
the way it does in Linux; **devres**; a kernel `printf`; real `udelay`/`msleep`/
|
||||
`ktime`; and **1000 Hz `jiffies`**, because the module's `HZ` is compiled in and
|
||||
phylib re-arms its link poll `HZ` jiffies out.
|
||||
- **Firmware** read from `/lib/firmware`.
|
||||
- **phylib's data**, the `link_mode_params` table and its bitmaps.
|
||||
- **The symbols whose stub-zero was a lie**: list validity, `try_module_get`,
|
||||
`dev_err_probe`, `eth_platform_get_mac_address`, `napi_consume_skb` (as a stub it
|
||||
leaked one buffer per transmitted frame until the DMA heap ran out), and the
|
||||
rest of the table at the end of this document.
|
||||
- `pdev->bus` is a real `pci_bus`; `net_device.priv_len` is set from BTF (the
|
||||
driver's `memset` of its ring bookkeeping is bounds-checked against it, and
|
||||
`__fortify_panic` fired on a length of zero); and `cmovcc` from per-CPU memory is
|
||||
rewritten like the other `%gs` forms.
|
||||
- **`__preempt_count` starts at `PREEMPT_ENABLED`.** mdio-bus keeps the register
|
||||
it has just read in `r11` across `preempt_enable()`, which on x86 is
|
||||
`decl %gs:__preempt_count; je`, to a call that preserves every register. With the
|
||||
count at zero the call was made, into a logging stub whose syscall left `RFLAGS`
|
||||
(0x246) in `r11` — so every MDIO read returned `0x02460246`. `preempt_schedule` is
|
||||
now a bare `ret`.
|
||||
- `__fortify_panic` names the module and function (`r8169: rtl_open.cold+0x…`) so
|
||||
a photograph of the screen is enough, and exits instead of spinning on a core and
|
||||
holding `/dev/netdev`.
|
||||
|
||||
The kernel did two things for it. It turns **ASPM off on Realtek NIC links** at
|
||||
boot, as `r8169` does through `pci_disable_link_state`, which a process cannot;
|
||||
and a network interface that **appears after boot starts the stack** — with the
|
||||
native driver out of the registry the laptop boots with no interface at all, and
|
||||
`ldls0` arrives later. `ldlsd` says which refusal it got when attaching: `EBUSY` is
|
||||
another process, anything else is the kernel.
|
||||
|
||||
The native `drivers/net/r8169/` stays in the tree, with Linux's 8168H setup, as
|
||||
the fallback; it is out of the registry.
|
||||
|
||||
### The KPI — `ldls/kpi/`
|
||||
|
||||
The Linux symbols, implemented. Register access to a mapped BAR; the PCI calls
|
||||
|
|
@ -232,8 +292,8 @@ and setting both to zero makes the whole thing reversible with no lookup table.
|
|||
|
||||
### The back end — `/dev/pci`
|
||||
|
||||
Already built, for `vgpud`, before any of this existed.
|
||||
[hybrid.md](../plan/hybrid.md) insisted the interface be about *PCI devices*
|
||||
Already built, for `vgpud`, before any of this existed. The design insisted the
|
||||
interface be about *PCI devices*
|
||||
rather than about a driver model, on the argument that an interface whose first
|
||||
consumers are native drivers is a general mechanism rather than a hole shaped
|
||||
like something foreign. A Linux driver's `pci_iomap`, `dma_alloc_coherent` and
|
||||
|
|
@ -241,10 +301,9 @@ like something foreign. A Linux driver's `pci_iomap`, `dma_alloc_coherent` and
|
|||
|
||||
### Giving a device up — `release`
|
||||
|
||||
A driver in the kernel now has a `release` hook, and `/dev/pci` calls it before
|
||||
handing its device to a process. `docs/plan/hybrid.md` had said the honest
|
||||
answer to "may I have the one you are using" is *no* until a driver could be
|
||||
told to let go; e1000 is the first that can.
|
||||
A driver in the kernel has a `release` hook, and `/dev/pci` calls it before
|
||||
handing its device to a process. The honest answer to "may I have the one you are
|
||||
using" was *no* until a driver could be told to let go; e1000 is the first that can.
|
||||
|
||||
Letting go is three things, and the third was missed at first:
|
||||
|
||||
|
|
@ -276,7 +335,7 @@ one flag and hands over all 65536 ports, including the interrupt controller,
|
|||
the timer and the disk; the TSS I/O permission bitmap is per port. A foreign
|
||||
driver already has DMA with no IOMMU, and the answer to "this is not a security
|
||||
boundary" is not to move it further — so it is the bitmap, at 8 KB per CPU of
|
||||
resident memory. See [../../include/ioperm.h](../../include/ioperm.h).
|
||||
resident memory. See [../../include/arch/ioperm.h](../../include/arch/ioperm.h).
|
||||
|
||||
Writing it found a latent hole. The TSS's I/O map base is at offset 102 and the
|
||||
old code wrote offset 64; the field it meant to set stayed 0, and a base of 0
|
||||
|
|
@ -336,17 +395,17 @@ point of having built it against our own:
|
|||
|
||||
## What it does not do
|
||||
|
||||
- **Two drivers, one kernel version, one machine.** `8139too` and `e1000` on
|
||||
6.18.46 against QEMU. The second driver was worth more than the first: it
|
||||
found the per-CPU problem, the retpoline problem, the port-I/O problem, the
|
||||
interrupt storm and a seven-year-old hole in the TSS. Nothing here has been
|
||||
tried on a second kernel or on real silicon, and "LDLS runs Linux drivers" is
|
||||
still a claim about a category from a sample of two.
|
||||
- **Three drivers, one kernel version.** `8139too` and `e1000` on 6.18.46
|
||||
against QEMU, and `r8169` on that version against QEMU (where it finds no
|
||||
device) and on one laptop (where it gets as far as the PHY). The second driver
|
||||
was worth more than the first: it found the per-CPU problem, the retpoline
|
||||
problem, the port-I/O problem, the interrupt storm and a seven-year-old hole in
|
||||
the TSS. Nothing here has been tried on a second kernel, and "LDLS runs Linux
|
||||
drivers" is still a claim about a category from a sample of three.
|
||||
- **No interrupts**, per the polling note above.
|
||||
- **`jiffies` is not `HZ`.** It comes from a 100 Hz `uptime()`, which is a
|
||||
plausible `HZ` and not necessarily the one the module was built for. A driver
|
||||
measuring a timeout against it gets a longer timeout, which is the safe
|
||||
direction and is still wrong.
|
||||
- **`jiffies` assumes `HZ` is 1000**, which is what the kernels LDLS loads from
|
||||
are built with (`ldls_load.hz` can say otherwise). It comes from the
|
||||
monotonic clock in milliseconds.
|
||||
- **One work item at a time, on the main loop.** A driver whose deferred work
|
||||
blocks stops everything, including the transmit path it is probably waiting
|
||||
for.
|
||||
|
|
@ -363,14 +422,21 @@ point of having built it against our own:
|
|||
process, which is a leak in the strict sense and the right trade for a
|
||||
program that will be killed to restart it.
|
||||
- **BTF has to come from somewhere.** SchOS has no `/sys/kernel/btf/vmlinux`,
|
||||
so the user brings it with the module, from the kernel the module was built
|
||||
for. A kernel built without `CONFIG_DEBUG_INFO_BTF` has neither.
|
||||
so `make ldls-stage` / `ldls-r8169` copy it from the Linux kernel the module
|
||||
came from. A kernel built without `CONFIG_DEBUG_INFO_BTF` has neither.
|
||||
- **There is address-space groundwork nothing uses.** `kernel/arch/x86_64/vm.c`
|
||||
maps the kernel image a second time at `LDLS_KALIAS`, in the top 2 GB where a
|
||||
`-mcmodel=kernel` module can reach, with a `vmemmap` and a module area beside it
|
||||
— at every boot — and `include/ldls/ldls.h` describes a `/dev/ldls` control
|
||||
channel to load modules into the kernel. That was the start of an in-kernel
|
||||
loader, contradicting rule 2. Nothing calls either, and it should be finished
|
||||
as a decision or deleted.
|
||||
- **No config-space writes.** `/dev/pci` has none, deliberately — a write is
|
||||
how a BAR gets moved out from under the kernel — so a driver that needs one
|
||||
is refused loudly rather than quietly succeeded.
|
||||
- **No module-to-module dependencies, no firmware loading, no sysfs, no
|
||||
unload.** `request_firmware` fails, `module_exit` is never called, and the
|
||||
way to stop a driver is to kill the process.
|
||||
- **No sysfs and no unload.** Modules depend on each other and firmware is read
|
||||
from `/lib/firmware`, but `module_exit` is never called, and the way to stop a
|
||||
driver is to kill the process.
|
||||
- **Some symbols are still logging stubs.** Deliberate, per rule 5 — and the
|
||||
single most expensive thing in this layer. **A stub that returns zero is not
|
||||
safe when zero is a meaningful answer**, and the list of ways that bit is
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
# Networking
|
||||
|
||||
ARP, IPv4, ICMP, UDP, TCP and DHCP, over a driver-agnostic interface class,
|
||||
reached through the same socket syscalls as Unix domain sockets.
|
||||
reached through the same socket syscalls as Unix domain sockets — plus DNS
|
||||
(resolved in the process, by musl), HTTPS (`wget`, Alpine's TLS libraries) and a
|
||||
small netlink responder for `getifaddrs`.
|
||||
|
||||
Verified end to end rather than by inspection: `bootcheck -Net` starts a
|
||||
listener and a UDP responder on the host, tells the guest to talk to them, and
|
||||
|
|
@ -25,7 +27,9 @@ that completes a handshake and drops every byte.
|
|||
└────────────────┬───────────────────┘
|
||||
│ struct netdev_ops
|
||||
┌────────────────┴───────────────────┐
|
||||
│ e1000, and any future NIC driver │
|
||||
│ e1000 rtl8139 r8169 — or a │ in the kernel
|
||||
│ Linux driver in a process, which │ (LDLS, via /dev/netdev)
|
||||
│ is a netdev like any other │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
|
|
@ -86,50 +90,55 @@ covers two decades of silicon — and why the driver prints the chip's XID on
|
|||
probe, since the PCI device id tells you almost nothing about which part
|
||||
arrived.
|
||||
|
||||
### Why it is native, when the plan said LDLS
|
||||
### Native first, then Linux's own
|
||||
|
||||
The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md). Measuring
|
||||
it is what changed the answer: the module has **183 undefined symbols, 130 of
|
||||
them not implemented**, and 35 of those 130 are **phylib** —
|
||||
`phy_connect_direct`, `phy_start`, `mdiobus_read`, `genphy_soft_reset`.
|
||||
The first plan was to run Linux's `r8169.ko` under [ldls.md](ldls.md), and
|
||||
measuring it changed that: the module has **183 undefined symbols, 130 of them not
|
||||
implemented**, and 35 of those 130 are **phylib** — `phy_connect_direct`,
|
||||
`phy_start`, `mdiobus_read`, `genphy_soft_reset`. phylib is a subsystem with a link
|
||||
state machine, not a set of leaf calls, and LDLS's usual answer for an
|
||||
unimplemented symbol — a stub that reports itself and returns zero — is precisely
|
||||
wrong for it: `phy_start` returning zero means the link never comes up and nothing
|
||||
says why. So a **native driver** came first: it reaches the PHY through the
|
||||
chip's own MII window (`PHYAR`, one register that is a whole MII bus) and skips
|
||||
the subsystem, in the shape of the e1000 beside it.
|
||||
|
||||
phylib is a subsystem with a link state machine, not a set of leaf calls, and
|
||||
LDLS's usual answer for an unimplemented symbol — a stub that reports itself
|
||||
and returns zero — is precisely wrong for it. `phy_start` returning zero means
|
||||
the link never comes up and there is nothing on screen to say why. The 8139too
|
||||
that ldls.md uses as its model has no phylib dependency, which is why that one
|
||||
was tractable.
|
||||
It has since been done the other way as well. LDLS grew a driver core, devres,
|
||||
firmware loading and phylib's tables ([ldls.md](ldls.md), *A set of modules*),
|
||||
and the ASUS TUF A15's RTL8168H is now driven by **Linux's r8169, unmodified, in
|
||||
a process**: `make ldls-r8169` stages it with libphy, mdio-bus and the PHY driver,
|
||||
and `etc/schinit/ldls-r8169.job` starts it at boot. The native driver is **out of
|
||||
the registry** — it would claim the device first — and stays in the tree, with
|
||||
Linux's 8168H setup, as a fallback.
|
||||
|
||||
A native driver reaches the PHY through the chip's own MII window (`PHYAR`,
|
||||
one register that is a whole MII bus) and skips the subsystem entirely. It is
|
||||
less work than the port, it carries no GPL entanglement, it needs no firmware
|
||||
blob, and it is not coupled to one Linux version.
|
||||
|
||||
The register layout is a *fact about the hardware*, taken the way
|
||||
[ldls.md](ldls.md) §3 says facts may be taken. The expression is this tree's:
|
||||
a descriptor-ring driver in the same shape as the e1000 beside it.
|
||||
On that laptop the interface is `ldls0`, and it appears *after* boot, which is why
|
||||
`net_attach_late()` starts the stack when the first interface arrives instead of
|
||||
turning it away.
|
||||
|
||||
### The Chromebox has one of these
|
||||
|
||||
The Asus Chromebox CN62 "Guado" turns out to carry an RTL8111 rather than the
|
||||
Intel part its spec sheet suggests, so this driver covers it as well as the
|
||||
laptop that prompted it — which is the argument for the family match in one
|
||||
line rather than a device id per board.
|
||||
The Asus Chromebox CN62 "Guado" carries an RTL8111 rather than the Intel part its
|
||||
spec sheet suggests, so the family match (`10EC:8168`) covers it as well as the
|
||||
laptop — which is the argument for matching a family in one line rather than a
|
||||
device id per board.
|
||||
|
||||
### It has not run on hardware
|
||||
### It has not been shown to work on hardware
|
||||
|
||||
**Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139`
|
||||
and nothing else from Realtek, and the 8139 is a different programming model —
|
||||
a circular receive buffer rather than descriptors — so it is deliberately *not*
|
||||
in the match table. Claiming it would break a NIC that works.
|
||||
**Stated plainly because it cannot be tested here.** QEMU emulates `rtl8139` and
|
||||
nothing else from Realtek, and the 8139 is a different programming model — a
|
||||
circular receive buffer rather than descriptors — so it is deliberately *not* in
|
||||
the match table. Claiming it would break a NIC that works.
|
||||
|
||||
What has been verified: the driver builds, its presence changes nothing on a
|
||||
machine without the device, and with an `rtl8139` on the bus it correctly
|
||||
leaves it unclaimed (`10ec:8139 network controller [-]`).
|
||||
What has been verified: both drivers build, their presence changes nothing on a
|
||||
machine without the device, and Linux's r8169 under LDLS loads, links and
|
||||
initialises in QEMU, registering and finding no device. On the laptop it reached
|
||||
probe and read the PHY (after a bug in LDLS that made every MDIO read return
|
||||
`0x02460246` was fixed); the native 8168H bring-up — the PLL, the receive gate, the
|
||||
PHY through the OCP window — has not run on a part.
|
||||
|
||||
What has not: every register write, on a real part.
|
||||
What has not: traffic on real hardware, from either.
|
||||
|
||||
So the probe says everything a first bug report would need, with `cprintf`
|
||||
The native probe says everything a first bug report would need, with `cprintf`
|
||||
rather than `kinfo` so it reaches the screen on a machine with no serial port:
|
||||
|
||||
```
|
||||
|
|
@ -141,6 +150,9 @@ register 1, where bit 2 is link and bit 5 is autonegotiation complete. Those
|
|||
four facts separate "the chip is not there", "the chip is there and the PHY
|
||||
sees nothing", and "everything is up and the rings are wrong".
|
||||
|
||||
The kernel turns **ASPM off** on Realtek NIC links at boot, as Linux's driver
|
||||
does through `pci_disable_link_state` — which a process under LDLS cannot do.
|
||||
|
||||
### Two constants that were wrong first
|
||||
|
||||
Recorded because both would have worked, quietly:
|
||||
|
|
@ -156,7 +168,7 @@ Recorded because both would have worked, quietly:
|
|||
|
||||
## The netdev class
|
||||
|
||||
A driver publishes a [`struct netdev_ops`](../../include/netdev.h) and is then
|
||||
A driver publishes a [`struct netdev_ops`](../../include/dev/netdev.h) and is then
|
||||
finished with the network stack forever: it never mentions lwIP, and the stack
|
||||
never mentions it.
|
||||
|
||||
|
|
@ -262,6 +274,12 @@ to. Measured across three pings: `rx_frames` and `wake_rx` both went 2 → 6.
|
|||
table, and it returns immediately: everything happens in that thread, which
|
||||
cannot run until the scheduler is entered.
|
||||
|
||||
**The thread follows each device's link into lwIP.** The link used to be read once
|
||||
at attach, 0.02 s into boot; real autonegotiation takes seconds, so on hardware
|
||||
the interface stayed down for good. It also starts the stack when an interface
|
||||
appears after boot (a driver in a process, a USB NIC), and attaches every device
|
||||
there is as its first act; `net_eth_attach()` refuses one it already has.
|
||||
|
||||
DHCP obtains an address at boot. `/proc/netif` reports each interface's address,
|
||||
netmask, gateway, MAC and frame counts, taken at the lwIP seam.
|
||||
|
||||
|
|
@ -326,19 +344,20 @@ The creation lives in `sys_bind()` rather than here, because that is the layer
|
|||
with both: `kernel/net/unix.c` knows about sockets and has no business calling
|
||||
`create()`.
|
||||
|
||||
**`flock()` is still missing.** libwayland locks `wayland-0.lock` beside the
|
||||
socket so that two compositors cannot claim one display. The lock file can be
|
||||
created and opened now that paths are real; the advisory lock itself is a
|
||||
separate mechanism with no other caller yet.
|
||||
`flock()` and `fcntl` record locks exist (`kernel/fs/lock.c`), so the lock files
|
||||
next to a socket work the way they do on Linux. Names in
|
||||
the **abstract namespace** (a `sun_path` starting with a NUL) name no file; they
|
||||
work, and `getsockname` reports them unnamed. `SO_PASSCRED` is honoured, and
|
||||
`SO_PEERCRED` answers (tmux asks).
|
||||
|
||||
### Descriptor passing
|
||||
|
||||
`sendmsg`/`recvmsg` with `SCM_RIGHTS`, which is the one thing a socket can carry
|
||||
that a pipe cannot: a reference to an object the kernel already knows, rather
|
||||
than a copy of its contents. Wayland's `wl_shm_create_pool` passes a descriptor
|
||||
and has no integer-shaped alternative, and
|
||||
[hybrid.md](../plan/hybrid.md)'s mechanism 1 is how a driver process is handed
|
||||
the device it is allowed to touch. Both wanted the same primitive.
|
||||
and has no integer-shaped alternative, and the same mechanism is how a driver
|
||||
process is handed the device it is allowed to touch ([devices.md](devices.md)).
|
||||
Both wanted the same primitive.
|
||||
|
||||
**A batch of descriptors is pinned to the stream position of the first byte of
|
||||
the message that carried it.** That single rule is the whole delivery model, and
|
||||
|
|
@ -374,9 +393,9 @@ descriptors to a position that never arrives.
|
|||
**There is no garbage collector.** An AF_UNIX socket descriptor, sent over that
|
||||
same socket and never received, holds a reference to the socket that holds the
|
||||
reference to it, and neither close can reach zero. Linux carries several hundred
|
||||
lines to break these cycles. Nothing on either plan passes a socket over a
|
||||
socket — Wayland passes memory, the driver work passes devices — so this is a
|
||||
known and bounded leak (thirty-two sockets) rather than a silent one.
|
||||
lines to break these cycles. Nothing passes a socket over a
|
||||
socket — display servers pass memory, the driver work passes devices — so this is
|
||||
a known and bounded leak (thirty-two sockets) rather than a silent one.
|
||||
|
||||
### AF_INET
|
||||
|
||||
|
|
@ -593,59 +612,51 @@ information was there and the code did not ask.
|
|||
|
||||
## What it does not do
|
||||
|
||||
- **`ping` is a first-party program**, `cmd/ping`, not toybox's. `recvmsg` now
|
||||
exists, so half of what stopped toybox's is gone; what remains is
|
||||
`IP_RECVTTL`, an `IPPROTO_IP` control message that would have to be generated
|
||||
on the receive path rather than merely carried. Ours has no `-I`, no flood
|
||||
mode and no TTL display.
|
||||
- **`SCM_RIGHTS` is the only control message.** `SCM_CREDENTIALS` is refused
|
||||
with `EINVAL` rather than ignored: a caller told its credentials were passed
|
||||
would have the receiver read a uid out of uninitialised memory and act on it.
|
||||
- **`ping` is a first-party program**, `cmd/ping`, not toybox's or busybox's. Ours
|
||||
has no `-I`, no flood mode and no TTL display (`IP_RECVTTL` is an `IPPROTO_IP`
|
||||
control message that would have to be generated on the receive path), and takes a
|
||||
dotted quad or `localhost` — it does not link musl's resolver. Busybox's `ping`,
|
||||
from the Alpine root, works over a `SOCK_RAW` ICMP socket.
|
||||
- **Only `SCM_RIGHTS` and `SCM_CREDENTIALS` travel in a control message.** Anything
|
||||
else is refused rather than ignored.
|
||||
- **A message may carry eight descriptors, and a socket eight such messages.**
|
||||
Past that `sendmsg` answers `ENOBUFS`. The cap exists because the kernel holds
|
||||
a reference to each one until it is received; a sender eight unreceived
|
||||
messages ahead of its receiver is not rescued by a ninth slot.
|
||||
- **`ping` still takes a dotted quad**, even though names now work. It is a
|
||||
first-party program that does not link musl's resolver, and `localhost` is
|
||||
special-cased in it rather than resolved. Every *ported* program resolves
|
||||
names properly; ours is the one that does not.
|
||||
- **Nothing re-runs `resolvconf`.** The nameserver is written once at boot. A
|
||||
lease that changes the DNS server, or a `/dev/pci` handover that brings up a
|
||||
new interface, leaves a stale `/etc/resolv.conf` until somebody runs it again.
|
||||
- **lwIP's own DNS client is still unused.** It is compiled in and only
|
||||
`dns_getserver()` is ever called; resolution happens entirely in the process.
|
||||
Nothing in the kernel can resolve a name, which matters the day something in
|
||||
the kernel wants to.
|
||||
- **`httpd` does not serve HTTP/1.1.** Every response is HTTP/1.0 with
|
||||
`Connection: close`, because the directory listing is generated straight into
|
||||
the socket and so has no `Content-Length` to give. One request per connection
|
||||
is the cost.
|
||||
- **Nothing re-runs `resolvconf`.** The nameserver is written once at boot
|
||||
(`etc/schinit/resolvconf.job`). A lease that changes the DNS server, or a
|
||||
`/dev/pci` handover that brings up a new interface, leaves a stale
|
||||
`/etc/resolv.conf` until somebody runs it again.
|
||||
- **lwIP's own DNS client is unused.** Resolution happens entirely in the process
|
||||
(musl); nothing in the kernel can resolve a name.
|
||||
- **`httpd` serves HTTP/1.0** with `Connection: close`, one request per connection,
|
||||
and has no timeouts: a client that connects and says nothing ties up a child
|
||||
process until it goes away, and `SO_RCVTIMEO` does not exist here.
|
||||
- **A queued-but-never-accepted connection is closed without clearing its
|
||||
callbacks.** `inet_close()` clears the argument and closes, which is enough
|
||||
because the pcb is freed — but it is a narrower guarantee than the one the
|
||||
main path makes, and it relies on `tcp_close()` succeeding.
|
||||
- **`httpd` has no timeouts.** A client that connects and says nothing ties up
|
||||
a child process until it goes away; `SO_RCVTIMEO` does not exist here (see
|
||||
below), so there is nothing to set.
|
||||
- **No routing table**, and no `/proc/net/route` to report one. With two
|
||||
interfaces up, the first is the default route and the second is reachable only
|
||||
by an address on its own subnet. That is a rule rather than an accident, and it
|
||||
is not routing. The missing *file* is the more immediately visible half: it is
|
||||
how a program asks which interface carries the default route, and without it
|
||||
anything that filters on "the default one" sees nothing.
|
||||
- **Netlink serves two message types.** No `RTM_GETROUTE`, no `RTM_NEWROUTE`, no
|
||||
event stream, no multicast groups. A program that expects to *subscribe* to
|
||||
link changes gets a socket that answers dumps and never speaks again.
|
||||
- **No `getsockname`/`getpeername`**, and `accept()` does not report the peer's
|
||||
address for an AF_INET socket. A server that wants to log who connected cannot.
|
||||
- **`setsockopt` covers six options**, and refuses the rest with `ENOPROTOOPT`
|
||||
rather than pretending: `SO_REUSEADDR`, `SO_KEEPALIVE`, `SO_BROADCAST` and
|
||||
`TCP_NODELAY` reach lwIP's pcb; `SO_SNDBUF`/`SO_RCVBUF` are accepted and
|
||||
report the real size on read. Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`.
|
||||
- **`SOCK_DGRAM` over AF_UNIX does not exist** — stream only.
|
||||
- **AF_UNIX names are not filesystem paths**, so nothing can `stat()` or
|
||||
`unlink()` a socket.
|
||||
- **One NIC driver exists.** The class is proven with two e1000 cards, not with
|
||||
two different drivers.
|
||||
- **No IPv6**, no DNS resolver in the kernel (lwIP's is compiled but unused by
|
||||
anything), and no firewall or packet filter of any kind.
|
||||
because the pcb is freed, but it is a narrower guarantee than the main path
|
||||
makes and relies on `tcp_close()` succeeding.
|
||||
- **No routing table**, and no `/proc/net/route`. With two interfaces up, the first
|
||||
is the default route and the second is reachable only by an address on its own
|
||||
subnet. That is a rule rather than an accident, and it is not routing.
|
||||
- **Netlink is a responder**: `RTM_GETLINK` and `RTM_GETADDR` dumps (what
|
||||
`getifaddrs` needs), `MSG_PEEK|MSG_TRUNC` sizing (GLib's network monitor measures
|
||||
each datagram that way and spun at 100% when it answered 0), and nothing else. No
|
||||
`RTM_GETROUTE`, no event stream, no multicast groups; a program that expects to
|
||||
*subscribe* to link changes gets a socket that answers dumps and never speaks
|
||||
again.
|
||||
- **`setsockopt` applies what it understands** (`SO_REUSEADDR`, `SO_KEEPALIVE`,
|
||||
`SO_BROADCAST`, `TCP_NODELAY` reach lwIP's pcb; `SO_SNDBUF`/`SO_RCVBUF` report the
|
||||
real size; `SO_PASSCRED` on Unix sockets) and refuses the rest with `ENOPROTOOPT`.
|
||||
Notably absent: `SO_RCVTIMEO`/`SO_SNDTIMEO`.
|
||||
- **`SOCK_DGRAM` and `SOCK_SEQPACKET` over AF_UNIX** keep message boundaries (each message carries a
|
||||
16-byte header in the same ring) but have no unconnected form: `socketpair` and listeners work,
|
||||
`sendto` a path does not.
|
||||
- **Netlink `NETLINK_KOBJECT_UEVENT`** is the one multicast family: sockets bound to group 1 hear the
|
||||
kernel's device events, group 2 what udevd re-announces, and a datagram can be addressed to a port id.
|
||||
- **No IPv6**, no firewall or packet filter of any kind, and a TCP stack that has
|
||||
been used for downloads at tens of megabits and no more than that.
|
||||
- **Three drivers in the kernel, one Linux driver outside it**, and the class is
|
||||
proven with two e1000 cards and an LDLS handover, not on a wide range of
|
||||
hardware. The Realtek parts have never carried a packet on real silicon from this
|
||||
tree.
|
||||
|
|
|
|||
102
docs/architecture/packages.md
Normal file
102
docs/architecture/packages.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Packages: an apk root
|
||||
|
||||
Alpine is musl with a different init, and SchOS is musl with a different kernel.
|
||||
Alpine's binaries run on SchOS unmodified — its dynamic linker, its busybox, and
|
||||
`apk` itself, fetching over TLS through SchOS's network stack. So instead of
|
||||
porting programs one at a time, SchOS installs them.
|
||||
|
||||
## The model
|
||||
|
||||
The root filesystem is an apk-managed root:
|
||||
|
||||
| Part | Where it comes from |
|
||||
|---|---|
|
||||
| musl, busybox, apk-tools, alpine-keys, CA certificates | Alpine's minirootfs (`ports/alpine`) |
|
||||
| what `ports/alpine/world` lists (zsh, Xorg, Window Maker, htop, NetSurf …) | Alpine's packages, pinned in `world.lock` |
|
||||
| schinit, its jobs, SchOS's tools, toybox, mksh, configuration | **schos-base**, an apk package the build makes from `sysroot/` |
|
||||
|
||||
`apk add` installs anything from Alpine's main and community repositories.
|
||||
`apk upgrade` updates Alpine's packages, and schos-base from the SchOS repository
|
||||
at `/var/lib/schos/repo`.
|
||||
|
||||
PID 1 is still schinit. Alpine's OpenRC is not installed, and packages that ship
|
||||
OpenRC services install them where nothing reads them.
|
||||
|
||||
## Who owns what
|
||||
|
||||
Where Alpine and SchOS both have a program, Alpine's wins, and SchOS keeps only
|
||||
what is its own (`tools/posix/mkaroot.sh` says so in its header):
|
||||
|
||||
- `/bin/sh`, `cat`, `sed`, `find` and the rest are busybox. Alpine's package scripts
|
||||
are written against busybox.
|
||||
- zsh is Alpine's. SchOS keeps its `zshrc` and `zshenv` (`replaces=zsh`).
|
||||
- `/etc/os-release` and `/etc/motd` are SchOS's (`replaces=`).
|
||||
- `passwd`, `group` and `profile` are Alpine's. schos-base's post-install sets root's
|
||||
shell to zsh, and SchOS's profile is `/etc/profile.d/schos.sh`.
|
||||
- `pci.ids` and `usb.ids` are Alpine's (`hwdata-pci`, `hwdata-usb`): Xorg depends on
|
||||
`hwdata-pci`, and two owners of one file is an error apk will not install past.
|
||||
- mksh is `/bin/mksh`. schinit's recovery shell looks for it first because it is static:
|
||||
a broken musl cannot take recovery down with it.
|
||||
|
||||
## Building
|
||||
|
||||
make qemu boots build/schfs.img, the root as a 1 GB SchFS disk
|
||||
make iso the same root as the live system's compressed ramdisk
|
||||
make alpine-lock re-resolve world against Alpine (needs the network)
|
||||
|
||||
It is the only root; `sysroot/` is what schos-base is packaged from. Only
|
||||
`alpine-lock` asks Alpine what is current. A normal build installs exactly what
|
||||
`world.lock` names — files in `dist/alpine/`, fetched once and checked against the
|
||||
lock's sha256, and against Alpine's signatures by apk — and never asks what is newer.
|
||||
`world.lock` resolves the thirty-odd names in `world` into about 180 packages, every
|
||||
dependency included, so every image after the first is reproducible.
|
||||
|
||||
schos-base and its repository index are signed with a key made on the first build
|
||||
(`build/apk-keys/`, never committed). The root trusts it beside Alpine's keys. The
|
||||
install runs as an ordinary user with `--no-scripts`, because apk's scripts want a
|
||||
chroot; `mkaroot.sh` does by hand the one change schos-base's post-install would
|
||||
have made.
|
||||
|
||||
## What it took from the kernel
|
||||
|
||||
All of it found by running apk, and none of it specific to apk:
|
||||
|
||||
- **TCP receive window:** it reopened before the reader drained, so lwIP refused data
|
||||
and the sender waited on retransmission timers (5 KB/s). It reopens on read now, with a
|
||||
32-segment window; downloads run at tens of megabits.
|
||||
- **`mmap` address reuse:** freed ranges were never reused, so musl's allocator used up the
|
||||
256 mapping records with 780 MB of RAM free.
|
||||
- **`unlinkat` / `renameat`** returned a bare `-1`, read as `EPERM`, where apk expects
|
||||
`ENOENT`; `renameat` could not replace its target.
|
||||
- **`#!` scripts in `execve`**, and `ENOEXEC`: apk runs triggers directly.
|
||||
- **`chroot`**, and mounting devtmpfs and proc more than once.
|
||||
- Later, as bigger programs arrived: dynamic linking, threads and `futex`, demand paging,
|
||||
pseudo-terminals, `inotify`, `flock`, Unix-socket credentials. Each is in
|
||||
[kernel.md](kernel.md) or [syscalls.md](syscalls.md) because a package asked for it.
|
||||
|
||||
## The live system
|
||||
|
||||
The live ISO runs the apk root from RAM. The ramdisk is the compressed image
|
||||
([scff.md](scff.md)); at boot the volume grows to half of the free memory (4 GB of root on
|
||||
an 8 GB machine), backed by pages as they are written. Xorg and its dependencies are in the
|
||||
image already; anything else installs into that space in about a minute, over the network,
|
||||
and **is gone at reboot**.
|
||||
|
||||
## What it does not do
|
||||
|
||||
- **No installer.** Nothing is written to the machine's disks, so packages do not survive
|
||||
a reboot. Keeping them needs an installer that writes the root to a SATA or NVMe disk,
|
||||
and a kernel that finds its root there by the SchFS GPT type rather than as IDE disk 1.
|
||||
- **Not every Alpine package runs.** The ones in `world` were chosen *because* they run:
|
||||
terminal programs that draw and poll (htop, btop, tmux, mc), X clients, SDL 2 games,
|
||||
NetSurf on GTK 3. Others hit whatever the kernel does not do yet; Epiphany (WebKit) is
|
||||
one — its web-process child dies at start-up, and it has not been run to ground.
|
||||
A package is one `apk add` away, and finding what it needs is how most of the kernel's
|
||||
Linux surface got written.
|
||||
- **Fortified libc is strict, and Alpine's packages are not always correct.** `lbreakout2`
|
||||
hands `vsnprintf` a size larger than its buffer and traps on Alpine too; `world` carries
|
||||
`lbreakouthd` instead.
|
||||
- **apk's scripts and triggers do not run at build time** (`--no-scripts`), so a package
|
||||
whose script matters has to have it done by hand in `mkaroot.sh`.
|
||||
- **OpenRC services do nothing.** Anything that expects an init script to start it has to
|
||||
be given a schinit job ([init.md](init.md)).
|
||||
|
|
@ -102,9 +102,8 @@ tracked. Two rules, with the two errnos POSIX gives them:
|
|||
alone. Without it nice is not a courtesy but a way for any process to
|
||||
outrank the rest of the system by asking.
|
||||
|
||||
Both are inert today, because everything runs as root. They are written now
|
||||
because the alternative is growing a priority knob and a permission model in
|
||||
separate commits, and the window between the two is exactly the bug.
|
||||
Both bite now that there are users ([users.md](users.md)); root is exempt from
|
||||
both.
|
||||
|
||||
## Deliberately not a red-black tree
|
||||
|
||||
|
|
|
|||
|
|
@ -74,19 +74,19 @@ machine off, and Linux number 48 is the socket call. Two syscalls cannot share a
|
|||
|
||||
## What is implemented
|
||||
|
||||
Roughly 190 native numbers covering:
|
||||
About 215 native numbers (193 of them with a Linux number) covering:
|
||||
|
||||
| Area | |
|
||||
|---|---|
|
||||
| Process | fork, exec, exit, wait/waitpid/wait4/waitid, clone, kill, sessions, process groups |
|
||||
| Process | fork, exec, exit, exit_group, wait/waitpid/wait4/waitid, clone (processes and threads), futex, kill, tgkill, sessions, process groups |
|
||||
| Identity | the full real/effective/saved set for both uid and gid, groups, umask |
|
||||
| Memory | mmap, munmap, mprotect, brk/sbrk, madvise, System V shared memory |
|
||||
| Memory | mmap, munmap, mremap, mprotect, brk/sbrk, madvise, System V shared memory, memfd |
|
||||
| Filesystem | the ordinary set, the whole `*at()` family, statfs, xattr, mount/umount2 |
|
||||
| I/O | read/write, readv/writev, pread/pwrite, sendfile, poll/select/ppoll/pselect6, epoll (create, create1, ctl, wait, pwait), ioctl, fcntl |
|
||||
| Sockets | socket, bind, listen, accept, connect, sendto, recvfrom, socketpair, setsockopt, getsockopt |
|
||||
| Sockets | socket, bind, listen, accept/accept4, connect, sendto/recvfrom, sendmsg/recvmsg, socketpair, getsockname/getpeername, setsockopt/getsockopt, over AF_UNIX, AF_INET and AF_NETLINK |
|
||||
| Signals | rt_sigaction, rt_sigreturn, rt_sigsuspend, rt_sigpending, rt_sigtimedwait, sigaltstack |
|
||||
| Time | gettimeofday, clock_gettime, clock_getres, nanosleep, clock_nanosleep, alarm, timers |
|
||||
| Notification | eventfd, eventfd2, timerfd_create/settime/gettime, signalfd, signalfd4 -- see `kernel/sys/sys_event.c` |
|
||||
| Notification | eventfd, timerfd, signalfd, inotify -- see `kernel/sys/sys_event.c` |
|
||||
|
||||
### Argument fetching
|
||||
|
||||
|
|
@ -113,20 +113,19 @@ field by field, each through its own translation, or use `copyin`/`copyout`.
|
|||
|
||||
## What it does not do
|
||||
|
||||
- **`sendmsg`/`recvmsg` carry `SCM_RIGHTS` and nothing else.**
|
||||
`SCM_CREDENTIALS` is refused rather than ignored. See
|
||||
[networking.md](networking.md#descriptor-passing).
|
||||
- **No `memfd_create`.**
|
||||
- **No `getsockname` or `getpeername`**, and `accept()` does not report an
|
||||
AF_INET peer's address.
|
||||
- **`setsockopt` handles six options** and answers `ENOPROTOOPT` for everything
|
||||
else. `SO_RCVTIMEO`/`SO_SNDTIMEO` are among the refused.
|
||||
- **`clone` does not share an address space**, so it is `fork` with extra steps.
|
||||
- **`sendmsg`/`recvmsg` carry `SCM_RIGHTS` and `SCM_CREDENTIALS`**, and nothing
|
||||
else. Credentials are delivered only to a socket that asked with `SO_PASSCRED`
|
||||
(D-Bus and WebKit do); other control messages are refused rather than ignored.
|
||||
See [networking.md](networking.md#descriptor-passing).
|
||||
- **`setsockopt` applies the options it understands** — they reach lwIP's pcb or
|
||||
the Unix socket — and answers `ENOPROTOOPT` for everything else, never a
|
||||
success that did nothing. The options that take a `struct` (`SO_RCVTIMEO`,
|
||||
`SO_SNDTIMEO`) are among the refused.
|
||||
- **`sched_setaffinity` is accepted and ignored.**
|
||||
- **`getrusage` does not distinguish `RUSAGE_CHILDREN`** from `RUSAGE_SELF`, and
|
||||
fills in only the calling process's own tick count.
|
||||
- **The ABI is chosen by link address.** It works, and it is a heuristic rather
|
||||
than a fact about the binary.
|
||||
- **`/proc/<pid>/exe` is a regular file, not a symlink**, so `readlink()` on it
|
||||
fails and `ps -o comm` prints `[init]` rather than `init`. Default `ps` and
|
||||
`top` output is unaffected.
|
||||
- **The ABI is chosen by link address.** Anything linked at `0x100000` or above
|
||||
— musl's programs, and every dynamically linked one, whose interpreter is
|
||||
placed at `0x40000000` — gets the Linux map; a first-party program linked at
|
||||
`0x4000` gets the native one. It works, and it is a heuristic rather than a
|
||||
fact about the binary.
|
||||
- **`ptrace` and `seccomp`** are not implemented; a program that needs them
|
||||
gets `ENOSYS`.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<!-- SPDX-License-Identifier: GPL-2.0-or-later -->
|
||||
# Virtual terminals
|
||||
|
||||
Six of them, `/dev/tty1` through `/dev/tty6`.
|
||||
Twelve of them, `/dev/tty1` through `/dev/tty12`, one per function key.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **AltGr+F1 .. F6** | go to that terminal |
|
||||
| **AltGr+-** / **AltGr+=** | previous / next terminal *in use* |
|
||||
| **Ctrl+Alt+F1 .. F12** | go to that terminal — on the console and under X alike |
|
||||
| **Alt+F1 .. F12** | the same, on the text console only (X keeps plain Alt) |
|
||||
| **Ctrl+Alt+-** / **Ctrl+Alt+=** | previous / next terminal *in use* |
|
||||
| `chvt N` | the same, from a script or a serial console |
|
||||
|
||||
The chord is settable — `chvt -c` — for the reasons below. Each one is an independent terminal: its own screen, its own
|
||||
|
|
@ -14,8 +15,9 @@ cursor, its own line discipline, its own foreground process group and its own
|
|||
session. Two shells on two terminals are two unrelated processes that happen to
|
||||
share a keyboard and a screen.
|
||||
|
||||
A seventh is deliberately not bound. That is where a display server goes,
|
||||
and it will claim switching with `VT_SETMODE` rather than a chord.
|
||||
A display server takes one of them with `VT_SETMODE` and does its own
|
||||
switching; X switches with Ctrl+Alt+F-key, which is why that is the default
|
||||
here too.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -93,7 +95,9 @@ nothing to choose between. It records `proc->ctty` now, which is what
|
|||
|---|---|---|
|
||||
| `/dev/console` | 1 | the **active** terminal, so kernel messages follow a switch |
|
||||
| `/dev/tty` | 10 | the caller's **controlling** terminal, via `proc->ctty` |
|
||||
| `/dev/tty1`..`tty6` | 11-16 | that terminal, whether or not it is on screen |
|
||||
| `/dev/tty0` | | the active terminal, which is what Xorg and `openvt` ask for |
|
||||
| `/dev/tty1`..`tty12` | 11-22 | that terminal, whether or not it is on screen |
|
||||
| `/dev/ptmx`, `/dev/pts/N` | | pseudo-terminals, below |
|
||||
|
||||
`/dev/tty` was an alias for `/dev/console` with the same major and minor. They
|
||||
are different questions and now have different answers.
|
||||
|
|
@ -188,13 +192,13 @@ every candidate.
|
|||
|---|---|
|
||||
| **The host takes it, and the guest never sees it** | |
|
||||
| `Alt+F4`, `Alt+F2` | window manager |
|
||||
| `Ctrl+Alt+F1..F6` | display manager — and it matches **loosely**, so adding Shift does not get past it |
|
||||
| `Ctrl+Alt+F1..F12` | display manager — and it matches **loosely**, so adding Shift does not get past it |
|
||||
| `Ctrl+Alt+`digit | QEMU's own console tabs |
|
||||
| **Applications want it, and lose it permanently if the kernel eats it** | |
|
||||
| `Alt+`anything | Meta. zsh binds Meta-digit as `digit-argument`; so does every readline editor |
|
||||
| `Ctrl+`letter | control characters |
|
||||
| `Ctrl+Shift+`digit | types a character — `Ctrl+Shift+2` is `^@` |
|
||||
| `F1..F6` alone | help, and whatever a full-screen program wants |
|
||||
| `F1..F12` alone | help, and whatever a full-screen program wants |
|
||||
| **The keyboard layout wants it** | |
|
||||
| `AltGr+`letter | Polish diacritics — ą ć ę ł ń ó ś ź ż — and on some setups AltGr arrives as Ctrl+Alt |
|
||||
|
||||
|
|
@ -202,32 +206,33 @@ So the kernel ships a default and lets it be moved:
|
|||
|
||||
```sh
|
||||
chvt # report the active terminal and the current chord
|
||||
chvt -c altgr+F # AltGr+F1..F6 (the default)
|
||||
chvt -c ctrl+shift+F # Ctrl+Shift+F1..F6
|
||||
chvt -c alt+1 # Alt+1..6
|
||||
chvt -c ctrl+alt+F # Ctrl+Alt+F1..F12 (the default)
|
||||
chvt -c altgr+F # AltGr+F1..F12 (the default until 2026-10)
|
||||
chvt -c ctrl+shift+F # Ctrl+Shift+F1..F12
|
||||
chvt -c alt+1 # Alt+1..9
|
||||
chvt -c off # no chord; chvt N is then the only way
|
||||
```
|
||||
|
||||
`off` is a legitimate setting, not a failure mode. On a machine whose host
|
||||
grabs everything worth binding, `chvt N` from a shell is the honest answer.
|
||||
|
||||
The default is **AltGr+F1..F6**, and the `AltGr+`letter row above needs reading
|
||||
carefully: what a Polish layout wants is AltGr with a **letter**. AltGr with a
|
||||
**function key** produces nothing on any layout, no window manager binds it,
|
||||
and no application asks for it. It is the one combination all three parties
|
||||
leave alone.
|
||||
The default is **Ctrl+Alt+F1..F12**. It was AltGr+F-key for a long time —
|
||||
AltGr with a **function key** produces nothing on any layout, no window manager
|
||||
binds it, and no application asks for it — but X owns the keyboard while it
|
||||
runs and switches only with Ctrl+Alt+F-key, so the console and the desktop
|
||||
answered different keys for the same thing. One chord everywhere wins. Inside
|
||||
a VM whose host grabs Ctrl+Alt+F-key, `chvt -c altgr+F` brings the old one
|
||||
back.
|
||||
|
||||
That distinction is only expressible because Right Alt is tracked as its own
|
||||
modifier bit. It used to share `ALT`, which is harmless for typing — neither is
|
||||
used to look up a character — and made this impossible to say.
|
||||
|
||||
Verified rather than argued: with the default set, `AltGr+F2` switches,
|
||||
`AltGr+F1` returns to a pixel-identical tty1, and `AltGr+a` still reaches the
|
||||
shell, so ą is untouched.
|
||||
AltGr+letter still reaches the shell untouched, so ą is unaffected either way.
|
||||
|
||||
Whatever the modifiers are, they also bind **minus** and **equals** to the
|
||||
previous and next terminal. Those step only through terminals that are *in
|
||||
use* — the same `opened` count `VT_GETSTATE` reports — because six terminals
|
||||
use* — the same `opened` count `VT_GETSTATE` reports — because twelve terminals
|
||||
with two shells on them means four blank screens in between, and cycling past
|
||||
them is worse than not having the key.
|
||||
|
||||
|
|
@ -267,12 +272,10 @@ Three layers, in increasing order of how much a program is claiming:
|
|||
`VT_SETMODE` used to be accepted and ignored — a program could ask for the
|
||||
chord, be told yes, and still not get it. It works now.
|
||||
|
||||
What `VT_PROCESS` does **not** do yet is the release handshake: Linux signals
|
||||
the owning process on every switch request and blocks until it answers
|
||||
`VT_RELDISP`, which is how a display server releases the GPU before the screen
|
||||
changes hands. Nothing here has anything to release, so the kernel simply
|
||||
stops switching rather than negotiating. `struct vt_mode` already carries
|
||||
`relsig`/`acqsig` for when Schcom needs it.
|
||||
`VT_PROCESS` also does the release handshake Linux does: a switch away from a terminal in
|
||||
that mode signals the owning process (`relsig`) and waits for `VT_RELDISP`, which is how a
|
||||
display server stops drawing before the screen changes hands; switching back sends `acqsig`.
|
||||
See *Handing a terminal to a program*, below.
|
||||
|
||||
Input goes to whichever terminal is on screen. That is what makes a terminal a
|
||||
terminal: the keyboard follows the eyes.
|
||||
|
|
@ -365,34 +368,56 @@ and erase characters — were added at the same time, because `TERM=linux`
|
|||
declares them and a terminal that drops them silently is the same class of bug
|
||||
waiting for a different editor.
|
||||
|
||||
## Handing a terminal to a program
|
||||
|
||||
A display server takes a terminal with `VT_SETMODE(VT_PROCESS)` and `KDSETMODE(KD_GRAPHICS)`,
|
||||
and from then on switching away from it is a **request**: the kernel sends the owner
|
||||
`relsig` and waits for `VT_RELDISP`, so the server stops drawing and only then says
|
||||
yes; switching back sends `acqsig` and the server redraws. A terminal in graphics mode
|
||||
is not repainted by the kernel. If the owner dies, the terminal goes back to the kernel
|
||||
whole. This is how Xorg runs on a terminal of its own, with Ctrl+Alt+F-key switching
|
||||
between it and the text consoles and Ctrl+Alt+Backspace ending the session.
|
||||
|
||||
## Pseudo-terminals
|
||||
|
||||
`/dev/ptmx` makes a pair on every open and hands back the master; the slave is
|
||||
`/dev/pts/N`, which the program on the other side gets as its stdin, stdout and
|
||||
controlling terminal. Between them is a line discipline, the console's in miniature:
|
||||
what the master writes is the slave's input (canonical editing, echo, `^C` as `SIGINT`
|
||||
to the foreground group) and what the slave writes is read back by the master with NL
|
||||
turned into CR NL. xterm, tmux, `script` and `sshd` all need exactly this. The pairs
|
||||
share one lock; `kernel/dev/pty.c`.
|
||||
|
||||
## The text cursor
|
||||
|
||||
A steady inverted block — the cell with its colours swapped relative to what it would
|
||||
show, so a cursor on reverse-video text is the plain text and every cursor on screen
|
||||
is visible against its cell. `ESC[?25l` hides it, `ESC[?25h` shows it. It is drawn into
|
||||
the grid's repaint and does not blink.
|
||||
|
||||
## What it does not do
|
||||
|
||||
- **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does
|
||||
nothing because there is nothing above the top row to show.
|
||||
- **No reflow on resize.** The geometry is read once at `vt_init()`. Nothing
|
||||
resizes a framebuffer console today; a compositor will, and that is when this
|
||||
has to grow.
|
||||
- **No `VT_RELDISP` handshake.** `VT_PROCESS` stops the kernel switching, but
|
||||
the kernel does not signal the owning process and wait for an answer. A
|
||||
display server that needs to release the GPU *before* the screen changes
|
||||
hands will need that, and `struct vt_mode` already has the fields.
|
||||
- **No terminal past 6.** Linux allows 63. Six is what the chords reach and
|
||||
what the grids are sized for.
|
||||
- **No reflow on resize.** The geometry is read once at `vt_init()`; the console's
|
||||
font is sized to the screen at that point, and a resolution change from `schmode`
|
||||
repaints it, but text is not re-wrapped.
|
||||
- **No terminal past 12.** Linux allows 63. Twelve is one per function key.
|
||||
- **Only 240x75 of any screen.** Larger framebuffers work; the grid is clamped
|
||||
and the remainder stays background. Raising `VT_MAXCOLS`/`VT_MAXROWS` is the
|
||||
fix, and costs BSS.
|
||||
- **No UTF-8 the font lacks.** The console draws through the TrueType
|
||||
rasteriser now, so the limit is the font's `cmap` rather than a 256-entry
|
||||
table — Latin, Latin Extended, Greek, Cyrillic and the common punctuation and
|
||||
maths symbols all render. Anything genuinely absent still draws as `?`, which
|
||||
is deliberate: drawing a *different* character would be worse.
|
||||
rasteriser, so the limit is the font's `cmap` — Latin, Latin Extended, Greek,
|
||||
Cyrillic and the common punctuation and maths symbols all render. Anything
|
||||
genuinely absent still draws as `?`, which is deliberate: drawing a
|
||||
*different* character would be worse.
|
||||
- **No shaping, and no wide characters.** One codepoint is one cell of one
|
||||
advance. Combining marks land on top of their own cell rather than the
|
||||
preceding one, and CJK draws at half the width it wants. Both need a
|
||||
width table and a cell that can span two columns.
|
||||
- **No dead keys**, so French and Spanish layouts are absent rather than
|
||||
broken. See above.
|
||||
broken.
|
||||
- **No per-terminal layout.** One keyboard, one layout. Switching to tty3 does
|
||||
not move anybody's fingers.
|
||||
- **Only three layouts.** Adding one is a table in `keymap.c` and a line in the
|
||||
- **Only three layouts** (US, Polish programmers', German). Adding one is a table in `keymap.c` and a line in the
|
||||
catalogue; the mechanism is not the limit, the typing is.
|
||||
|
|
|
|||
138
docs/architecture/udev.md
Normal file
138
docs/architecture/udev.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# udev
|
||||
|
||||
Alpine's `eudev` runs on SchOS as an ordinary process, started by `schinit`
|
||||
(`etc/schinit/udevd.job`, with `udev-trigger.job` replaying the devices that
|
||||
were there first). It is what lets Xorg *find* its keyboard and mouse instead of
|
||||
being told, and the road to a Wayland compositor starts here: libinput and
|
||||
libseat both ask udev what exists.
|
||||
|
||||
The kernel's half is three pieces, all small, none of them specific to udev.
|
||||
|
||||
## The device table: `kernel/dev/kdev.c`
|
||||
|
||||
The devices the kernel implements itself — input event nodes, the memory
|
||||
devices, terminals, the framebuffer, the disks — are listed once, with their
|
||||
class, name, `/dev` name and parent. Everything below is generated from it.
|
||||
|
||||
The device numbers are the ones `stat()` reports for the node, not SchOS's
|
||||
internal ones: `/dev/tty1` is `4:1` to a Linux program (`linux_tty_rdev` in
|
||||
`kernel/sys/sys_fs.c`), and udev compares what it reads in `/sys` with what
|
||||
`stat()` says about the node. `dev_stat_numbers()` is the one mapping both use.
|
||||
|
||||
## `/sys`: `kernel/fs/sysfs.c`
|
||||
|
||||
```
|
||||
/sys/devices/pci0000:00/0000:00:1f.2/ vendor device class ... uevent subsystem -> ../../../bus/pci driver
|
||||
/sys/devices/pci0000:00/0000:00:14.0/usb1/1-3 idVendor idProduct ... uevent subsystem -> ../../../../../bus/usb
|
||||
/sys/bus/pci/devices/0000:00:1f.2 -> ../../../devices/pci0000:00/0000:00:1f.2
|
||||
/sys/bus/usb/devices/1-3 -> ../../../devices/pci0000:00/0000:00:14.0/usb1/1-3
|
||||
/sys/devices/virtual/input/input0/ name capabilities/{ev,key,rel,abs} properties
|
||||
/sys/devices/virtual/input/input0/event0/ dev uevent subsystem -> ../../../../../class/input
|
||||
/sys/class/input/event0 -> ../../devices/virtual/input/input0/event0
|
||||
/sys/dev/char/13:64 ... -> ../../devices/virtual/...
|
||||
```
|
||||
|
||||
Symlinks are real (`T_SYMLINK` inodes whose target is generated). `uevent` is
|
||||
readable (`DEVPATH=`, `SUBSYSTEM=`, `MAJOR=`, `MINOR=`, `DEVNAME=`) and
|
||||
writable: `echo add > uevent` — which is what `udevadm trigger` does — makes the
|
||||
kernel announce the device again. PCI and USB devices announce the same way:
|
||||
`MODALIAS`, `PCI_ID`, `PCI_CLASS`, `PCI_SLOT_NAME` for a PCI device, `PRODUCT`,
|
||||
`TYPE`, `BUSNUM`, `DEVNUM` for a USB one.
|
||||
|
||||
**PCI devices** are nested under the PCI-to-PCI bridge they sit behind: a
|
||||
device's parent is the bridge whose secondary..subordinate bus range holds its
|
||||
bus (the innermost if several do), read from the bridge's config space, so an
|
||||
xHCI behind a root port is `pci0000:00/0000:00:04.0/0000:01:00.0`. **USB
|
||||
devices** sit under the xHCI controller that carries their bus, below that
|
||||
bus's root hub `usbN`; the controller tells the USB core its own PCI address
|
||||
when it comes up, so the link is recorded, not guessed. A hub's children are
|
||||
its siblings, not its children.
|
||||
|
||||
**USB interfaces** (`1-5:1.0`) are directories below their device, with
|
||||
`bInterfaceClass`, `bInterfaceNumber`, a `driver` link when one is bound, and a
|
||||
`uevent` carrying `DEVTYPE=usb_interface`, `INTERFACE=` and a `MODALIAS=usb:...`
|
||||
that names the interface's class. They are announced after the drivers have
|
||||
been offered the device, so the event says which interface was taken, and
|
||||
removed before the device. Devices also carry what `lsusb -t` and libusb read:
|
||||
`version`, `bMaxPacketSize0`, `bNumInterfaces`, `bmAttributes`, `bMaxPower`.
|
||||
|
||||
**Hot-plug is real**: the USB core announces `add` when a device is attached
|
||||
and `remove` as it detaches, and `usbN` is announced when its controller comes
|
||||
up. With `udevadm monitor`, plugging in a mouse prints the kernel event and
|
||||
then the same event after udev's rules have run. PCI has no hot-plug here.
|
||||
|
||||
The input capability files are what `input_id` reads to decide
|
||||
`ID_INPUT_KEYBOARD` or `ID_INPUT_MOUSE`. SchOS has two input devices whatever
|
||||
the machine — every keyboard on `event0`, every mouse on `event1`
|
||||
(`kernel/dev/input.c`) — so each has a fixed, plausible capability set.
|
||||
|
||||
## `/dev`: `kernel/fs/devfs.c`
|
||||
|
||||
devtmpfs is no longer read-only: udev creates `/dev/char/MAJ:MIN`, `/dev/fd`,
|
||||
`/dev/input/by-*` and the like. A small in-memory overlay (192 entries) holds
|
||||
directories and symlinks, listed after the kernel's own nodes in whichever
|
||||
directory they are in. Device nodes themselves are still the kernel's: `mknod`
|
||||
and regular files in `/dev` are refused.
|
||||
|
||||
## The sockets: `kernel/net/unix.c`
|
||||
|
||||
- **`AF_UNIX` `SOCK_DGRAM` and `SOCK_SEQPACKET`** keep message boundaries.
|
||||
udevd's control socket is `SEQPACKET`, and its manager and workers talk over
|
||||
a `DGRAM` socketpair. A message carries its sender's credentials with it
|
||||
(several workers share one end), a zero-length datagram is a datagram, and a
|
||||
datagram socket never reads end-of-stream.
|
||||
- **`AF_NETLINK` / `NETLINK_KOBJECT_UEVENT`** sockets bind to multicast groups:
|
||||
group 1 is the kernel's events, group 2 is what udevd re-announces once its
|
||||
rules have run. A datagram can also be addressed to a port id — udevd hands
|
||||
an event to an idle worker that way, and the worker believes it only from the
|
||||
manager's port. The kernel's messages arrive as credentials with uid 0, pid 0.
|
||||
A queue that is full drops the message, as Linux does, and `udevadm trigger`
|
||||
is how a reader recovers.
|
||||
|
||||
## libinput and libseat
|
||||
|
||||
`libinput` (Alpine) reads the evdev nodes udev tagged and `xf86-input-libinput`
|
||||
gives Xorg the same library; `libinput list-devices` as an ordinary user shows
|
||||
`SchOS keyboard` and `SchOS pointer`. Xorg's `40-libinput.conf` now claims both,
|
||||
and the log says `Using input driver 'libinput'`.
|
||||
|
||||
`seatd` (job `etc/schinit/seatd.job`, socket `/run/seatd.sock`, group `video`)
|
||||
and `libseat` let a program that is not root take devices from the system. The
|
||||
client asks seatd for `seat0`; seatd takes the VT (`VT_SETMODE`, `KDSETMODE`,
|
||||
`KDSKBMODE`, all there already), opens the device as root and passes the
|
||||
descriptor back over the socket with `SCM_RIGHTS`. Checked as `schos` against
|
||||
`/dev/input/event0` and `event1` with a small program linked to libseat, reading
|
||||
the device's name through the descriptor it was handed.
|
||||
|
||||
Getting there found two kernel bugs, both in descriptor passing:
|
||||
|
||||
- **A socket returned by `accept()` was not an `AF_UNIX` socket as far as
|
||||
`sendmsg` was concerned.** The end `connect()` queues for the listener was
|
||||
allocated with its domain left at zero, and `sendmsg` refuses `SCM_RIGHTS` on
|
||||
anything else. So a *server* could receive descriptors and never send one:
|
||||
seatd, and equally a Wayland compositor handing a client a buffer.
|
||||
- **A control buffer longer than its messages was an error.** seatd passes a
|
||||
zeroed array with one descriptor at the front; Linux treats a following header
|
||||
too short to be one as the end. `sendmsg` now does too.
|
||||
|
||||
## What is not there
|
||||
|
||||
- `/dev/bus/usb` nodes (usbfs). `lsusb -t` works because it reads sysfs; plain
|
||||
`lsusb` goes through libusb, which also refuses to start while `uname -r`
|
||||
says `0.1.0` (it wants Linux 2.6.32 or newer) and wants usbfs to open devices.
|
||||
- A hub's port count (`maxchild` reads 0), and string descriptors
|
||||
(`manufacturer`, `product`, `serial`).
|
||||
- A USB keyboard works because every keyboard is `event0`; udev knows the
|
||||
interface arrived but the input device does not hang below it.
|
||||
- `hwdb` (`/etc/udev/hwdb.bin`): `udevadm hwdb --update` has not been run in the
|
||||
image, so `60-input-id` logs "returned non-zero" for the hwdb import. The
|
||||
properties that matter come from `input_id`.
|
||||
- Block device partitions, `by-id` / `by-uuid` links, and a `/sys/block`.
|
||||
- A compositor. libinput and libseat are in place, DRM is not; see [../plan/roadmap.md](../plan/roadmap.md).
|
||||
|
||||
## Testing
|
||||
|
||||
Boot the ISO and run `udevd --debug &`, then `udevadm trigger; udevadm settle`.
|
||||
`/run/udev/data/c7:0` should contain
|
||||
`ID_INPUT_KEYBOARD=1`, and `udevadm info /dev/input/event0` should print the
|
||||
device. Under X, `/var/log/Xorg.0.log` says `config/udev: Adding input device`.
|
||||
|
|
@ -21,13 +21,14 @@ The specification is public and the controller is not complicated.
|
|||
> BSD is much more valuable as the reference you check yourself against than as
|
||||
> the code you paste.
|
||||
|
||||
Licensing points the same way: the BSD stacks are BSD-2/3-clause and compatible,
|
||||
Linux's drivers are GPL-2.0-only and are not — see
|
||||
[../licensing.md](../licensing.md).
|
||||
Licensing does not decide it: the BSD stacks are BSD-2/3-clause and compatible, and
|
||||
since SchOS became GPL-2.0-or-later Linux's GPL-2.0-only drivers are compatible too
|
||||
— see [../licensing.md](../licensing.md). The argument against porting is the
|
||||
framework underneath, not the licence.
|
||||
|
||||
## What the spec gives you for free
|
||||
|
||||
Everything in [`include/dev/usb.h`](../../include/usb.h) comes from USB 2.0
|
||||
Everything in [`include/dev/usb.h`](../../include/dev/usb.h) comes from USB 2.0
|
||||
chapter 9, which every device implements regardless of speed, class or vendor.
|
||||
|
||||
**That is what makes enumeration possible at all:** before you know anything
|
||||
|
|
@ -129,7 +130,8 @@ working one.
|
|||
filesystem's next read, by the rule in `blkdev_rw()`: a filesystem that went on
|
||||
with missing blocks would be guessing.
|
||||
- **HID is keyboards and pointers.** Consumer-control interfaces (volume keys),
|
||||
game pads and the rest are declined. Keyboard LEDs are not driven.
|
||||
game pads and the rest are declined. Caps, Num and Scroll Lock light their LEDs
|
||||
on USB and PS/2 keyboards, one state shared between keyboards.
|
||||
- **USB 3 hubs** are not driven: the hub driver speaks the USB 2 hub protocol.
|
||||
- **No isochronous transfers**, so no audio and no cameras.
|
||||
- **No USB 3 SuperSpeed-specific features** beyond what enumeration needs — the
|
||||
|
|
|
|||
79
docs/architecture/users.md
Normal file
79
docs/architecture/users.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Users and permissions
|
||||
|
||||
SchOS has accounts, a login prompt, and file permissions that the kernel
|
||||
enforces. Until now every process was root and `uid` was bookkeeping; this is
|
||||
the part that makes it a multi-user system.
|
||||
|
||||
## What you meet
|
||||
|
||||
- **`login` on all twelve terminals, tty1–tty12.** `etc/schos-getty` takes the
|
||||
terminal back from whoever had it and runs `login`, which asks who is there
|
||||
and starts their shell; when the shell exits `schinit` starts the prompt
|
||||
again. Alt+F1…F12 (or Ctrl+Alt+F1…F12) switches between them. (There is no
|
||||
longer a login-free root shell on tty6.)
|
||||
- **One account in the image: `root`**, with an empty password, so `login` lets
|
||||
it in without asking — it is a live image. `passwd` sets one. More users are
|
||||
an `adduser` away; `wheel` is empty, so `su` and `doas` have no one to serve
|
||||
until then.
|
||||
- **`su` and `passwd`** (`cmd/su`, `cmd/passwd`) are setuid root and small
|
||||
programs of their own, not toybox applets: toybox is one binary under every
|
||||
name, and a setuid copy of it can be asked for any applet by naming it, as
|
||||
root. `su` to root takes membership in `wheel`, whatever the password.
|
||||
- **`doas`** (Alpine's, setuid) with `permit nopass :wheel` in `/etc/doas.conf`.
|
||||
|
||||
## What the kernel enforces
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| open, create, unlink, rename, link | the usual owner/group/other bits, `w`+`x` on the directory, and the sticky bit (`/tmp`) |
|
||||
| path walk | `x` on every directory searched |
|
||||
| `exec` | `x` on the file; set-user-id and set-group-id make the new image run as its owner or group, and the saved ids follow |
|
||||
| `chmod`, `chown`, `utimensat` | owner or root; a non-root `chown` drops the set-id bits |
|
||||
| `access` / `faccessat` | by the *real* ids (`AT_EACCESS` for the effective ones) |
|
||||
| `kill`, `tkill`, `tgkill` | root, or a target whose real or saved uid is one of yours. The kernel's own signals (`SIGPIPE`, the terminal's) are nobody's to refuse |
|
||||
| `mount`, `umount`, `chroot`, `reboot`, `shutdown`, `setgroups`, `mknod` of a device | root |
|
||||
| `setuid` family | root may set anything; anyone else only to an id they already hold |
|
||||
|
||||
Root reads and writes everything and executes a file only if some execute bit
|
||||
is set. A process's supplementary groups (`setgroups`) are consulted along with
|
||||
its effective group. Ownership is stored by SchFS (it always had the fields);
|
||||
FAT volumes belong to everyone, since they cannot say otherwise, and `/proc`
|
||||
and `/sys` are root's.
|
||||
|
||||
`inode_permission()` in `kernel/fs/fs.c` is the one check; the rest of this is
|
||||
call sites.
|
||||
|
||||
## Devices
|
||||
|
||||
`/dev` is built by the kernel and each node has the owner, group and mode in the
|
||||
`devnodes[]` table of `kernel/fs/devfs.c`: `null` and `zero` are 0666,
|
||||
terminals `root:tty 0660`, `fb0` `root:video`, input `root:input`, raw disks
|
||||
`root:disk`, `pci`, `dbg` and `netdev` root's alone. `chmod` and `chown` on a
|
||||
node are kept (login claims its terminal this way, and udev sets modes) in an
|
||||
override table applied whenever the inode is loaded. A new pseudo-terminal's
|
||||
slave belongs to whoever opened `ptmx`.
|
||||
|
||||
udev would set a node it has no rule for to 0600, which made `/dev/null`
|
||||
root-only, so `etc/udev-schos.rules` (installed as `90-schos.rules`) names the
|
||||
kernel's devices with the answers the kernel gave.
|
||||
|
||||
## The image
|
||||
|
||||
Everything in the image is owned by root: the build host's user means nothing
|
||||
inside it. `mkschfs --owners FILE` takes the exceptions, one `path uid gid
|
||||
[mode]` per line (`tools/posix/mkaroot.sh` writes it: the modes of `/tmp`,
|
||||
`/var/tmp` and `/root`). The setuid bits are set
|
||||
on `su`, `passwd` and `doas` in the root tree directly, because `apk` run as an
|
||||
ordinary user may have dropped them.
|
||||
|
||||
## Not done
|
||||
|
||||
- **No PAM and no `/etc/securetty`.** `login` is busybox's and checks
|
||||
`/etc/shadow` itself.
|
||||
- **No capabilities.** Root is `euid == 0`, all of it.
|
||||
- **No per-user `/run`, no logind.** X as a non-root user works (`startx` as `schos`, under
|
||||
QEMU: Window Maker up) by group permission alone — video, input and tty — and
|
||||
`/etc/X11/Xwrapper.config` lets anyone start it, because the wrapper's "console users" test
|
||||
looks for a Linux VT on a standard descriptor that xinit's server child does not keep.
|
||||
- **`/proc` and `/sys` hide nothing from other users.**
|
||||
- **`mprotect` is still not enforced** ([kernel.md](kernel.md)).
|
||||
|
|
@ -192,7 +192,7 @@ If either is edited, edit both.
|
|||
|---|---|
|
||||
| Loader splash | Shift, from `include/gfx/logo.h`, shaded per row by `metal()` in `boot/common/ui.c` — silver, not accent. |
|
||||
| Kernel console / early boot | Shift, same mask, composited flat in a theme colour by `gfx_draw_logo`. |
|
||||
| A display with no compositor | Shift again, drawn by `vgpud` on `SCH_BG`. It replaced a test card — colour bands, gradients, a checkerboard and the driver's pid in binary — which was scaffolding for [../plan/hybrid.md](../plan/hybrid.md)'s H2 and H3 and outlived them. The mark answers the same question a black screen cannot, *something is driving this device*, and answers it as the system rather than as a test rig. |
|
||||
| A display with no compositor | Shift again, drawn by `vgpud` on `SCH_BG`. It replaced a test card — colour bands, gradients, a checkerboard and the driver's pid in binary — which was scaffolding for [../architecture/devices.md](../architecture/devices.md)'s driver work and outlived them. The mark answers the same question a black screen cannot, *something is driving this device*, and answers it as the system rather than as a test rig. |
|
||||
| PSOGM | Shift, the same mask again, by `gfx_draw_logo()` from `kernel/core/panic.c`. At its native 128 px — there is no resampler in the crash path, and the mark was only ever drawn at one size there anyway. |
|
||||
| Serial crash report | `:(`, as text. The mark cannot be drawn in ASCII, and a mood marker in a log is not a mark. |
|
||||
| SchFS | **Nowhere yet.** `uberblock.svg` has no consumer; it is waiting for `projects/schfs-linux/` to have a README worth heading. |
|
||||
|
|
|
|||
|
|
@ -12,12 +12,20 @@ virtio, NVMe and the Intel datasheets are public documents with no licence on
|
|||
what you write from them. `kernel/drivers/bus/xhci/xhci.c` came from the spec,
|
||||
and so did `net/e1000/` and `gpu/virtio_gpu/`.
|
||||
|
||||
**BSD: yes. Linux: no.** This is a licence incompatibility rather than a
|
||||
preference, and it decides the whole strategy — see
|
||||
[../licensing.md](../licensing.md). BSD-2 and BSD-3 code can be used directly
|
||||
with a `NOTICE` entry; GPL-2.0-only code can never be, at any distance. Reading
|
||||
Linux to understand *hardware* is fine and normal; translating a Linux driver
|
||||
function by function produces a derivative work whatever the identifiers say.
|
||||
**Three sources, in the order to try them:**
|
||||
|
||||
1. **A native driver, written from the specification.** The default, and what every driver in the
|
||||
kernel is.
|
||||
2. **Linux's own driver, unmodified, through LDLS** ([ldls.md](../architecture/ldls.md)), when the
|
||||
device is one Linux supports and SchOS cannot afford to write a driver for — a NIC with a PHY
|
||||
behind it, a GPU, a Wi-Fi card. It runs in a process, so a crash costs the interface and not the
|
||||
machine. This is how the laptop's Realtek NIC is driven.
|
||||
3. **BSD or Linux code taken into the tree.** Licensing no longer forbids either: SchOS is
|
||||
GPL-2.0-or-later, so GPL-2.0-only code combines with it, and BSD-2/3 code can be used directly with
|
||||
a `NOTICE` entry — see [../licensing.md](../licensing.md). The reason to prefer 1 and 2 is
|
||||
maintenance, not law: a driver copied in is a driver that has to be tracked.
|
||||
|
||||
Reading another kernel to understand *hardware* is fine and normal.
|
||||
|
||||
One caveat about "port it from BSD" as a plan, learned by looking: FreeBSD's
|
||||
`sys/dev/usb` is roughly 100k lines welded to newbus, devclass, `bus_dma`,
|
||||
|
|
@ -45,22 +53,29 @@ ours.
|
|||
kernel/drivers/
|
||||
registry.c
|
||||
gpu/vga/ gpu/virtio_gpu/
|
||||
net/e1000/
|
||||
net/e1000/ net/rtl8139/ net/r8169/
|
||||
storage/ahci/ storage/nvme/
|
||||
audio/ac97.c
|
||||
bus/xhci/
|
||||
usb/core/ usb/hid/ usb/storage/ usb/hub/
|
||||
|
||||
2. Declare it in `kernel/drivers/registry.c` and add it to the array.
|
||||
3. Add the object to `KERN_DRIVERS` in the `Makefile`.
|
||||
3½. If nothing else in the kernel calls it, make it optional: a `DRV_<NAME>` line in
|
||||
`config/drivers.conf`, the name in `DRV_LIST` in the Makefile, the object behind
|
||||
`$(filter y,$(DRV_<NAME>))`, and an `#ifdef CONFIG_DRV_<NAME>` around its line in `registry.c`.
|
||||
A driver other code calls (`vga_fb`, the USB stack) cannot be optional.
|
||||
4. Fill in the **class vtable** for what it is, and point `.ops` at it. That is
|
||||
the whole of what the rest of the kernel sees:
|
||||
|
||||
| class | vtable | consumer |
|
||||
|---|---|---|
|
||||
| `DEVCLASS_DISPLAY` | `struct display_ops` ([display.h](../../include/display.h)) | the compositor |
|
||||
| `DEVCLASS_NET` | `struct netdev_ops` ([netdev.h](../../include/netdev.h)) | the TCP/IP stack |
|
||||
| `DEVCLASS_DISPLAY` | `struct display_ops` ([display.h](../../include/gfx/display.h)) | none today — Xorg draws through `/dev/fb0` |
|
||||
| `DEVCLASS_NET` | `struct netdev_ops` ([netdev.h](../../include/dev/netdev.h)) | the TCP/IP stack |
|
||||
| `DEVCLASS_AUDIO` | `struct audio_ops` | `/dev/dsp` |
|
||||
|
||||
A display driver that owns a linear framebuffer also publishes it with
|
||||
`gfx_attach_scanout()` ([gfx.h](../../include/gfx.h)). That is what the text
|
||||
`gfx_attach_scanout()` ([gfx.h](../../include/gfx/gfx.h)). That is what the text
|
||||
console and the crash screen draw on, and it is separate from the display
|
||||
class on purpose: the class is about *frames* — acquire, present, vblank —
|
||||
and the crash screen has to work when nothing else in the system does.
|
||||
|
|
@ -102,7 +117,7 @@ That is data; the logic stays ours.
|
|||
- [ ] One directory under `kernel/drivers/<class>/<name>/`
|
||||
- [ ] Exports a `struct pci_driver` or `struct usb_driver`
|
||||
- [ ] Declared in `kernel/drivers/registry.c` **and** added to its array
|
||||
- [ ] Object added to `KERN_DRIVERS` in the `Makefile`
|
||||
- [ ] Object added to `KERN_DRIVERS` in the `Makefile`, and an option in `config/drivers.conf` if it can be left out
|
||||
- [ ] Class vtable filled in and `.ops` pointed at it
|
||||
- [ ] `.attached` implemented if it can drive more than one card
|
||||
- [ ] `devclass` set, if it is a USB driver
|
||||
|
|
|
|||
|
|
@ -8,8 +8,18 @@ change that only works on one of them is a bug.
|
|||
```sh
|
||||
make # kernel + UEFI loader + root filesystem
|
||||
make qemu # build and boot it
|
||||
make iso # a bootable ISO (the live system) for VMware or real hardware
|
||||
./write.sh sdX # write that ISO to a USB stick (Linux; it checks what it is writing over)
|
||||
```
|
||||
|
||||
The first build downloads what it needs (musl, toybox, Alpine's packages -- a few
|
||||
hundred megabytes, kept in `dist/`) and compiles Xorg's dependencies not at all: the
|
||||
packages are Alpine's binaries, pinned in `ports/alpine/world.lock`. See
|
||||
[packages](../architecture/packages.md).
|
||||
|
||||
Which drivers go into the kernel is `config/drivers.conf` (all of the optional ones by default);
|
||||
`make DRV_E1000=n` leaves one out for a build. See [devices](../architecture/devices.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Need | Why | Windows | Linux | macOS | BSD |
|
||||
|
|
@ -17,7 +27,7 @@ make qemu # build and boot it
|
|||
| `clang`, `ld.lld`, `llvm-objcopy`, `llvm-objdump` | cross-compiles the kernel, the 16/32-bit boot code, and musl userland | LLVM installer | `clang lld llvm` | `brew install llvm` | `pkg install llvm` |
|
||||
| `make` | — | Git Bash | `make` | Xcode CLT | `gmake` |
|
||||
| `perl` | generates the interrupt vector table | Git Bash | usually present | present | `pkg install perl5` |
|
||||
| `python3` | boot logo, crash-screen QR | python.org (`python`) | `python3` | `brew install python` | `pkg install python3` |
|
||||
| `python3` | boot logo, crash-screen QR, the numbers in the README | python.org (`python`) | `python3` | `brew install python` | `pkg install python3` |
|
||||
| `qemu-system-x86_64` | running and testing | `scoop install qemu` | `qemu-system-x86` | `brew install qemu` | `pkg install qemu` |
|
||||
| `curl`, `tar` | fetching ports | Git Bash | present | present | present |
|
||||
| `xorriso` **or** `mkisofs` | `make iso` only | `scoop install cdrtools` | `xorriso` | `brew install xorriso` | `pkg install xorriso` |
|
||||
|
|
@ -81,10 +91,11 @@ produce.
|
|||
|
||||
## Ports
|
||||
|
||||
`musl`, `toybox` and `mksh` are built from source into `sysroot/`. **This does
|
||||
not work on a Windows host** — `make` prints `ports: skipped` and uses whatever
|
||||
is already staged in `sysroot/`. Building them needs a Linux host (or any other
|
||||
Unix); do it once and the staged result serves the Windows builds.
|
||||
`musl`, `toybox`, `mksh`, lwIP, uACPI, zstd, ncurses and the rest of `ports/` are built from source
|
||||
into `sysroot/`; Alpine's packages are unpacked into the root from `dist/alpine/`. **This does not work
|
||||
on a Windows host** -- `make` prints `ports: skipped` and uses whatever is already staged in
|
||||
`sysroot/`. Building them needs a Linux host (or any other Unix); do it once and the staged result
|
||||
serves the Windows builds.
|
||||
|
||||
## Where the tools live
|
||||
|
||||
|
|
|
|||
|
|
@ -14,47 +14,46 @@ deliberately kept out of `clean`).
|
|||
schos/
|
||||
├─ boot/ uefi/ the PE/COFF loader (splash, F8 menu, kernel load),
|
||||
│ common/ the UI it shares. UEFI only -- there is no BIOS path.
|
||||
├─ kernel/ The kernel, by subsystem: arch/ core/ dev/ fs/ proc/ sys/
|
||||
├─ kernel/ The kernel, by subsystem: arch/ core/ dev/ drivers/ fs/ net/
|
||||
│ proc/ sys/ acpi/ (uACPI glue, the EC, I2C-HID) gmmos/ (the
|
||||
│ micro OS under the crash screen) scff/ (the decompressor).
|
||||
│ drivers/ is the in-kernel drivers, by class: audio bus gpu
|
||||
│ net storage usb.
|
||||
├─ ldls/ The Linux driver layer: the .ko loader, the BTF reader, the
|
||||
│ Linux symbols reimplemented (kpi/), and host-side test tools.
|
||||
│ The process that runs it is cmd/ldlsd.
|
||||
├─ include/ Headers shared by kernel, userland, and tools/mkfs, by
|
||||
│ subsystem -- see "include/" below
|
||||
├─ lib/ First-party userland runtime: _start glue, syscall stubs,
|
||||
│ the initcode blob embedded in the kernel, user.ld
|
||||
├─ cmd/ First-party programs — one directory per program:
|
||||
│ cmd/<name>/main.c → sysroot/bin/<name>
|
||||
│ schinit and schinitd are PID 1 and its supervisor; vgpud is
|
||||
│ the virtio-gpu driver, which runs out here rather than in
|
||||
│ the kernel; socktest/shmtest/nettest/pcitest are suites.
|
||||
├─ ports/ Third-party software — one directory per port.
|
||||
│ Contains ONLY a Makefile and optional patches/.
|
||||
├─ projects/ Things that run ON SchOS rather than things that ARE it:
|
||||
│ the compositor, window manager, desktop, and installer.
|
||||
│ schinit and schinitd are PID 1 and its supervisor; vgpud and
|
||||
│ ldlsd are drivers that run out here rather than in the
|
||||
│ kernel; socktest/shmtest/nettest/pcitest/xready are suites.
|
||||
├─ etc/ The configuration the image starts with: schinit's job
|
||||
│ manifests (etc/schinit/*.job), motd, os-release, the zsh files.
|
||||
├─ ports/ Third-party software -- one directory per port, containing
|
||||
│ ONLY a Makefile and optional patches/. musl, toybox, mksh,
|
||||
│ lwIP, uACPI, zstd, ncurses, the fonts, fastfetch, kilo; and
|
||||
│ alpine/, which is the apk root (world, world.lock).
|
||||
├─ projects/ Things that run ON SchOS rather than things that ARE it.
|
||||
│ Not part of the kernel build. See projects/README.md.
|
||||
├─ tools/ Host-side helpers, SOURCE ONLY: mkfs.c, the SchFS test
|
||||
│ suites, sign.pl, vectors.pl, and the PowerShell harnesses.
|
||||
│ Their binaries build into build/tools/ -- this directory
|
||||
│ used to hold both, which is the one rule this layout has.
|
||||
│ tools/windows/loc.ps1 counts how much of the tree is first-party.
|
||||
├─ sysroot/ Staging for the disk image. etc/ is hand-maintained;
|
||||
│ bin/ lib/ sbin/ are populated by `make sysroot-install`.
|
||||
├─ dist/ Upstream tarball cache. Survives `make clean`.
|
||||
├─ Refrence/ A FreeBSD checkout (Refrence/freebsd-src), kept as reference
|
||||
│ material and a parts bin. Never built, never in an image.
|
||||
│ Read it when a specification is ambiguous; copy from it only
|
||||
│ after the per-file licence check in docs/licensing.md.
|
||||
│ A full clone is 2.1 GB and a previous NetBSD one was deleted
|
||||
│ for that reason. Only sys/dev, sys/net and sys/netinet have
|
||||
│ ever been read, so a sparse checkout is ~100 MB:
|
||||
│ git clone --filter=blob:none --sparse <url> Refrence/freebsd-src
|
||||
│ git -C Refrence/freebsd-src sparse-checkout set │ sys/dev sys/net sys/netinet
|
||||
├─ src-trunk/ A NetBSD checkout, when present -- reference material and a
|
||||
│ parts bin. NOT built, NOT installed, must never reach an
|
||||
│ image. Currently absent; it was deleted for disk space and
|
||||
│ can be re-fetched sparsely. Copying from it is governed by
|
||||
│ the per-file licence check in docs/licensing.md §3 --
|
||||
│ NetBSD is not uniformly licensed.
|
||||
├─ tools/ Host-side helpers, SOURCE ONLY: mkfs.c, mkschfs.c, mkscff.c,
|
||||
│ the SchFS test suites, vectors.pl, and the harnesses, grouped
|
||||
│ by what runs them (windows/ .ps1, posix/ .sh, python/ .py).
|
||||
│ Their binaries build into build/tools/. tools/windows/loc.ps1
|
||||
│ and tools/python/stats.py count how much of the tree is
|
||||
│ first-party.
|
||||
├─ sysroot/ Staging for the disk image, GENERATED by the build -- edit
|
||||
│ tools/posix/mkaroot.sh and etc/, not this.
|
||||
├─ dist/ Upstream tarball and package cache. Survives `make clean`.
|
||||
├─ shell.nix NixOS development shell: the whole toolchain in one file
|
||||
├─ write.sh Writes schos.iso to a USB stick, safely (Linux).
|
||||
├─ run.ps1 The Windows-side QEMU launcher.
|
||||
├─ docs/ architecture/ what it is plan/ what's next
|
||||
│ guides/ how to work history/ why it's like this
|
||||
│ Website/ the site, and the docs reader that renders this
|
||||
└─ build/ ALL generated output (see below)
|
||||
```
|
||||
|
||||
|
|
@ -92,7 +91,10 @@ build/
|
|||
├─ kernel/ object files, the linked kernel, kernel.asm
|
||||
├─ lib/ libc glue objects, initcode blob
|
||||
├─ cmd/ linked first-party programs
|
||||
├─ fs.img the filesystem image
|
||||
├─ aroot/ the apk root the image is made from
|
||||
├─ schfs.img the root as a disk image (make qemu)
|
||||
├─ fs-ram.img the same root as the live ramdisk, fs-ram.scff compressed
|
||||
├─ iso/ … what make iso assembles
|
||||
└─ ports/<name>/
|
||||
├─ src/ extracted upstream tarball
|
||||
├─ obj/ out-of-tree build directory
|
||||
|
|
@ -130,57 +132,44 @@ Local modifications to upstream go in `ports/foo/patches/*.patch` and are
|
|||
applied after extraction — never by editing files under `build/`, which are
|
||||
wiped on every clean.
|
||||
|
||||
## Ramdisk root
|
||||
## The live root
|
||||
|
||||
```sh
|
||||
make # normal: root on the IDE disk (build/fs.img as hdb)
|
||||
make ramroot # root from a SCHFS image linked into the kernel
|
||||
make # normal: the root is a SchFS disk image, build/schfs.img
|
||||
make iso # the same root as a compressed ramdisk linked into the kernel
|
||||
make ramroot # the ramdisk, uncompressed
|
||||
```
|
||||
|
||||
`make ramroot` builds `build/fs-ram.img` (default 6000 blocks, override with
|
||||
`RAMDISK_BLOCKS=`) and links it into the kernel, so the system boots to a shell
|
||||
with **no data disk attached at all**. That is what makes destructive work on
|
||||
the real disk survivable — reformatting it, or bringing up a new on-disk format
|
||||
— and it is the first piece of the installer.
|
||||
`make iso` builds the root as a SchFS image, compresses it
|
||||
([SCFF](../architecture/scff.md)) and links it into the kernel, so the system boots
|
||||
to a shell with **no data disk attached at all** -- which is what the USB stick is.
|
||||
It is also what makes destructive work on a real disk survivable. Nothing written
|
||||
to a live root survives a reboot.
|
||||
|
||||
Use `make ramroot`, not `make RAMROOT=1`: the flag changes `CFLAGS`, which make
|
||||
cannot see, so objects compiled for one setting would be silently reused for
|
||||
the other and `ROOTDEV` would point at the wrong device. The target drops the
|
||||
compiled objects first. Switching back is the same — plain `make` after a
|
||||
`ramroot` build reuses stale objects, so run `rm -rf build/kernel build/lib
|
||||
build/cmd` first (or just `make clean`).
|
||||
|
||||
The image costs boot time: the loader reads the kernel through the firmware's
|
||||
file protocol, so a 6 MB ramroot kernel is visibly slower to start than the
|
||||
350 KB default.
|
||||
Use the targets, not `make RAMROOT=1`: the flag changes `CFLAGS`, which make cannot
|
||||
see, so objects compiled for one setting would be silently reused for the other and
|
||||
`ROOTDEV` would point at the wrong device. The targets drop the compiled objects
|
||||
first. Going back to the disk root is the same -- run `make clean` or remove
|
||||
`build/kernel build/lib build/cmd` first.
|
||||
|
||||
**What may go in it is a licensing question, not only a size one.** The image is
|
||||
linked into the kernel binary, so anything in `sysroot/` when it is built becomes
|
||||
part of a distributable artifact. A file staged there under a licence that
|
||||
is not ours to redistribute does not merely ride along — it makes the kernel
|
||||
itself undistributable. `RAMDISK_EXCLUDE` in the Makefile is the list of
|
||||
paths kept out, and enforcing it in the build rather than relying on someone
|
||||
remembering to unstage something first is deliberate. It has been needed once
|
||||
already: a 33 MB test payload landed in `sysroot/` and both overflowed the image
|
||||
and would have gone into the kernel binary.
|
||||
part of a distributable artifact. A file staged there under a licence that is not
|
||||
ours to redistribute does not merely ride along -- it makes the kernel itself
|
||||
undistributable. `RAMDISK_EXCLUDE` in the Makefile is the list of paths kept out,
|
||||
and enforcing it in the build rather than relying on someone remembering to unstage
|
||||
something first is deliberate. (Linux modules for LDLS are staged by `make
|
||||
ldls-stage`/`ldls-r8169` on request, and `.gitignore` refuses `*.ko`: binaries stay out of git. The
|
||||
r8169 set is meant to be staged before `make iso`, since the ISO ships it.)
|
||||
|
||||
Three knobs, all in the Makefile, none of them `FSSIZE`:
|
||||
The knobs: `RAMDISK_BLOCKS` (image size), `RAMDISK_NINODES` (the inode table is
|
||||
*preallocated*: the default of 12000 once cost a quarter of a small image to
|
||||
describe about 260 files), `RAMDISK_EXCLUDE`. If `mkfs` reports the image is full,
|
||||
read its message: it prints the metadata breakdown and names the right knob.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `RAMDISK_BLOCKS` | image size, 6000 (× 1 KiB `BSIZE`) |
|
||||
| `RAMDISK_NINODES` | inode table size, 2000 |
|
||||
| `RAMDISK_EXCLUDE` | sysroot-relative paths to omit, with their subtrees |
|
||||
|
||||
`RAMDISK_NINODES` is worth knowing about because the inode table is
|
||||
*preallocated*: mkfs's default of 12000 inodes cost 1501 of the 6000 blocks — a
|
||||
quarter of the image — to describe about 260 files. At 2000 it costs 251, and
|
||||
metadata overall fell from 1596 blocks to 345.
|
||||
|
||||
If `mkfs` reports the image is full, read its message: it now prints the
|
||||
metadata breakdown and names the right knob for whichever image overflowed. It
|
||||
used to say only "increase FSSIZE in param.h", which is a constant that has
|
||||
nothing to do with the ramdisk root and sends you to the wrong file.
|
||||
The ISO build is fast on purpose (a few seconds when little changed): it copies with
|
||||
`cp -p` and `install -C` so unchanged files keep their time stamps, and toggling the
|
||||
ramdisk does not wipe the tree.
|
||||
|
||||
## Two things that will bite you
|
||||
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ reporting that it started.
|
|||
|
||||
| | What it runs | What it proves |
|
||||
|---|---|---|
|
||||
| `make schfs-check` | Host-native unit suites | The filesystem's logic, in about a second |
|
||||
| `make schfs-check`, `make kmem-test`, `ldls-inspect`/`ldls-run` | Host-native unit suites and the Linux-module loader run on the build machine | The filesystem's logic, the slab allocator's bookkeeping, and a `.ko`'s layout, in about a second |
|
||||
| `tools/windows/bootcheck.ps1` | A real boot, driven over the serial console | That the system answers correctly |
|
||||
| `make crashqr`, `make schcom-test` | A boot, then pixel assertions from the host | That the *screen* is right |
|
||||
| `make crashqr` | A boot, then pixel assertions from the host | That the *screen* is right |
|
||||
|
||||
The levels exist because each catches what the one below cannot. A unit test
|
||||
cannot tell you the kernel fails to boot; a boot test cannot tell you the screen
|
||||
|
|
@ -77,21 +77,31 @@ dropped every byte would pass any check made from one side alone.
|
|||
Under QEMU's user-mode networking the host is `10.0.2.2` seen from inside, and
|
||||
SLIRP connects to the host's loopback — so the listener binds there.
|
||||
|
||||
## Test programs that run on SchOS
|
||||
|
||||
Several first-party programs in `cmd/` exist to be run **on** the system and print which of their checks
|
||||
failed, because only the system can say whether its own interfaces behave. `bootcheck` runs the ones it
|
||||
can; the rest are run by hand or by a script:
|
||||
|
||||
| | Checks |
|
||||
|---|---|
|
||||
| `socktest`, `nettest`, `netecho` | Unix and INET sockets, `SCM_RIGHTS` and credentials, `setsockopt`'s refusals |
|
||||
| `shmtest`, `memfdtest` | System V shared memory; `memfd` mapped by two processes, and one sent over a socket |
|
||||
| `pcitest` | `/dev/pci`: claim, map, DMA, interrupts, release |
|
||||
| `evtest`, `locktest`, `sigtest` | the event descriptors; `flock` and record locks; signals |
|
||||
| `dynhello` | dynamic linking: TLS, the heap, a forked child that still has its libraries |
|
||||
| `xready` | every kernel call Xorg makes, before Xorg is started — the evdev queries, the fbdev ioctls, the console calls, the `/sys` files |
|
||||
| `tools/python/ldls_check.py` | what LDLS needs from a kernel's BTF |
|
||||
|
||||
## Pixel assertions
|
||||
|
||||
```sh
|
||||
make crashshot && make crashqr # crash the kernel, decode the QR out of the framebuffer
|
||||
make schcom-test # boot, run the compositor and a client, check the pixels
|
||||
```
|
||||
|
||||
`tools/windows/splashshot.ps1` counts colours. **That exists because a log-only
|
||||
harness once passed on a blank screen.**
|
||||
|
||||
`schcom-test` asserts **from the host** — the compositor's own report that it
|
||||
composited something proves nothing. It runs two overlapping clients and checks
|
||||
that the newer one wins in the overlap, because a compositor that cannot order
|
||||
two surfaces cannot host a window manager.
|
||||
|
||||
Other capture tools, all in `tools/windows/`: `dispshot`, `fbshot`,
|
||||
`splashshot`, `textshot`, `ttyshot`.
|
||||
|
||||
|
|
@ -148,10 +158,12 @@ leak.
|
|||
- **No automated test runs on real hardware.** Everything above is QEMU.
|
||||
- **SMP is exercised but not stressed** — boots on 1, 2 and 4 processors are
|
||||
checked; nothing hunts for races.
|
||||
- **Nothing asserts the desktop by pixel.** `xready` checks the calls Xorg makes; whether Window Maker
|
||||
and an `xterm` appear on a real screen is looked at in screenshots, not asserted.
|
||||
- **No fuzzing anywhere**, and no `fsx`/`xfstests` against SchFS. Getting SchFS
|
||||
under those is the main argument for the FUSE implementation in
|
||||
[../plan/roadmap.md](../plan/roadmap.md).
|
||||
- **The SchFS round-trip test needs symlinks and hard links**, so it skips on a
|
||||
Git Bash host and only runs on Unix.
|
||||
- **`make ports` does not work on Windows.** musl, toybox and mksh must be built
|
||||
- **`make ports` does not work on Windows.** musl, toybox, mksh and the rest must be built
|
||||
once on a Unix host and staged.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ and the few write-ups that have nowhere better to live.
|
|||
|
||||
| | |
|
||||
|---|---|
|
||||
| [kernel.md](kernel.md) | The expensive ones. Boot, SMP, signals, ABI assumptions, `/proc`, input. |
|
||||
| [kernel.md](kernel.md) | The expensive ones. Boot, SMP, signals, ABI assumptions, `/proc`, input, TLBs, a stub that returned a flag word. |
|
||||
| [schfs.md](schfs.md) | How the filesystem was built, and what each phase cost. |
|
||||
| [moving-to-linux.md](moving-to-linux.md) | The Windows-to-Fedora move. |
|
||||
|
||||
|
|
|
|||
|
|
@ -298,17 +298,8 @@ exported entry points, `nameifrom()`/`nameiparentfrom()`. `sys_fs.c` gained
|
|||
Every `*at()` syscall now uses these instead of its own inline
|
||||
absolute-path/`AT_FDCWD`-only check.
|
||||
|
||||
**Known follow-on issue, not yet root-caused**: heavy `ls`/`ls -lR` use
|
||||
across many commands in one long-running shell session can eventually make
|
||||
a *later*, unrelated directory hang (reproduced with `ls -lR /var` after
|
||||
~10 prior commands in the same session; the identical command succeeds
|
||||
immediately on a fresh boot). Not an inode-table or file-table exhaustion
|
||||
— both `iget()` and `filealloc()` fail cleanly (panic / return 0
|
||||
respectively) when full, and neither fired. Smells like a lock or
|
||||
reference-count leak somewhere in the *at() paths above, or in toybox's
|
||||
own fd handling, that only manifests after enough accumulated opens/closes.
|
||||
Next step: instrument `itable`/`ftable` occupancy counts and watch them
|
||||
across a long `ls`-heavy session to see what's actually climbing.
|
||||
**Known follow-on issue** (heavy `ls -lR` in one long shell session eventually hanging a
|
||||
*later* directory): hunted, and not reproducible — see section 24.
|
||||
|
||||
## 6. Boot speed
|
||||
|
||||
|
|
@ -1255,3 +1246,86 @@ is at least as long as the width, and that the aperture holds that many rows.
|
|||
If not, it goes back to the previous mode and says so in the menu. This proves
|
||||
nothing about a mode that passes, but it stops a lying one from reaching the
|
||||
kernel.
|
||||
|
||||
---
|
||||
|
||||
## 24. The `ls -lR` hang that was hunted and not found
|
||||
|
||||
**Symptom.** After about ten `ls`-heavy commands in one long shell session, a later and
|
||||
unrelated `ls -lR /var` would hang; the same command succeeded at once on a fresh boot. It
|
||||
was not inode-table or file-table exhaustion (both fail cleanly), and the first hypothesis
|
||||
was a lock or reference-count leak in the `*at()` paths.
|
||||
|
||||
**What was done.** `/proc/fsstat` was given the occupancy counters the hypothesis asked
|
||||
for (inode, file and buffer peaks, the SchFS volume lock and transaction owner), and the
|
||||
workload was made heavier than the report: ten rounds of `ls -lR /bin` interleaved with
|
||||
writes and `cd` — 33 execs and 13,378 tree descents. Nothing climbed:
|
||||
|
||||
inode_peak 9 / 200 file_peak 9 / 512
|
||||
buf_pinned_peak 1 / 256 schfs0 locked 0 tx_owner_pid 0 tx_depth 0
|
||||
|
||||
**Conclusion.** The hypothesis is eliminated, and the bug is very likely gone — fixed in passing
|
||||
by the `*at()` dirfd rewrite, the `getdents` cookie rewrite, or SchFS becoming the root, all of
|
||||
which landed after it was reported. It was closed rather than chased. The instrumentation
|
||||
stayed: if it happens again, `/proc/fsstat` names the table, or shows `locked 1` with
|
||||
`tx_owner_pid 0`, which is an orphaned volume lock.
|
||||
|
||||
Two real defects turned up on the way and were fixed, neither proven to be the cause:
|
||||
`holdingsleep()` never compared the owning pid, which disarmed all seven `panic()` assertions
|
||||
built on it; and SchFS's `tx_mine()` compared a bare `struct proc *`, which a recycled ptable
|
||||
slot makes ambiguous.
|
||||
|
||||
**The transferable part.** A hang you cannot reproduce still earns its instrumentation. The
|
||||
absence of a result *is* the result, and the right place to leave it is where the next person
|
||||
with the same symptom will look.
|
||||
|
||||
## 25. Epiphany's heap was corrupted by a page nobody had unmapped yet
|
||||
|
||||
**Symptom.** Epiphany (WebKit: forty threads on four processors) died at a different address
|
||||
on every run; musl's allocator was the first to notice, a group's metadata no longer pointing
|
||||
back at its group.
|
||||
|
||||
**Diagnosis.** Nothing was freed twice. One thread unmapped a range, `mmap` handed the same
|
||||
addresses straight back, and a thread on *another* processor went on reading and writing the
|
||||
**old** pages through its TLB: stores vanished, loads were stale. Deferring the free of the
|
||||
unmapped pages had already kept a stale translation from reaching a page someone else owned;
|
||||
it did not keep one address space *coherent*.
|
||||
|
||||
**Fix.** Unmapping from a shared address space interrupts the processors running it and waits
|
||||
until each has reloaded `CR3` (`tlb_shootdown()`, `kernel/arch/x86_64/vm.c`).
|
||||
|
||||
**The transferable part.** Threads turned an invisible assumption — one CPU's TLB is the only
|
||||
one that matters — into the loudest bug in the tree, in the one program that used the most
|
||||
threads. Memory corruption that moves every run is a coherence bug until proven otherwise.
|
||||
|
||||
## 26. Every MDIO read returned `0x02460246`
|
||||
|
||||
**Symptom.** Linux's `r8169` under LDLS, on the laptop, read its PHY's ID as `0x02460246`.
|
||||
|
||||
**Diagnosis.** `0x246` is a value of `RFLAGS`, not a register. `mdio-bus` keeps the word it
|
||||
has just read in `r11` across `preempt_enable()`, which on x86 is `decl %gs:__preempt_count; je`
|
||||
to a call that preserves every register. LDLS had given `__preempt_count` zeroes, so the count
|
||||
reached zero after the decrement and the call was made — into a logging stub whose `syscall`
|
||||
instruction leaves `RFLAGS` in `r11`.
|
||||
|
||||
**Fix.** `__preempt_count` starts at `PREEMPT_ENABLED` (x86 keeps `NEED_RESCHED` inverted), and
|
||||
`preempt_schedule` and its notrace twin are a bare `ret`.
|
||||
|
||||
**The transferable part.** A stub that returns zero is not safe when zero changes which path
|
||||
runs — the same family as the five in [../architecture/ldls.md](../architecture/ldls.md). The
|
||||
tell here was a value that was a plausible *flag word* rather than a plausible *register*.
|
||||
|
||||
## 27. GLib's network monitor spun at 100%
|
||||
|
||||
**Symptom.** Epiphany's main thread consumed a full core in `poll()` returning at once.
|
||||
|
||||
**Diagnosis.** GLib sizes each netlink datagram with `recvmsg(MSG_PEEK|MSG_TRUNC)` and no buffer.
|
||||
The kernel answered 0, GLib read 0 bytes, the datagram stayed queued, and `poll()` said readable
|
||||
again, for ever. A drained netlink socket also reported hangup, because `sock_hup` took "no peer"
|
||||
for "closed".
|
||||
|
||||
**Fix.** `recvmsg` on netlink peeks without consuming and with `MSG_TRUNC` returns what is waiting.
|
||||
|
||||
**The transferable part.** A kernel that accepts a flag and does nothing with it turns a
|
||||
well-formed program into a busy loop. Do it, or refuse it.
|
||||
|
||||
|
|
|
|||
|
|
@ -267,42 +267,33 @@ copied file is a file to be understood, attributed and maintained forever.
|
|||
|
||||
---
|
||||
|
||||
## 6. LDLS, and the distribution boundary
|
||||
## 6. LDLS, and shipping Linux's drivers
|
||||
|
||||
[architecture/ldls.md](architecture/ldls.md) runs Linux drivers. This section
|
||||
was written when SchOS was Apache-2.0 and the boundary below was a legal
|
||||
necessity. Under GPL-2.0-or-later it is a **project policy** instead: shipping
|
||||
a GPL-2.0-only `.ko` with SchOS would now be lawful, provided the source goes
|
||||
with it. It is still not done, because a module we never ship is a module we
|
||||
never have to vouch for, track upstream, or provide source for.
|
||||
[architecture/ldls.md](architecture/ldls.md) runs Linux drivers. This section used to be about a
|
||||
distribution boundary: written when SchOS was Apache-2.0, it said a GPL-2.0-only `.ko` could not be
|
||||
shipped and so SchOS would ship the layer and never the modules. **That no longer holds.** SchOS is
|
||||
GPL-2.0-or-later, GPL-2.0-only code combines with it, and the owner has decided that Linux code and
|
||||
firmware blobs are shipped. The ISO carries Linux's **r8169 set** — `mdio-bus`, `libphy`,
|
||||
`mdio_devres`, the Realtek PHY driver and `r8169`, unmodified — and the RTL8168 PHY firmware.
|
||||
|
||||
### What shipping them asks of us
|
||||
|
||||
- **The licence stays with the code.** The modules are GPL-2.0-only, the firmware is Realtek's, redistributed
|
||||
under linux-firmware's terms. `NOTICE` lists both, with where they come from.
|
||||
- **The source goes with the binary** (GPLv2 §3). The modules are stock builds of the Linux kernel version
|
||||
that `make ldls-r8169` took them from, so the corresponding source is that release of Linux, unmodified.
|
||||
- **Nothing combines at link time.** The modules are loaded into an ordinary process by our loader; our code
|
||||
is GPL-2.0-or-later, theirs GPL-2.0-only, and both are GPLv2-compatible whichever way one reads "combined".
|
||||
- **No binary in git.** The repository stays source-only: `.gitignore` refuses `*.ko`, and `make ldls-r8169`
|
||||
stages the set from a Linux build into the image.
|
||||
|
||||
### LDLS is ours
|
||||
|
||||
Every file under `ldls/` is ours, GPL-2.0-or-later. The loader was written from
|
||||
the ELF-64 gABI and the x86-64 psABI; the shim is written under the rule in
|
||||
[architecture/ldls.md](architecture/ldls.md). Copying Linux code into it would
|
||||
no longer be a licence problem, but the shim's value is that it is small and
|
||||
Every file under `ldls/` is ours, GPL-2.0-or-later. The loader was written from the ELF-64 gABI and the
|
||||
x86-64 psABI; the shim is written under the rule in [architecture/ldls.md](architecture/ldls.md). Copying
|
||||
Linux code into it would no longer be a licence problem, but the shim's value is that it is small and
|
||||
understood, so the existing rule stands.
|
||||
|
||||
### The combination is the user's
|
||||
|
||||
| | licence | who distributes it |
|
||||
|---|---|---|
|
||||
| `ldls/` — loader, shim, host | **GPL-2.0-or-later** | us |
|
||||
| a `.ko` | GPL-2.0-only, usually | the user's distribution |
|
||||
| the two running together in a process | — | **nobody** |
|
||||
|
||||
GPLv2 §0 is explicit about where its obligations attach:
|
||||
|
||||
> Activities other than copying, distribution and modification are not covered
|
||||
> by this License; they are outside its scope. **The act of running the Program
|
||||
> is not restricted.**
|
||||
|
||||
So: we ship LDLS and no modules. The user obtains a module from wherever they
|
||||
get their software and runs it on their own machine. `RAMDISK_EXCLUDE` and the
|
||||
`*.ko` rule in `.gitignore` keep that boundary mechanical rather than
|
||||
intentional.
|
||||
|
||||
### `EXPORT_SYMBOL_GPL` does not apply
|
||||
|
||||
Linux marks much of its internal API `EXPORT_SYMBOL_GPL`, and modules carry
|
||||
|
|
@ -322,7 +313,7 @@ show.
|
|||
|
||||
### BTF: the layouts are not in our source at all
|
||||
|
||||
§3 of the plan has one uncomfortable row — struct layouts. A compiled driver
|
||||
Rule 3 of [architecture/ldls.md](architecture/ldls.md) has one uncomfortable row — struct layouts. A compiled driver
|
||||
indexes fields by offset, so the shim has to reproduce them exactly, and a
|
||||
field-for-field match *looks* like transcription even when it is not.
|
||||
|
||||
|
|
|
|||
3705
docs/niveus/dist/niveus.css
vendored
Normal file
3705
docs/niveus/dist/niveus.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
572
docs/plan/1.0.md
572
docs/plan/1.0.md
|
|
@ -7,17 +7,16 @@ A finish line, so that the list of open work stops being infinite.
|
|||
## Why this document exists
|
||||
|
||||
[schapi.md](schapi.md) §3 says the stability freeze starts at 1.0 and covers
|
||||
whatever is marked `stable` at that moment. **1.0 was never defined**, which
|
||||
means there has been no finish line — and with no finish line every gap is
|
||||
equally mandatory, the list only grows, and the project feels larger every week
|
||||
regardless of how much gets done.
|
||||
whatever is marked `stable` at that moment. 1.0 was never defined, which means
|
||||
there was no finish line — and with no finish line every gap is equally mandatory,
|
||||
the list only grows, and the project feels larger every week regardless of how much
|
||||
gets done.
|
||||
|
||||
That feeling is structural, not a judgement about progress. This document fixes
|
||||
the structure by drawing a line and putting most of the open work on the far
|
||||
side of it.
|
||||
That feeling is structural, not a judgement about progress. This document fixes the
|
||||
structure by drawing a line and putting most of the open work on the far side of it.
|
||||
|
||||
> **Anything not named in this document is not part of 1.0.** It is not
|
||||
> cancelled, not forgotten, and not up for reconsideration until 1.0 ships.
|
||||
> **Anything not named in this document is not part of 1.0.** It is not cancelled,
|
||||
> not forgotten, and not up for reconsideration until 1.0 ships.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -26,31 +25,42 @@ side of it.
|
|||
> **A machine one person can sit down at and use for an afternoon without
|
||||
> falling off an edge.**
|
||||
|
||||
Concretely — boots on real hardware, roots on SchFS, runs a shell and the real
|
||||
toybox userland, is on the network, and puts a windowed terminal on the screen.
|
||||
Concretely — boots on real hardware, roots on SchOS's own filesystem, runs a shell
|
||||
and a real userland, is on the network, and puts a windowed desktop on the screen.
|
||||
|
||||
It is deliberately a **small complete system** rather than a rich incomplete
|
||||
one. SchOS 1.0 is not Linux, does not run third-party applications, and has no
|
||||
package manager. It is a system that works, end to end, with nothing in it that
|
||||
lies about what it did.
|
||||
It started as a deliberately small system: *no third-party applications and no package
|
||||
manager*. That has changed on its own, and is worth recording rather than tidying away:
|
||||
the root is an **apk root** ([packages](../architecture/packages.md)), Alpine's
|
||||
programs run on it unmodified, and the desktop is Xorg with Window Maker
|
||||
([desktop](../architecture/desktop.md)). What did not change is the bar: nothing in the
|
||||
system lies about what it did.
|
||||
|
||||
### The acceptance test
|
||||
|
||||
One unattended run, asserted from the host, the way everything else here is
|
||||
tested. When this passes, it is 1.0:
|
||||
One unattended run, asserted from the host, the way everything else here is tested:
|
||||
|
||||
```
|
||||
boot on real hardware from a USB stick
|
||||
→ SchFS root mounts, generation counter advances
|
||||
→ schinit starts the compositor
|
||||
→ a terminal window appears and is asserted by pixel
|
||||
→ in it: ls, cat, a file written, a network fetch
|
||||
→ reboot
|
||||
→ the file is still there and the checksum matches
|
||||
→ schinit starts shells on the virtual terminals
|
||||
→ X starts; a window appears
|
||||
→ in it: ls, cat, a file written, a network fetch over HTTPS
|
||||
```
|
||||
|
||||
Nothing in that sentence is a feature list. Every clause is something that is
|
||||
currently either broken, missing, or unproven.
|
||||
**It passes. 1.0 is reached.** Where each clause stands:
|
||||
|
||||
| Clause | State |
|
||||
|---|---|
|
||||
| boot from a USB stick on real hardware | **done** on an AMD B550 desktop, an ASUS TUF A15 laptop and an Ivy Bridge HP all-in-one |
|
||||
| SchFS root, generation counter | **done** — the root is a SchFS image, in RAM when live |
|
||||
| a terminal to type in | **done** — a shell on the first six of the twelve virtual terminals (`etc/schinit/*-shell.job`) |
|
||||
| X starts, a window appears | **done** (`startx` → Window Maker + xterm); *asserted by pixel* has no automated test yet — `bootcheck` drives the serial console, and the screenshots are looked at |
|
||||
| `ls`, `cat`, a file written | **done** |
|
||||
| a network fetch over HTTPS | **done** (`wget`, `curl`, `apk`) |
|
||||
|
||||
The test used to end with *reboot — the file is still there*. That clause is **dropped**, deliberately: 1.0 is a **live**
|
||||
system. The root lives in RAM and nothing written to it survives a reboot, and that is what the project is calling finished.
|
||||
Persistence (an installer, or a persistent partition) is after 1.0 — see [roadmap.md](roadmap.md) §5.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -59,11 +69,10 @@ currently either broken, missing, or unproven.
|
|||
**No new capability is started until this register is empty.**
|
||||
|
||||
The failure this exists to stop is not laziness and not scope creep — it is
|
||||
*abandonment by promotion*. A thing gets built to the point where it works, the
|
||||
next thing becomes more interesting, and the first one is left in a corner:
|
||||
present in the tree, not finished, not deleted, and not visible in any list.
|
||||
Enough of those and the tree stops matching its own documentation, which is the
|
||||
state this register was written to end.
|
||||
*abandonment by promotion*. A thing gets built to the point where it works, the next
|
||||
thing becomes more interesting, and the first one is left in a corner: present in the tree,
|
||||
not finished, not deleted, and not visible in any list. Enough of those and the tree stops
|
||||
matching its own documentation.
|
||||
|
||||
Two rules, and they are the same rule seen from either end:
|
||||
|
||||
|
|
@ -71,73 +80,46 @@ Two rules, and they are the same rule seen from either end:
|
|||
>
|
||||
> **Adding a row requires closing one.** Two things in flight. Never three.
|
||||
|
||||
A row leaves this table in one of exactly two ways. Either it is **finished** —
|
||||
and finished means a description exists in [../architecture/](../architecture/),
|
||||
not that the code works — or it is **deleted**, and deleted means gone from the
|
||||
tree, not marked for deletion. A directory that has said "to be deleted" for
|
||||
months is the corner, not the cure; `projects/schwm/` and `projects/schde/`
|
||||
proved that and are now actually gone.
|
||||
A row leaves this table in one of exactly two ways. Either it is **finished** — and finished
|
||||
means a description exists in [../architecture/](../architecture/), not that the code works —
|
||||
or it is **deleted**, and deleted means gone from the tree, not marked for deletion. A
|
||||
directory that has said "to be deleted" for months is the corner, not the cure.
|
||||
|
||||
### Built, and not connected to anything
|
||||
|
||||
The galling ones. The work exists, is tested, and nothing uses it.
|
||||
|
||||
| | Where it stopped |
|
||||
|---|---|
|
||||
| ~~**The text layer**~~ | **Half closed, and the half that closed is the one that mattered.** The console draws real TrueType through `font_ops` now: cells hold a codepoint, the cell size comes from the font's measured advance, and `kernel/dev/ttf.c` has a caller that is not a self-test. What is still unused is `text.c`'s *proportional* path — the console remains a fixed grid. See Gate 0. |
|
||||
| **A font from disk** | Still true, and now deliberate rather than pending: the console typeface is linked into the kernel image, because a panic during boot has to be readable before there is a filesystem. Loading a *second* font from disk is the open part. |
|
||||
| **Socket activation** | `schinitd` parses `[Socket name]` and remembers the path. Nothing holds the socket. **This is the entire reason for copying launchd**, and it is the half that was not built. |
|
||||
| **`display_ops.wait_vblank`** | In the vtable, implemented by the VBE driver, left null by virtio-gpu. A compositor that needs it cannot rely on it. |
|
||||
| **The in-kernel LDLS window** | `vm.c` maps the kernel image a second time in the top 2 GB at every boot, and `include/ldls/ldls.h` describes a `/dev/ldls` loader — the start of loading Linux modules *into the kernel*, which [ldls.md](../architecture/ldls.md) rule 2 says is not how it is done. Nothing calls either. **Delete it**, or decide. |
|
||||
| **`display_ops`** | `acquire` and `wait_vblank` are in the vtable, implemented by one driver at most, with no consumer: Xorg draws through `/dev/fb0`. Kept for the GPU work ([gpu.md](gpu.md)); that is the reason it stays. |
|
||||
| **Socket activation** | `schinitd` parses `[Socket name]` and remembers the path. Nothing holds the socket. This is the entire reason for copying launchd, and it is the half that was not built. |
|
||||
| **`sys_delete`, `sys_show`, `sys_showstat`** | Orphaned entries from the old v1 filesystem's recycle bin. They die with `fs.c`. |
|
||||
|
||||
### Started, and stopped part-way
|
||||
|
||||
| | Where it stopped |
|
||||
|---|---|
|
||||
| **`foundations.md`** | All but item 4 (`setsockopt`) built: descriptor passing, `memfd`, filesystem-named `AF_UNIX`, dynamic linking, and epoll/eventfd/timerfd/signalfd. |
|
||||
| ~~**`hybrid.md`**~~ | **Closed.** All four mechanisms built; H1, H2 and H3 done. H4 — the USB eviction — is *withdrawn* rather than outstanding: USB is the bus peripherals arrive on, not a peripheral, and an eviction that has to leave an identical driver behind has moved nothing. See [hybrid.md](hybrid.md), "Decided: USB stays in the kernel". |
|
||||
| ~~**`ldls.md`**~~ | **Closed.** L0 through L4 built: the loader handles 2,440 stock modules, and both `8139too` and Linux's `e1000` run in a SchOS process through `/dev/pci` with `ping` working over each. The e1000 run is a handover — the kernel was driving that NIC and gave it up. Described in [../architecture/ldls.md](../architecture/ldls.md). Two drivers, one kernel version, one machine. |
|
||||
| **Schcom v0** | 715 lines that composite, over a wire format that is superseded. Frozen: it runs, nothing more is added, and it is kept only because it is the one thing in the tree that has ever put a client's pixels on screen. |
|
||||
| ~~**`debugger.md`**~~ | **Closed.** D0 through D5 built, including the network transport: `schdbgd` bridges TCP to `/dev/dbgro`, ships disabled, binds loopback, and cannot reach a view that changes state. Described in [../architecture/debugging.md](../architecture/debugging.md). |
|
||||
|
||||
**The row LDLS spent, and gave back.** `hybrid.md` closed — H3 built, H4
|
||||
withdrawn — and `ldls.md` took the slot it freed. It has since closed too, so
|
||||
the slot is free again. Worth being honest that this is
|
||||
the largest thing ever to enter this register, and that it was **explicitly not
|
||||
part of 1.0**: nothing in the acceptance test needs a foreign driver. It was
|
||||
taken on because the mechanisms it uses were finished and idle, and because
|
||||
the alternative to it is writing a driver for every device that exists.
|
||||
|
||||
**The row the debugger spent, and gave back.** Adding one requires closing one,
|
||||
and *A released PCI device* closed when H2 landed. It is worth saying plainly
|
||||
that the debugger is not a capability of the OS the way every other row here is:
|
||||
nothing in it ships to a user or appears in the acceptance test. The case for it
|
||||
is that **Gate 0 is a list of bugs to find in a kernel that could not be asked
|
||||
anything** — so it is the instrument the rest of this register is surveyed with,
|
||||
and it earned its slot by making every other row cheaper to close.
|
||||
|
||||
It has closed. **Every plan in `docs/plan/` that was open is now closed**, which
|
||||
means the next thing is Gate 0 itself: the survey, with the instrument built.
|
||||
|
||||
**Closed since this register was written.** *A released PCI device* — release is
|
||||
now symmetric with claim (bus mastering cleared before the DMA is freed, a
|
||||
refusal when the device is the only display), a supervised job starts the
|
||||
replacement, and it is described in
|
||||
[../architecture/devices.md](../architecture/devices.md) under "Taking it back".
|
||||
| **LDLS on real hardware** | `r8169` registers its interface on the laptop and reads its PHY; whether it carries traffic there is **not confirmed**. The native driver is out of the registry and stays as the fallback. |
|
||||
| **The laptop's embedded controller** | The EC is found and served (battery and AC read through it); **SCI queries are not handled**, so events from it are not seen. |
|
||||
| **Epiphany (WebKit)** | `apk add epiphany` installs it; its web-process child dies at start-up ("short read from the peer"). NetSurf runs. Not diagnosed. |
|
||||
|
||||
### Named in a "what it does not do" and still true
|
||||
|
||||
Not corners — these were written down honestly on the day and stayed written
|
||||
down. They are here so the register is the whole picture rather than the
|
||||
flattering part of it.
|
||||
Not corners — these were written down honestly on the day and stayed written down. They are here
|
||||
so the register is the whole picture rather than the flattering part of it. Each is described where
|
||||
it lives.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `/proc/<pid>/exe` is a regular file | so `readlink()` fails and `ps -o comm` prints `[init]` |
|
||||
| `getrusage` | does not distinguish `RUSAGE_CHILDREN` from `RUSAGE_SELF` |
|
||||
| No slab allocator | every subsystem's fixed-size table is a consequence |
|
||||
| No `getsockname`/`getpeername` | and `accept()` does not report an AF_INET peer |
|
||||
| No routing table | first interface wins, which is a rule and not routing |
|
||||
| No page cache | writable `MAP_SHARED` of a file is refused — [kernel.md](../architecture/kernel.md) |
|
||||
| `mprotect` is a no-op, nothing has NX | [kernel.md](../architecture/kernel.md) |
|
||||
| No routing table | first interface wins, which is a rule and not routing — [networking.md](../architecture/networking.md) |
|
||||
| No descriptor garbage collector | a socket passed over itself and never received leaks the pair |
|
||||
| udev sees only the kernel's own devices | PCI and USB are not in the `/sys/devices` tree — [udev.md](../architecture/udev.md) |
|
||||
| No EHCI driver | USB 2.0 ports wired only to EHCI do nothing — [usb.md](../architecture/usb.md) |
|
||||
| Touchpads are mice | no two-finger scroll — [desktop.md](../architecture/desktop.md) |
|
||||
| `ru_stime` is zero | [kernel.md](../architecture/kernel.md) |
|
||||
| No IOMMU | a driver in a process is isolated against bugs, not malice — [devices.md](../architecture/devices.md) |
|
||||
| One audio chip family | AC'97 only; laptops have HDA — [audio.md](../architecture/audio.md) |
|
||||
|
||||
### Frozen — scoped, unstarted, and not to be started
|
||||
|
||||
|
|
@ -147,431 +129,75 @@ Neither cancelled nor in progress. **Do not open these.**
|
|||
|---|---|
|
||||
| `projects/schinst/` | the installer, the installation environment, `.sip` |
|
||||
| `projects/schfs-linux/` | SchFS as a filesystem for Linux |
|
||||
| [roadmap.md](roadmap.md) §4 | the interactive crash screen |
|
||||
| [roadmap.md](roadmap.md) §6 | SchFS beyond SchOS |
|
||||
| [gpu.md](gpu.md) | amdgpu and nouveau through LDLS — measured, with the work it needs written down |
|
||||
| [finch.md](finch.md) | Finch inside SchOS (the language itself is finished and lives in its own repository) |
|
||||
| [schapi.md](schapi.md) | the API layering, designed and not built |
|
||||
|
||||
Each of those is well argued and none of them is on the path to the acceptance
|
||||
test above. That is the whole test for whether something belongs in this section:
|
||||
**not "is it worth doing" but "does 1.0 fail without it".**
|
||||
Each of those is well argued and none of them is on the path to the acceptance test above. That is
|
||||
the whole test for whether something belongs in this section: **not "is it worth doing" but "does 1.0
|
||||
fail without it".**
|
||||
|
||||
---
|
||||
|
||||
## The four gates
|
||||
## The gates
|
||||
|
||||
In order. **Do not start a gate before the one before it closes**, and closing
|
||||
means a description exists in [../architecture/](../architecture/) — see
|
||||
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||||
In order. **Do not start a gate before the one before it closes**, and closing means a description exists in
|
||||
[../architecture/](../architecture/).
|
||||
|
||||
### Gate 0 — Fix what is already wrong
|
||||
|
||||
**This comes first, before any new capability.** The desktop work is the
|
||||
temptation and it is the wrong thing to do next: a compositor built on a kernel
|
||||
with an unexplained hang and a socket layer that reports success for things it
|
||||
did not do will fail in ways indistinguishable from its own bugs. That is the
|
||||
same argument that put `netdev_selftest()` before the TCP/IP stack, and it was
|
||||
right then.
|
||||
|
||||
**Things that are actively wrong:**
|
||||
|
||||
- [x] ~~**The `ls -lR` hang.**~~ **Not reproducible.** Instrumented and hunted;
|
||||
see below. Left here rather than deleted because the absence of a result
|
||||
is the result.
|
||||
- [x] ~~**`setsockopt` returns 0 without setting anything.**~~ Six options now
|
||||
genuinely reach lwIP's pcb and everything else is `ENOPROTOOPT`. Round-trip
|
||||
checks in `cmd/socktest`, which a permissive stub cannot pass: it has to
|
||||
read back a value it was set *and* refuse an unknown option.
|
||||
- [x] ~~**`accept4` silently drops its flags.**~~ It is its own table entry now
|
||||
rather than an alias for `accept` — an alias would have had to read a
|
||||
fourth argument that `accept` never passes, inventing a `SOCK_NONBLOCK`
|
||||
out of whatever the caller last left in that register.
|
||||
- [x] ~~**Timestamps on everything created after boot read 1970.**~~ Both
|
||||
`know()` (SchFS) and `now_sec()` (v1) returned `uptime()` under comments
|
||||
saying the kernel had no wall clock. It has had one since `time_init()`
|
||||
started reading the RTC; the comments outlived the condition. Two errors,
|
||||
not one — `uptime()` counts 10 ms ticks and they were used as seconds, so
|
||||
the clocks also ran 100× fast.
|
||||
- [x] ~~**User processes are corrupted on SMP.**~~ **Found and fixed.**
|
||||
`syscall_entry` parked `%rax`, `%rcx` and `%rdx` in three *globals*
|
||||
across the `FS_BASE` swap, under a comment arguing that `SYSCALL`
|
||||
clearing `IF` made it safe. `IF` answers for an interrupt on *this* CPU
|
||||
and says nothing about a second CPU running the same three stores — and
|
||||
`%rcx` is the user return address `SYSRET` uses, so the CPU that lost the
|
||||
race returned its process into another process's instruction stream.
|
||||
|
||||
That is the whole of it: `rip 0x0` was a process handed a zero and
|
||||
following it, and `0xffffffffffffffea` was `-EINVAL` in a swapped `%rax`
|
||||
used as a pointer. Per-CPU words at `%gs:32..48` now. Measured at four
|
||||
CPUs against the same pristine image: **10 faults in 10 boots before, 0
|
||||
in 18 after.** See [../architecture/kernel.md](../architecture/kernel.md),
|
||||
"Per-CPU state".
|
||||
|
||||
- [x] ~~**`$(command substitution)` hangs the shell.**~~ **Not a kernel bug.**
|
||||
`ports/zsh/Makefile` answered configure's "does `kill(pid, 0)` return
|
||||
ESRCH correctly?" with `no`, which makes zsh `#define ESRCH EINVAL`.
|
||||
`waitforpid()` ends its wait on `errno != ESRCH`, so with the kernel
|
||||
correctly returning ESRCH (3) and zsh comparing against EINVAL (22), the
|
||||
shell waited for ever on a child it had already reaped. One word in a
|
||||
cross-compile cache. Autoconf's own default for that check is `yes`;
|
||||
omitting the line would have been right. See
|
||||
[../history/kernel.md](../history/kernel.md), section 7.
|
||||
- [x] ~~**`getrusage` does not distinguish `RUSAGE_CHILDREN`** from
|
||||
`RUSAGE_SELF`.~~ It did not distinguish anything: it ignored `who` and
|
||||
reported the machine's *uptime* as the caller's user time, so every
|
||||
process of every age answered with the same number. Per-process CPU time
|
||||
is sampled at the timer tick now, children's time is rolled up on reap,
|
||||
and `wait4()`'s fourth argument — a `struct rusage *` that was read off
|
||||
the stack and thrown away — is filled. That last one is why zsh's `time`
|
||||
reported `16777216.01s user` for everything: 2^24 seconds is
|
||||
uninitialised memory, and it never varied because it was never written.
|
||||
`time` now says 97% cpu for a busy loop and 0% for `sleep 2`.
|
||||
`ru_stime` is a known zero; see
|
||||
[../architecture/kernel.md](../architecture/kernel.md).
|
||||
- [x] ~~**`/proc/<pid>/exe` is a regular file, not a symlink.**~~
|
||||
`lrwxrwxrwx ... exe -> /bin/schinit`, and `readlink` returns it. It also
|
||||
needed a real `st_size`, which every other procfs file deliberately
|
||||
reports as zero: `readlink(2)` bounds its copy by it, so a symlink of
|
||||
size 0 resolves to the empty string — which looks like it worked and is
|
||||
worse than the regular file was.
|
||||
- [x] ~~**OVMF memory reporting.**~~ The region-list fix did close it. 1 GB
|
||||
reports 1017 MB usable, 2 GB reports 2041 MB, which is the right shape:
|
||||
the firmware keeps about 7 MB either way.
|
||||
- [x] ~~**`ps` printed a header and nothing else.**~~ Found while checking the
|
||||
`exe` fix. `ps` with no arguments shows only processes on *this*
|
||||
terminal, and `/proc/<pid>/stat` hardcoded `tty_nr` to 0, so nothing
|
||||
could ever match — `ps -e` always worked, which is what hid it. Fields
|
||||
14-17 (utime/stime/cutime/cstime) were hardcoded zero too, so `TIME` was
|
||||
`00:00:00` for everything however long it had run.
|
||||
|
||||
**Work already done and not connected:**
|
||||
|
||||
These are the galling ones — the replacement exists, is tested, and is unused.
|
||||
|
||||
- [x] ~~**Point the console at a real typeface.**~~ It draws JetBrains Mono
|
||||
through `font_ops` and `ttf.c`. The blocker was not the font: `vtcell.ch`
|
||||
was **one byte** holding a CP437 index, so `Ą` (U+0104) could not be
|
||||
*stored*, whatever was drawing it. The cell is a `uint32` codepoint now
|
||||
and the cell size comes from the font's measured advance.
|
||||
- [x] ~~**Delete the cell grid.**~~ Done as far as it should be done, and the
|
||||
rest of it retired — which needs explaining, because the item as written
|
||||
asked for something a terminal cannot be.
|
||||
|
||||
`include/gfx/text.h` made the case against the grid in four charges: it
|
||||
cannot do variable-width glyphs, cannot position text off a multiple of
|
||||
eight, cannot anti-alias, and cannot hold a codepoint above 255. **All
|
||||
four were about the drawing layer, and all four are now false** — glyphs
|
||||
carry metrics, PSOGM positions text at arbitrary pixels, the TrueType
|
||||
path blends 8-bit coverage, and `vtcell.ch` is a `uint32`.
|
||||
|
||||
What is left is a grid in `vt.c`, and that one is not in the way: it is
|
||||
what a terminal *is*. VT100 escapes address columns, `TIOCGWINSZ`
|
||||
reports rows and columns, and every program that draws a box assumes
|
||||
both. Deleting it would not modernise the console, it would stop it
|
||||
being a terminal.
|
||||
|
||||
So the real remainder was the duplicate: `gfx.c` walked `font_data`
|
||||
itself and blitted glyphs its own way, while `text.c` did the same job
|
||||
into a `struct surface` for nobody but a self-test. Two implementations
|
||||
that had to agree about a cell size only one of them was told had
|
||||
changed — and they did not agree, which is why every line of PSOGM once
|
||||
drew the next character's bitmap underneath itself. There is one blitter
|
||||
now: `gfx.c` hands `text_draw_glyph` the cell *as a surface of its own*,
|
||||
so clipping a tall glyph to its line is the same bounds check that clips
|
||||
to a screen. The bitmap is reached through `font_builtin()` like any
|
||||
other provider, and `gfx.c` no longer includes `font.h`.
|
||||
- [x] ~~**Load a font from disk**~~ and point the console at it. The built-in
|
||||
one is linked in on purpose — an early panic has to be readable — so this
|
||||
is about a *replaceable* font, not about having one. Drop a TrueType file
|
||||
at `\schos\console.ttf` on the ESP and the console is drawn with it.
|
||||
|
||||
**The loader reads it, not the kernel**, and that is the whole design
|
||||
decision. The kernel could open the file once the root filesystem is
|
||||
mounted — but the console has been drawing since long before that, so
|
||||
switching then means re-measuring the cell and re-laying out every
|
||||
terminal mid-boot, which wipes the kernel log off the screen at exactly
|
||||
the moment a machine being brought up for the first time needs it. The
|
||||
loader allocates the pages as `EfiReservedMemoryType`, so the E820
|
||||
handoff marks them reserved, the page allocator never offers them, and
|
||||
the kernel uses the file *in place* — no copy, and no need for a
|
||||
contiguous allocator the kernel does not have.
|
||||
- [x] ~~**Interrupt-driven receive.**~~ The e1000's handler calls
|
||||
`netdev_rx_signal()`, which ends the network thread's wait early; the frame
|
||||
is still *pulled* by the thread, because an interrupt handler can neither
|
||||
sleep on the buffer cache nor take the lwIP lock. The wait stays on the
|
||||
tick so lwIP's timers keep their floor. Proven rather than assumed:
|
||||
`/proc/netif` gained `wake_rx`/`wake_tick`, and across three pings
|
||||
`rx_frames` and `wake_rx` both went 2 -> 6.
|
||||
|
||||
**Structural, because everything after this compounds on it:**
|
||||
|
||||
- [x] ~~**A slab or pool allocator.**~~ `kernel/core/kmem.c`. Slabs are pages
|
||||
from `kalloc()` with a header at the front; free objects hold the free
|
||||
list in their own first eight bytes, so an object needs no header and
|
||||
freeing needs no search — the object's page base *is* its slab, and the
|
||||
slab names its cache. Two interfaces: named `kmem_cache` for a subsystem
|
||||
with one kind of thing, and `kmalloc`/`kmfree` over power-of-two classes
|
||||
for everything else. Visible in `/proc/slab`, asserted by
|
||||
`make kmem-test` on the host — the same arrangement as `schfs-objtest`,
|
||||
so the bookkeeping is checked in a second rather than in a boot.
|
||||
|
||||
**It does not do what this row said it would, and that is worth
|
||||
recording.** The claim was that every fixed table — `NPROC`, `NSOCK`,
|
||||
`NDEVICE`, `NFILE`, `NINODE`, `NBUF` — exists to work around `kalloc`.
|
||||
Measured: `struct proc` is 10656 bytes, `struct socket` 4928, `struct
|
||||
buf` 4272. **All three are larger than a page**, so a page-backed slab
|
||||
cannot hold them whatever it does; `NINODE` is a cache and is supposed to
|
||||
be bounded; and `NFILE`/`NDEVICE` are 48 and 120 bytes, where the table
|
||||
costs 24 KB and 3.8 KB and the ceiling has never been reached. The tables
|
||||
were not all working around `kalloc`. Freeing the big ones needs a
|
||||
contiguous multi-page allocator, which is a different thing and is not
|
||||
written.
|
||||
|
||||
What it does fix is real and was measured rather than assumed. A pipe is
|
||||
640 bytes and came from a whole 4096-byte page, so every `|` in a shell
|
||||
command burned 3.5 KB it never touched — **six pipes per page now**. And
|
||||
`procread()` built its answer in a 2048-byte array on the kernel stack,
|
||||
under a comment that had warned about exactly that since it was written;
|
||||
its frame is 64 bytes now instead of ~2100.
|
||||
- [x] ~~**SchFS on-disk format version policy.**~~ Done. `SCHFS_VERSION` for
|
||||
frame changes, plus `compat`/`ro_compat`/`incompat` feature flags in what
|
||||
was `reserved0` — so existing images keep every field offset and read
|
||||
unchanged. `uber_check()` now distinguishes *damaged* (step over it; that
|
||||
is what the ring is for) from *too new* (stop), because falling back to an
|
||||
older uberblock mounts a stale filesystem and reports success. Proven on a
|
||||
forged image: the old code mounted the stale root and exited 0.
|
||||
Still owed: an actual upgrade step, which is only writable once there are
|
||||
two formats. See [../architecture/schfs.md](../architecture/schfs.md).
|
||||
|
||||
**Found while doing the above, and fixed:**
|
||||
|
||||
- [x] ~~Every `AF_INET` socket failed with `ENFILE` until DHCP bound.~~
|
||||
`inet_new()` required an *address* before it would hand out a socket, and
|
||||
reported the refusal as "file table overflow". Creating a socket is not
|
||||
the same as having somewhere to send; a bind or connect with no route
|
||||
fails on its own with an error that says so. `net_is_ready()` (lwIP up) is
|
||||
now distinct from `net_is_up()` (DHCP bound).
|
||||
- [x] ~~`socktest` asserted "AF_INET is refused, not faked".~~ True when written,
|
||||
false since AF_INET landed — and **green the whole time**, because the
|
||||
suite runs before DHCP binds and the `ENFILE` above made the refusal look
|
||||
real. A test asserting a failure has to say *which* failure.
|
||||
|
||||
**The suite itself:**
|
||||
|
||||
Both of these were found by running `make schfs-check` rather than by reading
|
||||
it, and both predate the current work — confirmed by stashing and re-running.
|
||||
They are here rather than filed as nice-to-haves because a suite that is red for
|
||||
known reasons stops being read, and then it is red for unknown reasons too.
|
||||
|
||||
- [x] ~~**`make schfs-check` fails at baseline.**~~ Fixed, and it was not only a
|
||||
test bug. The allocation map was taken through a `palloc` callback and
|
||||
there was **no way to give it back** — no `schfs_space_release`, and a
|
||||
comment in `schfs_space_init` reading *"caller frees what was taken"* for
|
||||
an API the caller did not have.
|
||||
|
||||
So the kernel leaked the whole map on **every unmount**, up to 2 MB a
|
||||
time, and `schfs_space_init`'s own error path stranded a page it had
|
||||
already taken because `npages` was only raised after *both* allocations
|
||||
succeeded. The test's 518 leaked pages were the symptom that pointed at
|
||||
it.
|
||||
|
||||
The map now carries its own `pfree` so it can only be freed by the
|
||||
allocator that filled it, `schfs_vol_open`/`schfs_vol_format` release any
|
||||
map the volume already holds, and unmount releases. `schfs_objtest`
|
||||
asserts the balance — 520 pages taken, 520 given back — so the next leak
|
||||
fails the suite instead of being absorbed by it.
|
||||
|
||||
- [x] ~~**31 UndefinedBehaviorSanitizer errors.**~~ Fixed, plus two the test
|
||||
never reached. One root cause: a leaf's value area holds variable-length
|
||||
records packed end to end, so a record begins wherever the last one ended
|
||||
— a multiple of 8 only by luck — and the code read them by casting a
|
||||
pointer into the block, which tells the compiler they are aligned when
|
||||
they are not.
|
||||
|
||||
The seven record types that live at arbitrary offsets now carry
|
||||
`SCHFS_PACKED`. **No on-disk layout changed**, and that was measured
|
||||
before it was applied rather than assumed: every one already had explicit
|
||||
`pad` fields, so sizes and member offsets are identical with and without
|
||||
the annotation. Seven `_Static_assert`s now pin those sizes, because a
|
||||
record that silently grows misreads every existing volume from that point
|
||||
on and looks like corruption rather than like a struct that changed.
|
||||
|
||||
`schfs_node_hdr`, `schfs_item`, `schfs_kptr` and `schfs_uberblock` are
|
||||
deliberately left alone — the node header is exactly 32 bytes, so
|
||||
everything after it is already 8-aligned, and annotating them would cost
|
||||
byte-wise access on the tree's hottest paths to fix a problem they do not
|
||||
have.
|
||||
|
||||
Verified by reading an image written *before* the change: full
|
||||
`schfs_fsck --check` passes on structure, checksums, key ordering, data
|
||||
blocks and derived free space.
|
||||
|
||||
Also caught on the way: `mkschfs` passed a null pointer to `qsort` for an
|
||||
empty directory. Every implementation ignores it when the count is zero;
|
||||
the standard does not say they have to.
|
||||
|
||||
**The identity item:**
|
||||
|
||||
- [x] ~~**Boot from a USB disk.**~~ The root device is probed at boot instead of
|
||||
compiled in: ramdisk (only in a `RAMDISK_ROOT` build), then USB, then the
|
||||
IDE disks, first SchFS volume wins. Verified with **no IDE disk attached
|
||||
at all** — boots, mounts the stick, writes a file, and the file is still
|
||||
there on the next boot. `dmesg` says `root: USB disk (device 3)`, and a
|
||||
machine with no root names every candidate it looked at instead of
|
||||
panicking with "no root filesystem". See
|
||||
[../architecture/boot.md](../architecture/boot.md).
|
||||
|
||||
#### The `ls -lR` hang, and why it is closed
|
||||
|
||||
Hunted with the instrumentation it asked for, under a workload heavier than the
|
||||
one that originally triggered it — ten iterations of `ls -lR /bin`, interleaved
|
||||
with writes and `cd`, 33 execs and 13378 tree descents. Measured across it:
|
||||
|
||||
```
|
||||
inode_peak 9 / 200 unchanged
|
||||
file_peak 9 / 512 unchanged
|
||||
buf_pinned_peak 1 / 256 unchanged
|
||||
schfs0 locked 0 tx_owner_pid 0 tx_depth 0
|
||||
```
|
||||
|
||||
Nothing climbed, nothing wedged, and the repaired `holdingsleep` assertion never
|
||||
fired. **That eliminates this file's own stated hypothesis** — "a lock or
|
||||
reference-count leak somewhere in the `*at()` paths" — and it is very likely the
|
||||
bug was fixed in passing by the `*at()` dirfd rewrite, the getdents cookie
|
||||
rewrite, or SchFS becoming the root, all of which landed after it was reported.
|
||||
|
||||
It is closed rather than chased further, and that is a deliberate application of
|
||||
the two-things-in-flight rule. The instrumentation is permanent: if it ever
|
||||
happens again, `/proc/fsstat` names the table, or shows `locked 1` with
|
||||
`tx_owner_pid 0` and the volume lock is orphaned. That is a much better position
|
||||
than the one it was reported from.
|
||||
|
||||
Two real defects were found on the way and fixed, neither proven to be the
|
||||
cause: `holdingsleep()` never compared the owning pid, which disarmed all seven
|
||||
`panic()` assertions built on it; and SchFS's `tx_mine()` compared a bare
|
||||
`struct proc *`, which a recycled ptable slot makes ambiguous.
|
||||
|
||||
### Gate 1 — The foundations
|
||||
|
||||
[foundations.md](foundations.md), items 1–4. Descriptor passing, a shareable
|
||||
memory object, `AF_UNIX` in the filesystem namespace.
|
||||
|
||||
Nothing above needs these; everything below stops without them.
|
||||
|
||||
- [x] ~~**Item 1 — `sendmsg`/`recvmsg` with `SCM_RIGHTS`.**~~ Built and tested;
|
||||
fourteen checks in `cmd/socktest`, each moving data through the passed
|
||||
descriptor rather than inspecting it. Described in
|
||||
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
|
||||
This was also mechanism 1 of [hybrid.md](hybrid.md) — one primitive for
|
||||
both, which is the whole reason those are one plan.
|
||||
- [x] ~~**Item 2 — a shareable memory object.**~~ `memfd_create`, `ftruncate`,
|
||||
`mmap(MAP_SHARED)`. Built on the `shmget` segments exactly as
|
||||
[foundations.md](foundations.md) proposed — the pages, refcounts,
|
||||
`no_free` VMAs and the fork/exit accounting were all already right; what
|
||||
memfd adds is a segment reached by *descriptor* rather than by name or
|
||||
System V key, which is the whole reason it exists on Linux.
|
||||
`cmd/memfdtest`: seventeen checks, ending with a memfd sent over a
|
||||
socket to a receiver that closes every inherited copy first.
|
||||
- [x] ~~**Item 3 — `AF_UNIX` sockets in the filesystem namespace.**~~ `bind()`
|
||||
creates a `T_SOCK` node; `connect()` resolves the path and matches the
|
||||
socket bound to *that inode*, not to a string. `ls -l` shows
|
||||
`srwxr-xr-x`, `stat()` reports `S_IFSOCK`, `unlink()` removes it, and a
|
||||
second bind to a live path is `EADDRINUSE` *from the directory* — which
|
||||
is what makes a socket file left by a crashed server refuse a rebind, as
|
||||
it must. `cmd/socktest` section 15. `flock()` is still missing and is
|
||||
recorded as such.
|
||||
|
||||
### Gate 2 — Init as a role — **done**
|
||||
|
||||
Milestones I0–I3, all built. PID 1 forks a supervisor and spawns jobs on its
|
||||
behalf; the job table lives in `/proc/schinit`; a heartbeat runs both ways; and
|
||||
`proc_reincarnate()` re-execs PID 1 in place rather than panicking. Both failure
|
||||
paths are exercised by `sysrq` keys rather than reasoned about.
|
||||
|
||||
Before the desktop rather than after, because a compositor being brought up for
|
||||
the first time crashes constantly, and something that restarts it and says why is
|
||||
worth far more during that work than at the end of it.
|
||||
|
||||
The description is now [../architecture/init.md](../architecture/init.md); the
|
||||
plan document it replaced has been deleted, per
|
||||
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||||
|
||||
### Gate 3 — The desktop
|
||||
|
||||
[xorg.md](xorg.md): Xorg on the framebuffer, twm and xterm, then Window
|
||||
Maker. Wayland ([wayland.md](wayland.md)) is not being worked on -- what it
|
||||
needs, DRM/KMS above all, is far larger than what X needs.
|
||||
|
||||
**W4 and W5 are not in 1.0.** Multi-window focus policy and a panel are
|
||||
post-1.0. One terminal window that works is the bar.
|
||||
| Gate | What | State |
|
||||
|---|---|---|
|
||||
| **0 — Fix what is already wrong** | the SMP register corruption, `setsockopt` that lied, timestamps that read 1970, the `ls -lR` hang (hunted, not reproducible — [history](../history/kernel.md) §24), … | **closed** |
|
||||
| **1 — The foundations** | descriptor passing, a shareable memory object (`memfd`), `AF_UNIX` in the filesystem, dynamic linking, the event descriptors | **closed** — [kernel.md](../architecture/kernel.md), [networking.md](../architecture/networking.md) |
|
||||
| **2 — Init as a role** | `schinit`, the supervisor, the heartbeat, PID 1 re-exec'd in place | **closed** — [init.md](../architecture/init.md) |
|
||||
| **3 — The desktop** | Xorg on the framebuffer, Window Maker, a terminal | **closed** — [desktop.md](../architecture/desktop.md) |
|
||||
| **4 — A root that survives a reboot** | dropped from 1.0 by decision; it is the installer, after 1.0 | **not in 1.0** |
|
||||
|
||||
---
|
||||
|
||||
## Not in 1.0
|
||||
|
||||
The point of this list is that it is *written down*, so it can stop occupying
|
||||
attention. Nothing here is cancelled.
|
||||
The point of this list is that it is *written down*, so it can stop occupying attention. Nothing here is cancelled.
|
||||
|
||||
| | Why it waits |
|
||||
|---|---|
|
||||
| **The rest of the hybrid kernel** ([hybrid.md](hybrid.md)) | Still not a completeness requirement — but the mechanisms and the first eviction happened early, because the alternative to a driver compatibility layer is writing a driver for every device that exists, and that is not a plan. What is deferred is the foreign driver host, not the interface it needs. |
|
||||
| **Mesa, SDL3** | The kernel and musl do dynamic linking now (`cmd/dynhello`), but every shipped program is still static. Mesa is the single biggest post-1.0 unlock and it is a whole project. |
|
||||
| **A page cache** | `memfd` covers the desktop's needs. General writable `MAP_SHARED` of a file is a real project with no 1.0 consumer. |
|
||||
| **`.sip` and the installer** | 1.0 installs by writing a USB stick. That is how most systems start and it is honest. |
|
||||
| **SchFS snapshots, FUSE, Linux module, sparse files, online fsck** | The format version (Gate 0) is the prerequisite for all of them, and it is the only part on a clock. |
|
||||
| **The interactive crash screen** | PSOGM renders, prints a full report and emits a QR code. That is enough. |
|
||||
| **GPU acceleration, Mesa** | [gpu.md](gpu.md). A whole project; software rendering is enough to use the machine. |
|
||||
| **A page cache** | General writable `MAP_SHARED` of a file is a real project with no 1.0 consumer. |
|
||||
| **SchFS snapshots, FUSE, a Linux module, sparse files, online fsck** | The format version is the prerequisite and it exists; the rest is [roadmap.md](roadmap.md) §6. |
|
||||
| **CFF/OTF fonts, kerning, text layout** | TrueType and fixed advances are enough for a terminal. |
|
||||
| **Routing, `getsockname`/`getpeername`, IPv6** | One interface, one default route, works. |
|
||||
| **Multiple outputs, damage tracking, subsurfaces, xkbcommon** | One screen, full repaints, one keymap. |
|
||||
| **Audio** | Does not exist in any form and is not required to use a machine. |
|
||||
| **Panel, notifications, settings, file manager** | A terminal is a desktop. |
|
||||
| **SchAPI itself** | Deliberately. It should be designed *from* the compositor's experience, not before it. |
|
||||
| **Routing, IPv6, Wi-Fi** | One interface, one default route, works. |
|
||||
| **HDA audio, recording, mixing** | AC'97 plays; that is sound. |
|
||||
| **Threads' loose ends, `ptrace`, `seccomp`** | Whatever a package needs is found by running it. |
|
||||
| **SchAPI itself** | Deliberately. It should be designed from the desktop's experience, not before it. |
|
||||
|
||||
---
|
||||
|
||||
## The rule for this list
|
||||
|
||||
**Adding to Gate 0–3 requires deleting something else from it.**
|
||||
**Adding to the gates requires deleting something else from them.**
|
||||
|
||||
Not a productivity trick — a forcing function. The reason this project feels
|
||||
overwhelming is that ~20 fronts have been open simultaneously, and no amount of
|
||||
discipline makes that workable. The fix is structural.
|
||||
Not a productivity trick — a forcing function. The reason this project felt overwhelming is that ~20 fronts
|
||||
were open simultaneously, and no amount of discipline makes that workable. The fix is structural.
|
||||
|
||||
And one working limit, worth holding to literally:
|
||||
|
||||
> **Two things in flight. Never three.**
|
||||
|
||||
One in Gate 0 and one being written up. Everything else is closed — in the
|
||||
document, and out of your head.
|
||||
|
||||
---
|
||||
|
||||
## How you will know it is going well
|
||||
|
||||
Not by velocity. By this:
|
||||
|
||||
**Every closed item leaves a description behind.** When the `ls -lR` hang is
|
||||
fixed, `history/kernel.md` gains an entry saying what it actually was. When the
|
||||
console moves onto the text layer, `architecture/graphics.md` loses a line from
|
||||
its *What it does not do* section.
|
||||
|
||||
Those "does not do" sections shrinking is the real progress bar, and it is the
|
||||
only one that does not lie.
|
||||
|
||||
---
|
||||
Not by velocity. By this: **every closed item leaves a description behind.** When the `ls -lR` hang was
|
||||
closed, `history/kernel.md` gained an entry. When a gap closed, a "what it does not do" section shrank.
|
||||
Those sections shrinking is the real progress bar, and it is the only one that does not lie.
|
||||
|
||||
## A note worth keeping
|
||||
|
||||
This document was written because the work started to feel overwhelming, and
|
||||
that is worth recording rather than tidying away — because the feeling was
|
||||
about the *shape* of the work and not its quality, and the two are easy to
|
||||
confuse when you are inside it.
|
||||
This document was written because the work started to feel overwhelming, and that is worth recording rather
|
||||
than tidying away — because the feeling was about the *shape* of the work and not its quality, and the two are
|
||||
easy to confuse when you are inside it.
|
||||
|
||||
The evidence at the time: SMP working on real hardware after five bugs stacked
|
||||
behind each other; a copy-on-write filesystem whose crash test cuts the power one
|
||||
write later, 119 times, and demands a clean mount after each; a TCP/IP stack
|
||||
asserted from both ends; an xHCI driver written from the specification; and a
|
||||
documentation tree that records why things are strange so they do not get
|
||||
un-fixed by accident.
|
||||
The evidence at the time: SMP working on real hardware after five bugs stacked behind each other; a copy-on-write
|
||||
filesystem whose crash test cuts the power one write later, 119 times, and demands a clean mount after each; a
|
||||
TCP/IP stack asserted from both ends; an xHCI driver written from the specification; and a documentation tree that
|
||||
records why things are strange so they do not get un-fixed by accident. Since then: a desktop, real Linux
|
||||
programs and Linux's own drivers, running.
|
||||
|
||||
None of that is the output of a project in trouble. It was a project with too
|
||||
many tabs open.
|
||||
None of that is the output of a project in trouble. It was a project with too many tabs open.
|
||||
|
|
|
|||
|
|
@ -1,143 +1,65 @@
|
|||
# What happens next
|
||||
|
||||
Everything in this directory is **unbuilt**. When a piece of it is finished it
|
||||
leaves — deleted from here, rewritten as a description in
|
||||
[../architecture/](../architecture/). See [the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||||
Everything in this directory is **unbuilt**. When a piece of it is finished it leaves — deleted from here,
|
||||
rewritten as a description in [../architecture/](../architecture/). See
|
||||
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||||
|
||||
> ## Read [1.0.md](1.0.md) first.
|
||||
>
|
||||
> It draws a finish line and puts most of the work below on the far side of it.
|
||||
> **Anything not named there is not being worked on**, and the documents in this
|
||||
> directory describe more than 1.0 contains.
|
||||
>
|
||||
> It also holds **[the register of unfinished work](1.0.md#the-freeze-and-the-register-of-unfinished-work)**,
|
||||
> which is the one place anything half-built is listed. No new capability starts
|
||||
> until that register is empty, and a row leaves it only by being finished or by
|
||||
> being deleted from the tree.
|
||||
> It draws the finish line, says where the system stands against it, and holds
|
||||
> **[the register of unfinished work](1.0.md#the-freeze-and-the-register-of-unfinished-work)**
|
||||
> — the one place anything half-built is listed. No new capability starts until that register is
|
||||
> empty, and a row leaves it only by being finished or by being deleted from the tree.
|
||||
|
||||
---
|
||||
|
||||
## The three current decisions
|
||||
|
||||
Taken together, because they turn out to need the same foundation.
|
||||
## What is here
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **[alpine.md](alpine.md)** | **Packages.** The root is managed by Alpine's apk: Alpine's binaries run unmodified, and SchOS itself is the apk package schos-base. |
|
||||
| **[xorg.md](xorg.md)** | **The desktop.** Xorg on the framebuffer with the fbdev and evdev drivers; twm, then Window Maker. Replaces wayland.md for Gate 3. |
|
||||
| **[wayland.md](wayland.md)** | *Not being worked on; see xorg.md.* The GUI stack becomes Wayland. SchWM and SchDE are **deleted** — actually gone from the tree, not marked; Schcom survives and changes its wire format. |
|
||||
| ~~**hybrid.md**~~ | **Built.** All four mechanisms exist, the GPU driver runs outside the kernel, it survives being killed, and the display mode can be changed from a process. H4 (USB) was *withdrawn* rather than left open — USB is the bus peripherals arrive on, not a peripheral. |
|
||||
| ~~init.md~~ | **Built.** PID 1 is now a role the kernel refills; the description moved to [../architecture/init.md](../architecture/init.md). |
|
||||
| **[1.0.md](1.0.md)** | **The finish line.** What 1.0 is, the acceptance test and where each clause stands, the register, and everything explicitly deferred. |
|
||||
| **[roadmap.md](roadmap.md)** | Everything else that is scoped and agreed: storage and the installer, graphics, networking, the hardware that makes a laptop a laptop, SchFS beyond SchOS, the kernel's own gaps. |
|
||||
| **[gpu.md](gpu.md)** | Linux's own `amdgpu` through LDLS, and the `/dev/dri` a process has to serve. Measured, with the work it needs written down. |
|
||||
| **[finch.md](finch.md)** | **Later.** The author's own LLVM language for SchOS's applications and tools in place of C. The language is finished and self-hosting in its own repository; the plan here is a target, bindings generated from `include/uapi/`, and the compiler in the image. |
|
||||
| **[schapi.md](schapi.md)** | The long-term API layering and the compatibility promise. Direction agreed, nothing built. |
|
||||
|
||||
And the one they all wait on:
|
||||
## What used to be here, and is built
|
||||
|
||||
| | |
|
||||
Plans leave this directory when they are done. These did, and what replaced them is the description:
|
||||
|
||||
| Was | Now |
|
||||
|---|---|
|
||||
| **[foundations.md](foundations.md)** | Descriptor passing, shared memory, sockets in the filesystem. The critical path. **Item 1 is built** — `sendmsg`/`recvmsg` with `SCM_RIGHTS`. |
|
||||
| Descriptor passing, shared memory, sockets in the filesystem, dynamic linking, the event descriptors (`foundations.md`) | [kernel.md](../architecture/kernel.md), [networking.md](../architecture/networking.md), [syscalls.md](../architecture/syscalls.md) |
|
||||
| `init.md` | [init.md](../architecture/init.md) |
|
||||
| `hybrid.md` — drivers in processes, `/dev/pci`, a modeset API, the panic path, why USB stays | [devices.md](../architecture/devices.md) |
|
||||
| `ldls.md` — Linux drivers in a process | [ldls.md](../architecture/ldls.md) |
|
||||
| `debugger.md` | [debugging.md](../architecture/debugging.md) |
|
||||
| `alpine.md` — an apk-managed root | [packages.md](../architecture/packages.md) |
|
||||
| `xorg.md` — the desktop | [desktop.md](../architecture/desktop.md) |
|
||||
| The interactive crash screen (`roadmap.md` §4) | GMMOS, in [graphics.md](../architecture/graphics.md) |
|
||||
|
||||
And the one that says which parts of the above are actually being worked on:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **[1.0.md](1.0.md)** | The finish line. Four gates, and everything else explicitly deferred. |
|
||||
|
||||
And the one the hybrid mechanisms were built for:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| ~~ldls.md~~ | **Built.** The Linux Driver Layer for SchOS: someone else's drivers, in a process, reached through `/dev/pci`. `ping` works over `8139too` and over `e1000`. The description moved to [../architecture/ldls.md](../architecture/ldls.md). |
|
||||
| ~~debugger.md~~ | **Built**, all of it, including the network transport. A debugger that observes rather than halts and answers in this kernel's own vocabulary: symbols, a log that outlives the kernel, `/dev/dbg`, `whose`, hardware watchpoints, and `schdbgd`. The description moved to [../architecture/debugging.md](../architecture/debugging.md). |
|
||||
|
||||
Two longer-horizon documents sit behind these: [schapi.md](schapi.md), the API
|
||||
layering and the compatibility promise, and [roadmap.md](roadmap.md), which holds
|
||||
everything still open that is not one of the three above.
|
||||
And one that was **decided against**: `wayland.md`. Wayland needs DRM/KMS, libinput, udev and libseat, and DRM/KMS
|
||||
alone is a kernel graphics subsystem. The desktop is X. The reasoning moved to [desktop.md](../architecture/desktop.md).
|
||||
`SchWM`, `SchDE` and the compositor `Schcom` are gone from the tree: SchOS uses other people's window manager and desktop — Window Maker
|
||||
now; XLibre, Xorg or a Wayland compositor later — and puts its work into what they need underneath (udev, DRM).
|
||||
|
||||
---
|
||||
|
||||
## The order
|
||||
|
||||
Set by [1.0.md](1.0.md), which is the authority. In short:
|
||||
Set by [1.0.md](1.0.md), which is the authority. **1.0 is reached** (a live system; persistence is not part of it).
|
||||
What is left before anything new starts:
|
||||
|
||||
### 0. Fix what is already wrong — **first**
|
||||
|
||||
Not a document of its own; it is Gate 0 of [1.0.md](1.0.md), assembled from the
|
||||
*What it does not do* sections in [../architecture/](../architecture/).
|
||||
|
||||
**Before any new capability**, and the reason is the one that put
|
||||
`netdev_selftest()` before the TCP/IP stack: a compositor built on a kernel with
|
||||
an unexplained hang and a socket layer that reports success for things it did not
|
||||
do will fail in ways indistinguishable from its own bugs.
|
||||
|
||||
### 1. `foundations.md` — the critical path
|
||||
|
||||
Both the Wayland work and the hybrid work stop here. Descriptor passing and
|
||||
shared memory are the substrate for handing a buffer to a compositor *and* for
|
||||
handing a device to a driver process. **Doing it once for both is the entire
|
||||
reason these are one plan rather than three.**
|
||||
|
||||
### 2. `init.md` — before the desktop, not after — **done**
|
||||
|
||||
A compositor being brought up for the first time crashes constantly, and
|
||||
something that restarts it and says why is worth far more during that work than
|
||||
at the end of it. Built; see [../architecture/init.md](../architecture/init.md).
|
||||
|
||||
### 3. `hybrid.md` — **moved up, and the reason is worth recording**
|
||||
|
||||
This document used to put the hybrid work *after 1.0*, deliberately last, on this
|
||||
argument:
|
||||
|
||||
> designing a userspace driver interface with no driver consuming it produces an
|
||||
> interface that is wrong in ways nobody can see
|
||||
|
||||
The argument was sound and the conclusion drawn from it was wrong. What forced
|
||||
the change was the driver situation: SchOS cannot write a driver for every device
|
||||
that exists, no project can, and the only route to the hardware support an
|
||||
established OS has is a compatibility layer with the foreign drivers kept
|
||||
*outside* the kernel. That layer is this plan's mechanisms. It is not optional and
|
||||
it is not late work.
|
||||
|
||||
The original argument still holds, and it is answered rather than ignored:
|
||||
**SchOS's own drivers are the consumers.** `vgpud` is a real driver, with real
|
||||
requirements, and building the interface against it is what keeps it a general
|
||||
mechanism rather than a hole shaped like something foreign. The compositor is
|
||||
still a demanding consumer — it is no longer the *only* one available.
|
||||
|
||||
Four mechanisms, all built. One eviction done, the second withdrawn with its
|
||||
reasons recorded, and the foreign driver host they were for is
|
||||
[../architecture/ldls.md](../architecture/ldls.md) — built, and carrying
|
||||
packets.
|
||||
|
||||
### 4. `wayland.md` — with the GPU driver either side of the seam
|
||||
|
||||
W1–W3 and a terminal. W4 and W5 are post-1.0. Item 1 of `foundations.md` —
|
||||
descriptor passing — was the thing it could not start without, and it now
|
||||
exists.
|
||||
|
||||
---
|
||||
|
||||
## In one sentence
|
||||
|
||||
> Fix what is broken, build the descriptor-passing and shared-memory substrate,
|
||||
> make init survivable, move the drivers out through an interface SchOS's own
|
||||
> drivers have to live with first — and put a terminal on the screen.
|
||||
|
||||
---
|
||||
1. **Empty the register**: decide the unused in-kernel LDLS window (delete it, or use it), and build socket activation or
|
||||
remove the key.
|
||||
2. **The owner's list** ([roadmap.md](roadmap.md)): the Wayland groundwork (udev first, §5), making LDLS good enough that the GPU
|
||||
driver can run ([gpu.md](gpu.md)), a boot manager worth looking at (§5b), then the installer, then Finch inside SchOS
|
||||
([finch.md](finch.md)).
|
||||
|
||||
## What this plan does not address
|
||||
|
||||
Stated here so it is not mistaken for covered:
|
||||
|
||||
- **No IOMMU**, so the hybrid work isolates against bugs and not against a
|
||||
hostile driver.
|
||||
- **No page cache**, so `foundations.md` delivers a `memfd` and not a general
|
||||
answer to shared file mappings. The general answer is still owed.
|
||||
- **No garbage collector for descriptors in flight**, so an `AF_UNIX` socket
|
||||
passed over itself and never received leaks. Bounded at thirty-two sockets, and
|
||||
nothing on this plan does it.
|
||||
- **No dynamic linking**, so no Mesa, no accelerated clients, and no third party
|
||||
shipping a `.so` for anything.
|
||||
- **A new compatibility surface** between PID 1 and PID 2, created by a plan
|
||||
whose own [schapi.md](schapi.md) §3 spends a section warning about those.
|
||||
- **The three open networking items** — `getsockname`, routing, and
|
||||
interrupt-driven receive — are untouched by any of this and still open. They
|
||||
are in [roadmap.md](roadmap.md).
|
||||
- **No IOMMU**, so a driver in a process is isolated against its own bugs and not against a hostile driver.
|
||||
- **No page cache**, so a general writable `MAP_SHARED` of a file is refused; `memfd` is the narrow answer.
|
||||
- **No installer**, so nothing a live system writes survives a reboot — by decision, for 1.0.
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
# apk: Alpine's packages on SchOS
|
||||
|
||||
Alpine is musl with a different init, and SchOS is musl with a different
|
||||
kernel. Alpine's binaries run on SchOS unmodified: its dynamic linker, its
|
||||
busybox, and apk itself, fetching over TLS through SchOS's network stack.
|
||||
So instead of porting programs one at a time, SchOS can install them.
|
||||
|
||||
## The model
|
||||
|
||||
The root filesystem is an apk-managed root:
|
||||
|
||||
| Part | Where it comes from |
|
||||
|---|---|
|
||||
| musl, busybox, apk-tools, alpine-keys, CA certificates | Alpine's minirootfs (`ports/alpine`) |
|
||||
| what `ports/alpine/world` lists (zsh, terminfo, ...) | Alpine's packages, pinned in `world.lock` |
|
||||
| schinit, its jobs, SchOS's tools, toybox, mksh, configuration | **schos-base**, an apk package the build makes from `sysroot/` |
|
||||
|
||||
`apk add` installs anything from Alpine's main and community repositories.
|
||||
`apk upgrade` updates Alpine's packages, and schos-base from the SchOS
|
||||
repository at `/var/lib/schos/repo`.
|
||||
|
||||
PID 1 is still schinit. Alpine's OpenRC is not installed, and packages that
|
||||
ship OpenRC services install them where nothing reads them.
|
||||
|
||||
## Who owns what
|
||||
|
||||
Where Alpine and SchOS both have a program, Alpine's wins, and SchOS keeps
|
||||
only what is its own (see `tools/posix/mkaroot.sh`):
|
||||
|
||||
- `/bin/sh`, `cat`, `sed`, `find` and the rest are busybox. Alpine's package
|
||||
scripts are written against busybox.
|
||||
- zsh is Alpine's. SchOS keeps its `zshrc` and `zshenv` (`replaces=zsh`).
|
||||
- `/etc/os-release` and `/etc/motd` are SchOS's (`replaces=`).
|
||||
- `passwd`, `group` and `profile` are Alpine's. schos-base's post-install
|
||||
sets root's shell to zsh, and SchOS's profile is `/etc/profile.d/schos.sh`.
|
||||
- mksh is `/bin/mksh`. schinit's recovery shell looks for it first because it
|
||||
is static: a broken musl cannot take recovery down with it.
|
||||
|
||||
## Building
|
||||
|
||||
make qemu boots build/schfs.img, the root as a 1 GB SchFS disk
|
||||
make iso the same root as the live system's ramdisk
|
||||
make alpine-lock re-resolve world against Alpine (needs the network)
|
||||
|
||||
It is the only root. sysroot/ is what schos-base is packaged from.
|
||||
|
||||
Only `alpine-lock` asks Alpine what is current. A normal build installs
|
||||
exactly what `world.lock` names: files in `dist/alpine/`, fetched once and
|
||||
checked against the lock's sha256, and against Alpine's signatures by apk.
|
||||
|
||||
schos-base and its repository index are signed with a key made on the first
|
||||
build (`build/apk-keys/`, never committed). The root trusts it beside
|
||||
Alpine's keys.
|
||||
|
||||
## Kernel work this needed
|
||||
|
||||
All of it found by running apk:
|
||||
|
||||
- **TCP receive window:** it reopened before the reader drained, so lwIP
|
||||
refused data and the sender waited on retransmission timers (5 KB/s). It
|
||||
now reopens on read, with a 32-segment window.
|
||||
- **mmap address reuse:** freed ranges were never reused, so musl's malloc
|
||||
used up the 256 mapping records with 780 MB of RAM free.
|
||||
- **unlinkat / renameat:** bare `-1`, read as EPERM, where apk expects ENOENT;
|
||||
renameat could not replace its target, and wrote v1 dirents into SchFS.
|
||||
- **`#!` scripts in execve**, and ENOEXEC: apk runs triggers directly.
|
||||
- **chroot**, and mounting devtmpfs and proc more than once.
|
||||
|
||||
## Next
|
||||
|
||||
- The live ISO runs the apk root from RAM. The image in the kernel is only
|
||||
what the root holds; at boot the volume grows into half of the free memory
|
||||
(4 GB of root on an 8 GB machine), backed by pages as they are written.
|
||||
Xorg installs into it in about a minute. All of it is gone at reboot. Keeping packages needs an installer that writes the root
|
||||
to a SATA or NVMe disk, and a kernel that finds its root there by the
|
||||
SchFS GPT type rather than as IDE disk 1.
|
||||
- Xorg and Window Maker are in `world`, so the image starts with them:
|
||||
`startx` runs `wmaker` from `/root/.xinitrc`. Mesa brings LLVM with it,
|
||||
about 200 MB of the 358 MB root; the root ships compressed
|
||||
([SCFF](../architecture/scff.md)), so that costs about 60 MB of ISO.
|
||||
77
docs/plan/finch.md
Normal file
77
docs/plan/finch.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Finch, for SchOS's own programs
|
||||
|
||||
**Two things, and only one of them is later.**
|
||||
|
||||
- *Bringing Finch into SchOS* (below) is **later** — behind [1.0.md](1.0.md)'s
|
||||
freeze like everything else here that is not named there.
|
||||
- *Finishing Finch itself*, and making it fully self-hosting, is work on the
|
||||
Finch repository and does not touch this tree. It can proceed now; see
|
||||
[the gap](#the-self-hosting-gap).
|
||||
|
||||
Finch is the author's own language: <https://github.com/Nexoniarz/Finch> (mirrored to
|
||||
`pandor.pl/git/nexoniarz/Finch`, which is read-only). The plan is to write SchOS's own
|
||||
applications and tools in it, in place of C, and so leave behind what C does
|
||||
badly: silent narrowing, signed/unsigned mixing, undefined behaviour on an
|
||||
out-of-range index, a forgotten `free`.
|
||||
|
||||
## What it is today (version 2.4.0)
|
||||
|
||||
Measured from the repository, not from its README alone:
|
||||
|
||||
- **LLVM front end**, C-like syntax, no semicolons, no headers, no preprocessor.
|
||||
It compiles through LLVM's `-O2` pipeline to native code; there is no
|
||||
garbage collector.
|
||||
- **Values, not references:** lists, text and structs copy on assignment and
|
||||
are freed at the end of their block. Out-of-range indexes, division by zero
|
||||
and null pointers stop with a runtime error rather than undefined behaviour.
|
||||
- **Errors as values** (`int(s) or 0`, `try read_file(p)`), maps, methods,
|
||||
modules.
|
||||
- **C interop through libclang:** `import "foo.h"` plus `link "foo"` calls any C
|
||||
library, structs by value and callbacks included. That is the way in to
|
||||
everything SchOS already has — musl, the `/dev/*` interfaces, Alpine's
|
||||
libraries.
|
||||
- **Targets:** Linux, macOS, Windows, on x86-64 and ARM64 (`src/platform.fch`;
|
||||
the System V, AAPCS64 and Microsoft x64 ABIs are in `src/gen.fch`). ARM64 and
|
||||
Windows programs are run under qemu and Wine in the tests; macOS is IR-checked.
|
||||
- **Language:** enums, `match`, generics, errors as values, maps, methods,
|
||||
`defer`, and a standard library in Finch (`json`, `path`, `time`, `proc`).
|
||||
- **Tooling:** a language server (`finch lsp`, written in Finch), VS Code and
|
||||
Kate support, 107 tests.
|
||||
- **Written in itself:** the whole compiler is Finch (about 8,500 lines in
|
||||
`src/`, no C++) and builds itself from a committed seed to a byte-identical
|
||||
result — `bootstrap/build.sh`.
|
||||
|
||||
## What SchOS would need to do
|
||||
|
||||
1. **A `schos` target.** `finch` picks a triple, a C compiler and a linker per
|
||||
target. SchOS runs unmodified Alpine binaries and Alpine's musl
|
||||
(see [packages](../architecture/packages.md)), so the first target is **the existing
|
||||
`x86_64-unknown-linux-musl`**, and a Finch program built on the host should
|
||||
run on SchOS before anything in Finch is made SchOS-specific. Test that
|
||||
before writing a line of target code.
|
||||
2. **The compiler on SchOS.** Needs LLVM 21 and libclang, both of which Alpine
|
||||
packages. That is a question of apk and memory, not of SchOS.
|
||||
3. **The Finch-written compiler on SchOS.** It emits LLVM IR as text and needs
|
||||
only `clang` and libclang, no LLVM C++ libraries, so it is the compiler that
|
||||
would ship in the image. It needs a seed made for the SchOS target.
|
||||
4. **Bindings** for SchOS's own interfaces (`/dev/pci`, `/dev/netdev`,
|
||||
schinit's job protocol, the framebuffer) — generated by the C importer from
|
||||
the headers in `include/uapi/`, so they are not hand-written and cannot
|
||||
drift from the kernel.
|
||||
|
||||
## Where self-hosting stands
|
||||
|
||||
The compiler is written in Finch and nothing else, and it is `main` of `github.com/Nexoniarz/Finch`
|
||||
(`before-bootstrap` there is the all-C++ compiler, kept as the backup). `bootstrap/build.sh` builds it from a seed (its
|
||||
own IR) and then from itself, and checks the two make identical IR. 110 tests pass, also under valgrind with no leaks.
|
||||
Seeds: Linux x86-64 and ARM64; Windows x86-64 (runs under Wine, compiles itself and passes every test program); macOS
|
||||
ARM64 and x86-64 (made with the 11.3 SDK, checked as IR only — nobody has run them on a Mac). The language has enums
|
||||
(also with values inside), `match`, generics and a standard library; the compiler carries its runtime and std inside
|
||||
itself.
|
||||
|
||||
## Rules
|
||||
|
||||
- Inference, not an instruction: the kernel stays in C. Finch is for
|
||||
applications and tools; the user said "our apps or stuff like that".
|
||||
- No new capability starts until [the register of unfinished work](1.0.md#the-freeze-and-the-register-of-unfinished-work)
|
||||
is empty; the SchOS half of this document is a plan, not an exception to that.
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
# Foundations
|
||||
|
||||
The kernel work that [wayland.md](wayland.md) and [hybrid.md](hybrid.md) both
|
||||
stop at. **This is the critical path**, and it is the reason those two are one
|
||||
plan rather than two.
|
||||
|
||||
The insight is small and it decides the schedule: the substrate for passing a
|
||||
buffer to a compositor and the substrate for handing a device to a driver process
|
||||
are the same substrate. Descriptor passing and shared memory, built once.
|
||||
|
||||
---
|
||||
|
||||
## What is actually in the tree
|
||||
|
||||
Measured against the source, not against a README. Each of these was checked
|
||||
before anything below was planned around it:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| ~~`sendmsg`/`recvmsg`, and therefore **`SCM_RIGHTS`**~~ | **Built.** See item 1 below. |
|
||||
| Writable `MAP_SHARED` of a file | **Refused** — `kernel/sys/sys_mem.c:131` returns `-ENODEV`. |
|
||||
| `memfd_create` | Does not exist. |
|
||||
| `AF_UNIX` names | In a table in `kernel/net/unix.c`, not in the filesystem. |
|
||||
| `setsockopt` | `return 0;` unconditionally — `kernel/sys/sys_net.c:436`. |
|
||||
| `exec` | `ET_EXEC` only. No `PT_INTERP`, no `ET_DYN`. |
|
||||
|
||||
> **`projects/schcom/README.md` said `SCM_RIGHTS` was done while nothing in the
|
||||
> kernel implemented it** — the most load-bearing wrong line in the tree, with an
|
||||
> entire GUI plan drawn on top of it. The line is now true, which is not the same
|
||||
> as it having been right, and the README says so.
|
||||
|
||||
The `MAP_SHARED` refusal is worth defending rather than fixing carelessly. The
|
||||
code returns `-ENODEV` *honestly*: the contract of a writable shared mapping is
|
||||
that stores land in the file and other mappers see them, and there is no page
|
||||
cache that could make that true. Quietly handing back a private copy would be the
|
||||
same lie in a new place.
|
||||
|
||||
---
|
||||
|
||||
## The six items
|
||||
|
||||
In dependency order. The first three are what Wayland cannot start without.
|
||||
All but item 4 are built.
|
||||
|
||||
### 1. `sendmsg` / `recvmsg` with `SCM_RIGHTS` — **done**
|
||||
|
||||
Not optional and not substitutable. `wl_shm_create_pool` passes a **file
|
||||
descriptor**; there is no path in the Wayland protocol that passes an integer
|
||||
instead.
|
||||
|
||||
The `handle_kind` field in `projects/schcom/proto.h` was good foresight and it
|
||||
cannot help here — *our* protocol is the flexible one, and Wayland's is not.
|
||||
|
||||
Built as described: `struct msghdr`/`cmsghdr` in Linux's layout, ancillary data
|
||||
queued alongside the byte stream in `kernel/net/unix.c`, descriptor installation
|
||||
into the receiver's file table, and the three ways a message can end — received,
|
||||
received without room, never received — each releasing the kernel's reference.
|
||||
Fourteen checks in `cmd/socktest`, every one of them moving data through the
|
||||
descriptor rather than inspecting it. Documented in
|
||||
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
|
||||
|
||||
The delivery rule is the part worth knowing before building on it: **descriptors
|
||||
are pinned to the stream position of the first byte of the message that carried
|
||||
them**, and a read that has not reached that position is cut short so it stops
|
||||
before it. That is what keeps "which message did these belong to" answerable.
|
||||
|
||||
**This was also item 1 of the hybrid work**, where it is how a driver process is
|
||||
handed the device it is allowed to touch.
|
||||
|
||||
Two bugs fell out of building it, neither in the new code:
|
||||
|
||||
- `sockpair()` never set the socket's domain, so **every `socketpair()` socket
|
||||
claimed domain 0**. Invisible for as long as the data paths asked only whether
|
||||
a socket was `AF_INET` or `AF_NETLINK` — zero answers "neither" correctly.
|
||||
`SCM_RIGHTS` asks "is this `AF_UNIX`", and zero is wrong for that one.
|
||||
- `pipe_poll()` reports `POLLHUP` but not `POLLIN` for a pipe at end of file, so
|
||||
a **non-blocking read of a pipe whose writers have all gone returned `EAGAIN`
|
||||
forever** instead of 0. A program that polls and reads would spin waiting for
|
||||
an end that had already happened. `sockreadable()` always had the equivalent
|
||||
condition; the pipe path did not.
|
||||
|
||||
### 2. A shareable memory object — **done**
|
||||
|
||||
`memfd_create`, plus `mmap(MAP_SHARED)` on it genuinely working between two
|
||||
processes. Described in
|
||||
[../architecture/kernel.md](../architecture/kernel.md#shared-memory-objects);
|
||||
asserted by `cmd/memfdtest`, seventeen checks including the one that matters —
|
||||
a descriptor sent over a socket, mapped by a receiver that has closed every
|
||||
inherited copy, with stores visible in both directions.
|
||||
|
||||
Narrower than a general page cache, and sufficient. An anonymous shared object
|
||||
needs no file behind it, which is exactly why `memfd` exists on Linux at all —
|
||||
and it means this can be built on the shared-memory machinery already in
|
||||
`kernel/sys/sys_mem.c` (`shmget`/`shmat`/`shmdt` work, and `struct vma` already
|
||||
carries `shmid` and a `no_free` flag for pages that are not ours to release).
|
||||
|
||||
The general answer — a page cache, and writable `MAP_SHARED` of a real file —
|
||||
stays owed. It is a larger project and nothing on this plan needs it.
|
||||
|
||||
### 3. `AF_UNIX` sockets in the filesystem namespace — **done**
|
||||
|
||||
Clients find a Wayland compositor by `bind()`ing `$XDG_RUNTIME_DIR/wayland-0` as
|
||||
a real path, and libwayland `flock()`s the lock file beside it. A name table can
|
||||
express neither.
|
||||
|
||||
`bind()` creates a `T_SOCK` node and `connect()` resolves the path and matches
|
||||
the socket bound to **that inode** — not to a string, which is what made two
|
||||
spellings of one path miss each other. `ls -l` shows `srwxr-xr-x`, `stat()`
|
||||
reports `S_IFSOCK`, and `unlink()` removes it. Asserted by `cmd/socktest`,
|
||||
section 15. `flock()` is still not implemented; see below.
|
||||
|
||||
`kernel/net/unix.c`'s own header comment already predicted this:
|
||||
|
||||
> *"What is lost is `ls -l /tmp/schcom` showing an `s`, and `unlink()` of a socket
|
||||
> path failing with ENOENT. Both are worth fixing when the VFS next gets opened
|
||||
> up; neither blocks anything."*
|
||||
|
||||
They block something now. This is that moment, and it is a VFS change rather than
|
||||
a socket change: a socket inode type threaded through `iget`, `dirlookup` and
|
||||
`stat`, plus `flock`.
|
||||
|
||||
### 4. `setsockopt` that does not lie
|
||||
|
||||
At minimum `SO_PASSCRED` and `SO_SNDBUF`. Returning 0 for an option you ignored
|
||||
is worse than `ENOPROTOOPT`, because the caller adjusts its behaviour on the
|
||||
strength of the lie.
|
||||
|
||||
Small, and grouped here because it is in the same file as item 3 and will be
|
||||
found by the same testing.
|
||||
|
||||
### 5. Dynamic linking — **done**
|
||||
|
||||
`PT_INTERP` and `ET_DYN` in `kernel/fs/exec.c`. A program that names an
|
||||
interpreter gets it loaded at the bottom of the mmap area and started instead,
|
||||
with `AT_PHDR`, `AT_PHENT`, `AT_PHNUM`, `AT_BASE` and `AT_ENTRY` to find the
|
||||
program by; a PIE program is placed where a fixed one would be. The interpreter
|
||||
is musl's own: `ports/musl` now builds `libc.so` as well, shipped as
|
||||
`/lib/ld-musl-x86_64.so.1`, which is libc and loader in one file.
|
||||
|
||||
The kernel half that was missing besides `exec` was `mmap(MAP_FIXED)` of a
|
||||
file. It handed back zeroed pages whatever the descriptor said, and kept no
|
||||
record of them for `fork` — and a dynamic linker maps every library segment
|
||||
after the first exactly that way.
|
||||
|
||||
Every port still links `-static`, on purpose: this makes the other kind
|
||||
possible, it does not convert anything. `cmd/dynhello` is the test — a
|
||||
program against `libc.so` and a library of its own, checking TLS, the heap and
|
||||
a forked child that still has its libraries.
|
||||
|
||||
`mprotect` is accepted and not enforced, so RELRO is not read-only yet.
|
||||
|
||||
### 6. The event descriptors — **done**
|
||||
|
||||
`epoll`, `eventfd`, `timerfd` and `signalfd`, in `kernel/sys/sys_event.c`.
|
||||
libwayland's event loop is all four in one epoll set, which is why they arrive
|
||||
together; not in the original list because they were stubs that looked like
|
||||
syscalls, returning `ENOSYS` from `table.inc` entries that read as done.
|
||||
|
||||
They wait the way `poll()` does, a tick at a time, so the latency is 10 ms.
|
||||
`cmd/evtest` checks each one, then all four in one set, level- and
|
||||
edge-triggered.
|
||||
|
||||
Building it found a signal bug: a default-fatal signal killed on arrival even
|
||||
when it was blocked. A blocked signal now stays pending, which is the contract
|
||||
`signalfd` is built on.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
Each item gets a test before the thing that needs it is built, because a
|
||||
substrate that is subtly wrong fails inside the consumer and looks like the
|
||||
consumer's bug.
|
||||
|
||||
- **Item 1:** two processes, one passes an open descriptor, the other reads
|
||||
through it. Plus the leak case: a sender that exits with descriptors in flight.
|
||||
- **Item 2:** two processes map the same `memfd`, one writes, the other sees it.
|
||||
- **Item 3:** `bind()` a socket, then `stat()` and `ls -l` the path and see an
|
||||
`s`, then `unlink()` it.
|
||||
- **Items 1–3 together:** a third test that does all of it at once, because that
|
||||
combination is what Wayland actually performs and the three passing separately
|
||||
does not prove it.
|
||||
|
||||
These belong in `bootcheck` as a new switch, following the `-Sock` model.
|
||||
|
||||
---
|
||||
|
||||
## What this does not deliver
|
||||
|
||||
- **No page cache**, so writable `MAP_SHARED` of a real file stays refused.
|
||||
- **No `dlopen`** until item 5 is done, which means no Mesa and no third-party
|
||||
shared libraries.
|
||||
- **`SO_*` coverage is the two options named**, not the full set.
|
||||
- **No credential passing** (`SCM_CREDENTIALS`) — only descriptors. Nothing on
|
||||
this plan needs it, and a compositor that wants to know who connected will.
|
||||
- **No `flock()`.** libwayland locks `wayland-0.lock` beside the socket to stop
|
||||
two compositors claiming one display. The socket itself is now a real path,
|
||||
so the lock file can be created and opened; what is missing is the advisory
|
||||
lock, which is a separate mechanism and has no other caller yet.
|
||||
125
docs/plan/gpu.md
Normal file
125
docs/plan/gpu.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# The GPU, through Linux's own driver
|
||||
|
||||
The laptop this is for (ASUS TUF A15 FA507NU) has a Radeon 680M in the CPU and
|
||||
an RTX 4050 beside it. SchOS draws everything with the CPU on an EFI
|
||||
framebuffer. Rendering a browser page that way keeps every core busy, and the
|
||||
machine runs hot doing it.
|
||||
|
||||
Writing a Radeon driver is not on the table: amdgpu is around four million lines
|
||||
before its register headers. The plan is to **run Linux's amdgpu unmodified**,
|
||||
with its firmware, in a process under LDLS ([../architecture/ldls.md](../architecture/ldls.md)),
|
||||
and to give Mesa the `/dev/dri` it expects. Licensing is not an obstacle:
|
||||
SchOS is GPL-2.0-or-later, and the user has said that Linux code and firmware
|
||||
blobs may be shipped. That supersedes LDLS's rule 1.
|
||||
|
||||
## Why LDLS and not an in-kernel linuxkpi
|
||||
|
||||
FreeBSD's drm-kmod compiles Linux's DRM source into kernel modules against a
|
||||
shim (linuxkpi). SchOS has already chosen the other route, and for good reasons
|
||||
([devices.md](../architecture/devices.md)): GPU drivers live outside the kernel, a crashed driver
|
||||
falls back to the framebuffer instead of taking the machine down, and LDLS
|
||||
loads unmodified `.ko` files against the running kernel's BTF instead of
|
||||
needing a source port per Linux release. vgpud already drives a GPU from a
|
||||
process, and LDLS already runs 8139too and e1000 that way.
|
||||
|
||||
## What it costs: the numbers
|
||||
|
||||
Measured against Linux 6.18.54 (the host's kernel and modules), with
|
||||
`modprobe --show-depends`, `nm -u` and the export tables:
|
||||
|
||||
| Driver | Modules in the closure | Symbols needed from the core kernel | LDLS provides today |
|
||||
|---|---|---|---|
|
||||
| r8169 | 12 (libphy, mdio-bus, realtek PHY, ...) | 330 | 70 |
|
||||
| amdgpu | 15 (ttm, gpu-sched, drm_buddy, drm_exec, drm_display_helper, ...) | 1,166 | 76 |
|
||||
|
||||
Of amdgpu's 1,166 symbols, **314 are the DRM core** (`drm_*`). NixOS builds
|
||||
`drm.ko` and `drm_kms_helper.ko` into the kernel, so a module set that has them
|
||||
as modules is needed; see step 0. A further 45 are dma-fence, dma-buf and
|
||||
dma-resv. The rest is ordinary kernel: memory, locks, workqueues, timers, PCI,
|
||||
ACPI, firmware loading, i2c, backlight.
|
||||
|
||||
## What LDLS lacks, and must grow
|
||||
|
||||
LDLS today assumes one thread, no-op locks and a polled interrupt handler
|
||||
(ldls.md, "What it costs"). amdgpu cannot work under that:
|
||||
|
||||
1. **Threads.** `kthread_run`, workqueues with real worker threads,
|
||||
`gpu-sched`'s scheduler threads. Map them to pthreads in the driver process.
|
||||
2. **Locks that lock.** mutex, spinlock, rwsem, `ww_mutex` (TTM and dma-resv
|
||||
depend on wound/wait semantics), completions and wait queues, on futexes.
|
||||
3. **Interrupts as interrupts.** A thread blocked in `read()` on `/dev/pci`
|
||||
calls the handler and any threaded handler, instead of the poll loop.
|
||||
4. **Timers.** `timer_list`, `hrtimer`, `delayed_work` and `jiffies`, from a
|
||||
timer thread.
|
||||
5. **Memory with physical addresses.** TTM allocates system pages for GTT and
|
||||
maps them for the GPU. Without an IOMMU, DMA address equals physical
|
||||
address, so the `PCIIOC_DMA` arena has to grow to hundreds of megabytes,
|
||||
with `struct page` (vmemmap) coverage for all of it.
|
||||
6. **Firmware.** `request_firmware` reads `/lib/firmware/amdgpu/*.bin`, from
|
||||
Alpine's `linux-firmware-amdgpu` package.
|
||||
7. **ACPI.** On an APU the VBIOS comes from the VFCT table, and amdgpu calls
|
||||
ATIF/ATCS. A small `/dev/acpi` (read a table, evaluate a method through
|
||||
uACPI) covers both.
|
||||
|
||||
Each of these is general LDLS work, not amdgpu work. Every later driver needs
|
||||
the same pieces: e1000e, iwlwifi, nouveau.
|
||||
|
||||
## The new kernel piece: `/dev/dri` served by a process
|
||||
|
||||
Mesa and Xorg open `/dev/dri/card0` and `/dev/dri/renderD128`, then use
|
||||
`ioctl`, `mmap` and `poll`, and pass buffers to each other as dma-buf file
|
||||
descriptors. The kernel has to forward each of those to the driver process:
|
||||
|
||||
- **open, ioctl, poll, close** go to the server process with the client's
|
||||
identity attached, the same pattern `/dev/netdev` uses for NICs, extended to
|
||||
ioctls with their argument buffers.
|
||||
- **mmap** is the part the kernel must do itself. The server replies with
|
||||
physical ranges (VRAM through the BAR, or GTT pages from the arena), and the
|
||||
kernel maps those into the client's address space.
|
||||
- **dma-buf** becomes a kernel file object naming a buffer the server owns.
|
||||
`SCM_RIGHTS` passes it between processes (already built; see
|
||||
[networking.md](../architecture/networking.md#descriptor-passing)), and X's DRI3 needs exactly that.
|
||||
- **sysfs**: `/sys/class/drm/card0`, `/sys/dev/char/226:*`, and the
|
||||
vendor/device files that libdrm and Mesa read to pick a driver.
|
||||
|
||||
## Order
|
||||
|
||||
Each step ends with something you can run.
|
||||
|
||||
0. **One module set.** Build Linux 6.18 from source, once, with `DRM=m`,
|
||||
`DRM_AMDGPU=m` and BTF on. That gives `drm.ko`, `drm_kms_helper.ko` and the
|
||||
rest, plus a matching `vmlinux` BTF. `make ldls-modules` does the build, and
|
||||
packaging it as an apk makes it installable.
|
||||
1. **LDLS concurrency and memory** (lacks 1–5 above), proven on **r8169**.
|
||||
The real Linux r8169 brings PHY tuning, the firmware patch, ASPM handling
|
||||
and every RTL8168 variant. It is the laptop NIC, so this step also fixes the
|
||||
network properly. Done when the laptop's ethernet runs at gigabit speed
|
||||
through `ldls0`.
|
||||
2. **amdgpu probes.** The driver process loads firmware, brings up the IP
|
||||
blocks and takes the display from the EFI framebuffer. The kernel console
|
||||
must hand over the screen and get it back if the process dies. Done when
|
||||
`dmesg` shows amdgpu's "SMU is initialized" and the panel stays lit.
|
||||
3. **KMS.** `/dev/dri/card0` with ioctl forwarding and the mmap path. Done
|
||||
when `modetest` (Alpine `libdrm-tests`) sets a mode and draws a frame.
|
||||
4. **Rendering.** `renderD128`, GEM mmap, dma-buf, syncobj. Done when
|
||||
`eglinfo` names radeonsi and `glmark2-es2-drm` and `vkcube` (RADV) run.
|
||||
5. **The desktop.** Xorg's modesetting driver with glamor, then Epiphany with
|
||||
GL compositing. Done when the fans stay quiet while a page scrolls.
|
||||
|
||||
## The RTX 4050
|
||||
|
||||
Later, and in two parts:
|
||||
|
||||
- **Powered off, now.** Without a driver the dGPU sits in D0 and draws power.
|
||||
Putting it in D3cold through its ACPI power resources (`_PR3` / `_OFF`) is
|
||||
a few dozen lines in the kernel and helps the heat and the battery today.
|
||||
- **Driven, after amdgpu.** nouveau with GSP firmware (Ada needs it), through
|
||||
the same LDLS and `/dev/dri` machinery, for PRIME render offload.
|
||||
|
||||
## Not GPU, but also why the laptop runs hot
|
||||
|
||||
Until now, idle CPUs halted at C1 and never reached the deep states listed in
|
||||
ACPI `_CST`. That kept every core powered at an idle shell prompt. It is fixed
|
||||
alongside this plan: the scheduler's idle path now enters `_CST`'s C2 (CC6 on
|
||||
Zen). P-state control (AMD CPPC) is still missing, so the cores run at the
|
||||
firmware's default clock under load.
|
||||
|
|
@ -1,558 +0,0 @@
|
|||
# The kernel becomes hybrid
|
||||
|
||||
"Hybrid" is used to mean everything from "a monolith with a nice device model" to
|
||||
"a microkernel that gave up". **Drawing the line precisely is most of the work**,
|
||||
so that is what this document is.
|
||||
|
||||
The device model it builds on is
|
||||
[../architecture/devices.md](../architecture/devices.md). It is last on the
|
||||
plan, and the reason is in [README.md](README.md#the-order-and-why-it-is-not-the-order-they-are-listed-in).
|
||||
|
||||
---
|
||||
|
||||
## The line
|
||||
|
||||
### Stays in the kernel, permanently
|
||||
|
||||
- scheduling, address spaces, the trap and syscall path
|
||||
- IPC and shared memory — the mechanism everything else is built out of
|
||||
- the VFS and the **root** filesystem
|
||||
- IRQ demultiplexing
|
||||
- the console and PSOGM
|
||||
|
||||
**That last one is not a size compromise, it is an invariant.** The crash screen
|
||||
must work when everything else in the system is dead, which means it cannot
|
||||
depend on anything that could *be* dead. A crash screen served by a userspace
|
||||
process is a crash screen that is absent in exactly the case it exists for.
|
||||
|
||||
### May leave, and should
|
||||
|
||||
- GPU drivers
|
||||
- USB, and every USB class driver under it
|
||||
- non-root filesystems
|
||||
- NICs after the first
|
||||
- input transformation — layouts, gestures, remapping
|
||||
|
||||
### The rule that separates the columns
|
||||
|
||||
> **Anything needed to reach a shell stays in. Anything discovered at runtime,
|
||||
> replaceable, or plausibly written by someone else goes out.**
|
||||
|
||||
That is NT's compromise and macOS's, arrived at from the same pressure, and it is
|
||||
the one that survives contact with a real machine.
|
||||
|
||||
---
|
||||
|
||||
## Built so far
|
||||
|
||||
**Mechanisms 2 and 4 exist.** `/dev/pci` (include/dev/pciuser.h, kernel/dev/pciuser.c)
|
||||
enumerates the bus, hands a device to a process exclusively, maps a BAR into it
|
||||
and allocates memory a device can be pointed at. `pcitest` proves it end to end:
|
||||
|
||||
```
|
||||
# bdf vendor:device class driver
|
||||
6 00:04.0 8086:100e network e1000
|
||||
7 00:07.0 8086:10d3 network -
|
||||
|
||||
device 7: 8086:10d3 (network)
|
||||
claimed
|
||||
BAR0 at 0x0000000081060000, 128 KB
|
||||
regs[0..3] = 0x00140261 0x00000000 0x00080283 0x00000000
|
||||
MMIO reaches the device
|
||||
DMA 8 KB at virtual 0x40001000, physical 0x3f8eb000
|
||||
```
|
||||
|
||||
Those register values are the e1000e's CTRL and STATUS, with full-duplex and
|
||||
link-up set -- which is how you tell a working mapping from one that reads
|
||||
0xFFFFFFFF off an unmapped address. Claiming a device the kernel drives is
|
||||
refused, and everything a process held is released when it exits, because a
|
||||
driver that crashed must not keep its hardware away from its replacement.
|
||||
|
||||
**Mechanism 3 exists too.** A process asks for a device's interrupts and then
|
||||
reads them, uio-style:
|
||||
|
||||
```
|
||||
interrupts routed here (line 10)
|
||||
interrupt delivered, count = 1
|
||||
ICR = 0x80000004
|
||||
```
|
||||
|
||||
`0x80000004` is the e1000e's cause register with the bit the test asserted and
|
||||
INT_ASSERTED set, read back by userspace as its acknowledgement.
|
||||
|
||||
The line is **masked by the kernel on arrival and unmasked by userspace's
|
||||
write()**, and that is the entire design rather than a detail. A PCI interrupt
|
||||
line is level-triggered: it stays asserted until the cause is cleared *at the
|
||||
device*, which is a different register on every chip and therefore something
|
||||
only the driver can do. Signalling EOI on a line that is still asserted
|
||||
re-enters the handler immediately and for ever -- 72,000 times a second, when
|
||||
this kernel last did it by accident (history/kernel.md §13).
|
||||
|
||||
So a driver that dies mid-interrupt leaves the line **masked, not unmasked**.
|
||||
The instinct is the other way round -- do not leave the device dead -- and it
|
||||
is wrong: unmasking an asserted line with no handler attached is a crashed
|
||||
driver taking the machine down with it. Verified by killing one on purpose:
|
||||
|
||||
```
|
||||
interrupt asserted; exiting WITHOUT acking (simulated crash)
|
||||
timer: 3431 ticks taken, 3436 expected
|
||||
```
|
||||
|
||||
Five ticks lost in seven seconds, which is the ordinary background. The next
|
||||
driver to claim the device re-arms the line, so masked is safe without being
|
||||
permanent -- also verified.
|
||||
|
||||
## The first eviction happened
|
||||
|
||||
`cmd/vgpud` is the virtio-gpu driver, outside the kernel. It finds the device
|
||||
by vendor and device id, asks for it, and the kernel driver hands it over:
|
||||
|
||||
```
|
||||
vgpud: virtio-gpu at 00:03.0, currently held by virtio-gpu
|
||||
virtio-gpu: released to userspace; screen is the VBE framebuffer
|
||||
vgpud: claimed
|
||||
vgpud: display is 1280x800
|
||||
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
|
||||
vgpud: drew a test pattern from userspace
|
||||
```
|
||||
|
||||
Full virtio 1.0 bring-up from a process: reset, feature negotiation, virtqueue
|
||||
in DMA memory, GET_DISPLAY_INFO, RESOURCE_CREATE_2D, ATTACH_BACKING,
|
||||
SET_SCANOUT, and TRANSFER/FLUSH to present. The pattern it draws is on the
|
||||
virtio-gpu head while the console carries on in VBE on the other one -- which
|
||||
is the fallback rule working rather than being described.
|
||||
|
||||
**No syscall knows this is a GPU.** The kernel handed over a PCI device; the
|
||||
process decided what it was. That is the property that makes the interface
|
||||
general, and it is why the evictions come before anything foreign.
|
||||
|
||||
### What the first real driver asked for that a test program had not
|
||||
|
||||
- **PCI configuration space** (`PCIIOC_CFG`). A virtio device says where its
|
||||
registers are in *vendor capabilities*, which live in config space -- so a
|
||||
driver that cannot read it cannot find its own registers. Anything modern is
|
||||
discovered the same way.
|
||||
- **A way for the incumbent to let go.** `struct pci_driver` gained a
|
||||
`release` hook. A driver without one keeps its device, which is always safe
|
||||
and, for anything on the path to a shell, correct.
|
||||
- **The outgoing driver freeing its DMA.** Releasing the device while keeping
|
||||
its 4 MB framebuffer is half a handover: the incoming driver asks for one and
|
||||
is told there is no room. Found by doing it.
|
||||
- **A bigger arena.** 4 MB was exactly one framebuffer at this resolution, and
|
||||
for the moment either side of a handover there are two. 16 MB now.
|
||||
|
||||
### And one bug it reproduced immediately
|
||||
|
||||
The first working run raised this:
|
||||
|
||||
```
|
||||
irq 11: 77780 interrupts/second -- a device on this line is not acknowledging it.
|
||||
irq 11: handler 'e1000' is attached
|
||||
```
|
||||
|
||||
`vgpud` polls the used ring, so it registered no handler -- and a virtio device
|
||||
interrupts on every completion. A level-triggered line nobody acknowledges
|
||||
re-fires for ever, drowning the e1000 that shares it. The fix is one line,
|
||||
`VRING_AVAIL_F_NO_INTERRUPT` before the queue is enabled, and the value of it
|
||||
is that the storm detector built for history/kernel.md section 13 named the
|
||||
line, the rate and the neighbour without anyone having to look.
|
||||
|
||||
**All four mechanisms now exist**, and with H3 done and H4 withdrawn, nothing
|
||||
remains on this plan. What the mechanisms were built for next is the foreign
|
||||
driver host -- [ldls.md](ldls.md).
|
||||
|
||||
### One thing the interface is deliberately not
|
||||
|
||||
It is about **PCI devices**, not about any driver model, and its first users
|
||||
will be SchOS's own drivers. That ordering is the design: an interface whose
|
||||
first consumers are native drivers is a general mechanism that something
|
||||
foreign can use later, rather than a hole shaped like something foreign. The
|
||||
difference matters technically -- a Linux-shaped interface would be wrong for
|
||||
the evictions this plan actually calls for -- and it happens to be the only
|
||||
version that survives a licensing conversation.
|
||||
|
||||
---
|
||||
|
||||
## The driver came back
|
||||
|
||||
H2 is the milestone this plan calls the one that cannot be skipped, and the
|
||||
reason is that H1 proves only half of the idea. A driver that runs in a process
|
||||
is interesting; a driver that runs in a process, *dies*, and is replaced by
|
||||
another process that picks the device up is the thing the whole hybrid argument
|
||||
rests on.
|
||||
|
||||
`etc/schinit/vgpud.job` makes it a supervised job -- `KeepAlive = yes` -- so
|
||||
killing it is the whole test:
|
||||
|
||||
```
|
||||
# cat /proc/schinit
|
||||
1 4 11 os.schos.vgpud
|
||||
# kill 4
|
||||
schinitd: restarting os.schos.vgpud
|
||||
vgpud: virtio-gpu at 00:03.0
|
||||
vgpud: claimed
|
||||
vgpud: display is 1280x800
|
||||
vgpud: scanout is ours, framebuffer at physical 0x3eceb000
|
||||
vgpud: pid 8 drew a test pattern from userspace
|
||||
```
|
||||
|
||||
The test pattern ends in a twelve-block tally of the drawing process's pid,
|
||||
because a second run that redraws an identical picture is indistinguishable
|
||||
from a first run that never stopped. Read off the virtio-gpu scanout across
|
||||
four generations of the driver it says 4, 8, 10, 12. The same physical address
|
||||
comes back every time, which is the DMA arena reclaiming cleanly rather than
|
||||
drifting.
|
||||
|
||||
### What it took that H1 had not needed
|
||||
|
||||
- **A halt the kernel can apply without knowing what the device is.** A driver
|
||||
that dies leaves its device bus-mastering into memory the kernel is about to
|
||||
hand to somebody else. The kernel cannot quiesce a device it does not
|
||||
understand, but it can clear `PCI_CMD_BUS_MASTER`, and that is enough to stop
|
||||
the writes. `pciuser_stop()` does it before the DMA is freed, not after, and
|
||||
claim/release became a symmetric pair: claiming sets the bit, releasing
|
||||
clears it. `pcitest` grew a `bm` column so the state is visible rather than
|
||||
assumed.
|
||||
- **A refusal, when there is nothing to fall back to.** `vgpu_release()` now
|
||||
asks `vga_fb_restore()` whether there is a framebuffer to put the console on,
|
||||
and declines to hand the device over if there is not. The rule that the
|
||||
kernel keeps a minimal driver for everything on the path to a shell is only
|
||||
worth writing down if something enforces it.
|
||||
- **The DMA arena not being handed to two allocators at once.** This is the bug
|
||||
that stopped H2 for a day, and it is worth writing out: `freevm()` frees
|
||||
every mapped page below `PHYSTOP`, and the DMA arena is the top 16 MB of
|
||||
exactly that range. So a DMA block mapped into a driver and left for
|
||||
`freevm()` went onto the page allocator's free list when the driver died,
|
||||
while `pciuser_release_all()` handed the same pages back to the arena. Both
|
||||
allocators then believed they owned it. The restarted `vgpud` asked for its
|
||||
4 MB framebuffer, got pages `kalloc()` had already given out -- including its
|
||||
own page tables -- and `dma_alloc()`'s zeroing wiped them, so the driver died
|
||||
inside the ioctl, on the return from a syscall whose stack had stopped
|
||||
existing. `do_dma()` now records a VMA with `no_free` set, the same way
|
||||
shared memory does, so `proc_release_shared()` unmaps the range before the
|
||||
page tables are torn down.
|
||||
|
||||
### The clause that is not true yet
|
||||
|
||||
The milestone says *"the compositor reattaches"*. It does not, because there is
|
||||
no compositor running: Schcom is v0 and is not started at boot. What has been
|
||||
shown is the two parts that do not depend on it -- the screen falls back to
|
||||
VBE, and the driver restarts and takes the device back. During the gap the VBE
|
||||
head carries a live console and the virtio-gpu head still shows the dead
|
||||
driver's last frame, which is exactly what a device nobody is driving does.
|
||||
|
||||
(That last sentence is still true of a *crashed driver*, and no longer true of
|
||||
a crashed *kernel* -- see "The clause nobody wrote down" below.)
|
||||
|
||||
Reattachment is a Schcom milestone, not a hybrid one, and it is recorded that
|
||||
way rather than counted here.
|
||||
|
||||
---
|
||||
|
||||
## A modeset, from a process
|
||||
|
||||
H3 is one sentence in the milestone table -- *"a modeset API, which is the
|
||||
thing an out-of-kernel GPU driver makes affordable and an in-kernel one never
|
||||
got"* -- and the claim in it is worth testing rather than repeating.
|
||||
|
||||
```
|
||||
# schmode
|
||||
1280x800 current
|
||||
1280x800 maximum, framebuffer 4000 KB
|
||||
# schmode modes
|
||||
1280x800 *
|
||||
1280x720
|
||||
1024x768
|
||||
800x600
|
||||
640x480
|
||||
# schmode 1024 768
|
||||
1024x768 ok
|
||||
```
|
||||
|
||||
The scanout really resizes: captured from the virtio-gpu head, the frames come
|
||||
back 1280x800, then 1024x768, then 640x480, then 1280x800 again, with the
|
||||
checkerboard band showing no shear at any of them.
|
||||
|
||||
**Nothing in the kernel changed for this.** No syscall on the path has heard of
|
||||
a scanout; `schmode` writes a line to a socket and `vgpud` sends five commands
|
||||
to the device. That is the whole of the milestone, and the reason it is
|
||||
interesting is the comparison: the same five commands in the kernel would have
|
||||
been the same five commands *and a reason to be nervous*, because getting the
|
||||
sequence wrong there takes the machine instead of one process. **That is why
|
||||
the in-kernel driver never grew one** -- not difficulty, exposure.
|
||||
|
||||
### What it costs, and what it refuses
|
||||
|
||||
The framebuffer is allocated **once, at the largest mode**, and every smaller
|
||||
mode reuses it. `PCIIOC_DMA` has no free -- a driver's blocks come back when it
|
||||
exits and not before -- so a modeset that allocated would spend sixteen slots
|
||||
and then the arena, and the failure would look like a GPU bug rather than like
|
||||
counting. A mode larger than the initial allocation is refused, and says why:
|
||||
|
||||
```
|
||||
# schmode 1920 1080
|
||||
refused: larger than the framebuffer, which is fixed at startup
|
||||
```
|
||||
|
||||
That bound is 4 MB, `PCIIOC_DMA`'s cap, which at 32 bits per pixel is about
|
||||
1280x800. Raising it is a one-line change to a constant and a question about
|
||||
how much of a 16 MB arena one driver should be able to take, which is a better
|
||||
question than it looks and is not answered here.
|
||||
|
||||
### The socket, and the argument it settles
|
||||
|
||||
`vgpud` used to end in `for(;;) sleep(100);` under a comment arguing that
|
||||
building a control socket before there is a compositor to judge it is exactly
|
||||
the mistake this document spends a section warning about.
|
||||
|
||||
That argument was right about a *compositor protocol* and wrong as a reason to
|
||||
have no socket at all. A modeset API has a consumer that exists -- somebody
|
||||
changing the mode and looking at the screen -- and `schmode` is small enough
|
||||
that it can judge an interface this small. So the socket is there, it speaks
|
||||
lines of text for the same reasons
|
||||
[../architecture/debugging.md](../architecture/debugging.md) gives, and what is
|
||||
deliberately **not** on it is everything a compositor would need: no buffer
|
||||
submission, no damage, no vblank. Those still wait, for the original reason.
|
||||
|
||||
---
|
||||
|
||||
## The mechanisms
|
||||
|
||||
Four, and only the fourth is large.
|
||||
|
||||
### 1. A control channel — **built**
|
||||
|
||||
`AF_UNIX` plus `SCM_RIGHTS` — item 1 of [foundations.md](foundations.md), which
|
||||
the Wayland work also needed first. A descriptor is how a driver process is
|
||||
handed the device it is allowed to touch.
|
||||
|
||||
Built as `sendmsg`/`recvmsg` in Linux's shape, because the programs that will
|
||||
use it are not ours: libwayland and toybox both call `recvmsg` with Linux's
|
||||
structures and would not notice a friendlier door. Descriptors are pinned to the
|
||||
stream position of the first byte of their message, and the kernel holds a
|
||||
reference to each until it is received — or closes it when the socket is dropped
|
||||
with the message still queued, which is the case that leaks if nobody writes it
|
||||
down. See
|
||||
[../architecture/networking.md](../architecture/networking.md#descriptor-passing).
|
||||
|
||||
**Do not build Mach ports.** The port model is worth its cost when there is a
|
||||
naming and capability story to hang on it; there is not one yet, and inventing it
|
||||
before the first userspace driver exists is designing against an imaginary user.
|
||||
|
||||
### 2. MMIO to userspace
|
||||
|
||||
Map a PCI BAR into a process.
|
||||
|
||||
`sys_mmap` already maps the framebuffer's real physical range at its real
|
||||
address — the path exists. This is that path generalised, behind a check that the
|
||||
caller was granted that device.
|
||||
|
||||
### 3. Interrupts to userspace
|
||||
|
||||
**The handler stays in the kernel and does one thing: write to an `eventfd`.**
|
||||
The driver process reads.
|
||||
|
||||
`eventfd` exists already, and that is why it is the right primitive. A signal
|
||||
would be delivered to a process that cannot safely be interrupted mid-transaction.
|
||||
|
||||
### 4. DMA to userspace
|
||||
|
||||
Physically contiguous memory whose physical address the process is told.
|
||||
|
||||
`kernel/core/dma.c` already owns an arena reserved for exactly this, before
|
||||
`kalloc` gets anything. It needs a door, a per-device budget, and a way to hand
|
||||
the region back.
|
||||
|
||||
---
|
||||
|
||||
## The clause nobody wrote down: a panic has to reach the screen
|
||||
|
||||
Handing a device away costs something that was not on the list. The kernel
|
||||
panics, paints the report into the console framebuffer, and halts — and the
|
||||
display it gave to a process keeps showing whatever that process last put
|
||||
there. On a machine with two heads, one says the kernel is dead and the other
|
||||
shows a shell prompt.
|
||||
|
||||
It is not a race that can be won by being careful. There is no scheduling after
|
||||
a panic, so the process holding the device is never going to run again, and the
|
||||
kernel let go of the device deliberately.
|
||||
|
||||
**So the userspace driver says in advance where it is scanning out**, with
|
||||
`PCIIOC_PANICFB` (see `include/dev/pciuser.h`), and the kernel does the rest:
|
||||
|
||||
1. paints the report into that memory,
|
||||
2. asks the *kernel* driver the device was taken from to put it on screen —
|
||||
`panic_present` in `struct pci_driver`,
|
||||
3. which resets the device and rebuilds one queue from **static** memory: no
|
||||
allocation, no locks, no sleeping, because a corrupted heap is one of the
|
||||
ways a machine gets here in the first place.
|
||||
|
||||
The registration is checked like the promise it is: the range must lie inside a
|
||||
block that process got from `PCIIOC_DMA` on that device. Without that it would
|
||||
be a way to nominate any physical address in the machine to be overwritten at
|
||||
panic, which is a worse bug than the one it fixes.
|
||||
|
||||
What this buys is a property worth stating plainly: **a device can be evicted
|
||||
to userspace without the kernel losing its ability to tell you the machine
|
||||
died.** That is the difference between a driver boundary and a blind spot, and
|
||||
it is one more thing the in-kernel arrangement got for free and the hybrid one
|
||||
has to earn.
|
||||
|
||||
It costs 20 KB of static memory in the virtio-gpu driver, paid whether or not
|
||||
the machine ever panics. Allocating it on demand would be cheaper and would not
|
||||
work.
|
||||
|
||||
---
|
||||
|
||||
## The honest limitation
|
||||
|
||||
> **There is no IOMMU, so a userspace driver is isolated against its own bugs and
|
||||
> not against malice.**
|
||||
|
||||
A process that can program a device's DMA engine can write to any physical
|
||||
address, whatever its page tables say.
|
||||
|
||||
That is not a reason to stop — isolation against bugs is most of the value, and a
|
||||
crashed driver that restarts instead of panicking the machine is the entire
|
||||
point. But it must be written down, because *"userspace driver"* reads as
|
||||
*"sandboxed"* and here it does not mean that yet. VT-d/AMD-Vi is its own project
|
||||
and comes after.
|
||||
|
||||
---
|
||||
|
||||
## Order, and one thing not to do
|
||||
|
||||
### Do not build loadable kernel modules first
|
||||
|
||||
It is the obvious first step and it is a generation behind. It means designing a
|
||||
**stable in-kernel driver ABI**, which is the most expensive promise in this
|
||||
entire plan, for a mechanism Apple is actively retiring in favour of the one
|
||||
above.
|
||||
|
||||
`../architecture/devices.md` already separates the two ideas. They are separable,
|
||||
and the second one is the one to build.
|
||||
|
||||
### First eviction: the GPU driver
|
||||
|
||||
Four reasons it is the right one and not just the interesting one:
|
||||
|
||||
- **The boundary already exists and is already correct** — `struct display_ops`.
|
||||
- **It has exactly one consumer**, the compositor, so the interface has a real
|
||||
user to be judged by.
|
||||
- **There is a working fallback underneath.** The VBE framebuffer stays
|
||||
in-kernel, so a GPU driver that fails to start degrades to a working screen
|
||||
rather than to no screen.
|
||||
- It directly serves the work that will actually be happening.
|
||||
|
||||
### Second: USB — and why it turned out not to be
|
||||
|
||||
`xhci.c` is 1364 lines, self-contained, already spoken to through a bus
|
||||
abstraction, and it is where third-party drivers will show up first. This is what
|
||||
DriverKit was built for and the resemblance is not accidental.
|
||||
|
||||
#### It is not the same shape as the GPU, and that was missed
|
||||
|
||||
Written before the GPU eviction had been done, H4 says *"USB, same shape"*. It
|
||||
is not, and the difference is one this document's own rule already decides.
|
||||
|
||||
The GPU could leave because **there was a working fallback underneath it**: the
|
||||
VBE framebuffer stays in the kernel, so a GPU driver that fails to start
|
||||
degrades to a working screen. That is one of the four reasons the GPU was
|
||||
chosen to go first, listed a few lines above.
|
||||
|
||||
USB has no fallback, and it carries two things that are on the path to a shell:
|
||||
|
||||
- **The root disk.** `USBDISK_DEV` dispatches through `bio.c` to
|
||||
`usb_storage_rw`, and `usbstick.img` is a real boot medium built by `make`.
|
||||
- **The keyboard.** `usb_hid` feeds `input_report(INPUT_KBD, ...)`, which is how
|
||||
anything without PS/2 is typed on — including the Chromebox this is meant to
|
||||
boot.
|
||||
|
||||
So evicting `xhci.c` wholesale breaks the rule three sections down:
|
||||
|
||||
> **The kernel keeps a minimal driver for everything on the path to a shell, and
|
||||
> userspace owns the good one.**
|
||||
>
|
||||
> Break this and you get the circular boot failure where the compositor cannot
|
||||
> start because the display driver is a process that needs a compositor.
|
||||
|
||||
The USB version of that sentence is *the root filesystem cannot mount because
|
||||
the storage driver is a process that lives on the root filesystem*, and it is
|
||||
the same failure wearing different clothes.
|
||||
|
||||
There is a second difference, subtler and probably the harder one. `/dev/pci`
|
||||
hands over a **PCI function** — one device, one owner, nothing shared. A USB
|
||||
device is not that: it sits behind a controller that other devices also sit
|
||||
behind, so the unit of ownership and the unit of hardware are no longer the
|
||||
same thing. Handing a process "the xHCI controller" gives it the keyboard too;
|
||||
handing it "one USB device" is an interface `/dev/pci` does not have and that
|
||||
this plan has not designed.
|
||||
|
||||
#### Decided: USB stays in the kernel
|
||||
|
||||
**H4 is withdrawn.** Not deferred, not blocked on the interface above —
|
||||
withdrawn, because the premise was wrong.
|
||||
|
||||
> **USB is not a peripheral.** It is the bus the peripherals arrive on.
|
||||
|
||||
Nearly every machine has a USB port in some form, down to a bad micro-B on a
|
||||
board that has nothing else, and the ones that do not have one are not made
|
||||
worse by the stack being present. That makes USB structurally the same kind of
|
||||
thing as PCI, not the same kind of thing as a GPU — and this document has never
|
||||
proposed evicting PCI.
|
||||
|
||||
Reading the eviction rule again with that in mind, it already said so:
|
||||
|
||||
> **Anything needed to reach a shell stays in. Anything discovered at runtime,
|
||||
> replaceable, or plausibly written by someone else goes out.**
|
||||
|
||||
USB is how things are *discovered at runtime*, which is not the same as being
|
||||
one of them. The keyboard and the root disk that arrive over it are on the path
|
||||
to a shell, and the stack that finds them is part of that path.
|
||||
|
||||
The two obstacles in the section above are real and they are why the premise
|
||||
got questioned, but they are not the reason for this decision. Even with a
|
||||
minimal in-kernel fallback written and a per-device ownership interface
|
||||
designed, the answer would be the same: the good USB driver and the minimal one
|
||||
would be the same driver, and an eviction that leaves an identical copy behind
|
||||
has moved nothing.
|
||||
|
||||
What the eviction was *for* — third-party drivers, written by someone else,
|
||||
living outside the kernel — is a real goal and it survives this. It is simply
|
||||
not a USB goal; it belongs to the foreign driver host, which is
|
||||
[ldls.md](ldls.md).
|
||||
|
||||
### The rule that keeps the boot path honest
|
||||
|
||||
> **The kernel keeps a minimal driver for everything on the path to a shell, and
|
||||
> userspace owns the good one.**
|
||||
|
||||
Break this and you get the circular boot failure where the compositor cannot
|
||||
start because the display driver is a process that needs a compositor.
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
| **H0** | Mechanisms 2, 3 and 4, each with a test that exercises it from a userspace program against a device nothing else is using. | done |
|
||||
| **H1** | A userspace virtio-gpu driver, publishing `display_ops` across the channel. Schcom talks to it and does not know the difference. | done, except that Schcom does not talk to it -- `cmd/vgpud` holds the device and draws its own frame |
|
||||
| **H2** | Kill the driver process on purpose. The screen falls back to VBE, the driver restarts, the compositor reattaches. **This is the milestone that proves the whole idea**, and it is the one that cannot be skipped. | done, less the compositor -- see "The driver came back" |
|
||||
| **H3** | A modeset API, which is the thing an out-of-kernel GPU driver makes affordable and an in-kernel one never got. | done -- see "A modeset, from a process" |
|
||||
| ~~**H4**~~ | ~~USB, same shape.~~ **Withdrawn** — USB is the bus peripherals arrive on, not a peripheral. See "Decided: USB stays in the kernel". | |
|
||||
|
||||
---
|
||||
|
||||
## What it will still not do
|
||||
|
||||
- **No isolation against a hostile driver**, per the IOMMU note above.
|
||||
- **No stable driver ABI**, deliberately. Out-of-tree drivers remain a
|
||||
recompile-with-the-tree affair until there is a reason to promise otherwise.
|
||||
- **The root filesystem stays in the kernel**, so SchFS cannot be restarted and a
|
||||
bug in it is still fatal. Moving it out needs an initramfs story that does not
|
||||
exist.
|
||||
- **The first NIC stays in the kernel**, because the network thread and lwIP are
|
||||
in the kernel and moving them is a separate project with no current benefit.
|
||||
- **No driver sandboxing beyond address-space separation** — no seccomp-shaped
|
||||
syscall restriction, no resource limits per driver.
|
||||
|
|
@ -1,183 +1,195 @@
|
|||
# Roadmap — everything else still open
|
||||
|
||||
The three current decisions have their own documents; see
|
||||
[README.md](README.md). This file holds what is left: work that is scoped,
|
||||
agreed and unstarted, with nothing in it that is finished.
|
||||
[1.0.md](1.0.md) draws the finish line and holds the register of what is half-built.
|
||||
This file holds what is left beyond it: work that is scoped, agreed and unstarted, with
|
||||
nothing in it that is finished.
|
||||
|
||||
**Finished work is not here.** It moved to
|
||||
[../architecture/](../architecture/) and was rewritten as a description of what
|
||||
it produced — see [the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||||
What used to be phases 1 through 4 of this file, several hundred lines of ticked
|
||||
boxes, is now `usb`, `networking.md`, `graphics.md` and `devices.md` over there.
|
||||
**Finished work is not here.** It moved to [../architecture/](../architecture/) and was
|
||||
rewritten as a description of what it produced — see
|
||||
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
|
||||
|
||||
---
|
||||
|
||||
## 1. Storage and the boot path
|
||||
|
||||
**Done: a USB disk, a SATA disk and their partitions can be booted from.** The
|
||||
root is chosen at boot from what is present (`rootdev_mount()` in
|
||||
`kernel/proc/proc.c`), and `make disk` builds a GPT image with an ESP and a SchFS
|
||||
partition that a PC boots from a stick or a disk.
|
||||
A USB disk, a SATA disk, an NVMe SSD and their partitions boot, and the root is chosen at boot from
|
||||
what is present ([boot.md](../architecture/boot.md)).
|
||||
|
||||
NVMe (`kernel/drivers/storage/nvme/`) registers with the same block registry,
|
||||
so an NVMe SSD boots the same way.
|
||||
|
||||
**Next:** an installer that writes that layout to a disk, and interrupt-driven,
|
||||
queued I/O for AHCI and NVMe.
|
||||
- **An installer** that writes the layout `make disk` builds (a GPT disk with an ESP and a SchFS
|
||||
partition) to a machine's disk, so a root survives a reboot. See §5.
|
||||
- **Interrupt-driven, queued I/O for AHCI and NVMe.** Both poll for completion, one command at a time
|
||||
per disk: enough to boot and run, not to be fast.
|
||||
- **An EHCI driver.** USB 2.0 ports wired only to EHCI do nothing; xHCI ports work.
|
||||
|
||||
## 2. Graphics
|
||||
|
||||
- **A `/dev/dri`-style device with mode setting**, so resolution stops being
|
||||
whatever the loader asked for. This was to be done *with*
|
||||
[hybrid.md](hybrid.md)'s GPU eviction, and that eviction has now happened —
|
||||
`cmd/vgpud` drives virtio-gpu from userspace. The modeset API is the next thing
|
||||
it makes affordable, and is milestone H3 over there rather than here.
|
||||
- **Double buffering and vblank.** `display_ops` specifies both and no driver
|
||||
implements either. Schcom needs both to not tear.
|
||||
- **Port the console onto the text layer and delete the cell grid.** The layer
|
||||
exists, the rasteriser exists, and the console still indexes `font_data[256][16]`.
|
||||
- **Load a font from disk** and point the console at it.
|
||||
- **CFF/Type 2 charstrings**, so OTF fonts work. A separate `font_ops` provider
|
||||
and a bytecode interpreter, *not* an extension of the `glyf` path.
|
||||
- **Text layout** — advance widths, kerning — as a library in `lib/`, so the
|
||||
terminal and the file manager are not each solving it.
|
||||
|
||||
The console keeps its fixed grid until the first item above; proportional text
|
||||
belongs to the GUI.
|
||||
- **A `/dev/dri` device with mode setting**, so a real panel's resolution stops being whatever the
|
||||
loader's menu chose and Mesa has something to open. This is the GPU plan, [gpu.md](gpu.md).
|
||||
- **Double buffering and vblank**, which `display_ops` specifies and no driver implements; the same plan.
|
||||
- **CFF/Type 2 charstrings**, so OTF fonts work. A separate `font_ops` provider and a bytecode
|
||||
interpreter, *not* an extension of the `glyf` path.
|
||||
- **Text layout** — advance widths, kerning — as a library, so the terminal and the file manager are not
|
||||
each solving it.
|
||||
|
||||
## 3. Networking
|
||||
|
||||
Two things the stack owed on the day it landed, both still owed:
|
||||
- **A routing table** and `/proc/net/route`. With two interfaces up the first is the default route and the
|
||||
second is reachable only by an address on its own subnet; that is a rule and not routing.
|
||||
- **Wi-Fi.** The laptop has no other way to be wireless. The route is the same as for the NIC: a Linux
|
||||
driver (`iwlwifi`, `mt76`…) through LDLS, which needs `nl80211`, a firmware loader that exists, and a
|
||||
supplicant (`wpa_supplicant` is an Alpine package).
|
||||
- **IPv6.**
|
||||
- **A netlink event stream**, for anything that wants to be told a link changed.
|
||||
|
||||
- **`accept()` does not report the peer's address**, and there is no
|
||||
`getsockname`/`getpeername`. Nothing has needed them yet; a server that wants
|
||||
to log who connected will.
|
||||
- **No routing table.** With two interfaces up, the first is the default route
|
||||
and the second is reachable only by an address on its own subnet. That is a
|
||||
rule rather than an accident, and it is not routing.
|
||||
## 4. The hardware that makes a laptop a laptop
|
||||
|
||||
Two that were on this list are now done and described in
|
||||
[../architecture/networking.md](../architecture/networking.md): `setsockopt`
|
||||
honours six options and refuses the rest, and `sendmsg`/`recvmsg` carry
|
||||
`SCM_RIGHTS`. A third is closer than it was — toybox's `ping` needed `recvmsg`
|
||||
*and* `IP_RECVTTL`, and only the second is still missing.
|
||||
- **HDA audio.** AC'97 is what QEMU and old boards have; a laptop built this century has Intel HDA,
|
||||
which is codec enumeration, CORB/RIRB and stream descriptors. It arrives as a second `struct audio_ops`.
|
||||
- **Embedded-controller events** (the ACPI SCI), so a lid, a power button on the EC and a battery change are seen.
|
||||
- **CPU frequency control** (AMD CPPC; Intel HWP/EIST). Idle already reaches `_CST`'s C2; under load the cores run at
|
||||
the firmware's default clock.
|
||||
- **Touchpad gestures** — two-finger scroll, palm rejection — on top of I2C-HID's mouse mode.
|
||||
- **An IOMMU** (VT-d / AMD-Vi), which turns "a driver in a process is isolated against bugs" into "against malice" —
|
||||
the largest honest caveat on [ldls.md](../architecture/ldls.md).
|
||||
- **udev's job** — a uevent socket and `/sys/devices` — so X and others are told about devices instead of being
|
||||
given a list.
|
||||
|
||||
## 4. The interactive crash screen
|
||||
## 5. The desktop, and the road to Wayland
|
||||
|
||||
PSOGM renders and stops. The idea is *a small OS inside the crash*, and the
|
||||
framing decides the design:
|
||||
SchOS does not write its own compositor. (Schcom, a 715-line v0, was deleted: its wire format was a dead end, and a
|
||||
compositor worth running is years of work that Alpine already packages.) The desktop is **Xorg with Window Maker** now
|
||||
([desktop.md](../architecture/desktop.md)); **XLibre**, the Xorg fork, is a drop-in alternative if Alpine carries it, and a
|
||||
Wayland compositor (labwc, sway, weston, a GNOME or KDE session) is the larger step. What Wayland — and a modern X — need
|
||||
from the system, in the order they are useful:
|
||||
|
||||
> At panic time the kernel is the only thing running, which is exactly why it can
|
||||
> offer what a live system cannot: walk the stack, read another process's memory,
|
||||
> dump page tables, inspect the process table — with no scheduler and no locking
|
||||
> to respect, because nothing else can race you.
|
||||
1. **udev — done** ([udev.md](../architecture/udev.md)). eudev runs from `schinit`, the uevent netlink socket and the
|
||||
`/sys/devices` tree exist, PCI and USB devices are in it with `add`/`remove` events from USB hot-plug, devtmpfs takes udev's
|
||||
links, and Xorg finds its keyboard and mouse through it. **Left:** USB interfaces, PCI bridges as parents, `hwdb`.
|
||||
2. **libinput and libseat — done.** `libinput list-devices` sees the keyboard and the pointer as an ordinary user, Xorg now
|
||||
drives both through `xf86-input-libinput`, and `seatd` runs from `schinit`: a non-root program opens `seat0` through
|
||||
`libseat`, is handed `/dev/input/event0` over the socket, and reads it. Getting there found a real kernel bug (sockets
|
||||
returned by `accept()` could not carry file descriptors) and a second one in `sendmsg`'s control-buffer parsing; both would
|
||||
have stopped a Wayland compositor and its clients too. **Not yet tried:** a compositor.
|
||||
3. **A DRM device.** Wayland compositors want `/dev/dri/card0` with KMS, and Mesa wants `renderD128`. SchOS has neither: it has
|
||||
`/dev/fb0`. This is the GPU plan ([gpu.md](gpu.md)) — Linux's own `amdgpu` run by LDLS and `/dev/dri` served by that
|
||||
process — and, for machines without a real GPU driver, a *simple-framebuffer* DRM shim (Linux's `simpledrm`, which is a
|
||||
`.ko` LDLS could also host).
|
||||
4. **dma-buf, `epoll`-able sync files and the rest of the Linux graphics ABI**, as the compositors turn out to need them.
|
||||
|
||||
What makes it a *system* rather than a print statement is that it keeps the base
|
||||
machine's drivers. Concretely, the ones that work with interrupts off: the
|
||||
framebuffer (`gfx_draw_pixel` is a plain memory write), the PS/2 keyboard
|
||||
(pollable via ports 0x60/0x64), and the serial port. **Those three are a complete
|
||||
console.**
|
||||
None of this is a kernel rewrite: it is the Linux interfaces that already exist half-built (`/sys`, netlink, evdev, `SCM_RIGHTS`,
|
||||
`memfd`), finished until real programs stop asking for what is missing. The method is the one that has worked all along: `apk
|
||||
add` the program, run it, and fix what it names.
|
||||
|
||||
Everything else — IDE, USB, the timer, anything that sleeps or takes a
|
||||
sleeplock — is off limits, because the lock it needs may be precisely the one
|
||||
held by the CPU that just died.
|
||||
Nothing here starts before [1.0.md](1.0.md)'s register is empty, except where the owner says otherwise.
|
||||
|
||||
So the shape is a **polled event loop over preallocated state**: no `kalloc`, no
|
||||
`sleep`/`wakeup`, no `acquire` on anything a live path might hold, one static
|
||||
command buffer, and that driver set. **That restriction is not a limitation to
|
||||
work around later; it is the invariant that makes the mode trustworthy when
|
||||
everything else is not.** Anything wanting more should be answered by rebooting
|
||||
into a recovery environment.
|
||||
## 5b. The boot manager
|
||||
|
||||
A useful precedent to read but not copy: DDB, the BSD in-kernel debugger, is
|
||||
exactly this, and its hard-won rule is the same one — *a debugger that can
|
||||
deadlock is worse than no debugger.*
|
||||
The loader ([boot.md](../architecture/boot.md#the-boot-manager)) is now a Niveus-styled window: bevelled keys, wells, Instrument Sans,
|
||||
keyboard and pointer, a list of other systems it finds on the disks and can chain-load, the F8 options (resolution with its
|
||||
"keep this?" confirmation, verbose boot), and real Firmware Setup, Restart and Shut Down.
|
||||
|
||||
## 5. The installer and `.sip`
|
||||
Still to do:
|
||||
|
||||
Scoped in [`projects/schinst/README.md`](../../projects/schinst/README.md),
|
||||
which holds the detail. In short:
|
||||
- [ ] boot entries from a file on the ESP (several kernels, command lines, "drivers off", a default and a timeout), instead of
|
||||
the two fixed ways of starting this system
|
||||
- [ ] a SchFS partition as a boot source, not just the volume the loader came from
|
||||
- [ ] an honest failure screen for the loader's own errors (today `die()` prints to the console)
|
||||
- [ ] the pointer path verified on a real machine; QEMU with OVMF never delivered a USB mouse event to the loader
|
||||
- [ ] light appearance, and a theme read from the ESP
|
||||
|
||||
It is its own piece of work in `boot/uefi/`; the kernel's TrueType rasteriser is not available at that point, which is why the
|
||||
loader carries a pre-rasterised face (`include/gfx/nvfont.h`).
|
||||
|
||||
### The installer and `.sip`
|
||||
|
||||
Scoped in [`projects/schinst/README.md`](../../projects/schinst/README.md), which holds the detail. In short:
|
||||
|
||||
- [ ] `.sip` format specification, **written down before any code**
|
||||
- [ ] `tools/mksip` — capture a directory tree into a package
|
||||
- [ ] The apply path
|
||||
- [ ] An installation environment that boots from the ISO, partitions, applies
|
||||
the `.sip`, and installs the loader
|
||||
- [ ] An installation environment that boots from the ISO, partitions, applies the `.sip`, and installs the loader
|
||||
|
||||
`.sip` is modelled on WIM, and the thing worth copying from WIM is that it is
|
||||
**file-based, not block-based**: one file holds a whole filesystem, applies to a
|
||||
partition of any size, and several images can share their common files.
|
||||
`.sip` is modelled on WIM, and the thing worth copying from WIM is that it is **file-based, not block-based**: one
|
||||
file holds a whole filesystem, applies to a partition of any size, and several images can share their common files.
|
||||
|
||||
**Most of this already exists.** `tools/mkschfs.c` and `tools/mkfs.c` both do
|
||||
content dedup with an `(st_dev, st_ino)` fast path, and SchFS's B-tree is content
|
||||
addressed. `mksip` should share that code rather than reimplement it — that is
|
||||
the reason the dedup was factored the way it was.
|
||||
**Most of this already exists.** `tools/mkschfs.c` and `tools/mkfs.c` both do content dedup with an `(st_dev, st_ino)`
|
||||
fast path, and SchFS's B-tree is content addressed. `mksip` should share that code rather than reimplement it. And
|
||||
the live image is already a compressed block image of a SchFS root ([scff.md](../architecture/scff.md)); an installer
|
||||
may need nothing more than to write that, decompressed, to a partition.
|
||||
|
||||
Open questions, all wanting a decision before code:
|
||||
|
||||
- **Compression.** Something with a small decompressor is worth more than
|
||||
something with a good ratio, because the decompressor runs in the installation
|
||||
environment.
|
||||
- **Multiple images per `.sip`** — WIM's real trick, install/recovery/boot
|
||||
sharing one blob store. Worth the complexity now, or not?
|
||||
- **Can the kernel mount a `.sip` read-only directly?** It is close to a
|
||||
filesystem already, and if it can, the installation environment's root can
|
||||
simply *be* the `.sip`.
|
||||
- **Compression.** Something with a small decompressor is worth more than something with a good ratio, because the
|
||||
decompressor runs in the installation environment. (zstd is already in the kernel for SCFF.)
|
||||
- **Multiple images per `.sip`** — WIM's real trick, install/recovery/boot sharing one blob store. Worth the complexity
|
||||
now, or not?
|
||||
- **Can the kernel mount a `.sip` read-only directly?** If it can, the installation environment's root can simply *be* the `.sip`.
|
||||
|
||||
The GUI for this is a Wayland client; see [wayland.md](wayland.md).
|
||||
Its GUI is the boot manager's (5b) — the installation environment starts in it — or an X client.
|
||||
|
||||
## 6. SchFS beyond SchOS
|
||||
|
||||
In the order they should happen.
|
||||
|
||||
**~~An on-disk format version~~, and an upgrade path.** The version policy is
|
||||
done — see [../architecture/schfs.md](../architecture/schfs.md). What remains is
|
||||
the **upgrade step**, and it cannot be written until there are two formats to
|
||||
convert between; the first bump is when that becomes real work rather than
|
||||
speculation.
|
||||
**An upgrade step.** The format version policy is done ([schfs.md](../architecture/schfs.md)); the upgrade step cannot be
|
||||
written until there are two formats to convert between, and the first bump is when it becomes real work.
|
||||
|
||||
The format is now pinned, which is what the other two items were waiting on.
|
||||
**A Linux implementation, FUSE first.** Three reasons in order of weight: it can be tested with `fsx` and `xfstests`, which
|
||||
are far harsher than anything written alongside the code; it carries no licence entanglement, since libfuse is LGPL and used
|
||||
across a process boundary; and it can reuse the read/write logic already in `tools/mkschfs` and `tools/schfs_fsck` rather than
|
||||
being written twice and having the two disagree. A kernel module comes only after FUSE passes `xfstests`; SchOS is GPL-2.0-or-later
|
||||
([../licensing.md](../licensing.md)), so a Linux module is a technical project now and not a licensing one.
|
||||
|
||||
**A Linux implementation, FUSE first.** Three reasons in order of weight: it can
|
||||
be tested with `fsx` and `xfstests`, which are far harsher than anything written
|
||||
alongside the code; it carries no licence entanglement, since libfuse is LGPL and
|
||||
used across a process boundary; and it can reuse the read/write logic already in
|
||||
`tools/mkschfs` and `tools/schfs_fsck` rather than being written twice and having
|
||||
the two disagree.
|
||||
|
||||
A kernel module is a different project, comes only after FUSE passes `xfstests`,
|
||||
and carries the licence problem [../licensing.md](../licensing.md) describes.
|
||||
|
||||
**The missing features**, listed honestly before adding any: no journal replay
|
||||
across an unclean shutdown beyond what copy-on-write gives for free, no
|
||||
snapshots, no sparse-file support, and no online `fsck`. **Snapshots are the one
|
||||
where the existing design already pays for most of the cost** — the B-tree is
|
||||
copy-on-write, which is most of the machinery; nothing takes or names a snapshot.
|
||||
**The missing features**, listed honestly before adding any: no journal replay across an unclean shutdown beyond what
|
||||
copy-on-write gives for free, no snapshots, no sparse-file support, and no online `fsck`. **Snapshots are the one where the
|
||||
existing design already pays for most of the cost** — the B-tree is copy-on-write, which is most of the machinery; nothing takes
|
||||
or names a snapshot.
|
||||
|
||||
## 7. The kernel's own gaps
|
||||
|
||||
Not phases, and each one improves everything above it:
|
||||
|
||||
- **A slab or pool allocator.** `kalloc` hands out one page at a time, and every
|
||||
subsystem written since has invented its own fixed-size table to work around
|
||||
that. This is the single change that would most improve the rest of the kernel.
|
||||
- **A page cache**, which is what a general writable `MAP_SHARED` needs.
|
||||
[foundations.md](foundations.md) delivers a `memfd` instead, which is narrower
|
||||
and sufficient for the GUI.
|
||||
- **Symlinks in procfs**, so `/proc/<pid>/exe` can be one and `ps -o comm` stops
|
||||
printing `[init]`. Belongs with the rest of the VFS work rather than on its own.
|
||||
- **The `ls -lR` hang** — **not reproducible**, and kept here because the absence
|
||||
of a result is a result. Instrumented and hunted at length; see Gate 0 of
|
||||
[1.0.md](1.0.md). Two real defects were found on the way and fixed, neither
|
||||
proven to be the cause.
|
||||
- **No garbage collector for descriptors in flight.** An `AF_UNIX` socket passed
|
||||
over itself and never received leaks the pair. Bounded at thirty-two sockets,
|
||||
and nothing in the tree does it — but a third-party program could.
|
||||
- **A page cache**, which is what a general writable `MAP_SHARED` of a file needs. `memfd` is the narrow answer and has been enough.
|
||||
- **A working `mprotect`, and NX.** `mprotect` is accepted and ignored and every mapping is executable; RELRO, guard pages
|
||||
and a W^X JIT all want the real thing.
|
||||
- **Per-CPU run queues** and work stealing, with a load balancer that is not worse than the lock it replaces.
|
||||
- **A garbage collector for descriptors in flight.** An `AF_UNIX` socket passed over itself and never received leaks the pair;
|
||||
bounded at thirty-two, and nothing in the tree does it.
|
||||
- **Kernel-time accounting**, so `ru_stime` is not zero.
|
||||
- **A contiguous multi-page allocator**, so `struct proc` and `struct socket` can come from somewhere other than fixed tables.
|
||||
|
||||
---
|
||||
|
||||
## Kept out of scope on purpose
|
||||
|
||||
- Anything that needs pushing or publishing. This work stays local.
|
||||
- TrueType hinting.
|
||||
- XWayland.
|
||||
- DRM/KMS as a *kernel* subsystem; the GPU plan serves `/dev/dri` from a process.
|
||||
|
||||
## Unfinished, as of 2026-10-26
|
||||
|
||||
Left where it stood; none of it is hidden by the code.
|
||||
|
||||
- **`startx` as root fails.** The image now has only root, and in QEMU `startx`
|
||||
ends with `xinit: unable to connect to X server: Bad file descriptor` and
|
||||
writes `/var/log/Xorg.0.log` (not `~/.local/share/xorg/`). It worked as the
|
||||
old `schos` user. The cause has not been looked at; start with that log.
|
||||
- **Window Maker on the laptop hung** (no cursor movement, no exit, no VT
|
||||
switch) while Xorg was using `xf86-input-libinput`. That package is out of the
|
||||
image again; whether Xorg's own evdev driver is enough on that machine is not
|
||||
confirmed. If it still hangs, the `Xorg.0.log` from the machine is what is
|
||||
needed.
|
||||
- **ldlsd's crash on real hardware** was a misaligned `movaps` in a variadic
|
||||
function; LDLS guest code is now built with `-mstackrealign`. Not run on the
|
||||
laptop since. Two `SchOS: permission denied (write)` lines appear just before
|
||||
it; the message now names the process and inode so the next log says who.
|
||||
- **Boot manager pointer**: the cursor is drawn whenever a pointer device
|
||||
exists, but no click or movement has been seen from a real device (QEMU's USB
|
||||
mouse never reached the loader). A laptop touchpad is usually not exposed to
|
||||
UEFI at all.
|
||||
- **Kernel-side splash**: the loader's splash is in the new style; what vgpud
|
||||
draws once the kernel is up is still the old mark.
|
||||
- **USB**: no `/dev/bus/usb` (usbfs), so libusb tools such as plain `lsusb` do
|
||||
not work (`lsusb -t` does); `uname -r` says `0.1.0`, which libusb refuses; hub
|
||||
port counts and string descriptors are missing.
|
||||
- **Terminals**: all twelve ask for a login. There is no longer a login-free
|
||||
root shell to fall back on if `login` itself breaks.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
# SchAPI, and SchOS as a system
|
||||
|
||||
Status: **direction agreed, not yet implemented.** Nothing in this document
|
||||
Status: **direction agreed, not yet implemented**, and frozen until 1.0 ships
|
||||
([1.0.md](1.0.md)). Read it as a direction and not as a schedule: written when the plan was a
|
||||
compositor of SchOS's own, it has not been revisited since the desktop became Xorg and the
|
||||
packages became Alpine's, and §7 (what happens now) in particular predates both. It also assumed SchOS would write its
|
||||
own compositor (Schcom) for the API to be designed *from*; that compositor was deleted, and SchOS's desktop
|
||||
is other people's, so read every mention of Schcom below as "the desktop". Nothing in this document
|
||||
changes what is being built this week; it changes what everything is being
|
||||
built *towards*. See §7 for what actually happens now.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,177 +0,0 @@
|
|||
# The GUI becomes Wayland
|
||||
|
||||
Schcom survives and changes its wire format. SchWM and SchDE are deleted. What
|
||||
follows is why that is not a retreat, and what it costs.
|
||||
|
||||
Blocked on [foundations.md](foundations.md). Everything there is built except
|
||||
item 4, `setsockopt` that does not lie: descriptor passing, shareable memory,
|
||||
sockets in the filesystem, dynamic linking, and the epoll/eventfd/timerfd/
|
||||
signalfd set libwayland's event loop is made of.
|
||||
|
||||
---
|
||||
|
||||
## What was deleted
|
||||
|
||||
`projects/schwm/` and `projects/schde/`, both README-only; no code was lost. They
|
||||
are **gone from the tree now** rather than marked for deletion — a directory that
|
||||
has said "to be deleted" for months is a corner, not a decision. The arguments
|
||||
each made are kept below, because they are the part worth keeping.
|
||||
|
||||
### SchWM is not abandoned — Wayland dissolves it
|
||||
|
||||
In X11 the window manager is a separate privileged client because X11 puts
|
||||
compositing in the server and policy nowhere. **Wayland's central decision is
|
||||
that the compositor *is* the window manager**: there is no protocol by which a
|
||||
second process places another's windows, and every attempt to add one has been a
|
||||
private extension.
|
||||
|
||||
So SchWM's own argument —
|
||||
|
||||
> *"Compositing is mechanical and must never stall: it runs every frame and a
|
||||
> mistake there is a frozen screen. Window management is policy, full of special
|
||||
> cases and personal taste, and it changes constantly."*
|
||||
|
||||
— is a real and correct observation **about X11**, which Wayland answers a
|
||||
different way. It stays worth honouring, but as a **module boundary inside
|
||||
Schcom**, not a process boundary: policy in its own file behind its own
|
||||
interface. Do not keep it in its own process, because the protocol to talk to it
|
||||
does not exist.
|
||||
|
||||
### SchDE is re-sited rather than abandoned
|
||||
|
||||
A panel, a launcher and a wallpaper are ordinary Wayland clients using
|
||||
`wlr-layer-shell` — MIT, an XML file, no dependency. They need no directory of
|
||||
their own until one of them is written. SchDE's other observation stands on its
|
||||
own and is recorded here so it does not go with the directory: **a GUI terminal
|
||||
is not the kernel console with a window around it.** The console parses ANSI
|
||||
because it draws straight to the framebuffer; a terminal client parses ANSI
|
||||
itself and renders it with proportional-capable text. The two share no code.
|
||||
|
||||
### `projects/schinst/` stays
|
||||
|
||||
Its GUI was the part tied to SchWM, and that part becomes a Wayland client.
|
||||
Everything else in it — the `.sip` format, `mksip`, the apply path, the
|
||||
installation environment — is untouched by any of this, and remains the
|
||||
best-specified unstarted work in the tree.
|
||||
|
||||
---
|
||||
|
||||
## What survives
|
||||
|
||||
**Schcom stays, and keeps its name.** It already owns the display, allocates and
|
||||
owns client buffers, implements surface/attach/commit, routes input from
|
||||
`/dev/input/eventN`, composites multiple clients bottom-to-top, and cleans up
|
||||
after clients that die.
|
||||
|
||||
Its own README says the protocol is deliberately Wayland-shaped — surfaces rather
|
||||
than windows, buffers attached and made visible only on commit, the compositor
|
||||
telling clients when to draw. That was the right call and this is where it is
|
||||
cashed in: **what changes is the wire format and the buffer handle, not the
|
||||
architecture.**
|
||||
|
||||
---
|
||||
|
||||
## What has to exist first
|
||||
|
||||
Items 1–4 of [foundations.md](foundations.md), and the first three are absolute —
|
||||
libwayland does not start without them.
|
||||
|
||||
The sharpest is descriptor passing. `wl_shm_create_pool` passes a **file
|
||||
descriptor**, and there is no path in the protocol that passes an integer
|
||||
instead. `struct schcom_surface`'s `handle_kind` field was written precisely so
|
||||
that switching would be a new value rather than a protocol break, and it cannot
|
||||
help: *our* protocol is the flexible one.
|
||||
|
||||
---
|
||||
|
||||
## Port libwayland. Do not port Weston.
|
||||
|
||||
**libwayland is the small piece**: a wire protocol, an object map, and an event
|
||||
loop, needing `libffi` and — for the build-time scanner, on the host only —
|
||||
`libexpat`. A real port, but a bounded one.
|
||||
|
||||
**Weston is the trap.** It arrives welded to `libinput`, `udev`, `libdrm`,
|
||||
`logind`-shaped seat management and a GL renderer, and porting it means porting
|
||||
Linux's device management first and finding the compositor somewhere underneath.
|
||||
|
||||
This is precisely the lesson already recorded twice in this tree — about
|
||||
FreeBSD's `sys/dev/usb` (100k lines welded to newbus) and about FreeBSD's
|
||||
`sys/netinet` (not a library; reaches into mbufs, epoch, netisr, VNET and uma).
|
||||
|
||||
> The tree has learned this twice. It should not pay to learn it a third time.
|
||||
|
||||
So **Schcom links `libwayland-server` and remains ours.** Input comes from
|
||||
`/dev/input/eventN`, which already works, rather than from libinput. Output goes
|
||||
through `display_ops`, which already exists.
|
||||
|
||||
### Software rendering first, and probably for a long time
|
||||
|
||||
`pixman` for composition, `wl_shm` for buffers. Mesa means EGL means `dlopen`
|
||||
means a dynamic loader -- which now exists (see [foundations.md](foundations.md)
|
||||
item 5), so it is no longer what stands in the way of Mesa; a GPU driver is.
|
||||
Static binaries are still enough to reach a working desktop, and the desktop's
|
||||
own programs can stay static until something concrete demands otherwise.
|
||||
|
||||
### One sharp edge
|
||||
|
||||
`wl_keyboard` hands the client **a keymap as an XKB text blob over a file
|
||||
descriptor**. A compositor cannot avoid xkbcommon by having simpler opinions
|
||||
about keyboards — the protocol requires the blob.
|
||||
|
||||
The cheap route is one precompiled keymap string shipped in the binary, with
|
||||
xkbcommon and `xkeyboard-config` deferred until more than one layout is wanted.
|
||||
|
||||
---
|
||||
|
||||
## Licensing
|
||||
|
||||
Checked before anything is fetched — the standing rule is that the licence goes
|
||||
in `NOTICE` **before** the file is committed, not after.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| wayland, wayland-protocols | MIT |
|
||||
| `wlr-layer-shell` | MIT |
|
||||
| pixman | MIT |
|
||||
| libffi | MIT |
|
||||
| libxkbcommon | MIT |
|
||||
| xkeyboard-config | MIT |
|
||||
|
||||
All GPL-2.0-or-later compatible, all needing only a `NOTICE` entry. The
|
||||
Wayland ecosystem is permissively licensed almost throughout, so nothing here
|
||||
raises a licensing question at all.
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **W0** | [foundations.md](foundations.md) items 1–4, with the combined test that passes a descriptor *and* shares a `memfd` *and* binds a socket at a real path. |
|
||||
| **W1** | libwayland builds and installs into the sysroot. `wayland-scanner` runs on the host. |
|
||||
| **W2** | Schcom serves `wl_compositor`, `wl_shm`, `wl_surface`, `xdg_shell`. A client draws one window. |
|
||||
| **W3** | `wl_seat`: keyboard and pointer, from the evdev path that exists. |
|
||||
| **W4** | Two clients, focus, stacking, move and resize. **This is where SchWM's policy lives, as a file.** |
|
||||
| **W5** | `wlr-layer-shell`, and a panel. This is SchDE's first pixel. |
|
||||
|
||||
Every milestone from W2 on is asserted **from the host, by pixel**, the way
|
||||
`schcom-test` already does. The compositor's own report that it composited
|
||||
something proves nothing.
|
||||
|
||||
---
|
||||
|
||||
## What it will still not do
|
||||
|
||||
- **No hardware acceleration.** Software rendering only, until Mesa, which is
|
||||
not on this plan. Dynamic linking, which it also needs, is in.
|
||||
- **No XWayland**, so no X11 clients, ever, unless someone ports it.
|
||||
- **No mode setting.** Resolution stays whatever the loader asked for — see
|
||||
[hybrid.md](hybrid.md), where the GPU driver's eviction is also where a modeset
|
||||
API would arrive.
|
||||
- **No damage tracking beyond "repaint everything"**, no multiple outputs, no
|
||||
subsurfaces. Schcom's README already lists these; they are additions rather
|
||||
than rewrites, which is the reason for committing to the surface/commit model
|
||||
now.
|
||||
- **One keyboard layout**, until xkbcommon is ported.
|
||||
- **No clipboard, no drag and drop, no screen capture protocol**, none of which
|
||||
W0–W5 touches.
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# Xorg
|
||||
|
||||
The desktop is X11, on the framebuffer. This replaces [wayland.md](wayland.md)
|
||||
as the plan for Gate 3 of [1.0.md](1.0.md); Wayland stays on file and is not
|
||||
being worked on.
|
||||
|
||||
## Why X and not Wayland
|
||||
|
||||
Every Wayland compositor worth running -- labwc, and everything else built on
|
||||
wlroots -- needs DRM/KMS, libinput, udev and libseat. DRM/KMS is the largest of
|
||||
them by far: a kernel graphics subsystem, and in practice a great deal taken
|
||||
from Linux. Xorg runs on a plain framebuffer through its `fbdev` driver and
|
||||
reads input through `evdev`, both of which are interfaces SchOS already has.
|
||||
It is the shorter road to a desktop by a long way, and it brings the window
|
||||
managers with it: twm first, then Window Maker.
|
||||
|
||||
DRM/KMS is not planned. libinput, udev and libseat are worth having for their
|
||||
own sake and may come later; Xorg does not need them.
|
||||
|
||||
## What is built
|
||||
|
||||
- **The kernel calls** (`b1323e1`, checked by `cmd/xready`): evdev's queries,
|
||||
fbdev's mode-set to the current mode, `KDSETMODE`, `KDSKBMODE`, the
|
||||
`VT_PROCESS` switch handshake, and `/sys/bus/pci/devices/*/{boot_vga,
|
||||
enable,graphics/fb0}` for libpciaccess.
|
||||
- **Dynamic linking and `dlopen`** (`4330576`): Xorg loads its drivers as
|
||||
modules.
|
||||
- **`<linux/*.h>`** in the build sysroot, as `ports/linux-headers`.
|
||||
|
||||
## What is left, in order
|
||||
|
||||
1. **The X client libraries.** xorgproto, xtrans, libXau, libXdmcp,
|
||||
xcb-proto, libxcb, libX11, libXext, libXrender, libXfixes, libxkbfile,
|
||||
libfontenc, libXfont2, freetype, zlib, pixman, libpciaccess.
|
||||
2. **Keyboard and fonts.** xkbcomp and xkeyboard-config, the data X compiles
|
||||
keymaps from; a misc-fixed font.
|
||||
3. **The server.** xorg-server built with meson, GLX, glamor and DRI off;
|
||||
xf86-video-fbdev; xf86-input-evdev with libevdev and mtdev.
|
||||
4. **The first picture.** `twm` and `xterm`, started by hand from a VT.
|
||||
5. **Window Maker**, and an `startx` that works.
|
||||
|
||||
The kernel calls were written against what these programs are documented
|
||||
to do. Each step past it is expected to find a call that was not -- that is
|
||||
what running the real program is for.
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
# The interactive shell on /dev/tty1, the first virtual terminal.
|
||||
|
||||
Label = os.schos.console-shell
|
||||
# zsh, not /bin/sh. mksh stays as /bin/sh and is what schinit falls back to
|
||||
# when no manifest loads -- so if zsh ever fails to start, the recovery shell
|
||||
# is a different program rather than the same broken one. See ports/zsh.
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
# A login prompt, not a shell: /etc/schos-getty hands the terminal to login,
|
||||
# which asks who is there and starts their shell. tty6 keeps a root shell
|
||||
# with no login, for when this part of the system is what is broken.
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
|
|
@ -15,8 +15,6 @@ KeepAlive = yes
|
|||
# most: a line editor with no TERM cannot ask terminfo how to move a cursor.
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
# This job reads the console, so it has to own it: schinit puts every job in
|
||||
# its own session, and a session with no controlling terminal is not delivered
|
||||
|
|
|
|||
17
etc/schinit/ldls-r8169.job
Normal file
17
etc/schinit/ldls-r8169.job
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# The Realtek RTL8111/8168 ethernet, driven by Linux's own r8169.
|
||||
#
|
||||
# The modules (mdio-bus, libphy, mdio_devres, the Realtek PHY driver and r8169
|
||||
# itself), the kernel's BTF and the RTL PHY firmware are put in the image by
|
||||
# `make ldls-r8169`. ldlsd loads them in that order, binds the PHY driver
|
||||
# through its driver core, and probes the NIC; the interface is ldls0.
|
||||
#
|
||||
# On a machine with no RTL8168 ldlsd exits SCHINIT_EXIT_NORESTART and that is
|
||||
# the end of it. KeepAlive is off while this is new: a driver that crashes on
|
||||
# real hardware would otherwise be restarted into the same crash every few
|
||||
# seconds. Its output, every line of it, is in /var/log/ldls-r8169.log.
|
||||
|
||||
Label = os.schos.ldls-r8169
|
||||
Program = /bin/sh
|
||||
Args = sh /lib/modules/ldls-r8169.sh
|
||||
RunAtLoad = yes
|
||||
KeepAlive = no
|
||||
17
etc/schinit/seatd.job
Normal file
17
etc/schinit/seatd.job
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# The seat daemon: hands a Wayland compositor (through libseat) the input and
|
||||
# display devices it needs without running the compositor as root. seatd opens
|
||||
# them as itself and passes the file descriptors over its socket; the socket is
|
||||
# group `video`, which the first user is in.
|
||||
#
|
||||
# Alpine's seatd package. A compositor that runs as root, or one started with
|
||||
# seatd-launch, needs none of this.
|
||||
|
||||
Label = os.schos.seatd
|
||||
Program = /usr/bin/seatd
|
||||
Args = seatd -g video
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
ThrottleInterval = 3
|
||||
|
||||
StandardOut = /dev/null
|
||||
StandardError = /dev/null
|
||||
20
etc/schinit/tty10-shell.job
Normal file
20
etc/schinit/tty10-shell.job
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# A login on /dev/tty10: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F10 (or Ctrl+Alt+F10) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty10-shell
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty10
|
||||
StandardOut = /dev/tty10
|
||||
StandardError = /dev/tty10
|
||||
|
||||
ThrottleInterval = 1
|
||||
20
etc/schinit/tty11-shell.job
Normal file
20
etc/schinit/tty11-shell.job
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# A login on /dev/tty11: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F11 (or Ctrl+Alt+F11) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty11-shell
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty11
|
||||
StandardOut = /dev/tty11
|
||||
StandardError = /dev/tty11
|
||||
|
||||
ThrottleInterval = 1
|
||||
20
etc/schinit/tty12-shell.job
Normal file
20
etc/schinit/tty12-shell.job
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# A login on /dev/tty12: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F12 (or Ctrl+Alt+F12) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty12-shell
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty12
|
||||
StandardOut = /dev/tty12
|
||||
StandardError = /dev/tty12
|
||||
|
||||
ThrottleInterval = 1
|
||||
|
|
@ -6,15 +6,13 @@
|
|||
# what /dev/tty1 and /dev/tty2 being different terminals means.
|
||||
|
||||
Label = os.schos.tty2-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
# An interactive shell on /dev/tty3.
|
||||
#
|
||||
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F3 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
# A login on /dev/tty3: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F3 (or Ctrl+Alt+F3) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty3-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
# An interactive shell on /dev/tty4.
|
||||
#
|
||||
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F4 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
# A login on /dev/tty4: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F4 (or Ctrl+Alt+F4) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty4-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
# An interactive shell on /dev/tty5.
|
||||
#
|
||||
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F5 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
# A login on /dev/tty5: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F5 (or Ctrl+Alt+F5) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty5-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,15 @@
|
|||
# An interactive shell on /dev/tty6.
|
||||
#
|
||||
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F6 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
# A login on /dev/tty6: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F6 (or Ctrl+Alt+F6) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty6-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
|
|
|
|||
20
etc/schinit/tty7-shell.job
Normal file
20
etc/schinit/tty7-shell.job
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# A login on /dev/tty7: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F7 (or Ctrl+Alt+F7) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty7-shell
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty7
|
||||
StandardOut = /dev/tty7
|
||||
StandardError = /dev/tty7
|
||||
|
||||
ThrottleInterval = 1
|
||||
20
etc/schinit/tty8-shell.job
Normal file
20
etc/schinit/tty8-shell.job
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# A login on /dev/tty8: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F8 (or Ctrl+Alt+F8) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty8-shell
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty8
|
||||
StandardOut = /dev/tty8
|
||||
StandardError = /dev/tty8
|
||||
|
||||
ThrottleInterval = 1
|
||||
20
etc/schinit/tty9-shell.job
Normal file
20
etc/schinit/tty9-shell.job
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# A login on /dev/tty9: every one of the twelve virtual terminals asks who is there.
|
||||
# Alt+F9 (or Ctrl+Alt+F9) shows it. Twelve manifests rather than one with a
|
||||
# range, because schinit has no notion of a template.
|
||||
|
||||
Label = os.schos.tty9-shell
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/schos-getty
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty9
|
||||
StandardOut = /dev/tty9
|
||||
StandardError = /dev/tty9
|
||||
|
||||
ThrottleInterval = 1
|
||||
14
etc/schinit/udev-trigger.job
Normal file
14
etc/schinit/udev-trigger.job
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Replay the devices that were already there when udevd started, so /run/udev
|
||||
# knows about them. Runs once; see /etc/udev-trigger.
|
||||
#
|
||||
# udevd is started by its own job and schinit has no ordering, so the script
|
||||
# waits for the daemon's control socket.
|
||||
|
||||
Label = os.schos.udev-trigger
|
||||
Program = /bin/sh
|
||||
Args = sh /etc/udev-trigger
|
||||
RunAtLoad = yes
|
||||
KeepAlive = no
|
||||
|
||||
StandardOut = /dev/tty1
|
||||
StandardError = /dev/tty1
|
||||
16
etc/schinit/udevd.job
Normal file
16
etc/schinit/udevd.job
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# eudev's daemon: the thing that turns the kernel's uevents into /run/udev
|
||||
# data and /dev symlinks, which is how Xorg and every Wayland compositor learn
|
||||
# what input and display devices exist.
|
||||
#
|
||||
# Alpine's eudev package; the kernel side is the uevent netlink socket and the
|
||||
# /sys device tree (kernel/dev/kdev.c, kernel/fs/sysfs.c).
|
||||
|
||||
Label = os.schos.udevd
|
||||
Program = /sbin/udevd
|
||||
Args = udevd
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
ThrottleInterval = 3
|
||||
|
||||
StandardOut = /dev/tty1
|
||||
StandardError = /dev/tty1
|
||||
10
etc/schos-getty
Normal file
10
etc/schos-getty
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
# What a virtual terminal runs: take the terminal back from whoever had it, and
|
||||
# ask for a login. login hands the terminal to the user it lets in, and this
|
||||
# runs again when their shell exits.
|
||||
t=$(tty 2>/dev/null)
|
||||
if [ -n "$t" ]; then
|
||||
chown root:tty "$t" 2>/dev/null
|
||||
chmod 0660 "$t" 2>/dev/null
|
||||
fi
|
||||
exec /bin/login
|
||||
14
etc/udev-schos.rules
Normal file
14
etc/udev-schos.rules
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# SchOS's own device permissions, applied after Alpine's (the name sorts last
|
||||
# once installed as 90-schos.rules).
|
||||
#
|
||||
# The kernel makes these nodes with the permissions it means them to have
|
||||
# (kernel/fs/devfs.c). udev, with no rule that names a node, would chmod it to
|
||||
# 0600 -- which made /dev/null unusable to everyone but root -- so every kernel
|
||||
# device is named here with the same answer the kernel gave.
|
||||
|
||||
SUBSYSTEM=="mem", MODE="0666"
|
||||
SUBSYSTEM=="tty", KERNEL=="console", GROUP="root", MODE="0600"
|
||||
SUBSYSTEM=="tty", KERNEL=="tty0|tty[0-9]|tty1[0-2]", GROUP="tty", MODE="0660"
|
||||
SUBSYSTEM=="input", GROUP="input", MODE="0660"
|
||||
SUBSYSTEM=="graphics", GROUP="video", MODE="0660"
|
||||
SUBSYSTEM=="block", GROUP="disk", MODE="0660"
|
||||
9
etc/udev-trigger
Normal file
9
etc/udev-trigger
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
# Wait for udevd to listen, then ask the kernel to announce every device again.
|
||||
i=0
|
||||
while [ ! -S /run/udev/control ] && [ $i -lt 100 ]; do
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
udevadm trigger --action=add
|
||||
udevadm settle --timeout=60
|
||||
|
|
@ -198,6 +198,14 @@ lcr3(uint64 val)
|
|||
asm volatile("mov %0,%%cr3" : : "r" (val));
|
||||
}
|
||||
|
||||
static inline uint64
|
||||
rcr3(void)
|
||||
{
|
||||
uint64 val;
|
||||
asm volatile("mov %%cr3,%0" : "=r" (val));
|
||||
return val;
|
||||
}
|
||||
|
||||
// Invalidate the TLB entry for one page. Needed any time a PTE is changed
|
||||
// or cleared for an address space that may still be the active CR3 —
|
||||
// otherwise the CPU can keep translating through the stale entry until the
|
||||
|
|
|
|||
|
|
@ -61,6 +61,39 @@ int pciuser_mmio_ok(uint64 phys, uint64 len);
|
|||
void pciuser_release_all(struct proc*);
|
||||
int pciuser_owner_pid(int idx);
|
||||
int proc_reincarnate(int status);
|
||||
int sig_default_stops(int);
|
||||
struct mm* mm_alloc(void);
|
||||
void mm_get(struct mm*);
|
||||
void mm_put(struct mm*);
|
||||
int mm_shared(struct mm*);
|
||||
void mm_lock(struct mm*);
|
||||
void mm_unlock(struct mm*);
|
||||
struct files* files_alloc(void);
|
||||
void files_get(struct files*);
|
||||
void files_put(struct files*);
|
||||
void files_lock(void);
|
||||
void files_unlock(void);
|
||||
struct sighand* sighand_alloc(void);
|
||||
void sighand_get(struct sighand*);
|
||||
void sighand_put(struct sighand*);
|
||||
struct proc* proc_leader(struct proc*);
|
||||
int proc_thread_count(int);
|
||||
void thread_exit(void) __attribute__((noreturn));
|
||||
int futex_wake_key(void*, int);
|
||||
int futex_op(addr_t, int, int, addr_t, addr_t, int);
|
||||
int clone_proc(uint64, uint64, uint64, uint64, uint64);
|
||||
void proc_exec_threads(void);
|
||||
void proc_exec_done(void);
|
||||
void thread_reaper_start(void);
|
||||
void vm_defer_tick(void);
|
||||
void mm_release_shared(struct mm*);
|
||||
void proc_stop(int);
|
||||
int sig_attention(struct proc*);
|
||||
int sig_blocking_check(void);
|
||||
void proc_cputime(uint64 *user, uint64 *sys);
|
||||
// kernel/dev/procfs.c: /proc/strace
|
||||
int strace_wanted(const char *name);
|
||||
int proc_write_strace(int user_src, uint64 src, uint n);
|
||||
void proc_kill_init(void);
|
||||
extern uint init_generation;
|
||||
void cprintf(char*, ...);
|
||||
|
|
@ -147,6 +180,10 @@ void mountinit(void);
|
|||
// 1 if the filesystem on this device refuses writes -- a mounted snapshot.
|
||||
// Ask before mutating, not after: the create path panics on a failed dirlink.
|
||||
int fs_readonly(uint dev);
|
||||
int in_group(uint gid, int real);
|
||||
uint inode_mode_bits(struct inode *ip);
|
||||
int inode_permission(struct inode *ip, int mask, int real);
|
||||
int capable(void);
|
||||
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
|
||||
const char *path, const char *source, const char *type);
|
||||
// Walk the mount table for /proc/mounts. `path` and `source` need MAXPATH
|
||||
|
|
@ -457,6 +494,8 @@ extern uint64 boot_epoch;
|
|||
int cpunum(void);
|
||||
extern volatile uint* lapic;
|
||||
void lapiceoi(void);
|
||||
extern volatile int panic_stop; // kernel/core/panic.c
|
||||
void lapic_nmi_others(void);
|
||||
void lapicinit(void);
|
||||
void lapicstartap(uchar, uint);
|
||||
void microdelay(int);
|
||||
|
|
@ -498,8 +537,22 @@ int sockname_inet(struct socket*, int peer, uint *addr, ushort *port
|
|||
#define SOCK_ABSTRACT_DEV 0xFFFFFFFFu // no filesystem has it; see unix.c
|
||||
int sockreadable(struct socket*);
|
||||
int sock_hup(struct socket*);
|
||||
uint socknl_pending(struct socket*);
|
||||
int sock_sendercred(struct socket*, int*, uint*, uint*);
|
||||
int socknl_copy(struct socket*, uint, char*, int);
|
||||
void socknl_take(struct socket*, uint);
|
||||
int sock_shutdown(struct socket*, int how);
|
||||
int sockpair(struct socket **a, struct socket **b);
|
||||
int sockpair_type(struct socket **a, struct socket **b, int type);
|
||||
int sock_is_packet(struct socket *s);
|
||||
uint sock_nl_groups(struct socket *s);
|
||||
int sock_netlink_bind(struct socket *s, uint pid, uint groups);
|
||||
int sock_uevent_send(struct socket *s, const char *msg, int len, uint dpid, uint dgroups);
|
||||
uint sock_nl_from(struct socket *s);
|
||||
uint sock_nl_pid(struct socket *s);
|
||||
uint sock_nl_bound_groups(struct socket *s);
|
||||
void dev_stat_numbers(int major, int minor, int *maj, int *min);
|
||||
void uevent_send(uint groups, const char *msg, int len, struct socket *from);
|
||||
const char* sockname(struct socket*);
|
||||
// AF_INET addressing. Separate from sockbind/sockconnect because the families
|
||||
// do not name the same kind of thing; `addr` and `port` are network order.
|
||||
|
|
@ -510,6 +563,8 @@ int socksendto(struct socket*, char *buf, int n,
|
|||
int sockbind_in(struct socket*, uint addr, ushort port);
|
||||
int sockconnect_in(struct socket*, uint addr, ushort port);
|
||||
int sock_domain(struct socket*);
|
||||
int sock_peercred(struct socket*, int*, uint*, uint*);
|
||||
int sock_passcred(struct socket*);
|
||||
// Socket options. The syscall layer does the user-memory marshalling and these
|
||||
// do the work, because struct socket's fields live in kernel/net/unix.c and the
|
||||
// syscall layer deliberately cannot see them.
|
||||
|
|
@ -550,6 +605,8 @@ int fork(void);
|
|||
int growproc(int64);
|
||||
int kill(int);
|
||||
int sigsend_pgrp(int pgid, int sig);
|
||||
int sigsend_user(int pid, int sig);
|
||||
int sigsend_pgrp_user(int pgid, int sig);
|
||||
int proc_pgid(int pid);
|
||||
int proc_setpgid(int pid, int pgid);
|
||||
int proc_setsid(void);
|
||||
|
|
@ -564,6 +621,7 @@ int proc_exists(int);
|
|||
int proc_num(void);
|
||||
struct procinfo;
|
||||
int proc_snapshot(int idx, struct procinfo *out);
|
||||
int proc_peek(int idx, struct procinfo *out); // no lock: GMMOS
|
||||
int proc_snapshot_pid(int pid, struct procinfo *out);
|
||||
void pinit(void);
|
||||
void procdump(void);
|
||||
|
|
@ -696,6 +754,18 @@ int net_eth_detach(struct device *);
|
|||
int net_detach(struct device *);
|
||||
int net_attach_late(struct device *);
|
||||
int net_eth_poll(void);
|
||||
void net_eth_link_check(void);
|
||||
uint poll_generation(void);
|
||||
// kernel/proc/mapname.c
|
||||
struct mm;
|
||||
void mapname_add(struct mm*, uint64 start, uint64 len, uint64 off, const char *path);
|
||||
void mapname_unmap(struct mm*, uint64 start, uint64 len);
|
||||
void mapname_free(struct mm*);
|
||||
void mapname_copy(struct mm *dst, struct mm *src);
|
||||
int mapname_find(struct mm*, uint64 addr, char *out, int cap, uint64 *off);
|
||||
int mapname_gen(struct mm*, char *buf, int cap);
|
||||
void mapname_report(struct proc*);
|
||||
int proc_maps(int pid, char *buf, int cap);
|
||||
int net_eth_count(void);
|
||||
void net_eth_counts(int i, uint64 *rx, uint64 *tx);
|
||||
struct netif *net_eth_netif(int i);
|
||||
|
|
@ -760,9 +830,27 @@ void shm_ref(int id);
|
|||
char* shm_page(int id, int pg);
|
||||
uint64 memfd_size(int id);
|
||||
int memfd_truncate(int id, uint64 len);
|
||||
int memfd_seals(int id, int add);
|
||||
int dsp_writable(void);
|
||||
// inotify (kernel/sys/sys_event.c), and the lookup it needs (sys_fs.c)
|
||||
void inotify_event(uint dev, uint64 inum, uint32 mask, const char *name, uint32 cookie);
|
||||
void inotify_file(struct file *f, uint32 mask);
|
||||
uint32 inotify_cookie(void);
|
||||
int ev_fionread(struct evobj *o);
|
||||
int pipe_avail(struct pipe *p);
|
||||
int vm_fault(addr_t va);
|
||||
void tlb_init(void);
|
||||
// keyboard lock keys and their lights: kernel/dev/kbd.c
|
||||
void kbd_set_leds(uint locks);
|
||||
void kbd_lock_key(uint lock, int down);
|
||||
uint kbd_lock_state(void);
|
||||
void hid_leds_want(int bits);
|
||||
void lapic_send_ipi(int apicid, int vector);
|
||||
pte_t* walkpte(pml4e_t *pgdir, const void *va);
|
||||
int path_identity(char *path, int follow, int onlydir, uint *dev, uint64 *inum);
|
||||
int memfd_rw(int id, int user, uint64 buf, uint64 off, int n, int writing);
|
||||
void shm_detach(int id);
|
||||
void proc_release_shared(struct proc *);
|
||||
|
||||
int copy_vma_range(pml4e_t *dstpgdir, pml4e_t *srcpgdir, uint64 va, uint64 length, int is_fb);
|
||||
void clearpteu(pml4e_t *pgdir, char *uva);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
//
|
||||
// 4 MB was one framebuffer at 1280x800, which was fine while exactly one driver
|
||||
// wanted one. It stopped being fine the day a display driver could be handed to
|
||||
// userspace (docs/plan/hybrid.md): either side of that handover **both drivers
|
||||
// userspace (docs/architecture/devices.md): either side of that handover **both drivers
|
||||
// exist**, and the incoming one has to allocate its framebuffer before the
|
||||
// outgoing one lets go of its own. So the real requirement is two framebuffers
|
||||
// at once, not one.
|
||||
|
|
|
|||
|
|
@ -57,7 +57,31 @@ struct usb_info {
|
|||
uint16 vid, pid, bcd;
|
||||
uint8 cls, subcls, proto; // the device descriptor's
|
||||
uint8 nconf;
|
||||
uint16 bcdusb; // the version the device speaks
|
||||
uint8 maxp0; // bMaxPacketSize0
|
||||
uint8 nif, confval, attrs; // the configuration descriptor's
|
||||
uint16 maxpower; // in mA
|
||||
char path[24]; // Linux's name: "1-3", "2-1.4"
|
||||
char driver[16]; // the first interface's, or empty
|
||||
};
|
||||
int usb_info_nth(int i, struct usb_info *out);
|
||||
// How many USB buses have shown a device so far; bus numbers run 1..this.
|
||||
int usb_nbuses(void);
|
||||
// Tell /sys and udev a USB device appeared ("add") or is about to go ("remove").
|
||||
void sysfs_usb_uevent(const struct usb_info *u, const char *action);
|
||||
void sysfs_usb_root_uevent(int bus, const char *action);
|
||||
// A USB controller is up, at this PCI address; number its bus and announce its
|
||||
// root hub.
|
||||
void usb_bus_add(void *hc, int pci_bus, int pci_slot, int pci_func);
|
||||
// The PCI address of the controller behind bus number `bus`; 0, or -1 if none.
|
||||
int usb_bus_pci(int bus, int *pci_bus, int *pci_slot, int *pci_func);
|
||||
|
||||
// One interface of a configured USB device, in the order the configuration
|
||||
// descriptor lists them (alternate setting 0 only).
|
||||
struct usb_if_info {
|
||||
uint8 num, cls, subcls, proto, nendp;
|
||||
char driver[16]; // what is bound to it, or empty
|
||||
};
|
||||
int usb_if_nth(int bus, int devnum, int n, struct usb_if_info *out);
|
||||
void sysfs_usb_if_uevent(const struct usb_info *u, const struct usb_if_info *i,
|
||||
const char *action);
|
||||
|
|
|
|||
|
|
@ -41,4 +41,5 @@ void klog_putc(int c);
|
|||
int klog_read(uint64 off, char *dst, int n);
|
||||
uint64 klog_len(void);
|
||||
uint64 klog_prev_bytes(void);
|
||||
uint64 klog_boot_bytes(void);
|
||||
int klog_recovered(void);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
#define NOFILE 128 // open files per process
|
||||
#define NFILE 512 // open files per system
|
||||
#define NINODE 200 // maximum number of active i-nodes
|
||||
#define NDEV 13 // maximum major device number
|
||||
#define NDEV 16 // maximum major device number
|
||||
#define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c)
|
||||
#define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,41 @@ struct vma {
|
|||
int shmid; // shared segment this came from, or -1
|
||||
};
|
||||
|
||||
// --- What threads share ---------------------------------------------------
|
||||
//
|
||||
// A thread is a struct proc like any other, scheduled like any other. What
|
||||
// makes several of them one program is that they point at the same three
|
||||
// objects below, each counted, each freed with its last user: clone() with
|
||||
// CLONE_VM, CLONE_FILES and CLONE_SIGHAND shares them, fork() copies them.
|
||||
|
||||
// The address space. pgdir is mirrored in every struct proc that uses it,
|
||||
// because the scheduler, trapasm and half the kernel read it from there; it
|
||||
// only changes at exec, which makes a new mm.
|
||||
struct mm {
|
||||
int ref;
|
||||
int locked; // mm_lock(): one mmap/munmap/brk at a time
|
||||
uint64 sz; // top of the heap; p->sz mirrors it
|
||||
uint64 mmap_next; // next free mmap VA (bump allocator, see sys_mmap)
|
||||
pml4e_t *pgdir;
|
||||
struct vma vma[NVMA]; // VMA tracking for mmap
|
||||
struct mapname *names; // which file each span came from; mapname.c
|
||||
};
|
||||
|
||||
// The descriptor table, and FD_CLOEXEC beside it.
|
||||
struct files {
|
||||
int ref;
|
||||
struct file *ofile[NOFILE];
|
||||
char flags[NOFILE];
|
||||
};
|
||||
|
||||
// Signal dispositions: what sigaction() sets.
|
||||
struct sighand {
|
||||
int ref;
|
||||
uint64 handler[32]; // 0 SIG_DFL, 1 SIG_IGN, else an address
|
||||
uint64 restorer[32];
|
||||
uint64 flags[32]; // sa_flags; SA_RESTART is the one read
|
||||
};
|
||||
|
||||
// Per-CPU state
|
||||
struct cpu {
|
||||
uchar id;
|
||||
|
|
@ -174,8 +209,14 @@ struct procinfo {
|
|||
uint64 nvcsw, nivcsw; // voluntary / involuntary context switches
|
||||
char name[16];
|
||||
char exe[64]; // full path, for /proc/<pid>/exe
|
||||
int kthread; // a kernel thread, not a program
|
||||
int stopped; // stopped by a signal (ps shows T)
|
||||
int tgid; // the process this thread belongs to
|
||||
int nthreads; // threads in that process
|
||||
};
|
||||
|
||||
#define NGROUPS_MAX 32
|
||||
|
||||
struct proc {
|
||||
uint64 sz; // Size of process memory (bytes) [+0x00]
|
||||
pml4e_t* pgdir; // Page table (PML4 on x86_64) [+0x08]
|
||||
|
|
@ -189,7 +230,7 @@ struct proc {
|
|||
void *chan; // [+0x40]
|
||||
int killed; // [+0x48]
|
||||
int _pad; // [+0x4c]
|
||||
struct file *ofile[NOFILE]; // Open files [+0x50]
|
||||
struct files *files; // Open files, shared by threads [+0x50]
|
||||
struct inode *cwd; // follows
|
||||
char name[16]; //
|
||||
// The path exec() was given. `name` is only the last component and only 15
|
||||
|
|
@ -236,7 +277,6 @@ struct proc {
|
|||
// alarmd, in kernel/proc/proc.c, is what posts it.
|
||||
uint alarm_at;
|
||||
uint alarm_every;
|
||||
char ofile_flags[NOFILE];
|
||||
|
||||
// Real signal delivery (added for SIGCHLD — see kernel/sys/syscall.c's
|
||||
// deliver_signals() and kernel/proc/proc.c's exit()). sig_handler[sig]/
|
||||
|
|
@ -245,8 +285,7 @@ struct proc {
|
|||
// the pre-signal trapframe so sys_rt_sigreturn() can restore it; in_signal
|
||||
// prevents re-entering a handler while one is already running (we don't
|
||||
// support nested signal delivery).
|
||||
uint64 sig_handler[32];
|
||||
uint64 sig_restorer[32];
|
||||
struct sighand *sighand; // dispositions, shared by threads
|
||||
struct trapframe saved_sigtf;
|
||||
int in_signal;
|
||||
|
||||
|
|
@ -257,8 +296,7 @@ struct proc {
|
|||
uint64 sigsusp_oldmask;
|
||||
int sigsusp_active;
|
||||
|
||||
struct vma vma[NVMA]; // VMA tracking for mmap
|
||||
uint64 mmap_next; // next free mmap VA (bump allocator, see sys_mmap)
|
||||
struct mm *mm; // address space, shared by threads
|
||||
|
||||
// ABI translation (Linux compat syscall number map)
|
||||
int *syscall_map;
|
||||
|
|
@ -299,6 +337,10 @@ struct proc {
|
|||
// are tracked honestly so getuid()/setuid() stop lying to userland.
|
||||
uint uid, euid, suid;
|
||||
uint gid, egid, sgid;
|
||||
// Supplementary groups, as setgroups(2) set them: a file's group is
|
||||
// consulted against these as well as against egid.
|
||||
int ngroups;
|
||||
uint groups[NGROUPS_MAX];
|
||||
|
||||
// --- Fair-share scheduling --------------------------------------------
|
||||
// The whole of the scheduler's per-process state. What each field means,
|
||||
|
|
@ -321,6 +363,34 @@ struct proc {
|
|||
// chroot(2): where an absolute path starts, and where ".." stops. 0 is the
|
||||
// system's root. Inherited across fork, held as a reference like cwd.
|
||||
struct inode *root;
|
||||
|
||||
// Syscall tracing: every call and its result go to the log. Set by exec
|
||||
// when the program's name is the one written to /proc/strace, and kept
|
||||
// across fork so that what a traced program starts is traced too.
|
||||
int strace;
|
||||
|
||||
// umask(2). Inherited across fork; 022 for the first process.
|
||||
uint umask;
|
||||
|
||||
// Job control. stopsig is the signal that stopped the process, 0 while it
|
||||
// runs; stop_report and cont_report are what wait(WUNTRACED/WCONTINUED)
|
||||
// has yet to tell the parent.
|
||||
int stopsig;
|
||||
int stop_report;
|
||||
int cont_report;
|
||||
|
||||
// Threads. tgid is the pid of the thread group's leader -- what getpid()
|
||||
// returns -- and equal to pid for every ordinary process.
|
||||
int tgid;
|
||||
uint64 clear_child_tid; // CLONE_CHILD_CLEARTID: zeroed and woken at exit
|
||||
void *futex_key; // the futex word this thread waits on, or 0
|
||||
int futex_woken;
|
||||
int autoreap; // a thread: reapd frees it, nobody waits for it
|
||||
struct proc *vfork_parent; // CLONE_VFORK: released at exec or exit
|
||||
int vfork_wait; // the vfork child this process is waiting on
|
||||
int thread_dying; // killed with its group: exits as a thread
|
||||
int group_status; // a thread's exit_group status, for the leader
|
||||
int group_status_set;
|
||||
};
|
||||
#define FD_CLOEXEC 1
|
||||
|
||||
|
|
|
|||
|
|
@ -30,4 +30,35 @@ void acpi_thread_start(void);
|
|||
void acpi_pci_route(void);
|
||||
|
||||
int acpi_poweroff(void); // S5; returns only on failure
|
||||
void acpi_poweroff_now(void); // S5 from a panic, no AML; returns only on failure
|
||||
int acpi_reboot(void); // the FADT reset register; ditto
|
||||
|
||||
// Batteries and AC adapters, for /sys/class/power_supply. Units are the
|
||||
// battery's own: unit 0 is mW/mWh, 1 is mA/mAh; ACPI_UNKNOWN where the
|
||||
// firmware does not say.
|
||||
#define ACPI_UNKNOWN 0xFFFFFFFFu
|
||||
struct acpi_psu {
|
||||
int battery; // else an AC adapter
|
||||
int present; // battery: in the bay
|
||||
int online; // adapter: plugged in
|
||||
int unit;
|
||||
uint32 state; // _BST: 1 discharging, 2 charging, 4 critical
|
||||
uint32 rate, remaining, voltage; // _BST, voltage in mV
|
||||
uint32 design, full, design_voltage, cycles;
|
||||
int technology; // 0 primary, 1 rechargeable
|
||||
char model[32], serial[32], kind[16], oem[32];
|
||||
};
|
||||
int acpi_psu_count(void);
|
||||
int acpi_psu_get(int i, struct acpi_psu *out);
|
||||
|
||||
// How an idle CPU sleeps: kernel/acpi/aml.c reads it from _CST, and the
|
||||
// scheduler's idle path in kernel/proc/proc.c acts on it.
|
||||
#define ACPI_IDLE_HLT 0
|
||||
#define ACPI_IDLE_IO 1 // read a port
|
||||
#define ACPI_IDLE_MWAIT 2 // MWAIT with a hint
|
||||
struct acpi_idle {
|
||||
int kind;
|
||||
uint64 addr; // the port, or the MWAIT hint
|
||||
uint latency; // microseconds
|
||||
};
|
||||
extern struct acpi_idle acpi_idle;
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ int audio_configure(struct audio_format *f); // 0, or negative
|
|||
int audio_write(const void *pcm, int bytes); // bytes queued, or negative
|
||||
void audio_drain(void);
|
||||
void audio_stop(void);
|
||||
int dsp_writable(void); // poll(): room for a write
|
||||
|
||||
// --- what userspace uses ---------------------------------------------------
|
||||
//
|
||||
|
|
@ -92,6 +93,23 @@ void audio_stop(void);
|
|||
#define SNDCTL_DSP_SYNC 0x00005001 // wait for the queue to drain
|
||||
#define SNDCTL_DSP_GETOSPACE 0x8010500C // struct audio_buf_info out
|
||||
#define SNDCTL_DSP_RESET 0x00005000 // stop, discard
|
||||
// The rest of what a buffer-and-pointer client needs -- ALSA's OSS plugin, which
|
||||
// is how SDL and everything else that speaks ALSA reaches this device.
|
||||
#define SNDCTL_DSP_SETFRAGMENT 0xC004500A // int in/out: count<<16 | log2(size)
|
||||
#define SNDCTL_DSP_GETFMTS 0x8004500B // int out: AFMT_* mask
|
||||
#define SNDCTL_DSP_GETCAPS 0x8004500F // int out: DSP_CAP_*
|
||||
#define SNDCTL_DSP_SETTRIGGER 0x40045010 // int in: PCM_ENABLE_OUTPUT or 0
|
||||
#define SNDCTL_DSP_GETOPTR 0x800C5012 // struct count_info out
|
||||
#define SNDCTL_DSP_GETODELAY 0x80045017 // int out: bytes not yet heard
|
||||
|
||||
#define DSP_CAP_TRIGGER 0x00001000
|
||||
#define PCM_ENABLE_OUTPUT 0x00000002
|
||||
|
||||
struct count_info {
|
||||
int bytes; // played since the last reset
|
||||
int blocks; // fragments played since the last GETOPTR
|
||||
int ptr; // where playback is in a ring of fragsize * fragments
|
||||
};
|
||||
|
||||
#define AFMT_S16_LE 0x00000010
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,13 @@ void *hid_attach(const uchar *desc, int n, const char *what);
|
|||
// One input report, report ID included when the device uses them.
|
||||
void hid_attached_input(void *h, const uchar *rep, int len);
|
||||
|
||||
// For GMMOS, after a panic: whether a device is a keyboard, and one of its
|
||||
// reports rewritten into the 8-byte boot layout (modifiers, reserved, six
|
||||
// usages) with nothing else done -- no queue, no lock, no events. 0 when the
|
||||
// report was not a keyboard's.
|
||||
int hid_is_keyboard(void *h);
|
||||
int hid_boot_report(void *h, const uchar *rep, int len, uchar *boot);
|
||||
|
||||
// I2C-HID: the touchpads (and some keyboards) of laptops. Started once ACPI
|
||||
// and kernel threads are up; see kernel/drivers/i2c/i2c_hid.c.
|
||||
void i2c_hid_start(void);
|
||||
|
|
|
|||
|
|
@ -59,6 +59,9 @@
|
|||
#define KBD_PGDN (KBD_SPECIAL + 0x07)
|
||||
#define KBD_INS (KBD_SPECIAL + 0x08)
|
||||
#define KBD_DEL (KBD_SPECIAL + 0x09)
|
||||
// F1..F12, consecutive: KBD_F1 + n is F(n+1).
|
||||
#define KBD_F1 (KBD_SPECIAL + 0x10)
|
||||
#define KBD_F12 (KBD_SPECIAL + 0x1B)
|
||||
|
||||
// Switch virtual terminal.
|
||||
//
|
||||
|
|
@ -89,12 +92,10 @@
|
|||
// leaves `chvt N` as the only way to switch, which is a legitimate choice on
|
||||
// a machine whose host takes everything.
|
||||
//
|
||||
// The default is AltGr+F1..F6, and the "AltGr wants it" line above needs
|
||||
// reading carefully: what a Polish layout wants is AltGr+*letter*. AltGr with
|
||||
// a function key produces nothing on any layout, no window manager binds it,
|
||||
// and no application asks for it. Right Alt being a separate modifier bit
|
||||
// here is what makes the distinction expressible at all -- it used to share
|
||||
// ALT, which is harmless for typing and useless for this.
|
||||
// The default is Ctrl+Alt+F1..F12: the keys X switches with, so the console
|
||||
// and the desktop answer the same chord. It used to be AltGr+F-key, which
|
||||
// nothing else claims -- Right Alt being a separate modifier bit here is what
|
||||
// makes that expressible, and `chvt -c altgr+F` still sets it.
|
||||
#define KBD_VT_SWITCH (KBD_SPECIAL + 0x20)
|
||||
|
||||
// Previous / next terminal, on the same modifiers as the chord above. The
|
||||
|
|
|
|||
46
include/dev/kdev.h
Normal file
46
include/dev/kdev.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// The devices the kernel implements itself, as /sys and udev see them.
|
||||
//
|
||||
// /dev holds them already (kernel/fs/devfs.c). This is the other half of what
|
||||
// udev needs: for each node a place in /sys/devices, the class and dev/ links
|
||||
// that lead back to it, the facts udev's rules read (MAJOR, MINOR, DEVNAME,
|
||||
// input capabilities), and a way to announce it on the uevent netlink socket.
|
||||
#pragma once
|
||||
|
||||
#include "core/types.h"
|
||||
|
||||
#define KDEV_NAME 24
|
||||
#define KDEV_DEVNAME 32
|
||||
#define KDEV_BLK0 256 // kdev indexes from here are block devices
|
||||
|
||||
enum { KD_NONE, KD_KEYBOARD, KD_MOUSE };
|
||||
|
||||
struct kdev {
|
||||
char subsys[16]; // class: input, mem, tty, graphics, block
|
||||
char name[KDEV_NAME]; // directory name under the parent
|
||||
char devname[KDEV_DEVNAME]; // path below /dev, "" for a device with no node
|
||||
short major, minor;
|
||||
int block;
|
||||
int parent; // kdev index of the device it hangs off, or -1
|
||||
int cap; // KD_*: what an input device can do
|
||||
};
|
||||
|
||||
int kdev_nsubsys(void);
|
||||
const char *kdev_subsys(int s);
|
||||
|
||||
// Index of the Nth device that exists, 0.. until -1.
|
||||
int kdev_nth(int n);
|
||||
int kdev_get(int i, struct kdev *k);
|
||||
// The numbers stat() shows for its node, which are what /sys must show too.
|
||||
void kdev_numbers(const struct kdev *k, int *maj, int *min);
|
||||
|
||||
// "devices/virtual/input/input0/event0": where it lives below /sys.
|
||||
int kdev_path(int i, char *buf, int cap);
|
||||
|
||||
// Announce a device on the uevent socket: action is "add", "remove", "change".
|
||||
void kdev_uevent(int i, const char *action);
|
||||
// The next SEQNUM: events from every source (kdev, PCI, USB) share one count.
|
||||
uint kdev_seqnum(void);
|
||||
// What reading a device's `uevent` file says.
|
||||
int kdev_uevent_text(int i, char *buf, int cap);
|
||||
|
|
@ -151,7 +151,7 @@ struct pci_driver {
|
|||
void (*attached)(struct pci_dev *, struct device *);
|
||||
// Give the device up, so a userspace driver can take it.
|
||||
//
|
||||
// docs/plan/hybrid.md is built on the kernel *handing over* a device
|
||||
// docs/architecture/devices.md is built on the kernel *handing over* a device
|
||||
// rather than a process wrestling it away: two drivers writing one set of
|
||||
// registers is not a subtle failure, it is hardware that appears to have
|
||||
// started lying. So the transfer is the incumbent's decision, and this is
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//
|
||||
// /dev/pci -- device access from userspace.
|
||||
//
|
||||
// docs/plan/hybrid.md, mechanisms 2 and 4: MMIO and DMA to a process. This is
|
||||
// docs/architecture/devices.md, mechanisms 2 and 4: MMIO and DMA to a process. This is
|
||||
// the interface a driver that does not live in the kernel talks to, and it is
|
||||
// deliberately about *PCI devices*, not about any particular driver model.
|
||||
//
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
//
|
||||
// There is no IOMMU. A process holding a DMA buffer can point the device at
|
||||
// any physical address, so this isolates a driver against its own *bugs* and
|
||||
// not against its own *intent*. docs/plan/hybrid.md says so at length and it
|
||||
// not against its own *intent*. docs/architecture/devices.md says so at length and it
|
||||
// remains true here: a crashed driver that restarts instead of taking the
|
||||
// machine down is the whole benefit, and it is not a security boundary.
|
||||
|
||||
|
|
|
|||
42
include/dev/pty.h
Normal file
42
include/dev/pty.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
// Pseudo-terminals, Unix98 style: open /dev/ptmx and get the master side of a
|
||||
// new pair; the slave is /dev/pts/N. What sits between the two is the same
|
||||
// line discipline a console has. See kernel/dev/pty.c.
|
||||
|
||||
#include "core/types.h"
|
||||
|
||||
#define PTMX_MAJOR 13 // /dev/ptmx: opening it allocates a pair
|
||||
#define PTM_MAJOR 14 // a master, minor = pair number; never in a listing
|
||||
#define PTS_MAJOR 15 // /dev/pts/N
|
||||
#define NPTY 32
|
||||
|
||||
// A controlling terminal that is a pty, in proc->ctty: PTY_CTTY + N. The
|
||||
// console's are 1..NVT.
|
||||
#define PTY_CTTY 64
|
||||
|
||||
struct inode;
|
||||
struct file;
|
||||
|
||||
int pty_alloc(void); // the new pair's number, or -1
|
||||
int pty_used(int n);
|
||||
int pty_ioctl(struct inode *ip, uint request, addr_t arg);
|
||||
int pty_poll(struct inode *ip, int events, int readable, int writable);
|
||||
struct inode *ptmx_open(struct inode *ip); // ip locked; returns the master, locked
|
||||
void pty_init(void);
|
||||
|
||||
// Lines the devsw table needs.
|
||||
int ptm_read(struct inode*, uint64, char*, int);
|
||||
int ptm_write(struct inode*, uint64, char*, int);
|
||||
int ptm_ready(struct inode*);
|
||||
void ptm_open(struct inode*);
|
||||
void ptm_close(struct inode*);
|
||||
int pts_read(struct inode*, uint64, char*, int);
|
||||
int pts_write(struct inode*, uint64, char*, int);
|
||||
int pts_ready(struct inode*);
|
||||
void pts_open(struct inode*);
|
||||
void pts_close(struct inode*);
|
||||
|
||||
// From kernel/fs/devfs.c: the inode of master N.
|
||||
struct inode *devfs_pty_master(int n);
|
||||
void devfs_pty_claim(int n, uint uid);
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
//
|
||||
// It lived next to the driver while exactly one driver read it, with a note
|
||||
// saying it should move when a second reader appeared. One has: cmd/vgpud is
|
||||
// the same device driven from userspace (docs/plan/hybrid.md's first
|
||||
// the same device driven from userspace (docs/architecture/devices.md's first
|
||||
// eviction), and it needs the same register offsets and the same commands.
|
||||
//
|
||||
// Still one file rather than transport-and-protocol split in two. Both readers
|
||||
|
|
|
|||
|
|
@ -26,10 +26,10 @@
|
|||
#include "core/param.h"
|
||||
#include "uapi/posix.h"
|
||||
|
||||
// Six, and Linux's numbering: /dev/tty1 through /dev/tty6, reached with
|
||||
// Alt+1..Alt+6. A seventh is deliberately not bound -- that is where a
|
||||
// display server goes, and Schcom will want it.
|
||||
#define NVT 6
|
||||
// Twelve, one per function key, and Linux's numbering: /dev/tty1 through
|
||||
// /dev/tty12, reached with Ctrl+Alt+F1..F12 (and plain Alt on the text
|
||||
// console). The key codes leave room for sixteen; see KBD_VT_SWITCH.
|
||||
#define NVT 12
|
||||
|
||||
// A cell.
|
||||
//
|
||||
|
|
@ -82,7 +82,7 @@ struct vtcell {
|
|||
// Escape parser states. E_OSC and E_OSC_ESC exist for hyperlinks: a terminal
|
||||
// that does not follow OSC 8 must still consume it, or the URL is printed as
|
||||
// text. See the comment in kernel/core/console.c that this came from.
|
||||
enum { VE_NORMAL, VE_ESC, VE_CSI, VE_OSC, VE_OSC_ESC };
|
||||
enum { VE_NORMAL, VE_ESC, VE_CSI, VE_OSC, VE_OSC_ESC, VE_CHARSET };
|
||||
|
||||
struct vt {
|
||||
int index; // 0-based; /dev/tty<index+1>
|
||||
|
|
@ -98,6 +98,10 @@ struct vt {
|
|||
// CR, LF, backspace, an escape sequence -- clears it.
|
||||
int wrap_pending;
|
||||
int saved_cx, saved_cy; // DECSC/DECRC
|
||||
// The scrolling region, DECSTBM: rows scroll_top..scroll_bot inclusive.
|
||||
// scroll_bot 0 means unset, the whole screen -- which keeps a grid resized
|
||||
// underneath it from holding a stale bottom.
|
||||
int scroll_top, scroll_bot;
|
||||
|
||||
uint8 fg, bg; // current SGR colours
|
||||
int bold;
|
||||
|
|
@ -108,6 +112,16 @@ struct vt {
|
|||
int esc_parm[VT_MAXPARM];
|
||||
int esc_nparm;
|
||||
int esc_private;
|
||||
int esc_qmark; // the marker was '?': a DEC private mode
|
||||
|
||||
// The text cursor, a steady block. Drawn by painting its cell inverted over
|
||||
// the grid, so the grid never holds it: curs_drawn says whether the screen
|
||||
// currently does, and where, because erasing it means repainting that cell
|
||||
// from the grid -- after the byte that moves the cursor has been consumed.
|
||||
// DECTCEM (ESC [ ? 25 l) hides it; curses programs do before drawing.
|
||||
int curs_hidden;
|
||||
int curs_drawn;
|
||||
int curs_x, curs_y;
|
||||
|
||||
// UTF-8 decoder.
|
||||
uint32 u8_cp;
|
||||
|
|
|
|||
|
|
@ -130,6 +130,13 @@ struct inode {
|
|||
uint64 size;
|
||||
|
||||
uint rwmode;
|
||||
// POSIX permission bits from a filesystem that keeps them, or 0 for one
|
||||
// that does not, in which case stat makes some up from the type. Bit 18 set
|
||||
// alone means a mode of 0000, which chmod can ask for.
|
||||
uint mode;
|
||||
// Owner. Whatever the filesystem keeps: SchFS has stored them all along,
|
||||
// FAT and the synthetic filesystems report root.
|
||||
uint uid, gid;
|
||||
uint supermode;
|
||||
uint showmode;
|
||||
uint atime;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue