diff --git a/assets/img/mbus-diagram1.png b/assets/img/mbus-diagram1.png new file mode 100644 index 0000000..aa9f47e Binary files /dev/null and b/assets/img/mbus-diagram1.png differ diff --git a/content/blog/MOP2/mbus-messaging-ipc.adoc b/content/blog/MOP2/mbus-messaging-ipc.adoc new file mode 100644 index 0000000..dace531 --- /dev/null +++ b/content/blog/MOP2/mbus-messaging-ipc.adoc @@ -0,0 +1,153 @@ += MBus - in-kernel messaging system +Kamil Kowalczyk +2025-11-12 +:jbake-type: post +:jbake-tags: MOP2 osdev +:jbake-status: published + +In this article I would like to present to you `MBus` - a kernel-space messaging IPC mechanism for +the MOP2 operating system. + +== One-to-many messaging + +MBus is a one-to-many messaging system. This means that there's one sender/publisher and many +readers/subscribers. Think of a YouTube channel - a person posts a video and their subscribers get +a push notification that there's content to consume. + +image::/img/mbus-diagram1.png["One-to-many messaging diagram"] + +== User-space API + +This is the user-space API for MBus. The application can create a message bus for `objmax` +messages of `objsize`. Message buses are indentified via a global string ID, for eg. the PS/2 +keyboard driver uses ID "ps2kb". + +`ipc_mbusattch` and `ipc_mbusdttch` are used for attaching/detattching a consumer to/from the +message bus. + +[source,c] +---- +int32_t ipc_mbusmake(char *name, size_t objsize, size_t objmax); +int32_t ipc_mbusdelete(char *name); +int32_t ipc_mbuspublish(char *name, const uint8_t *const buffer); +int32_t ipc_mbusconsume(char *name, uint8_t *const buffer); +int32_t ipc_mbusattch(char *name); +int32_t ipc_mbusdttch(char *name); +---- + +=== Usage + +The usage of MBus can be found for eg. inside of TB - the MOP2's shell: + +.Initalizing interactive shell mode +[source,c] +---- + if (CONFIG.mode == MODE_INTERACTIVE) { + ipc_mbusattch("ps2kb"); + do_mode_interactive(); + // ... +---- + +.Reading key presses +[source,c] +---- + // ... + int32_t read = ipc_mbusconsume("ps2kb", &b); + if (read > 0) { + switch (b) { + case C('C'): + case 0xE9: + uprintf("\n"); + goto begin; + break; + case C('L'): + uprintf(ANSIQ_CUR_SET(0, 0)); + uprintf(ANSIQ_SCR_CLR_ALL); + goto begin; + break; + } + // ... +---- + +Previously reading the keyboard was done in a quite ugly manner via specialized functions of the +`ps2kbdev` device object (`DEV_PS2KBDEV_ATTCHCONS` and `DEV_PS2KBDEV_READCH`). It was a one big +hack, but the MBus API has turned out quite nicely ;). + +With the new MBus API, the PS/2 keyboard driver becomes way cleaner than before (you can dig +through the commit history...). + +.Kernel-side code for the PS/2 keyboard driver +[source,c] +---- +// ... +IpcMBus *PS2KB_MBUS; + +void ps2kbdev_intr(void) { + int32_t c = ps2kb_intr(); + if (c >= 0) { + uint8_t b = c; + ipc_mbuspublish("ps2kb", &b); + } +} + +void ps2kbdev_init(void) { + intr_attchhandler(&ps2kbdev_intr, INTR_IRQBASE+1); + + Dev *ps2kbdev; + HSHTB_ALLOC(DEVTABLE.devs, ident, "ps2kbdev", ps2kbdev); + spinlock_init(&ps2kbdev->spinlock); + PS2KB_MBUS = ipc_mbusmake("ps2kb", 1, 0x100); +} +---- + +The messaging logic is ~20 lines of code now. + +== The tricky part + +The trickiest part to figure out while implementing MBus was to implement +cleaning up dangling/dead consumers. In the current model, a message bus +doesn't really know if a consumer has died without explicitly detattching +itself from the bus. This is solved by going through each message bus and +it's corresponding consumers and deleting the ones that aren't in the list +of currently running processes. This operation is ran every cycle of the +scheduler - you could say it's a form of garbage collection. All of this +is implemented inside `ipc_mbustick`: + +[source,c] +---- +void ipc_mbustick(void) { + spinlock_acquire(&IPC_MBUSES.spinlock); + // Go through all message buses + for (size_t i = 0; i < LEN(IPC_MBUSES.mbuses); i++) { + IpcMBus *mbus = &IPC_MBUSES.mbuses[i]; + // Skip unused slots + if (mbus->_hshtbstate != HSHTB_TAKEN) { + continue; + } + + IpcMBusCons *cons, *constmp; + spinlock_acquire(&mbus->spinlock); + // Go through every consumer of this message bus + LL_FOREACH_SAFE(mbus->consumers, cons, constmp) { + spinlock_acquire(&PROCS.spinlock); + Proc *proc = NULL; + LL_FINDPROP(PROCS.procs, proc, pid, cons->pid); + spinlock_release(&PROCS.spinlock); + + // If not on the list of processes, purge! + if (proc == NULL) { + LL_REMOVE(mbus->consumers, cons); + dlfree(cons->rbuf.buffer); + dlfree(cons); + } + } + spinlock_release(&mbus->spinlock); + } + spinlock_release(&IPC_MBUSES.spinlock); +} +---- + +As you can see it's a quite heavy operation and thus not ideal - but still +way ahead of what we had before. I guess the next step would be to figure out +a way to optimize this further, although the system doesn't seem to take a +noticable hit in performance (maybe do some benchmarks in the future?).