死锁检测

死锁检测技术

一、死锁概述

1. 什么是死锁

死锁(Deadlock)是多线程或多进程环境中一种常见的同步问题。它发生在两个或多个线程相互等待对方持有的资源,导致所有相关线程都无法继续执行,系统陷入永久阻塞状态。

典型场景

  • 线程A持有互斥锁M1,并请求锁M2;
  • 线程B持有互斥锁M2,并请求锁M1;
  • 两者互相等待,永远无法释放资源,形成死锁。

2. 死锁的四个必要条件

死锁的发生必须同时满足以下四个条件:

  1. 互斥(Mutual Exclusion):资源不能被共享,同一时刻只能由一个线程占用。
  2. 持有并等待(Hold and Wait):线程已持有至少一个资源,同时等待获取其他线程持有的资源。
  3. 不可剥夺(No Preemption):资源只能由持有者主动释放,不能被强行剥夺。
  4. 循环等待(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遍历图,检测环的存在。
posted @ 2026-03-25 00:29  xggx  阅读(8)  评论(0)    收藏  举报