Skip to content

字符设备驱动最佳实践

1. 始终检查用户空间指针

永远不要直接解引用来自用户空间的指针,必须通过内核提供的安全函数:

c
/* ❌ 错误:直接访问用户指针 */
int val = *(int *)arg;

/* ✅ 正确:使用 get_user / put_user */
int val;
if (get_user(val, (int __user *)arg))
    return -EFAULT;

/* ✅ 正确:使用 copy_from_user / copy_to_user */
if (copy_from_user(kbuf, (void __user *)arg, size))
    return -EFAULT;

__user 标注是给 sparse 静态分析工具看的,帮助发现未检查的用户指针。

2. ioctl 命令编码规范

使用内核提供的宏编码 ioctl 命令,避免与系统命令冲突:

c
/* _IO(type, nr)           — 无数据传输 */
/* _IOR(type, nr, datatype) — 从驱动读数据到用户空间 */
/* _IOW(type, nr, datatype) — 从用户空间写数据到驱动 */
/* _IOWR(type, nr, datatype)— 双向 */

#define MY_IOC_MAGIC  'X'   /* 选一个唯一的魔数,参考 Documentation/userspace-api/ioctl/ioctl-number.rst */
#define MY_RESET      _IO(MY_IOC_MAGIC,   0)
#define MY_GET_STATUS _IOR(MY_IOC_MAGIC,  1, struct my_status)
#define MY_SET_CONFIG _IOW(MY_IOC_MAGIC,  2, struct my_config)
#define MY_TRANSFER   _IOWR(MY_IOC_MAGIC, 3, struct my_transfer)

/* ioctl 处理中验证命令合法性 */
static long mydev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    /* 检查魔数 */
    if (_IOC_TYPE(cmd) != MY_IOC_MAGIC)
        return -ENOTTY;

    /* 检查命令编号范围 */
    if (_IOC_NR(cmd) > MY_IOC_MAXNR)
        return -ENOTTY;

    /* 检查用户空间缓冲区可访问性 */
    if (_IOC_DIR(cmd) & _IOC_READ)
        if (!access_ok((void __user *)arg, _IOC_SIZE(cmd)))
            return -EFAULT;

    switch (cmd) { /* ... */ }
}

3. 正确处理 O_NONBLOCK

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

    /* 非阻塞模式:数据未就绪立即返回 */
    if (filp->f_flags & O_NONBLOCK) {
        if (!data_ready(priv))
            return -EAGAIN;
    } else {
        /* 阻塞模式:等待数据就绪 */
        if (wait_event_interruptible(priv->read_wq, data_ready(priv)))
            return -ERESTARTSYS;
    }

    /* 读取数据 */
    return do_read(priv, buf, count);
}

4. 引用计数与并发打开

c
static atomic_t open_count = ATOMIC_INIT(0);

static int mydev_open(struct inode *inode, struct file *filp)
{
    /* 限制只允许一个进程打开(独占设备) */
    if (!atomic_inc_and_test(&open_count)) {
        atomic_dec(&open_count);
        return -EBUSY;
    }
    return 0;
}

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

5. 正确的错误返回码

场景返回码
用户指针无效-EFAULT
参数非法-EINVAL
设备忙-EBUSY
无数据(非阻塞)-EAGAIN
被信号中断-ERESTARTSYS
未知 ioctl 命令-ENOTTY
权限不足-EPERM-EACCES
超时-ETIMEDOUT
I/O 错误-EIO

6. 避免在 file_operations 中睡眠时持有自旋锁

c
/* ❌ 错误:持有 spinlock 时调用 copy_to_user(可能睡眠) */
spin_lock(&priv->lock);
copy_to_user(buf, priv->data, len);   /* 可能触发缺页中断 */
spin_unlock(&priv->lock);

/* ✅ 正确:先拷贝到临时缓冲区,再释放锁 */
spin_lock(&priv->lock);
memcpy(tmp_buf, priv->data, len);
spin_unlock(&priv->lock);
copy_to_user(buf, tmp_buf, len);

7. 设备节点权限

通过 udev 规则设置权限,避免所有用户都能访问硬件:

bash
# /etc/udev/rules.d/99-mydev.rules
KERNEL=="mydev", MODE="0660", GROUP="dialout"

8. 多实例驱动设计

c
/* 使用 idr 管理多个设备实例 */
static DEFINE_IDA(mydev_ida);

static int mydev_probe(struct platform_device *pdev)
{
    int id = ida_alloc(&mydev_ida, GFP_KERNEL);
    if (id < 0)
        return id;

    /* 创建 /dev/mydev0, /dev/mydev1, ... */
    device_create(mydev_class, &pdev->dev,
                  MKDEV(mydev_major, id), priv, "mydev%d", id);
    priv->id = id;
    return 0;
}

static void mydev_remove(struct platform_device *pdev)
{
    struct mydev_priv *priv = platform_get_drvdata(pdev);
    device_destroy(mydev_class, MKDEV(mydev_major, priv->id));
    ida_free(&mydev_ida, priv->id);
}

褚成志的笔记