I am a coffee snob and my FSE friend always complained about the bad coffee at the customer sites. Anyhow, like i was saying yesterday kernelspace is not developer friendly as it lacks the basic decencies of a modern developer tooling. So you need some way of exposing this Kernel data to the userspace and thats where the Character Devices come-in very handy.
When I discovered Character Devices for the very first time, it was like kingdom come. Now I had a somewhat direct way of getting at the kernelspace data without too much drama. And here is what my humble tempmon looked like with the addition of the character devices. This still wont win me any Turing awards, but that's beyond the point.
In this quick demo, we will fake some temperature data from the GPU(reminder I am on Ubuntu 22.04 RTX2060 Super. YMMV) and we magically bridge it over to userspace.
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/random.h> #define DEVICE_NAME "tempmon"
So let us take a moment to familiarize ourselves with the headers.
- <linux/module.h> Required for any kernel module
- <linux/fs.h> provides us with the file ops to read/write/openfiles
- <linux/uaccess.h> Copies data between kernel and userspace. This is the "💎"
- <linux/cdev.h> Any character device that is created gets registered
- <linux/device.h> Creates the /dev/ entry automatically
static dev_t dev_num; static struct cdev temp_cdev; static struct class *temp_class;
Now the globals are ;
- dev_num Is the device ID
- temp_cdev is the character device that we create
- temp_class informs the kernel to create /dev/tempmon device
static ssize_t temp_read(struct file *filp, char __user *buf, size_t len, loff_t *off){ char temp_data[64]; int temp_celsius = 45 + (get_random_u32() % 30); int bytes; if (*off > 0) return 0; bytes = snprintf(temp_data, sizeof(temp_data), "Temperature: %d C\n", temp_celsius); if (copy_to_user(buf, temp_data, bytes)){ return -EFAULT; } *off += bytes; return bytes; }
Here is a line-line explanation of what this function does
temp_read() function:This runs when userspace does cat/dev/tempmon
If (*off > 0) return 0 - Already read once? Return EOF so cat stopssnprintf() - Format string in kernel buffercopy_to_user() - Can't directly write to userspace memory from kernel. This safely copies across the boundary*off so next read returns EOF
static struct file_operations fops = { .owner = THIS_MODULE, .read = temp_read, };
file_operations:
Tells kernel "when someone reads this device, call temp_read()". That's the bridge between userspace read() syscall and your kernel function.
static int __init temp_init(void){ alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); cdev_init(&temp_cdev, &fops); cdev_add(&temp_cdev, dev_num, 1); temp_class = class_create(DEVICE_NAME); device_create(temp_class, NULL, dev_num, NULL, DEVICE_NAME); printk(KERN_INFO "Temp Monitor: Device created at /dev/%s\n", DEVICE_NAME); return 0;
temp_init(): Runs when you insmod. Creates the device in 4 steps:
- alloc_chrdev_region() - Get a device number from kernel
- cdev_init() + cdev_add() - Register your read function
- class_create() - Tell kernel this is a device class
- device_create() - Actually make /dev/tempmon appear
static void __exit temp_exit(void){
device_destroy(temp_class, dev_num);
class_destroy(temp_class);
cdev_del(&temp_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "TempMonitor: Device removed\n");
}
temp_exit(): Runs when you rmmod. Undoes everything in reverse order (always cleanup in reverse).
There we go, fruits of our labor. Kernelspace event being logged over to the userspace.
In tomorrow's installment lets embellish this code even more to be able to continuously "log" temperature to a ringbuffer that we can cat.
The code is hosted here ;


