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