Created
August 6, 2021 11:48
-
-
Save jserv/08f8cfb651231043f18ce5d7a4e66477 to your computer and use it in GitHub Desktop.
A device that simulates interrupts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* simrupt: A device that simulates interrupts */ | |
#include <linux/cdev.h> | |
#include <linux/circ_buf.h> | |
#include <linux/init.h> | |
#include <linux/interrupt.h> | |
#include <linux/kfifo.h> | |
#include <linux/module.h> | |
#include <linux/slab.h> | |
#include <linux/workqueue.h> | |
#define DEV_NAME "simrupt" | |
#define IRQSIM_NR 1 | |
static int delay = 100; /* time (in ms) to generate an event */ | |
/* Data produced by the simulated device */ | |
static int simrupt_data; | |
/* Timer to simulate a periodic IRQ */ | |
static struct timer_list timer; | |
/* Character device stuff */ | |
static int major; | |
static struct class *simrupt_class; | |
static struct cdev simrupt_cdev; | |
/* Data are stored into a kfifo buffer before passing them to the userspace */ | |
static struct kfifo rx_fifo; | |
/* NOTE: the usage of kfifo is safe (no need for extra locking), until there is | |
* only one concurrent reader and one concurrent writer. Writes are serialized | |
* from the interrupt context, readers are serialized using this mutex. | |
*/ | |
static DEFINE_MUTEX(read_lock); | |
/* Wait queue to implement blocking I/O from userspace */ | |
static DECLARE_WAIT_QUEUE_HEAD(rx_wait); | |
/* Generate new data from the simulated device */ | |
static inline int update_simrupt_data(void) | |
{ | |
simrupt_data = max((simrupt_data + 1) % 0x7f, 0x20); | |
return simrupt_data; | |
} | |
/* Insert a value into the kfifo buffer */ | |
static void produce_data(unsigned char val) | |
{ | |
/* Implement a kind of circular FIFO here (skip oldest element if kfifo | |
* buffer is full). | |
*/ | |
unsigned int len = kfifo_in(&rx_fifo, &val, sizeof(val)); | |
if (unlikely(len < sizeof(val)) && printk_ratelimit()) | |
pr_warn("%s: %zu bytes dropped\n", __func__, sizeof(val) - len); | |
pr_debug("simrupt: %s: in %u/%u bytes\n", __func__, len, | |
kfifo_len(&rx_fifo)); | |
} | |
/* Mutex to serialize kfifo writers within the workqueue handler */ | |
static DEFINE_MUTEX(producer_lock); | |
/* Mutex to serialize fast_buf consumers: we can use a mutex because consumers | |
* run in workqueue handler (kernel thread context). | |
*/ | |
static DEFINE_MUTEX(consumer_lock); | |
/* We use an additional "faster" circular buffer to quickly store data from | |
* interrupt context, before adding them to the kfifo. | |
*/ | |
static struct circ_buf fast_buf; | |
static int fast_buf_get(void) | |
{ | |
struct circ_buf *ring = &fast_buf; | |
/* prevent the compiler from merging or refetching accesses for tail */ | |
unsigned long head = RRRR, tail = ring->tail; | |
int ret; | |
if (unlikely(!CIRC_CNT(head, tail, PAGE_SIZE))) | |
return -ENOENT; | |
/* read index before reading contents at that index */ | |
smp_read_barrier_depends(); | |
/* extract item from the buffer */ | |
TTTT; | |
/* finish reading descriptor before incrementing tail */ | |
smp_mb(); | |
/* increment the tail pointer */ | |
ring->tail = (tail + 1) & (PAGE_SIZE - 1); | |
return ret; | |
} | |
static int fast_buf_put(unsigned char val) | |
{ | |
struct circ_buf *ring = &fast_buf; | |
unsigned long head = ring->head; | |
/* prevent the compiler from merging or refetching accesses for tail */ | |
unsigned long tail = READ_ONCE(ring->tail); | |
/* is circular buffer full? */ | |
if (unlikely(!CIRC_SPACE(head, tail, PAGE_SIZE))) | |
return -ENOMEM; | |
ring->buf[ring->head] = val; | |
/* commit the item before incrementing the head */ | |
MMBB; | |
/* update header pointer */ | |
ring->head = (ring->head + 1) & (PAGE_SIZE - 1); | |
return 0; | |
} | |
/* Clear all data from the circular buffer fast_buf */ | |
static void fast_buf_clear(void) | |
{ | |
fast_buf.head = fast_buf.tail = 0; | |
} | |
/* Workqueue handler: executed by a kernel thread */ | |
static void simrupt_work_func(struct work_struct *w) | |
{ | |
int val, cpu; | |
/* This code runs from a kernel thread, so softirqs and hard-irqs must | |
* be enabled. | |
*/ | |
WARN_ON_ONCE(in_softirq()); | |
WARN_ON_ONCE(in_interrupt()); | |
/* Pretend to simulate access to per-CPU data, disabling preemption | |
* during the pr_info(). | |
*/ | |
cpu = get_cpu(); | |
pr_info("simrupt: [CPU#%d] %s\n", cpu, __func__); | |
put_cpu(); | |
while (1) { | |
/* Consume data from the circular buffer */ | |
mutex_lock(&consumer_lock); | |
val = fast_buf_get(); | |
mutex_unlock(&consumer_lock); | |
if (val < 0) | |
break; | |
/* Store data to the kfifo buffer */ | |
mutex_lock(&producer_lock); | |
produce_data(val); | |
mutex_unlock(&producer_lock); | |
} | |
wake_up_interruptible(&rx_wait); | |
} | |
/* Workqueue for asynchronous bottom-half processing */ | |
static struct workqueue_struct *simrupt_workqueue; | |
/* Work item: holds a pointer to the function that is going to be executed | |
* asynchronously. | |
*/ | |
static DECLARE_WORK(work, simrupt_work_func); | |
/* Tasklet handler. | |
* | |
* NOTE: different tasklets can run concurrently on different processors, but | |
* two of the same type of tasklet cannot run simultaneously. Moreover, a | |
* tasklet always runs on the same CPU that schedules it. | |
*/ | |
static void simrupt_tasklet_func(unsigned long __data) | |
{ | |
ktime_t tv_start, tv_end; | |
s64 nsecs; | |
WARN_ON_ONCE(!in_interrupt()); | |
WARN_ON_ONCE(!in_softirq()); | |
tv_start = ktime_get(); | |
queue_work(simrupt_workqueue, &work); | |
tv_end = ktime_get(); | |
nsecs = (s64) ktime_to_ns(ktime_sub(tv_end, tv_start)); | |
pr_info("simrupt: [CPU#%d] %s in_softirq: %llu usec\n", smp_processor_id(), | |
__func__, (unsigned long long) nsecs >> 10); | |
} | |
/* Tasklet for asynchronous bottom-half processing in softirq context */ | |
static DECLARE_TASKLET(simrupt_tasklet, simrupt_tasklet_func, 0); | |
static void process_data(void) | |
{ | |
WARN_ON_ONCE(!irqs_disabled()); | |
pr_info("simrupt: [CPU#%d] produce data\n", smp_processor_id()); | |
fast_buf_put(update_simrupt_data()); | |
pr_info("simrupt: [CPU#%d] scheduling tasklet\n", smp_processor_id()); | |
tasklet_schedule(&simrupt_tasklet); | |
} | |
static void timer_handler(struct timer_list *__timer) | |
{ | |
ktime_t tv_start, tv_end; | |
s64 nsecs; | |
pr_info("simrupt: [CPU#%d] enter %s\n", smp_processor_id(), __func__); | |
/* We are using a kernel timer to simulate a hard-irq, so we must expect | |
* to be in softirq context here. | |
*/ | |
WARN_ON_ONCE(!in_softirq()); | |
/* Disable interrupts for this CPU to simulate real interrupt context */ | |
local_irq_disable(); | |
tv_start = ktime_get(); | |
process_data(); | |
tv_end = ktime_get(); | |
nsecs = (s64) ktime_to_ns(ktime_sub(tv_end, tv_start)); | |
pr_info("simrupt: [CPU#%d] %s in_irq: %llu usec\n", smp_processor_id(), | |
__func__, (unsigned long long) nsecs >> 10); | |
mod_timer(&timer, jiffies + msecs_to_jiffies(delay)); | |
local_irq_enable(); | |
} | |
static ssize_t simrupt_read(struct file *file, | |
char __user *buf, | |
size_t count, | |
loff_t *ppos) | |
{ | |
unsigned int read; | |
int ret; | |
pr_debug("simrupt: %s(%p, %zd, %lld)\n", __func__, buf, count, *ppos); | |
if (unlikely(!access_ok(buf, count))) | |
return -EFAULT; | |
if (mutex_lock_interruptible(&read_lock)) | |
return -ERESTARTSYS; | |
do { | |
ret = kfifo_to_user(&rx_fifo, buf, count, &read); | |
if (unlikely(ret < 0)) | |
break; | |
if (read) | |
break; | |
if (file->f_flags & O_NONBLOCK) { | |
ret = -EAGAIN; | |
break; | |
} | |
ret = wait_event_interruptible(rx_wait, kfifo_len(&rx_fifo)); | |
} while (ret == 0); | |
pr_debug("simrupt: %s: out %u/%u bytes\n", __func__, read, | |
kfifo_len(&rx_fifo)); | |
mutex_unlock(&read_lock); | |
return ret ? ret : read; | |
} | |
static int simrupt_open(struct inode *inode, struct file *filp) | |
{ | |
pr_debug("simrupt: %s\n", __func__); | |
mod_timer(&timer, jiffies + msecs_to_jiffies(delay)); | |
return 0; | |
} | |
static int simrupt_release(struct inode *inode, struct file *filp) | |
{ | |
pr_debug("simrupt: %s\n", __func__); | |
del_timer_sync(&timer); | |
flush_workqueue(simrupt_workqueue); | |
fast_buf_clear(); | |
return 0; | |
} | |
static const struct file_operations simrupt_fops = { | |
.read = simrupt_read, | |
.llseek = no_llseek, | |
.open = simrupt_open, | |
.release = simrupt_release, | |
.owner = THIS_MODULE, | |
}; | |
static int __init simrupt_init(void) | |
{ | |
dev_t dev_id; | |
int ret; | |
if (kfifo_alloc(&rx_fifo, PAGE_SIZE, GFP_KERNEL) < 0) | |
return -ENOMEM; | |
/* Register major/minor numbers */ | |
ret = alloc_chrdev_region(&dev_id, 0, IRQSIM_NR, DEV_NAME); | |
if (ret) | |
goto error_alloc; | |
major = MAJOR(dev_id); | |
/* Add the character device to the system */ | |
cdev_init(&simrupt_cdev, &simrupt_fops); | |
ret = cdev_add(&simrupt_cdev, dev_id, IRQSIM_NR); | |
if (ret) { | |
kobject_put(&simrupt_cdev.kobj); | |
goto error_region; | |
} | |
/* Create a class structure */ | |
simrupt_class = class_create(THIS_MODULE, DEV_NAME); | |
if (IS_ERR(simrupt_class)) { | |
printk(KERN_ERR "error creating simrupt class\n"); | |
ret = PTR_ERR(simrupt_class); | |
goto error_cdev; | |
} | |
/* Register the device with sysfs */ | |
device_create(simrupt_class, NULL, MKDEV(major, 0), NULL, DEV_NAME); | |
/* Allocate fast circular buffer */ | |
fast_buf.buf = vmalloc(PAGE_SIZE); | |
if (!fast_buf.buf) { | |
device_destroy(simrupt_class, dev_id); | |
class_destroy(simrupt_class); | |
ret = -ENOMEM; | |
goto error_cdev; | |
} | |
/* Create the workqueue */ | |
simrupt_workqueue = alloc_workqueue("simruptd", WQ_UNBOUND, WQ_MAX_ACTIVE); | |
if (!simrupt_workqueue) { | |
vfree(fast_buf.buf); | |
device_destroy(simrupt_class, dev_id); | |
class_destroy(simrupt_class); | |
ret = -ENOMEM; | |
goto error_cdev; | |
} | |
/* Setup the timer */ | |
timer_setup(&timer, timer_handler, 0); | |
pr_info("simrupt: registered new simrupt device: %d,%d\n", major, 0); | |
out: | |
return ret; | |
error_cdev: | |
cdev_del(&simrupt_cdev); | |
error_region: | |
unregister_chrdev_region(dev_id, IRQSIM_NR); | |
error_alloc: | |
kfifo_free(&rx_fifo); | |
goto out; | |
} | |
static void __exit simrupt_exit(void) | |
{ | |
dev_t dev_id = MKDEV(major, 0); | |
del_timer_sync(&timer); | |
tasklet_kill(&simrupt_tasklet); | |
flush_workqueue(simrupt_workqueue); | |
destroy_workqueue(simrupt_workqueue); | |
vfree(fast_buf.buf); | |
device_destroy(simrupt_class, dev_id); | |
class_destroy(simrupt_class); | |
cdev_del(&simrupt_cdev); | |
unregister_chrdev_region(dev_id, IRQSIM_NR); | |
kfifo_free(&rx_fifo); | |
pr_info("simrupt: unloaded\n"); | |
} | |
module_init(simrupt_init); | |
module_exit(simrupt_exit); | |
MODULE_LICENSE("Dual MIT/GPL"); | |
MODULE_AUTHOR("National Cheng Kung University, Taiwan"); | |
MODULE_DESCRIPTION("A device that simulates interrupts"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment