Circular buffers were shared between user space and kernel. (Allocated by user space, mapped into kernel with get_user_pages.)
User space threads just added messages to their circular buffer by copying in data, hitting a barrier and updating a head pointer.
Like io_uring, I avoided too many system calls by having a kernel thread poll for changes. User space would only make a system call when the ring would near a "high water mark", like 85% full or whatever.
The circular buffers could outlive the process which created them and attached them. A process could crash and go away; the kernel thread would still process the last messages written right up to the crash.
Not everyone around was keen on the idea. What do you mean just stick it into a buffer and the kernel will pick it up. Polling? Bah.