Skip to content

字符设备驱动故障处理案例

案例 1:设备节点未自动创建

现象insmod 成功,但 /dev/ 下没有设备节点。

排查步骤

bash
# 1. 确认设备号已分配
cat /proc/devices | grep mydev

# 2. 检查 sysfs 是否有设备
ls /sys/class/myclass/

# 3. 查看 udev 日志
udevadm monitor --udev
# 重新加载模块,观察是否有事件

# 4. 手动触发 udev
udevadm trigger

常见原因

  • 未调用 device_create(),只调用了 cdev_add()
  • class_create() 失败但未检查返回值
  • udev 规则冲突

解决方法

c
/* 确保完整的设备创建流程 */
mydev_class = class_create(CLASS_NAME);
if (IS_ERR(mydev_class)) {
    pr_err("class_create failed\n");
    return PTR_ERR(mydev_class);
}

device = device_create(mydev_class, NULL, devt, NULL, "mydev");
if (IS_ERR(device)) {
    pr_err("device_create failed\n");
    class_destroy(mydev_class);
    return PTR_ERR(device);
}

案例 2:copy_to_user 导致 Oops

现象read() 时内核崩溃,dmesg 显示 Unable to handle kernel paging request

错误代码

c
/* ❌ 直接写用户空间指针 */
static ssize_t mydev_read(struct file *filp, char __user *buf, ...)
{
    memcpy(buf, priv->data, len);   /* 崩溃! */
    return len;
}

正确代码

c
/* ✅ 使用 copy_to_user */
if (copy_to_user(buf, priv->data, len))
    return -EFAULT;
return len;

调试技巧

bash
# 查看崩溃地址
dmesg | grep "PC is at"
# 反汇编定位代码行
addr2line -e mydriver.ko <>

案例 3:设备忙无法卸载

现象rmmod 报错 Device or resource busy

排查

bash
# 查看模块引用计数
lsmod | grep mydev

# 查找打开设备的进程
lsof | grep /dev/mydev
fuser -v /dev/mydev

# 强制杀死进程
fuser -k /dev/mydev

常见原因

  • 用户空间进程未关闭文件描述符
  • 驱动中 module_get() 未配对 module_put()
  • 循环引用(如 sysfs 文件未删除)

案例 4:ioctl 返回 ENOTTY

现象:用户空间调用 ioctl() 返回 -1errno = ENOTTY

排查

c
/* 在驱动中添加调试 */
static long mydev_ioctl(struct file *filp, unsigned int cmd, ...)
{
    pr_info("ioctl: cmd=0x%08x, type=%c, nr=%d\n",
            cmd, _IOC_TYPE(cmd), _IOC_NR(cmd));
    /* ... */
}

常见原因

  • 用户空间与驱动的 ioctl 命令定义不一致(魔数或编号错误)
  • 32 位用户空间 + 64 位内核,结构体对齐问题
  • 未实现 .compat_ioctl(32 位程序在 64 位内核上运行)

解决方法

c
/* 确保用户空间与内核使用相同的头文件 */
#define MY_IOC_MAGIC  'X'
#define MY_CMD        _IOW(MY_IOC_MAGIC, 1, int)

/* 64 位内核支持 32 位程序 */
static const struct file_operations mydev_fops = {
    .unlocked_ioctl = mydev_ioctl,
    .compat_ioctl   = mydev_ioctl,   /* 简单情况可直接复用 */
};

案例 5:poll 不工作

现象:用户空间 select()poll() 永远阻塞。

错误代码

c
/* ❌ 忘记调用 poll_wait */
static __poll_t mydev_poll(struct file *filp, poll_table *wait)
{
    if (data_ready)
        return EPOLLIN;
    return 0;
}

正确代码

c
/* ✅ 必须注册等待队列 */
static __poll_t mydev_poll(struct file *filp, poll_table *wait)
{
    struct mydev_priv *priv = filp->private_data;

    poll_wait(filp, &priv->read_wq, wait);   /* 关键! */

    if (data_ready(priv))
        return EPOLLIN | EPOLLRDNORM;
    return 0;
}

/* 数据到达时唤醒 */
wake_up_interruptible(&priv->read_wq);

案例 6:内存泄漏

现象:长时间运行后 free 内存持续下降。

排查工具

bash
# kmemleak(需 CONFIG_DEBUG_KMEMLEAK=y)
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak

# slabtop 查看 slab 使用
slabtop -o

# 查看模块内存占用
cat /proc/slabinfo | grep mydev

常见原因

  • open() 中分配内存,release() 中忘记释放
  • 中断处理函数中分配内存但未释放
  • 循环引用导致对象无法释放

解决方法

c
/* 使用 devm_* 系列函数自动管理 */
priv->buf = devm_kzalloc(&pdev->dev, size, GFP_KERNEL);
/* 设备解绑时自动释放,无需手动 kfree */

案例 7:竞态条件导致数据损坏

现象:多进程同时读写设备时数据错乱。

错误代码

c
/* ❌ 无锁保护 */
static ssize_t mydev_write(struct file *filp, ...)
{
    memcpy(priv->buf, ubuf, len);   /* 竞态! */
    priv->len = len;
    return len;
}

正确代码

c
/* ✅ 使用互斥锁 */
static ssize_t mydev_write(struct file *filp, ...)
{
    if (mutex_lock_interruptible(&priv->lock))
        return -ERESTARTSYS;

    memcpy(priv->buf, ubuf, len);
    priv->len = len;

    mutex_unlock(&priv->lock);
    return len;
}

调试技巧

bash
# lockdep 检测死锁(需 CONFIG_LOCKDEP=y)
dmesg | grep "possible circular locking"

褚成志的笔记