Compare commits

...
Author SHA1 Message Date
Nexoniarz
e43f56c619 kernel: laptop touchpads over I2C-HID
A laptop's touchpad is on the chipset's I2C bus, found only through ACPI
(PNP0C50 / ACPI0C50), and SchOS had no I2C at all, so the touchpad of
every recent laptop did nothing.

kernel/acpi/i2c_hid.c drives the DesignWare I2C controller every x86
chipset uses (AMDI0010 on Ryzen) by polling, speaks HID over I2C to the
device the _DSM points at, and feeds the reports through the USB HID
parser. On AMD the interrupt pin's level is read every tick, so a report
is read exactly when one is waiting; elsewhere the input register is polled.

The touchpad is driven in mouse mode, which it starts in after a reset:
motion, clicks and taps become /dev/input/event1 like any mouse. The HID
parser now prefers a descriptor's relative pointer over an absolute one
for this, and takes descriptors from transports other than USB.

Untested on hardware: QEMU emulates no I2C touchpad.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 21:54:33 +02:00
Nexoniarz
4a7165c951 X gets a VT: keyboard, terminal switching, and a way out
Xorg uses VTs only when stat("/dev/tty0") reports Linux's tty major 4.
SchOS's terminals sit under major 1, so X ran on no VT at all: the console
kept drawing over it, Ctrl+Alt+F-keys did nothing, and nothing ended the
session. stat now reports terminals under Linux's numbers.

startx picks the VT only when uname says Linux, so root's xserverrc passes
the current one itself. An xorg.conf.d file names X's input devices, since
Alpine's Xorg looks them up through udev and SchOS has none yet.

Alt+F1..F6 and Ctrl+Alt+F1..F6 now switch terminals as on Linux, alongside
the settable chord. USB keyboards gain arrows, Home/End/PgUp/PgDn/Ins/Del
and the switch keys, which the PS/2 path always had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 21:45:50 +02:00
Nexoniarz
df1394eaa3 SCFF: the live root ships compressed, with Xorg and Window Maker
SCFF, the SchOS Compressed File Format (include/fs/scff.h), is a block
image cut into 128 KB chunks, each its own zstd frame, behind an index.
tools/mkscff writes it from fs-ram.img on every core (-19, 11 s) and
checks each chunk decompresses to itself. The kernel carries the .scff
as its ramdisk and unpacks a chunk the first time a block in it is
read; zero blocks share one page. The kernel and the ISO are the
compressed size, and the root costs memory only for what is touched.
See docs/architecture/scff.md.

zstd 1.5.7 comes from ports/zstd like uACPI: the decoder is compiled
into the kernel at -O2, with a static context and a 64 KB stack of its
own, and mkscff builds the whole library from the same release.

world now has xorg-server, the fbdev and evdev drivers, xinit, Window
Maker, xterm and DejaVu, and /root/.xinitrc runs wmaker. That root is
358 MB, 102 MB as SCFF.

The ISO no longer carries the root a third time as an ISO 9660 tree,
which nothing read. schos.iso: 1389 MB -> 318 MB. El Torito keeps its
own copy of the EFI image, the layout real machines have booted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 21:17:31 +02:00
Nexoniarz
1ba93dde2c kernel: syscalls get the whole kernel stack
syscall_entry put the frame at kstack + 4096 - 16, from when a kernel
stack was one page. It has been two since, so every syscall ran in the
bottom 4 KB while traps used the top, and a deep enough read -- startx
exec'ing through the new compressed ramdisk -- ran over the canary at
kstack[0]. The frame now sits at the top of KSTACKSIZE, where allocproc
and the TSS already put it, and exec's user-rsp slot moves with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 21:17:02 +02:00
Nexoniarz
8351e0323b kernel: HTTPS through wget, half-close, and /proc/<pid>/fd
busybox wget hands TLS to ssl_client over a socketpair and then does
shutdown(SHUT_WR). That cut the AF_UNIX link both ways, so ssl_client's
reply was a SIGPIPE and every https:// download hung. SHUT_WR is now a
half-close: the peer reads EOF and can still answer. TCP sockets get
shutdown() at all (tcp_shutdown for the FIN); it used to do nothing.

- exec keeps ignored signals and the signal mask, as execve(2) says.
- /proc/<pid>/fd: a link per descriptor -- the path it was opened by,
  socket:[n], pipe:[n], anon_inode:[eventpoll]. /proc/self too. The
  global inode bit moves above pid << 8; at 0x10000 it was /proc/256.
- fstat on a socket or pipe answers S_IFSOCK / S_IFIFO instead of EINVAL.
- poll, select and epoll wake as soon as a pipe, socket or input device
  changes, not on the next tick.
- Pipes are 64 KB of lazily allocated pages, copied in blocks, with
  PIPE_BUF atomicity: 300 MB through `yes | head | wc` in 7 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 20:28:03 +02:00
Nexoniarz
906318f2b6 X on the console: /dev/tty0, a palette, and startx that works
Xorg finds its VT through /dev/tty0, and without one ran on none: the
console went on drawing into the framebuffer over the X screen. tty0 is
the terminal on screen, as on Linux. FBIOPUTCMAP succeeds, as it does on
a true-colour Linux framebuffer, instead of 256 logged errors per start.

schos-base gives root a .xserverrc with -dumbSched, Xorg's switch for
input on the main loop: SchOS has no threads, and Xorg crashed in
pthread_join on its input thread when the first client left. It adds
the hostname to /etc/hosts, so xauth can resolve schos:0.

zsh loses PROMPT_SP: on a serial terminal narrower than the console its
padding wrapped and left a # above every prompt. bootcheck knows zsh's
prompt and matches without colour codes, so busybox's ls reads as text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 19:44:10 +02:00
Nexoniarz
a4ed556543 kernel: timers that fire, raw ping, and the sockets X needs
alarm() and setitimer() stored a countdown nothing counted down, so no
timer has ever fired; setitimer and getitimer were also registered with
no Linux number. alarmd now posts SIGALRM on the tick, while any process
has a timer armed. busybox's ping, /bin/ping on the Alpine root, sends
every request after the first from its SIGALRM handler.

It also opens SOCK_RAW, which was EPROTOTYPE. A raw ICMP socket is now a
ping socket whose reads start at the IP header. And a blocked socket
call gives up for a signal with a handler, not only for a kill: the
alarm woke recvfrom, and the wait put it straight back to sleep.

For Xorg: AF_UNIX abstract names, which read as the empty path and so as
EADDRINUSE ("server already running"); and getsockname and getpeername,
which did not exist, so libxcb sent no cookie and the server refused
every client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 19:44:10 +02:00
Nexoniarz
d0941780f5 ramdisk: the live root grows into half of memory
The live root was a fixed 96 MB image in the kernel: 60 MB of zeros on
the ISO, twice, and room for 60 MB of packages whatever the machine had.

Now the image is only what the root holds, plus slack, measured by
building it once large. Past its end the ramdisk goes on to half of the
memory free at mount, as pages taken the first time each block is
written; an unwritten block reads as zeros and costs nothing. SchFS
keeps no free-space map on disk, so growing a volume is a larger total
before open builds the map: grow_to, set for the ramdisk root.

8 GB machine: a 4 GB root, Xorg installed into it in 77 s with 7.6 GB
still free. The ISO is 172 MB instead of 344.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 17:20:43 +02:00
Nexoniarz
3f8e5190aa build: the apk root is the only root
make qemu and make iso went on building sysroot/ as the root while only
make aroot built the apk one. Both now come from the apk root: a 1 GB
schfs.img for make qemu, and a 96 MB ramdisk for the ISO, 35 MB of it
the base system and the rest room for apk add on the live system.
sysroot/ is what schos-base is packaged from, not a root of its own.

hwdata leaves the ramdisk exclude list: it is Alpine's package now, and
a root missing files apk's database lists is one apk has to repair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 17:06:11 +02:00
Nexoniarz
0f7bceeb50 shell.nix: no hardening flags
Nix's compiler wrapper adds -fPIC and friends to every compile. The
kernel is freestanding at fixed addresses, and clang refuses -fPIC for
the UEFI loader's PE target outright, so a plain make in nix-shell
stopped at BOOTX64.EFI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-24 15:28:32 +02:00
52 changed files with 2761 additions and 202 deletions

150
Makefile
View file

@ -101,7 +101,19 @@ KERN_ACPI = $(addprefix $(BUILD)/uacpi/, \
namespace.o notify.o opcodes.o opregion.o osi.o registers.o \
resources.o shareable.o sleep.o stdlib.o tables.o types.o \
uacpi.o utilities.o)
KERN_ACPI += $(addprefix $(BUILD)/kernel/acpi/, host.o aml.o)
KERN_ACPI += $(addprefix $(BUILD)/kernel/acpi/, host.o aml.o i2c_hid.o)
# zstd's decoder, for SCFF -- the compressed live root (include/fs/scff.h).
# Unpacked by ports/zstd into build/ports/zstd/src and compiled into the kernel
# like uACPI; kernel/scff/ is the host side of it.
ZSTDDIR = $(BUILD)/ports/zstd/src/lib
KERN_ZSTD = $(addprefix $(BUILD)/zstd/, \
common/entropy_common.o common/error_private.o \
common/fse_decompress.o common/zstd_common.o common/xxhash.o \
common/debug.o decompress/huf_decompress.o \
decompress/zstd_ddict.o decompress/zstd_decompress.o \
decompress/zstd_decompress_block.o)
KERN_ZSTD += $(BUILD)/kernel/scff/scff.o
KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \
proc.o)
@ -114,7 +126,7 @@ 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_PROC) $(KERN_SYS)
$(KERN_FS) $(KERN_NET) $(KERN_ACPI) $(KERN_ZSTD) $(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))
@ -392,6 +404,7 @@ endif
# looks satisfied.
MKFS = $(BUILD)/tools/mkfs$(EXE)
MKSCHFS = $(BUILD)/tools/mkschfs$(EXE)
MKSCFF = $(BUILD)/tools/mkscff$(EXE)
SCHFS_TEST = $(BUILD)/tools/schfs_test$(EXE)
SCHFS_OBJT = $(BUILD)/tools/schfs_objtest$(EXE)
SCHFS_FSCK = $(BUILD)/tools/schfs_fsck$(EXE)
@ -431,7 +444,7 @@ CFLAGS += $(NO_SSP) $(CFLAGS_EXTRA)
# copies and memset into %xmm, and an interrupt then scribbles over the
# registers of the program it interrupted. Kernel objects only -- cmd/ is
# built with these CFLAGS too and is welcome to use SSE.
$(BUILD)/kernel/%.o $(BUILD)/lwip/%.o $(BUILD)/uacpi/%.o: CFLAGS += -mgeneral-regs-only
$(BUILD)/kernel/%.o $(BUILD)/lwip/%.o $(BUILD)/uacpi/%.o $(BUILD)/zstd/%.o: CFLAGS += -mgeneral-regs-only
# lwIP's own headers, plus ours (cc.h and lwipopts.h live in kernel/net/lwip).
# Only the network files are compiled with these: putting them in the global
@ -461,14 +474,14 @@ CFLAGS_SYS = $(CFLAGS) -I./kernel/sys
# loads that by PIO one sector at a time -- a kernel with a userland inside it
# takes visibly longer to boot than a 350 KB one.
#
# 8000 blocks of 4 KB is 32 MB. It was 6000, which the userland had quietly
# grown to fill: 5157 of 6000 used, 86%, after toybox gained its pending
# applets and an editor arrived. That still fit, and the *next* thing added
# would not have -- and what mkschfs says when an image is full is "try a
# larger --size", which is a poor way to find out that a build variant you were
# not using has been broken for a while.
# Sized to fit, not fixed. The image is what the root uses plus RAMDISK_SLACK
# blocks and an eighth, measured by building it once large; the kernel then
# grows the volume into half of the free memory at boot (kernel/dev/ramdisk.c),
# which is where `apk add` on the live system writes. It was a fixed 96 MB,
# 60 of it zeros carried in the kernel and twice over on the ISO, and room for
# 60 MB of packages whatever the machine had.
# -----------------------------------------------------------------------
RAMDISK_BLOCKS ?= 8000
RAMDISK_SLACK ?= 1024
# RAMDISK_NINODES used to live here, sizing mkfs's preallocated inode table.
# The ramdisk is a SchFS image now (see $(BUILD)/fs-ram.img) and SchFS allocates
@ -486,10 +499,10 @@ RAMDISK_BLOCKS ?= 8000
# This is not hypothetical: a 33 MB test payload once landed here and overflowed
# the 6 MB image, and mkfs's "increase FSSIZE" message then pointed at a constant
# that had nothing to do with it.
# usb.ids and pci.ids are 2 MB of a 6 MB image and nothing needs them to boot.
# They are lookup tables for userspace tools, so they stay on the disk image
# and out of the kernel binary.
RAMDISK_EXCLUDE = usr/share/hwdata
# usb.ids and pci.ids used to be on it, as 2 MB of a 6 MB image. They are
# Alpine's hwdata packages now: apk owns them, and a root missing files its
# database lists would be one apk had to repair.
RAMDISK_EXCLUDE =
RAMDISK_EXCLUDE_ARGS = $(foreach p,$(RAMDISK_EXCLUDE),--exclude $(p))
ifdef RAMROOT
@ -519,6 +532,17 @@ $(shell mkdir -p $(BUILD); \
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
# 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.
# -----------------------------------------------------------------------
AROOT_DIR = $(BUILD)/aroot
ROOT_TREE = $(AROOT_DIR)/root
ROOT_STAMP = $(AROOT_DIR)/stamp
# -----------------------------------------------------------------------
# Top-level targets
# -----------------------------------------------------------------------
@ -757,9 +781,9 @@ iso:
$(MAKE) RAMROOT=1 schos.iso
# One El Torito entry, pointing at a FAT image holding the loader and kernel.
# sysroot-install because the disc also carries the tree as a real hierarchy.
schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI tools/posix/mkiso.sh sysroot-install
SYSROOT=$(SYSROOT) sh tools/posix/mkiso.sh $@
# The root is in the kernel, as its ramdisk; the disc carries no other copy.
schos.iso: $(BUILD)/kernel/kernel $(BUILD)/uefi/BOOTX64.EFI tools/posix/mkiso.sh $(ROOT_STAMP)
sh tools/posix/mkiso.sh $@
# A partitioned disk -- GPT, an ESP and a SchFS partition -- which is what a
# real machine boots from: dd it to a USB stick or a disk. See
@ -816,7 +840,7 @@ $(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
$(OBJDUMP) -S $@ > $(BUILD)/kernel/kernel.asm
# The ramdisk root image, and the object wrapping it for the linker. Built from
# the same staged sysroot as the main image, just capped at RAMDISK_BLOCKS --
# the same staged sysroot as the main image, sized to fit (see RAMDISK_SLACK) --
# mkfs's content dedup means the whole userland still fits in a few MB.
# Built with mkschfs, not mkfs.
#
@ -825,19 +849,33 @@ $(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
# volume on the root device" and made `make iso` and `make ramroot` unbootable.
# RAMDISK_NINODES is gone with it: SchFS has no preallocated inode table, which
# is what that knob existed to size.
$(BUILD)/fs-ram.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
$(BUILD)/fs-ram.img: $(MKSCHFS) $(ROOT_STAMP)
@mkdir -p $(BUILD)
@rm -f $@
$(MKSCHFS) $@ --size $(RAMDISK_BLOCKS) --label schos \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(SYSROOT)
@used=$$($(MKSCHFS) $@ --size 262144 --label schos \
$(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)
$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.img
# 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
# is read. -19 is slow to write and costs nothing to read -- zstd decodes at
# the same speed whatever level made the frame -- and mkscff uses every core.
SCFF_LEVEL ?= 19
SCFF_CHUNK ?= 128
$(BUILD)/fs-ram.scff: $(BUILD)/fs-ram.img $(MKSCFF)
$(MKSCFF) $< $@ --level $(SCFF_LEVEL) --chunk $(SCFF_CHUNK)
$(BUILD)/kernel/ramdisk_obj: $(BUILD)/fs-ram.scff
@mkdir -p $(BUILD)/kernel
$(LD) -r -b binary -m elf_x86_64 -o $@ $<
$(OBJCOPY) \
--redefine-sym _binary_build_fs_ram_img_start=_binary_ramdisk_start \
--redefine-sym _binary_build_fs_ram_img_end=_binary_ramdisk_end \
--redefine-sym _binary_build_fs_ram_img_size=_binary_ramdisk_size \
--redefine-sym _binary_build_fs_ram_scff_start=_binary_ramdisk_start \
--redefine-sym _binary_build_fs_ram_scff_end=_binary_ramdisk_end \
--redefine-sym _binary_build_fs_ram_scff_size=_binary_ramdisk_size \
$@
# The console font, linked into the kernel.
@ -1003,6 +1041,30 @@ $(UACPIDIR):
$(patsubst $(BUILD)/uacpi/%.o,$(UACPIDIR)/source/%.c,$(filter $(BUILD)/uacpi/%,$(KERN_ACPI))): | $(UACPIDIR)
@:
# zstd at -O2 whatever OPT says: the decoder runs on every first read of a
# root block, and at -O0 it is several times slower. No assembly, no BMI2
# dispatch, no legacy formats, no tracing hooks -- the kernel has none of
# what those need. compat/ stands in for the libc headers it includes.
ZSTD_CFLAGS = -Ikernel/scff/compat -I$(ZSTDDIR) -I$(ZSTDDIR)/common -O2 \
-DZSTD_DISABLE_ASM -DDYNAMIC_BMI2=0 -DZSTD_LEGACY_SUPPORT=0 \
-DZSTD_TRACE=0 -DDEBUGLEVEL=0 -DZSTD_STRIP_ERROR_STRINGS \
-DXXH_NAMESPACE=ZSTD_
ZSTD_CC = $(CC) $(filter-out -I./include,$(CFLAGS)) $(ZSTD_CFLAGS)
$(BUILD)/zstd/%.o: $(ZSTDDIR)/%.c | $(ZSTDDIR)
@mkdir -p $(dir $@)
$(ZSTD_CC) -Wno-unused-function -c $< -o $@
$(BUILD)/kernel/scff/%.o: kernel/scff/%.c | $(ZSTDDIR)
@mkdir -p $(dir $@)
$(ZSTD_CC) -c $< -o $@
$(ZSTDDIR):
$(MAKE) -C ports zstd
$(patsubst $(BUILD)/zstd/%.o,$(ZSTDDIR)/%.c,$(filter $(BUILD)/zstd/%,$(KERN_ZSTD))): | $(ZSTDDIR)
@:
$(BUILD)/kernel/proc/%.o: kernel/proc/%.c
@mkdir -p $(BUILD)/kernel/proc
$(CC) $(CFLAGS) -c $< -o $@
@ -1206,6 +1268,14 @@ $(SCHFS_FSCK): tools/schfs_fsck.c $(SCHFS_SRC)
$(HOSTCC) $(HOSTCFLAGS) -o $@ tools/schfs_fsck.c
mkschfs: $(MKSCHFS)
# The SCFF writer, built from the same zstd release as the kernel's decoder.
MKSCFF_ZSTD = $(wildcard $(ZSTDDIR)/common/*.c $(ZSTDDIR)/compress/*.c $(ZSTDDIR)/decompress/*.c)
$(MKSCFF): tools/mkscff.c include/fs/scff.h | $(ZSTDDIR)
@mkdir -p $(BUILD)/tools
$(HOSTCC) -O2 -pthread -iquote ./include -I$(ZSTDDIR) -DZSTD_DISABLE_ASM \
-DZSTD_LEGACY_SUPPORT=0 -o $@ tools/mkscff.c \
$(ZSTDDIR)/common/*.c $(ZSTDDIR)/compress/*.c $(ZSTDDIR)/decompress/*.c
schfs-fsck: $(SCHFS_FSCK)
# End-to-end filesystem test: build an image from a directory tree, read it
@ -1548,12 +1618,15 @@ qemu-console: esp $(BUILD)/schfs.img $(OVMF_CODE) $(OVMF_VARS)
# is "try a larger --size", which is a worse thing to read than a number here
# is to change. A raw disk image is sparse on any filesystem worth the name,
# so the unused part costs nothing until it is used.
SCHFS_BLOCKS ?= 25600
#
# 262144 blocks, 1 GB, since the root became one apk installs into: Xorg alone
# is 270 MB. Sparse, so the empty part costs nothing on the host.
SCHFS_BLOCKS ?= 262144
$(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
$(BUILD)/schfs.img: $(MKSCHFS) $(ROOT_STAMP)
@mkdir -p $(BUILD)
@rm -f $@
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(SYSROOT)
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(ROOT_TREE)
# --- The apk-managed root ------------------------------------------------------
#
@ -1561,14 +1634,11 @@ $(BUILD)/schfs.img: $(MKSCHFS) $(BUILD)/sysroot.stamp
# apk package schos-base -- a root that `apk add` and `apk upgrade` manage.
# See tools/posix/mkaroot.sh for what goes where and why.
#
# make aroot build/aroot.img, a SchFS root volume of AROOT_BLOCKS
# make qemu-aroot boot it as the root disk (the disk-root kernel)
# make aroot the tree and build/schfs.img; `make qemu` boots it
# make alpine-lock re-resolve ports/alpine/world against Alpine's mirror;
# the one step that looks for newer packages
#
# The build itself is offline once dist/ has the files world.lock names.
AROOT_BLOCKS ?= 262144
AROOT_DIR = $(BUILD)/aroot
AROOT_KEYS = $(BUILD)/apk-keys
APK_STATIC = $(BUILD)/ports/alpine/apk.static
ALPINE_MINI = dist/alpine-minirootfs-3.24.3-x86_64.tar.gz
@ -1585,21 +1655,9 @@ $(AROOT_DIR)/stamp: $(BUILD)/sysroot.stamp ports/alpine/world.lock tools/posix/m
$(AROOT_KEYS) $(SCHOS_PKGVER) ports/alpine/world.lock dist/alpine
touch $@
$(BUILD)/aroot.img: $(MKSCHFS) $(AROOT_DIR)/stamp
@rm -f $@
./$(MKSCHFS) $@ --size $(AROOT_BLOCKS) --label schos --sysroot $(AROOT_DIR)/root
aroot: $(BUILD)/schfs.img
aroot: $(BUILD)/aroot.img
qemu-aroot: esp $(BUILD)/aroot.img $(OVMF_CODE) $(OVMF_VARS)
$(QEMU) -machine pc-i440fx-9.2 \
-drive if=pflash,format=raw,unit=0,readonly=on,file=$(OVMF_CODE) \
-drive if=pflash,format=raw,unit=1,file=$(OVMF_VARS) \
-drive file=fat:rw:$(BUILD)/esp,index=0,media=disk,format=raw \
-drive file=$(BUILD)/aroot.img,format=raw,index=1,media=disk \
-m 2G -smp 2 $(QEMU_ACCEL) -serial mon:stdio $(QEMU_DISPLAY) $(QEMU_INPUT) $(QEMU_NET) $(QEMU_AUDIO)
.PHONY: alpine-port alpine-lock aroot qemu-aroot
.PHONY: alpine-port alpine-lock aroot
# Boot with a SchFS volume attached as the second disk.
#

40
NOTICE
View file

@ -151,6 +151,46 @@ interpreter, used for PCI interrupt routing, the power button and power-off.
The host layer written for SchOS (kernel/acpi/) is GPL-2.0-or-later and is not
part of uACPI; uACPI's own sources are unmodified.
--------------------------------------------------------------------------------
zstd 1.5.7 BSD 3-Clause (or GPL-2.0)
--------------------------------------------------------------------------------
BSD License
For Zstandard software
Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook, nor Meta, nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Fetched by ports/zstd. Its decoder is compiled into the kernel to read SCFF,
the compressed live root (docs/architecture/scff.md), and tools/mkscff builds
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.
--------------------------------------------------------------------------------
Intel 8254x device identifiers BSD 3-Clause
--------------------------------------------------------------------------------

79
docs/architecture/scff.md Normal file
View file

@ -0,0 +1,79 @@
# SCFF: the SchOS Compressed File Format
The live root is shipped compressed. `make iso` builds the root as a SchFS
image, `tools/mkscff` writes that image as SCFF, and the kernel carries the
`.scff` file as its ramdisk. Blocks are decompressed the first time something
reads them.
| | raw image | SCFF |
|---|---|---|
| root with Xorg and Window Maker | 358 MB | 102 MB |
| kernel and ISO | carry all 358 MB | carry 102 MB |
| memory the root takes at boot | 358 MB | what has been read |
## Layout
The definition is `include/fs/scff.h`, which the kernel and `mkscff` both
use.
offset 0 struct scff_header (64 bytes, "SCFF", version 1)
index_off uint64 off[nchunks + 1]
off[i]..off[i+1] chunk i
The image is cut into chunks of `chunk_bytes`, 128 KB by default. Each chunk
is stored in one of three ways, and its stored length says which:
- **Zero bytes:** every byte of the chunk is zero. The slack at the end of the
image costs nothing.
- **The chunk's full length:** stored as it is, because it did not compress.
- **Anything else:** one zstd frame, with a content checksum.
## Why it is shaped like this
**Chunks, not one stream.** Each chunk can be decompressed without the others,
so reading one file does not mean decompressing the root. This costs a little
ratio: the 358 MB root is 104 MB in 128 KB chunks and 97 MB in 1 MB chunks.
128 KB is squashfs's default too.
**A block image, not a compressed filesystem.** squashfs compresses files and
is read-only, so a writable root needs an overlay filesystem above it. SCFF
compresses a SchFS image instead, and the ramdisk is still a block device that
takes writes. A block that is written gets a page of its own, exactly as it
did before SCFF. SchFS does not know the image was ever compressed.
**zstd.** It compresses close to xz at the levels a build can afford (`-19`).
It decompresses at memory speed whatever level made the frame. Its decoder is
dual BSD and GPL-2.0, and it is a plain C library. `ports/zstd` pins the
release. The kernel compiles only the decoder half. `mkscff` compiles all of
it from the same release, so neither side depends on the host's libzstd.
## In the kernel
`kernel/dev/ramdisk.c` reads the header at boot, checks it and the index, and
panics if either is wrong. A damaged image is found before anything mounts.
The ramdisk's page directory covers every block. A block with no page has not
been unpacked yet. Reading one decompresses its whole chunk into `rd_chunk`,
and every block in that chunk without a page gets one. A block of zeros points
at one shared zero page and gets a real page only when it is written.
`kernel/scff/scff.c` holds the decoder: one static zstd context in a 256 KB
workspace, so nothing allocates. **Decompression runs on a 64 KB stack of its
own.** A kernel stack is 8 KB, and a chunk is decompressed at the bottom of a
read that has already used most of it. zstd's sequence decoder needs several
KB of locals, and Linux gives it a 16 KB stack. The chunk lock (`rd_zlock`)
makes that stack its holder's alone. `kernel/scff/compat/` stands in for the
few libc headers zstd includes.
zstd is built at `-O2` whatever `OPT` says, because it runs on every first
read of a block.
## Building
make iso builds build/fs-ram.scff and links it in
make iso SCFF_LEVEL=3 much faster to build, about a third bigger
make iso SCFF_CHUNK=256 bigger chunks: a little smaller, more work per miss
`mkscff` uses every core. At `-19` the root takes about 11 seconds on 16
threads. It decompresses every chunk again before writing it, so an image
that would not boot fails the build instead.

View file

@ -38,10 +38,12 @@ only what is its own (see `tools/posix/mkaroot.sh`):
## Building
make aroot build/aroot.img: the root, as a SchFS volume
make qemu-aroot boot it
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.
@ -66,8 +68,13 @@ All of it found by running apk:
## Next
- The live ISO and USB stick are still the ramdisk root. They need the apk
root on the stick itself, or an installer that writes it to a disk.
- Xorg from apk (`xorg-server xf86-video-fbdev xf86-input-evdev`) instead of
the hand port in [xorg.md](xorg.md). The packages install; running them is
the next test.
- 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.

View file

@ -22,6 +22,12 @@ setopt EXTENDED_GLOB
setopt INTERACTIVE_COMMENTS # a # in an interactive line is a comment
setopt NO_BEEP # SchOS has no audio, but it would also scroll
unsetopt FLOW_CONTROL # ^S/^Q are worth more as keys than as flow control
# No PROMPT_SP. It marks output that did not end in a newline by printing a
# `#` and then padding to $COLUMNS, trusting the terminal to wrap so the next
# CR can erase it. The console is mirrored to the serial port, and a host
# terminal narrower than the SchOS screen wraps the padding early: every
# prompt came with a `#` line above it.
unsetopt PROMPT_SP
# ---------------------------------------------------------------------------
# Keys

View file

@ -368,6 +368,7 @@ void input_mouse(int dx, int dy, int buttons);
void ramdiskinit(void);
void ramdiskrw(struct buf*);
int ramdisk_present(void);
uint64 ramdisk_blocks(void);
// usb_storage.c
void usb_storage_rw(struct buf*);
@ -491,6 +492,10 @@ int sockbind(struct socket*, const char *path, uint dev, uint64 ino)
int socklisten(struct socket*, int backlog);
struct socket* sockaccept(struct socket*);
int sockconnect(struct socket*, uint dev, uint64 ino);
int sockbind_abstract(struct socket*, uint64 key);
int sockname_unix(struct socket*, int peer, char *path);
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*);
int sock_shutdown(struct socket*, int how);
@ -522,12 +527,18 @@ void* inet_accept(void *h);
int inet_connect(void *h, uint addr, ushort port);
int inet_read(void *h, char *buf, int n);
int inet_write(void *h, char *buf, int n);
int inet_name(void *h, int peer, uint *addr, ushort *port);
int inet_recvfrom(void *h, char *buf, int n,
uint *addr, ushort *port);
int inet_sendto(void *h, char *buf, int n,
uint addr, ushort port);
int inet_readable(void *h);
int inet_hup(void *h);
int inet_shutdown(void *h, int how);
int file_describe(struct file *f, char *buf, int cap);
int proc_fd_mask(int pid, uint64 mask[2]);
int proc_fd_link(int pid, int fd, char *buf, int cap);
const char *ev_kind_name(struct evobj *o);
void inet_close(void *h);
// proc.c
@ -543,6 +554,11 @@ int proc_pgid(int pid);
int proc_setpgid(int pid, int pgid);
int proc_setsid(void);
int sigsend(int pid, int sig);
void alarm_arm(struct proc *p, uint after, uint every);
uint alarm_left(struct proc *p);
void alarm_thread_start(void);
void poll_kick(void);
int poll_wait_tick(void);
int sig_default_kills(int sig);
int proc_exists(int);
int proc_num(void);

View file

@ -231,7 +231,11 @@ struct proc {
uint64 signal_mask;
uint64 pending_signals;
int alarm_ticks;
// ITIMER_REAL, which alarm() is the whole-second form of: SIGALRM is due at
// tick alarm_at (0 = not armed) and then every alarm_every ticks after.
// 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

17
include/dev/hid.h Normal file
View file

@ -0,0 +1,17 @@
#pragma once
#include "core/types.h"
// HID over transports other than USB. The report parser and the keyboard and
// pointer paths are kernel/drivers/usb/hid/usb_hid.c's; a transport hands
// over the report descriptor once and every input report after it.
// The device described by `desc`, or 0 if it is neither a keyboard nor a
// pointer. `what` names it in the log.
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);
// 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);

View file

@ -25,6 +25,11 @@ struct file {
int memfd_seg;
struct evobj *ev;
uint64 off;
// What /proc/<pid>/fd shows. `ino` names the open thing (one per socket,
// one per pipe); `path` is the absolute path an inode was opened by, as
// written -- symlinks along it are not resolved. kmalloc'd, or 0.
uint64 ino;
char *path;
struct file *fnext; // file.c's free list, while ref is 0
};

54
include/fs/scff.h Normal file
View file

@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// SCFF, the SchOS Compressed File Format: a block image cut into chunks, each
// compressed on its own, with an index in front.
//
// It is what the live root is shipped as. The kernel carries the compressed
// image and decompresses a chunk the first time any block in it is read, so
// the kernel and the ISO are the compressed size, and the memory the root
// takes is what has been touched, not the whole of it. See kernel/dev/ramdisk.c
// for the reader and tools/mkscff.c for the writer; this header is the one
// definition of the layout both of them use.
//
// offset 0 struct scff_header
// index_off uint64_t off[nchunks + 1], from the start of the file
// off[i]..off[i+1] chunk i
//
// A chunk is chunk_bytes of the image (the last one may be shorter). Its
// stored length says how it is kept:
//
// 0 every byte is zero, and nothing is stored
// its full length stored as it is: it did not compress
// anything else one zstd frame
//
// Little-endian throughout, which is every machine SchOS runs on.
#ifndef SCHOS_FS_SCFF_H
#define SCHOS_FS_SCFF_H
#include <stdint.h>
#define SCFF_MAGIC "SCFF"
#define SCFF_VERSION 1
#define SCFF_CODEC_ZSTD 1
// The kernel decompresses one chunk at a time into a buffer this big, so it
// is the largest chunk a reader accepts. 128 KB is squashfs's default and
// the point past which a bigger chunk buys little ratio for a lot of
// decompression on every miss.
#define SCFF_CHUNK_MAX (1u << 20)
struct scff_header {
char magic[4]; // "SCFF"
uint16_t version; // SCFF_VERSION
uint16_t codec; // SCFF_CODEC_ZSTD
uint32_t chunk_bytes; // a power of two, a multiple of 4096, <= SCFF_CHUNK_MAX
uint32_t nchunks;
uint64_t image_bytes; // the uncompressed image
uint64_t index_off;
uint64_t stored_bytes; // the whole file, header and index included
uint8_t reserved[24];
};
_Static_assert(sizeof(struct scff_header) == 64, "scff_header is 64 bytes");
#endif

View file

@ -19,6 +19,7 @@
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602
#define FBIOPUTCMAP 0x4605
#define FBIOBLANK 0x4611
#define FBIOPAN_DISPLAY 0x4606

View file

@ -330,6 +330,9 @@ struct socket {
// bidirectional connection.
struct socket *peer;
int peer_closed;
// shutdown(SHUT_WR): this end has said all it will. The peer reads EOF but
// can still answer, which is what a request/response helper depends on.
int wr_shut;
char data[SOCKBUF];
uint nread, nwrite;

635
kernel/acpi/i2c_hid.c Normal file
View file

@ -0,0 +1,635 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Laptop touchpads: HID over I2C, on the SoC's DesignWare I2C controller.
//
// A laptop's touchpad is not on PS/2 any more and not on USB either. It hangs
// off an I2C bus inside the chipset, and the only place that says so is ACPI:
// a device whose _HID or _CID is PNP0C50, with an I2cSerialBus resource
// naming the controller and the touchpad's address, a GpioInt naming the pin
// it pulls when it has something to say, and a _DSM giving the one register
// number the protocol needs to bootstrap itself. Without this the touchpad of
// every recent laptop -- the ASUS TUF A15's included -- does nothing at all.
//
// Three layers, all here because each is small:
//
// The controller. Every x86 chipset since Haswell, AMD's included, uses the
// same Synopsys DesignWare I2C block (AMDI0010 on Ryzen, INT33C3, INT3433
// and the PCI LPSS functions on Intel). Driven by polling: a touchpad report
// is about 30 bytes, a millisecond on the bus.
//
// HID over I2C (Microsoft's specification, version 1.0). Read the HID
// descriptor from the register _DSM names; power the device on and reset
// it; read the report descriptor; then every input report is a read of the
// input register, two bytes of length and the report.
//
// The interrupt line. It is level-triggered and stays asserted for as long
// as a report is waiting, so instead of routing it through the GPIO
// controller's interrupt the pin's *level* is read, every tick: low means
// read a report. That is exact -- no report is read twice and none is
// missed -- and needs nothing from the GPIO controller but its registers.
// On AMD those are one 32-bit word per pin. Elsewhere there is no pin to
// watch, and the input register is simply read every tick; devices answer
// a read with nothing waiting with a zero length.
//
// The reports themselves go through the USB HID driver's parser and pointer
// path (include/dev/hid.h). A Windows Precision Touchpad starts in mouse mode
// after a reset -- relative motion, a button for a click and for a tap -- and
// that is the report taken: it is what makes it a mouse on /dev/input/event1,
// for the console and for X. Multi-finger reports (two-finger scroll) need the
// device switched to touchpad mode and a gesture layer; not yet.
#include "core/types.h"
#include "core/defs.h"
#include "core/param.h"
#include "core/spinlock.h"
#include "arch/mmu.h"
#include "dev/hid.h"
#include "dev/acpi.h"
#include <uacpi/uacpi.h>
#include <uacpi/utilities.h>
#include <uacpi/namespace.h>
#include <uacpi/resources.h>
// ---------------------------------------------------------------------------
// DesignWare I2C
// ---------------------------------------------------------------------------
#define IC_CON 0x00
#define IC_TAR 0x04
#define IC_DATA_CMD 0x10
#define IC_SS_SCL_HCNT 0x14
#define IC_SS_SCL_LCNT 0x18
#define IC_FS_SCL_HCNT 0x1C
#define IC_FS_SCL_LCNT 0x20
#define IC_INTR_MASK 0x30
#define IC_RAW_INTR_STAT 0x34
#define IC_RX_TL 0x38
#define IC_TX_TL 0x3C
#define IC_CLR_INTR 0x40
#define IC_CLR_TX_ABRT 0x54
#define IC_CLR_STOP_DET 0x60
#define IC_ENABLE 0x6C
#define IC_STATUS 0x70
#define IC_TXFLR 0x74
#define IC_RXFLR 0x78
#define IC_SDA_HOLD 0x7C
#define IC_TX_ABRT_SOURCE 0x80
#define IC_ENABLE_STATUS 0x9C
#define IC_COMP_PARAM_1 0xF4
#define IC_COMP_TYPE 0xFC
#define DW_COMP_TYPE 0x44570140
#define CON_MASTER (1 << 0)
#define CON_SPEED_STD (1 << 1)
#define CON_SPEED_FAST (2 << 1)
#define CON_RESTART_EN (1 << 5)
#define CON_SLAVE_DISABLE (1 << 6)
#define CMD_READ (1 << 8)
#define CMD_STOP (1 << 9)
#define CMD_RESTART (1 << 10)
#define INTR_TX_ABRT (1 << 6)
#define INTR_STOP_DET (1 << 9)
#define NCTL 4
struct dwi2c {
uint64 phys;
volatile uint32 *regs;
int txdepth, rxdepth;
int fast;
};
static struct dwi2c ctls[NCTL];
static int nctl;
static uint32
rd(struct dwi2c *c, int reg)
{
return c->regs[reg / 4];
}
static void
wr(struct dwi2c *c, int reg, uint32 v)
{
c->regs[reg / 4] = v;
}
static int
dw_enable(struct dwi2c *c, int on)
{
int i;
for(i = 0; i < 1000; i++){
wr(c, IC_ENABLE, on);
if((rd(c, IC_ENABLE_STATUS) & 1) == (uint32)on)
return 0;
microdelay(25);
}
return -1;
}
// SCL high and low periods, in controller clocks, from the firmware's own
// numbers when it gives them: SSCN and FMCN are packages of (HCNT, LCNT,
// SDA hold) for exactly this controller's clock.
static int
acpi_counts(uacpi_namespace_node *node, const char *name,
uint32 *hcnt, uint32 *lcnt, uint32 *hold)
{
uacpi_object *o = 0;
uacpi_object_array a;
uacpi_u64 v[3];
int i;
if(uacpi_eval_package(node, name, 0, &o) != UACPI_STATUS_OK)
return -1;
if(uacpi_object_get_package(o, &a) != UACPI_STATUS_OK || a.count < 2){
uacpi_object_unref(o);
return -1;
}
v[2] = 0;
for(i = 0; i < 3 && i < (int)a.count; i++)
if(uacpi_object_get_integer(a.objects[i], &v[i]) != UACPI_STATUS_OK){
uacpi_object_unref(o);
return -1;
}
uacpi_object_unref(o);
*hcnt = v[0];
*lcnt = v[1];
*hold = v[2];
return 0;
}
static int
dw_init(struct dwi2c *c, uacpi_namespace_node *node, uint32 speed)
{
uint32 type, param, hcnt, lcnt, hold = 0;
c->regs = ioremap(c->phys, 0x1000);
if(c->regs == 0)
return -1;
type = rd(c, IC_COMP_TYPE);
if(type != DW_COMP_TYPE){
cprintf("i2c: controller at 0x%lx is not DesignWare (0x%x)\n",
c->phys, type);
return -1;
}
param = rd(c, IC_COMP_PARAM_1);
c->txdepth = param ? ((param >> 16) & 0xFF) + 1 : 8;
c->rxdepth = param ? ((param >> 8) & 0xFF) + 1 : 8;
c->fast = speed >= 400000;
if(dw_enable(c, 0) < 0)
return -1;
wr(c, IC_CON, CON_MASTER | CON_SLAVE_DISABLE | CON_RESTART_EN |
(c->fast ? CON_SPEED_FAST : CON_SPEED_STD));
// Without the firmware's numbers, periods long enough for a 150 MHz clock,
// the fastest any of these runs at: on a slower clock the bus is merely
// slower than it could be, never out of specification.
if(c->fast){
if(acpi_counts(node, "FMCN", &hcnt, &lcnt, &hold) < 0){
hcnt = 135; // 0.6 us high, 1.3 us low, plus rise and fall
lcnt = 240;
}
wr(c, IC_FS_SCL_HCNT, hcnt);
wr(c, IC_FS_SCL_LCNT, lcnt);
} else {
if(acpi_counts(node, "SSCN", &hcnt, &lcnt, &hold) < 0){
hcnt = 645; // 4.0 us high, 4.7 us low, plus the same
lcnt = 750;
}
wr(c, IC_SS_SCL_HCNT, hcnt);
wr(c, IC_SS_SCL_LCNT, lcnt);
}
if(hold)
wr(c, IC_SDA_HOLD, hold);
wr(c, IC_INTR_MASK, 0);
wr(c, IC_RX_TL, 0);
wr(c, IC_TX_TL, 0);
rd(c, IC_CLR_INTR);
return 0;
}
// Write `wn` bytes to `addr`, then read `rn` bytes back after a repeated
// start, all as one transaction. 0, or -1 if the device did not answer.
//
// The command FIFO holds reads as well as writes, and each read becomes a
// byte in the receive FIFO, so reads are queued no faster than they are
// drained: a full receive FIFO stalls the bus.
static int
dw_xfer(struct dwi2c *c, int addr, const uchar *w, int wn, uchar *r, int rn)
{
int total = wn + rn, sent = 0, got = 0, spin;
uint32 raw;
if(dw_enable(c, 0) < 0)
return -1;
wr(c, IC_TAR, addr);
rd(c, IC_CLR_INTR);
if(dw_enable(c, 1) < 0)
return -1;
for(spin = 0; got < rn || sent < total; spin++){
raw = rd(c, IC_RAW_INTR_STAT);
if(raw & INTR_TX_ABRT)
goto abort;
while(got < rn && rd(c, IC_RXFLR) > 0)
r[got++] = rd(c, IC_DATA_CMD);
while(sent < total && (int)rd(c, IC_TXFLR) < c->txdepth){
uint32 cmd;
if(sent >= wn && (sent - wn) - got >= c->rxdepth)
break;
if(sent < wn)
cmd = w[sent];
else
cmd = CMD_READ | (sent == wn && wn > 0 ? CMD_RESTART : 0);
if(sent == total - 1)
cmd |= CMD_STOP;
wr(c, IC_DATA_CMD, cmd);
sent++;
}
if(spin > 20000) // 200 ms: no transfer of ours takes that long
goto abort;
microdelay(10);
}
for(spin = 0; !(rd(c, IC_RAW_INTR_STAT) & INTR_STOP_DET); spin++){
if(rd(c, IC_RAW_INTR_STAT) & INTR_TX_ABRT)
goto abort;
if(spin > 2000)
goto abort;
microdelay(10);
}
rd(c, IC_CLR_STOP_DET);
dw_enable(c, 0);
return 0;
abort:
rd(c, IC_TX_ABRT_SOURCE);
rd(c, IC_CLR_TX_ABRT);
rd(c, IC_CLR_INTR);
dw_enable(c, 0);
return -1;
}
// ---------------------------------------------------------------------------
// ACPI
// ---------------------------------------------------------------------------
struct found {
uint64 mem; // the first memory range
int i2c, addr;
uint32 speed;
char bus[64]; // the controller's namespace path
int gpio, pin, active_low;
char gpioctl[64];
};
static void
copy_source(char *dst, const uacpi_resource_source *s)
{
int n = s->string && s->length ? s->length : 0;
if(n > 63)
n = 63;
if(n)
memmove(dst, s->string, n);
dst[n] = 0;
}
static uacpi_iteration_decision
take_resource(void *user, uacpi_resource *r)
{
struct found *f = user;
switch(r->type){
case UACPI_RESOURCE_TYPE_FIXED_MEMORY32:
if(!f->mem) f->mem = r->fixed_memory32.address;
break;
case UACPI_RESOURCE_TYPE_MEMORY32:
if(!f->mem) f->mem = r->memory32.minimum;
break;
case UACPI_RESOURCE_TYPE_ADDRESS32:
if(!f->mem && r->address32.common.type == UACPI_RANGE_MEMORY)
f->mem = r->address32.minimum;
break;
case UACPI_RESOURCE_TYPE_ADDRESS64:
if(!f->mem && r->address64.common.type == UACPI_RANGE_MEMORY)
f->mem = r->address64.minimum;
break;
case UACPI_RESOURCE_TYPE_SERIAL_I2C_CONNECTION:
if(!f->i2c){
f->i2c = 1;
f->addr = r->i2c_connection.slave_address;
f->speed = r->i2c_connection.connection_speed;
copy_source(f->bus, &r->i2c_connection.common.source);
}
break;
case UACPI_RESOURCE_TYPE_GPIO_CONNECTION:
if(!f->gpio && r->gpio_connection.type == UACPI_GPIO_CONNECTION_INTERRUPT &&
r->gpio_connection.pin_table_length > 0){
f->gpio = 1;
f->pin = r->gpio_connection.pin_table[0];
f->active_low = r->gpio_connection.intr.polarity != UACPI_POLARITY_ACTIVE_HIGH;
copy_source(f->gpioctl, &r->gpio_connection.source);
}
break;
}
return UACPI_ITERATION_DECISION_CONTINUE;
}
static int
resources(uacpi_namespace_node *node, struct found *f)
{
memset(f, 0, sizeof(*f));
return uacpi_for_each_device_resource(node, "_CRS", take_resource, f) ==
UACPI_STATUS_OK ? 0 : -1;
}
// The I2C-HID _DSM, function 1: the HID descriptor's register.
static int
hid_desc_register(uacpi_namespace_node *node, uint32 *reg)
{
static const uchar uuid[16] = { // 3CDFF6F7-4267-4555-AD05-B30A3D8938DE
0xF7, 0xF6, 0xDF, 0x3C, 0x67, 0x42, 0x55, 0x45,
0xAD, 0x05, 0xB3, 0x0A, 0x3D, 0x89, 0x38, 0xDE,
};
uacpi_data_view view = { .const_bytes = uuid, .length = sizeof(uuid) };
uacpi_object_array none = { 0, 0 };
uacpi_object *args[4], *ret = 0;
uacpi_object_array a = { args, 4 };
uacpi_u64 v = 0;
int rc = -1, i;
args[0] = uacpi_object_create_buffer(view);
args[1] = uacpi_object_create_integer(1); // revision
args[2] = uacpi_object_create_integer(1); // function
args[3] = uacpi_object_create_package(none);
if(args[0] && args[1] && args[2] && args[3] &&
uacpi_eval(node, "_DSM", &a, &ret) == UACPI_STATUS_OK && ret &&
uacpi_object_get_integer(ret, &v) == UACPI_STATUS_OK){
*reg = v;
rc = 0;
}
if(ret)
uacpi_object_unref(ret);
for(i = 0; i < 4; i++)
if(args[i])
uacpi_object_unref(args[i]);
return rc;
}
static uacpi_namespace_node *
node_at(const char *path)
{
uacpi_namespace_node *n = 0;
if(path[0] == 0 ||
uacpi_namespace_node_find(0, path, &n) != UACPI_STATUS_OK)
return 0;
return n;
}
static struct dwi2c *
controller(const char *path, uint32 speed)
{
uacpi_namespace_node *node = node_at(path);
struct found f;
struct dwi2c *c;
int i;
if(node == 0 || resources(node, &f) < 0 || f.mem == 0){
cprintf("i2c: no registers for controller %s\n", path);
return 0;
}
for(i = 0; i < nctl; i++)
if(ctls[i].phys == f.mem)
return &ctls[i];
if(nctl == NCTL)
return 0;
uacpi_execute(node, "_PS0", 0);
c = &ctls[nctl];
memset(c, 0, sizeof(*c));
c->phys = f.mem;
if(dw_init(c, node, speed) < 0)
return 0;
nctl++;
cprintf("i2c: %s at 0x%lx, %s mode, FIFOs %d/%d\n", path, c->phys,
c->fast ? "fast" : "standard", c->txdepth, c->rxdepth);
return c;
}
// ---------------------------------------------------------------------------
// HID over I2C
// ---------------------------------------------------------------------------
#define NI2CHID 4
struct i2chid {
struct dwi2c *c;
int addr;
uint32 cmdreg, inreg, maxin;
volatile uint32 *pin; // the interrupt pin's register, or 0
int active_low;
void *hid;
uchar *buf; // one page, for input reports
char name[8];
};
static struct i2chid devs[NI2CHID];
static int ndev;
static int
reg_write(struct i2chid *d, const uchar *w, int n)
{
return dw_xfer(d->c, d->addr, w, n, 0, 0);
}
static int
reg_read(struct i2chid *d, uint32 reg, uchar *r, int n)
{
uchar w[2] = { reg & 0xFF, reg >> 8 };
return dw_xfer(d->c, d->addr, w, 2, r, n);
}
// SET_POWER and RESET, the two commands needed: written to the command
// register as (register, opcode byte, power state / report byte).
static int
command(struct i2chid *d, int opcode, int arg)
{
uchar w[4] = { d->cmdreg & 0xFF, d->cmdreg >> 8, arg, opcode };
return reg_write(d, w, 4);
}
#define OP_RESET 0x01
#define OP_SET_POWER 0x08
static void
wait_ticks(uint n)
{
acquire(&tickslock);
uint start = ticks;
while(ticks - start < n)
sleep(&ticks, &tickslock);
release(&tickslock);
}
// AMD's GPIO controller: one 32-bit register per pin, its level in bit 16.
#define AMD_PIN_STS (1u << 16)
static int
asserted(struct i2chid *d)
{
int high = (*d->pin & AMD_PIN_STS) != 0;
return d->active_low ? !high : high;
}
static volatile uint32 *
interrupt_pin(const struct found *f)
{
static const char *const amd[] = { "AMDI0030", "AMD0030", 0 };
uacpi_namespace_node *node = node_at(f->gpioctl);
struct found g;
if(!f->gpio || node == 0 || !uacpi_device_matches_pnp_id(node, amd))
return 0;
if(resources(node, &g) < 0 || g.mem == 0)
return 0;
return (volatile uint32 *)((char *)ioremap(g.mem, 0x1000) + f->pin * 4);
}
// One input report: two bytes of length, length included, then the report.
// A length of 0 is the device saying it has nothing, or acknowledging a
// reset.
static int
read_input(struct i2chid *d)
{
int len;
if(dw_xfer(d->c, d->addr, 0, 0, d->buf, d->maxin) < 0)
return -1;
len = d->buf[0] | (d->buf[1] << 8);
if(len <= 2 || len > (int)d->maxin)
return 0;
hid_attached_input(d->hid, d->buf + 2, len - 2);
return 1;
}
static uacpi_iteration_decision
probe(void *user, uacpi_namespace_node *node, uacpi_u32 depth)
{
struct i2chid *d;
struct found f;
uchar desc[30], *rdesc;
uint32 descreg, rlen, rreg;
int i;
(void)user;
(void)depth;
if(ndev == NI2CHID)
return UACPI_ITERATION_DECISION_BREAK;
d = &devs[ndev];
memset(d, 0, sizeof(*d));
memmove(d->name, uacpi_namespace_node_name(node).text, 4);
if(resources(node, &f) < 0 || !f.i2c){
cprintf("i2c-hid: %s has no I2C connection\n", d->name);
return UACPI_ITERATION_DECISION_CONTINUE;
}
if(hid_desc_register(node, &descreg) < 0){
cprintf("i2c-hid: %s: _DSM gave no descriptor register\n", d->name);
return UACPI_ITERATION_DECISION_CONTINUE;
}
if((d->c = controller(f.bus, f.speed)) == 0)
return UACPI_ITERATION_DECISION_CONTINUE;
d->addr = f.addr;
uacpi_execute(node, "_PS0", 0);
if(reg_read(d, descreg, desc, sizeof(desc)) < 0 ||
(desc[0] | desc[1] << 8) != 30){
cprintf("i2c-hid: %s at 0x%x does not answer\n", d->name, d->addr);
return UACPI_ITERATION_DECISION_CONTINUE;
}
rlen = desc[4] | desc[5] << 8;
rreg = desc[6] | desc[7] << 8;
d->inreg = desc[8] | desc[9] << 8;
d->maxin = desc[10] | desc[11] << 8;
d->cmdreg = desc[16] | desc[17] << 8;
if(d->maxin < 3 || d->maxin > PGSIZE || rlen == 0 || rlen > PGSIZE){
cprintf("i2c-hid: %s: unusable descriptor\n", d->name);
return UACPI_ITERATION_DECISION_CONTINUE;
}
d->pin = interrupt_pin(&f);
d->active_low = f.active_low;
command(d, OP_SET_POWER, 0); // 0 = on
wait_ticks(2);
command(d, OP_RESET, 0);
wait_ticks(10);
d->buf = (uchar *)kalloc();
rdesc = (uchar *)kalloc();
if(d->buf == 0 || rdesc == 0)
return UACPI_ITERATION_DECISION_BREAK;
// The reset is acknowledged with an empty report. Read it, and anything
// else already waiting, so the first real report is not behind it.
for(i = 0; i < 4 && (d->pin == 0 || asserted(d)); i++)
if(dw_xfer(d->c, d->addr, 0, 0, d->buf, d->maxin) < 0)
break;
if(reg_read(d, rreg, rdesc, rlen) < 0){
cprintf("i2c-hid: %s: report descriptor unreadable\n", d->name);
kfree((char *)rdesc);
return UACPI_ITERATION_DECISION_CONTINUE;
}
char what[24] = "i2c-hid ";
memmove(what + 8, d->name, 4);
d->hid = hid_attach(rdesc, rlen, what);
kfree((char *)rdesc);
if(d->hid == 0){
cprintf("i2c-hid: %s is neither a keyboard nor a pointer\n", d->name);
return UACPI_ITERATION_DECISION_CONTINUE;
}
cprintf("i2c-hid: %s: %04x:%04x at 0x%x, %s\n", d->name,
desc[20] | desc[21] << 8, desc[22] | desc[23] << 8, d->addr,
d->pin ? "interrupt pin watched" : "polled");
ndev++;
return UACPI_ITERATION_DECISION_CONTINUE;
}
static void
i2c_hid_thread(void)
{
static const char *const ids[] = { "PNP0C50", "ACPI0C50", 0 };
int i, n;
uacpi_find_devices_at(uacpi_namespace_root(), ids, probe, 0);
if(ndev == 0)
for(;;)
wait_ticks(1000000);
for(;;){
wait_ticks(1);
for(i = 0; i < ndev; i++){
struct i2chid *d = &devs[i];
if(d->pin == 0){
read_input(d);
continue;
}
// Everything waiting, up to a bound so that a stuck line cannot keep
// the other devices from their turn.
for(n = 0; n < 16 && asserted(d); n++)
if(read_input(d) < 0)
break;
}
}
}
void
i2c_hid_start(void)
{
if(acpi_rsdp_phys() == 0)
return;
if(kthread_create(i2c_hid_thread, "i2chid") < 0)
cprintf("i2c-hid: no thread; touchpads will not work\n");
}

View file

@ -1,3 +1,4 @@
#include "core/param.h" /* KSTACKSIZE */
# trapasm.S — kernel/user entry/exit with FS_BASE swapping for TLS
#
# The kernel uses __thread struct cpu *cpu and __thread struct proc *proc,
@ -209,7 +210,13 @@ syscall_entry:
movq %rax, %fs:(8) # save rax in per-CPU scratch slot
movq %gs:24, %rax # proc pointer (PERCPU_PROC)
movq 0x10(%rax), %rax # proc->kstack (offset 16)
addq $(4096-16), %rax # point to tf->rsp slot
# The top of the whole stack, where allocproc() put the trapframe and
# where a trap from user mode lands (TSS rsp0). This was $(4096-16) from
# when the stack was one page, and stayed that when it became two: every
# syscall ran in the bottom 4 KB with the top 4 KB unused, and a deep
# enough read -- startx exec'ing through a SCFF chunk unpack -- ran
# through the canary at kstack[0].
addq $(KSTACKSIZE-16), %rax # point to tf->rsp slot
movq %rsp, (%rax) # save user rsp
movq %rax, %rsp # switch to kernel stack
movq %fs:(8), %rax # restore rax

View file

@ -442,7 +442,8 @@ erase_one(struct vt *v)
// Keyboard input
// ---------------------------------------------------------------------------
// Ctrl+Alt+Shift+F1..F6 switch terminals. kbd.c reports them as
// Alt+F1..F6, Ctrl+Alt+F1..F6 and the settable chord (AltGr+F1..F6 by
// default) switch terminals. kbd.c reports them as
// KBD_VT_SWITCH+n so that the
// decision of what a chord *means* is made here, where the terminals are,
// rather than in a driver that should not know they exist.

View file

@ -12,6 +12,7 @@
#include "core/klog.h"
#include "dev/usb.h"
#include "dev/acpi.h"
#include "dev/hid.h"
#include "arch/vmap.h"
#include "core/hwinfo.h"
@ -391,6 +392,9 @@ kmain(uint64 fb_addr)
usb_hotplug_start();
// Where AML runs for the SCI: the power button, GPEs, notifications.
acpi_thread_start();
// Laptop touchpads, on the chipset's I2C bus; found through ACPI, so after
// the namespace is loaded, and polled from a thread of their own.
i2c_hid_start();
// The debugger's own thread, for the same reason netinit's comes after
// sinit: kthread_create needs a usable process table. From here on Ctrl-G
// on the console answers, with no filesystem mounted and nothing in
@ -400,6 +404,7 @@ kmain(uint64 fb_addr)
random_init();
netuser_init();
dbg_thread_start();
alarm_thread_start();
if (boot_verbose) cprintf("verbose boot: dbg thread started\n");
// The greeting is not printed here.

View file

@ -230,6 +230,13 @@ fb_ioctl(int request, addr_t arg)
// blanking does too, as Linux's own drivers without the hook do.
case FBIOBLANK:
return 0;
// A palette, on a framebuffer that is always true-colour. Linux's drivers
// keep the values for the console's sixteen colours and succeed; refusing
// made Xorg's fbdev driver log an error for every entry it loads, 256 of
// them at each start.
case FBIOPUTCMAP:
return 0;
}
return -ENOTTY;

View file

@ -156,6 +156,7 @@ input_sync(int dev)
return;
input_report(dev, EV_SYN, SYN_REPORT, 0);
wakeup(&idev[dev]);
poll_kick(); // Xorg polls its devices; a mouse moves at the next tick otherwise
}
int

View file

@ -152,6 +152,16 @@ kbdgetc(void)
if (data == SCAN_EQUALS) return KBD_VT_NEXT;
}
// And Linux's own: Alt+F1..F6 and Ctrl+Alt+F1..F6, whatever the chord is set
// to. These are what anyone who has used Linux presses, on a real machine,
// and nothing reached them: SchOS answered only AltGr+F-keys, which no one
// thinks to try. Under X the terminal is in VT_PROCESS, console_vt_hotkey()
// lets the keys through, and X switches with them itself -- as on Linux.
if (data >= SCAN_F1 && data < SCAN_F1 + NVT &&
((shift & (CTL | SHIFT | ALT | RALT)) == ALT ||
(shift & (CTL | SHIFT | ALT | RALT)) == (CTL | ALT)))
return KBD_VT_SWITCH + (data - SCAN_F1);
cp = keymap_lookup(data, shift);
if (cp == 0)
return 0;

View file

@ -14,6 +14,28 @@
//
// Enabled by building with RAMROOT=1. Without it this file still compiles, the
// device reports zero size, and the kernel boots from the IDE disk as before.
//
// The device is larger than the image. The image is only what the root holds,
// with a little slack; past its end the device goes on to half of the memory
// free at mount time, and those blocks are pages taken from kalloc the first
// time each is written -- a block nobody has written reads as zeros and costs
// nothing. SchFS grows the volume to the device's size when it mounts (see
// grow_to in kernel/fs/schfs/vol.c), so `apk add` on a live system has as
// much room as the machine has memory to give, rather than whatever empty
// space was built into the ISO. The ISO used to carry 60 MB of zeros for it.
//
// Pages are not given back when SchFS frees a block: there is no discard.
// A freed block is reused by the next write, so the cost is the high-water
// mark, not a leak.
//
// **A compressed image.** The blob can also be SCFF (include/fs/scff.h): the
// image cut into chunks, each compressed with zstd. Then no block is served in
// place. The page directory covers the image too, and a block with no page
// has not been unpacked yet: reading it decompresses its whole chunk, and
// every block of that chunk nobody has written gets a page of its own. The
// kernel and the ISO are the compressed size -- the 360 MB root with Xorg in
// it is 104 MB -- and the root costs memory only for what has been read or
// written. A block of zeros points at one shared zero page and costs nothing.
#include "core/types.h"
#include "core/defs.h"
@ -24,6 +46,8 @@
#include "core/sleeplock.h"
#include "fs/fs.h"
#include "fs/buf.h"
#include "fs/scff.h"
#include "../scff/scff_zstd.h"
#ifdef RAMDISK_ROOT
// Supplied by `ld -r -b binary` over build/fs-ram.img, renamed by objcopy.
@ -33,15 +57,63 @@ extern uchar _binary_ramdisk_size[];
static uchar *rd_base;
static uint64 rd_size; // bytes
static uint rd_blocks; // rd_size / BSIZE
static uint rd_blocks; // blocks in the image
static uint rd_first; // the first block with a page: rd_blocks, or 0 for SCFF
// SCFF only. rd_zlock is held across a chunk's decompression, and makes
// rd_chunk -- the one chunk-sized buffer -- its holder's.
static struct scff_header *rd_scff;
static uint64 *rd_index;
static struct sleeplock rd_zlock;
static uchar rd_chunk[SCFF_CHUNK_MAX] __attribute__((aligned(PGSIZE)));
static uchar rd_zero[BSIZE] __attribute__((aligned(PGSIZE)));
static uint64 rd_unpacked; // chunks decompressed
// rd_dir[i] is a page of pointers, entry j the page holding block
// rd_first + i*RD_PTRS + j, or 0 for one never written. 32768 pages of
// 512 pointers reach 64 GB, which is as far as half of any machine this runs
// on is likely to go, and costs 256 KB of pointers here.
#define RD_PTRS (PGSIZE / sizeof(uchar *))
#define RD_DIRS 32768
static uchar **rd_dir[RD_DIRS];
static uint64 rd_total; // blocks, image and the rest; 0 until sized
static uint64 rd_pages; // pages taken for blocks past the image
static struct spinlock rd_lock;
void
ramdiskinit(void)
{
initlock(&rd_lock, "ramdisk");
#ifdef RAMDISK_ROOT
rd_base = _binary_ramdisk_start;
rd_size = (uint64)_binary_ramdisk_size;
rd_blocks = (uint)(rd_size / BSIZE);
rd_first = rd_blocks;
if(rd_size >= sizeof(struct scff_header) && memcmp(rd_base, SCFF_MAGIC, 4) == 0){
struct scff_header *h = (struct scff_header *)rd_base;
uint64 chunks = h->chunk_bytes ? (h->image_bytes + h->chunk_bytes - 1) / h->chunk_bytes : 0;
if(h->version != SCFF_VERSION || h->codec != SCFF_CODEC_ZSTD ||
h->chunk_bytes < BSIZE || h->chunk_bytes > SCFF_CHUNK_MAX ||
(h->chunk_bytes & (h->chunk_bytes - 1)) || h->image_bytes % BSIZE ||
chunks != h->nchunks || h->stored_bytes > rd_size ||
h->index_off + (h->nchunks + 1) * sizeof(uint64) > rd_size)
panic("ramdiskinit: the SCFF header does not describe this image");
rd_index = (uint64 *)(rd_base + h->index_off);
for(uint i = 0; i < h->nchunks; i++)
if(rd_index[i] > rd_index[i + 1] || rd_index[i + 1] > h->stored_bytes ||
rd_index[i + 1] - rd_index[i] > h->chunk_bytes)
panic("ramdiskinit: the SCFF index is damaged");
if(scff_zstd_init() < 0)
panic("ramdiskinit: no room for the zstd decoder");
initsleeplock(&rd_zlock, "scff");
rd_scff = h;
rd_blocks = (uint)(h->image_bytes / BSIZE);
rd_first = 0;
cprintf("ramdisk: SCFF, %u MB in %u MB, %u chunks of %u KB\n",
(uint)(h->image_bytes >> 20), (uint)(rd_size >> 20),
h->nchunks, h->chunk_bytes >> 10);
}
if(rd_blocks == 0)
panic("ramdiskinit: embedded image is empty");
@ -51,8 +123,9 @@ ramdiskinit(void)
if(rd_blocks < 2)
panic("ramdiskinit: embedded image smaller than a superblock");
cprintf("ramdisk: %u blocks (%u KB) at %p\n",
rd_blocks, (uint)(rd_size / 1024), rd_base);
if(!rd_scff)
cprintf("ramdisk: %u blocks (%u KB) at %p\n",
rd_blocks, (uint)(rd_size / 1024), rd_base);
#else
rd_base = 0;
rd_size = 0;
@ -66,6 +139,115 @@ ramdisk_present(void)
return rd_blocks != 0;
}
// The device's size in blocks, fixed the first time anyone asks -- which is
// SchFS mounting the root, after every page has reached the allocator. Asked
// earlier, during ramdiskinit, the free count would be the first few
// megabytes kinit1 handed over and nothing more.
uint64
ramdisk_blocks(void)
{
if (rd_blocks == 0)
return 0;
acquire(&rd_lock);
if (rd_total == 0) {
uint64 extra = kalloc_free_bytes() / 2 / BSIZE;
uint64 room = (uint64)RD_DIRS * RD_PTRS - (rd_blocks - rd_first);
if (extra > room)
extra = room;
rd_total = rd_blocks + extra;
cprintf("ramdisk: %u MB image, %u MB of memory to grow into\n",
(uint)(rd_size >> 20), (uint)(extra * BSIZE >> 20));
}
release(&rd_lock);
return rd_total;
}
// The page behind a block, made when `make` says so. One that is only the
// shared zero page gets a page of its own before anything writes to it.
static uchar *
rd_page(uint64 blk, int make)
{
uint64 i = (blk - rd_first) / RD_PTRS, j = (blk - rd_first) % RD_PTRS;
uchar *pg = 0;
acquire(&rd_lock);
if (rd_dir[i] == 0 && make) {
if ((rd_dir[i] = (uchar **)kalloc()) != 0)
memset(rd_dir[i], 0, PGSIZE);
}
if (rd_dir[i]) {
pg = rd_dir[i][j];
if ((pg == 0 || pg == rd_zero) && make && (pg = (uchar *)kalloc()) != 0) {
if (rd_dir[i][j] == rd_zero)
memset(pg, 0, BSIZE);
rd_dir[i][j] = pg;
rd_pages++;
}
}
release(&rd_lock);
if (make && pg == 0)
panic("ramdisk: out of memory for the live root");
return pg;
}
// Give every block of SCFF chunk c that has no page one: its contents, or the
// zero page. Blocks already there -- written since boot, or unpacked by
// whoever held the lock before us -- are left as they are.
static void
rd_unpack(uint c)
{
uint per = rd_scff->chunk_bytes / BSIZE;
uint64 first = (uint64)c * per;
uint n = rd_blocks - first < per ? (uint)(rd_blocks - first) : per;
uint64 len = rd_index[c + 1] - rd_index[c];
uchar *src = rd_base + rd_index[c];
uint64 want = (uint64)n * BSIZE;
uchar *data = rd_chunk;
acquiresleep(&rd_zlock);
if (len == 0) {
data = 0; // all zeros
} else if (len == want) {
data = src; // stored as it is
} else {
long got = scff_zstd_decompress(rd_chunk, sizeof(rd_chunk), src, len);
if (got != (long)want)
panic("ramdisk: an SCFF chunk does not decompress");
rd_unpacked++;
}
for (uint k = 0; k < n; k++) {
uint64 blk = first + k, i = blk / RD_PTRS, j = blk % RD_PTRS;
uchar *b = data ? data + (uint64)k * BSIZE : 0;
int zero = 1;
if (b)
for (uint w = 0; w < BSIZE / 8 && zero; w++)
zero = ((uint64 *)b)[w] == 0;
uchar *pg = zero ? rd_zero : (uchar *)kalloc();
if (pg == 0)
panic("ramdisk: out of memory for the live root");
if (!zero)
memmove(pg, b, BSIZE);
acquire(&rd_lock);
if (rd_dir[i] == 0 && (rd_dir[i] = (uchar **)kalloc()) != 0)
memset(rd_dir[i], 0, PGSIZE);
if (rd_dir[i] == 0)
panic("ramdisk: out of memory for the live root");
if (rd_dir[i][j] == 0) {
rd_dir[i][j] = pg;
if (!zero)
rd_pages++;
pg = 0;
}
release(&rd_lock);
if (pg && pg != rd_zero)
kfree((char *)pg); // someone was there first
}
releasesleep(&rd_zlock);
}
// Sync buf with the in-memory image. Mirrors iderw()'s contract: on a read,
// fill b->data and set B_VALID; on a write, copy out and clear B_DIRTY.
void
@ -77,8 +259,25 @@ ramdiskrw(struct buf *b)
panic("ramdiskrw: nothing to do");
if(rd_base == 0)
panic("ramdiskrw: no ramdisk");
if(b->blockno >= rd_blocks)
panic("ramdiskrw: block out of range");
if(b->blockno >= rd_first){
if(b->blockno >= ramdisk_blocks())
panic("ramdiskrw: block out of range");
uchar *pg = rd_page(b->blockno, (b->flags & B_DIRTY) != 0);
if(pg == 0 && rd_scff && b->blockno < rd_blocks && !(b->flags & B_DIRTY)){
rd_unpack((uint)(b->blockno / (rd_scff->chunk_bytes / BSIZE)));
pg = rd_page(b->blockno, 0);
}
if(b->flags & B_DIRTY){
memmove(pg, b->data, BSIZE);
b->flags &= ~B_DIRTY;
} else if(pg){
memmove(b->data, pg, BSIZE);
} else {
memset(b->data, 0, BSIZE); // never written
}
b->flags |= B_VALID;
return;
}
uchar *p = rd_base + (uint64)b->blockno * BSIZE;

View file

@ -25,6 +25,9 @@
#include "core/dma.h"
#include "dev/usb.h"
#include "dev/input.h"
#include "dev/hid.h"
#include "dev/kbd.h"
#include "dev/vt.h"
#define NHID 8
@ -47,6 +50,11 @@ struct hid {
// report, for a mouse whose descriptor could not be used.
int boot;
int report_id; // 0 when reports carry no ID byte
// 1 + the only report ID to take pointer fields from, 0 for any; and
// 1 + the first report ID with a relative X. A touchpad describes both a
// mouse report and a multi-finger one with absolute coordinates, and only
// the mouse report makes sense as a pointer; see hid_attach().
int only_id1, rel_id1;
struct hfield x, y, wheel, btn[HID_NBTN];
// A keyboard in report protocol: where its keys are, so that each report
@ -106,7 +114,7 @@ static const uchar hid_shifted[0x39] = {
// which means the entire line discipline is reused rather than reimplemented.
#define KQSIZE 64
static struct {
uchar buf[KQSIZE];
int buf[KQSIZE]; // characters, and KBD_* codes past KBD_SPECIAL
uint r, w;
} kq;
@ -149,6 +157,32 @@ usb_kbd_getc(void)
}
// Was this usage in that report?
// The keys past the character table, as the console names them. The PS/2
// path has always produced these (kernel/dev/kbd.c); this one skipped every
// usage from 0x39 up, so on a USB keyboard the console had no arrows, no
// Home/End/PgUp/PgDn/Delete, and no way to switch terminals.
static int
hid_special(uchar usage, uchar mods)
{
// F1..F6 with Alt, AltGr or Ctrl+Alt: switch terminals, as on Linux.
if (usage >= 0x3A && usage < 0x3A + NVT && (mods & HID_MOD_ALT) &&
!(mods & HID_MOD_SHIFT))
return KBD_VT_SWITCH + (usage - 0x3A);
switch (usage) {
case 0x49: return KBD_INS;
case 0x4A: return KBD_HOME;
case 0x4B: return KBD_PGUP;
case 0x4C: return KBD_DEL;
case 0x4D: return KBD_END;
case 0x4E: return KBD_PGDN;
case 0x4F: return KBD_RT;
case 0x50: return KBD_LF;
case 0x51: return KBD_DN;
case 0x52: return KBD_UP;
}
return 0;
}
static int
was_down(const uchar *prev, uchar usage)
{
@ -222,18 +256,23 @@ keyboard_report(struct hid *h, const uchar *rep, int len)
for(i = 2; i < 8; i++){
uchar usage = rep[i];
if(usage == 0 || usage >= 0x39)
if(usage == 0)
continue;
if(was_down(h->prev, usage))
continue; // still held from last time, not a new press
c = (mods & HID_MOD_SHIFT) ? hid_shifted[usage] : hid_unshifted[usage];
if(c == 0)
continue;
if(usage >= 0x39){
if((c = hid_special(usage, mods)) == 0)
continue;
} else {
c = (mods & HID_MOD_SHIFT) ? hid_shifted[usage] : hid_unshifted[usage];
if(c == 0)
continue;
}
// Ctrl folds a letter down into its control character, which is how ^C
// and ^D reach the line discipline at all.
if(mods & HID_MOD_CTRL){
if((mods & HID_MOD_CTRL) && c < KBD_SPECIAL){
if(c >= 'a' && c <= 'z')
c = c - 'a' + 1;
else if(c >= 'A' && c <= 'Z')
@ -242,6 +281,8 @@ keyboard_report(struct hid *h, const uchar *rep, int len)
continue;
}
kq_put(c);
if(c >= KBD_VT_SWITCH && c < KBD_VT_SWITCH + NVT)
continue; // a held chord switches once
// The newest key is the one that repeats, as on any keyboard.
acquire(&kqlock);
@ -308,6 +349,10 @@ take_field(struct hid *h, uint32 usage, int bit, int size, int lmin, int lmax,
struct hfield *f = 0;
int page = usage >> 16, u = usage & 0xFFFF;
if(page == PAGE_DESKTOP && u == U_X && rel && h->rel_id1 == 0)
h->rel_id1 = id + 1;
if(h->only_id1 && id + 1 != h->only_id1)
return;
if(page == PAGE_DESKTOP && u == U_X) f = &h->x;
else if(page == PAGE_DESKTOP && u == U_Y) f = &h->y;
else if(page == PAGE_DESKTOP && u == U_WHEEL) f = &h->wheel;
@ -595,12 +640,8 @@ pointer_report(struct hid *h, const uchar *rep, int len)
// ---------------------------------------------------------------------------
static void
hid_complete(struct usb_dev *dev, void *buf, int len)
hid_input(struct hid *h, const uchar *buf, int len)
{
struct hid *h = dev->drvdata;
if(h == 0)
return;
// One interface can be both: a wireless receiver describes its keyboard
// and its mouse as two report IDs of one descriptor.
if(h->rkbd)
@ -611,6 +652,70 @@ hid_complete(struct usb_dev *dev, void *buf, int len)
pointer_report(h, buf, len);
}
static void
hid_complete(struct usb_dev *dev, void *buf, int len)
{
struct hid *h = dev->drvdata;
if(h != 0)
hid_input(h, buf, len);
}
static struct hid *
hid_slot(void)
{
int i;
if(!kqinit){
initlock(&kqlock, "usbkbd");
kqinit = 1;
}
for(i = 0; i < NHID; i++)
if(!hids[i].in_use){
memset(&hids[i], 0, sizeof(hids[i]));
return &hids[i];
}
return 0;
}
// A HID device on some other transport -- I2C, for a laptop's touchpad --
// handing over its report descriptor. Keyboards and pointers come out of it
// exactly as from USB. A descriptor with a relative pointer is driven
// through that report alone, even if an absolute one comes first: that is a
// touchpad's mouse report, which it sends until told to report fingers.
void *
hid_attach(const uchar *desc, int n, const char *what)
{
struct hid *h = hid_slot();
int rel;
if(h == 0)
return 0;
if(parse_report(h, desc, n) < 0)
return 0;
rel = h->rel_id1;
if(h->x.ok && !h->x.rel && rel){
memset(h, 0, sizeof(*h));
h->only_id1 = rel;
if(parse_report(h, desc, n) < 0)
return 0;
}
h->proto = !h->x.ok ? USB_HID_PROTO_KEYBOARD :
h->x.rel ? USB_HID_PROTO_MOUSE : USB_HID_PROTO_TABLET;
h->in_use = 1;
cprintf("%s: %s%s\n", what,
h->proto == USB_HID_PROTO_KEYBOARD ? "keyboard" :
h->proto == USB_HID_PROTO_MOUSE ? "pointer" : "absolute pointer",
h->rkbd && h->x.ok ? " and keyboard" : "");
return h;
}
void
hid_attached_input(void *h, const uchar *rep, int len)
{
hid_input(h, rep, len);
}
static int
hid_request(struct usb_dev *dev, int request, int value)
{
@ -686,18 +791,10 @@ hid_probe(struct usb_dev *dev)
cprintf("usb-hid: no interrupt IN endpoint; nothing to read\n");
return -1;
}
if(!kqinit){
initlock(&kqlock, "usbkbd");
kqinit = 1;
}
for(i = 0; i < NHID; i++)
if(!hids[i].in_use){ h = &hids[i]; break; }
if(h == 0){
if((h = hid_slot()) == 0){
cprintf("usb-hid: %d devices already; this one is not used\n", NHID);
return -1;
}
memset(h, 0, sizeof(*h));
if(boot && proto == USB_HID_PROTO_KEYBOARD){
// Ask for the fixed layout explicitly. A device that supports the boot

View file

@ -83,6 +83,11 @@ static const struct devnode devnodes[] = {
was one terminal. Minor 10 resolves through proc->ctty; see
vt_for_minor() in kernel/core/console.c. */
{ "tty", 1, 10, D_ROOT },
/* The terminal on screen, as on Linux. Xorg opens it to find a free VT and
switch to it, and without one ran on no VT at all: the console went on
drawing into the framebuffer over the X screen. Any minor that is not a
numbered terminal or the controlling one resolves to vt_current(). */
{ "tty0", 1, 0, D_ROOT },
{ "tty1", 1, 11, D_ROOT },
{ "tty2", 1, 12, D_ROOT },
{ "tty3", 1, 13, D_ROOT },

View file

@ -574,11 +574,15 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
// first delivered signal jump to a stale address in the new image.
// mmap_next likewise belongs to the old address space; left alone it
// could hand out mmap ranges overlapping the new image's layout.
// Ignored signals stay ignored, and the mask is inherited (execve(2)):
// neither points into the old image. `nohup` and every program that ignores
// SIGPIPE before running a helper depend on it.
memset(myproc()->vma, 0, sizeof(myproc()->vma));
memset(myproc()->sig_handler, 0, sizeof(myproc()->sig_handler));
for (int i = 0; i < 32; i++)
if (myproc()->sig_handler[i] != 1)
myproc()->sig_handler[i] = 0;
memset(myproc()->sig_restorer, 0, sizeof(myproc()->sig_restorer));
myproc()->fs_base = 0;
myproc()->signal_mask = 0;
myproc()->pending_signals = 0;
myproc()->in_signal = 0;
myproc()->sigsusp_active = 0;
@ -615,11 +619,11 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
myproc()->tf->rcx = entry;
myproc()->tf->rsp = sp;
// syscall_entry saves user RSP at kstack+PGSIZE-16 (not in the trapframe).
// syscall_entry saves user RSP at kstack+KSTACKSIZE-16 (not in the trapframe).
// sysretq reads RSP from that slot, so update it to the new stack pointer.
// Without this, sysretq returns with the old parent's RSP, causing lksh to
// start with an invalid/wrong stack pointer.
*(uint64*)((char*)myproc()->kstack + PGSIZE - 16) = sp;
*(uint64*)((char*)myproc()->kstack + KSTACKSIZE - 16) = sp;
// Userspace is running now, so the splash has done its job and the display
// becomes a terminal. Only the first exec does anything; every later one

View file

@ -24,6 +24,7 @@ struct {
struct file *free;
struct kmem_cache *more;
uint n;
uint64 serial; // the last ino handed out
} ftable;
void
@ -63,6 +64,8 @@ filealloc(void)
ftable.n++;
}
f->ref = 1;
f->ino = ++ftable.serial;
f->path = 0;
FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 1
// Cleared here rather than left to the caller: every creator sets
// readable/writable/off, but only open() sets nonblock, so a struct file
@ -101,12 +104,16 @@ fileclose(struct file *f)
lock_close_file(f);
ff = *f;
f->ref = 0;
f->path = 0;
f->type = FD_NONE;
f->fnext = ftable.free;
ftable.free = f;
FSSTAT_DROP(fsstat.file_used); // 1 -> 0
release(&ftable.lock);
if(ff.path)
kmfree(ff.path);
if(ff.type == FD_MEMFD)
shm_detach(ff.memfd_seg); // the last mapper frees the pages
else if(ff.type == FD_EVENT)
@ -129,6 +136,42 @@ fileclose(struct file *f)
}
}
// What /proc/<pid>/fd/N points at, in Linux's spelling: the path, or
// "socket:[ino]", "pipe:[ino]", "anon_inode:[eventpoll]". Returns the length.
// ftable.lock is what keeps `path` alive: fileclose() takes it from the file
// under the lock and frees it after.
int
file_describe(struct file *f, char *buf, int cap)
{
const char *pre = 0, *s = 0;
int n = 0;
acquire(&ftable.lock);
if(f->ref > 0){
switch(f->type){
case FD_INODE: s = f->path; if(!s) pre = "inode:["; break;
case FD_SOCKET: pre = "socket:["; break;
case FD_PIPE: pre = "pipe:["; break;
case FD_MEMFD: s = "/memfd: (deleted)"; break;
case FD_EVENT: s = ev_kind_name(f->ev); break;
default: break;
}
}
if(pre){
char num[24];
int k = 0;
uint64 v = f->type == FD_INODE && f->ip ? f->ip->inum : f->ino;
do { num[k++] = '0' + v % 10; v /= 10; } while(v);
while(*pre && n < cap) buf[n++] = *pre++;
while(k && n < cap) buf[n++] = num[--k];
if(n < cap) buf[n++] = ']';
} else if(s){
while(*s && n < cap) buf[n++] = *s++;
}
release(&ftable.lock);
return n;
}
int
filestat(struct file *f, struct stat *st)
{

View file

@ -11,24 +11,40 @@
#include "uapi/posix.h"
#include "uapi/errno.h"
#define PIPESIZE 512
// 64 KB, as on Linux, in pages taken the first time each is written.
//
// It was 512 bytes in the struct. That is xv6's size, and it made every pipe a
// handoff per half kilobyte: busybox's wget does TLS in a helper process that
// talks to it over pipes, a TLS record is 16 KB, and an HTTPS download that
// should have taken nine seconds was still going after a minute and a half. A
// shell pipeline that only ever moves a line still costs one page.
#define PIPESIZE 65536
#define PIPE_PAGES (PIPESIZE / PGSIZE)
#define PIPE_BUF 4096 // writes up to this size are not interleaved
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
char *page[PIPE_PAGES];
uint nread; // number of bytes read
uint nwrite; // number of bytes written
int readopen; // read fd is still open
int writeopen; // write fd is still open
};
// A pipe is about 560 bytes and used to come from kalloc(), which is a whole
// 4096-byte page -- so every `|` in a shell command burned three and a half
// kilobytes it never touched. This is what kmem_cache exists for, and the
// cache is created on first use rather than at boot so a kernel that never
// opens a pipe holds no page for one.
// The struct is small and comes from a cache; the buffer is pages. The cache
// is created on first use so a kernel that never opens a pipe holds no page
// for one.
static struct kmem_cache *pipe_cache;
static void
pipe_free(struct pipe *p)
{
for(int i = 0; i < PIPE_PAGES; i++)
if(p->page[i])
kfree(p->page[i]);
kmem_cache_free(pipe_cache, p);
}
int
pipealloc(struct file **f0, struct file **f1)
{
@ -42,6 +58,7 @@ pipealloc(struct file **f0, struct file **f1)
goto bad;
if((p = kmem_cache_alloc(pipe_cache)) == 0)
goto bad;
memset(p->page, 0, sizeof(p->page));
p->readopen = 1;
p->writeopen = 1;
p->nwrite = 0;
@ -55,6 +72,7 @@ pipealloc(struct file **f0, struct file **f1)
(*f1)->readable = 0;
(*f1)->writable = 1;
(*f1)->pipe = p;
(*f1)->ino = (*f0)->ino; // one pipe, as /proc/<pid>/fd names it
return 0;
//PAGEBREAK: 20
@ -81,9 +99,10 @@ pipeclose(struct pipe *p, int writable)
}
if(p->readopen == 0 && p->writeopen == 0){
release(&p->lock);
kmem_cache_free(pipe_cache, p);
pipe_free(p);
} else
release(&p->lock);
poll_kick(); // a poller sees the hangup now, not at the tick
}
//PAGEBREAK: 40
@ -108,8 +127,12 @@ pipewrite(struct pipe *p, char *addr, int n)
return -EPIPE;
}
for(i = 0; i < n; i++){
while(p->nwrite == p->nread + PIPESIZE){ //DOC: pipewrite-full
// A write of PIPE_BUF or less goes in whole or waits for room, so two
// writers' records never interleave; a larger one goes in as room appears.
uint want_room = (n <= PIPE_BUF) ? (uint)n : 1;
i = 0;
while(i < n){
while(PIPESIZE - (p->nwrite - p->nread) < want_room){ //DOC: pipewrite-full
if(p->readopen == 0){
release(&p->lock);
sigsend(myproc()->pid, SIGPIPE);
@ -120,12 +143,32 @@ pipewrite(struct pipe *p, char *addr, int n)
return -1;
}
wakeup(&p->nread);
release(&p->lock);
poll_kick();
acquire(&p->lock);
if(PIPESIZE - (p->nwrite - p->nread) >= want_room)
break;
sleep(&p->nwrite, &p->lock); //DOC: pipewrite-sleep
}
p->data[p->nwrite++ % PIPESIZE] = addr[i];
// One run: up to the end of this page, of what is left, of the room.
uint off = p->nwrite % PIPESIZE;
uint run = PGSIZE - off % PGSIZE;
uint room = PIPESIZE - (p->nwrite - p->nread);
if(run > (uint)(n - i)) run = (uint)(n - i);
if(run > room) run = room;
char **pg = &p->page[off / PGSIZE];
if(*pg == 0 && (*pg = kalloc()) == 0){
release(&p->lock);
return i > 0 ? i : -ENOMEM;
}
memmove(*pg + off % PGSIZE, addr + i, run);
p->nwrite += run;
i += run;
want_room = 1;
}
wakeup(&p->nread); //DOC: pipewrite-wakeup1
release(&p->lock);
poll_kick();
return n;
}
@ -142,13 +185,19 @@ piperead(struct pipe *p, char *addr, int n)
}
sleep(&p->nread, &p->lock); //DOC: piperead-sleep
}
for(i = 0; i < n; i++){ //DOC: piperead-copy
if(p->nread == p->nwrite)
break;
addr[i] = p->data[p->nread++ % PIPESIZE];
i = 0;
while(i < n && p->nread != p->nwrite){ //DOC: piperead-copy
uint off = p->nread % PIPESIZE;
uint run = PGSIZE - off % PGSIZE;
if(run > (uint)(n - i)) run = (uint)(n - i);
if(run > p->nwrite - p->nread) run = p->nwrite - p->nread;
memmove(addr + i, p->page[off / PGSIZE] + off % PGSIZE, run);
p->nread += run;
i += run;
}
wakeup(&p->nwrite); //DOC: piperead-wakeup
release(&p->lock);
poll_kick(); // a writer polling for room
return i;
}
@ -168,8 +217,8 @@ pipe_poll(struct pipe *p, int readable, int writable)
if(writable){
if(p->readopen == 0)
rev |= POLLERR;
else if(p->nwrite < p->nread + PIPESIZE)
rev |= POLLOUT;
else if(PIPESIZE - (p->nwrite - p->nread) >= PIPE_BUF)
rev |= POLLOUT; // room for a whole PIPE_BUF write, as on Linux
}
release(&p->lock);
return rev;

View file

@ -43,18 +43,22 @@
// identity of the object.
//
// 1 the root, /proc
// 2 /proc/self
// PROCFS_GLOBAL | id a global file, id being the PROC_* minor that
// kernel/dev/procfs.c already uses
// pid << 8 the directory /proc/<pid>
// (pid << 8) | fileid a file inside it
// (pid << 8) | 0x80 | fd /proc/<pid>/fd/<fd>
//
// pid is bounded by NPROC and file ids by PF_MAX, so the ranges cannot
// collide. Shifting by 8 rather than packing tightly keeps the numbers
// readable in a debugger, which matters more than density here.
// File ids stay below 0x80 and fds below 128 (NOFILE). pids are not bounded
// by NPROC -- they count up -- so the global bit sits far above any pid << 8;
// at 0x10000 it was /proc/256. Shifting by 8 rather than packing tightly keeps
// the numbers readable in a debugger, which matters more than density here.
// ---------------------------------------------------------------------------
#define PROCFS_ROOT 1ULL
#define PROCFS_GLOBAL 0x10000ULL
#define PROCFS_SELF 2ULL
#define PROCFS_GLOBAL (1ULL << 40)
#define PF_DIR 0 // the directory itself
#define PF_STAT 1
@ -63,10 +67,14 @@
#define PF_STATM 4
#define PF_EXE 5
#define PF_SCHEDSTAT 6
#define PF_MAX 6
#define PF_FD 7 // the directory /proc/<pid>/fd
#define PF_MAX 7
#define PF_FDENT 0x80 // | fd: one link in it
#define INO_PID(ino) ((int)((ino) >> 8))
#define INO_FILE(ino) ((int)((ino) & 0xFF))
#define INO_IS_FDENT(ino) (!INO_IS_GLOBAL(ino) && (INO_FILE(ino) & PF_FDENT))
#define INO_FD(ino) (INO_FILE(ino) & 0x7F)
#define INO_IS_GLOBAL(ino) (((ino) & PROCFS_GLOBAL) != 0)
// The global files, by the name they appear under and the PROC_* id that
@ -121,6 +129,9 @@ static const struct global_file pidfiles[] = {
// inventing a wait time that is not measured would be worse than saying
// something true in its place.
{ "schedstat", PF_SCHEDSTAT },
// One symlink per open descriptor, named by its number and pointing at the
// path it was opened by, or at "socket:[ino]" and the like.
{ "fd", PF_FD },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
@ -267,6 +278,15 @@ gen_file(uint64 ino, char *buf, int cap)
if (INO_IS_GLOBAL(ino))
return proc_gen_global((int)(ino & 0xFF), buf, cap);
// Whoever is asking: the one file whose content depends on the reader.
if (ino == PROCFS_SELF) {
sbputi(&s, myproc()->pid);
return s.n;
}
if (INO_IS_FDENT(ino)) {
int n = proc_fd_link(INO_PID(ino), INO_FD(ino), buf, cap);
return n > 0 ? n : 0;
}
if (proc_snapshot_pid(INO_PID(ino), &pi) < 0)
return 0; // it exited between lookup and read
@ -310,7 +330,8 @@ procfs_iload(struct inode *ip)
ip->showmode = 1;
ip->atime = ip->mtime = ip->ctime = 0;
if (ino == PROCFS_ROOT || (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_DIR)) {
if (ino == PROCFS_ROOT ||
(!INO_IS_GLOBAL(ino) && (INO_FILE(ino) == PF_DIR || INO_FILE(ino) == PF_FD))) {
ip->type = T_DIR;
ip->size = 0;
ip->nlink = 2;
@ -329,8 +350,12 @@ procfs_iload(struct inode *ip)
// symlink whose size is 0 resolves to the empty string -- which is worse
// than the regular file was, because it looks like it worked. Generating it
// here costs one small render per stat() of one file.
if (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_EXE) {
char path[64];
// /proc/self and /proc/<pid>/fd/N are links too. Their targets can change
// under a cached inode, which is why readlink() does not trust this size
// for procfs.
if (ino == PROCFS_SELF || INO_IS_FDENT(ino) ||
(!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_EXE)) {
char path[256];
int len = gen_file(ino, path, sizeof(path));
ip->type = T_SYMLINK;
ip->size = len > 0 ? (uint64)len : 0;
@ -469,8 +494,19 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff)
if (namecmp(name, ".") == 0)
return iget(dp->dev, dp->inum);
if (namecmp(name, "..") == 0)
return iget(dp->dev, PROCFS_ROOT); // /proc/<pid>/.. is /proc
if (namecmp(name, "..") == 0) // /proc/<pid>/.. is /proc
return iget(dp->dev, INO_FILE(dp->inum) == PF_FD ? (dp->inum & ~0xFFULL)
: PROCFS_ROOT);
// Inside /proc/<pid>/fd/: a number that is an open descriptor.
if (dp->inum != PROCFS_ROOT && INO_FILE(dp->inum) == PF_FD) {
char tmp[4];
int fd = name_to_pid(name);
if (fd < 0 || fd >= NOFILE || fd >= 128 ||
proc_fd_link(INO_PID(dp->inum), fd, tmp, sizeof(tmp)) < 0)
return 0;
return iget(dp->dev, (dp->inum & ~0xFFULL) | PF_FDENT | fd);
}
// Inside /proc/<pid>/
if (dp->inum != PROCFS_ROOT) {
@ -480,6 +516,9 @@ procfs_dirlookup(struct inode *dp, char *name, uint *poff)
return 0;
}
if (namecmp(name, "self") == 0)
return iget(dp->dev, PROCFS_SELF);
// In /proc itself: a number is a process, a word is a global file.
pid = name_to_pid(name);
if (pid > 0) {
@ -516,6 +555,7 @@ struct linux_dirent64 {
#define DT_DIR_ 4
#define DT_REG_ 8
#define DT_LNK_ 10
static int
emit(char *out, int cap, int used, uint64 ino, int type, const char *name)
@ -574,17 +614,30 @@ procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
if (dp->inum == PROCFS_ROOT) {
EMIT(PROCFS_ROOT, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
EMIT(PROCFS_SELF, DT_LNK_, "self");
for (i = 0; i < NGLOBALS; i++)
EMIT(PROCFS_GLOBAL | globals[i].id, DT_REG_, globals[i].name);
for (i = 0; proc_snapshot(i, &pi) == 0; i++) {
utoa((uint)pi.pid, name);
EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name);
}
} else if (INO_FILE(dp->inum) == PF_FD) {
uint64 mask[2];
EMIT(dp->inum, DT_DIR_, ".");
EMIT(dp->inum & ~0xFFULL, DT_DIR_, "..");
if (proc_fd_mask(INO_PID(dp->inum), mask) == 0)
for (i = 0; i < 128; i++)
if (mask[i / 64] & (1ULL << (i % 64))) {
utoa((uint)i, name);
EMIT((dp->inum & ~0xFFULL) | PF_FDENT | i, DT_LNK_, name);
}
} else {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NPIDFILES; i++)
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id, DT_REG_, pidfiles[i].name);
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id,
pidfiles[i].id == PF_FD ? DT_DIR_ : pidfiles[i].id == PF_EXE ? DT_LNK_ : DT_REG_,
pidfiles[i].name);
}
full:

View file

@ -641,6 +641,9 @@ kmount_slot(int slot, uint bdev)
// Opening with a page allocator also builds the allocation bitmap, by walking
// the tree — the only mount-time cost SchFS has, and what buys it a mount
// that cannot disagree with the filesystem it just adopted.
// The live root fills its ramdisk, however much memory that turned out to
// be. See kernel/dev/ramdisk.c.
m->vol.grow_to = (bdev == RAMDISK_DEV) ? ramdisk_blocks() : 0;
int r = schfs_vol_open(&m->vol, &bd, &cx, kd_palloc, kd_pfree);
if (r == -1)
return -EINVAL; /* not a SchFS volume at all */

View file

@ -104,6 +104,13 @@ struct schfs_vol {
uint64_t total_blocks;
uint64_t free_blocks;
// Set by the opener before schfs_vol_open: the device's size, when the
// volume should grow to fill it. Nothing on disk records which blocks are
// free -- open rebuilds that from the tree -- so growing is only a larger
// total before the map is built, and the next commit writes it down. The
// live root uses it to fill a ramdisk sized from memory at boot. 0, and
// anything not larger than the volume, leaves it alone.
uint64_t grow_to;
uint64_t next_objectid;
uint64_t root_objectid;
uint64_t generation; // last *committed* transaction group

View file

@ -215,6 +215,8 @@ schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
}
vol_from_uber(v, u, best_slot);
if (v->grow_to > v->total_blocks)
v->total_blocks = v->grow_to;
v->readonly = 1; // until an allocation map exists, below
if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0)

View file

@ -94,6 +94,7 @@ struct inet_sock {
int inuse;
int type; // SOCK_STREAM or SOCK_DGRAM
int proto; // 0, or IPPROTO_ICMP for a ping socket
int iphdr; // SOCK_RAW: reads start at the IP header
struct tcp_pcb *tpcb;
struct udp_pcb *upcb;
@ -114,6 +115,7 @@ struct inet_sock {
int connected;
int conn_failed;
int peer_closed; // FIN seen, or the connection died
int wr_shut; // shutdown(SHUT_WR): our FIN is sent
int listening;
// Connections lwIP has completed but nobody has accept()ed yet.
@ -149,13 +151,36 @@ net_seq(void)
return s;
}
// Sleep until the count moves past `seen`, or this process is killed. kill()
// makes a sleeping process runnable, so the loop re-checks and leaves.
// A blocked socket call gives up for a kill and for any signal it can take.
// The second used to be missing, so a signal woke the sleeper and the loop
// below put it straight back: busybox's ping sends each request from a
// SIGALRM handler while it sits in recvfrom, and got one reply and then
// silence. There is no SA_RESTART, so the call returns EINTR and the caller
// asks again, which is what every one of them is written to do.
//
// Only signals with a handler count, as on Linux. A SIGCHLD left at its
// default is posted to a parent all the same, and ending a server's accept()
// for it would be an EINTR that program had never been handed before.
static int
net_interrupted(void)
{
struct proc *p = myproc();
if (p->killed)
return 1;
uint64 live = p->pending_signals & ~p->signal_mask;
for (int sig = 1; sig < 32; sig++)
if ((live & (1ULL << sig)) && p->sig_handler[sig] > 1) // not DFL/IGN
return 1;
return 0;
}
// Sleep until the count moves past `seen`, or a signal arrives. kill() makes
// a sleeping process runnable, so the loop re-checks and leaves.
static void
net_wait(uint seen)
{
acquire(&netwait_lock);
while (netwait_seq == seen && !myproc()->killed)
while (netwait_seq == seen && !net_interrupted())
sleep(&netwait_seq, &netwait_lock);
release(&netwait_lock);
}
@ -170,6 +195,7 @@ net_wake(void)
netwait_seq++;
release(&netwait_lock);
wakeup(&netwait_seq);
poll_kick(); // and poll/select/epoll waiters, not at the tick
}
/* ------------------------------------------------------------------ */
@ -438,6 +464,11 @@ inet_udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p,
// processes ping at once; with one ping at a time the difference is invisible,
// and a socket that sees a reply to somebody else's request will read a
// sequence number it never sent and ignore it.
//
// A SOCK_RAW ICMP socket is the same socket, except that a read starts at the
// IP header: that is the raw-socket contract, and busybox's ping reads the TTL
// out of it and finds the ICMP message by IHL. Writing is the same for both --
// without IP_HDRINCL a raw socket's caller sends the ICMP message alone too.
static u8_t
inet_icmp_recv_cb(void *arg, struct raw_pcb *pcb, struct pbuf *p,
const ip_addr_t *addr)
@ -462,7 +493,8 @@ inet_icmp_recv_cb(void *arg, struct raw_pcb *pcb, struct pbuf *p,
if (pbuf_copy_partial(p, &type, 1, hlen) != 1 || type != 0)
return 0;
u16_t icmplen = (u16_t)(p->tot_len - hlen);
u16_t skip = is->iphdr ? 0 : hlen;
u16_t icmplen = (u16_t)(p->tot_len - skip);
if (icmplen <= rx_space(is) && dq_used(is) < NDGRAM) {
struct dgram *d = &is->dq[is->dhead % NDGRAM];
@ -471,9 +503,9 @@ inet_icmp_recv_cb(void *arg, struct raw_pcb *pcb, struct pbuf *p,
d->len = icmplen;
is->dhead++;
// Copy the ICMP message out, skipping the IP header, across whatever pbuf
// chain it arrived in.
u16_t off = hlen;
// Copy the message out, skipping the IP header unless this is a raw
// socket, across whatever pbuf chain it arrived in.
u16_t off = skip;
while (off < p->tot_len) {
char tmp[64];
u16_t want = (u16_t)(p->tot_len - off);
@ -515,8 +547,15 @@ inet_alloc(int type)
void *
inet_new(int type, int proto)
{
if (type != SOCK_STREAM && type != SOCK_DGRAM)
if (type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_RAW)
return 0;
if (type == SOCK_RAW && proto != IPPROTO_ICMP)
return 0;
// Past this point a raw ICMP socket is a ping socket with one flag: every
// datagram path below already does the right thing for it.
int iphdr = (type == SOCK_RAW);
if (iphdr)
type = SOCK_DGRAM;
// Ready, not *up*: see net_is_ready() in kernel/net/net.c. Requiring an
// address here made every socket() fail until DHCP answered.
if (!net_is_ready())
@ -529,6 +568,7 @@ inet_new(int type, int proto)
return 0;
}
is->proto = proto;
is->iphdr = iphdr;
if (proto == IPPROTO_ICMP) {
// A ping socket. raw_new registers for one IP protocol number; the
@ -688,7 +728,7 @@ inet_accept(void *h)
uint seen = net_seq();
lwip_sys_unlock();
if (myproc()->killed)
if (net_interrupted())
return 0;
net_wait(seen);
}
@ -729,7 +769,7 @@ inet_connect(void *h, uint32 addr, uint16 port)
return 0;
if (failed)
return -ECONNREFUSED;
if (myproc()->killed)
if (net_interrupted())
return -EINTR;
net_wait(seen);
}
@ -742,6 +782,47 @@ inet_connect(void *h, uint32 addr, uint16 port)
// peer it is connected to and is the same for every byte; for a datagram
// socket it belongs to the individual datagram, which is why UDP needs the
// queue above and TCP does not.
// getsockname / getpeername: this end's address or the other's, the address
// in network order and the port in host order, as inet_recvfrom gives them. A
// socket not yet bound reads as 0.0.0.0:0 locally, which is what Linux says;
// one with no peer is ENOTCONN. A listening pcb has no remote fields at all
// -- lwIP's listen pcb is the smaller struct -- so it is never asked for them.
int
inet_name(void *h, int peer, uint *addr, ushort *port)
{
struct inet_sock *is = h;
int r = 0;
*addr = 0;
*port = 0;
lwip_sys_lock();
if (is->tpcb) {
if (peer && (is->listening || is->tpcb->state < SYN_SENT)) {
r = -ENOTCONN;
} else if (peer) {
*addr = ip4_addr_get_u32(ip_2_ip4(&is->tpcb->remote_ip));
*port = is->tpcb->remote_port;
} else {
*addr = ip4_addr_get_u32(ip_2_ip4(&is->tpcb->local_ip));
*port = is->tpcb->local_port;
}
} else if (is->upcb) {
if (peer && !(is->upcb->flags & UDP_FLAGS_CONNECTED)) {
r = -ENOTCONN;
} else if (peer) {
*addr = ip4_addr_get_u32(ip_2_ip4(&is->upcb->remote_ip));
*port = is->upcb->remote_port;
} else {
*addr = ip4_addr_get_u32(ip_2_ip4(&is->upcb->local_ip));
*port = is->upcb->local_port;
}
} else if (peer) {
r = -ENOTCONN; // a ping socket has no peer
}
lwip_sys_unlock();
return r;
}
int
inet_recvfrom(void *h, char *buf, int n, uint *addr, ushort *port)
{
@ -787,7 +868,7 @@ inet_recvfrom(void *h, char *buf, int n, uint *addr, ushort *port)
return got;
if (eof)
return 0; // end of file, not an error
if (myproc()->killed)
if (net_interrupted())
return -EINTR;
net_wait(seen);
}
@ -879,7 +960,7 @@ inet_sendto(void *h, char *buf, int n, uint addr, ushort port)
uint seen;
lwip_sys_lock();
if (is->peer_closed || is->tpcb == 0) {
if (is->peer_closed || is->wr_shut || is->tpcb == 0) {
lwip_sys_unlock();
// Same contract as a pipe with no reader, which is what the AF_UNIX side
// does: the signal first, then the error.
@ -909,7 +990,7 @@ inet_sendto(void *h, char *buf, int n, uint addr, ushort port)
lwip_sys_unlock();
if (done < n) {
if (myproc()->killed)
if (net_interrupted())
return done > 0 ? done : -EINTR;
// Woken by inet_sent_cb when an acknowledgement frees send buffer, or by
// a failure. Without tcp_sent registered this would be a sleep with
@ -926,6 +1007,32 @@ inet_write(void *h, char *buf, int n)
return inet_sendto(h, buf, n, 0, 0);
}
// shutdown(2) on a TCP socket. SHUT_WR sends our FIN and the peer can keep
// sending; SHUT_RD is local, reads see EOF. The pcb is never freed here --
// tcp_shutdown(pcb, 1, 1) would free it -- so close() still owns it.
int
inet_shutdown(void *h, int how)
{
struct inet_sock *is = h;
if (is->type != SOCK_STREAM)
return 0;
lwip_sys_lock();
if (is->tpcb == 0 || is->listening || !is->connected) {
lwip_sys_unlock();
return -ENOTCONN;
}
if ((how == SHUT_WR || how == SHUT_RDWR) && !is->wr_shut) {
is->wr_shut = 1;
tcp_shutdown(is->tpcb, 0, 1);
}
if (how == SHUT_RD || how == SHUT_RDWR)
is->peer_closed = 1;
lwip_sys_unlock();
net_wake();
return 0;
}
int
inet_readable(void *h)
{

View file

@ -230,6 +230,7 @@ sockclose(struct socket *s)
s->ref = 0;
wakeup(&s->nqueue); // an accept() blocked on this listener
release(&stab.lock);
poll_kick(); // the peer's poll() sees the hangup now
}
void
@ -459,6 +460,7 @@ sockread_fds(struct socket *s, char *addr, int n, struct file **fout, int *nfout
// on a full buffer that this call has just made room in.
wakeup(&s->nwrite);
release(&stab.lock);
poll_kick(); // a writer polling for room
// Outside the lock, because fileclose can sleep.
if(fout){
@ -520,7 +522,7 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
// look for it. Same contract as a pipe with no reader: a signal, then an
// error. A process that writes to a dead connection and is not told about
// it spins forever believing it is making progress.
if(s->peer == 0){
if(s->peer == 0 || s->wr_shut){
release(&stab.lock);
sigsend(myproc()->pid, SIGPIPE);
return -EPIPE;
@ -569,6 +571,9 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
wakeup(&s->peer->nread);
release(&stab.lock);
// poll() and epoll waiters, now rather than at the next tick: an X client
// and its server talk over these, a request and a reply at a time.
poll_kick();
return i;
}
@ -688,6 +693,61 @@ sockbind(struct socket *s, const char *path, uint dev, uint64 ino)
return 0;
}
// The abstract namespace: a sun_path that starts with a NUL names no file.
// Linux keeps these names in the socket layer, and Xorg binds one before its
// /tmp/.X11-unix path -- which this used to read as the empty path, resolve
// to the directory it was called from and refuse with EADDRINUSE, so Xorg
// reported "server already running" and exited.
//
// The name becomes a key on a device number no filesystem has, and the rest
// is the inode machinery unchanged: connect() finds the listener bound to
// (SOCK_ABSTRACT_DEV, key). The name goes when the socket does, because
// nothing else holds it.
int
sockbind_abstract(struct socket *s, uint64 key)
{
acquire(&stab.lock);
if(s->state != SS_UNBOUND){
release(&stab.lock);
return -EINVAL;
}
for(struct socket *o = stab.sock; o < stab.sock + NSOCK; o++)
if(o != s && o->state != SS_FREE && o->state != SS_UNBOUND &&
o->bind_dev == SOCK_ABSTRACT_DEV && o->bind_ino == key){
release(&stab.lock);
return -EADDRINUSE;
}
s->name[0] = '\0'; // getsockname reports it unnamed, not as a path
s->bind_dev = SOCK_ABSTRACT_DEV;
s->bind_ino = key;
s->state = SS_BOUND;
release(&stab.lock);
return 0;
}
// The path for getsockname (peer 0) or getpeername (peer 1) of an AF_UNIX
// socket. Empty for an unnamed one -- a client that never bound, or an
// abstract name, which the caller reports as the family alone.
int
sockname_inet(struct socket *s, int peer, uint *addr, ushort *port)
{
return inet_name(s->inet, peer, addr, port);
}
int
sockname_unix(struct socket *s, int peer, char *path)
{
acquire(&stab.lock);
struct socket *o = peer ? s->peer : s;
if(o == 0){
release(&stab.lock);
return -ENOTCONN;
}
safestrcpy(path, o->name, UNIX_PATH_MAX);
release(&stab.lock);
return 0;
}
int
socklisten(struct socket *s, int backlog)
{
@ -799,6 +859,7 @@ sockconnect(struct socket *s, uint dev, uint64 ino)
wakeup(&srv->nqueue); // an accept() may be blocked on this
release(&stab.lock);
poll_kick(); // or a listener in poll(), as Xorg's is
// Deliberately does *not* wait for accept(). On Unix a queued connection is
// already established: the client may write, and the bytes sit in the
@ -817,6 +878,9 @@ sockconnect(struct socket *s, uint dev, uint64 ino)
int
sock_shutdown(struct socket *s, int how)
{
if(s->domain == AF_INET)
return inet_shutdown(s->inet, how);
acquire(&stab.lock);
if(s->state != SS_CONNECTED){
release(&stab.lock);
@ -827,9 +891,20 @@ sock_shutdown(struct socket *s, int how)
s->peer_closed = 1;
wakeup(&s->nread);
}
if(how == SHUT_WR || how == SHUT_RDWR)
if(how == SHUT_RDWR)
orphan_peer(s);
else if(how == SHUT_WR){
// Half-close: the peer sees EOF once it has read what is there, and the
// link stays, so its reply still reaches us. Cutting the link here made
// the reply a SIGPIPE -- busybox wget does exactly this to ssl_client.
s->wr_shut = 1;
if(s->peer){
s->peer->peer_closed = 1;
wakeup(&s->peer->nread);
}
}
release(&stab.lock);
poll_kick();
return 0;
}

View file

@ -311,6 +311,7 @@ found:
memset(p->sig_handler, 0, sizeof(p->sig_handler));
memset(p->sig_restorer, 0, sizeof(p->sig_restorer));
p->pending_signals = 0;
p->alarm_at = p->alarm_every = 0; // a child's timers start disarmed
p->utime = p->stime = p->cutime = p->cstime = 0;
// A recycled slot must not inherit a dead process's place in the queue --
@ -1543,6 +1544,97 @@ sig_kills_now(struct proc *p, int sig)
return p->sig_handler[sig] == 0 && sig_default_kills(sig);
}
// SIGALRM, on time.
//
// alarm() and setitimer() stored a countdown that nothing ever counted down,
// so no timer anyone set has ever fired. Most programs never noticed, because
// what they used it for was a timeout; busybox's ping sends every request but
// the first from its SIGALRM handler, and on the Alpine root it is /bin/ping,
// so ping printed one reply and then nothing.
//
// A kernel thread rather than the timer interrupt, which is in trap.c, and
// which would have to walk the process table with interrupts off: this walks
// it once a tick, and only while some process has a timer armed.
static int alarms_armed; // processes with alarm_at set; a hint, not a lock
void
alarm_arm(struct proc *p, uint after, uint every)
{
acquire(&ptable.lock);
if (p->alarm_at == 0 && after)
alarms_armed++;
else if (p->alarm_at && after == 0)
alarms_armed--;
p->alarm_at = after ? ticks + after : 0;
if (p->alarm_at == 0 && after)
p->alarm_at = 1; // 0 means disarmed; a tick late is fine
p->alarm_every = after ? every : 0;
release(&ptable.lock);
}
// Ticks until p's timer is due, 0 if it is not armed.
uint
alarm_left(struct proc *p)
{
uint at = p->alarm_at;
if (at == 0)
return 0;
int left = (int)(at - ticks);
return left > 0 ? (uint)left : 1;
}
static void
alarm_thread(void)
{
for (;;) {
acquire(&tickslock);
sleep(&ticks, &tickslock);
uint now = ticks;
release(&tickslock);
if (alarms_armed == 0)
continue;
acquire(&ptable.lock);
for (struct proc *p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->alarm_at == 0 || (int)(now - p->alarm_at) < 0)
continue;
if (p->state == UNUSED || p->state == ZOMBIE) {
p->alarm_at = 0;
alarms_armed--;
continue;
}
if (p->alarm_every) {
p->alarm_at = now + p->alarm_every;
} else {
p->alarm_at = 0;
alarms_armed--;
}
// sigsend takes ptable.lock itself, and with this many lines it is
// clearer to repeat its three effects than to split it.
if (!init_ignores(p, SIGALRM)) {
p->pending_signals |= 1ULL << SIGALRM;
if (sig_kills_now(p, SIGALRM)) {
p->killed = 1;
if (p->killsig == 0)
p->killsig = SIGALRM;
}
if (p->state == SLEEPING) {
p->state = RUNNABLE;
sched_enqueue(p);
}
}
}
release(&ptable.lock);
}
}
void
alarm_thread_start(void)
{
if (kthread_create(alarm_thread, "alarmd") < 0)
cprintf("alarm: no kernel thread; alarm() and setitimer() will not fire\n");
}
int
sigsend(int pid, int sig)
{
@ -1815,6 +1907,50 @@ proc_snapshot_pid(int pid, struct procinfo *out)
return -1;
}
// /proc/<pid>/fd: which descriptors are open, and what one points at. The
// ptable lock keeps the process from being reaped -- exit() closes its files
// first -- and file_describe() holds the file steady itself.
int
proc_fd_mask(int pid, uint64 mask[2])
{
struct proc *p;
int r = -1;
mask[0] = mask[1] = 0;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED || p->pid != pid)
continue;
for (int fd = 0; fd < NOFILE && fd < 128; fd++)
if (p->ofile[fd])
mask[fd / 64] |= 1ULL << (fd % 64);
r = 0;
break;
}
release(&ptable.lock);
return r;
}
int
proc_fd_link(int pid, int fd, char *buf, int cap)
{
struct proc *p;
int r = -1;
if (fd < 0 || fd >= NOFILE)
return -1;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED || p->pid != pid)
continue;
if (p->ofile[fd])
r = file_describe(p->ofile[fd], buf, cap);
break;
}
release(&ptable.lock);
return r;
}
int proc_num(void)
{
struct proc *p;

View file

@ -0,0 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// zstd builds with DEBUGLEVEL 0, where assert() is never expanded.
#ifndef SCFF_COMPAT_ASSERT_H
#define SCFF_COMPAT_ASSERT_H
#define assert(x) ((void)0)
#endif

View file

@ -0,0 +1,5 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Included by zstd's debug printing, which DEBUGLEVEL 0 compiles out.
#ifndef SCFF_COMPAT_STDIO_H
#define SCFF_COMPAT_STDIO_H
#endif

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// zstd's allocator hooks. The kernel decoder is a static context in a fixed
// workspace (kernel/scff/scff.c), so nothing allocates: these exist only for
// code paths that are compiled in and never taken.
#ifndef SCFF_COMPAT_STDLIB_H
#define SCFF_COMPAT_STDLIB_H
#include <stddef.h>
static inline void *malloc(size_t n) { (void)n; return 0; }
static inline void *calloc(size_t n, size_t s) { (void)n; (void)s; return 0; }
static inline void free(void *p) { (void)p; }
#endif

View file

@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// What zstd asks of <string.h>, from the kernel's own (kernel/core/string.c).
// zstd calls __builtin_memcpy and friends where it can, so these are the
// fallbacks for the sizes the compiler does not open-code.
#ifndef SCFF_COMPAT_STRING_H
#define SCFF_COMPAT_STRING_H
#include <stddef.h>
void *memcpy(void *, const void *, unsigned int);
void *memmove(void *, const void *, unsigned long);
void *memset(void *, int, unsigned long);
#endif

80
kernel/scff/scff.c Normal file
View file

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// The zstd decoder for SCFF chunks (include/fs/scff.h).
//
// One static decompression context in a fixed workspace, so nothing here
// allocates, and one caller at a time: kernel/dev/ramdisk.c holds its chunk
// lock across every call.
//
// **The decoder runs on a stack of its own.** A kernel stack is 8 KB, and a
// chunk is decompressed at the bottom of a read -- syscall, namei, SchFS,
// bread, ramdiskrw -- with most of it already used. zstd's sequence decoder
// keeps several KB of state in locals, which Linux gives it on a 16 KB stack.
// Here that would run off the end of the process's stack into whatever is
// below it. So the call is made on scff_stack, which the chunk lock makes
// this caller's alone. An interrupt taken meanwhile lands on it too, which
// is fine: it is a kernel stack like any other, and a big one.
//
// This file sees zstd's headers and none of the kernel's, which keeps the two
// sets of type definitions from meeting.
#define ZSTD_STATIC_LINKING_ONLY
#include "zstd.h"
#include "scff_zstd.h"
static unsigned char scff_work[256 * 1024] __attribute__((aligned(64)));
static unsigned char scff_stack[64 * 1024] __attribute__((aligned(16)));
static ZSTD_DCtx *dctx;
// call_on_stack(fn, arg, top): fn(arg) with %rsp at top, then back.
// %rbp keeps the old stack pointer across the call; it is callee-saved.
long call_on_stack(long (*fn)(void *), void *arg, void *top);
__asm__(
".text\n"
".globl call_on_stack\n"
"call_on_stack:\n"
" pushq %rbp\n"
" movq %rsp, %rbp\n"
" movq %rdx, %rsp\n"
" movq %rdi, %rax\n"
" movq %rsi, %rdi\n"
" callq *%rax\n"
" movq %rbp, %rsp\n"
" popq %rbp\n"
" retq\n");
struct job {
void *dst;
unsigned long cap;
const void *src;
unsigned long n;
};
static long
run(void *arg)
{
struct job *j = arg;
size_t r = ZSTD_decompressDCtx(dctx, j->dst, j->cap, j->src, j->n);
return ZSTD_isError(r) ? -1 : (long)r;
}
int
scff_zstd_init(void)
{
if (dctx)
return 0;
if (ZSTD_estimateDCtxSize() > sizeof(scff_work))
return -1;
dctx = ZSTD_initStaticDCtx(scff_work, sizeof(scff_work));
return dctx ? 0 : -1;
}
long
scff_zstd_decompress(void *dst, unsigned long cap, const void *src, unsigned long n)
{
struct job j = { dst, cap, src, n };
if (!dctx)
return -1;
return call_on_stack(run, &j, scff_stack + sizeof(scff_stack));
}

13
kernel/scff/scff_zstd.h Normal file
View file

@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// The SCFF decoder's interface, in types both the kernel's headers and zstd's
// agree on. See kernel/scff/scff.c.
#ifndef SCHOS_SCFF_ZSTD_H
#define SCHOS_SCFF_ZSTD_H
// 0, or -1 if the workspace is too small for this zstd.
int scff_zstd_init(void);
// Decompress one zstd frame. The length written, or -1 for a bad frame.
// One caller at a time.
long scff_zstd_decompress(void *dst, unsigned long cap, const void *src, unsigned long n);
#endif

View file

@ -113,19 +113,13 @@ clock_ns(int clock)
return sec * 1000000000ULL + usec * 1000ULL;
}
// One tick, or until a signal or kill makes waiting pointless.
// One tick, a poll_kick, or a signal or kill that makes waiting pointless:
// poll's wait, so data arriving wakes an epoll_wait as it wakes a poll.
// Returns 1 for a tick, 0 for a kick, -EINTR.
static int
ev_block(void)
{
struct proc *p = myproc();
if (p->killed || (p->pending_signals & ~p->signal_mask))
return -EINTR;
acquire(&tickslock);
uint t0 = ticks;
while (ticks == t0 && !p->killed && !(p->pending_signals & ~p->signal_mask))
sleep(&ticks, &tickslock);
release(&tickslock);
return 0;
return poll_wait_tick();
}
static int
@ -178,6 +172,19 @@ ev_arg(int n, int kind, struct file **pf)
return f->ev;
}
// The name Linux gives each kind in /proc/<pid>/fd.
const char *
ev_kind_name(struct evobj *o)
{
switch (o ? o->kind : 0) {
case EV_EVENTFD: return "anon_inode:[eventfd]";
case EV_TIMERFD: return "anon_inode:[timerfd]";
case EV_SIGNALFD: return "anon_inode:[signalfd]";
case EV_EPOLL: return "anon_inode:[eventpoll]";
}
return "anon_inode:[event]";
}
// Called by fileclose() for the last reference.
void
ev_close(struct evobj *o)
@ -714,11 +721,12 @@ epoll_wait_common(int timeout_ms)
got = ep_ready(o, buf, max);
if (got > 0 || ticks_left == 0)
break;
if (ev_block() < 0) {
int w = ev_block();
if (w < 0) {
got = -EINTR;
break;
}
if (ticks_left > 0)
if (w > 0 && ticks_left > 0)
ticks_left--;
}
if (got > 0 && copy_to_user(uevents, buf, got * sizeof(*buf)) < 0)

View file

@ -13,6 +13,7 @@
#include "uapi/posix.h"
#include "core/syscall.h"
#include "core/uaccess.h"
#include "core/kmem.h"
#include "uapi/errno.h"
#include "fs/schfs.h" /* SCHFS_BLKSIZE, SCHFS_NAME_MAX — statfs reports them */
#include "gfx/gfx.h" /* gfx_cols/gfx_rows — TIOCGWINSZ reports them */
@ -69,6 +70,22 @@ linux_makedev(uint32 major, uint32 minor)
| ((uint64)(major & ~0xfffu) << 32) | ((uint64)(minor & ~0xffu) << 12);
}
// Terminals as Linux numbers them: tty0..63 are (4, n), /dev/tty is (5, 0) and
// /dev/console (5, 1). SchOS keeps them all under major 1 (kernel/fs/devfs.c),
// and Linux programs look at the number rather than the name. Xorg is the one
// that matters: it uses VTs only if stat("/dev/tty0") says major 4
// (xf86HasTTYs), and otherwise runs on none -- no VT switch, no KD_GRAPHICS,
// the console drawing over it, and Ctrl+Alt+F-keys doing nothing.
static uint64
linux_tty_rdev(uint32 major, uint32 minor)
{
if (major == 1 && minor == 0) return linux_makedev(4, 0);
if (major == 1 && minor >= 11 && minor <= 16) return linux_makedev(4, minor - 10);
if (major == 1 && minor == 10) return linux_makedev(5, 0);
if (major == 1 && minor == 1) return linux_makedev(5, 1);
return linux_makedev(major, minor);
}
static void
stati_linux(struct inode *ip, struct linux_stat *st)
{
@ -101,7 +118,7 @@ stati_linux(struct inode *ip, struct linux_stat *st)
// entry in /dev printed as "0, 0" under ls -l -- harmless while nothing
// read it, and wrong the moment something has to tell two disks apart.
if (ip->type == T_DEV)
st->st_rdev = linux_makedev(ip->major, ip->minor);
st->st_rdev = linux_tty_rdev(ip->major, ip->minor);
st->st_atime = ip->atime; st->st_mtime = ip->mtime; st->st_ctime = ip->ctime;
}
@ -139,6 +156,53 @@ fdalloc(struct file *f)
return -1;
}
// Remember the absolute path `f` was opened by, for /proc/<pid>/fd. `path` is
// joined to the directory it was resolved from -- the cwd, or dirfd's own
// remembered path -- and `.`, `..` and repeated slashes are folded out. This
// is lexical: a symlink along the way stays in the name. Nothing depends on
// it being there, so running out of memory just leaves it unset.
static void
file_setpath(struct file *f, int dirfd, const char *path)
{
const char *base = "";
if (path[0] != '/') {
struct file *d;
if (dirfd == AT_FDCWD)
base = myproc()->cwd_path;
else if (dirfd >= 0 && dirfd < NOFILE && (d = myproc()->ofile[dirfd]) && d->path)
base = d->path;
else
return;
}
char *buf = kalloc();
if (!buf) return;
int n = 0; // buf is "/a/b", no trailing slash
for (int pass = 0; pass < 2; pass++) {
const char *s = pass ? path : base;
for (;;) {
while (*s == '/') s++;
const char *e = s;
while (*e && *e != '/') e++;
int len = (int)(e - s);
if (len == 0) break;
if (len == 2 && s[0] == '.' && s[1] == '.') {
while (n > 0 && buf[--n] != '/') ;
} else if (!(len == 1 && s[0] == '.')) {
if (n + 1 + len >= PGSIZE) { kfree(buf); return; }
buf[n++] = '/';
memmove(buf + n, s, len);
n += len;
}
s = e;
}
}
if (n == 0) buf[n++] = '/';
buf[n] = 0;
if ((f->path = kmalloc(n + 1)) != 0)
memmove(f->path, buf, n + 1);
kfree(buf);
}
// Resolves a dirfd (AT_FDCWD or a real fd) to the inode a relative path
// should be resolved from. 0 on an invalid dirfd.
static struct inode *
@ -602,6 +666,7 @@ addr_t sys_open(void)
}
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip;
file_setpath(f, AT_FDCWD, path);
f->off = (omode & O_APPEND) ? ip->size : 0;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
@ -659,6 +724,7 @@ addr_t sys_openat(void)
uint64 start = (omode & O_APPEND) ? ip->size : 0;
iunlock(ip); end_op();
f->type = FD_INODE; f->ip = ip; f->off = start;
file_setpath(f, dirfd, path);
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
f->nonblock = (omode & O_NONBLOCK) != 0;
@ -697,6 +763,19 @@ addr_t sys_fstat(void)
}
return 0;
}
// A socket, a pipe or an anonymous inode: Linux answers with its type and
// an inode number, which is what `ls -l /proc/<pid>/fd` and every program
// asking "is my stdin a socket?" look at. ino matches /proc/<pid>/fd.
if (f->type != FD_INODE && f->type != FD_MEMFD && myproc()->syscall_map) {
struct linux_stat *ls = (struct linux_stat *)st;
memset(ls, 0, sizeof(*ls));
ls->st_mode = f->type == FD_SOCKET ? LINUX_S_IFSOCK | 0777
: f->type == FD_PIPE ? LINUX_S_IFIFO | 0600 : 0600;
ls->st_ino = f->ino;
ls->st_nlink = 1;
ls->st_blksize = PGSIZE;
return 0;
}
if (f->type != FD_INODE) return -EINVAL;
ilock(f->ip);
if (myproc()->syscall_map) stati_linux(f->ip, (struct linux_stat*)st);
@ -1229,10 +1308,14 @@ addr_t sys_readlink(void)
if (!(ip = namei(path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
// A /proc link can change after its inode was loaded -- an fd dup2()ed
// over -- so its size is not trusted: the read says how long it is.
int proc = ip->dev == PROCFS_DEV;
int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size;
int got = readi(ip, 0, (uint64)buf, 0, n);
if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
return got;
}
// -----------------------------------------------------------------------
@ -2090,10 +2173,14 @@ addr_t sys_readlinkat(void)
if (!(ip = at_namei(dirfd, path))) return -ENOENT;
ilock(ip);
if (ip->type != T_SYMLINK) { iunlockput(ip); return -EINVAL; }
int n = (int)ip->size < bufsiz ? (int)ip->size : bufsiz;
if (readi(ip, 0, (uint64)buf, 0, n) != n) { iunlockput(ip); return -EIO; }
// A /proc link can change after its inode was loaded -- an fd dup2()ed
// over -- so its size is not trusted: the read says how long it is.
int proc = ip->dev == PROCFS_DEV;
int n = (proc || (int)ip->size > bufsiz) ? bufsiz : (int)ip->size;
int got = readi(ip, 0, (uint64)buf, 0, n);
if (got < 0 || (!proc && got != n)) { iunlockput(ip); return -EIO; }
iunlockput(ip);
return n;
return got;
}
addr_t sys_fchmodat(void) { return 0; } // stub

View file

@ -66,6 +66,28 @@ fetch_sun_path(addr_t uaddr, int ulen, char *out)
return 0;
}
// For an abstract address -- sun_path[0] == 0 -- the name as a key, and 1.
// The name is every byte after the NUL up to the length given, NULs included,
// as Linux has it; FNV-1a, which is plenty for a table of NSOCK.
static int
sun_abstract(addr_t uaddr, int ulen, uint64 *key)
{
struct sockaddr_un sa;
int plen = ulen - (int)sizeof(sa.sun_family);
if (plen < 2 || ulen > (int)sizeof(sa))
return 0;
if (copy_from_user(&sa, uaddr, (uint64)ulen) < 0 || sa.sun_path[0] != 0)
return 0;
uint64 h = 1469598103934665603ULL;
for (int i = 1; i < plen; i++) {
h ^= (uchar)sa.sun_path[i];
h *= 1099511628211ULL;
}
*key = h ? h : 1;
return 1;
}
// Wrap an established socket in a file and a descriptor. Consumes the
// socket's reference on success; on failure the caller still owns it.
//
@ -144,7 +166,8 @@ SYSCALL_DEFINE(socket)
type &= 0xFF;
if (domain == AF_UNIX && type != SOCK_STREAM)
return -EPROTOTYPE;
if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM)
if (domain == AF_INET && type != SOCK_STREAM && type != SOCK_DGRAM &&
type != SOCK_RAW)
return -EPROTOTYPE;
// AF_NETLINK takes SOCK_RAW or SOCK_DGRAM and its own protocol numbering;
// socknew checks both, because which protocols exist is the socket layer's
@ -168,10 +191,17 @@ SYSCALL_DEFINE(socket)
(domain == AF_INET && type == SOCK_DGRAM && protocol == IPPROTO_UDP);
if (redundant)
protocol = 0;
else if (!(domain == AF_INET && type == SOCK_DGRAM &&
else if (!(domain == AF_INET &&
(type == SOCK_DGRAM || type == SOCK_RAW) &&
protocol == IPPROTO_ICMP))
return -EPROTONOSUPPORT;
}
// A raw socket is ICMP or nothing: it exists because busybox's ping, which
// is /bin/ping on the Alpine root, opens SOCK_RAW and stops at
// EPROTOTYPE. Raw TCP or UDP would be the stack handing over packets it is
// also processing, and nothing has asked for that.
if (domain == AF_INET && type == SOCK_RAW && protocol != IPPROTO_ICMP)
return -EPROTONOSUPPORT;
// ENETDOWN rather than ENFILE when the stack itself is not running: the
// old blanket ENFILE said "file table overflow" for a machine with no
@ -240,6 +270,10 @@ SYSCALL_DEFINE(bind)
return sockbind_in(s, ip, port_from_net(port));
}
uint64 key;
if (sun_abstract(uaddr, ulen, &key))
return sockbind_abstract(s, key);
if ((r = fetch_sun_path(uaddr, ulen, path)) < 0)
return r;
@ -315,6 +349,10 @@ SYSCALL_DEFINE(connect)
return sockconnect_in(s, ip, port_from_net(port));
}
uint64 key;
if (sun_abstract(uaddr, ulen, &key))
return sockconnect(s, SOCK_ABSTRACT_DEV, key);
if ((r = fetch_sun_path(uaddr, ulen, path)) < 0)
return r;
@ -550,9 +588,76 @@ SYSCALL_DEFINE(recvfrom)
return r;
}
// shutdown(2). Half-close is not modelled -- there is one buffer per
// direction and no per-direction flag -- so SHUT_WR and SHUT_RDWR both mean
// "the peer sees end-of-file", which is the behaviour programs depend on.
// getsockname(2) and getpeername(2).
//
// Neither existed, and the first program to mind was every X client: libxcb
// picks the cookie to send by the family of the connection's address, asked
// with getpeername and then getsockname, and with both failing it sent none.
// Xorg answered "Authorization required, but no authorization protocol
// specified" and startx gave up.
static int
sock_name(int peer)
{
struct socket *s;
addr_t uaddr, ulenp;
int r, cap;
if ((r = argsock(0, &s)) < 0)
return r;
argaddr(1, &uaddr);
argaddr(2, &ulenp);
if (get_user(cap, ulenp) < 0)
return -EFAULT;
if (cap < 0)
return -EINVAL;
union {
struct sockaddr_in in;
struct sockaddr_un un;
} sa;
int alen;
memset(&sa, 0, sizeof(sa));
if (sock_domain(s) == AF_INET) {
uint ip;
ushort port;
if ((r = sockname_inet(s, peer, &ip, &port)) < 0)
return r;
sa.in.sin_family = AF_INET;
sa.in.sin_addr = ip;
sa.in.sin_port = port_to_net(port);
alen = (int)sizeof(sa.in);
} else if (sock_domain(s) == AF_NETLINK) {
// A sockaddr_nl of zeros: family, padding, port id 0 (the kernel),
// no groups.
sa.un.sun_family = AF_NETLINK;
alen = 12;
} else {
if ((r = sockname_unix(s, peer, sa.un.sun_path)) < 0)
return r;
sa.un.sun_family = (uint16)sock_domain(s);
// The family, the path and its NUL; an unnamed socket is the family
// alone, as on Linux.
alen = (int)sizeof(sa.un.sun_family);
if (sa.un.sun_path[0])
alen += (int)strlen(sa.un.sun_path) + 1;
}
// Truncated to the caller's buffer, with the full length written back.
if (cap > alen)
cap = alen;
if (cap > 0 && copy_to_user(uaddr, &sa, (uint64)cap) < 0)
return -EFAULT;
if (copy_to_user(ulenp, &alen, sizeof(alen)) < 0)
return -EFAULT;
return 0;
}
SYSCALL_DEFINE(getsockname) { return sock_name(0); }
SYSCALL_DEFINE(getpeername) { return sock_name(1); }
// shutdown(2). SHUT_WR is a half-close: the peer reads EOF and can still
// write back (see sock_shutdown).
SYSCALL_DEFINE(sockshutdown)
{
struct socket *s;

View file

@ -24,22 +24,47 @@
#define POLL_MAX_FDS 64
// Sleep one timer tick, or until killed/signaled. Returns 0 to keep
// waiting, -EINTR if the wait must be abandoned.
static int
poll_block_one_tick(void)
// Bumped, under tickslock, by anything that can make a descriptor ready
// sooner than the next tick: see poll_kick().
static uint poll_seq;
// Something a poller may be waiting for has happened. The wait below sleeps
// on the tick channel, so this wakes it now rather than at the tick.
//
// Without it every poll() that found nothing slept out the rest of a 10 ms
// tick whatever arrived in the meantime. busybox's wget polls before each
// read, and downloaded at 85 KB/s -- a segment per tick -- where apk, which
// blocks in read() and is woken by the data, managed 5.8 MB/s over the same
// connection. Called from the network stack's wakeup; anything else that
// makes a file readable can call it too, and until it does it is no worse
// than it was.
void
poll_kick(void)
{
if (myproc()->killed)
return -EINTR;
if (myproc()->pending_signals & ~myproc()->signal_mask)
acquire(&tickslock);
poll_seq++;
release(&tickslock);
wakeup(&ticks);
}
// Sleep one timer tick, or until a poll_kick, or until killed/signaled.
// Returns 1 if a tick went by, 0 if it was a kick -- which must not count
// against a timeout, or a timed poll on a busy network would end early --
// and -EINTR if the wait must be abandoned. epoll waits with it too.
int
poll_wait_tick(void)
{
struct proc *p = myproc();
if (p->killed || (p->pending_signals & ~p->signal_mask))
return -EINTR;
acquire(&tickslock);
uint t0 = ticks;
while (ticks == t0 && !myproc()->killed &&
!(myproc()->pending_signals & ~myproc()->signal_mask))
uint t0 = ticks, s0 = poll_seq;
while (ticks == t0 && poll_seq == s0 && !p->killed &&
!(p->pending_signals & ~p->signal_mask))
sleep(&ticks, &tickslock);
int ticked = ticks != t0;
release(&tickslock);
return 0;
return ticked;
}
// Shared core: fds[] already in kernel memory. timeout_ticks < 0 means
@ -67,9 +92,10 @@ do_poll(struct pollfd *fds, int nfds, int timeout_ticks)
}
if (nready > 0 || timeout_ticks == 0)
return nready;
if (poll_block_one_tick() < 0)
int w = poll_wait_tick();
if (w < 0)
return -EINTR;
if (timeout_ticks > 0)
if (w > 0 && timeout_ticks > 0)
timeout_ticks--;
}
}
@ -215,9 +241,10 @@ sys_select(void)
}
return nready;
}
if (poll_block_one_tick() < 0)
int w = poll_wait_tick();
if (w < 0)
return -EINTR;
if (timeout_ticks > 0)
if (w > 0 && timeout_ticks > 0)
timeout_ticks--;
}
}
@ -287,9 +314,10 @@ sys_pselect6(void)
}
return nready;
}
if (poll_block_one_tick() < 0)
int w = poll_wait_tick();
if (w < 0)
return -EINTR;
if (timeout_ticks > 0)
if (w > 0 && timeout_ticks > 0)
timeout_ticks--;
}
}

View file

@ -10,14 +10,36 @@
#include "core/uaccess.h"
#include "uapi/errno.h"
// Timer values in ticks, rounding a nonzero time up: POSIX says a timer set
// to 1 us goes off, not that it is disarmed.
static uint
tv_ticks(struct timeval *tv)
{
if (tv->tv_sec < 0 || tv->tv_usec < 0)
return 0;
uint64 t = (uint64)tv->tv_sec * HZ +
((uint64)tv->tv_usec + USEC_PER_TICK - 1) / USEC_PER_TICK;
return t > 0x7fffffff ? 0x7fffffff : (uint)t;
}
static void
ticks_tv(uint t, struct timeval *tv)
{
tv->tv_sec = t / HZ;
tv->tv_usec = (t % HZ) * USEC_PER_TICK;
}
// The seconds left on the previous alarm, rounded up, as POSIX asks.
addr_t
sys_alarm(void)
{
int seconds;
if (argint(0, &seconds) < 0)
return -1;
myproc()->alarm_ticks = seconds * HZ;
return 0;
struct proc *p = myproc();
uint left = alarm_left(p);
alarm_arm(p, seconds > 0 ? (uint)seconds * HZ : 0, 0);
return (left + HZ - 1) / HZ;
}
addr_t
@ -77,21 +99,52 @@ sys_clock_gettime(void)
return 0;
}
addr_t sys_getitimer(void) { return 0; } // stub
// ITIMER_REAL only. ITIMER_VIRTUAL and ITIMER_PROF count CPU time, which
// nothing here measures per tick, and are refused rather than accepted and
// never fired -- which is what all three used to be.
static int
itimer_get(struct proc *p, struct itimerval *iv)
{
ticks_tv(p->alarm_every, &iv->it_interval);
ticks_tv(alarm_left(p), &iv->it_value);
return 0;
}
addr_t
sys_getitimer(void)
{
int which;
addr_t curp;
argint(0, &which); argaddr(1, &curp);
if (which != 0)
return -EINVAL;
struct itimerval iv;
itimer_get(myproc(), &iv);
if (copy_to_user(curp, &iv, sizeof(iv)) != 0)
return -EFAULT;
return 0;
}
addr_t
sys_setitimer(void)
{
int which;
addr_t newp;
argint(0, &which); argaddr(1, &newp);
// We don't implement interval timers; just track the alarm if it's ITIMER_REAL.
if (which == 0 && newp) {
struct itimerval iv;
if (copy_from_user(&iv, newp, sizeof(iv)) == 0)
myproc()->alarm_ticks = (int)(iv.it_value.tv_sec * HZ +
iv.it_value.tv_usec / USEC_PER_TICK);
addr_t newp, oldp;
argint(0, &which); argaddr(1, &newp); argaddr(2, &oldp);
if (which != 0)
return -EINVAL;
struct proc *p = myproc();
struct itimerval iv;
if (oldp) {
itimer_get(p, &iv);
if (copy_to_user(oldp, &iv, sizeof(iv)) != 0)
return -EFAULT;
}
if (newp == 0)
return 0;
if (copy_from_user(&iv, newp, sizeof(iv)) != 0)
return -EFAULT;
alarm_arm(p, tv_ticks(&iv.it_value), tv_ticks(&iv.it_interval));
return 0;
}

View file

@ -184,8 +184,8 @@ SYSCALL(102, 157, prctl)
SYSCALL(103, 234, tgkill)
SYSCALL(104, 200, tkill)
SYSCALL(105, 34, pause)
SYSCALL(106, NO_LINUX, getitimer)
SYSCALL(107, NO_LINUX, setitimer)
SYSCALL(106, 36, getitimer)
SYSCALL(107, 38, setitimer)
SYSCALL(108, 98, getrusage)
SYSCALL(109, 100, times)
SYSCALL(110, 99, sysinfo)
@ -296,6 +296,8 @@ SYSCALL(203, 281, epoll_pwait)
SYSCALL(204, 282, signalfd)
SYSCALL(205, 284, eventfd)
SYSCALL(206, 73, flock)
SYSCALL(207, 51, getsockname)
SYSCALL(208, 52, getpeername)
// --- mounts ---
SYSCALL(185, 165, mount)

View file

@ -16,3 +16,13 @@ ncurses-terminfo-base
# The PCI and USB ID databases: lspci, fastfetch and Xorg read them.
hwdata-pci
hwdata-usb
# X on the framebuffer, with startx. /root/.xserverrc and .xinitrc (from
# mkaroot.sh) start it with -dumbSched and run Window Maker in it.
xorg-server
xf86-video-fbdev
xf86-input-evdev
xinit
# The desktop: Window Maker, a terminal for it, and a font for both.
windowmaker
xterm
font-dejavu

View file

@ -1,12 +1,91 @@
# Generated by tools/posix/alpine-lock.sh from world. Do not edit;
# change world and run `make alpine-lock`.
# file sha256 repository
brotli-libs-1.2.1-r0.apk 62a7e37e43bbfc1f8525a00ad2125fd5897135dc93b849733884cb78ffd8e26c main
busybox-1.37.0-r31.apk 0e626fa97cf937fda7bba4ed4dde8e2f2dbb5083ac34e02180cec0c4eba9351b main
busybox-binsh-1.37.0-r31.apk 93f803ae5db6b729c0f66f4afba6e907ccc17d7d5f8b8c3d25bdb4636de2c48a main
encodings-1.1.0-r0.apk f7c0eb762638d0249b4fbbe07a8e4ed6aab8e86b74c239c40e47a36219777d52 main
eudev-libs-3.2.14-r6.apk 45fdbcf37c5c0e82067e98a818d3c1fd16d0261c62128dcb6b935b86b4ec6c86 main
font-alias-1.0.6-r0.apk 754e5c1d19c2c69d5a2c3c02c7340cad7df3c26e63789516372ea164b564b927 main
fontconfig-2.17.1-r1.apk 7634cf2fb1066ec97c8f41ec90ec9f036540e056276b7bf9a10dc4ec9e44d238 main
font-cursor-misc-1.0.4-r1.apk a1a0c0659a9a48e24b4cc4b35583f4e57da9d2a0ae2cc8adb9be1143b66911b4 main
font-dejavu-2.37-r6.apk 6e7609e7a1aaed947afe4ddcc02bbdeec571dac5ff4ea1c04af9b8480b7b9dcf main
font-misc-misc-1.1.3-r1.apk 2c48f2a8529ac63243a0bdd5e7ab2329e0a8b8f996ffe95faed8b79fb05b4a52 main
freetype-2.14.3-r0.apk 09d76fde675a9ca3cfeb83d72fb09cb2e9ccf79b2937daa05a1af9a37a6b7815 main
gmp-6.3.0-r4.apk 85323ef5bce8ff234736dc2d6592ea37aebdc6348a531e27430bdfe38364d256 main
hwdata-pci-0.408-r0.apk fe61404e6d2bd1e04c021d53641c0ebce97911084d77994e43a288a1278686c1 main
hwdata-usb-0.408-r0.apk 272d365cb05dd085dd8022154cf71e3f3b0aaddd41c1269eeaf939c9d38675c8 main
libbsd-0.12.2-r0.apk 407597c04b41dbfe8a05b35d45a3453e32fae7df84aed3a06ff48dd4df1338b6 main
libbz2-1.0.8-r6.apk 2737e2b23ed08d4911ee62cd0163b5f4bf0379cacf4dc35b23a00209584c98fd main
libcap2-2.78-r0.apk 42886a7b4bcde26347c712925c8dc7db202b6029ef9b8c7ee81577ab7bdd8fad main
libdrm-2.4.134-r0.apk 584fd84ad5030040e613fa1816097cd1706ddd542100070852b0eab6e09e12ca main
libelf-0.195-r0.apk 78c5a1993342ab1bdcdce30639fd0937e8000f6d0d85cea171f573d31decd036 main
libepoxy-1.5.10-r1.apk 0be13b4b524fe64315e0663ab615a244d75d7da73130ac1c630cd6096841444e main
libevdev-1.13.6-r0.apk b38a27a4ac55701fd2eb0032977af2664b6fc23b2f9c2cc6e893ad4452fd82d5 community
libexpat-2.9.0-r0.apk 32b127f31e0a919110a18a7f0bb3780d8fb66ba4d8d61b2d301d633f0e319d0e main
libffi-3.5.2-r1.apk 6da1a19313a40c56cac756fb11e2c4d5267831e1c22fc8323f24c943c94da6e0 main
libfontenc-1.1.9-r0.apk ea7f6d097653572991da749ac0764c2568d8902a01eea4236e56b10b90a7b0e3 main
libgcc-15.2.0-r5.apk 393dcd32629f06d7d85409c272d142d0c082772d10b87ef55ee82f47de3be637 main
libice-1.1.2-r0.apk 08daa589d57149b52b00c666b4009212aa3af95021ebdb2a359555c9bcaa76e1 main
libjpeg-turbo-3.1.3-r0.apk 6299627dd0a9156a7463511e4915a6294dfb592d8b4bdf85e55265adf1b8ea04 main
libmd-1.2.0-r0.apk f6d68d072fcefc8edd4ffe3afc341ca391f8a9a209130628fb065d98b18a2792 main
libncursesw-6.6_p20260516-r0.apk cf8caa8a88bc4ce9d9e395567fefaf9ef7fbd55c50ae6155b35c1b58f3755023 main
libpciaccess-0.19-r0.apk 03fdccd1e2a0825e7f7fabbac67212e05d271d31d2eff44cc92f707e69908ad4 main
libpng-1.6.59-r0.apk d16aeb0597f2bc43e0583ac9ff779388ec160c3e22568b640122e94ba55be6d7 main
libsharpyuv-1.6.0-r0.apk 34621ff53f43030d24d1224fe9cb53b1590ab614cbce6080005cde703e6c874a main
libsm-1.2.6-r0.apk dd7eff58a46fd60e4afee9c7c1c3508920b0e1d6560129e59aee82329f68afc8 main
libstdc++-15.2.0-r5.apk 14c987b556f5385a5db18376e788c75f37d85321b8dc1920d926ea7daac1d6f6 main
libuuid-2.42.3-r1.apk 8306e5bb577696c9069fe1dfd9e1dcc39d2d481c6a1b0e707fd03c3e21aa6aa2 main
libwebp-1.6.0-r0.apk dc50c0280348330bb113079331c4a35d85e28351c7baa9268ee346aad8398288 main
libx11-1.8.14-r0.apk 016efc189c6d566061ffe206ed0780e32686c1a8c832a3917d35d2c66d61a40f main
libxau-1.0.12-r0.apk 3506cdba7ed916a6f814a010fbb127dbfdae4147107f64a15f5fa2651c5d8510 main
libxaw-1.0.16-r1.apk 8ac469cddeac40ff2e91c84cb02ba526d7c62e30a79a25c2e3b6e5269236801d main
libxcb-1.17.0-r2.apk 7935d5ad9c7137b7b3f1292d62f55f7e0cc5c919155218c3060ea46503b3f211 main
libxcvt-0.1.3-r0.apk 809b454981887c1d1d835c909c46a80650ca72a73a92268a2cf8a666ad82b6b7 community
libxdmcp-1.1.5-r1.apk 2a93f44f3f85c1eda9ed53691c35d0742b5fcd8e0057cd7380934025936cbf6f main
libxext-1.3.7-r0.apk 8afe1ee6ab8cb656e12f20dc5b7fb2d4103faee4a38298b94f26ab52e597cbc8 main
libxfont2-2.0.8-r0.apk f8bf87b9ce835cb307eb8a3d270e226c4437757478c52ba3ad384cecc2ac6fef community
libxft-2.3.9-r0.apk eba6fd85599b4fedf3acdd3ad3e2eec84ea97a5ba8f1ac570fa1ee37a7d81420 main
libxinerama-1.1.6-r0.apk 9f780efb3cf675b5253b73b84f278b40757fa7869c6fe70a9790a73f4f7e810f main
libxkbfile-1.2.0-r0.apk e575c6c314ad87e74887526aca3b1429f5c1d6290e66f2945d4053f387beea57 main
libxml2-2.13.9-r2.apk 4b2f986159c659f014b942fe8a5d70c67af475153ca826c3dd53389eea46a300 main
libxmu-1.3.1-r0.apk 5d0b06d1a4b38ac0e070e0dad5e61583e5e147e378ba50418c83e1be4b19986c main
libxpm-3.5.20-r0.apk 1ed312a00abac5a5840c1f3721921ea8183e664566c19763ed00aa9f35bb2178 main
libxrender-0.9.13-r0.apk 724aa6c6353e70869e0eb5aa5461e8fc0753b99db765de47a3ae238573dbdcf8 main
libxshmfence-1.3.3-r0.apk 53790552de7eea17a47aa4c082b06b735c545a0947c41128f97c7be95a7d3d92 main
libxt-1.3.1-r0.apk 08b167b05ac7dfb892421876cc2467214208b83b2960a9f49e84f9dbcf36e812 main
libxxf86vm-1.1.7-r0.apk 887a3c5438e1db624d07ac23d16ec08d21fb16de438d75ad2dd5637c958b4108 main
llvm22-libs-22.1.3-r1.apk dec15bdd29b6ec7fc22aee43bac153a22e6b132c599e5e5d9ccd0d159eae764f main
mcookie-2.42.3-r1.apk 64f86340991e2f283ce95b1bae21157da6849118cd9f1a2931de5c73d0f9fc3d main
mesa-26.1.6-r0.apk 8fb0e3d386f09df4f3e3643108b38197558465b2558e6d66d6a9c055adc8d1a9 main
mesa-egl-26.1.6-r0.apk ccdd25c8f3a069b018a39a5641e4f13ab7ceb511d5991e9f8fb18bd551172ffa main
mesa-gbm-26.1.6-r0.apk db6b00543e7e686f377a5c250036067742f914112839255e2e5edbbc241bfb0d main
mesa-gl-26.1.6-r0.apk 4d2d502a3938bd5dc4affec13545271571bb8bec40e7078ef01f34222bfb5d5d main
mesa-gles-26.1.6-r0.apk 2544a75e74778ab9ddcecfc8d9dc35375cb3a4ac3661e94a1156e2c528d7b59b main
mkfontscale-1.2.4-r0.apk 2385a7aad3971fcad9d862cb90937e643e633b14bf4b91f8b8ce3040a1468d20 main
mtdev-1.1.7-r0.apk 004f42bc67bc0eb573d64d802c5f0325f49013cf4aa7576d4b571399df46f3f7 community
musl-1.2.6-r3.apk e142255d0fa397f14318996bc10378093dcc069dc3dd1b6818c8f28a789c21da main
ncurses-terminfo-base-6.6_p20260516-r0.apk ce83be6d0bd10584a53c3a5868cc1595ce5f0f8e0b621f8f7f70b6144e521fc9 main
nettle-3.10.2-r0.apk 693c1a99b781cd17149916df30113ccd6bdddac130298c17b1286fd2ae438386 main
pixman-0.46.4-r0.apk 378550cbe348dda9b0733014375dba87a89f0077c5f3dbeba7dce8dbcdda707e main
pkgconf-2.5.1-r0.apk 20cc47b76b82e9424a35ecc4fdd7bd904ef49ada1684214910fae31bc0cab737 main
spirv-tools-1.4.341.0-r0.apk 6034c22c4f61fb76e517c1d5b11ef5c26c7154d2baeacec8b0a9dcca94ff9a93 main
tiff-4.7.2-r0.apk 757ce87ebe4923a6be9869d026e250958ca09696529c464781e8a91d2b81670b main
util-macros-1.20.2-r0.apk 7c129d085b646b13ccef3738e93038221aec840c1a6764279c5adc9950ba1bdc main
wayland-libs-client-1.25.0-r0.apk a728f7103217cafdc83ff6ad0d679c197dbbe280bd5788c8336259f7a39cb303 main
windowmaker-0.96.0-r0.apk 15b500d9afccf94a385e042b01ed9b1d709b1627d97597cea59bf63d0bab4f6b community
xauth-1.1.5-r0.apk 94e80e9687a1829a6fe95fb28d4b08a5531dab182cde8a58ff1b370b2cd293fb community
xf86-input-evdev-2.11.0-r0.apk 1f3fcd3e837b43ad5e32005e03095b547ddbfc25973f0511e9ff74bf342d2fe0 community
xf86-video-fbdev-0.5.0-r6.apk 6a8a1b16f82d8810896f00c00b356c8b8d923e9aa0ce64737ae8274ca82f507a community
xinit-1.4.4-r0.apk efb5dc1e7929824f5add7d5d51d0ecb34ecfd0a773dfca4f061adc8a353b234f community
xkbcomp-1.5.0-r0.apk 8bfbd3c8ce7180e8cf599d5a0b791993b61fa98f3a19c47aaf5fcbe30c12d0ba main
xkeyboard-config-2.47-r0.apk 8c8f9af2b78f8e955480aa88103e495d2637edac659a417075e877c88a41fed0 main
xmodmap-1.0.11-r1.apk 42489c8865d401e894b8da210f5c8e27f58c1d72c83250a505a65bdb8c45bec0 community
xorg-server-21.1.25-r0.apk 8130c341a5f047069cb35ac77a44d8bab6a2e3cb5a7d0745e3c5096b1909ecfb community
xorg-server-common-21.1.25-r0.apk bf5994707b4030f249ad44a123ad2686501c82d2002d73d0a641d9001caa37c3 community
xrdb-1.2.2-r0.apk 29649088693b5974dec3f928a5303e354f7b91e381b778006fe3011d003b1369 community
xterm-410-r0.apk eff11da325ffb1dcfa4c9902ef27d34cc27b76457e1fc8dad90f10944ecb33b8 community
xz-libs-5.8.4-r0.apk ab362dd515ead04c31a6c302163248da7aa4813e283c971e6dd3f789b453a1d9 main
zlib-1.3.2-r1.apk 63aeea03c15a2f9018f81805cfc8aa926bdf5cd68921f22149c2fbb5d0ee9f47 main
zsh-5.9-r7.apk 22ec3c4de69e38437e351ba5a881f5d88e9b518a248c8aa06d36b148cf16ce32 main
zstd-libs-1.5.7-r2.apk 23c6065b0049b2406441564bcf0032515a43f78e80d76fcb85535a3803ef5d4e main

43
ports/zstd/Makefile Normal file
View file

@ -0,0 +1,43 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# zstd -- the decompressor behind SCFF, the compressed live root. Dual
# BSD-3-Clause / GPL-2.0; see LICENSE and COPYING in the tarball and
# docs/licensing.md.
#
# Kernel code, like uACPI: nothing is installed into the sysroot. This
# directory fetches and unpacks a pinned release, and the top-level Makefile
# compiles the decoder half of lib/ straight into the kernel against
# kernel/scff/compat, which stands in for the libc headers it includes. The
# host side, tools/mkscff, links the system's libzstd.
#
# Chosen for what SCFF asks of a codec: compress as hard as the build likes,
# once, and decompress at memory speed on every first read of a block.
include ../mk/port.mk
ZSTD_VERSION := 1.5.7
ZSTD_URL := https://github.com/facebook/zstd/releases/download/v$(ZSTD_VERSION)/zstd-$(ZSTD_VERSION).tar.gz
ZSTD_SHA256 := eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3
TARBALL := $(DISTDIR)/zstd-$(ZSTD_VERSION).tar.gz
$(TARBALL): | $(DISTDIR)
curl -fsSL -o $@ $(ZSTD_URL)
sh $(SHA256CHECK) "$(ZSTD_SHA256)" "$@"
download: $(TARBALL)
$(SRCDIR): $(TARBALL)
mkdir -p $(PORTDIR)
cd $(PORTDIR) && tar xzf $(abspath $<)
mv $(PORTDIR)/zstd-$(ZSTD_VERSION) $@
# The kernel build depends on this directory existing, nothing more.
all: | $(SRCDIR)
install:
clean:
rm -rf $(PORTDIR)
distclean: clean
rm -f $(TARBALL)

View file

@ -16,6 +16,14 @@
pkgs.mkShell {
name = "schos";
# Off, all of it. Nix's compiler wrapper adds -fPIC, -fstack-protector,
# -D_FORTIFY_SOURCE and friends to every compile, which is right for Linux
# programs and wrong for everything here: the kernel is freestanding and
# linked at fixed addresses, and the UEFI loader targets PE/COFF, where clang
# refuses -fPIC outright ("unsupported option '-fPIC' for target
# 'x86_64-unknown-windows-msvc'") and `make` stops at BOOTX64.EFI.
hardeningDisable = [ "all" ];
nativeBuildInputs = with pkgs; [
# The cross-compiler, and the reason this file exists. clang targets
# bare-metal ELF, 16-bit code and PE/COFF from one install; lld-link is what

190
tools/mkscff.c Normal file
View file

@ -0,0 +1,190 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// mkscff: write a block image as SCFF, the SchOS Compressed File Format.
//
// mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]
//
// The layout is include/fs/scff.h. Chunks are compressed on every core, since
// zstd's high levels are slow and the image is hundreds of megabytes, and each
// one is decompressed again and compared before it is written: an image the
// kernel cannot read is found here rather than at boot.
//
// Built against the zstd release the kernel's decoder comes from (ports/zstd),
// not the host's libzstd, so both sides are one version and nothing needs to
// be installed for it.
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "fs/scff.h"
#include "zstd.h"
static unsigned char *img;
static uint64_t img_bytes;
static uint32_t chunk_bytes = 128 * 1024;
static int level = 19;
static uint32_t nchunks;
static unsigned char **out; // chunk i as stored, or 0 for an all-zero one
static size_t *outlen;
static uint32_t next_chunk;
static pthread_mutex_t next_lock = PTHREAD_MUTEX_INITIALIZER;
static void
die(const char *what)
{
fprintf(stderr, "mkscff: %s\n", what);
exit(1);
}
static int
all_zero(const unsigned char *p, size_t n)
{
for (size_t i = 0; i < n; i++)
if (p[i])
return 0;
return 1;
}
static void *
worker(void *arg)
{
(void)arg;
ZSTD_CCtx *cc = ZSTD_createCCtx();
ZSTD_DCtx *dc = ZSTD_createDCtx();
size_t cap = ZSTD_compressBound(chunk_bytes);
unsigned char *buf = malloc(cap), *back = malloc(chunk_bytes);
if (!cc || !dc || !buf || !back)
die("out of memory");
ZSTD_CCtx_setParameter(cc, ZSTD_c_compressionLevel, level);
ZSTD_CCtx_setParameter(cc, ZSTD_c_checksumFlag, 1);
for (;;) {
pthread_mutex_lock(&next_lock);
uint32_t i = next_chunk++;
pthread_mutex_unlock(&next_lock);
if (i >= nchunks)
break;
const unsigned char *src = img + (uint64_t)i * chunk_bytes;
size_t n = img_bytes - (uint64_t)i * chunk_bytes;
if (n > chunk_bytes)
n = chunk_bytes;
if (all_zero(src, n)) {
outlen[i] = 0;
continue;
}
size_t c = ZSTD_compress2(cc, buf, cap, src, n);
if (ZSTD_isError(c))
die(ZSTD_getErrorName(c));
if (c >= n) { // did not compress: stored as it is
out[i] = malloc(n);
memcpy(out[i], src, n);
outlen[i] = n;
continue;
}
size_t d = ZSTD_decompressDCtx(dc, back, chunk_bytes, buf, c);
if (ZSTD_isError(d) || d != n || memcmp(back, src, n) != 0)
die("a chunk did not decompress to itself");
out[i] = malloc(c);
memcpy(out[i], buf, c);
outlen[i] = c;
}
ZSTD_freeCCtx(cc);
ZSTD_freeDCtx(dc);
free(buf);
free(back);
return 0;
}
int
main(int argc, char **argv)
{
const char *in = 0, *outpath = 0;
long threads = sysconf(_SC_NPROCESSORS_ONLN);
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--chunk") && i + 1 < argc)
chunk_bytes = (uint32_t)atoi(argv[++i]) * 1024;
else if (!strcmp(argv[i], "--level") && i + 1 < argc)
level = atoi(argv[++i]);
else if (!strcmp(argv[i], "--threads") && i + 1 < argc)
threads = atol(argv[++i]);
else if (!in)
in = argv[i];
else if (!outpath)
outpath = argv[i];
else
die("usage: mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]");
}
if (!in || !outpath)
die("usage: mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]");
if (chunk_bytes < 4096 || chunk_bytes > SCFF_CHUNK_MAX ||
(chunk_bytes & (chunk_bytes - 1)))
die("--chunk must be a power of two from 4 to 1024 KB");
if (threads < 1)
threads = 1;
FILE *f = fopen(in, "rb");
if (!f)
die(strerror(errno));
fseek(f, 0, SEEK_END);
img_bytes = (uint64_t)ftell(f);
fseek(f, 0, SEEK_SET);
if (img_bytes == 0 || img_bytes % 4096)
die("the image is not a whole number of 4 KB blocks");
if (!(img = malloc(img_bytes)) || fread(img, 1, img_bytes, f) != img_bytes)
die("cannot read the image");
fclose(f);
nchunks = (uint32_t)((img_bytes + chunk_bytes - 1) / chunk_bytes);
out = calloc(nchunks, sizeof(*out));
outlen = calloc(nchunks, sizeof(*outlen));
if (!out || !outlen)
die("out of memory");
pthread_t *t = calloc((size_t)threads, sizeof(*t));
for (long i = 0; i < threads; i++)
pthread_create(&t[i], 0, worker, 0);
for (long i = 0; i < threads; i++)
pthread_join(t[i], 0);
struct scff_header h;
memset(&h, 0, sizeof(h));
memcpy(h.magic, SCFF_MAGIC, 4);
h.version = SCFF_VERSION;
h.codec = SCFF_CODEC_ZSTD;
h.chunk_bytes = chunk_bytes;
h.nchunks = nchunks;
h.image_bytes = img_bytes;
h.index_off = sizeof(h);
uint64_t *index = malloc(((size_t)nchunks + 1) * sizeof(uint64_t));
uint64_t pos = sizeof(h) + ((uint64_t)nchunks + 1) * sizeof(uint64_t);
for (uint32_t i = 0; i < nchunks; i++) {
index[i] = pos;
pos += outlen[i];
}
index[nchunks] = pos;
h.stored_bytes = pos;
if (!(f = fopen(outpath, "wb")))
die(strerror(errno));
fwrite(&h, sizeof(h), 1, f);
fwrite(index, sizeof(uint64_t), (size_t)nchunks + 1, f);
for (uint32_t i = 0; i < nchunks; i++)
if (outlen[i])
fwrite(out[i], 1, outlen[i], f);
if (fclose(f) != 0)
die("cannot write the output");
printf("mkscff: %llu KB -> %llu KB (%.1f%%), %u chunks of %u KB, zstd -%d\n",
(unsigned long long)(img_bytes >> 10), (unsigned long long)(pos >> 10),
100.0 * (double)pos / (double)img_bytes, nchunks, chunk_bytes >> 10, level);
return 0;
}

View file

@ -97,6 +97,77 @@ mkdir -p "$P/etc/profile.d"
grep -v '^export \(PATH\|HOME\|TERM\)=' "$P/etc/profile" > "$P/etc/profile.d/schos.sh"
rm -f "$P/etc/profile"
# How startx starts the X server for root: Alpine's xserverrc, plus
# -dumbSched. Xorg reads input on a thread of its own, SchOS has no threads
# yet, and the server crashed on its way out the first time a client
# disconnected, in pthread_join on a thread that was never made. -dumbSched is
# Xorg's own switch for input on the main loop. The file is root's own rather
# than /etc/X11/xinit/xserverrc, which belongs to the xinit package.
mkdir -p "$P/root"
cat > "$P/root/.xserverrc" <<'XEOF'
#!/bin/sh
# The VT X runs on is the one startx was started from. startx works that out
# only when `uname -s` says Linux, and SchOS's says SchIFT.
vt=$(tty | sed -n 's|^/dev/tty\([0-9][0-9]*\)$|vt\1|p')
exec /usr/bin/X -nolisten tcp -dumbSched $vt "$@"
XEOF
# What startx runs inside it: Window Maker, which world installs. Without this
# xinit falls back to twm and an xterm, neither of which is in the image.
cat > "$P/root/.xinitrc" <<'XEOF'
#!/bin/sh
exec wmaker
XEOF
# X's input devices, named rather than found. Alpine's Xorg asks udev for
# them, SchOS has no udev, and X came up with no keyboard and no mouse: the
# pointer did not move, Ctrl+Alt+F-keys did nothing and nothing could end the
# session. SchOS has exactly two input nodes whatever the machine -- every
# keyboard on event0, every mouse on event1 (kernel/dev/input.c) -- so a
# fixed list is the whole answer. The file is SchOS's, in xorg.conf.d, which
# no Alpine package writes into.
mkdir -p "$P/etc/X11/xorg.conf.d"
cat > "$P/etc/X11/xorg.conf.d/10-schos.conf" <<'XEOF'
# SchOS has no udev, so X is told its input devices instead of finding them.
# event0 is every keyboard and event1 every mouse (kernel/dev/input.c).
Section "ServerFlags"
Option "AutoAddDevices" "false"
# Ctrl+Alt+Backspace ends X.
Option "DontZap" "false"
EndSection
Section "InputDevice"
Identifier "keyboard"
Driver "evdev"
Option "Device" "/dev/input/event0"
Option "XkbRules" "evdev"
Option "XkbOptions" "terminate:ctrl_alt_bksp"
EndSection
Section "InputDevice"
Identifier "mouse"
Driver "evdev"
Option "Device" "/dev/input/event1"
EndSection
Section "Device"
Identifier "fb"
Driver "fbdev"
EndSection
Section "Screen"
Identifier "screen"
Device "fb"
EndSection
Section "ServerLayout"
Identifier "schos"
Screen "screen"
InputDevice "keyboard" "CoreKeyboard"
InputDevice "mouse" "CorePointer"
EndSection
XEOF
# Mount points and runtime directories are alpine-baselayout's; an empty
# directory here only duplicates it.
for d in dev proc sys run tmp srv; do
@ -110,6 +181,9 @@ sed -i 's|^\(root:[^:]*:0:0:[^:]*:[^:]*:\)/bin/sh$|\1/bin/zsh|' /etc/passwd
for s in /bin/zsh /bin/mksh; do
grep -qx "$s" /etc/shells 2>/dev/null || echo "$s" >> /etc/shells
done
# The machine's own name, which Alpine's hosts leaves out. Without it xauth
# cannot resolve "schos:0", and startx says "bad display name" four times.
grep -qw schos /etc/hosts || printf '127.0.1.1\tschos\n' >> /etc/hosts
exit 0
EOF
cp "$PKG/post-install" "$PKG/post-upgrade"
@ -157,7 +231,8 @@ ROOT_PI=$ROOT sh -c '
sed -i "s|^\(root:[^:]*:0:0:[^:]*:[^:]*:\)/bin/sh$|\1/bin/zsh|" "$ROOT_PI/etc/passwd"
for s in /bin/zsh /bin/mksh; do
grep -qx "$s" "$ROOT_PI/etc/shells" 2>/dev/null || echo "$s" >> "$ROOT_PI/etc/shells"
done'
done
grep -qw schos "$ROOT_PI/etc/hosts" || printf "127.0.1.1\tschos\n" >> "$ROOT_PI/etc/hosts"'
# The repository goes in too, so the installed system can reinstall and
# upgrade schos-base, and Alpine's are listed for everything else.

View file

@ -3,11 +3,16 @@
#
# tools/posix/mkiso.sh [output.iso]
#
# The disc carries one El Torito boot entry and a real directory tree:
# The disc carries one El Torito boot entry and the loader and kernel as files:
#
# /boot/efiboot.img a FAT image holding the loader and the kernel
# /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc
# /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy
# /schos/kernel the same kernel, for chainloaders (below)
#
# The root filesystem is inside the kernel, as its ramdisk. The disc used to
# carry it a third time as an ISO 9660 tree, which nothing reads -- SchOS has
# no ISO 9660 driver -- and once Xorg was in the root that copy alone was
# 360 MB.
#
# --- Why only one entry -----------------------------------------------------
#
@ -69,7 +74,6 @@ cd "$repo"
BUILD=${BUILD:-build}
STAGING="$BUILD/iso"
SYSROOT=${SYSROOT:-sysroot}
KERNEL="$BUILD/kernel/kernel"
EFIAPP="$BUILD/uefi/BOOTX64.EFI"
@ -113,9 +117,6 @@ cp "$KERNEL" "$STAGING/schos/kernel"
# this FAT image -- not the ISO 9660 tree around it. Without the kernel in here
# the loader comes up and halts with EFI_NOT_FOUND, which is what it did.
#
# The ISO 9660 tree still carries the sysroot for a running system to read; the
# loader's own two files live in here because that is the volume it is handed.
#
# Sized from what actually goes in plus slack, rather than fixed: a hardcoded
# size is a thing that breaks quietly the first time the kernel grows.
EFI_IMG="$STAGING/boot/efiboot.img"
@ -145,15 +146,6 @@ if true; then
mcopy -i "$EFI_IMG" "$KERNEL" ::/schos/kernel
fi
# The sysroot, as an ordinary hierarchy. Copied rather than linked so that
# whatever ends up on the disc is a snapshot, not a view of a tree that may be
# rebuilt underneath it.
if [ -d "$SYSROOT" ]; then
for d in "$SYSROOT"/*; do
[ -e "$d" ] || continue
cp -a "$d" "$STAGING/"
done
fi
# xorriso if it is here, mkisofs otherwise. Both accept the El Torito flags
# below unchanged -- xorriso's -as mkisofs mode exists precisely to be
@ -173,6 +165,11 @@ if command -v xorriso >/dev/null 2>&1; then
# of type 0xEF is what every UEFI firmware looks for on a removable
# disk, MBR rather than GPT because the oldest ones read that best.
HYBRID="-append_partition 2 0xef $EFI_IMG"
# El Torito keeps its own copy of the image in the tree, the layout real
# machines have booted. Pointing it into the partition instead
# (--interval:appended_partition_2:all::, as Arch does) saves a copy, and
# an ASUS laptop stopped listing the stick the day that was tried. With
# the root compressed (SCFF) the copy is only the kernel's size.
elif command -v mkisofs >/dev/null 2>&1; then
MKISOFS="mkisofs"
HYBRID=""

View file

@ -495,8 +495,11 @@ try {
Start-Sleep -Milliseconds 150
$t = Read-Log
if ($null -eq $t -or $t.Length -le $Since) { continue }
# A prompt at the very end, with nothing typed after it.
if ($t.Substring($Since) -match '(?s)#[ ]?$') { return $true }
# A prompt at the very end, with nothing typed after it: mksh's `#`,
# or zsh's `»`, which root's shell has been since the root became
# Alpine's. zsh follows it with erase-to-end-of-line and turns
# bracketed paste on, so both escapes may trail the prompt.
if ($t.Substring($Since) -match '(?s)(#|\u00bb)(\e\[[0-9;]*m)*[ ]?(\e\[K)?(\e\[\?2004h)?$') { return $true }
}
return $false
}
@ -629,6 +632,12 @@ if ($null -eq $text) { $text = '' }
Write-Host "==> serial transcript ($($text.Length) bytes) -> $log" -ForegroundColor Cyan
Write-Host $text
# Matched without terminal escapes. busybox's ls, which is the Alpine root's,
# colours what it lists -- `copy.txt` arrives as ESC[1;32mcopy.txt -- and zsh
# wraps every prompt in them, so a pattern anchored at a line start found
# nothing in output that was perfectly right. Shown raw above, matched clean.
$text = $text -replace '\e\[[0-9;?]*[A-Za-z]', '' -replace '\e[()][0-9A-Za-z]', '' -replace '[\x0e\x0f]', ''
$script:fail = $false
function Expect($pattern, $what) {
if ($script:text -match $pattern) {
@ -697,7 +706,10 @@ Expect 'schfs: mounted' 'the kernel mounted SchFS as the root filesystem'
Expect 'init: ready' 'init came up on a SchFS root'
Expect 'label:\s*schos' '/proc/schfs reports the volume label'
Expect 'root_objectid:\s*16' '/proc/schfs reports the root object id'
Expect 'dir\s+bin' 'the root directory really contains bin/'
# /proc/schfs lists the first 16 entries, in the tree's order, and on the
# Alpine root bin/ is the seventeenth: any of the top-level directories proves
# the same thing.
Expect '(?m)^\s*dir\s+(bin|etc|usr)\s' 'the root directory really contains bin/, etc/ or usr/'
# / is the mounted volume, so mounting that same device again must be
# refused. Without this a kernel that had quietly fallen back to some other
# root would still pass everything else here.