Character Device Drivers
A character device driver is the most general and most common shape a Linux device driver takes: it exposes the device as a byte stream behind a node in
/dev, and userspace talks to it with the ordinary file syscalls —open,read,write,lseek,ioctl,close. The kernel does almost nothing of substance on a character device’s behalf; the driver supplies a table of callbacks (struct file_operations) and the kernel routes each syscall straight to the matching callback. Writing one is a four-step ritual: reserve a device number (adev_tmajor/minor pair), wire up astruct cdevthat binds that number to yourfile_operations, register it withcdev_add(), and create the/devnode so userspace can find it. This note is the archetype overview — what a character device is, the full lifecycle of writing one, the open→read/write→release flow from the userspace syscall down to your callback, the role ofprivate_data, and a complete worked minimal driver. The exactfile_operationstable is dissected in file_operations and the Character Driver Interface; thecdevregistration plumbing is detailed in struct cdev and Character Device Registration.
Mental Model: a byte pipe with a name in /dev
The three classic Unix driver archetypes are distinguished by what abstraction the kernel imposes between userspace and the hardware. A character device imposes essentially none: it is an unstructured stream of bytes, accessed sequentially (or with lseek if the driver chooses to support seeking), one byte or one buffer at a time, with no kernel-side buffering, scheduling, or caching forced upon it. A block device is the opposite — the kernel interposes a whole request layer (the block I/O stack, request queues, the page cache, the I/O scheduler) so that fixed-size blocks can be cached, merged, and reordered for the benefit of rotating or flash storage; its mechanics live in Block Device Drivers. A network device is not file-like at all: it has no /dev node, and packets enter and leave through the socket/protocol stack rather than read/write on a descriptor, as covered in Network Device Drivers and net_device.
Because the character abstraction is so thin, it is the catch-all: serial ports, /dev/null, /dev/zero, /dev/random, terminals (/dev/tty*), input event devices (/dev/input/event*), GPU render nodes, framebuffers, sound devices, and the vast majority of “control” interfaces for custom hardware are all character devices. If a device does not naturally fit the block-storage or network-packet mold, it is almost certainly a character device. The kernel docs confirm that the read/write members of the operations table are “called by read(2)/write(2) and related system calls” — the driver is the implementation of those syscalls for that file (VFS docs, v6.12).
flowchart TB subgraph US["Userspace"] APP["application:<br/>fd = open("/dev/foo")<br/>read(fd, buf, n)"] end subgraph VFS["VFS layer (generic)"] SC["read(2) syscall<br/>→ ksys_read → vfs_read"] INODE["char-special inode<br/>i_fop = def_chr_fops<br/>i_rdev = (major,minor)"] end subgraph CHR["Character device core (fs/char_dev.c)"] COPEN["chrdev_open():<br/>kobj_lookup(cdev_map, i_rdev)<br/>→ recover struct cdev<br/>→ replace_fops(filp, cdev->ops)"] end subgraph DRV["Your driver (a kernel module)"] FOPS["struct file_operations:<br/>.open .read .write<br/>.unlocked_ioctl .release"] end APP -->|"open(2)"| SC SC -->|"f_op->open = chrdev_open"| INODE INODE --> COPEN COPEN -->|"now filp->f_op == your table"| FOPS APP -->|"read(2)"| SC SC -->|"f_op->read"| FOPS
From /dev node to your callback. What it shows: the first open on a character-special file hits a generic stub (def_chr_fops.open == chrdev_open); that stub uses the inode’s device number i_rdev to look up the cdev you registered, then swaps the file’s operation table to your driver’s file_operations. Every subsequent read/write/ioctl on that descriptor goes directly to your callbacks with no further indirection. The insight: the device number is the only thing connecting the filesystem node to your code — everything else is a table-of-function-pointers dispatch the VFS performs on your behalf.
Mechanical Walk-through: the four-step lifecycle
Step 1 — reserve a device number
Every character device is identified by a dev_t: a 32-bit value packing a major number (which driver) and a minor number (which instance/sub-device the driver manages). In Linux 6.12 the split is fixed in include/linux/kdev_t.h: MINORBITS is 20, so the low 20 bits are the minor (MINOR(dev) = dev & MINORMASK, up to 1,048,575) and the upper 12 bits are the major (MAJOR(dev) = dev >> 20, up to 4095); MKDEV(ma,mi) composes the two. The mechanics and history of this split are detailed in Major and Minor Numbers.
You almost always ask the kernel to allocate a free major dynamically rather than hard-coding one, because static major assignments are a scarce, registry-managed resource. alloc_chrdev_region(&dev, baseminor, count, name) does this: it returns a fresh major (searching downward from 234, then through an extended range 511→384, per the constants CHRDEV_MAJOR_DYN_END/CHRDEV_MAJOR_DYN_EXT_START/CHRDEV_MAJOR_DYN_EXT_END in fs.h and find_dynamic_major() in char_dev.c), reserves count consecutive minors starting at baseminor, and writes the resulting dev_t back through the pointer. If you genuinely need a known major (rare), register_chrdev_region(from, count, name) reserves a specific range. Both are covered with their kernel-doc and failure semantics in struct cdev and Character Device Registration.
Step 2 — initialize and add a cdev
struct cdev is the kernel’s representation of a live character device. In v6.12 it is exactly (include/linux/cdev.h):
struct cdev {
struct kobject kobj; /* refcounted node in the device model */
struct module *owner; /* the module that owns the ops table */
const struct file_operations *ops; /* YOUR callbacks */
struct list_head list; /* links inodes currently using it */
dev_t dev; /* first device number this cdev covers */
unsigned int count; /* how many minors it covers */
} __randomize_layout;You associate your file_operations with a cdev and then publish it to the kernel’s device map:
cdev_init(&my_cdev, &my_fops); /* zero it, set ops, init the embedded kobject */
my_cdev.owner = THIS_MODULE; /* tie the ops' lifetime to your module */
cdev_add(&my_cdev, dev, count); /* go live: now openable */cdev_add() records dev/count and calls kobj_map(cdev_map, dev, count, …), inserting your cdev into the global radix-tree-like map keyed by device number. After this returns 0, the device is “live immediately” (the kernel’s own kernel-doc wording in char_dev.c). The internals of cdev_init/cdev_add/cdev_del and the alternative cdev_device_add() (which couples the cdev to a struct device) belong to struct cdev and Character Device Registration.
Step 3 — create the /dev node
Reserving a number and adding a cdev makes the device openable in principle, but userspace still needs a filesystem node whose i_rdev carries that device number. There are three eras of how that node appears, and on a modern system the first dominates:
devtmpfs+ astruct class. The modern idiom is to create a class and then calldevice_create(class, parent, dev, NULL, "foo%d", i). This registers astruct devicewith the device model, which emits a uevent; the kernel-maintaineddevtmpfs(mounted at/dev) auto-creates/dev/foo0immediately, and udev in userspace then fixes up ownership/permissions per its rules. See Device Classes, devtmpfs and the dev Directory, and udev and Device Management.mknodby hand.mknod /dev/foo c 511 0creates a character-special node with major 511, minor 0. This is what you do for quick experiments before wiring up a class; the node persists on disk and you must keep its number in sync with what the driver reserved.init_special_inode. Underneath both, when the VFS instantiates the inode for a character-special file,init_special_inode()(infs/inode.c) setsinode->i_mode, pointsinode->i_fop = &def_chr_fops, and stampsinode->i_rdev = rdev. Thatdef_chr_fopsis the generic stub whose.openischrdev_open— the hinge of the dispatch described next.
Step 4 — implement file_operations, and let the VFS dispatch
The driver’s substance is its file_operations table. When userspace runs open("/dev/foo", …), the syscall walks to the inode, and do_dentry_open() (in fs/open.c) does f->f_op = fops_get(inode->i_fop) — which for a char-special inode is def_chr_fops — then calls f->f_op->open(inode, f), i.e. chrdev_open().
chrdev_open() (in fs/char_dev.c) is the whole trick. Verbatim, its core is:
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); /* find the cdev by dev number */
new = container_of(kobj, struct cdev, kobj); /* recover the struct cdev */
inode->i_cdev = p = new; /* cache it on the inode */
...
fops = fops_get(p->ops); /* pin the driver module */
replace_fops(filp, fops); /* SWAP in your table */
if (filp->f_op->open)
ret = filp->f_op->open(inode, filp); /* call YOUR ->open */So the inode’s i_rdev looks up your cdev; container_of recovers the full structure from its embedded kobj; replace_fops overwrites filp->f_op with your ops; and from this open onward, every operation on the descriptor goes to your callbacks. fops_get performs try_module_get(ops->owner), which is why owner = THIS_MODULE matters: it pins your module in memory for as long as any file is open against it, so it cannot be unloaded out from under an active read. The full dispatch and the owner refcount tie are dissected in file_operations and the Character Driver Interface.
Subsequent read(2) follows ksys_read → vfs_read → file->f_op->read(file, buf, count, pos) (verified in fs/read_write.c, v6.12). vfs_read first checks FMODE_READ/FMODE_CAN_READ and runs access_ok(buf, count) on the userspace pointer, then dispatches: if (file->f_op->read) … else if (file->f_op->read_iter) new_sync_read(...). Your callback then moves bytes across the user/kernel boundary with copy_to_user/copy_from_user and updates *pos.
private_data: per-open state
A single cdev (one driver, one major) frequently backs many opens — many descriptors, possibly many minors. The driver needs somewhere to hang per-open state: which minor was opened, a position cursor, a per-session buffer, a pointer back to the driver’s own device structure. The struct file provides exactly one slot for this: void *private_data. The convention is that ->open allocates or looks up the per-device structure and stashes it: filp->private_data = my_dev;. Every later read/write/ioctl/release recovers it with struct my_dev *d = filp->private_data;. Because private_data lives on the struct file (the open file description), it is per-open, not per-inode and not per-process — two open()s of the same node get two struct files and two independent private_data pointers. The lifetime and sharing semantics of struct file itself are in The struct file and Open File Description.
A complete minimal character driver
The following is a self-contained module implementing /dev/scullsingle-style behavior: a fixed in-kernel buffer you can write to and read back. Every line is commented; APIs are verified against v6.12 headers.
#include <linux/module.h>
#include <linux/fs.h> /* file_operations, alloc_chrdev_region */
#include <linux/cdev.h> /* struct cdev, cdev_init, cdev_add */
#include <linux/uaccess.h> /* copy_to_user / copy_from_user */
#include <linux/slab.h> /* kzalloc / kfree */
#include <linux/device.h> /* class_create / device_create */
#define BUFSZ 4096
struct hello_dev { /* our per-device state */
char buf[BUFSZ];
size_t len; /* bytes currently stored */
};
static dev_t hello_devt; /* allocated (major,minor) */
static struct cdev hello_cdev; /* the live character device */
static struct class *hello_class; /* drives /dev node creation */
static struct hello_dev *the_dev; /* single shared instance */
/* open: stash the per-device struct on the open file description */
static int hello_open(struct inode *inode, struct file *filp)
{
filp->private_data = the_dev; /* recoverable in read/write */
return 0;
}
/* read: copy stored bytes out to userspace, honoring the cursor *ppos */
static ssize_t hello_read(struct file *filp, char __user *ubuf,
size_t count, loff_t *ppos)
{
struct hello_dev *d = filp->private_data;
if (*ppos >= d->len) /* past EOF → return 0 */
return 0;
if (count > d->len - *ppos) /* clamp to what we have */
count = d->len - *ppos;
if (copy_to_user(ubuf, d->buf + *ppos, count)) /* returns #unwritten */
return -EFAULT; /* faulting user pointer */
*ppos += count; /* advance the cursor */
return count; /* bytes actually delivered */
}
/* write: overwrite the buffer from userspace */
static ssize_t hello_write(struct file *filp, const char __user *ubuf,
size_t count, loff_t *ppos)
{
struct hello_dev *d = filp->private_data;
if (count > BUFSZ) /* don't overrun the buffer */
count = BUFSZ;
if (copy_from_user(d->buf, ubuf, count))
return -EFAULT;
d->len = count; /* remember new length */
*ppos = count;
return count;
}
static int hello_release(struct inode *inode, struct file *filp)
{
return 0; /* nothing per-open to free */
}
static const struct file_operations hello_fops = {
.owner = THIS_MODULE, /* pins module while open */
.open = hello_open,
.read = hello_read,
.write = hello_write,
.release = hello_release,
.llseek = default_llseek, /* enables lseek on the buffer */
};
static int __init hello_init(void)
{
int err;
the_dev = kzalloc(sizeof(*the_dev), GFP_KERNEL);
if (!the_dev)
return -ENOMEM;
/* Step 1: reserve one (major,minor) dynamically */
err = alloc_chrdev_region(&hello_devt, 0, 1, "hello");
if (err)
goto err_free;
/* Step 2: bind ops to a cdev and go live */
cdev_init(&hello_cdev, &hello_fops);
hello_cdev.owner = THIS_MODULE;
err = cdev_add(&hello_cdev, hello_devt, 1);
if (err)
goto err_region;
/* Step 3: create /dev/hello via a class + devtmpfs */
hello_class = class_create("hello");
if (IS_ERR(hello_class)) { err = PTR_ERR(hello_class); goto err_cdev; }
device_create(hello_class, NULL, hello_devt, NULL, "hello");
return 0;
err_cdev: cdev_del(&hello_cdev);
err_region: unregister_chrdev_region(hello_devt, 1);
err_free: kfree(the_dev);
return err;
}
static void __exit hello_exit(void)
{
device_destroy(hello_class, hello_devt);
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(hello_devt, 1);
kfree(the_dev);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");The teardown order is the mirror image of setup — destroy the /dev node and class, cdev_del, free the device number, free memory — and matters because reversing it can leave a node pointing at a freed cdev. The single-argument class_create(const char *name) used above is correct for v6.12 (verified against include/linux/device/class.h); the old owner/key parameters were dropped in the 6.4 era.
Failure Modes and Common Misunderstandings
Forgetting owner = THIS_MODULE. If ops->owner is NULL, fops_get()’s try_module_get(NULL) returns success without pinning anything, so the module can be unloaded while a file is open — and the next read jumps into freed code, an immediate oops. Always set it (via cdev_init it is not set automatically; register_chrdev’s legacy path copies fops->owner, but the modern cdev_init path requires you to set cdev.owner yourself).
Ignoring copy_*_user return values. Both functions return the number of bytes not copied; a nonzero return means the user pointer faulted partway and you must return -EFAULT. Treating the return as a success/fail boolean is correct only because nonzero is truthy — but dereferencing user memory directly (without copy_*_user) is a classic exploitable bug and will fault or leak kernel memory.
Not advancing *ppos. read/write receive loff_t *ppos, the open file’s position. If you do not advance it, cat /dev/foo loops forever re-reading the same bytes (it never sees EOF). Returning count < requested is not EOF; only returning 0 signals end-of-file to the reader.
Assuming one open == one read. A read(fd, buf, 8192) may legitimately be satisfied by your returning fewer bytes; userspace must loop. Conversely the kernel caps any single transfer at MAX_RW_COUNT (vfs_read clamps count), so your callback never sees an arbitrarily huge count.
Device-number collision. Hard-coding a major that another driver already registered makes register_chrdev_region fail with -EBUSY. Dynamic allocation (alloc_chrdev_region) sidesteps this — at the cost that the major is not known until runtime, which is exactly why udev/devtmpfs create the node from the kernel-reported number rather than a hard-coded mknod.
Alternatives and When to Choose Them
Use the misc framework for a single-minor device. If your driver needs exactly one device node and does not care about the major, misc_register(&my_miscdev) is dramatically less boilerplate: all misc devices share major 10, you get a dynamically allocated minor (MISC_DYNAMIC_MINOR == 255 asks for one), and misc_register internally does the class/device_create/cdev dance for you. You supply only a name and a file_operations pointer. The full struct miscdevice and trade-offs are in The miscdevice Framework.
Use a block driver if the device is random-access fixed-block storage that benefits from the page cache and I/O scheduler — see Block Device Drivers. Use a network driver for a packet interface — Network Device Drivers and net_device. Use the legacy register_chrdev(major, name, fops) helper only for the simplest cases: it grabs 256 minors at once and allocates+adds a hidden cdev for you, but gives you no per-minor control and is discouraged for new code in favor of the explicit alloc_chrdev_region + cdev_add pair.
Production Notes
Real character drivers rarely use a global single buffer; they embed a struct cdev inside their per-device structure (container_of(inode->i_cdev, struct my_dev, cdev) in ->open recovers the device from the inode) so that one driver can manage many minors, each with independent state. The canonical teaching example is scull from Linux Device Drivers, 3rd ed. (LDD3); its APIs are dated (it predates devtmpfs, class_create, and the current unlocked_ioctl) but its structure — container_of, per-device cdev, private_data — is exactly how in-tree drivers like drivers/char/mem.c (/dev/mem, /dev/null, /dev/zero) are still organized in 6.12.
Two correctness disciplines dominate production char drivers: (1) concurrency — multiple processes can hold the same node open simultaneously, so any shared state (the buffer, the length) needs a mutex or other lock; the worked example above is racy on purpose for brevity. (2) blocking semantics — a device with no data to read should block the caller (sleep on a wait queue) unless O_NONBLOCK is set, in which case it returns -EAGAIN; getting this right is the difference between a usable device and one that busy-spins userspace. Both topics are mechanism owned by file_operations and the Character Driver Interface (blocking vs non-blocking, poll) and kernel synchronization.
See Also
- file_operations and the Character Driver Interface — the callback table this archetype is built on, and the VFS dispatch in depth
- struct cdev and Character Device Registration — the
cdev_init/cdev_add/cdev_device_addregistration plumbing - Major and Minor Numbers — the
dev_tmajor/minor encoding and allocation - The miscdevice Framework — the low-boilerplate path for single-minor char devices
- Device Classes / devtmpfs and the dev Directory / udev and Device Management — how the
/devnode gets created - The struct file and Open File Description — where
private_dataandf_poslive - Block Device Drivers / Network Device Drivers and net_device — the sibling archetypes
- Linux Device Drivers and Device Model MOC — parent MOC (§5, The Driver Archetypes)