diff --git a/CHANGELOG.md b/CHANGELOG.md index 83124f0..e9eb3e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), Versioning is similar to [semver](https://semver.org) but is more flexible and similar to how linux does it, and uses custom tags (like `-dev` and `-rc`). +## 0.9-dev - 2026-10-2 + +### Added +- `ipc/` filled in with message passing and synchronisation between kernel tasks, replacing the stubs. +- `IpcPort` in `ipc/message.h`, a FIFO of `IpcPortCapacity` (16) fixed size messages. `IpcSend()` waits while the port is full and `IpcReceive()` waits while it is empty, so two tasks can hand work back and forth without polling. `IpcTrySend()` and `IpcTryReceive()` are the non blocking forms. +- `IpcSemaphore` and `IpcMutex` in `ipc/sync.h`. A semaphore takes a ceiling, so a `Limit` of 1 is a binary semaphore and `SemPost()` past the ceiling reports `IpcErrFull` rather than letting the count run away. +- `IpcStatus` for every entry point, so a refused call says why instead of failing silently. +- A shared wait queue in `ipc/ipc.c`. `IpcPark()` registers the calling task and sleeps as one step under the interrupt lock, so a waker that runs in between either finds the task parked or is seen by the re-check afterwards. A wakeup cannot be lost. +- Wait queues keyed by task id rather than `Task *`, and `IpcWaitTakeLive()` drops entries left by a task that was killed, since a dead task can never run again. +- Boot tests in `kernel_main()` covering a 40 message rendezvous between two tasks, more than a port can hold so both the send and receive paths really do park, a binary semaphore, and two tasks contending on one mutex for several hundred rounds. +- `ipc/` added to the `Makefile` VPATH and its objects to `KERNEL_OBJS`. + +### Changed +- `MutexUnlock()` hands ownership straight to the first waiter instead of clearing the owner, so a task that merely happens to be ready cannot take the lock ahead of the task that was woken for it. +- `ipc/message.h` and `ipc/sync.h` are no longer listed as planned interfaces, since their contracts are documented in `Documentation/kernel_api.md` now. + +### Known issues +- Nothing here is reentrant, so calling into `ipc/` from an interrupt handler is not supported yet. +- A wait queue holds `IpcMaxWaiters` (16) entries. Parking on a 17th waiter returns `IpcErrFull` rather than overwriting an entry. +- Ports, semaphores, and mutexes are static kernel objects rather than something a task can allocate, so two tasks cannot share one that lives on only one of their stacks. + ## 0.8 - 2026-10-02 Stable release. See `0.8-dev` for the release contents and `0.8-rc2` for the final candidate fixes. diff --git a/Documentation/kernel_api.md b/Documentation/kernel_api.md index 9a4df4a..93374f1 100644 --- a/Documentation/kernel_api.md +++ b/Documentation/kernel_api.md @@ -335,12 +335,137 @@ robin scheduling chooses the next ready task by increasing task ID and wraps back to the start of the ready queue. `SchedGetStats()` needs a non-null output pointer. +## Inter-process communication + +`ipc/` carries messages and synchronisation between kernel tasks. Every object is +a fixed size value that the caller owns, so a task never allocates to talk to +another one and nothing is left behind when a task dies. Nothing here is +reentrant, so do not call into it from an interrupt handler. + +Every call returns an `IpcStatus`: + +```c +typedef enum { + IpcOk = 0, + IpcErrInvalid, /* null or out of range argument */ + IpcErrFull, /* a bounded queue or counter is already at its limit */ + IpcErrEmpty, /* nothing to take */ + IpcErrBusy, /* a non blocking call could not proceed right now */ + IpcErrOwner, /* unlock from a task that does not hold the mutex */ + IpcErrNoTask /* the caller cannot sleep and the call would have to */ +} IpcStatus; +``` + +The blocking calls sleep through `SchedBlock()` and are woken with +`SchedWakeTask()`. Two rules follow from how the scheduler works here: + +- The idle task cannot sleep, so a call that would have to wait returns + `IpcErrNoTask` instead of parking. `timer_sleep_ms()` follows the same rule. +- A blocking call may return `IpcErrFull` when a wait queue is full. Queues hold + `IpcMaxWaiters` (16) entries, so this needs 16 tasks parked on one object. + +A woken task re-checks the condition in a loop rather than assuming the wakeup +still applies, so a spurious wakeup costs a retry and never a lost message. + +### Message ports + +```c +#define IpcMessageWords 8 +#define IpcPortCapacity 16 + +typedef struct IpcMessage { + u32 Length; /* words of Data in use, at most IpcMessageWords */ + u32 Sender; /* task id of the sender, filled in by IpcSend */ + u32 Data[IpcMessageWords]; +} IpcMessage; + +typedef struct IpcPort { /* private, build one with IpcPortCreate */ }; + +IpcStatus IpcPortCreate(IpcPort *Port); +IpcStatus IpcPortDestroy(IpcPort *Port); +IpcStatus IpcSend(IpcPort *Port, const IpcMessage *Message); +IpcStatus IpcTrySend(IpcPort *Port, const IpcMessage *Message); +IpcStatus IpcReceive(IpcPort *Port, IpcMessage *out); +IpcStatus IpcTryReceive(IpcPort *Port, IpcMessage *out); +u32 IpcPortQueued(const IpcPort *Port); +bool IpcPortIsCreated(const IpcPort *Port); +``` + +A port is a FIFO of up to `IpcPortCapacity` messages. A message is copied by +value, so the sender's buffer is free as soon as the call returns and neither +side holds a pointer into the other's memory. `IpcSend()` fills in `Sender` with +the sending task's id. + +`IpcSend()` waits while the port is full and `IpcReceive()` waits while it is +empty, so the two sides can hand work back and forth without polling. The `Try` +forms never wait and return `IpcErrFull` or `IpcErrEmpty` instead. + +A port carries a magic word, so `IpcPortCreate()` on a port that is already in +use fails with `IpcErrInvalid` rather than emptying a queue another task is +reading. For the same reason `IpcPortDestroy()` refuses with `IpcErrBusy` while +any task is parked on the port, since dropping those waiters would strand them. +`Length` above `IpcMessageWords` is rejected as `IpcErrInvalid`. Every call +checks its arguments, so a null or uncreated port reports `IpcErrInvalid` +instead of touching memory. + +### Semaphores and mutexes + +```c +typedef struct IpcSemaphore { u32 Count; u32 Limit; IpcWait Wait; } IpcSemaphore; +typedef struct IpcMutex { bool Locked; u32 Owner; IpcWait Wait; } IpcMutex; + +void SemInit(IpcSemaphore *Sem, u32 Initial, u32 Limit); +IpcStatus SemWait(IpcSemaphore *Sem); +IpcStatus SemTryWait(IpcSemaphore *Sem); +IpcStatus SemPost(IpcSemaphore *Sem); +u32 SemCount(const IpcSemaphore *Sem); + +void MutexInit(IpcMutex *Mutex); +IpcStatus MutexLock(IpcMutex *Mutex); +IpcStatus MutexTryLock(IpcMutex *Mutex); +IpcStatus MutexUnlock(IpcMutex *Mutex); +bool MutexHeldByCaller(const IpcMutex *Mutex); +``` + +`SemInit()` takes a ceiling. A `Limit` of 0 is an unbounded counter and a +`Limit` of 1 is a binary semaphore; `SemPost()` past the ceiling returns +`IpcErrFull` rather than letting the count grow. An initial count above the +ceiling is clamped down. + +A mutex is not recursive: `MutexLock()` on a mutex the caller already holds +returns `IpcErrOwner` instead of parking on a lock nobody else can open. +`MutexUnlock()` from a task that does not hold the mutex also returns +`IpcErrOwner`. Unlock passes ownership straight to the first waiter instead of +clearing the owner and letting a task that merely happens to be ready take it, +so a wakeup and the lock stay in step. `Locked` is tracked separately from +`Owner` because the idle task is id 0 as well, so an owner of 0 cannot mean +free. + +### Wait queues + +```c +typedef struct IpcWait { u32 Ids[IpcMaxWaiters]; u32 Count; } IpcWait; +typedef bool (*IpcReadyFn)(void *arg); + +u32 IpcSelfId(void); +IpcStatus IpcPark(IpcWait *Wait, IpcReadyFn Ready, void *arg); +void IpcWakeOne(IpcWait *Wait); +u32 IpcWaitTakeLive(IpcWait *Wait); +``` + +These are the pieces ports and semaphores are built from. `IpcPark()` registers +the calling task and sleeps in one step while interrupts are off, so a waker +that runs in between either finds the task parked or is seen by the re-check +afterwards. A wakeup is never lost and a parked entry is never left behind. +`IpcWaitTakeLive()` drops entries whose task was killed, since a dead task can +never run again, and `IpcWakeOne()` calls `SchedWakeTask()` for the first +waiter that is still alive. + ## Planned interfaces These headers are still placeholders, so they are not usable kernel APIs yet: - `include/drivers/framebuffer.h` - `fs/vfs.h` and `fs/initrd.h` -- `ipc/message.h` and `ipc/sync.h` We will document their contracts here when the implementations land. diff --git a/Documentation/todo.md b/Documentation/todo.md index 01a6db7..2752f31 100644 --- a/Documentation/todo.md +++ b/Documentation/todo.md @@ -10,7 +10,6 @@ For the history of everything completed so far, see [CHANGELOG.md](../CHANGELOG. ## Next up: Filesystem - [ ] Simple filesystem (VFS + ext2) -- [ ] Basic IPC between kernel threads/processes --- @@ -31,4 +30,4 @@ For the history of everything completed so far, see [CHANGELOG.md](../CHANGELOG. ## Notes -- `ipc/`, `fs/`, have stubs but they'll get filled in as the kernel progresses. +- `fs/` has stubs but it will get filled in as the kernel progresses. diff --git a/Makefile b/Makefile index 402cb45..4384dac 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ VERSION = 0 -PATCHLEVEL = 8 +PATCHLEVEL = 9 SUBLEVEL = 0 EXTRAVERSION = VERSION_TAG = $(VERSION).$(PATCHLEVEL)$(if $(filter-out 0,$(SUBLEVEL)),.$(SUBLEVEL))$(EXTRAVERSION) @@ -12,7 +12,7 @@ GRUB_MKRESCUE ?= grub-mkrescue ARCH ?= x86_32 BUILD_DIR = build -VPATH = init drivers/video drivers/input kernel lib mm arch/$(ARCH)/boot arch/$(ARCH)/kernel +VPATH = init drivers/video drivers/input kernel lib mm ipc arch/$(ARCH)/boot arch/$(ARCH)/kernel DRIVE_FLAGS ?= -hda QEMU_FLAGS ?= -m 64M -serial stdio @@ -66,7 +66,10 @@ KERNEL_OBJS = \ $(BUILD_DIR)/sched.o \ $(BUILD_DIR)/process.o \ $(BUILD_DIR)/switch.o \ - $(BUILD_DIR)/heap.o + $(BUILD_DIR)/heap.o \ + $(BUILD_DIR)/ipc.o \ + $(BUILD_DIR)/message.o \ + $(BUILD_DIR)/sync.o # Header dependencies collected by -MMD, one .d next to each .o KERNEL_DEPS = $(KERNEL_OBJS:.o=.d) diff --git a/README.md b/README.md index 46324e0..57a05a8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Novium OS is a hobby x86 operating system built from scratch. The architecture is heavily inspired by Linux, but stripped down to the absolute basics. If you are into low-level engineering, feel free to open a PR or share ideas. > [!NOTE] -> Directories like `ipc/` and `fs/` currently contain early stubs. +> The `fs/` directory still contains early stubs. ## Project structure @@ -39,6 +39,7 @@ GRUB finds and loads the kernel, checks the Multiboot header, and hands control - [x] Switch to GRUB bootloader - [x] Added a basic Memory Manager (PMM/VMM) - [x] Basic multi-page and sub-page heap allocator +- [x] IPC: message ports, semaphores, and mutexes See [CHANGELOG.md](CHANGELOG.md) for official release history See [Documentation/todo.md](Documentation/todo.md) for the detailed task list and future plans. diff --git a/init/main.c b/init/main.c index 628848b..959bf0a 100644 --- a/init/main.c +++ b/init/main.c @@ -1,5 +1,8 @@ #include #include +#include +#include +#include #include #include #include @@ -12,6 +15,33 @@ #define TASK_STACK_SIZE 4096 static u8 ShellStack[TASK_STACK_SIZE] __attribute__((aligned(16))); static u8 WorkerStack[TASK_STACK_SIZE] __attribute__((aligned(16))); +static u8 ConsumerStack[TASK_STACK_SIZE] __attribute__((aligned(16))); +static u8 ProducerStack[TASK_STACK_SIZE] __attribute__((aligned(16))); +static u8 LockAStack[TASK_STACK_SIZE] __attribute__((aligned(16))); +static u8 LockBStack[TASK_STACK_SIZE] __attribute__((aligned(16))); + +/* + * More messages than a port can hold, so the rendezvous below cannot pass by + * accident: the producer has to park on a full port and the consumer on an + * empty one, over and over, and both sides have to be woken the right number + * of times for the counts to line up. + */ +#define IPC_TEST_MESSAGES 40 +#define IPC_TEST_TIMEOUT 200 +/* Enough trips through the mutex to show two tasks really do alternate. */ +#define IPC_TEST_ROUNDS 50 + +static IpcPort TestPort; +static IpcSemaphore TestSem; +static IpcMutex TestMutex; +static volatile u32 ConsumerDone; +static volatile u32 ProducerDone; +static volatile u32 ConsumerBad; +/* Id of the sending task, so the consumer checks the stamp without guessing. */ +static volatile u32 ProducerId; +static volatile u32 LockRounds; +static volatile u32 LockStop; +static volatile u32 LockBad; void shell_task(void) { kprintf("System ready. You can now type inside the console:\n"); @@ -41,6 +71,218 @@ void worker_task(void) { } } +/* Reads every message back and checks the order and the sender id. */ +static void ipc_consumer_task(void) { + IpcMessage Message; + u32 Index; + + for (Index = 0; Index < IPC_TEST_MESSAGES; Index++) { + if (IpcReceive(&TestPort, &Message) != IpcOk) { + ConsumerBad++; + break; + } + + if (Message.Length != 1 || Message.Data[0] != Index || + Message.Sender != ProducerId) { + ConsumerBad++; + } + } + + ConsumerDone = 1; +} + +/* Fills the port past its capacity, so the send path has to park as well. */ +static void ipc_producer_task(void) { + IpcMessage Message; + u32 Index; + + for (Index = 0; Index < IPC_TEST_MESSAGES; Index++) { + Message.Length = 1; + Message.Data[0] = Index; + + if (IpcSend(&TestPort, &Message) != IpcOk) { + ConsumerBad++; + break; + } + } + + ProducerDone = 1; +} + +/* + * Takes the mutex, bumps a counter, and checks that nobody slipped in while it + * was held. A shared counter that only ever grows by one is the evidence that + * the handoff did not let two tasks into the critical section at once. + */ +static void ipc_lock_task(void) { + u32 Seen; + + while (!LockStop) { + if (MutexLock(&TestMutex) != IpcOk) { + LockBad++; + break; + } + + Seen = LockRounds; + LockRounds = Seen + 1; + + if (LockRounds != Seen + 1) { + LockBad++; + } + + MutexUnlock(&TestMutex); + } +} + +/* + * Runs the ipc self tests once the scheduler is up, so the two sides of the + * rendezvous really do park and get woken. Prints a line per outcome, the same + * way the heap tests above report theirs. + */ +static void ipc_test_tasks(void) { + Task *Consumer; + Task *Producer; + u32 Waited; + IpcStatus Status; + + if (IpcPortCreate(&TestPort) != IpcOk) { + kprintf("ERROR: ipc port could not be created.\n"); + return; + } + + ConsumerDone = 0; + ProducerDone = 0; + ConsumerBad = 0; + + Consumer = SchedCreate("Consumer", (u32)ipc_consumer_task, + (u32)&ConsumerStack[TASK_STACK_SIZE]); + Producer = SchedCreate("Producer", (u32)ipc_producer_task, + (u32)&ProducerStack[TASK_STACK_SIZE]); + + if (Consumer == 0 || Producer == 0) { + kprintf("ERROR: ipc test tasks could not be created.\n"); + return; + } + + ProducerId = Producer->Id; + + /* Both tasks are created, so both sides have to finish on their own. */ + for (Waited = 0; Waited < IPC_TEST_TIMEOUT; Waited++) { + if (ConsumerDone && ProducerDone) { + break; + } + cpu_idle(); + } + + if (!ConsumerDone || !ProducerDone) { + kprintf("ERROR: ipc rendezvous did not finish, %u of %u messages.\n", + IpcPortQueued(&TestPort), IPC_TEST_MESSAGES); + return; + } + + if (ConsumerBad != 0) { + kprintf("ERROR: ipc rendezvous reported %u bad messages.\n", ConsumerBad); + return; + } + + kprintf("OK: %u messages passed through a blocking port.\n", IPC_TEST_MESSAGES); + + if (IpcPortQueued(&TestPort) != 0) { + kprintf("ERROR: port still holds %u messages after draining it.\n", + IpcPortQueued(&TestPort)); + } else { + kprintf("OK: port drained, no message lost or duplicated.\n"); + } + + /* A binary semaphore plus a mutex, driven from this task alone. */ + SemInit(&TestSem, 1, 1); + Status = SemWait(&TestSem); + + /* + * Runs from the idle task, which cannot sleep. So the last wait here has + * nowhere to park and has to say so instead of hanging, which is the same + * rule timer_sleep_ms follows. + */ + if (Status != IpcOk || SemTryWait(&TestSem) != IpcErrEmpty) { + kprintf("ERROR: binary semaphore did not hold at its limit.\n"); + } else if (SemPost(&TestSem) != IpcOk || SemPost(&TestSem) != IpcErrFull) { + kprintf("ERROR: binary semaphore did not stop at its limit.\n"); + } else if (SemWait(&TestSem) != IpcOk || SemWait(&TestSem) != IpcErrNoTask) { + kprintf("ERROR: binary semaphore did not hand its permit back.\n"); + } else { + kprintf("OK: binary semaphore counted, blocked, and refused to park the idle.\n"); + } + + MutexInit(&TestMutex); + + if (MutexLock(&TestMutex) != IpcOk || !MutexHeldByCaller(&TestMutex)) { + kprintf("ERROR: mutex could not be locked.\n"); + } else if (MutexTryLock(&TestMutex) != IpcErrBusy) { + kprintf("ERROR: mutex was free while this task held it.\n"); + } else if (MutexLock(&TestMutex) != IpcErrOwner) { + kprintf("ERROR: mutex allowed a recursive lock.\n"); + } else if (MutexUnlock(&TestMutex) != IpcOk || MutexHeldByCaller(&TestMutex)) { + kprintf("ERROR: mutex did not release.\n"); + } else if (MutexUnlock(&TestMutex) != IpcErrOwner) { + kprintf("ERROR: mutex let a task unlock one it did not hold.\n"); + } else { + kprintf("OK: mutex locked, refused a re-lock, and released.\n"); + } + + /* Two tasks fight over one mutex, so the handoff is exercised for real. */ + MutexInit(&TestMutex); + LockRounds = 0; + LockStop = 0; + LockBad = 0; + + { + Task *A = SchedCreate("LockA", (u32)ipc_lock_task, (u32)&LockAStack[TASK_STACK_SIZE]); + Task *B = SchedCreate("LockB", (u32)ipc_lock_task, (u32)&LockBStack[TASK_STACK_SIZE]); + + if (A == 0 || B == 0) { + kprintf("ERROR: ipc lock tasks could not be created.\n"); + return; + } + + for (Waited = 0; Waited < IPC_TEST_ROUNDS; Waited++) { + if (LockRounds >= IPC_TEST_ROUNDS) { + break; + } + cpu_idle(); + } + + LockStop = 1; + + /* Let both tasks notice the stop flag and let go of the mutex. */ + for (Waited = 0; Waited < IPC_TEST_TIMEOUT; Waited++) { + if (!TestMutex.Locked) { + break; + } + cpu_idle(); + } + } + + if (LockBad != 0) { + kprintf("ERROR: mutex let %u tasks into the critical section at once.\n", + LockBad); + } else if (LockRounds < IPC_TEST_ROUNDS) { + kprintf("ERROR: mutex contention stalled at %u of %u rounds.\n", LockRounds, + IPC_TEST_ROUNDS); + } else { + kprintf("OK: %u rounds of contention on one mutex, no overlap.\n", LockRounds); + } + + /* A released port cannot be claimed or destroyed twice. */ + if (IpcPortDestroy(&TestPort) != IpcOk || IpcPortDestroy(&TestPort) != IpcErrInvalid) { + kprintf("ERROR: port could not be destroyed cleanly.\n"); + } else if (IpcPortCreate(&TestPort) != IpcOk || IpcPortCreate(&TestPort) != IpcErrInvalid) { + kprintf("ERROR: port was claimed twice.\n"); + } else { + kprintf("OK: port rejected a second create and a second destroy.\n"); + IpcPortDestroy(&TestPort); + } +} + void kernel_main(struct boot_info *boot) { console_clear(); kprintf("NoviumOS\n\n"); @@ -121,6 +363,14 @@ void kernel_main(struct boot_info *boot) { SchedInit(); kprintf("OK: Scheduling Init Succesfull\n\n"); + /* + * Before the shell and worker start, so this output stays on one run of + * lines instead of landing in the middle of the shell's prompt. + */ + ipc_test_tasks(); + + kprintf("\n"); + SchedCreate("Worker", (u32)worker_task, (u32)&WorkerStack[TASK_STACK_SIZE]); SchedCreate("Shell", (u32)shell_task, (u32)&ShellStack[TASK_STACK_SIZE]); diff --git a/ipc/ipc.c b/ipc/ipc.c new file mode 100644 index 0000000..7b62cc8 --- /dev/null +++ b/ipc/ipc.c @@ -0,0 +1,141 @@ +#include "ipc.h" +#include + +u32 IpcSelfId(void) { + Task *Self = SchedCurrent(); + + if (Self == 0) { + return 0; + } + + return Self->Id; +} + +/* + * Parks the caller on Wait. Needs interrupts off for the whole queue + * bookkeeping, so the helpers below are the only thing allowed to touch a + * queue while it is asleep, and every waker goes through IpcWakeOne. + */ +static bool IpcWaitAdd(IpcWait *Wait, u32 Id) { + u32 Index; + + if (Wait->Count >= IpcMaxWaiters) { + return false; + } + + for (Index = 0; Index < Wait->Count; Index++) { + if (Wait->Ids[Index] == Id) { + return true; /* already parked, a second entry would only confuse a wake */ + } + } + + Wait->Ids[Wait->Count] = Id; + Wait->Count++; + return true; +} + +static void IpcWaitRemove(IpcWait *Wait, u32 Id) { + u32 Index; + + for (Index = 0; Index < Wait->Count; Index++) { + if (Wait->Ids[Index] == Id) { + /* Not worth keeping order for, and the queue is short. */ + Wait->Ids[Index] = Wait->Ids[Wait->Count - 1]; + Wait->Count--; + return; + } + } +} + +/* + * True when Id still names a task that is alive and can be woken. A killed + * task has its id cleared and a recycled descriptor gets a fresh id, so a + * single lookup is enough to rule out both. + */ +static bool IpcWaitIsLive(u32 Id) { + return Id != 0 && SchedFind(Id) != 0; +} + +u32 IpcWaitTakeLive(IpcWait *Wait) { + u32 Index; + + for (Index = 0; Index < Wait->Count; Index++) { + u32 Id = Wait->Ids[Index]; + + IpcWaitRemove(Wait, Id); + + if (Id != 0 && IpcWaitIsLive(Id)) { + return Id; + } + } + + return 0; +} + +IpcStatus IpcPark(IpcWait *Wait, IpcReadyFn Ready, void *arg) { + u32 Self = IpcSelfId(); + + if (Self == 0) { + /* The idle task cannot sleep, so it must not pretend it did. */ + return IpcErrNoTask; + } + + for (;;) { + bool was_enabled = cpu_irqs_enabled(); + + cpu_disable_irqs(); + + if (Ready(arg)) { + if (was_enabled) { + cpu_enable_irqs(); + } + return IpcOk; + } + + if (!IpcWaitAdd(Wait, Self)) { + if (was_enabled) { + cpu_enable_irqs(); + } + return IpcErrFull; + } + + /* + * The lock stays held from here into SchedBlock on purpose. Being on + * the queue and being blocked have to happen as one step: if the lock + * were dropped in between, a waker could take the entry, find the task + * still runnable, and its SchedWakeTask would do nothing, leaving the + * task to park afterwards with nobody left to wake it. + * + * Nothing else can run while the lock is held, since a task only + * changes at a SchedYield and those are reached from the interrupt + * epilogue. So the entry is on the queue before we are marked blocked, + * and Ready() was checked after the last thing that could set it. + * + * SchedBlock leaves the lock held for its caller: it restores the task + * eflags on the way back, which are clear here. + */ + SchedBlock(); + + /* Woken, or the scheduler found nothing to run and undid the block. */ + cpu_disable_irqs(); + IpcWaitRemove(Wait, Self); + if (was_enabled) { + cpu_enable_irqs(); + } + } +} + +void IpcWakeOne(IpcWait *Wait) { + u32 Id; + bool was_enabled = cpu_irqs_enabled(); + + cpu_disable_irqs(); + Id = IpcWaitTakeLive(Wait); + if (was_enabled) { + cpu_enable_irqs(); + } + + if (Id != 0) { + SchedWakeTask(Id); + } +} \ No newline at end of file diff --git a/ipc/ipc.h b/ipc/ipc.h index 6bce274..2bc2d5a 100644 --- a/ipc/ipc.h +++ b/ipc/ipc.h @@ -1 +1,80 @@ -/* TODO: System stub - implement when ready. */ +#ifndef NOVIUM_IPC_H +#define NOVIUM_IPC_H + +#include +#include + +/* + * Inter-process communication between kernel tasks. + * + * Every object here is a fixed size value that lives in kernel memory, so a + * task never has to allocate to talk to another one and there is nothing to + * leak when a task dies. Ports, semaphores and mutexes are created by the + * caller and may sit in the .bss, in a task's static area, or in a struct on + * its own stack. + */ + +/* + * A wait queue never holds more than this, so a full queue is an error the + * caller can see, instead of a task that blocks with nowhere to be woken. + */ +#define IpcMaxWaiters 16 + +typedef enum { + IpcOk = 0, + IpcErrInvalid, /* null or out of range argument */ + IpcErrFull, /* a bounded queue or counter is already at its limit */ + IpcErrEmpty, /* nothing to take */ + IpcErrBusy, /* a non blocking call could not proceed right now */ + IpcErrOwner, /* unlock from a task that does not hold the mutex */ + IpcErrNoTask /* the caller cannot sleep and the call would have to */ +} IpcStatus; + +/* Id of the calling task, 0 for the idle task and for no task at all. The idle + * task cannot sleep, so a call that would block must refuse to use it. */ +u32 IpcSelfId(void); + +/* + * Wait queue, shared by ports, semaphores and mutexes. Entries are task ids + * rather than Task pointers, because a Task descriptor is recycled once its + * task dies while an id is not, so a stale entry can be spotted instead of + * waking whichever task owns that slot now. + */ +typedef struct IpcWait { + u32 Ids[IpcMaxWaiters]; + u32 Count; +} IpcWait; + +/* Reports whether a parked task may stop waiting. Called with interrupts off, + * so it may only read the object it is handed. */ +typedef bool (*IpcReadyFn)(void *arg); + +/* + * Parks the calling task on Wait until Ready() turns true. + * + * The task registers itself while interrupts are off and only then sleeps, and + * Ready() is re-checked under that same lock, so a waker that runs in between + * either finds the task parked or is seen by the re-check afterwards. Waiting + * is never lost and never left behind. + * + * Returns IpcErrNoTask when the caller is not a task that can sleep, and + * IpcErrFull when the wait queue has no room for another entry. + */ +IpcStatus IpcPark(IpcWait *Wait, IpcReadyFn Ready, void *arg); + +/* + * Hands the object to the first waiter that is still alive, waking it if there + * is one. Woken tasks are picked in no particular order. Safe to call with + * interrupts on or off. + */ +void IpcWakeOne(IpcWait *Wait); + +/* + * Removes the first entry that still names a live task and returns its id, or + * 0 when the queue holds nobody. Entries left by a killed task are dropped + * along the way. For a mutex unlock, which needs the id before it can hand the + * lock over. Interrupts must be off. + */ +u32 IpcWaitTakeLive(IpcWait *Wait); + +#endif diff --git a/ipc/message.c b/ipc/message.c index 6bce274..7bac44e 100644 --- a/ipc/message.c +++ b/ipc/message.c @@ -1 +1,223 @@ -/* TODO: System stub - implement when ready. */ +#include "message.h" +#include + +#define PORT_MAGIC 0x504F5254u /* ASCII "PORT", tells a real port from garbage */ + +/* Drops whatever is in the slots and wait queues, keeping the port claimed. */ +static void PortReset(IpcPort *Port) { + u32 Index; + + Port->Head = 0; + Port->Count = 0; + + for (Index = 0; Index < IpcPortCapacity; Index++) { + Port->Slots[Index].Length = 0; + Port->Slots[Index].Sender = 0; + } + + Port->Senders.Count = 0; + Port->Receivers.Count = 0; +} + +bool IpcPortIsCreated(const IpcPort *Port) { + return Port != 0 && Port->Magic == PORT_MAGIC; +} + +IpcStatus IpcPortCreate(IpcPort *Port) { + bool was_enabled; + + if (Port == 0) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (Port->Magic == PORT_MAGIC) { + /* Already in use. Wiping it here would empty a queue under a reader. */ + if (was_enabled) { + cpu_enable_irqs(); + } + return IpcErrInvalid; + } + + Port->Magic = PORT_MAGIC; + PortReset(Port); + + if (was_enabled) { + cpu_enable_irqs(); + } + + return IpcOk; +} + +IpcStatus IpcPortDestroy(IpcPort *Port) { + bool was_enabled; + IpcStatus Status = IpcOk; + + if (!IpcPortIsCreated(Port)) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (Port->Senders.Count != 0 || Port->Receivers.Count != 0) { + /* + * Parked tasks would never learn the port went away, so a busy port + * stays claimed and the caller has to drain it first. + */ + Status = IpcErrBusy; + } else { + PortReset(Port); + Port->Magic = 0; + } + + if (was_enabled) { + cpu_enable_irqs(); + } + + return Status; +} + +u32 IpcPortQueued(const IpcPort *Port) { + if (!IpcPortIsCreated(Port)) { + return 0; + } + + return Port->Count; +} + +/* True while the port has room, which is what a blocked sender is waiting for. */ +static bool PortHasRoom(void *arg) { + IpcPort *Port = (IpcPort *)arg; + + return Port->Count < IpcPortCapacity; +} + +/* True while the port holds something, which is what a receiver waits for. */ +static bool PortHasMessage(void *arg) { + IpcPort *Port = (IpcPort *)arg; + + return Port->Count > 0; +} + +/* Copies the message into the next free slot. Caller holds the lock. */ +static void PortPush(IpcPort *Port, const IpcMessage *Message) { + u32 Tail = (Port->Head + Port->Count) % IpcPortCapacity; + + Port->Slots[Tail] = *Message; + Port->Slots[Tail].Sender = IpcSelfId(); + Port->Count++; +} + +/* Copies the oldest message out. Caller holds the lock. */ +static void PortPop(IpcPort *Port, IpcMessage *out) { + *out = Port->Slots[Port->Head]; + Port->Head = (Port->Head + 1) % IpcPortCapacity; + Port->Count--; +} + +IpcStatus IpcTrySend(IpcPort *Port, const IpcMessage *Message) { + bool was_enabled; + IpcStatus Status = IpcOk; + + if (!IpcPortIsCreated(Port) || Message == 0) { + return IpcErrInvalid; + } + + if (Message->Length > IpcMessageWords) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (Port->Count == IpcPortCapacity) { + Status = IpcErrFull; + } else { + PortPush(Port, Message); + } + + if (was_enabled) { + cpu_enable_irqs(); + } + + if (Status == IpcOk) { + IpcWakeOne(&Port->Receivers); + } + + return Status; +} + +IpcStatus IpcSend(IpcPort *Port, const IpcMessage *Message) { + if (!IpcPortIsCreated(Port) || Message == 0) { + return IpcErrInvalid; + } + + if (Message->Length > IpcMessageWords) { + return IpcErrInvalid; + } + + for (;;) { + IpcStatus Status = IpcTrySend(Port, Message); + + if (Status != IpcErrFull) { + return Status; + } + + /* Full. Wait for a receiver to make room, then try again. */ + Status = IpcPark(&Port->Senders, PortHasRoom, Port); + if (Status != IpcOk) { + return Status; + } + } +} + +IpcStatus IpcTryReceive(IpcPort *Port, IpcMessage *out) { + bool was_enabled; + IpcStatus Status = IpcOk; + + if (!IpcPortIsCreated(Port) || out == 0) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (Port->Count == 0) { + Status = IpcErrEmpty; + } else { + PortPop(Port, out); + } + + if (was_enabled) { + cpu_enable_irqs(); + } + + if (Status == IpcOk) { + IpcWakeOne(&Port->Senders); + } + + return Status; +} + +IpcStatus IpcReceive(IpcPort *Port, IpcMessage *out) { + if (!IpcPortIsCreated(Port) || out == 0) { + return IpcErrInvalid; + } + + for (;;) { + IpcStatus Status = IpcTryReceive(Port, out); + + if (Status != IpcErrEmpty) { + return Status; + } + + /* Empty. Wait for a sender, then try again. */ + Status = IpcPark(&Port->Receivers, PortHasMessage, Port); + if (Status != IpcOk) { + return Status; + } + } +} diff --git a/ipc/message.h b/ipc/message.h index 6bce274..51f5cbc 100644 --- a/ipc/message.h +++ b/ipc/message.h @@ -1 +1,70 @@ -/* TODO: System stub - implement when ready. */ +#ifndef NOVIUM_IPC_MESSAGE_H +#define NOVIUM_IPC_MESSAGE_H + +#include "ipc.h" + +/* + * Message passing over ports. + * + * A message is a fixed size value: a length and IpcMessageWords of payload. It + * is copied by value rather than referenced, so the sender's buffer is free + * again as soon as IpcSend returns and neither side has to keep the other's + * memory alive. + */ + +#define IpcMessageWords 8 + +typedef struct IpcMessage { + u32 Length; /* words of Data in use, at most IpcMessageWords */ + u32 Sender; /* task id of the sender, filled in by IpcSend */ + u32 Data[IpcMessageWords]; +} IpcMessage; + +/* How many messages a port holds before senders have to wait for a receiver. */ +#define IpcPortCapacity 16 + +/* + * A port is a bounded queue of messages in arrival order. What it adds over a + * plain queue is the other half: a sender that finds the port full and a + * receiver that finds it empty both park until the other side moves. Port + * fields are private, build one with IpcPortCreate and do not touch it after. + */ +typedef struct IpcPort { + u32 Magic; + u32 Head; /* slot the next receive takes from */ + u32 Count; /* messages queued right now */ + IpcMessage Slots[IpcPortCapacity]; + IpcWait Senders; + IpcWait Receivers; +} IpcPort; + +/* + * Claims a port the caller owns, in its own memory, and empties it. The port + * must not be in use already, which reports IpcErrInvalid rather than wiping + * a queue another task is reading from. + */ +IpcStatus IpcPortCreate(IpcPort *Port); + +/* Empties the port and releases it. Fails while tasks are parked on it, since + * dropping them would strand them. */ +IpcStatus IpcPortDestroy(IpcPort *Port); + +/* Queues a message, waiting for room while the port is full. */ +IpcStatus IpcSend(IpcPort *Port, const IpcMessage *Message); + +/* Queues a message only when there is room. Never waits. */ +IpcStatus IpcTrySend(IpcPort *Port, const IpcMessage *Message); + +/* Takes the oldest queued message, waiting while the port is empty. */ +IpcStatus IpcReceive(IpcPort *Port, IpcMessage *out); + +/* Takes the oldest message only when one is queued. Never waits. */ +IpcStatus IpcTryReceive(IpcPort *Port, IpcMessage *out); + +/* Messages waiting in the port. */ +u32 IpcPortQueued(const IpcPort *Port); + +/* True once IpcPortCreate has claimed the port. */ +bool IpcPortIsCreated(const IpcPort *Port); + +#endif diff --git a/ipc/sync.c b/ipc/sync.c index 6bce274..0ced247 100644 --- a/ipc/sync.c +++ b/ipc/sync.c @@ -1 +1,246 @@ -/* TODO: System stub - implement when ready. */ +#include "sync.h" +#include + +/* + * Semaphore. + * + * A Limit of 0 is an unbounded counter. Setting one turns it into a binary + * semaphore, which is what the kernel's own locks will mostly want. + */ +void SemInit(IpcSemaphore *Sem, u32 Initial, u32 Limit) { + if (Sem == 0) { + return; + } + + if (Limit != 0 && Initial > Limit) { + /* Start at the ceiling rather than above it, so Count <= Limit holds. */ + Initial = Limit; + } + + Sem->Count = Initial; + Sem->Limit = Limit; + Sem->Wait.Count = 0; +} + +/* True while a permit is free, which is what a parked sender is waiting for. */ +static bool SemHasPermit(void *arg) { + IpcSemaphore *Sem = (IpcSemaphore *)arg; + + return Sem->Count > 0; +} + +IpcStatus SemTryWait(IpcSemaphore *Sem) { + bool was_enabled; + IpcStatus Status = IpcOk; + + if (Sem == 0) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (Sem->Count == 0) { + Status = IpcErrEmpty; + } else { + Sem->Count--; + } + + if (was_enabled) { + cpu_enable_irqs(); + } + + return Status; +} + +IpcStatus SemWait(IpcSemaphore *Sem) { + if (Sem == 0) { + return IpcErrInvalid; + } + + for (;;) { + IpcStatus Status = SemTryWait(Sem); + + if (Status != IpcErrEmpty) { + return Status; + } + + /* No permit. Wait for a post, then try again. */ + Status = IpcPark(&Sem->Wait, SemHasPermit, Sem); + if (Status != IpcOk) { + return Status; + } + } +} + +IpcStatus SemPost(IpcSemaphore *Sem) { + bool was_enabled; + IpcStatus Status = IpcOk; + + if (Sem == 0) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (Sem->Limit != 0 && Sem->Count >= Sem->Limit) { + /* At the ceiling. Posting here would let the count run away. */ + Status = IpcErrFull; + } else { + Sem->Count++; + } + + if (was_enabled) { + cpu_enable_irqs(); + } + + if (Status == IpcOk) { + IpcWakeOne(&Sem->Wait); + } + + return Status; +} + +u32 SemCount(const IpcSemaphore *Sem) { + if (Sem == 0) { + return 0; + } + + return Sem->Count; +} + +/* + * Mutex. + * + * Not recursive. Locking a mutex the caller already holds reports IpcErrOwner + * rather than parking on a lock nobody else is able to open, which would hang + * the task until the scheduler undid the block. + */ +void MutexInit(IpcMutex *Mutex) { + if (Mutex == 0) { + return; + } + + Mutex->Locked = false; + Mutex->Owner = 0; + Mutex->Wait.Count = 0; +} + +bool MutexHeldByCaller(const IpcMutex *Mutex) { + if (Mutex == 0 || !Mutex->Locked) { + return false; + } + + return Mutex->Owner == IpcSelfId(); +} + +/* True while the mutex is free, which is what a parked locker waits for. */ +static bool MutexIsFree(void *arg) { + IpcMutex *Mutex = (IpcMutex *)arg; + + return !Mutex->Locked; +} + +IpcStatus MutexTryLock(IpcMutex *Mutex) { + bool was_enabled; + IpcStatus Status = IpcOk; + + if (Mutex == 0) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (Mutex->Locked) { + Status = IpcErrBusy; + } else { + Mutex->Locked = true; + Mutex->Owner = IpcSelfId(); + } + + if (was_enabled) { + cpu_enable_irqs(); + } + + return Status; +} + +IpcStatus MutexLock(IpcMutex *Mutex) { + if (Mutex == 0) { + return IpcErrInvalid; + } + + if (MutexHeldByCaller(Mutex)) { + return IpcErrOwner; + } + + for (;;) { + IpcStatus Status = MutexTryLock(Mutex); + + if (Status == IpcErrBusy && MutexHeldByCaller(Mutex)) { + /* + * An unlock passed the lock straight to us while we were parked, + * so the mutex is ours even though it still reads as taken. + */ + return IpcOk; + } + + if (Status != IpcErrBusy) { + return Status; + } + + /* Held. Wait for the owner to release it, then try again. */ + Status = IpcPark(&Mutex->Wait, MutexIsFree, Mutex); + if (Status != IpcOk) { + return Status; + } + } +} + +IpcStatus MutexUnlock(IpcMutex *Mutex) { + bool was_enabled; + u32 Next = 0; + IpcStatus Status = IpcOk; + + if (Mutex == 0) { + return IpcErrInvalid; + } + + was_enabled = cpu_irqs_enabled(); + cpu_disable_irqs(); + + if (!Mutex->Locked || Mutex->Owner != IpcSelfId()) { + /* + * Only the holder may open the lock. Allowing anyone through would let a + * task release a mutex it never took and hand it to a waiter at the + * same time. + */ + Status = IpcErrOwner; + } else { + /* + * Take a waiter now and pass the lock straight to it, rather than + * clearing the owner and letting a task that is merely ready take it + * first. That keeps the wakeup and the lock in step. + */ + Next = IpcWaitTakeLive(&Mutex->Wait); + + if (Next != 0) { + Mutex->Owner = Next; + } else { + Mutex->Locked = false; + Mutex->Owner = 0; + } + } + + if (was_enabled) { + cpu_enable_irqs(); + } + + if (Status == IpcOk && Next != 0) { + SchedWakeTask(Next); + } + + return Status; +} diff --git a/ipc/sync.h b/ipc/sync.h index 6bce274..bc662e7 100644 --- a/ipc/sync.h +++ b/ipc/sync.h @@ -1 +1,65 @@ -/* TODO: System stub - implement when ready. */ +#ifndef NOVIUM_IPC_SYNC_H +#define NOVIUM_IPC_SYNC_H + +#include "ipc.h" + +/* + * Counting semaphores and mutexes. Both park a task on a shared wait queue and + * hand the resource straight to the next waiter, so a wakeup cannot be taken + * by a task that is not the one the queue picked. + */ + +typedef struct IpcSemaphore { + u32 Count; /* permits currently available */ + u32 Limit; /* ceiling for Count, 0 means there is no ceiling */ + IpcWait Wait; +} IpcSemaphore; + +typedef struct IpcMutex { + /* The owner alone cannot say whether the mutex is held, because the idle + * task is id 0 too, so held-ness is tracked apart from who holds it. */ + bool Locked; + u32 Owner; /* task id of the holder, meaningless while Locked is false */ + IpcWait Wait; +} IpcMutex; + +/* + * Semaphore. + * + * A Limit of 0 is an unbounded counter, and a Limit of 1 turns it into a binary + * semaphore, which is what most of the kernel's own locks will want. + */ +void SemInit(IpcSemaphore *Sem, u32 Initial, u32 Limit); + +/* Takes a permit, blocking while none is available. */ +IpcStatus SemWait(IpcSemaphore *Sem); + +/* Takes a permit only when one is free. Never blocks. */ +IpcStatus SemTryWait(IpcSemaphore *Sem); + +/* Returns a permit and wakes one waiter. Fails when the ceiling is reached. */ +IpcStatus SemPost(IpcSemaphore *Sem); + +u32 SemCount(const IpcSemaphore *Sem); + +/* + * Mutex. + * + * Not recursive: locking a mutex the caller already holds reports IpcErrOwner + * instead of parking on a lock nobody else is able to open. + */ +void MutexInit(IpcMutex *Mutex); + +/* Takes the mutex, blocking while another task holds it. */ +IpcStatus MutexLock(IpcMutex *Mutex); + +/* Takes the mutex only when it is free. Never blocks. */ +IpcStatus MutexTryLock(IpcMutex *Mutex); + +/* Releases the mutex and hands it to the first waiter. */ +IpcStatus MutexUnlock(IpcMutex *Mutex); + +/* True when the calling task is the one holding the mutex. */ +bool MutexHeldByCaller(const IpcMutex *Mutex); + +#endif