From 103abbb35f05878a2d592515836c75f67a670755 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 11:18:40 +0200 Subject: [PATCH] feat: grow sched --- include/sched.h | 208 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) diff --git a/include/sched.h b/include/sched.h index 64d63e6..d6dfc2b 100644 --- a/include/sched.h +++ b/include/sched.h @@ -533,6 +533,214 @@ void threadSleep(uint64 milliseconds); void threadSetBrk(uint32 id, uint64 brk); uint64 currentThreadSbrk(int64 increment); +/* Real alarm(2): schedules a real SIGALRM (signalnum.h's sigAlrm) against + the CALLING thread only, `seconds` real seconds from now (converted to + pitTicks() ticks via pitFrequency(), the same conversion threadSleep + already uses for milliseconds) -- never another thread, exactly like + POSIX alarm() has no target-pid argument at all. `seconds` == 0 cancels + any alarm already pending for this thread instead of scheduling a new + one, matching real alarm(0). Returns the number of whole seconds that + remained on a PREVIOUSLY pending alarm for this thread (rounded up, so a + deadline even one tick in the future still reports at least 1, exactly + like real alarm() never reports "0 remaining" for one that has not + actually fired yet), or 0 if none was pending -- the caller's own + previous alarm is silently replaced (or cleared) either way, matching + real alarm()'s "at most one pending alarm per process" contract. Actual + delivery happens later, from schedule() (sched.c), once pitTicks() + reaches the deadline this call records. */ +uint64 threadSetAlarm(uint64 seconds); + +/* Weighted-fair scheduling (CFS-lite): every thread starts at + schedDefaultWeight. A thread with weight W accrues virtual runtime + schedDefaultWeight/W times as fast as a default-weight thread each time + it runs, so it is picked from the ready queue proportionally more often + -- not given a longer timeslice, since this scheduler's granularity is + already one timer tick. Returns false (no-op) for the same targets + threadSetPgid already refuses -- the idle thread (id 0), an out-of-range + id, or a slot that is not currently a live thread (Dead or Unused) -- + plus a zero weight, which used to be silently ignored (kmain.c-only + caller never triggered it) before this became reachable from userspace + through sysNice (syscall.c). Was a kernel-internal helper only used + once, at boot, for workerA/workerB's own priority demo (kmain.c) -- + still is, but ALSO now the mechanism sysNice's syscall.c handler calls, + so its own validation had to catch up to threadSetPgid's, not stay at + the older, weaker "just check threadTotal" bound that let a userspace + nice() call quietly reweight thread 0 (idle) or a slot no thread has + ever occupied. */ +bool threadSetPriority(uint32 id, uint32 weight); + +/* Returns thread `id`'s current scheduling weight, or 0 (never a real + weight -- threadSetPriority above always rejects weight 0, and every + thread starts at schedDefaultWeight) for an out-of-range id or a slot + that has never been a live thread (Unused) -- same "0 doubles as the + error return, since it is never a valid one" shape threadPgid below + has with threadInvalidId. A Dead-but-not-yet-reaped thread still + reports its last real weight, exactly like threadPgid still reports + its last real pgid for the same state -- see that function's own + comment for why. */ +uint32 threadPriority(uint32 id); +uint64 threadVruntime(uint32 id); + +/* Returns thread `id`'s process group id (Thread.pgid above), or + threadInvalidId for an out-of-range id or a slot that is not currently a + live thread -- the same "invalid id sentinel doubles as an error return" + convention threadInvalidId already has for allocation failure, safe here + because a real pgid is always some other live thread's own id (never + 0xFFFFFFFF, since ids never reach that high, see threadCapacity). */ +uint32 threadPgid(uint32 id); + +/* Real setpgid(2)'s underlying mechanism: unconditionally sets thread + `id`'s pgid to `pgid`, whatever value the caller resolved "0 means self"/ + "0 means use pid itself as the new group id" down to (that resolution is + the SYSCALL layer's job, syscall.c's sysSetpgid -- mirroring how + threadSetBrk/threadSetPriority/threadSetAlarm above already take a fully + resolved value, not a raw syscall argument). Deliberately does not + validate that `pgid` names any other real thread at all: this kernel has + no session concept to check membership against (real setpgid(2) would + reject joining a group outside the caller's own session), so -- like + several other omissions this codebase documents honestly rather than + fakes -- any uint32 is accepted, exactly as if every process were in one + giant session. Returns false, changing nothing, for id 0 (idle -- it has + no job-control identity to reassign) or an out-of-range/Dead/Unused id. */ +bool threadSetPgid(uint32 id, uint32 pgid); + +/* Real kill(-pgid, SIGKILL)-shaped bulk termination: calls threadKill on + every currently live thread (Runnable/Sleeping/Blocked/Stopped, per + threadKill's own accepted set) whose pgid equals `pgid`, one at a time, + through the ordinary single-target threadKill above -- so its own + per-target SIGCHLD-to-parent notification and wait-channel wakes already + apply per victim exactly as they would for a plain, non-group kill, + rather than this function duplicating any of that. A pgid whose only + member is the CALLER's own currently-Running thread naturally kills + nobody (threadKill excludes Running, i.e. self, by construction, the + same "self-kill via plain kill(2) is not really different" reasoning + userKill's own doc comment already gives) without needing a special + case here. Returns false, killing nobody, if no currently live thread + has this pgid at all -- the real ESRCH case for kill(-pgid, ...) with no + matching group. */ +bool threadKillGroup(uint32 pgid); + +/* Real getcwd(2)/chdir(2)'s underlying per-thread state (Thread.cwd above): + always the CALLING thread's own directory, never another pid's -- unlike + threadSetPgid, there is no `id` parameter here at all, since POSIX's + chdir() has no cross-process form the way setpgid(2) does for a child + still between fork() and exec(). currentThreadSetCwd does no validation + of its own (mirrors threadSetPgid's own "that is the syscall layer's + job" split) -- vfs.c's vfsChdir is what actually confirms `path` names a + real, already-resolved-to-absolute directory before ever calling this; + a `path` longer than fsPathMax-1 is rejected here (false, unchanged) + since it could never round-trip back out through currentThreadCwd(). */ +const char* currentThreadCwd(void); +bool currentThreadSetCwd(const char* path); + +/* Real getuid(2)/setuid(2)'s underlying per-thread state (Thread.uid + above): always the CALLING thread's own identity, mirroring + currentThreadCwd's own "no id parameter, no cross-process form" shape + exactly (POSIX setuid() has no cross-process form either -- a process can + only ever change its OWN credential). currentThreadSetUid enforces the + real privilege rule itself (this kernel's syscall layer does no + additional validation of its own, unlike currentThreadSetCwd's split): + uid 0 ("root") may become anyone; any other uid may only pass its own + current value back in (a no-op that still reports success, matching real + setuid(getuid())), and any other target is rejected, state unchanged. + There is deliberately no way back to uid 0 once dropped, short of + already being uid 0 at the time of the call -- the same one-way + privilege-drop shape a real setuid daemon relies on. */ +uint32 currentThreadUid(void); +bool currentThreadSetUid(uint32 newUid); + +/* Real getgid(2)/setgid(2)'s underlying per-thread state (Thread.gid above) + -- same "no id parameter, no cross-process form" shape as currentThreadUid + immediately above. currentThreadSetGid's privilege gate is the CALLING + thread's own currentThreadUid() (== 0), not its gid -- matching real + setgid(2), whose privilege check is CAP_SETGID (effectively "is this + process root"), never based on the gid being changed itself. A + non-privileged thread may only pass its own current gid back in (a + no-op success, matching real setgid(getgid())); any other target is + rejected, state unchanged. */ +uint32 currentThreadGid(void); +bool currentThreadSetGid(uint32 newGid); + +/* Real getenv(3)/setenv(3)'s underlying per-thread state (Thread.env + above): always the CALLING thread's own table, mirroring + currentThreadCwd/currentThreadSetCwd's own "no id parameter, no + cross-process form" shape exactly (POSIX setenv() has no cross-process + form either). currentThreadGetEnv returns a pointer straight into the + matching slot's own VALUE half (never a copy) or nullptr if `name` has + no entry -- that pointer stays valid only until the next + currentThreadSetEnv call on the SAME name, which may overwrite the slot + in place. currentThreadSetEnv overwrites an existing entry with a + matching `name` in place, or appends a new one if a slot is still free + under envMaxVars; returns false, table unchanged, if `name`+'='+`value` + would not fit in envEntryMax bytes, or if `name` is new and every slot + is already taken. */ +const char* currentThreadGetEnv(const char* name); +bool currentThreadSetEnv(const char* name, const char* value); + +/* Real unsetenv(3): removes the calling thread's own entry named `name`, + if any, compacting the table (memmove of every later slot down by one) + so envCount and currentThreadEnvEntry's own index space never develop a + hole -- unlike currentThreadSetEnv's in-place overwrite, this changes + WHICH index every later entry lives at. Returns true if an entry was + actually removed, false (table unchanged) if `name` had no entry -- + matching real unsetenv(3), which is a no-op success either way, but the + bool lets a caller (shell.c's `unset` builtin) tell the difference if it + ever wants to. */ +bool currentThreadUnsetEnv(const char* name); + +/* Iterates the calling thread's own environment table for listing + (shell.c's `env` builtin): currentThreadEnvCount() is how many of the + envMaxVars slots are actually in use; currentThreadEnvEntry(index) + returns the raw "NAME=VALUE" string stored at that slot, or nullptr for + an out-of-range index. */ +int currentThreadEnvCount(void); +const char* currentThreadEnvEntry(int index); + +/* Copies up to threadNameMax-1 bytes of `name` into thread `id`'s own name + buffer (truncating, always nul-terminated). Used by exec() (syscall.c) so + a process's visible identity (shell `ps`, the exit log) reflects the new + image after it replaces the old one -- the same way a real process's + name changes across execve(). Invalid id is a silent no-op. */ +void threadSetName(uint32 id, const char* name); + +/* Base of thread `id`'s user-memory slot (see Thread.userSlotBase above), + or 0 for an out-of-range id or a kernel thread that owns no user slot. + Used by exec() (syscall.c) to find the OLD slot to reclaim before it + overwrites its own image with a new one. */ +uint64 threadUserSlotBase(uint32 id); + +/* Records which elfSlotSize-sized slot thread `id` currently occupies. + Only exec() (syscall.c) needs this directly -- threadCreateUser sets it + itself from the entry point it is given. Invalid id is a silent no-op. */ +void threadSetUserSlotBase(uint32 id, uint64 base); + +/* Wait-queue primitives ("sleep on a channel", xv6-style). A channel is any + stable address the caller controls (e.g. the address of the resource being + waited on) - it is never dereferenced, only compared for identity. + + schedBlockCurrent marks the calling thread as blocked on a channel; it does + NOT itself give up the CPU (this scheduler is timer-preemptive, not + cooperative - callers park with halt() the same way threadSleep callers + already do, see kmain.c). schedWakeChannel moves every thread blocked on a + matching channel back to runnable; it is always safe to call even when no + thread is waiting (no-op) and must be called by whichever side of a + producer/consumer relationship changes the condition the other side is + waiting on. */ +void schedBlockCurrent(const void* channel); +void schedWakeChannel(const void* channel); + +/* The one wait channel every not-yet-satisfied sysPoll call blocks on (see + Thread.pollActive's own comment above) -- any code path that can make a + previously-not-ready fd become ready (pipe.c's pipeWrite/pipeRead + freeing space, pipeCloseRead/pipeCloseWrite breaking a pipe, + keyboard.c's keyboardHandleScancode enqueueing a keystroke) calls + schedWakeChannel(schedPollChannel()) in addition to whatever + fd-specific channel it already wakes, so every blocked poller gets a + chance to re-check its own fd list. Always returns the same address for + the life of the kernel -- not tied to any particular fd or pipe, unlike + pipeChannel/keyboardReaderChannel. */ +const void* schedPollChannel(void); + uint32 threadCount(void); Thread* threadAt(uint32 index); Thread* currentThread(void);