feat: add pidfd and pipe

This commit is contained in:
dualfroz committed 2026-09-01 20:49:05 +02:00
1 parent 9e9c28fe80
commit 25f50f156a
2 files changed
+201

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
#ifndef FROSTBYTE_PIDFD_H
#define FROSTBYTE_PIDFD_H
#include "types.h"
#define maxPidfds 32
/*
* Real Linux precedent: pidfd_open(2) (Linux 5.3+) exists for exactly the
* ABA-style race sched.c's own allocSlot doc comment describes and
* deliberately leaves unfixed for plain pid arguments -- killtest.c's own
* comment already admits it ("killing it again must fail cleanly, not
* resurrect a slot that has since been reused") without ever guarding
* against it. A raw numeric pid can always be reassigned to a completely
* unrelated process once its original holder is reaped, so a pid cached
* from an earlier spawn/lookup is fundamentally unsafe to act on later
* with no way to tell "still the same process" apart from "coincidentally
* the same number now". Real Linux's own fix was not to make pid itself
* opaque -- every existing kill(2)/waitpid(2)/setpgid(2) caller still
* takes a raw pid, completely unchanged, forever -- but to add a SEPARATE,
* opt-in handle type that pins one specific incarnation of a slot instead.
* This file mirrors that shape exactly: pidfdOpen captures the target
* thread's CURRENT sched.c Thread.generation (the same "distinguish this
* specific occupant from any future occupant of the same numeric slot"
* idea pipe.c's own pipeIncarnation already uses for pipe handles), and
* every later pidfd operation re-checks it before ever touching the
* target -- if the original thread has since been reaped (its generation
* reset to 0 by threadReap/threadDiscard's memset) or the slot has been
* handed to a brand new, unrelated thread (a different, fresh nonzero
* generation), the pidfd cleanly reports "gone" (threadInvalidId) instead
* of silently operating on whichever live thread now happens to occupy
* that slot number. sched.c's raw pid-based threadKill/threadJoin/
* sigRaiseInfo/threadSetPgid/threadSetPriority are deliberately untouched
* by this file -- see allocSlot's own doc comment (sched.c) for why that
* remains a separate, much larger, deferred topic.
*/
/* Opens a new pidfd handle on thread `targetId`, pinning its CURRENT
generation (sched.h). Returns a small non-negative handle for
pidfdTarget/pidfdClose below, or -1 for id 0 (idle, never a valid
target), an out-of-range id, an already-unused slot, or a full pidfd
table (maxPidfds handles system-wide, mirroring pipe.c's own fixed-size
maxPipes table). Does not itself keep the target alive or pinned in any
way -- it can still exit, be killed, and be reaped by anyone (this
kernel's pids are never ownership-scoped, see sched.h's parentId doc
comment) while a pidfd on it is open; that is exactly the case
pidfdTarget below exists to detect. */
int pidfdOpen(uint32 targetId);
/* Returns the live target thread id this handle still safely refers to,
or threadInvalidId if the handle itself is invalid/closed OR the
original target has been reaped or recycled since pidfdOpen -- the one
check every pidfd-based operation needs, folded into a single call so
no caller can accidentally act on a stale target by forgetting a
separate validity check. A target that has exited but not yet been
reaped (threadDead) still returns its real id here -- exactly like a
raw pid, waiting on it should still be able to observe the real exit
status once, via whatever reaps it. */
uint32 pidfdTarget(int handle);
/* Frees a pidfd handle. No-op on an already-invalid handle. Never affects
the target thread itself in any way -- a pidfd, like a real Linux one,
is purely an observer/signaler, not a reference that keeps anything
alive. */
void pidfdClose(int handle);
#endif
+134
View File
@@ -0,0 +1,134 @@
#ifndef FROSTBYTE_PIPE_H
#define FROSTBYTE_PIPE_H
#include "types.h"
#define maxPipes 32
#define pipeBufferSize 256
/*
* Anonymous pipes: a fixed table of ring-buffer-backed byte queues, each
* shared between one read end and one write end. The raw pipeRead/pipeWrite
* primitives are always non-blocking, matching real O_NONBLOCK semantics:
*
* pipeRead:
* >0 bytes actually read
* 0 end-of-file: buffer drained AND the write end is closed
* -1 no data right now but the write end is still open (retry later)
*
* pipeWrite:
* >0 bytes actually written
* 0 length was 0
* -1 broken pipe (no read end left), or no free space right now
* (retry later)
*
* pipeReadBlocking/pipeWriteBlocking are built on top: they park the calling
* kernel thread on the pipe's wait channel (sched.h) instead of returning
* "try again", and are woken as soon as the other end changes the condition
* (data arrives, space frees up, or either end closes). They only make sense
* from kernel-thread context (see the producer/consumer demo in kmain.c) -
* a single process's own fd table cannot hand one pipe end to a second,
* independent execution context by itself.
*
* pipeCreateDetached + pipeAddReader/pipeAddWriter (below) close that gap
* without needing fork(): a handle from pipeCreateDetached starts with
* NEITHER end attached (unlike pipeCreate, which assumes its caller
* immediately owns both), so it can be handed to two entirely separate,
* independently spawned processes -- typically as a decimal string in argv
* (see user/fifomaster.c) -- each of which attaches its own end via
* vfsPipeAttach (vfs.h). syscall.c's sysRead/sysWrite give a real blocking
* read()/write() to whichever of those two ends is waiting, using the same
* restart-the-syscall trick sysWait already uses for threadJoin.
*/
int pipeCreate(void);
/* Like pipeCreate, but neither end starts "attached": readers and writers
both begin at 0, and pipeRead/pipeWrite treat "nobody has attached that
end YET" as "try again" rather than the true EOF/broken-pipe result they
report once an end that WAS attached goes away (see everHadReader/
everHadWriter, pipe.c) -- otherwise a writer that attaches before any
reader does would see an immediately, permanently "broken" pipe, and a
reader that attaches first would see immediate spurious EOF. */
int pipeCreateDetached(void);
/* Registers a new, independent reader/writer on an existing (typically
pipeCreateDetached'd) handle -- vfsPipeAttach (vfs.h) calls these when a
process attaches its end. No-op on an invalid handle. */
void pipeAddReader(int handle);
void pipeAddWriter(int handle);
/* True if a write to this pipe can never succeed again: either the handle
is not valid, or a reader was attached at some point and every attached
reader has since gone away. False for a pipeCreateDetached handle that
simply has not had its reader attach yet -- that is "try again", not
broken. Lets syscall.c's sysWrite tell "keep retrying" apart from "give
up and return -1" without reaching into pipe.c's own Pipe struct. */
bool pipeWriteBroken(int handle);
/* The address schedBlockCurrent/schedWakeChannel use for this pipe's wait
queue -- the same one pipeReadBlocking/pipeWriteBlocking already block
on. Exposed so a syscall handler (syscall.c) can park on it via the
restart-the-syscall pattern instead of calling halt() directly (which,
run from inside an interrupt-gate syscall handler with IF=0, would freeze
the whole machine rather than just the calling thread -- see sysWait's
own doc comment in syscall.c for the same hazard). Returns nullptr for
an invalid handle. */
const void* pipeChannel(int handle);
bool pipeValid(int handle);
/* Opaque token identifying THIS specific pipe living at `handle`'s slot,
distinct from any other pipe that has occupied (or will ever occupy) the
same numeric handle before or after -- this fixed-size table reuses a
slot number the moment both its ends are gone (pipeCloseRead/
pipeCloseWrite's pipeReleaseIfUnused), so pipeValid(handle) alone cannot
tell a caller that cached a handle across such a release "the same pipe I
bound to earlier" apart from "a different, unrelated pipe that happens to
have been assigned the same number since" (see vfs.c's SFS-backed named
fifo bindings, sfsFifoHandleFor, the motivating caller). Always non-zero
for any handle pipeCreate/pipeCreateDetached has ever handed out; 0 for
an invalid handle, which can never equal a real incarnation, so a naive
"compare and treat 0 as impossible" check is always safe. */
uint32 pipeIncarnation(int handle);
int64 pipeRead(int handle, void* buffer, uint64 length);
int64 pipeWrite(int handle, const void* buffer, uint64 length);
/* Non-consuming readiness peeks for poll.h's vfsPollReady (vfs.c): report
exactly the same two conditions pipeRead/pipeWrite's own success paths
check (data queued or true EOF; free space in the ring), without moving
a single byte or touching readers/writers bookkeeping. False for an
invalid handle either way. */
bool pipeHasData(int handle);
bool pipeHasSpace(int handle);
/* Blocks (parks the calling thread and halts) until data is available or
true end-of-file is reached. Returns exactly what pipeRead would once the
wait is over; -1 only for an invalid handle (never blocks in that case). */
int64 pipeReadBlocking(int handle, void* buffer, uint64 length);
/* Blocks until free space is available. Returns exactly what pipeWrite would
once the wait is over; returns -1 immediately without blocking for an
invalid handle or a broken pipe (no reader left), since neither condition
can ever resolve by waiting. */
int64 pipeWriteBlocking(int handle, const void* buffer, uint64 length);
void pipeCloseRead(int handle);
void pipeCloseWrite(int handle);
/* Frees a pipeCreateDetached handle that nobody has ever attached a reader
or writer to (readers==0 and writers==0) -- the one case pipeCloseRead/
pipeCloseWrite's own release-when-both-zero check (pipeReleaseIfUnused)
can never reach on its own, since neither is ever called for an end that
was never attached in the first place. Needed by a named pipe (vfs.h's
vfsMkfifo/vfsUnlink) created but never opened by anyone: without this, its
underlying handle would sit "used" in the fixed-size pipe table forever
once the vfs node naming it is removed. No-op (and reports false) if the
handle is invalid or still has a reader or writer attached -- unlinking a
named pipe that IS currently open must leave it alone, matching real
unlink-while-open semantics (existing fds keep working off the raw handle
number, not the vfs node). */
bool pipeDestroyIfUnattached(int handle);
#endif