SchOS/etc/schinit
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Nexoniarz d1f50c4da3 hybrid: the driver is killed, and the driver comes back
H2 of docs/plan/hybrid.md -- the milestone the plan itself calls the one
that proves the whole idea, and the one that cannot be skipped.

    # cat /proc/schinit
    1 4 11 os.schos.vgpud
    # kill 4
    schinitd: restarting os.schos.vgpud
    vgpud: claimed
    vgpud: pid 8 drew a test pattern from userspace

The test pattern ends in a twelve-block tally of the drawing process's
pid, because a second run that redraws an identical picture is
indistinguishable from a first run that never stopped. Read off the
virtio-gpu scanout across four generations it says 4, 8, 10, 12, and the
DMA arena hands back the same physical address every time.

What it took:

* A halt the kernel can apply to a device it does not understand.
  Clearing PCI_CMD_BUS_MASTER is the only one there is, and it has to
  happen before the DMA goes back to the arena -- a dead driver's device
  keeps reading a framebuffer the kernel is about to give to somebody
  else. Claim sets the bit, release clears it; pcitest shows it in a bm
  column, with a third state so a failed read is never printed as off.

* A refusal. vgpu_release() asks vga_fb_restore() whether there is a
  framebuffer to put the console back on, and declines to hand over the
  device if there is not. The rule that the kernel keeps a minimal
  driver for everything on the path to a shell is worth writing down
  only if something enforces it.

* The DMA arena not being owned by two allocators at once. This is the
  bug that stopped H2: freevm() frees every mapped page below PHYSTOP
  and the arena is the top 16 MB of exactly that range, so a DMA block
  mapped into a driver went onto the page allocator's free list when the
  driver died, while pciuser_release_all() handed the same pages back to
  the arena. The restarted vgpud asked for its framebuffer, got pages
  kalloc() had already given out -- including its own page tables -- and
  dma_alloc()'s zeroing wiped them, so it died inside the ioctl on the
  return from a syscall whose stack had stopped existing. do_dma() now
  records a VMA with no_free set, the way shared memory does.

* One exit status. KeepAlive must restart a driver that crashed and must
  not restart one that has looked at the machine and found its device
  absent: on hardware with no virtio-gpu that was fifteen spawns in
  forty-five seconds, each printing a line to the console, for ever.
  SCHINIT_EXIT_NORESTART is 78, sysexits.h's EX_CONFIG. It is not a new
  manifest key -- the wait status has always travelled from PID 1 to the
  supervisor, and note_exit() simply stopped discarding it.

The milestone's third clause, "the compositor reattaches", is not done
and is not claimed: there is no compositor running. That is recorded in
the plan as a Schcom milestone rather than counted here.

Closes the register row "A released PCI device" in docs/plan/1.0.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 00:05:43 +02:00
..
console-shell.job tty: six virtual terminals, and the plumbing that makes them terminals 2026-09-13 00:08:00 +02:00
README hybrid: the driver is killed, and the driver comes back 2026-09-14 00:05:43 +02:00
tty2-shell.job tty: six virtual terminals, and the plumbing that makes them terminals 2026-09-13 00:08:00 +02:00
vgpud.job hybrid: the driver is killed, and the driver comes back 2026-09-14 00:05:43 +02:00

Job manifests for schinit, SchOS process 1.

Every *.job file in this directory is loaded at boot. Format is sectioned
key = value; # starts a comment, keys are case-insensitive.

  Label             a reverse-DNS name, as launchd uses. Identity, not a path.
  Program           the binary to execute
  Args              argv, space separated. argv[0] is conventionally the
                    basename, so `Args = sh` for `/bin/sh`.
  RunAtLoad         start it when schinit loads
  KeepAlive         restart it when it exits
  Disabled          load it but do not start it
  ThrottleInterval  seconds between respawns (default 1)
  StandardIn        \
  StandardOut        > paths to open as fd 0, 1, 2
  StandardError     /

  [Socket <name>]
  Path              a unix socket schinit will hold on the job's behalf and
                    start the job on first connect

A KeepAlive job that exits with status 78 (sysexits.h's EX_CONFIG) is not
restarted, and schinit says which job stopped and that it stopped on purpose.
This is for a program that has looked at the machine and found nothing here it
can run against -- a driver whose device is absent, say -- which would otherwise
report the same thing every ThrottleInterval for the rest of the uptime. A job
that was killed or crashed carries a signal in its status, never matches, and
comes back exactly as KeepAlive promises.

Socket sections are parsed but not yet acted on -- that is milestone 2, and
demand launching is the whole reason this is shaped after launchd rather than
sysvinit. The key exists now so manifests do not have to change later.

Unknown keys and unknown sections are ignored rather than fatal: a manifest
written for a newer schinit must not stop this one booting the machine.