I did this years before io_uring, circa 2006. Working on a Linux-based networking startup called Zeugma Systems, I implemented a kernel-based logging system for a multi-process, multi-node distributed application. I started on Linux 2.6.14. When that startup folded, I think we were on 2.6.27.
The logging system was implemented in a module which was inserted into the kernel and then used the calling thread to run a service. The module provided a device and some ioctls. Processes used the ioctls to attach circular buffers to the device, which was mapped into kernel space using get_user_pages.
Processes would just place messages into their circular buffers and update an index variable in the buffer header. The kernel would automatically pick up the message, without any system call. There was a wakeup ioctl to poke the kernel thread, which was used upon hitting a high water mark (buffer getting near full). This is the basic intuition behind io_uring.
The kernel thread collected messages from multiple buffers and sent them into several destination (files and sockets).
I do not have most of this code, but some of it survived, including a kernel mutex and condition variable library featuring a function that lets you give up a mutex to wait on a condition variable, while also polling any mixture of kernel file and socket handles, with a timeout. This function at the core of the kernel thread's loop.
The nice thing was that when processes crashed, their buffer would not go away immediately. Of course their own address space would be gone, but the kernel's mapping of the shared buffer mapping would gracefully persist, until the thread emptied the buffer; everything put into the buffer before the crash was safe. Empty buffers belonging to processes that had died would then be cleaned away.
I had a utility program that would list the buffers and the PIDs of their processes, and provide stats, like outstanding bytes, and is that process still alive.
(The one inefficiency in logging was that log messages need time stamps. Depending on where you get a time stamp from, that requires a trip to the kernel. I can't remember what I did about that.)
A bit of a difficulty in the whole approach is that I wasn't getting a linear mapping of the user pages in the kernel. So I wrote the grotty C code (kernel side) to pull the messages correctly from a scrambled buffer whose pages are out of order, without making an extra copy.
I find myself wondering what a truly high performance WebSocket server would look like, and if it would require loading a custom kernel module. Consider the worst-case, 1-msg-in N-msg-out for N "connections" (aka fan-out ratio). My understanding is that at the physical level subnets time slice a shared, serialized medium. The units there are a network frame (either wifi or ethernet). These frames are organized into IP and then TCP and finally give your process a "connection" from which data comes and into which data goes. WebSockets, to me, simply make the TCP socket abstraction accessible to browsers, with some extra setup cost but no runtime cost.
To be honest, ordinary "naive" programming methods are good enough to run a sizable single node WebSocket server. I'd be curious how much performance you can get out of a single server or, perhaps more broadly useful, a single core in a single Linux VPS, using different languages and relatively esoteric techniques like this.
3D APIs used the same idea for a long time, probably going all the way back to the mid-90's with D3D2 'execute buffers' (GL display lists are even older but just similar, they're expected to be recorded once and executed many times instead of being rebuilt each frame).
And io_uring itself was more directly inspired by NVMe and RDMA, which of course work with these same queues as GFX cards. The original io_uring patch compares itself to SPDK, whose premise is "what if we expose an abstraction for a hardware queue per thread to an application " - basically the same programming model as io_uring. And SPDK was just taking techniques from networking (DPDK) and applying them to storage.
I must have had barriers in there. I knew what they are. I had worked on glibc threads some five years before that (under the maintenance of Ulrich Drepper at the time), using lock-free algorithms, where we had operation like "compare and swap with acquire semantics" (meaning doing the right kind of barrier for acquiring a mutex).
The hardware this was running on supported cache-coherent NUMA, but we didn't use it in that mode; the blades that had multiple nodes ran multiple Linux instances that didn't share memory. Reordering effects at the hardware level weren't observed in the separate mode; if one core wrote to a location A and then B another core would not see the B update before A. Under ccNUMA, had we used it, I believe that would have been an issue.
Compiler reordering is always a threat, so you need at least that __volatile__ __asm__(": : memory"). In the user space infrastructure of that project, I made a library of atomic operations; there must have been barrier macros in there; I can't imagine providing atomic primitives without barriers.
The logging system was implemented in a module which was inserted into the kernel and then used the calling thread to run a service. The module provided a device and some ioctls. Processes used the ioctls to attach circular buffers to the device, which was mapped into kernel space using get_user_pages.
Processes would just place messages into their circular buffers and update an index variable in the buffer header. The kernel would automatically pick up the message, without any system call. There was a wakeup ioctl to poke the kernel thread, which was used upon hitting a high water mark (buffer getting near full). This is the basic intuition behind io_uring.
The kernel thread collected messages from multiple buffers and sent them into several destination (files and sockets).
I do not have most of this code, but some of it survived, including a kernel mutex and condition variable library featuring a function that lets you give up a mutex to wait on a condition variable, while also polling any mixture of kernel file and socket handles, with a timeout. This function at the core of the kernel thread's loop.
The nice thing was that when processes crashed, their buffer would not go away immediately. Of course their own address space would be gone, but the kernel's mapping of the shared buffer mapping would gracefully persist, until the thread emptied the buffer; everything put into the buffer before the crash was safe. Empty buffers belonging to processes that had died would then be cleaned away.
I had a utility program that would list the buffers and the PIDs of their processes, and provide stats, like outstanding bytes, and is that process still alive.
(The one inefficiency in logging was that log messages need time stamps. Depending on where you get a time stamp from, that requires a trip to the kernel. I can't remember what I did about that.)
A bit of a difficulty in the whole approach is that I wasn't getting a linear mapping of the user pages in the kernel. So I wrote the grotty C code (kernel side) to pull the messages correctly from a scrambled buffer whose pages are out of order, without making an extra copy.