303 lines
17 KiB
C
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
|