51. Character device drivers: file_operations, cdev, and copy_to/from_user
A character device driver is what turns module_init from Section 50
into something a user-space program can open()/read()/write() as
/dev/something — the kernel-side implementation of every /dev node
Section 48's examples opened from the outside.
// chardev.c — a device that remembers the last message written to it
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h> // copy_to_user / copy_from_user
#include <linux/device.h>
#include <linux/version.h> // LINUX_VERSION_CODE / KERNEL_VERSION, for the class_create() shim below
#define DEVICE_NAME "mychardev"
#define BUF_SIZE 128
static char message[BUF_SIZE];
static size_t message_len;
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static int dev_open(struct inode *inode, struct file *file) {
(void)inode; (void)file;
return 0; // nothing to set up for this simple device
}
static ssize_t dev_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
(void)file;
if (*offset >= message_len) return 0; // EOF
size_t remaining = message_len - (size_t)*offset;
size_t to_copy = len < remaining ? len : remaining;
// copy_to_user validates the user pointer AND does the copy — a raw memcpy would
// let a malicious/buggy caller pass a kernel address and read arbitrary kernel memory
if (copy_to_user(buf, message + *offset, to_copy)) return -EFAULT;
*offset += (loff_t)to_copy;
return (ssize_t)to_copy;
}
static ssize_t dev_write(struct file *file, const char __user *buf, size_t len, loff_t *offset) {
(void)file; (void)offset;
size_t to_copy = len < BUF_SIZE - 1 ? len : BUF_SIZE - 1; // never overflow `message`
if (copy_from_user(message, buf, to_copy)) return -EFAULT;
message[to_copy] = '\0';
message_len = to_copy;
return (ssize_t)to_copy;
}
static const struct file_operations fops = {
.owner = THIS_MODULE,
.open = dev_open,
.read = dev_read,
.write = dev_write,
};
static int __init chardev_init(void) {
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) return -1;
cdev_init(&my_cdev, &fops);
if (cdev_add(&my_cdev, dev_num, 1) < 0) {
unregister_chrdev_region(dev_num, 1);
return -1;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 4, 0)
my_class = class_create(THIS_MODULE, DEVICE_NAME); // two-arg form: kernels before 6.4
#else
my_class = class_create(DEVICE_NAME); // 6.4+ dropped the owner argument
#endif
device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME); // makes /dev/mychardev appear
printk(KERN_INFO "chardev: /dev/%s ready\n", DEVICE_NAME);
return 0;
}
static void __exit chardev_exit(void) {
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
make && sudo insmod chardev.ko
echo "hello kernel" > /dev/mychardev
cat /dev/mychardev # hello kernel
sudo rmmod chardev
Why copy_to_user/copy_from_user instead of memcpy (Section 10's
usual tool): kernel and user space are separate address spaces with
separate page tables. A user-space pointer is only meaningful in the
context of that process's mappings — memcpying through it directly
from kernel code would either fault or, worse, silently read/write
whatever the raw address happens to resolve to in kernel space.
copy_to_user/copy_from_user validate that the user address is
actually mapped and accessible to that process before touching it,
returning an error instead of corrupting memory — this single pair of
functions is the kernel-space equivalent of Section 29's entire "trust
nothing that crosses a boundary" chapter, with the boundary being the
user/kernel split instead of the network.