// SPDX-License-Identifier: Apache-2.0 // // 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 // 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