Skip to content

Character and Misc Devices

Exposing driver functionality to userspace via /dev

Character devices

A character device (cdev) is a special file in /dev that provides a byte-stream interface to a driver. Unlike block devices, character devices do not buffer data in the page cache — I/O goes directly to the driver. The two-step alloc_chrdev_region / cdev_add interface was introduced by Al Viro just before Linux 2.6.0 to support an expanded dev_t type; the old register_chrdev() remains as a compatibility shim (LWN).

/dev/mydevice (major=240, minor=0)
 struct cdev ────────► struct file_operations
        │                   .open
        │                   .read
        │                   .write
        │                   .unlocked_ioctl
        │                   .mmap
        │                   .release
   driver code

Allocating major/minor numbers

/* Static allocation (reserved in Documentation/admin-guide/devices.txt) */
int ret = register_chrdev_region(MKDEV(240, 0), 1, "mydev");

/* Dynamic allocation (preferred): kernel assigns major */
dev_t devt;
int ret = alloc_chrdev_region(&devt, 0, /* first minor */
                               1,        /* count */
                               "mydev");
int major = MAJOR(devt);
int minor = MINOR(devt);

/* Release on exit */
unregister_chrdev_region(devt, 1);

struct cdev

#include <linux/cdev.h>

struct cdev {
    struct kobject          kobj;
    struct module          *owner;
    const struct file_operations *ops;
    struct list_head        list;
    dev_t                   dev;         /* major:minor */
    unsigned int            count;       /* number of minors */
};

struct file_operations

/* include/linux/fs.h */
struct file_operations {
    struct module *owner;
    loff_t (*llseek)(struct file *, loff_t, int);
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    ssize_t (*read_iter)(struct kiocb *, struct iov_iter *);  /* preferred */
    ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
    int (*iopoll)(struct kiocb *, struct io_comp_batch *, unsigned int flags);
    int (*iterate_shared)(struct file *, struct dir_context *);
    __poll_t (*poll)(struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
    long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
    int (*mmap)(struct file *, struct vm_area_struct *);
    int (*open)(struct inode *, struct file *);
    int (*flush)(struct file *, fl_owner_t id);
    int (*release)(struct inode *, struct file *);
    int (*fsync)(struct file *, loff_t, loff_t, int datasync);
    int (*fasync)(int, struct file *, int);
    /* ... */
};

A complete minimal character driver

/* drivers/mychardev/mychardev.c */
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define DEVICE_NAME "mychardev"
#define BUF_SIZE    4096

struct mychardev {
    struct cdev cdev;
    char    *buf;
    size_t   buf_len;
    struct mutex lock;
};

static struct mychardev mydev;
static dev_t devt;

static int mydev_open(struct inode *inode, struct file *filp)
{
    /* Store per-file state in filp->private_data if needed */
    struct mychardev *dev = container_of(inode->i_cdev,
                                          struct mychardev, cdev);
    filp->private_data = dev;
    return 0;
}

static ssize_t mydev_read(struct file *filp, char __user *buf,
                           size_t count, loff_t *ppos)
{
    struct mychardev *dev = filp->private_data;
    ssize_t ret;

    mutex_lock(&dev->lock);

    if (*ppos >= dev->buf_len) {
        ret = 0;  /* EOF */
        goto out;
    }

    count = min(count, dev->buf_len - (size_t)*ppos);
    if (copy_to_user(buf, dev->buf + *ppos, count)) {
        ret = -EFAULT;
        goto out;
    }

    *ppos += count;
    ret = count;

out:
    mutex_unlock(&dev->lock);
    return ret;
}

static ssize_t mydev_write(struct file *filp, const char __user *buf,
                            size_t count, loff_t *ppos)
{
    struct mychardev *dev = filp->private_data;
    ssize_t ret;

    if (count > BUF_SIZE)
        return -EINVAL;

    mutex_lock(&dev->lock);

    if (copy_from_user(dev->buf, buf, count)) {
        ret = -EFAULT;
        goto out;
    }

    dev->buf_len = count;
    *ppos = 0;
    ret = count;

out:
    mutex_unlock(&dev->lock);
    return ret;
}

static int mydev_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static const struct file_operations mydev_fops = {
    .owner   = THIS_MODULE,
    .open    = mydev_open,
    .read    = mydev_read,
    .write   = mydev_write,
    .release = mydev_release,
    .llseek  = default_llseek,  /* uses f_pos */
};

static int __init mydev_init(void)
{
    int ret;

    /* Allocate device number */
    ret = alloc_chrdev_region(&devt, 0, 1, DEVICE_NAME);
    if (ret)
        return ret;

    /* Allocate and initialize cdev */
    cdev_init(&mydev.cdev, &mydev_fops);
    mydev.cdev.owner = THIS_MODULE;

    /* Allocate buffer */
    mydev.buf = kzalloc(BUF_SIZE, GFP_KERNEL);
    if (!mydev.buf) {
        unregister_chrdev_region(devt, 1);
        return -ENOMEM;
    }

    mutex_init(&mydev.lock);

    /* Register cdev — after this, open() can be called */
    ret = cdev_add(&mydev.cdev, devt, 1);
    if (ret) {
        kfree(mydev.buf);
        unregister_chrdev_region(devt, 1);
        return ret;
    }

    pr_info("mychardev: major=%d minor=%d\n", MAJOR(devt), MINOR(devt));
    return 0;
}

static void __exit mydev_exit(void)
{
    cdev_del(&mydev.cdev);
    kfree(mydev.buf);
    unregister_chrdev_region(devt, 1);
}

module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");
# After loading the module, read the major number from /proc/devices:
grep mychardev /proc/devices   # → "240 mychardev" (dynamic major)
mknod /dev/mychardev c 240 0
# Or use class_create/device_create in the driver for udev auto-creation
echo "hello" > /dev/mychardev
cat /dev/mychardev  # → hello

misc_register: the easy path

For simple devices that don't need multiple minors, misc_register handles major/minor allocation and class creation automatically:

#include <linux/miscdevice.h>

static struct miscdevice mymisc = {
    .minor = MISC_DYNAMIC_MINOR,   /* kernel assigns minor */
    .name  = "mymisc",             /* /dev/mymisc */
    .fops  = &mydev_fops,
};

static int __init mymisc_init(void)
{
    return misc_register(&mymisc);
    /* /dev/mymisc created automatically via udev */
}

static void __exit mymisc_exit(void)
{
    misc_deregister(&mymisc);
}

All misc devices share major number 10 — a value reserved in the kernel's official device list since at least Linux 1.3. They appear under /sys/class/misc/.

ioctl: control operations

ioctl lets userspace send control commands to the driver:

/* In driver: */
static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct mydev_query query;

    switch (cmd) {
    case MYDEV_IOCTL_RESET:
        reset_hardware();
        return 0;

    case MYDEV_IOCTL_QUERY:
        /* Copy struct to/from userspace */
        if (copy_from_user(&query, (void __user *)arg, sizeof(query)))
            return -EFAULT;
        query.result = do_query(query.param);
        if (copy_to_user((void __user *)arg, &query, sizeof(query)))
            return -EFAULT;
        return 0;

    default:
        return -ENOTTY;  /* unknown ioctl */
    }
}

ioctl numbers use a structured format to avoid conflicts:

/* include/uapi/linux/mydev.h */
#include <linux/ioctl.h>

/* _IO(type, nr)          — no argument */
/* _IOR(type, nr, dtype)  — read from kernel */
/* _IOW(type, nr, dtype)  — write to kernel */
/* _IOWR(type, nr, dtype) — read+write */

#define MYDEV_MAGIC 'M'
#define MYDEV_IOCTL_RESET  _IO(MYDEV_MAGIC, 0)
#define MYDEV_IOCTL_QUERY  _IOWR(MYDEV_MAGIC, 1, struct mydev_query)
/* Userspace: */
int fd = open("/dev/mydev", O_RDWR);
ioctl(fd, MYDEV_IOCTL_RESET);
ioctl(fd, MYDEV_IOCTL_QUERY, &query);

mmap: mapping device memory to userspace

Drivers can let userspace map device memory or kernel buffers directly:

static int mydev_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct mychardev *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;

    if (size > BUF_SIZE)
        return -EINVAL;

    /* Map kernel buffer to userspace */
    return remap_pfn_range(vma,
        vma->vm_start,
        virt_to_phys(dev->buf) >> PAGE_SHIFT,
        size,
        vma->vm_page_prot);
}

/* For device I/O memory (e.g., PCI BAR): */
static int mydev_mmap_io(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long phys = dev->bar_phys + (vma->vm_pgoff << PAGE_SHIFT);
    unsigned long size = vma->vm_end - vma->vm_start;

    /* Mark as non-cacheable for device registers */
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    return remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT,
                           size, vma->vm_page_prot);
}
/* Userspace: */
void *mapped = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
/* Access device memory directly: */
volatile uint32_t *reg = (volatile uint32_t *)mapped;
uint32_t val = *reg;

poll/select support

static __poll_t mydev_poll(struct file *filp, poll_table *wait)
{
    struct mychardev *dev = filp->private_data;
    __poll_t mask = 0;

    poll_wait(filp, &dev->read_queue, wait);

    if (dev->data_available)
        mask |= EPOLLIN | EPOLLRDNORM;
    if (dev->write_space)
        mask |= EPOLLOUT | EPOLLWRNORM;

    return mask;
}

struct device integration

To get a proper /sys/class/ entry and auto-create /dev/:

static struct class *mydev_class;

static int __init mydev_init(void)
{
    /* ... alloc_chrdev_region, cdev_init, cdev_add ... */

    /* Create /sys/class/mydev/ */
    mydev_class = class_create(THIS_MODULE, "mydev");
    if (IS_ERR(mydev_class))
        goto err_class;

    /* Create /sys/class/mydev/mydev0/ and trigger udev → /dev/mydev0 */
    device_create(mydev_class, NULL, devt, NULL, "mydev%d", 0);

    return 0;

err_class:
    /* cleanup */
}

Further reading