内核视角看线程是如何一步步死锁的?
关于死锁你能搜到很多千篇一律的资料,今天我们来讲点不一样的,那就是从内核视角看线程是如何一步步死锁的。
我们知道线程是由内核调度器决定是否运行的,调度器本质是通过线程状态来判定运行哪个线程,进程/线程状态图想必大家都了解:

只有位于就绪态(ready)的线程才有资格被调度器调度。
来看一个简单的死锁示例:
// 线程 1:先锁 A,再锁 B
void* thread1_func(void* arg) {
pthread_mutex_lock(&mutex_A); // 锁 A
pthread_mutex_lock(&mutex_B); // 锁 B
// 省略临界区操作...
pthread_mutex_unlock(&mutex_B);
pthread_mutex_unlock(&mutex_A);
returnNULL;
}
// 线程 2:先锁 B,再锁 A
void* thread2_func(void* arg) {
pthread_mutex_lock(&mutex_B); // 锁 B
pthread_mutex_lock(&mutex_A); // 锁 A
// 省略临界区操作...
pthread_mutex_unlock(&mutex_A);
pthread_mutex_unlock(&mutex_B);
returnNULL;
}
假设现在一个单核系统中只有两个线程,线程A和线程B,这两个线程都刚被创建出来,此时的内核状态是:

现在调度器决定首先调度线程A,线程A来到thread1_func拿到锁A后系统产生中断,此时内核把线程A置于就绪态后处理中断,中断处理完毕后决定调度线程B,线程B开始运行,此时的内核状态:

线程B来到thread2_func后先拿锁B,接着拿锁A时发现锁A已被占用,因此线程B来到阻塞态,此时的内核状态:

接着调度器决定运行处于就绪态的线程A,线程A继续运行去拿锁B,但发现锁已被占用,因此线程A也来到阻塞态,此时的内核状态:

现在所有线程来到了阻塞态,注意看此时的就绪队列是空的,无就绪线程可供调度,内核调度器认为此时没有任何一个线程需要分配CPU,此时CPU来到空闲状态。
死锁的本质在于调度器只会去调度位于就绪态的线程,而位于阻塞状态的线程因为资源依赖关系无法改变线程状态,从而形成僵局,但此时在内核看来系统一切正常,如果有个新的线程还可以继续运行。
内核对位于阻塞态的线程A和线程B是视而不见的,因为内核调度器仅关注线程状态,不跟踪资源依赖关系。
你可能会问既然调度器知道线程的状态,为什么内核不能主动检测死锁?
因为调度器仅跟踪线程的执行状态(如运行、就绪、阻塞),不维护资源(如锁、信号量)的分配关系,死锁涉及资源分配以及跟踪策略,这部分通常由用户态应用程序自己处理。而且如果让内核去检测所有线程的资源依赖的话开销过高。
数据库系统是最典型的自带死锁检测机制的系统,由于数据库需要处理高并发事务和共享资源(如数据行、表、索引),死锁检测是事务管理的关键功能,这通常基于等待图(Wait-for Graph)算法,周期性地检查事务间的等待关系,发现循环等待则判定为死锁。





