Skip to content

Day50 - Pollable Character Driver with Event Queue

Goal

Build a pollable character driver and integrate it with userspace epoll.

The final driver supports:

  • /dev/mypoll
  • blocking read()
  • non-blocking read()
  • poll() / epoll()
  • wait queue wakeup
  • event queue
  • queue full handling

Final Behavior

write(/dev/mypoll)
driver pushes one event into queue
wake_up_interruptible()
epoll_wait() wakes up
read(/dev/mypoll)
driver pops one event

Files

Suggested lab directory:

d50-pollable-chrdrv/
├── Makefile
├── mypoll.c
└── test_mypoll.py

Step 1 - Minimal Module Template

Start from an empty kernel module.

/**
 * @file mypoll.c
 * @brief Pollable character driver lab
 */

#include <linux/module.h>
#include <linux/init.h>

static int __init mypoll_init(void)
{
    pr_info("mypoll: init\n");
    return 0;
}

static void __exit mypoll_exit(void)
{
    pr_info("mypoll: exit\n");
}

module_init(mypoll_init);
module_exit(mypoll_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Pollable character driver lab");

/* End of File */

Step 2 - Add Character Device Skeleton

Add required headers:

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>

Define the device context:

#define DEVICE_NAME "mypoll"

struct mypoll_dev {
    dev_t devt;
    struct cdev cdev;
    struct class *class;
    struct device *dev;
};

static struct mypoll_dev *g_dev;

Add basic open() and release():

static int mypoll_open(struct inode *inode, struct file *file)
{
    struct mypoll_dev *mydev;

    mydev = container_of(inode->i_cdev, struct mypoll_dev, cdev);
    file->private_data = mydev;

    pr_info("mypoll: open\n");
    return 0;
}

static int mypoll_release(struct inode *inode, struct file *file)
{
    pr_info("mypoll: release\n");
    return 0;
}

Add file operations:

static const struct file_operations mypoll_fops = {
    .owner = THIS_MODULE,
    .open = mypoll_open,
    .release = mypoll_release,
};

Register the char device in module init:

g_dev = kzalloc(sizeof(*g_dev), GFP_KERNEL);
if (!g_dev)
    return -ENOMEM;

ret = alloc_chrdev_region(&g_dev->devt, 0, 1, DEVICE_NAME);
if (ret)
    goto err_alloc_chrdev;

cdev_init(&g_dev->cdev, &mypoll_fops);
g_dev->cdev.owner = THIS_MODULE;

ret = cdev_add(&g_dev->cdev, g_dev->devt, 1);
if (ret)
    goto err_cdev_add;

g_dev->class = class_create(DEVICE_NAME);
if (IS_ERR(g_dev->class)) {
    ret = PTR_ERR(g_dev->class);
    goto err_class_create;
}

g_dev->dev = device_create(g_dev->class, NULL,
                           g_dev->devt, NULL,
                           DEVICE_NAME);
if (IS_ERR(g_dev->dev)) {
    ret = PTR_ERR(g_dev->dev);
    goto err_device_create;
}

Cleanup in module exit:

device_destroy(g_dev->class, g_dev->devt);
class_destroy(g_dev->class);
cdev_del(&g_dev->cdev);
unregister_chrdev_region(g_dev->devt, 1);
kfree(g_dev);

Step 3 - Add Wait Queue, Mutex, and Event Queue

Add headers:

#include <linux/wait.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/poll.h>
#include <linux/types.h>

Define event queue structures:

#define MYPOLL_QUEUE_SIZE 16

struct mypoll_event {
    u32 counter;
};

struct mypoll_queue {
    struct mypoll_event events[MYPOLL_QUEUE_SIZE];
    unsigned int head;
    unsigned int tail;
    unsigned int count;
};

Update driver context:

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;
};

Initialize synchronization and queue state:

init_waitqueue_head(&g_dev->wq);
mutex_init(&g_dev->lock);
mypoll_queue_init(&g_dev->queue);
g_dev->event_counter = 0;

Step 4 - Add Queue Helpers

static int mypoll_queue_init(struct mypoll_queue *q)
{
    if (!q)
        return -EINVAL;

    memset(q, 0, sizeof(*q));
    return 0;
}

static bool mypoll_queue_is_empty(const struct mypoll_queue *q)
{
    return q->count == 0;
}

static bool mypoll_queue_is_full(const struct mypoll_queue *q)
{
    return q->count == MYPOLL_QUEUE_SIZE;
}

static int mypoll_queue_push(struct mypoll_queue *q,
                             const struct mypoll_event *event)
{
    if (!q || !event)
        return -EINVAL;

    if (mypoll_queue_is_full(q))
        return -ENOBUFS;

    q->events[q->tail] = *event;
    q->tail = (q->tail + 1) % MYPOLL_QUEUE_SIZE;
    q->count++;

    return 0;
}

static int mypoll_queue_pop(struct mypoll_queue *q,
                            struct mypoll_event *event)
{
    if (!q)
        return -EINVAL;

    if (mypoll_queue_is_empty(q))
        return -ENODATA;

    if (event)
        *event = q->events[q->head];

    q->head = (q->head + 1) % MYPOLL_QUEUE_SIZE;
    q->count--;

    return 0;
}

The queue helpers are expected to be called while holding mydev->lock.


Step 5 - Implement Write as Fake Event Source

static ssize_t mypoll_write(struct file *file,
                            const char __user *buf,
                            size_t len,
                            loff_t *off)
{
    struct mypoll_dev *mydev = file->private_data;
    struct mypoll_event event;
    int ret;

    (void)buf;
    (void)off;

    if (!mydev)
        return -ENODEV;

    mutex_lock(&mydev->lock);

    event.counter = mydev->event_counter + 1;

    ret = mypoll_queue_push(&mydev->queue, &event);
    if (ret) {
        mutex_unlock(&mydev->lock);
        return ret;
    }

    mydev->event_counter = event.counter;

    mutex_unlock(&mydev->lock);

    wake_up_interruptible(&mydev->wq);

    dev_info(mydev->dev, "mypoll: event trigger %u\n", event.counter);

    return len;
}

Step 6 - Implement Poll

static __poll_t mypoll_poll(struct file *file, poll_table *wait)
{
    struct mypoll_dev *mydev = file->private_data;
    __poll_t mask = 0;

    if (!mydev)
        return EPOLLERR;

    poll_wait(file, &mydev->wq, wait);

    mutex_lock(&mydev->lock);

    if (!mypoll_queue_is_empty(&mydev->queue))
        mask |= EPOLLIN | EPOLLRDNORM;

    mutex_unlock(&mydev->lock);

    return mask;
}

poll_wait() registers the wait queue used to wake poll/epoll waiters.

The returned mask reports the current device readiness.


Step 7 - Implement Blocking and Non-blocking Read

Add wait condition helper:

static bool mypoll_is_event_enqueued(const struct mypoll_queue *q)
{
    return READ_ONCE(q->count) > 0;
}

Implement read():

static ssize_t mypoll_read(struct file *file,
                           char __user *buf,
                           size_t len,
                           loff_t *off)
{
    struct mypoll_dev *mydev = file->private_data;
    struct mypoll_event event;
    char kbuf[64];
    int n;
    int ret;

    (void)off;

    if (!mydev)
        return -ENODEV;

    while (1) {
        mutex_lock(&mydev->lock);

        if (!mypoll_queue_is_empty(&mydev->queue)) {
            ret = mypoll_queue_pop(&mydev->queue, &event);
            mutex_unlock(&mydev->lock);

            if (ret)
                return ret;

            break;
        }

        mutex_unlock(&mydev->lock);

        if (file->f_flags & O_NONBLOCK)
            return -EAGAIN;

        ret = wait_event_interruptible(
            mydev->wq,
            mypoll_is_event_enqueued(&mydev->queue)
        );
        if (ret)
            return ret;
    }

    n = scnprintf(kbuf, sizeof(kbuf), "event=%u\n", event.counter);

    if (len < n)
        return -EINVAL;

    if (copy_to_user(buf, kbuf, n))
        return -EFAULT;

    return n;
}

Important behavior:

  • Blocking read waits when the queue is empty.
  • Non-blocking read returns -EAGAIN.
  • Each successful read pops one event.
  • File offset is not used.

Step 8 - Update File Operations

static const struct file_operations mypoll_fops = {
    .owner = THIS_MODULE,
    .open = mypoll_open,
    .read = mypoll_read,
    .write = mypoll_write,
    .poll = mypoll_poll,
    .release = mypoll_release,
};

Step 9 - Makefile

# SPDX-License-Identifier: GPL-2.0
# Simple Makefile for out-of-tree kernel module

obj-m := mypoll.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

.PHONY: all clean

Step 10 - Python Integrated Test

The Python test verifies:

  • device node exists
  • blocking read waits
  • write wakes blocking read
  • non-blocking read returns EAGAIN
  • epoll receives EPOLLIN
  • event queue preserves FIFO order
  • full queue rejects new writes with ENOBUFS

Run:

sudo python3 test_mypoll.py

Expected 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 =====

Manual Test

Build and load:

make
sudo insmod mypoll.ko
ls -l /dev/mypoll

Trigger events:

echo 123 | sudo tee /dev/mypoll
echo 456 | sudo tee /dev/mypoll

Read events:

sudo cat /dev/mypoll
sudo cat /dev/mypoll

Unload:

sudo rmmod mypoll

Key Observations

EPOLLIN

EPOLLIN means that read() can return data without blocking.

It does not describe where the data came from.

In this driver, userspace write() is only a fake event source.

In a real driver, the event source may be an IRQ, timer, hardware FIFO, or bus transaction.


poll_table *wait

The wait argument is passed from the poll/epoll core.

The driver does not directly manipulate it.

It is passed into poll_wait():

poll_wait(file, &mydev->wq, wait);

This tells the poll/epoll core which wait queue can wake this file descriptor.


read() and File Offset

This driver does not use *off.

It behaves like an event device.

Each read() consumes one queued event.


Lab Result

The final driver successfully demonstrates a complete event-driven path:

write()
queue event
wake_up_interruptible()
epoll_wait()
read()
pop event

This completes Day50.