1. 死锁现象初探:驱动死锁到底是个啥?

各位同学,咱们今天开篇就聊一个让无数驱动工程师头疼的问题——死锁。

我记得刚入行那会儿,第一次遇到死锁,系统直接卡死,按啥都没反应。我当时还以为是硬件挂了,折腾了半天才发现,原来是代码里两个锁互相等着不放。嗯,从那以后我就记住了:死锁这玩意儿,不搞清楚,迟早要栽跟头。

什么是驱动死锁?

说白了,驱动死锁就是内核里的多个任务(比如进程、中断处理、内核线程)互相等待对方释放资源,结果谁也没法继续往下走。就像两个人过独木桥,谁也不让谁,最后都堵在桥上。

在Linux驱动里,最常见的资源就是自旋锁和互斥锁。你拿了一个锁,没释放就去拿另一个;别人正好反过来。好了,死锁了。

核心定义:死锁是指两个或多个执行单元因争夺资源而造成的一种互相等待的现象,若无外力干预,它们将永远阻塞。

死锁的四个必要条件

搞驱动的人都知道,死锁不是随机发生的。它必须同时满足四个条件。缺一个,死锁就成不了。我习惯把这四个条件记成「互持非循」,你听听看:

条件 含义 驱动中的典型场景
互斥 资源一次只能被一个任务使用 自旋锁、互斥锁、硬件寄存器
持有并等待 任务拿着一个资源,同时等待另一个 函数A拿了锁1,又去拿锁2
非剥夺 资源不能被强制拿走,只能主动释放 内核不会强行解锁,必须由持有者释放
循环等待 多个任务形成等待环路 任务A等锁2,任务B等锁1

注意:这四个条件必须同时成立,死锁才会发生。你只要破坏其中一个,死锁就解了。这是咱们后面调试和排查的核心思路。

一个简单的死锁示例

光说不练假把式。咱们写个简单的字符设备驱动,看看死锁是怎么发生的。

假设驱动里有两个全局资源,分别用两个自旋锁保护。一个读操作需要同时访问这两个资源:

/* 伪代码 - 死锁版本 */
spinlock_t lock_a;
spinlock_t lock_b;

ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    spin_lock(&lock_a);          // 先拿锁A
    printk("拿了锁A,准备拿锁B...\n");

    /* 模拟一些处理 */
    mdelay(10);

    spin_lock(&lock_b);          // 再拿锁B
    printk("拿了锁B,开始读取数据\n");

    /* 读取数据... */

    spin_unlock(&lock_b);
    spin_unlock(&lock_a);

    return count;
}

ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    spin_lock(&lock_b);          // 先拿锁B
    printk("拿了锁B,准备拿锁A...\n");

    /* 模拟一些处理 */
    mdelay(10);

    spin_lock(&lock_a);          // 再拿锁A
    printk("拿了锁A,开始写入数据\n");

    /* 写入数据... */

    spin_unlock(&lock_a);
    spin_unlock(&lock_b);

    return count;
}

你看,两个函数拿锁的顺序正好相反。如果两个进程同时调用——一个读、一个写——就会这样:

  • 进程1(读):拿到锁A,等着锁B
  • 进程2(写):拿到锁B,等着锁A

好了,死锁了。两个进程都卡在 spin_lock() 里,谁也别想出来。系统日志里最后看到的可能就是「拿了锁A,准备拿锁B...」和「拿了锁B,准备拿锁A...」,然后就没了。

我的经验:我曾经在一个网卡驱动里遇到过类似的死锁。当时两个线程分别处理发送和接收,锁的顺序没统一。排查了整整一个下午,最后用JTAG挂上去一看,两个CPU核都在spin_lock里自旋。嗯,从那以后我写驱动,拿锁顺序一定画个图贴在显示器上。

死锁的直观理解

为了让你更直观地理解,我画了一张流程图。你看看这个循环等待的样子:

死锁循环等待示意图 进程1(读) 持有锁A,等待锁B 进程2(写) 持有锁B,等待锁A 资源锁A 资源锁B 等待 等待 持有 持有 ⚠ 循环等待形成 进程1等锁B → 锁B被进程2持有 → 进程2等锁A → 锁A被进程1持有

看到没?箭头形成了一个闭环。这就是循环等待。只要这个环不断,两个进程就永远卡死。

怎么判断是不是死锁?

在实际项目中,我一般通过这几个现象来判断:

  • 系统无响应:用户态进程卡住,按Ctrl+C都杀不掉
  • CPU占用异常:某个CPU核跑到100%,但啥活也不干(自旋锁死锁典型特征)
  • 内核日志中断:日志打印到某个点就停了,后面再也没有输出
  • /proc/lock_stat 或 lockdep 报错:内核自带死锁检测工具会给出警告

特别提醒:自旋锁死锁比互斥锁死锁更隐蔽。互斥锁死锁时任务会进入睡眠状态,你还能看到D状态的进程。自旋锁死锁是CPU在那空转,连调度器都跑不了。我遇到过最惨的一次,死锁后连串口打印都出不来,只能按复位键。

小结一下

这一节咱们把死锁的基本概念和四个必要条件讲清楚了。你记住:互持非循,四个条件缺一不可。后面咱们会深入讲怎么用JTAG抓死锁现场、怎么分析日志、怎么从代码层面避免死锁。