292 lines
8.4 KiB
C
292 lines
8.4 KiB
C
#include <linux/cdev.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/init.h>
|
|
#include <linux/module.h>
|
|
|
|
// Ref:
|
|
// https://linux-kernel-labs.github.io/refs/heads/master/labs/device_drivers.html
|
|
// https://olegkutkov.me/2018/03/14/simple-linux-character-device-driver/
|
|
|
|
// BEGIN_LST_DEF
|
|
#define MYPIPE_BUFFER_SIZE 4096
|
|
#define MYPIPE_DEVICE_NAME "wendy"
|
|
|
|
static dev_t dev_major = 0;
|
|
|
|
enum MyPipeMinors {
|
|
MYPIPE_MINOR_IN = 0,
|
|
MYPIPE_MINOR_OUT,
|
|
MYPIPE_MINOR_COUNT,
|
|
};
|
|
|
|
struct mypipe_data {
|
|
struct cdev cdev;
|
|
// Whether opened by some writer
|
|
bool is_active;
|
|
u8 buffer[MYPIPE_BUFFER_SIZE];
|
|
size_t write_pos;
|
|
size_t read_pos;
|
|
size_t data_len;
|
|
};
|
|
|
|
// This could be an array if we want multiple cdev
|
|
static struct mypipe_data pdata;
|
|
|
|
static struct class *mypipedev_class = NULL;
|
|
|
|
// Protects the buffer
|
|
static DEFINE_MUTEX(mypipe_lock);
|
|
// Protects the is_active value
|
|
static DEFINE_MUTEX(isactive_lock);
|
|
// These are two wait_queue_head_t for storing tasks that are waiting for
|
|
// writing/ reading
|
|
static DECLARE_WAIT_QUEUE_HEAD(read_queue);
|
|
static DECLARE_WAIT_QUEUE_HEAD(write_queue);
|
|
// END_LST_DEF
|
|
|
|
// BEGIN_LST_OPEN_RELASE
|
|
static int mypipe_open(struct inode *inode, struct file *file) {
|
|
int minor = iminor(inode);
|
|
printk(KERN_INFO "%s opened, minor = %d", MYPIPE_DEVICE_NAME, minor);
|
|
|
|
// If its opening the writing end, we limit it to only one writer
|
|
if (minor == MYPIPE_MINOR_IN) {
|
|
if (mutex_lock_interruptible(&isactive_lock)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
|
|
if (pdata.is_active) {
|
|
return -EBUSY;
|
|
}
|
|
pdata.is_active = true;
|
|
mutex_unlock(&isactive_lock);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int mypipe_release(struct inode *inode, struct file *file) {
|
|
int minor = iminor(inode);
|
|
printk(KERN_INFO "%s released, minor = %d", MYPIPE_DEVICE_NAME, minor);
|
|
|
|
if (minor == MYPIPE_MINOR_IN) {
|
|
mutex_lock(&isactive_lock);
|
|
pdata.is_active = false;
|
|
mutex_unlock(&isactive_lock);
|
|
wake_up_interruptible(&read_queue);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// END_LST_OPEN_RELEASE
|
|
|
|
// BEGIN_LST_READ_WRITE
|
|
static ssize_t mypipe_read(struct file *filep, char __user *buf, size_t size,
|
|
loff_t *offset) {
|
|
ssize_t ret = 0;
|
|
|
|
if (iminor(file_inode(filep)) != MYPIPE_MINOR_OUT) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
// Acquire the lock. If not acquired and interrupted by signal, let kernel
|
|
// to try restart the syscall
|
|
if (mutex_lock_interruptible(&mypipe_lock)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
|
|
// We currently have no data, but we can wait for possible new to write in
|
|
while (pdata.data_len == 0) {
|
|
mutex_unlock(&mypipe_lock);
|
|
|
|
// Check if the writer is still active
|
|
if (mutex_lock_interruptible(&isactive_lock)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
bool writer_is_active = pdata.is_active;
|
|
mutex_unlock(&isactive_lock);
|
|
|
|
if (!writer_is_active) {
|
|
return 0; // No data left and the writer is gone -> EOF for reader
|
|
}
|
|
|
|
// There is still writer, wait for data
|
|
if (filep->f_flags & O_NONBLOCK) {
|
|
// The caller says no waiting, so we don't wait
|
|
return -EAGAIN;
|
|
}
|
|
if (wait_event_interruptible(read_queue, pdata.data_len > 0)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
// Woken, relock for checking the condition
|
|
if (mutex_lock_interruptible(&mypipe_lock)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
}
|
|
|
|
// data_len > 0
|
|
size_t to_read = min(size, pdata.data_len);
|
|
size_t until_end = min(to_read, MYPIPE_BUFFER_SIZE - pdata.read_pos);
|
|
if (copy_to_user(buf, pdata.buffer + pdata.read_pos, until_end)) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
|
|
if (to_read > until_end) {
|
|
if (copy_to_user(buf + until_end, pdata.buffer, to_read - until_end)) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
}
|
|
pdata.read_pos = (pdata.read_pos + to_read) % MYPIPE_BUFFER_SIZE;
|
|
pdata.data_len -= to_read;
|
|
ret = to_read;
|
|
|
|
wake_up_interruptible(&write_queue);
|
|
|
|
out:
|
|
mutex_unlock(&mypipe_lock);
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t mypipe_write(struct file *filep, const char __user *buf,
|
|
size_t size, loff_t *offset) {
|
|
// This is very similar to read(), just write instead of read
|
|
|
|
ssize_t ret = 0;
|
|
|
|
if (iminor(file_inode(filep)) != MYPIPE_MINOR_IN) {
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (mutex_lock_interruptible(&mypipe_lock)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
|
|
while (pdata.data_len == MYPIPE_BUFFER_SIZE) {
|
|
mutex_unlock(&mypipe_lock);
|
|
if (filep->f_flags & O_NONBLOCK) {
|
|
return -EAGAIN;
|
|
}
|
|
if (wait_event_interruptible(write_queue,
|
|
pdata.data_len < MYPIPE_BUFFER_SIZE)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
if (mutex_lock_interruptible(&mypipe_lock)) {
|
|
return -ERESTARTSYS;
|
|
}
|
|
}
|
|
|
|
size_t space_left = MYPIPE_BUFFER_SIZE - pdata.data_len;
|
|
size_t to_write = min(size, space_left);
|
|
size_t until_end = min(to_write, MYPIPE_BUFFER_SIZE - pdata.write_pos);
|
|
|
|
if (copy_from_user(pdata.buffer + pdata.write_pos, buf, until_end)) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
|
|
if (to_write > until_end) {
|
|
if (copy_from_user(pdata.buffer, buf + until_end,
|
|
to_write - until_end)) {
|
|
ret = -EFAULT;
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
pdata.write_pos = (pdata.write_pos + to_write) % MYPIPE_BUFFER_SIZE;
|
|
pdata.data_len += to_write;
|
|
ret = to_write;
|
|
|
|
wake_up_interruptible(&read_queue);
|
|
|
|
out:
|
|
mutex_unlock(&mypipe_lock);
|
|
return ret;
|
|
}
|
|
|
|
// END_LST_READ_WRITE
|
|
|
|
// BEGIN_LST_FOP
|
|
const struct file_operations mypipe_fops = {.owner = THIS_MODULE,
|
|
.open = mypipe_open,
|
|
.read = mypipe_read,
|
|
.write = mypipe_write,
|
|
.release = mypipe_release};
|
|
// END_LST_FOP
|
|
|
|
// Used to set the permission to allow any user to r/w
|
|
static int mypipe_uevent(const struct device *dev,
|
|
struct kobj_uevent_env *env) {
|
|
add_uevent_var(env, "DEVMODE=%#o", 0666);
|
|
return 0;
|
|
}
|
|
|
|
// BEGIN_LST_INIT
|
|
static int __init mypipe_init(void) {
|
|
int ret;
|
|
dev_t dev;
|
|
|
|
// Acquire a region of major, minor for us to use. The name appears in
|
|
// /proc/devices (and potentially /sys ?).
|
|
ret = alloc_chrdev_region(&dev, 0, MYPIPE_MINOR_COUNT, MYPIPE_DEVICE_NAME);
|
|
if (ret) {
|
|
return ret;
|
|
}
|
|
|
|
dev_major = MAJOR(dev);
|
|
|
|
// Create the class in sysfs. The name appears in /sys/class/<name>
|
|
mypipedev_class = class_create(MYPIPE_DEVICE_NAME);
|
|
if (IS_ERR(mypipedev_class)) {
|
|
ret = PTR_ERR(mypipedev_class);
|
|
goto del_cdev;
|
|
}
|
|
mypipedev_class->dev_uevent = mypipe_uevent;
|
|
|
|
// Create a cdev in our pdata
|
|
cdev_init(&pdata.cdev, &mypipe_fops);
|
|
pdata.cdev.owner = THIS_MODULE;
|
|
|
|
// Register this newly created cdev to kernel. We only have one dev, so we
|
|
// can MKDEV(dev_major, 0).
|
|
|
|
// Also, we want to devices that one handles write and one read, so the last
|
|
// arg is not 1, but 2 (In this case, MYPIPE_MINOR_COUNT)
|
|
ret = cdev_add(&pdata.cdev, MKDEV(dev_major, 0), MYPIPE_MINOR_COUNT);
|
|
if (ret)
|
|
goto unregister;
|
|
|
|
// Create device node /dev/mypipe_in and /dev/mypipe_out
|
|
device_create(mypipedev_class, NULL, MKDEV(dev_major, MYPIPE_MINOR_IN),
|
|
NULL, "%s_in", MYPIPE_DEVICE_NAME);
|
|
device_create(mypipedev_class, NULL, MKDEV(dev_major, MYPIPE_MINOR_OUT),
|
|
NULL, "%s_out", MYPIPE_DEVICE_NAME);
|
|
|
|
pdata.is_active = false;
|
|
|
|
printk(KERN_INFO "%s: module loaded\n", MYPIPE_DEVICE_NAME);
|
|
return 0;
|
|
|
|
del_cdev:
|
|
cdev_del(&pdata.cdev);
|
|
unregister:
|
|
unregister_chrdev_region(MKDEV(dev_major, 0), MYPIPE_MINOR_COUNT);
|
|
return ret;
|
|
}
|
|
|
|
static void __exit mypipe_destroy(void) {
|
|
// This is basically the reverse operation of mypipe_init
|
|
device_destroy(mypipedev_class, MKDEV(dev_major, MYPIPE_MINOR_IN));
|
|
device_destroy(mypipedev_class, MKDEV(dev_major, MYPIPE_MINOR_OUT));
|
|
class_unregister(mypipedev_class);
|
|
class_destroy(mypipedev_class);
|
|
cdev_del(&pdata.cdev);
|
|
unregister_chrdev_region(MKDEV(dev_major, 0), MYPIPE_MINOR_COUNT);
|
|
printk(KERN_INFO "%s: module unloaded", MYPIPE_DEVICE_NAME);
|
|
}
|
|
|
|
module_init(mypipe_init);
|
|
module_exit(mypipe_destroy);
|
|
// END_LST_INIT
|
|
|
|
MODULE_LICENSE("GPL"); |