Files
frostbyte/include/sched.h
T
2026-09-01 21:39:05 +02:00

303 lines
17 KiB
C

#ifndef FROSTBYTE_SCHED_H
#define FROSTBYTE_SCHED_H
#include "types.h"
#include "vfs.h"
#include "prioq.h"
#include "isr.h"
#include "signalnum.h"
#include "mmap.h"
/* Default scheduling weight (xv6/CFS-style "nice" niceWeight -- higher runs
more often, not longer). threadSetPriority scales relative to this. */
#define schedDefaultWeight 1024u
/* Returned by threadCreate/threadCreateUser when the thread table is full
(see threadCapacity) -- never a valid thread id, since id 0 is always the
permanent idle thread. Callers must check for this before using the
result to index into anything thread-related. */
#define threadInvalidId 0xFFFFFFFFu
/* Capacity of Thread.name, including the terminator. Large enough for every
name this kernel currently produces (kernel thread literals, "/bin/..."
paths passed to threadCreateUser by spawnUser/doSpawn) with room to
spare; longer names are truncated, never overflowed. */
#define threadNameMax 32
/* Capacity of Thread.env below: envMaxVars is how many "NAME=VALUE" slots
a process may hold at once, envEntryMax is the max size in bytes of a
single slot INCLUDING both the '=' separator and the nul terminator
(so "NAME=VALUE\0" must fit in envEntryMax bytes). Generous enough for
anything this kernel's own shell/demos actually set (see user/envprint.c)
with room to spare, the same "small fixed table, not a growable heap
allocation" choice Thread.cwd already made for a single path. */
#define envMaxVars 8
#define envEntryMax 64
/* One bit per signal number in Thread.sigPending/sigBlocked, and the size
of Thread.sigHandlers[] below -- see signal.h for the actual signal
subsystem this supports. Defined here, not signal.h, purely to break the
include cycle (signal.h includes sched.h for the Thread type). */
#define sigMaxSignals 32
/* Depth of the per-thread nested-handler context stack (Thread.sigSavedRegs/
sigSavedBlocked below) -- how many signal handlers may genuinely interrupt
each other (handler A running, a second signal arrives and is not blocked
by A's own sa_mask, so handler B runs nested inside A, then A resumes once
B returns) before sigConsumePending (signal.c) refuses to deliver a new
one and simply leaves it pending. A real kernel bounds this too (rlimit
on the signal-frame stack it pushes into user memory); this one has no
user-memory signal frame to overflow at all (see signal.h's file comment
-- the saved context lives in the Thread struct itself), so the bound
exists purely to keep that struct's size fixed and to fail a runaway
signal-storm safely (further deliveries just wait) instead of unboundedly
growing kernel memory per thread. 4 is generous headroom for anything
this kernel's own handlers actually do (none nest more than one deep
today -- see user/nesttarget.c) without being large enough to make
Thread noticeably bigger than sigMaxSignals-worth of handlers already
makes it. */
#define sigMaxNestDepth 4
typedef enum {
threadUnused = 0,
threadRunnable = 1,
threadRunning = 2,
threadSleeping = 3,
threadDead = 4,
threadBlocked = 5,
/* Job-control stop (signal.h's SIGSTOP/SIGTSTP): excluded from
schedule()'s ready-queue rebuild exactly like Sleeping/Blocked
already are (only threadRunnable is ever inserted), so a stopped
thread simply never gets picked until threadContinue moves it back
to threadRunnable. Distinct from threadBlocked because it is NOT
waiting on a wait-channel condition -- schedWakeChannel must never
treat it as a match (see schedWakeChannel), only threadContinue
(an explicit SIGCONT, not "the resource became available") ever
moves a thread out of this state. */
threadStopped = 6
} ThreadState;
typedef void (*ThreadEntry)(void);
typedef struct Thread {
uint64 rsp;
uint64 stackBase;
uint64 kernelStackTop;
uint64 sleepUntil;
/* Absolute pitTicks() deadline for a pending alarm(2)-style SIGALRM
(0 == no alarm pending), set by threadSetAlarm and consulted once
per timer tick by schedule() -- the exact same shape as sleepUntil
above, checked in the exact same loop, except expiry raises a real
signal (sigRaise, signal.h) against the owning thread instead of
just flipping its own ThreadState back to Runnable. Deliberately a
plain deadline, not a wait-channel/ThreadState change: unlike
threadSleep, a thread with a pending alarm keeps running (or
sleeping, or blocked on something unrelated) exactly as it already
was right up until the deadline, exactly like real alarm()'s timer
ticking down in the background regardless of what the process is
doing. */
uint64 alarmAt;
/* poll.h's sysPoll (syscall.c) uses the same restart-the-syscall park
pattern sysRead/sysWrite/sysWait already use, but a poll call can be
woken by any one of several unrelated fds, not one -- so instead of
parking on a single fd's own wait channel (vfsBlockingChannel),
sysPoll blocks every waiting thread on ONE shared channel
(schedPollChannel below) and simply re-checks its whole fd list from
scratch on every wake, spurious or not, exactly like the retry loop
every OTHER restartable syscall already has. pollActive marks "this
thread is mid a not-yet-satisfied sysPoll call" (so a retry entry
can tell itself apart from a genuinely fresh call); pollDeadline is
the absolute pitTicks() deadline schedule()'s own expiry scan forces
a wake at if no fd ever becomes ready first (0 == wait forever,
meaningful only while pollActive is true -- the same "0 reserved"
convention alarmAt above already uses, but here 0 is a real,
legal, requested state (a timeout of -1) rather than "nothing
pending"). */
bool pollActive;
uint64 pollDeadline;
uint64 brk;
uint64 vruntime;
uint64 exitCode;
uint32 weight;
uint32 id;
/* Id of whichever thread was actually running (currentIndex, sched.c)
at the moment this thread was created -- threadCreate/
threadCreateUser both set this unconditionally, so it is always
meaningful, not just for real userspace parent/child pairs: a
boot-time spawnUser (kmain.c) call happens before schedStart() ever
runs, so every process kmain.c spawns directly gets the permanent
idle thread (id 0) as its parentId, which is deliberately never a
valid sigRaise target (see signal.c's targetId==0 rejection) --
only a RUNTIME sysSpawn (syscall.c's doSpawn, called from inside a
live thread's own syscall) ever produces a parentId that is itself
a live, signalable process. Consulted by threadExit/threadKill
below to deliver a real SIGCHLD to whoever spawned this thread the
instant it actually dies, matching real wait()/SIGCHLD semantics --
NOT the same thing as "whoever later calls threadJoin/sysWait on
this id", which can be anyone that knows the pid, not just the
spawner (this kernel's honest fork()-less design already documents
that distinction, see doSpawn's own file comment in syscall.c). */
uint32 parentId;
/* Distinguishes THIS specific occupant of slot `id` from any other
thread that has occupied (or will ever occupy) the same numeric slot
before or after -- the exact same "handle number alone is not enough
once a slot is recycled" idea pipe.c's own Pipe.incarnation already
solves for pipe handles (see pipeIncarnation's own doc comment,
pipe.h). Assigned once, at creation time, from a single monotonic
counter (sched.c's nextThreadGeneration, never reused, mirroring
pipe.c's nextPipeIncarnation exactly) -- threadReap/threadDiscard's
memset resets this back to 0 when the slot is freed, and the next
threadCreate/threadCreateUser to land on that slot gets a brand new,
never-before-seen value, so 0 here always means "this exact slot is
currently unoccupied," never a live thread's own generation. Exists
purely so pidfd.h's pidfdOpen/pidfdTarget can tell "the same thread I
opened a handle on earlier" apart from "a different, unrelated thread
that has since been handed the same slot number" -- allocSlot's own
doc comment (sched.c) documents the real, previously-unguarded race
this closes for any caller that opts into pidfd.h instead of a raw
pid (kill/wait/sigSend/setpgid/nice below all still take a raw pid,
completely unchanged, exactly as that same comment explains why). */
uint32 generation;
/* Process group id (POSIX pgid): the scope real job control and real
kill(2)/sigSend's pid<0 convention actually target, not a single
thread (signalnum.h's sigInt/sigTstp arriving from keyboard.c's real
Ctrl-C/Ctrl-Z tracking are the live proof -- see keyboard.c's
keyboardHandleScancode and signal.c's sigRaiseGroup). Set once, at
creation time, by threadCreate/threadCreateUser's own initialPgid
helper (sched.c): a thread created while the idle thread (id 0) is
"current" -- i.e. one of kmain.c's boot-time spawnUser/threadCreate
calls, the only time that can ever be true, see parentId's own
comment just above -- becomes its own new group leader (pgid == its
own id), exactly like a real init process placing each service it
starts into its own job; a thread created by a genuinely running
parent (any runtime sysSpawn, syscall.c's doSpawn) instead INHERITS
that parent's pgid, matching real fork()'s own default (a child
stays in its parent's process group until something explicitly
calls setpgid). threadSetPgid (below) is the only thing that ever
changes it again after creation -- the sysSetpgid syscall's
userspace-facing half of real setpgid(2). */
uint32 pgid;
/* Real POSIX credential (uid(2)/setuid(2)): the identity vfs.c's
vfsPermitted consults, alongside a file's own owner uid and mode, to
decide owner-vs-other access at exactly the two enforcement choke
points fs.h's fsModeOwner and fsModeOther bits already gate
(resolveOpen's write check, loadImageByPath/jobctlSpawn's exec check) -- see
vfsPermitted's own doc comment for the full owner/other algorithm
(deliberately no uid-0-bypasses-everything superuser rule -- see that
same comment for why). Same creation-time default/copy split as pgid
immediately above (sched.c's new initialUid, mirroring initialPgid's
own "is the idle thread currently running" test): a boot-time thread
(kmain.c's spawnUser/threadCreate calls, idle currently "current")
starts as uid 0 ("root" in name only here, not in VFS privilege --
see fs.h's fsModeOwner* doc comment), exactly like a real init
process; a genuine runtime child (syscall.c's doSpawn) instead
INHERITS its parent's uid, matching real fork()'s own default before
any setuid ever runs. currentThreadSetUid (below) is the only thing
that ever changes it again after creation -- and, matching real
setuid(2)'s own privilege rule, only ever lets uid 0 become anyone;
any other uid may only "become" itself (a no-op), so dropping root is
real and permanent for the lifetime of this thread, exactly like a
real daemon that setuid()s away from root at startup and can never
regain it. */
uint32 uid;
/* Real POSIX group credential (getgid(2)/setgid(2)): the group identity
vfs.c's vfsPermitted consults, alongside a file's own group gid, as
the MIDDLE fallback between the owner and other checks fs.h's
fsModeGroup* bits gate. Same creation-time default/copy split as uid
immediately above (sched.c's new initialGid, mirroring initialUid's
own shape exactly): a boot-time thread starts at gid 0, a genuine
runtime child (syscall.c's doSpawn) INHERITS its parent's gid.
currentThreadSetGid (below) is the only thing that ever changes it
again -- unlike currentThreadSetUid, the privilege gate for THIS
call is the calling thread's own uid (currentThreadUid() == 0), not
its gid, matching real setgid(2): a privileged (root) PROCESS may
setgid to any group; an unprivileged one may only pass its own
current gid back in (a no-op), so dropping to a group is real and
permanent for the lifetime of a non-root thread, same shape as uid's
own one-way drop. */
uint32 gid;
/* Real per-process working directory (POSIX chdir(2)/getcwd(2)): always
a fully resolved absolute path (vfs.h's vfsChdir resolves a relative
argument against the CALLING thread's own cwd before ever storing
here, so this field itself never needs re-resolving against
anything). Set once, at creation time, by threadCreate/
threadCreateUser's own initialCwd helper (sched.c) -- a thread
created while the idle thread (id 0) is "current" (kmain.c's
boot-time spawns, see parentId's own comment above) starts at "/",
exactly like a real init process; a thread created by a genuinely
running parent (any runtime sysSpawn, syscall.c's doSpawn) instead
INHERITS a byte-for-byte COPY of that parent's cwd, matching real
fork()'s own copy-not-share semantics -- the two are independent
buffers from that point on, so a child's later chdir() can never
reach back and change its parent's, and vice versa. Before this
field existed, the only cwd concept anywhere in this kernel was a
single static local in shell.c shared by nothing else -- a spawned
user process calling open()/mkdir()/etc with a relative path had
never actually worked, since vfs.c's lookupNode/sfsLookup always
resolved relative to the filesystem root, never to any notion of
"current directory" (see vfs.c's vfsResolvePath, the only thing that
reads this field). */
char cwd[fsPathMax];
/* Real per-process environment variables (POSIX getenv(3)/setenv(3)):
up to envMaxVars "NAME=VALUE\0" strings, each at most envEntryMax-1
bytes, plus envCount tracking how many slots are actually in use.
Same creation-time default/copy split as Thread.cwd immediately
above (sched.c's new initialEnv, mirroring initialCwd's own shape):
a boot-time thread (idle is currently running) starts with an EMPTY
table, like a real init process before anything ever calls
setenv(); a genuine runtime child (syscall.c's doSpawn) instead
inherits a byte-for-byte COPY of its parent's table, so a child's
later setenv() can never reach back and change its parent's, and
vice versa -- the exact same independent-buffer guarantee cwd
already gives. */
char env[envMaxVars][envEntryMax];
int envCount;
ThreadState state;
/* Set once, permanently, the first time schedule() ever actually
switches execution to this thread. Distinguishes "freshly created,
never scheduled even once" (state threadRunnable, everRan false) from
"was running and got preempted, still Runnable" (everRan true) -- the
two look identical by state alone, but only the former is safe for
threadDiscard to tear down (see threadDiscard). */
bool everRan;
/* Fixed buffer, not a pointer: threadCreateUser/doSpawn (syscall.c) may
be called with a name that lives in a caller's stack-local buffer
(a path just copied in from userspace), which does not outlive the
call. thread->name must remain valid for the thread's entire life
(shell.c's `ps` listing and the exit log both read it long after
creation), so the bytes are always copied in, never aliased -- see
threadSetName/copyThreadName in sched.c. */
char name[threadNameMax];
const void* waitChannel;
/* Base virtual address of this thread's elfSlotSize-sized user-memory
slot (image + stack + brk-heap all live within
[userSlotBase, userSlotBase+elfSlotSize), see elf.h) -- 0 for kernel
threads (threadCreate), which own no user pages to give back.
Set from the entry point by threadCreateUser at spawn time and
updated by exec() (syscall.c doExec) when a thread's image moves to
a different slot. threadReap/threadDiscard pass this to
pagingUnmapUserRange so a dead or rolled-back process's physical
frames are actually reclaimed instead of leaking forever. */
uint64 userSlotBase;
PrioQNode schedNode;
FileDescriptor fds[maxOpenFiles];
} Thread;
void schedInit(void);
uint32 threadCreate(const char* name, ThreadEntry entry);
uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop);
uint64 schedule(uint64 currentRsp);
void schedStart(void);
void threadExit(uint64 code);
void threadSleep(uint64 milliseconds);
void threadSetBrk(uint32 id, uint64 brk);
uint64 currentThreadSbrk(int64 increment);
uint32 threadCount(void);
Thread* threadAt(uint32 index);
Thread* currentThread(void);
uint32 currentThreadId(void);
#endif