Skip to content

字符设备驱动:原理与架构

什么是字符设备

字符设备以字节流方式访问,没有缓冲区概念,读写操作直接与硬件交互。典型例子:串口、GPIO、传感器、自定义硬件接口。

用户空间
  open("/dev/mydev", O_RDWR)
  read() / write() / ioctl()

       ▼  VFS 层
  struct file_operations

       ▼  字符设备驱动
  硬件寄存器操作

完整字符设备驱动

c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/mutex.h>

#define DEVICE_NAME "mydev"
#define CLASS_NAME  "myclass"
#define BUF_SIZE    4096

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

static struct class  *mydev_class;
static dev_t          mydev_devt;   /* 主设备号 + 次设备号 */
static struct mydev_priv mydev_priv;

/* ===== file_operations 实现 ===== */

static int mydev_open(struct inode *inode, struct file *filp)
{
    struct mydev_priv *priv = container_of(inode->i_cdev,
                                            struct mydev_priv, cdev);
    filp->private_data = priv;
    return 0;
}

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

static ssize_t mydev_read(struct file *filp, char __user *ubuf,
                           size_t count, loff_t *ppos)
{
    struct mydev_priv *priv = filp->private_data;
    ssize_t ret;

    if (mutex_lock_interruptible(&priv->lock))
        return -ERESTARTSYS;

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

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

    *ppos += count;
    ret = count;

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

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

    if (count > BUF_SIZE)
        return -EINVAL;

    if (mutex_lock_interruptible(&priv->lock))
        return -ERESTARTSYS;

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

    priv->buf_len = count;
    *ppos += count;
    ret = count;

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

/* ioctl 命令定义(通常放在头文件中供用户空间使用) */
#define MYDEV_IOC_MAGIC  'M'
#define MYDEV_RESET      _IO(MYDEV_IOC_MAGIC,  0)
#define MYDEV_GET_LEN    _IOR(MYDEV_IOC_MAGIC, 1, int)
#define MYDEV_SET_MODE   _IOW(MYDEV_IOC_MAGIC, 2, int)

static long mydev_ioctl(struct file *filp, unsigned int cmd,
                         unsigned long arg)
{
    struct mydev_priv *priv = filp->private_data;
    int val;

    switch (cmd) {
    case MYDEV_RESET:
        mutex_lock(&priv->lock);
        memset(priv->buf, 0, BUF_SIZE);
        priv->buf_len = 0;
        mutex_unlock(&priv->lock);
        return 0;

    case MYDEV_GET_LEN:
        val = priv->buf_len;
        return put_user(val, (int __user *)arg);

    case MYDEV_SET_MODE:
        if (get_user(val, (int __user *)arg))
            return -EFAULT;
        /* 设置硬件模式 */
        return 0;

    default:
        return -ENOTTY;   /* 未知命令 */
    }
}

/* poll/select 支持 */
static __poll_t mydev_poll(struct file *filp, poll_table *wait)
{
    struct mydev_priv *priv = filp->private_data;
    __poll_t mask = 0;

    poll_wait(filp, &priv->read_wq, wait);   /* 注册等待队列 */

    if (priv->buf_len > 0)
        mask |= EPOLLIN | EPOLLRDNORM;   /* 可读 */

    mask |= EPOLLOUT | EPOLLWRNORM;      /* 始终可写 */
    return mask;
}

static const struct file_operations mydev_fops = {
    .owner          = THIS_MODULE,
    .open           = mydev_open,
    .release        = mydev_release,
    .read           = mydev_read,
    .write          = mydev_write,
    .unlocked_ioctl = mydev_ioctl,
    .poll           = mydev_poll,
    .llseek         = default_llseek,
};

/* ===== 模块初始化 ===== */

static int __init mydev_init(void)
{
    int ret;

    /* 1. 动态分配设备号 */
    ret = alloc_chrdev_region(&mydev_devt, 0, 1, DEVICE_NAME);
    if (ret) {
        pr_err("alloc_chrdev_region failed: %d\n", ret);
        return ret;
    }

    /* 2. 初始化 cdev */
    cdev_init(&mydev_priv.cdev, &mydev_fops);
    mydev_priv.cdev.owner = THIS_MODULE;

    ret = cdev_add(&mydev_priv.cdev, mydev_devt, 1);
    if (ret) {
        pr_err("cdev_add failed: %d\n", ret);
        goto err_cdev;
    }

    /* 3. 创建设备类(/sys/class/myclass/) */
    mydev_class = class_create(CLASS_NAME);
    if (IS_ERR(mydev_class)) {
        ret = PTR_ERR(mydev_class);
        goto err_class;
    }

    /* 4. 创建设备节点(/dev/mydev) */
    mydev_priv.device = device_create(mydev_class, NULL,
                                       mydev_devt, NULL, DEVICE_NAME);
    if (IS_ERR(mydev_priv.device)) {
        ret = PTR_ERR(mydev_priv.device);
        goto err_device;
    }

    /* 5. 分配缓冲区 */
    mydev_priv.buf = kzalloc(BUF_SIZE, GFP_KERNEL);
    if (!mydev_priv.buf) {
        ret = -ENOMEM;
        goto err_buf;
    }

    mutex_init(&mydev_priv.lock);

    pr_info("mydev: registered with major=%d minor=%d\n",
            MAJOR(mydev_devt), MINOR(mydev_devt));
    return 0;

err_buf:
    device_destroy(mydev_class, mydev_devt);
err_device:
    class_destroy(mydev_class);
err_class:
    cdev_del(&mydev_priv.cdev);
err_cdev:
    unregister_chrdev_region(mydev_devt, 1);
    return ret;
}

static void __exit mydev_exit(void)
{
    kfree(mydev_priv.buf);
    device_destroy(mydev_class, mydev_devt);
    class_destroy(mydev_class);
    cdev_del(&mydev_priv.cdev);
    unregister_chrdev_region(mydev_devt, 1);
    pr_info("mydev: unregistered\n");
}

module_init(mydev_init);
module_exit(mydev_exit);
MODULE_LICENSE("GPL");

设备号

dev_t = 主设备号(12位) + 次设备号(20位)

主设备号:标识驱动类型
次设备号:区分同类型的多个设备实例
bash
# 查看已分配的字符设备号
cat /proc/devices

# 手动创建设备节点(通常由 udev 自动完成)
mknod /dev/mydev c 240 0

mmap 零拷贝

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

    if (size > BUF_SIZE)
        return -EINVAL;

    /* 将内核缓冲区映射到用户空间 */
    return remap_pfn_range(vma,
                           vma->vm_start,
                           virt_to_phys(priv->buf) >> PAGE_SHIFT,
                           size,
                           vma->vm_page_prot);
}

用户空间使用:

c
int fd = open("/dev/mydev", O_RDWR);
void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* 直接读写 addr,无需 read/write 系统调用 */
munmap(addr, 4096);

褚成志的笔记