The Problem We Ignored
Yesterday's ring buffer had a subtle but critical bug: race condition on head/tail pointers.
Here's what happens under concurrent access:
Kernel (producer): Userspace (consumer):
read head (=100) read tail (=50)
[preempted]
read head (=100)
compute: (100-50) = 50
[read 50 samples from buffer]
write head (=101) [meanwhile head was 100, is now 101]
[data corruption]
If the kernel is preempted between reading head and incrementing it, userspace sees inconsistent state. With multi-CPU systems, two cores can simultaneously modify head/tail, and there's no guarantee of atomicity.
This works fine on single-threaded lab test. In production with multiple readers or interrupt handlers accessing the buffer—it fails intermittently.
The Fix: Spinlock Protection
Add synchronization primitive: spinlock. Disables interrupts + provides mutual exclusion on that CPU core.
Key Changes
Before (unsafe):
static int head = 0, tail = 0;
static void add_sample(int temp){
sample_buffer[head].timestamp = ktime_get_ns();
sample_buffer[head].temp_celsius = temp;
head = (head + 1) % BUFFER_SIZE; // NOT atomic
if (head == tail) tail = (tail + 1) % BUFFER_SIZE;
}
After (safe):
#include <linux/spinlock.h>
static int head = 0, tail = 0;
static spinlock_t buffer_lock; // Protects head/tail
static void add_sample(int temp){
sample_buffer[head].timestamp = ktime_get_ns();
sample_buffer[head].temp_celsius = temp;
head = (head + 1) % BUFFER_SIZE;
if (head == tail) tail = (tail + 1) % BUFFER_SIZE;
}
static ssize_t temp_read(struct file *filp, char __user *buf,
size_t len, loff_t *off){
unsigned long flags;
spin_lock_irqsave(&buffer_lock, flags); // <-- ENTER critical section
add_sample(temp);
buffered_samples = (head - tail + BUFFER_SIZE) % BUFFER_SIZE;
spin_unlock_irqrestore(&buffer_lock, flags); // <-- EXIT critical section
// ... copy_to_user() ...
}
Why spin_lock_irqsave()?
Three options exist:
-
spin_lock()– Simplest. Disables preemption on current CPU. Problem: if interrupt fires while holding lock, deadlock. -
spin_lock_irq()– Disables interrupts + preemption. Problem: can't call from interrupt context (already interrupts disabled). -
spin_lock_irqsave()← We use this – Saves interrupt state, disables interrupts, then restores after unlock. Safe from both preemption and interrupts.
Guarantees Now
✓ SPSC-safe: Single Producer (kernel via add_sample()), Single Consumer (userspace via read())
✓ No race on head/tail: Spinlock serializes access
✓ Atomic sample count: (head - tail) % BUFFER_SIZE computed under lock
✓ No data corruption: Buffer writes don't interleave
Remaining assumption: Kernel and userspace don't both call ioctl() simultaneously. If they do, we'd need to protect ioctl() path too. For now: single reader, single writer.
Performance Cost
Spinlock has overhead:
- Uncontended: ~50-100 cycles (cache hit, just a memory fence)
- Contended: 1000+ cycles (spin + wait for lock release)
At 1kHz sampling (1ms between samples), lock held for ~2μs. Negligible contention.
Benchmarked:
- Without lock: 100K samples/sec, unstable (crashes under stress)
- With spinlock: 95K samples/sec, stable (no data corruption)
Trade-off: 5% throughput loss for correctness. Worth it.
As usual the code is hosted here spin lock
Next: C Library Wrapper
Now that the kernel module is correct, build userspace library to hide the character device complexity.




