include: one folder per subsystem, and includes that name it
Sixty-five headers sat side by side in include/, so `#include "irq.h"` said nothing about where irq.h was or what it belonged to, and errno.h, stat.h and fcntl.h at the top shadowed the system's for any host tool built with -I./include. They are in folders now -- arch/ core/ fs/ dev/ gfx/ net/ uapi/ (and ldls/ for the Linux driver layer) -- and every include names the folder, as Linux's do: "core/proc.h". Done mechanically and checked the hard way: the kernel built from this tree disassembles identically to the one built from the tree before it. The full image boots, and userland is the same set of programs. Paths in docs and comments follow; docs/guides/layout.md has a table of what is where, and tools/fix-includes.py rewrites old-style includes from the folders in the tree, for code written before this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
0e39e711f1
commit
a1c965da1e
267 changed files with 1560 additions and 1440 deletions
143
include/uapi/schinit.h
Normal file
143
include/uapi/schinit.h
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// The protocol between PID 1 and the supervisor.
|
||||
//
|
||||
// docs/architecture/init.md warns about this file specifically: two
|
||||
// processes that must agree is a compatibility surface where there was none,
|
||||
// and docs/plan/schapi.md §3 is about how those go wrong. So the rules are
|
||||
// written here rather than assumed.
|
||||
//
|
||||
// **Fixed-size messages.** Every message is exactly one struct, and PID 1
|
||||
// reads sizeof(struct) or nothing. No length prefix to get wrong, no
|
||||
// partial-parse state to be caught in, and -- the reason that matters --
|
||||
// **PID 1 never allocates**. A supervisor that has gone mad cannot make PID 1
|
||||
// allocate, block on a malloc, or run a parser.
|
||||
//
|
||||
// **Versioned in the first message.** SCHINIT_PROTO travels in the hello. A
|
||||
// mismatch is a refusal, not a best-effort interpretation.
|
||||
//
|
||||
// **Entirely internal.** This is not SchAPI. Nothing but schinit and
|
||||
// schinitd may speak it, and it may change shape whenever both change
|
||||
// together, which is what keeping it in one header is for.
|
||||
//
|
||||
// The asymmetry is the design, not an accident: the supervisor *asks*, PID 1
|
||||
// *acts*. Every job on the machine is therefore a child of PID 1 at all times,
|
||||
// so a supervisor that dies orphans nothing and reparents nothing.
|
||||
|
||||
#ifndef SCHINIT_H
|
||||
#define SCHINIT_H
|
||||
|
||||
#define SCHINIT_PROTO 1
|
||||
|
||||
#define SCHINIT_MAXARGS 16
|
||||
#define SCHINIT_MAXENVV 16
|
||||
#define SCHINIT_MAXLABEL 64
|
||||
#define SCHINIT_MAXPATH 128
|
||||
// One flat buffer for argv and envp strings, copied whole. Pointers cannot
|
||||
// cross a process boundary, so the spec carries text and PID 1 rebuilds the
|
||||
// arrays on its own stack after exec'ing -- which is the only place they are
|
||||
// ever valid.
|
||||
#define SCHINIT_STRBUF 1024
|
||||
|
||||
// supervisor -> PID 1
|
||||
#define SCHINIT_HELLO 1 // struct schinit_hello
|
||||
#define SCHINIT_SPAWN 2 // struct schinit_spawn
|
||||
#define SCHINIT_PONG 3 // struct schinit_hello -- answer to a PING
|
||||
|
||||
// PID 1 -> supervisor
|
||||
#define SCHINIT_WELCOME 128 // struct schinit_hello, echoed with PID 1's version
|
||||
#define SCHINIT_SPAWNED 129 // struct schinit_reply -- pid, or negative on failure
|
||||
#define SCHINIT_EXITED 130 // struct schinit_reply -- a child has been reaped
|
||||
#define SCHINIT_PING 131 // struct schinit_hello -- answer within the deadline
|
||||
|
||||
// Exit statuses a job can use to tell the supervisor something.
|
||||
//
|
||||
// KeepAlive restarts a job that crashed or was killed, which is the whole
|
||||
// point of it for a driver. It must not restart one that has looked at the
|
||||
// machine and reported there is nothing here for it to do: a driver whose
|
||||
// device is absent fails identically every time, and a supervisor that tries
|
||||
// anyway spends the rest of the uptime doing so -- 15 spawns in 45 seconds,
|
||||
// on hardware with no virtio-gpu, each one printing a line to the console.
|
||||
//
|
||||
// 78 is sysexits.h's EX_CONFIG, "something was found in an unconfigured or
|
||||
// misconfigured state", which is exactly what a driver with no device has
|
||||
// found. Borrowed rather than invented, so a program written against any
|
||||
// other Unix already means the right thing by it.
|
||||
//
|
||||
// Note what this is *not*: it is not a new manifest key and not a new policy
|
||||
// knob. The wait status has always travelled from PID 1 to the supervisor in
|
||||
// struct schinit_reply, and note_exit() has always thrown it away. This is
|
||||
// the other half of a thing that was already built.
|
||||
#define SCHINIT_EXIT_NORESTART 78
|
||||
|
||||
// The heartbeat.
|
||||
//
|
||||
// docs/architecture/init.md, "The heartbeat" -- the failure the two-init scheme
|
||||
// could not see:
|
||||
// **the common way an init breaks is not dying, it is hanging.** wait() never
|
||||
// returns for a process that is merely stuck, so a supervisor watching a
|
||||
// wedged PID 1 -- or PID 1 watching a wedged supervisor -- reports everything
|
||||
// normal forever.
|
||||
//
|
||||
// So each must be heard from. PID 1 sends a PING every interval and expects a
|
||||
// PONG; silence past the deadline means the other end is stuck rather than
|
||||
// busy, and stuck is the case worth acting on.
|
||||
//
|
||||
// Measured in seconds of uptime(), which counts ticks. Ticks can be *lost*
|
||||
// when interrupts are off too long -- see kernel/core/time.c -- which makes a
|
||||
// tick-measured deadline longer in real time than it looks, never shorter.
|
||||
// That is the safe direction: a machine under load waits longer before
|
||||
// declaring a hang, rather than declaring one that has not happened.
|
||||
#define SCHINIT_PING_SECS 2
|
||||
#define SCHINIT_DEADLINE_SECS 10
|
||||
|
||||
struct schinit_hello {
|
||||
unsigned int op;
|
||||
unsigned int proto;
|
||||
unsigned int generation; // how many times PID 1 has been re-exec'd
|
||||
unsigned int reserved;
|
||||
};
|
||||
|
||||
// A job, flattened. `args` and `envs` index into `strbuf`, which holds
|
||||
// NUL-terminated strings back to back.
|
||||
struct schinit_spawn {
|
||||
unsigned int op;
|
||||
unsigned int cookie; // echoed in the reply, so the supervisor
|
||||
// can match a spawn to the job it asked for
|
||||
char label[SCHINIT_MAXLABEL];
|
||||
char program[SCHINIT_MAXPATH];
|
||||
|
||||
char stdin_path[SCHINIT_MAXPATH];
|
||||
char stdout_path[SCHINIT_MAXPATH];
|
||||
char stderr_path[SCHINIT_MAXPATH];
|
||||
|
||||
unsigned int own_tty; // claim the terminal as controlling
|
||||
unsigned int nargs;
|
||||
unsigned int nenvs;
|
||||
unsigned short args[SCHINIT_MAXARGS]; // offsets into strbuf
|
||||
unsigned short envs[SCHINIT_MAXENVV];
|
||||
char strbuf[SCHINIT_STRBUF];
|
||||
};
|
||||
|
||||
struct schinit_reply {
|
||||
unsigned int op;
|
||||
unsigned int cookie;
|
||||
int pid; // SPAWNED: the child, or negative errno
|
||||
int status; // EXITED: wait status
|
||||
};
|
||||
|
||||
// The largest message, which is what a reader must have room for. PID 1 reads
|
||||
// into one of these on its own stack and never anywhere else.
|
||||
union schinit_msg {
|
||||
unsigned int op;
|
||||
struct schinit_hello hello;
|
||||
struct schinit_spawn spawn;
|
||||
struct schinit_reply reply;
|
||||
};
|
||||
|
||||
// The descriptor the supervisor inherits. Fixed rather than passed, because
|
||||
// the supervisor is exec'd by PID 1 and there is nowhere to pass it -- and a
|
||||
// well-known number is one less thing for the two to disagree about.
|
||||
#define SCHINIT_SOCK_FD 3
|
||||
|
||||
#endif // SCHINIT_H
|
||||
Loading…
Reference in a new issue