#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