Four Linux kernel modules, each targeting a different driver subsystem and the contract it imposes between kernel and user space, hardware, or the networking stack. None require physical hardware: every driver builds and runs end-to-end in Docker + QEMU against a real kernel, on Linux, macOS, or Windows.
| Driver | Subsystem | Core mechanisms |
|---|---|---|
linux-character-device-driver |
Character device | file_operations, mutex, wait queues, blocking I/O, ioctl |
linux-block-device-driver |
Block I/O (gendisk/blk-mq) |
bio/request segment iteration, spinlock-protected memcpy, page cache vs. O_DIRECT |
linux-network-device-driver |
Networking (net_device) |
sk_buff, descriptor rings, NAPI, TX queue backpressure |
linux-pcie-device-driver |
PCI / PCIe | BAR-mapped MMIO, MSI interrupts, DMA buffer management |
Each project targets a different kernel subsystem on purpose, so that together they cover most of the contracts a driver author has to get right.
circbuf — Virtual Character Device
A kernel module that registers /dev/circbuf as a bounded circular-buffer
device. Writers produce bytes into a fixed-size buffer; readers consume them in
FIFO order. When the buffer is full, writers block; when it is empty, readers
block. Multiple processes can open the device concurrently and are safely
arbitrated through kernel synchronization primitives.
What it covers: file_operations vtable (C-level polymorphism between VFS
and the driver); copy_to_user/copy_from_user for crossing the kernel/user
boundary safely; mutex for sleeping critical sections vs. spinlock for
atomic ones; wait_queue_head_t for blocking I/O; and ioctl design for
out-of-band device control. Because there is no hardware dependency, the focus
stays entirely on getting these contracts right.
Key object: struct file_operations — a vtable of function pointers the
kernel calls when user space invokes open, read, write, ioctl, or
release on the device file.
vblk — Virtual RAM-backed Block Device
A kernel module that registers /dev/vblk0 backed by a vmalloc'd region
of kernel memory. It behaves like a real disk: dd, mkfs, mount, and
arbitrary pread/pwrite all work against it because it implements the same
gendisk / block_device_operations / blk_mq_ops contract every real block
driver implements. Only the "hardware" — a contiguous kernel buffer instead of
a physical device — differs.
What it covers: gendisk + blk-mq registration (blk_mq_alloc_disk,
add_disk); bio/request segment iteration inside queue_rq; why a
spinlock is the right primitive once copy_to/from_user is off the table;
and the page-cache vs. O_DIRECT distinction that governs whether I/O ever
reaches the driver at all. The test suite validates all three paths: buffered
I/O through the page cache, O_DIRECT bypassing it, and a full
mke2fs + mount + file I/O + umount round trip through ext2.
Key object: struct gendisk paired with a struct blk_mq_tag_set — the
block-layer analogue of file_operations.
netdrv — Virtual Ethernet Driver Pair with NAPI
A kernel module that creates a software point-to-point Ethernet pair (vnet0
↔ vnet1) built around the same structures a real NIC driver uses: TX/RX
descriptor rings, NAPI interrupt-mitigated packet reception, and
netif_stop_queue/netif_wake_queue TX backpressure. A packet transmitted
on one interface appears as received on the other; the "wire" is software,
but the driver-side mechanics follow the real networking stack contracts.
What it covers: net_device registration and net_device_ops; sk_buff
lifecycle (allocation, headroom, push/pull); NAPI (why per-packet interrupts
would saturate the CPU and how poll-based batching avoids it); TX queue
backpressure under load; and network namespace isolation for testing. The
project includes a real end-to-end ping test that caught an actual bug during
development — IFF_NOARP made ring counters look healthy while ping silently
failed 100% of the time.
Key object: struct net_device — the networking stack's equivalent of
gendisk, paired with a struct napi_struct per interface.
pcie-edu-driver — PCIe Driver for QEMU's edu Device
A kernel module that drives QEMU's built-in edu educational PCI device
(vendor 0x1234, device 0x11e8). Unlike the virtual drivers above, this
targets a device that is enumerated over the PCIe bus, has BAR-mapped memory
registers, raises hardware interrupts, and exposes a DMA engine — it just
happens to be emulated by QEMU rather than soldered to a board. The driver
code, and everything it has to get right, is identical to what a real NIC,
storage controller, or accelerator driver requires.
What it covers: the full PCI driver lifecycle (pci_enable_device,
pci_iomap for BAR0 MMIO, pci_alloc_irq_vectors for MSI,
dma_alloc_coherent for DMA buffers, pci_unregister_driver cleanup);
ioread32/iowrite32 for MMIO register access; an MSI interrupt handler that
wakes a wait queue to unblock a user-space ioctl; and a DMA round-trip test
(host → device → host). The driver also exposes a misc character device at
/dev/pcie_edu so user space can trigger factorial computations and DMA
transfers via ioctl.
Key object: struct pci_dev — the bus-enumerated device handle, paired
with BAR-mapped MMIO, an MSI interrupt vector, and a dma_alloc_coherent
buffer.
The character device establishes the kernel/user-space boundary and
synchronization primitives. The block device moves that same contract into the
block layer's request/bio model, and introduces the page-cache visibility
problem. The networking and PCIe drivers build further on that foundation while
introducing their own subsystem-specific object models (net_device/sk_buff
vs. pci_dev/MMIO/MSI) and execution-context rules (softirq NAPI polling vs.
hardirq MSI handling).
.
├── linux-character-device-driver/ # circbuf — virtual character device
├── linux-block-device-driver/ # vblk — virtual RAM-backed block device
├── linux-network-device-driver/ # netdrv — virtual Ethernet driver pair
├── linux-pcie-device-driver/ # pcie-edu-driver — QEMU "edu" PCIe driver
└── LICENSE # MIT
Each subdirectory is self-contained, with its own Makefile, docker/
test harness, and README.md covering architecture, key concepts,
build/test instructions, design tradeoffs, and verified test results.
| Tool | Minimum version | Notes |
|---|---|---|
| Docker | 20.10+ | Required for the recommended Docker + QEMU path |
| QEMU | 6.0+ | Embedded inside the Docker image; no host install needed |
| make / gcc | any recent | Only needed for native (non-Docker) builds |
| Linux host with matching kernel headers | — | Alternative to Docker; see each project's README |
No physical hardware, no Linux host, and no QEMU pre-install are required for the default Docker path. Docker pulls the matching kernel headers and QEMU image automatically.
Every driver follows the same pattern: build against real kernel headers inside Docker, boot a stock kernel under QEMU, then load and exercise the module end-to-end — not a build-only check.
# circbuf, vblk, pcie-edu-driver
cd <project-directory>
./docker/run.sh test# netdrv (self-contained Docker image)
cd linux-network-device-driver
docker build -f docker/Dockerfile -t netdrv-e2e . && docker run --rm netdrv-e2edocker/run.sh test (and the equivalent netdrv command):
- Builds the
.koagainst real kernel headers inside a Docker container. - Boots a stock
6.8.0-124-generickernel under QEMU with a minimal busybox initramfs. insmods the module, runs the test suite (basic I/O, stress, filesystem mount where applicable, ioctl round-trips), andrmmods the module.- Checks
dmesgfor panics, lockups, or unexpected warnings. - Exits non-zero on any failure.
Each project's README documents a native build path for environments where Docker is unavailable. The general pattern:
make # build the .ko against the running kernel's headers
sudo insmod <mod>.ko
dmesg | tail -20 # confirm probe / registration
sudo ./<test_binary>
sudo rmmod <mod>This requires a Linux host (or VM) with matching kernel headers and — for the
PCIe driver — a QEMU session started with -device edu.
All four drivers pass their full test suites against kernel
6.8.0-124-generic (Ubuntu 24.04, aarch64 / QEMU virt or q35).
| Check | Result |
|---|---|
insmod / rmmod (default 4096-byte buffer) |
✅ Pass |
basic_test — write → read → ioctl roundtrip |
✅ Pass |
query_stats — ioctl buffer introspection |
✅ capacity=4096 used=0 available=4096 |
stress — 4 writers / 4 readers, 5 s |
✅ total_written=13682240 == total_read=13682240 |
Reload with buffer_size=16384 |
✅ capacity=16384 confirmed |
dmesg |
No panics, no lockups |
| Check | Result |
|---|---|
insmod / rmmod (default 16 MiB) |
✅ Pass |
basic_test — O_DIRECT pwrite/pread/pwritev + ioctl |
✅ capacity_bytes=16777216 reads=3 writes=3 |
stress — 4 threads, disjoint regions, 5 s |
✅ total_written=222857728, zero corruption |
mke2fs + mount -t ext2 + file I/O + umount |
✅ Pass |
Reload with disk_size_mb=8 |
✅ capacity_bytes=8388608 confirmed |
dmesg |
No panics, no lockups |
| Check | Result |
|---|---|
Module load — vnet0 / vnet1 registered |
✅ |
Carrier state on open / stop |
✅ |
ping across netns pair |
✅ 4/4 packets, 0% loss |
tx_packets / rx_packets symmetry |
✅ 11 == 11 |
Flood ping with ring_size=4 |
✅ 2000/2000, 0% loss |
TX backpressure under iperf3 UDP load |
✅ tx_queue_stops/wakes = 2, 0 packets lost (59670 == 59670) |
| 10× load/unload loop | ✅ clean dmesg throughout |
| Check | Result |
|---|---|
edu device enumerated (lspci -d 1234:11e8) |
✅ Pass |
probe() — BAR0 mapped, ident register read, DMA buffer allocated |
✅ Pass |
MSI vector registered (/proc/interrupts) |
✅ Pass |
/dev/pcie_edu opens successfully |
✅ Pass |
Liveness register round-trip (write_val → ~write_val) |
✅ Pass |
PCIE_EDU_COMPUTE — 0!, 1!, 5!, 10!, 12! via MSI |
✅ Pass (5/5) |
PCIE_EDU_DMA_TRANSFER — 4096-byte host→device→host |
✅ Pass |
remove() / rmmod — no leaked MMIO or IRQ |
✅ Pass |
- Linux Device Drivers, 3rd Edition — Corbet, Rubini, Kroah-Hartman (free at lwn.net)
- Linux Kernel Development, 3rd Edition — Robert Love
- Rosen, R. Linux Kernel Networking: Implementation and Theory
Documentation/driver-api/,Documentation/block/,Documentation/networking/napi.rst,Documentation/PCI/pci.rstin the Linux kernel source tree
MIT — see LICENSE.