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:
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:
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:
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:
Trigger events:
Read events:
Unload:
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():
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:
This completes Day50.