MBus messaging IPC
This commit is contained in:
153
content/blog/MOP2/mbus-messaging-ipc.adoc
Normal file
153
content/blog/MOP2/mbus-messaging-ipc.adoc
Normal file
@ -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?).
|
||||
Reference in New Issue
Block a user