12 November 2025
In this article I would like to present to you MBus - a kernel-space messaging IPC mechanism for
the MOP2 operating system.
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.
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.
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);
The usage of MBus can be found for eg. inside of TB - the MOP2’s shell:
if (CONFIG.mode == MODE_INTERACTIVE) {
ipc_mbusattch("ps2kb");
do_mode_interactive();
// ...
// ...
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…).
// ...
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 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:
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?).