Day50 - Pollable Character Driver with Event Queue¶
Date¶
2026-05-05
Topic¶
Pollable character driver and userspace epoll integration.
Goal¶
The goal of Day50 was to connect the previous event-driven userspace learning with a kernel driver event source.
The target was to implement a character driver that supports:
- wait queue
- blocking read
- non-blocking read
poll()/epoll()- fake event generation
- event queue
- FIFO event consumption
- queue full handling
Background¶
Before Day50, an epoll-based TCP server had already been completed.
That server included:
- multi-client support
- non-blocking sockets
- epoll event loop
- TX queue with partial send handling
- timerfd idle timeout
- signalfd graceful shutdown
- client lifecycle management
Day50 extended the same event-driven thinking into kernel driver development.
Implementation Summary¶
Implemented a character device:
The driver exposes a fake event source through write().
Each write generates one event and pushes it into an internal FIFO queue.
Userspace can monitor the device with epoll.
When the queue is not empty, the driver reports:
Then userspace can call read() to pop one event.
Driver Architecture¶
The driver uses:
struct mypoll_event {
u32 counter;
};
struct mypoll_queue {
struct mypoll_event events[MYPOLL_QUEUE_SIZE];
unsigned int head;
unsigned int tail;
unsigned int count;
};
struct mypoll_dev {
dev_t devt;
struct cdev cdev;
struct class *class;
struct device *dev;
wait_queue_head_t wq;
struct mutex lock;
u32 event_counter;
struct mypoll_queue queue;
};
Event Flow¶
The final event flow is:
userspace write()
↓
mypoll_write()
↓
push event into queue
↓
wake_up_interruptible()
↓
epoll_wait() wakes up
↓
mypoll_poll() reports EPOLLIN
↓
userspace read()
↓
mypoll_read()
↓
pop one event from queue
Read Behavior¶
The driver supports both blocking and non-blocking read.
| Mode | Queue Empty Behavior |
|---|---|
| Blocking read | Sleep with wait_event_interruptible() |
| Non-blocking read | Return -EAGAIN |
The read path re-checks the queue after waking up.
This is required because waking up does not guarantee that the event is still available when the reader gets the mutex.
Another reader may have consumed the event first.
Poll Behavior¶
The poll callback does two things:
- Registers the wait queue with
poll_wait() - Reports readiness based on queue state
poll_wait(file, &mydev->wq, wait);
if (!mypoll_queue_is_empty(&mydev->queue))
mask |= EPOLLIN | EPOLLRDNORM;
poll_wait() does not directly block.
It registers the wait queue so that the poll/epoll core knows how the file descriptor can be woken later.
Queue Behavior¶
The driver uses a 16-entry FIFO queue.
Queue behavior:
| Condition | Result |
|---|---|
| Queue has space | write() queues one event |
| Queue is full | write() returns -ENOBUFS |
| Queue has events | poll() returns EPOLLIN |
| Queue is empty | blocking read() waits |
| Queue is empty + non-blocking fd | read() returns -EAGAIN |
Testing¶
A Python integrated test was created to verify the driver.
The test covers:
- device node existence
- blocking read wait behavior
- write wakeup behavior
- non-blocking read
EAGAIN - epoll
EPOLLIN - FIFO queue ordering
- queue full
ENOBUFS
Observed test result:
===== mypoll Python integrated test =====
===== Test 0: Device exists =====
[OK] Found /dev/mypoll
===== Test 1: Blocking read should wait =====
[OK] Blocking read is waiting as expected
[OK] Blocking reader cleaned up
===== Test 2: Write should wake blocking read =====
event=26
[OK] Blocking read received event
===== Test 3: Non-blocking read without event should return EAGAIN =====
[OK] Non-blocking read returned EAGAIN as expected
===== Test 4: epoll should receive EPOLLIN =====
event=27
[OK] epoll received EPOLLIN and read consumed event
===== Test 5: Event queue FIFO ordering =====
event=28
event=29
event=30
[OK] Queued events were read in FIFO order: [28, 29, 30]
===== Test 6: Queue full should reject new event =====
[OK] Queue full write returned ENOBUFS as expected
[OK] Full queue drained in FIFO order: 31..46
===== Test completed =====
Important Lessons¶
1. EPOLLIN means readable¶
EPOLLIN means that read() can return data without blocking.
It does not mean that the data must come from hardware or a network peer.
In this lab, write() is only used as a fake event trigger.
2. poll_wait() registers a wait queue¶
poll_wait() does not directly control blocking or non-blocking behavior.
It registers the wait queue used by the poll/epoll core.
Blocking behavior is controlled by the userspace call, such as:
3. Event devices should not use file offset¶
The driver does not update *off.
This is intentional.
The device behaves like an event stream, not a file-like buffer.
Each read() consumes one event.
4. Wakeup does not guarantee ownership¶
After wait_event_interruptible() wakes up, the reader must re-check queue state under lock.
Multiple readers may wake up, but only one reader can consume a queued event.
5. Queue helpers need clear lock ownership¶
Queue mutation helpers are called while holding the device mutex.
The wait condition helper uses READ_ONCE() because it is evaluated without holding the mutex.
Current Limitations¶
The current driver is still a learning driver.
Limitations:
- fake event source uses userspace
write() - event payload only contains a counter
- events are popped before
copy_to_user() - no per-open private event queue
- no IRQ or hardware event integration yet
- no timestamp or event type field yet
Possible Next Steps¶
Potential future improvements:
- add timestamp to each event
- add event type field
- replace fake write trigger with timer-based event source
- replace fake write trigger with GPIO IRQ source
- support binary event records instead of text output
- integrate this driver with the existing epoll TCP server
- forward driver events to connected TCP clients
Result¶
Day50 successfully connected kernel driver events with userspace epoll.
The final model is:
This completes Day50.