字符设备驱动最佳实践
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);
}