Linux userspace block devices
Userspace block devices, with Go and Zig libraries
ublk serves Linux block devices from a userspace process through io_uring. This site explains the kernel protocol and documents go-ublk, a pure-Go library, and lib-ublk, a Zig core with a C ABI and language bindings. The benchmarks report measured RAM workloads with their evidence. lib-ublk’s source release is pending.
Learn how ublk works Get started with go-ublk Explore lib-ublk Read the benchmarks
The ublk guide
The kernel protocol, from control commands to request delivery, copy modes, recovery, and feature availability. For server authors working in any language.
go-ublk
A pure-Go engine with a Backend or raw request Handler interface. No cgo or liburing. The library handles io_uring, feature negotiation, and the device lifecycle.
lib-ublk
A Zig core with a C ABI and caller-owned step/poll queues. Bindings for C, C++, Rust, Go, Python, Zig, and lang. Source release pending.
Comparative benchmarks
An overhead ladder from the kernel floor through userspace null and RAM targets. Completion latency, throughput, and CPU cost, with evidence and qualification limits.
How it fits together#
An application reads and writes /dev/ublkbN like any disk. The kernel’s ublk_drv turns each block request into a descriptor and completes an io_uring command that your server left waiting. The server handles the request against its backend, then commits the result and re-arms the slot. The architecture chapter walks a request through the path. The UAPI reference lists the kernel commands and features.
Quick start with go-ublk#
A complete block device that discards writes and reads zeros. Implement five methods and hand the backend to CreateAndServe:
package main
import (
"context"
"os/signal"
"syscall"
"github.com/ehrlich-b/go-ublk"
)
type null struct{ size int64 }
func (n null) ReadAt(p []byte, off int64) (int, error) { clear(p); return len(p), nil }
func (n null) WriteAt(p []byte, off int64) (int, error) { return len(p), nil }
func (n null) Size() int64 { return n.size }
func (n null) Flush() error { return nil }
func (n null) Close() error { return nil }
func main() {
// Serve until a signal; cancelling the context stops the device gracefully.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
dev, err := ublk.CreateAndServe(ctx, ublk.DefaultParams(null{1 << 30}), nil)
if err != nil {
panic(err)
}
<-dev.Done()
dev.Close() // delete the device
}
sudo modprobe ublk_drv
go build -o nulldisk . && sudo ./nulldisk &
sudo mkfs.ext4 /dev/ublkb0 && sudo mount /dev/ublkb0 /mnt
Getting started covers requirements, the two example servers, and how to clean up a device left behind by a killed process.
Library status and testing#
go-ublk v0.2.0 implements the kernel interface through Linux 7.3-rc5, including recovery and zero copy. Its API can change between minor releases. Releases and testing describe its current scope.
lib-ublk’s RAM and multi-queue device stages passed on six kernels. Its bindings passed RAM data-integrity stages on Linux 6.12, and the property campaign passed 200,000 cases per property. Its source release is pending, and a license hasn’t been selected.
The compatibility matrix keeps the libraries’ test scopes separate. Known kernel bugs includes the zero-copy UBSAN finding and the source analysis behind it.