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.

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.

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.

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
  if (CONFIG.mode == MODE_INTERACTIVE) {
    ipc_mbusattch("ps2kb");
    do_mode_interactive();
    // ...
Reading key presses
  // ...
      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
// ...
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:

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?).