死锁检测
死锁检测技术
一、死锁概述
1. 什么是死锁
死锁(Deadlock)是多线程或多进程环境中一种常见的同步问题。它发生在两个或多个线程相互等待对方持有的资源,导致所有相关线程都无法继续执行,系统陷入永久阻塞状态。
典型场景:
- 线程A持有互斥锁M1,并请求锁M2;
- 线程B持有互斥锁M2,并请求锁M1;
- 两者互相等待,永远无法释放资源,形成死锁。
2. 死锁的四个必要条件
死锁的发生必须同时满足以下四个条件:
- 互斥(Mutual Exclusion):资源不能被共享,同一时刻只能由一个线程占用。
- 持有并等待(Hold and Wait):线程已持有至少一个资源,同时等待获取其他线程持有的资源。
- 不可剥夺(No Preemption):资源只能由持有者主动释放,不能被强行剥夺。
- 循环等待(Circular Wait):存在一条线程-资源-线程的循环链,链中每个线程都在等待下一个线程持有的资源。
3. 死锁的危害
- 系统资源被浪费,相关线程永远无法执行。
- 可能导致CPU占用率飙升(线程忙等待)或完全无响应。
- 难以调试和复现,尤其在多核高并发环境下。
二、死锁检测的基本思想
死锁检测的核心是构建并分析资源分配图(Resource Allocation Graph)。在互斥锁场景下,我们可以将锁视为资源,线程视为节点。
资源分配图包含两类边:
- 分配边(Assignment Edge):资源→线程,表示资源已被该线程持有。
- 请求边(Request Edge):线程→资源,表示线程正在请求该资源(当前被其他线程持有)。
死锁检测即判断图中是否存在环(循环依赖)。对于锁来说,我们通常简化模型:只关心线程之间的等待关系。若线程A等待线程B持有的锁,则建立有向边A→B。若图中存在环,则死锁发生。
1. 需要跟踪的关键信息
- 锁与持有者:每个锁当前被哪个线程持有(即分配边)。
- 线程的等待关系:当线程尝试获取已被占用的锁时,记录它正在等待哪个线程释放锁(即请求边)。
2. 实现思路
通过劫持(hook)线程的互斥锁操作(如pthread_mutex_lock、pthread_mutex_unlock),在加锁前后维护两个数据结构:
- 锁-线程映射表:记录当前哪个线程持有了某个锁。
- 有向图(邻接表):记录线程之间的等待关系(线程A等待线程B)。
然后由一个独立的后台线程定期扫描有向图,使用深度优先搜索(DFS)检测是否存在环路,若存在则输出死锁信息。
三、Hook 机制:劫持系统函数
1. 为什么要 Hook
我们需要在每次调用pthread_mutex_lock/unlock时插入自定义逻辑,以更新映射表和等待图。通常采用动态链接库的拦截技术,通过dlsym获取原始函数地址,然后替换为自定义函数。
2. Hook 实现步骤
#define _GNU_SOURCE
#include <dlfcn.h>
#include <pthread.h>
#include <stdio.h>
// 函数指针类型定义
typedef int (*pthread_mutex_lock_t)(pthread_mutex_t *mutex);
typedef int (*pthread_mutex_unlock_t)(pthread_mutex_t *mutex);
// 保存原始函数地址
pthread_mutex_lock_t original_mutex_lock = NULL;
pthread_mutex_unlock_t original_mutex_unlock = NULL;
// 初始化:获取原始函数地址
void init_hook() {
original_mutex_lock = (pthread_mutex_lock_t)dlsym(RTLD_NEXT, "pthread_mutex_lock");
original_mutex_unlock = (pthread_mutex_unlock_t)dlsym(RTLD_NEXT, "pthread_mutex_unlock");
}
// 自定义加锁函数
int pthread_mutex_lock(pthread_mutex_t *mutex) {
// 加锁前的检测逻辑(查表、记录等待)
// ...
int ret = original_mutex_lock(mutex); // 真正加锁
// 加锁后的更新逻辑(更新持有关系)
// ...
return ret;
}
// 自定义解锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex) {
// 解锁前的清理逻辑
// ...
int ret = original_mutex_unlock(mutex);
// 解锁后的更新
// ...
return ret;
}
编译时需要链接dl库:gcc -o deadlock_detect deadlock_detect.c -ldl -lpthread
四、数据结构设计
1. 锁-线程映射表(Relation Table)
存储每个锁当前被哪个线程持有。可以使用哈希表或数组实现,这里用固定大小的结构体数组。
#define MAX_LOCKS 100
typedef struct {
pthread_mutex_t *mutex;
pthread_t owner; // 持有该锁的线程ID,0表示空闲
} lock_entry_t;
lock_entry_t lock_table[MAX_LOCKS];
int lock_count = 0;
// 查找锁对应的条目
lock_entry_t* find_lock_entry(pthread_mutex_t *mutex) {
for (int i = 0; i < lock_count; i++) {
if (lock_table[i].mutex == mutex) return &lock_table[i];
}
return NULL;
}
// 添加新锁(在第一次使用时)
lock_entry_t* add_lock_entry(pthread_mutex_t *mutex) {
if (lock_count >= MAX_LOCKS) return NULL;
lock_table[lock_count].mutex = mutex;
lock_table[lock_count].owner = 0;
return &lock_table[lock_count++];
}
2. 有向图(等待关系)
使用邻接表表示有向图,节点为线程ID,边表示等待关系。
#define MAX_THREADS 100
typedef struct edge {
pthread_t to;
struct edge *next;
} edge_t;
typedef struct node {
pthread_t tid;
edge_t *edges; // 邻接链表头
} node_t;
node_t graph[MAX_THREADS];
int node_count = 0;
// 根据线程ID查找或添加节点
node_t* find_node(pthread_t tid) {
for (int i = 0; i < node_count; i++) {
if (graph[i].tid == tid) return &graph[i];
}
if (node_count >= MAX_THREADS) return NULL;
graph[node_count].tid = tid;
graph[node_count].edges = NULL;
return &graph[node_count++];
}
// 添加边 from -> to
void add_edge(pthread_t from, pthread_t to) {
node_t *from_node = find_node(from);
node_t *to_node = find_node(to); // 确保目标节点存在
if (!from_node || !to_node) return;
// 检查是否已存在边,避免重复
edge_t *e = from_node->edges;
while (e) {
if (e->to == to) return;
e = e->next;
}
edge_t *new_edge = malloc(sizeof(edge_t));
new_edge->to = to;
new_edge->next = from_node->edges;
from_node->edges = new_edge;
}
// 删除从 from 出发到 to 的边
void remove_edge(pthread_t from, pthread_t to) {
node_t *from_node = find_node(from);
if (!from_node) return;
edge_t **p = &from_node->edges;
while (*p) {
if ((*p)->to == to) {
edge_t *tmp = *p;
*p = (*p)->next;
free(tmp);
return;
}
p = &(*p)->next;
}
}
五、Hook 函数中的具体操作
1. 加锁前(before_lock)
- 检查锁是否已被占用(查找lock_table)。
- 若未被占用,则无事,等待加锁后更新。
- 若已被占用,则记录等待关系:当前线程等待锁的持有者。即添加边
current_thread -> owner_thread。
2. 加锁后(after_lock)
- 锁已被当前线程成功获取。
- 删除之前可能添加的等待边(如果之前等待过该锁,但现在已获得,应取消等待)。
- 更新lock_table中该锁的owner为当前线程。
3. 解锁后(after_unlock)
- 删除lock_table中该锁的记录(owner置0)。
- 注意:不需要删除等待边,因为等待边是动态请求,解锁后等待请求不再存在,但等待边在每次加锁前都会重新添加,解锁后自然失效。
4. 线程创建钩子
为了捕获新线程的tid,需要钩子pthread_create,在创建时添加图节点。
typedef int (*pthread_create_t)(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
pthread_create_t original_pthread_create = NULL;
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg) {
int ret = original_pthread_create(thread, attr, start_routine, arg);
if (ret == 0) {
// 添加新节点到图
find_node(*thread);
}
return ret;
}
六、环路检测(DFS)
由一个独立线程定期扫描有向图,使用DFS检测是否存在环。对于每个节点,执行深度优先搜索,记录访问状态:
- 0:未访问
- 1:正在当前递归栈中(标记环)
- 2:已访问完毕(无环)
int visited[MAX_THREADS]; // 按索引
// 辅助函数:根据tid找到节点索引
int find_node_index(pthread_t tid) {
for (int i = 0; i < node_count; i++) {
if (graph[i].tid == tid) return i;
}
return -1;
}
// DFS 检测环
int dfs_cycle(int idx, int *stack) {
visited[idx] = 1; // 正在访问
stack[stack[0]+1] = idx;
stack[0]++;
edge_t *e = graph[idx].edges;
while (e) {
int next_idx = find_node_index(e->to);
if (next_idx == -1) {
e = e->next;
continue;
}
if (visited[next_idx] == 0) {
if (dfs_cycle(next_idx, stack)) return 1;
} else if (visited[next_idx] == 1) {
// 发现环,输出路径
printf("Deadlock detected: ");
for (int i = 1; i <= stack[0]; i++) {
printf("%lu -> ", graph[stack[i]].tid);
if (graph[stack[i]].tid == graph[next_idx].tid) break;
}
printf("%lu\n", graph[next_idx].tid);
return 1;
}
e = e->next;
}
visited[idx] = 2; // 访问完成
stack[0]--;
return 0;
}
void check_deadlock() {
// 重置访问标记
for (int i = 0; i < node_count; i++) visited[i] = 0;
int stack[MAX_THREADS+1] = {0}; // 栈,索引0存长度
for (int i = 0; i < node_count; i++) {
if (visited[i] == 0) {
if (dfs_cycle(i, stack)) {
// 检测到死锁,可采取行动(如打印日志、终止进程等)
}
}
}
}
七、完整代码示例
由于代码量较大,此处给出核心结构示意。完整代码通常包含:
- hook初始化
- lock/unlock钩子函数
- 线程创建钩子
- 后台检测线程
示例:自定义锁函数中更新关系
int pthread_mutex_lock(pthread_mutex_t *mutex) {
pthread_t self = pthread_self();
lock_entry_t *entry = find_lock_entry(mutex);
if (!entry) {
entry = add_lock_entry(mutex);
}
// before_lock: 如果锁已被占用,记录等待
if (entry && entry->owner != 0) {
add_edge(self, entry->owner);
}
int ret = original_mutex_lock(mutex);
// after_lock: 更新持有者,删除等待边(如果之前有)
if (ret == 0 && entry) {
if (entry->owner != 0 && entry->owner != self) {
remove_edge(self, entry->owner); // 不再等待
}
entry->owner = self;
}
return ret;
}
int pthread_mutex_unlock(pthread_mutex_t *mutex) {
lock_entry_t *entry = find_lock_entry(mutex);
pthread_t self = pthread_self();
// before_unlock: 可做一些清理
if (entry && entry->owner == self) {
entry->owner = 0;
}
int ret = original_mutex_unlock(mutex);
// after_unlock: 无需额外操作
return ret;
}
八、测试死锁场景
编写多线程程序,故意制造循环等待:
pthread_mutex_t m1, m2, m3, m4;
void* t1(void* arg) {
pthread_mutex_lock(&m1);
sleep(1);
pthread_mutex_lock(&m2);
pthread_mutex_unlock(&m2);
pthread_mutex_unlock(&m1);
return NULL;
}
void* t2(void* arg) {
pthread_mutex_lock(&m2);
sleep(1);
pthread_mutex_lock(&m3);
pthread_mutex_unlock(&m3);
pthread_mutex_unlock(&m2);
return NULL;
}
void* t3(void* arg) {
pthread_mutex_lock(&m3);
sleep(1);
pthread_mutex_lock(&m4);
pthread_mutex_unlock(&m4);
pthread_mutex_unlock(&m3);
return NULL;
}
void* t4(void* arg) {
pthread_mutex_lock(&m4);
sleep(1);
pthread_mutex_lock(&m1);
pthread_mutex_unlock(&m1);
pthread_mutex_unlock(&m4);
return NULL;
}
当四个线程同时启动时,会形成循环等待。死锁检测线程会输出类似:
Deadlock detected: 1401 -> 1402 -> 1403 -> 1404 -> 1401
九、死锁检测的局限性
- 性能开销:每次锁操作都需要更新映射和图,高频锁操作可能影响性能。
- 假阳性:如果检测周期太长,可能错过短暂死锁;周期太短则增加开销。
- 仅能检测,不能预防:检测到死锁后,通常只能终止或重启线程,无法自动恢复。
- 线程创建钩子:需要拦截所有线程创建,否则图节点可能不全。
十、总结
死锁检测通过分析线程对互斥锁的等待关系,构建有向图,定期检测环路,从而识别死锁。实现的核心技术包括:
- 函数劫持(hook):利用
dlsym拦截系统锁操作,插入自定义逻辑。 - 关系维护:维护锁与持有者的映射,以及线程间的等待图。
- 环路检测:DFS遍历图,检测环的存在。

浙公网安备 33010602011771号