The path through the kernel
A userspace application calls open(), read(), write(), or ioctl(). The VFS resolves the file and dispatches operations through the file's operations table. For a character device, the driver's callbacks implement the behavior exposed by that device node.
What the callbacks mean
open()prepares per-file state and can reject unsupported access.read()transfers data from kernel space to userspace.write()accepts data from userspace for the device.unlocked_ioctl()handles device-specific commands.release()cleans up state when the file is closed.
Copy data safely
Kernel and userspace address spaces are distinct. Use copy_to_user() and copy_from_user() for transfers; do not dereference a userspace pointer directly. These helpers can fail partially, so callbacks must handle their return values correctly.
static ssize_t demo_read(struct file *file,
char __user *buf,
size_t count, loff_t *ppos)
{
char data[] = "hello from kernel\n";
size_t len = sizeof(data) - 1;
if (*ppos >= len)
return 0;
if (count > len - *ppos)
count = len - *ppos;
if (copy_to_user(buf, data + *ppos, count))
return -EFAULT;
*ppos += count;
return count;
}This snippet illustrates file-offset handling, but a production driver needs a deliberate data model, synchronization, and registration/cleanup paths.
Prefer the right abstraction
Many devices belong to a subsystem such as input, IIO, networking, TTY, or hwmon. Use a custom character device only when it fits the device's purpose; a bespoke ioctl ABI creates a long-term compatibility responsibility.
Final takeaway
Low-level engineering is about understanding the contract between hardware, firmware, and the operating system. Start with the specification, form a testable hypothesis, collect evidence, and validate behavior on the target platform.