Skip to content

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:

/dev/mypoll

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:

EPOLLIN | EPOLLRDNORM

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:

  1. Registers the wait queue with poll_wait()
  2. 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.

#define MYPOLL_QUEUE_SIZE 16

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:

  1. device node existence
  2. blocking read wait behavior
  3. write wakeup behavior
  4. non-blocking read EAGAIN
  5. epoll EPOLLIN
  6. FIFO queue ordering
  7. 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:

epoll_wait(epfd, events, maxevents, timeout);

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:

kernel event queue
wait queue wakeup
poll/epoll readiness
userspace read

This completes Day50.