Basic: Major and Minor Numbers {:.gc-basic}
Basic
Every character device in Linux is identified by a device number — a 32-bit value encoding a major and minor number packed into the type dev_t.
#include <linux/types.h>
#include <linux/kdev_t.h>
dev_t devno;
/* Pack/unpack */
devno = MKDEV(240, 0); /* major=240, minor=0 */
int maj = MAJOR(devno); /* extract major */
int min = MINOR(devno); /* extract minor */
Major number identifies the driver. Minor number identifies the specific instance managed by that driver (e.g., /dev/ttyS0 vs /dev/ttyS1 share the same major).
# View registered character device majors
cat /proc/devices
# Example output:
# Character devices:
# 1 mem
# 4 tty
# 10 misc
# 89 i2c
# 240 mydrv
Registering device numbers
#include <linux/fs.h>
dev_t devno;
int major = 0; /* 0 = ask kernel to assign */
int minor_start = 0;
int count = 1; /* number of minors */
/* Dynamic allocation — preferred */
int ret = alloc_chrdev_region(&devno, minor_start, count, "mydrv");
if (ret < 0) {
pr_err("mydrv: failed to allocate device number\n");
return ret;
}
major = MAJOR(devno);
pr_info("mydrv: allocated major %d\n", major);
/* Static allocation — requires pre-assigned major from Documentation/admin-guide/devices.txt */
devno = MKDEV(240, 0);
ret = register_chrdev_region(devno, count, "mydrv");
/* Release on exit */
unregister_chrdev_region(devno, count);
file_operations Structure {:.gc-basic}
Basic
The file_operations structure is the dispatch table connecting VFS calls from userspace to your driver’s functions.
#include <linux/fs.h>
static int mydrv_open(struct inode *inode, struct file *filp);
static int mydrv_release(struct inode *inode, struct file *filp);
static ssize_t mydrv_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos);
static ssize_t mydrv_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos);
static loff_t mydrv_llseek(struct file *filp, loff_t offset, int whence);
static long mydrv_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg);
static __poll_t mydrv_poll(struct file *filp, struct poll_table_struct *pt);
static const struct file_operations mydrv_fops = {
.owner = THIS_MODULE,
.open = mydrv_open,
.release = mydrv_release,
.read = mydrv_read,
.write = mydrv_write,
.llseek = mydrv_llseek,
.unlocked_ioctl = mydrv_ioctl,
.poll = mydrv_poll,
};
Any operation left as NULL causes the VFS to return a sensible default or error (-EINVAL for ioctl, 0 for llseek, EPERM for read/write).
cdev Registration {:.gc-mid}
Intermediate
A complete character device driver ties together device number allocation, cdev registration, and a kernel buffer for data storage.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define DEVICE_NAME "mydrv"
#define BUF_SIZE 4096
struct mydrv_dev {
struct cdev cdev;
dev_t devno;
char *kbuf;
size_t data_len;
};
static struct mydrv_dev *mydev;
/* open: allocate per-file state if needed */
static int mydrv_open(struct inode *inode, struct file *filp)
{
struct mydrv_dev *dev = container_of(inode->i_cdev,
struct mydrv_dev, cdev);
filp->private_data = dev;
pr_info("mydrv: opened\n");
return 0;
}
static int mydrv_release(struct inode *inode, struct file *filp)
{
pr_info("mydrv: released\n");
return 0;
}
/* read: copy kernel buffer to userspace */
static ssize_t mydrv_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct mydrv_dev *dev = filp->private_data;
ssize_t retval = 0;
if (*ppos >= dev->data_len)
return 0; /* EOF */
count = min(count, (size_t)(dev->data_len - *ppos));
if (copy_to_user(buf, dev->kbuf + *ppos, count)) {
retval = -EFAULT;
goto out;
}
*ppos += count;
retval = count;
out:
return retval;
}
/* write: copy userspace data into kernel buffer */
static ssize_t mydrv_write(struct file *filp, const char __user *buf,
size_t count, loff_t *ppos)
{
struct mydrv_dev *dev = filp->private_data;
if (count > BUF_SIZE)
count = BUF_SIZE;
if (copy_from_user(dev->kbuf, buf, count))
return -EFAULT;
dev->data_len = count;
*ppos = count;
return count;
}
static const struct file_operations mydrv_fops = {
.owner = THIS_MODULE,
.open = mydrv_open,
.release = mydrv_release,
.read = mydrv_read,
.write = mydrv_write,
};
static int __init mydrv_init(void)
{
int ret;
mydev = kzalloc(sizeof(*mydev), GFP_KERNEL);
if (!mydev)
return -ENOMEM;
mydev->kbuf = kzalloc(BUF_SIZE, GFP_KERNEL);
if (!mydev->kbuf) {
ret = -ENOMEM;
goto err_alloc_buf;
}
ret = alloc_chrdev_region(&mydev->devno, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("mydrv: alloc_chrdev_region failed: %d\n", ret);
goto err_alloc_region;
}
cdev_init(&mydev->cdev, &mydrv_fops);
mydev->cdev.owner = THIS_MODULE;
ret = cdev_add(&mydev->cdev, mydev->devno, 1);
if (ret < 0) {
pr_err("mydrv: cdev_add failed: %d\n", ret);
goto err_cdev_add;
}
pr_info("mydrv: registered at %d:%d\n",
MAJOR(mydev->devno), MINOR(mydev->devno));
return 0;
err_cdev_add:
unregister_chrdev_region(mydev->devno, 1);
err_alloc_region:
kfree(mydev->kbuf);
err_alloc_buf:
kfree(mydev);
return ret;
}
static void __exit mydrv_exit(void)
{
cdev_del(&mydev->cdev);
unregister_chrdev_region(mydev->devno, 1);
kfree(mydev->kbuf);
kfree(mydev);
pr_info("mydrv: unregistered\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
copy_to_user / copy_from_user
Direct pointer dereference of userspace addresses from kernel context is illegal — the address may be unmapped, paged out, or belong to another process. These functions safely transfer data while handling page faults and returning the number of bytes that could not be copied (0 on full success).
ioctl Interface {:.gc-mid}
Intermediate
ioctl provides out-of-band control commands beyond read/write. Command codes are constructed with standard macros to encode direction, type, number, and argument size.
/* mydrv_ioctl.h — shared between kernel driver and userspace application */
#ifndef MYDRV_IOCTL_H
#define MYDRV_IOCTL_H
#include <linux/ioctl.h>
#define MYDRV_IOC_MAGIC 'M' /* unique magic byte — see Documentation/userspace-api/ioctl/ioctl-number.rst */
struct mydrv_config {
unsigned int speed;
unsigned int mode;
};
#define MYDRV_IOC_RESET _IO (MYDRV_IOC_MAGIC, 0)
#define MYDRV_IOC_GET_STATUS _IOR (MYDRV_IOC_MAGIC, 1, int)
#define MYDRV_IOC_SET_CONFIG _IOW (MYDRV_IOC_MAGIC, 2, struct mydrv_config)
#define MYDRV_IOC_RW_DATA _IOWR(MYDRV_IOC_MAGIC, 3, struct mydrv_config)
#define MYDRV_IOC_MAXNR 3
#endif
/* Kernel-side ioctl handler */
#include "mydrv_ioctl.h"
static long mydrv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct mydrv_config cfg;
int status = 42;
int ret = 0;
/* Validate magic and command number */
if (_IOC_TYPE(cmd) != MYDRV_IOC_MAGIC)
return -ENOTTY;
if (_IOC_NR(cmd) > MYDRV_IOC_MAXNR)
return -ENOTTY;
switch (cmd) {
case MYDRV_IOC_RESET:
pr_info("mydrv: reset requested\n");
/* perform hardware reset */
break;
case MYDRV_IOC_GET_STATUS:
if (copy_to_user((int __user *)arg, &status, sizeof(status)))
return -EFAULT;
break;
case MYDRV_IOC_SET_CONFIG:
if (copy_from_user(&cfg, (struct mydrv_config __user *)arg,
sizeof(cfg)))
return -EFAULT;
pr_info("mydrv: set speed=%u mode=%u\n", cfg.speed, cfg.mode);
break;
default:
return -ENOTTY;
}
return ret;
}
/* Userspace usage */
#include <sys/ioctl.h>
#include <fcntl.h>
#include "mydrv_ioctl.h"
int fd = open("/dev/mydrv0", O_RDWR);
ioctl(fd, MYDRV_IOC_RESET);
int status;
ioctl(fd, MYDRV_IOC_GET_STATUS, &status);
struct mydrv_config cfg = { .speed = 1000000, .mode = 1 };
ioctl(fd, MYDRV_IOC_SET_CONFIG, &cfg);
close(fd);
Compat ioctl handles 32-bit userspace processes running on a 64-bit kernel. If pointer sizes differ in your struct, you must provide a .compat_ioctl handler that translates the 32-bit layout.
Automatic Device Nodes with udev {:.gc-adv}
Advanced
Manually creating device nodes with mknod is error-prone. The kernel’s device model plus udev automates this.
#include <linux/device.h>
#include <linux/class.h>
static struct class *mydrv_class;
static struct device *mydrv_device;
static int __init mydrv_init(void)
{
int ret;
/* ... alloc_chrdev_region, cdev_add ... */
/* Create /sys/class/mydrv/ */
mydrv_class = class_create(THIS_MODULE, "mydrv");
if (IS_ERR(mydrv_class)) {
ret = PTR_ERR(mydrv_class);
goto err_class;
}
/* Creates /sys/class/mydrv/mydrv0 and signals udev to create /dev/mydrv0 */
mydrv_device = device_create(mydrv_class, NULL,
mydev->devno, NULL, "mydrv%d", 0);
if (IS_ERR(mydrv_device)) {
ret = PTR_ERR(mydrv_device);
goto err_device;
}
return 0;
err_device:
class_destroy(mydrv_class);
err_class:
/* cleanup cdev and region */
return ret;
}
static void __exit mydrv_exit(void)
{
device_destroy(mydrv_class, mydev->devno);
class_destroy(mydrv_class);
/* ... cdev_del, unregister_chrdev_region ... */
}
# Custom udev rule: /etc/udev/rules.d/99-mydrv.rules
KERNEL=="mydrv[0-9]", SUBSYSTEM=="mydrv", MODE="0666", GROUP="dialout"
# Reload udev rules
udevadm control --reload-rules
udevadm trigger
devtmpfs (mounted at /dev) is populated by the kernel itself during boot. udev then applies rules to rename, set permissions, and create symlinks.
Advanced: Poll/Select Support {:.gc-adv}
Advanced
Drivers support poll()/select()/epoll() by implementing the .poll file operation. The kernel calls this to register the process on one or more wait queues and to check current readiness.
#include <linux/wait.h>
#include <linux/poll.h>
struct mydrv_dev {
/* ... */
wait_queue_head_t read_wq;
wait_queue_head_t write_wq;
bool data_ready;
};
/* In init, after kzalloc: */
init_waitqueue_head(&mydev->read_wq);
init_waitqueue_head(&mydev->write_wq);
/* poll implementation */
static __poll_t mydrv_poll(struct file *filp, struct poll_table_struct *pt)
{
struct mydrv_dev *dev = filp->private_data;
__poll_t mask = 0;
poll_wait(filp, &dev->read_wq, pt); /* register on read wait queue */
poll_wait(filp, &dev->write_wq, pt); /* register on write wait queue */
if (dev->data_ready)
mask |= EPOLLIN | EPOLLRDNORM; /* readable */
mask |= EPOLLOUT | EPOLLWRNORM; /* always writable for this example */
return mask;
}
/* Blocking read with wait queue */
static ssize_t mydrv_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos)
{
struct mydrv_dev *dev = filp->private_data;
if (!dev->data_ready) {
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
/* Sleep until data arrives (woken by ISR or write path) */
if (wait_event_interruptible(dev->read_wq, dev->data_ready))
return -ERESTARTSYS;
}
/* ... copy data, clear data_ready ... */
return count;
}
/* In the ISR or write path, wake sleeping readers: */
// dev->data_ready = true;
// wake_up_interruptible(&dev->read_wq);
Interview Q&A {:.gc-iq}
Interview Q&A
Q1: What is the difference between a cdev and a misc device?
A cdev requires you to manually allocate a major number, initialise the cdev struct, and call cdev_add. A misc device (registered with misc_register) automatically uses major number 10 and the kernel assigns a minor number from the misc range. Misc devices are simpler for drivers that only need one device node and don’t need multiple minors.
Q2: Why is copy_to_user necessary instead of a direct pointer copy?
Userspace pointers are virtual addresses in the user process’s address space, which may not be mapped in the current context, may trigger a page fault, or could point to kernel memory (a security exploit). copy_to_user validates the address, handles page faults safely, and on architectures with separate user/kernel address spaces (e.g. SMAP on x86) performs the necessary privilege switch.
Q3: How are major and minor numbers related to a device file?
When the VFS opens a file under /dev, it reads the device number stored in the inode (inode->i_rdev). It looks up the major number in cdev_map to find the registered cdev, then calls the appropriate file_operations function. The minor number is passed to the driver so it can manage multiple hardware instances from a single major.
Q4: What is the convention for ioctl magic numbers?
Each driver should use a unique single-byte “magic number” as the type field of the ioctl command. Assigned magic numbers are listed in Documentation/userspace-api/ioctl/ioctl-number.rst. Using a unique magic prevents a userspace program accidentally issuing a valid-looking command to the wrong driver.
Q5: What happens if the .release file operation is NULL?
The VFS checks for NULL before calling any file operation. If .release is NULL, the kernel simply skips the call when the file descriptor is closed. For many simple drivers this is fine — no cleanup is needed per file. However if your driver tracks open count or holds per-file resources allocated in .open, omitting .release causes a resource leak.
Q6: What race condition exists in open and release?
If multiple processes open the device simultaneously, open may be called concurrently before any release. If the driver enforces single-open semantics using a global flag (atomic_cmpxchg on an open_count), you must ensure the flag is cleared in release atomically. Without proper synchronisation, a second open could succeed while the first is still active, or two concurrent opens could both see the device as free.
Q7: What does container_of do in the open handler?
container_of(ptr, type, member) performs pointer arithmetic to retrieve a pointer to the enclosing structure given a pointer to one of its members. In mydrv_open, inode->i_cdev points to the struct cdev embedded inside struct mydrv_dev. container_of recovers the outer mydrv_dev * so the driver can access its private state.
References {:.gc-ref}
References
| Resource | Link |
|---|---|
| Linux Device Drivers, 3rd ed. (LDD3) — Chapter 3 | https://lwn.net/Kernel/LDD3/ |
| Linux Kernel Module Programming Guide — Character Devices | https://sysprog21.github.io/lkmpg/#character-device-drivers |
| kernel.org: char device registration | https://www.kernel.org/doc/html/latest/driver-api/index.html |
| ioctl number assignments | https://www.kernel.org/doc/html/latest/userspace-api/ioctl/ioctl-number.html |
| udev rules documentation | https://www.freedesktop.org/software/systemd/man/udev_rules.html |
| poll/select kernel internals | https://www.kernel.org/doc/html/latest/filesystems/vfs.html |