diff --git a/src/sched.c b/src/sched.c index e36828a..7faf2bb 100644 --- a/src/sched.c +++ b/src/sched.c @@ -1010,6 +1010,119 @@ static void reclaimUserSlot(uint32 id, const Thread* thread) } /* Every open fd a thread still holds when it dies represents a live + reference into some other subsystem's own refcounted table -- fd 0's + console reader queue (keyboard.h, allocated unconditionally by vfsFdInit + regardless of whether the thread ever reads stdin, see vfs.c) and any + fdPipeRead/fdPipeWrite fd (pipe.h's readers/writers counts on the + underlying handle, from vfsPipe/vfsPipeAttach/a named fifo's vfsOpen). + vfsClose already decrements both correctly, but only for a LIVE thread + closing its OWN fd through currentThread() -- a thread that dies + (threadKill, or simply returns/threadExit) while still holding either + open skips vfsClose entirely, so without this the memset below would + just drop the handle on the floor. + For the console reader this was caught live (FALA 13): the reader table + is a small fixed pool (maxConsoleReaders), and schedTest.c alone creates + and tears down dozens of throwaway threads while proving the sched table + itself reuses freed slots (threadCreateExhaustsThenThreadDiscardFrees...), + which permanently exhausted the ENTIRELY SEPARATE console reader pool + before boot ever reached kmain.c's real spawnUser calls -- every + boot-time process's fd 0 silently went non-blocking (keyboardOpenReader + returning consoleReaderInvalid), so consolereader1.c/consolereader2.c's + userRead(0, ...) returned -1 immediately instead of blocking for the + synthetic keystroke. + The pipe/fifo case is the same shape of leak, just against pipe.c's own + readers/writers counters instead of a fixed pool: the OTHER end of a + shared pipe (a second, independent process, e.g. fifomaster.c's + children) would never see the true EOF/broken-pipe pipeRead/pipeWrite + report once every attached end on this side is gone, and would instead + retry "try again" forever -- exactly the resource-exhaustion shape + bug#2 above had, just not yet observed breaking anything live because + nothing today loses a pipe/fifo fd without an orderly close() first (see + schedTest.c's reclaimOpenFds tests, which prove the leak by fault + injection rather than waiting for a live symptom). */ +static void reclaimOpenFds(const Thread* thread) +{ + for (uint32 i = 0; i < maxOpenFiles; i++) { + const FileDescriptor* fd = &thread->fds[i]; + if (fd->kind == fdConsole && fd->pipeHandle >= 0) + keyboardCloseReader(fd->pipeHandle); + else if (fd->kind == fdPipeRead) + pipeCloseRead(fd->pipeHandle); + else if (fd->kind == fdPipeWrite) + pipeCloseWrite(fd->pipeHandle); + else if (fd->kind == fdPidfd) + pidfdClose(fd->pipeHandle); + } +} + +bool threadReap(uint32 id) +{ + if (id == 0 || id >= maxThreads) + return false; + if (threads[id].state != threadDead) + return false; + + reclaimUserSlot(id, &threads[id]); + reclaimOpenFds(&threads[id]); + if (threads[id].stackBase) + kfree((void*)threads[id].stackBase); + memset(&threads[id], 0, sizeof(Thread)); + /* memset leaves state at 0, which is threadUnused -- exactly the value + allocSlot() scans for when looking for a slot to reuse. */ + return true; +} + +bool threadDiscard(uint32 id) +{ + if (id == 0 || id >= maxThreads) + return false; + if (threads[id].state != threadRunnable || threads[id].everRan) + return false; + + if (threads[id].userSlotBase != 0) + pagingUnmapUserRange(threads[id].userSlotBase, elfSlotSize); + reclaimOpenFds(&threads[id]); + if (threads[id].stackBase) + kfree((void*)threads[id].stackBase); + memset(&threads[id], 0, sizeof(Thread)); + return true; +} + +int64 threadJoin(uint32 id) +{ + if (id == 0 || id >= maxThreads) + return -1; + + for (;;) { + ThreadState state = threads[id].state; + if (state == threadUnused) + return -1; + if (state == threadDead) + break; + schedBlockCurrent(&threads[id]); + halt(); + } + + uint64 code = threads[id].exitCode; + threadReap(id); + return (int64)code; +} + +WaitScanResult threadJoinAnyChild(uint32 parentId, uint32* outId, int64* outCode) +{ + bool hasChild = false; + for (uint32 i = 1; i < threadTotal; i++) { + if (threads[i].state == threadUnused || threads[i].parentId != parentId) + continue; + hasChild = true; + if (threads[i].state == threadDead) { + *outId = i; + *outCode = (int64)threads[i].exitCode; + threadReap(i); + return waitScanFoundDead; + } + } + return hasChild ? waitScanNoneDead : waitScanNoChildren; } void threadSleep(uint64 milliseconds) @@ -1021,18 +1134,107 @@ void threadSleep(uint64 milliseconds) threads[currentIndex].state = threadSleeping; } +uint64 threadSetAlarm(uint64 seconds) +{ + Thread* self = &threads[currentIndex]; + uint64 now = pitTicks(); + + uint64 previousRemaining = 0; + if (self->alarmAt != 0 && self->alarmAt > now) { + uint64 remainingTicks = self->alarmAt - now; + uint32 freq = pitFrequency(); + /* Ceiling division: a deadline anywhere inside the current second + still counts as "1 second remaining", never "0" -- matching real + alarm()'s own contract that a not-yet-fired alarm never reports + 0 remaining (0 is reserved for "none pending" everywhere in this + API, see the header comment). */ + previousRemaining = (remainingTicks + freq - 1) / freq; + } + + if (seconds == 0) + self->alarmAt = 0; + else + self->alarmAt = now + seconds * (uint64)pitFrequency(); + + return previousRemaining; +} + +void schedBlockCurrent(const void* channel) +{ + threads[currentIndex].waitChannel = channel; + threads[currentIndex].state = threadBlocked; +} + +const void* schedPollChannel(void) +{ + static const int pollChannelToken; + return (const void*)&pollChannelToken; +} + +void schedWakeChannel(const void* channel) +{ + for (uint32 i = 0; i < threadTotal; i++) { + if (threads[i].state == threadBlocked && threads[i].waitChannel == channel) { + threads[i].waitChannel = nullptr; + threads[i].state = threadRunnable; + } + } +} + void threadSetBrk(uint32 id, uint64 brk) { + if (id >= maxThreads) + return; threads[id].brk = brk; } +uint64 threadUserSlotBase(uint32 id) +{ + if (id >= maxThreads) + return 0; + return threads[id].userSlotBase; +} + +void threadSetUserSlotBase(uint32 id, uint64 base) +{ + if (id >= maxThreads) + return; + threads[id].userSlotBase = base; +} + +bool threadSetPriority(uint32 id, uint32 weight) +{ + if (id == 0 || id >= maxThreads || weight == 0) + return false; + ThreadState state = threads[id].state; + if (state == threadDead || state == threadUnused) + return false; + + threads[id].weight = weight; + return true; +} + +uint32 threadPriority(uint32 id) +{ + if (id >= maxThreads || threads[id].state == threadUnused) + return 0; + return threads[id].weight; +} + +uint64 threadVruntime(uint32 id) +{ + if (id >= threadTotal) + return 0; + return threads[id].vruntime; +} + uint64 currentThreadSbrk(int64 increment) { Thread* thread = &threads[currentIndex]; uint64 previous = thread->brk; if (increment > 0) - pagingMapUserRange(previous, (uint64)increment); + pagingMapUserRangeFlags(previous, (uint64)increment, pageNoExecute); thread->brk = previous + (uint64)increment; return previous;