feat: extend sched with reclaimOpenFds and threadReap

This commit is contained in:
dualfroz committed 2026-09-07 13:25:41 +02:00
1 parent 48b66a86f7
commit 3376f44f5e
1 file changed
+203 -1
+203 -1
View File
@@ -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 /* 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) void threadSleep(uint64 milliseconds)
@@ -1021,18 +1134,107 @@ void threadSleep(uint64 milliseconds)
threads[currentIndex].state = threadSleeping; 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) void threadSetBrk(uint32 id, uint64 brk)
{ {
if (id >= maxThreads)
return;
threads[id].brk = brk; 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) uint64 currentThreadSbrk(int64 increment)
{ {
Thread* thread = &threads[currentIndex]; Thread* thread = &threads[currentIndex];
uint64 previous = thread->brk; uint64 previous = thread->brk;
if (increment > 0) if (increment > 0)
pagingMapUserRange(previous, (uint64)increment); pagingMapUserRangeFlags(previous, (uint64)increment, pageNoExecute);
thread->brk = previous + (uint64)increment; thread->brk = previous + (uint64)increment;
return previous; return previous;