死锁检测组件
一 死锁存在的条件
死锁,是指多个线程或者进程在运行过程中因争夺资源而造成的一种僵局,当进程或者线程
处于这种僵持状态,若无外力作用,它们将无法再向前推进。
二 死锁检测的原理
资源获取环可以采用图来存储,使用有向图来存储。 线程 A 获取线程 B 已占用的锁,则为线程 A 指向线程 B 。 线程 B 获取成功的锁就是线程B占用的锁。检测原理就是采用另一个线程定时对图进程检测是否有环的存在。转化为有向图的检测。
三 有向图的构建
在这里,我使用了邻接表法构建了一个有向图。这个有向图的数据结构如下:
struct source_type {
uint64 id;
enum Type type;
uint64 lock_id;
int degress;
};
struct vertex { //有向图的点
struct source_type s;
struct vertex *next;
};
struct task_graph { //临接法的图
struct vertex list[MAX]; //数组上的是点,每个数组元素后面挂的节点是由数组元素指向节点的有向边
int num; //有向图中点的个数
struct source_type locklist[MAX]; //线程和锁的映射关系
int lockidx; //锁的数量
};
当一个线程上了一把锁,这个线程和锁便会作为节点加入这个有向图中。
当一个线程加了一把已经被别的线程加过的锁,这个线程便会作为一个节点加入有向图,并在有向图中加一个指向拥有目标锁的线程的边。
四 三个原语操作
为了完成第三部分中的最后两点功能,我设置了三个函数,分别是lock_before、lock_after和unlock_after。
它们的用法是:
...
lock_before(selfid, (uint64)mutex);
pthread_mutex_lock_f(mutex);
lock_after(selfid, (uint64)mutex);
...
pthread_mutex_unlock_f(mutex);
unlock_after(selfid, (uint64)mutex);
这三个源于操作的参数都是线程id和锁的地址。下面详细介绍它们的作用
- lock_before:
这个函数紧挨着加锁的函数之前调用。
如果他要加的锁已经被别的线程加过,那么他首先将作为一个点加入有向图,然后将那个线程也加入有向图,最后在把他指向那个线程的有向边加入有向图中。
如果要加的锁没有被其他线程使用,那什么也不做。 - lock_after:
这个函数紧挨着加锁的函数之后调用。
如果这把锁没有在有向图的locklist数组中,就把这把锁和线程id组成一个元素加入到locklist中。
如果这把锁在有向图中,如果,本线程有指向其他拥有锁的线程的边,那删除掉这条边。 - unlock_after:
如果locklist里面有包含这个锁的元素,且这把锁已经没有别的线程使用或将要使用,那就从locklist中删除掉这个元素。
通过这三个操作,可以将加解锁和有向图关联起来。
五 hook包装函数
如果每次使用加解锁函数还需要在前后相关地方加上那三个函数,未免太过于复杂。为了方便用户的使用,这里,我使用了hook技术包装了这些操作。
Hook即钩子,截获API调用的技术,是将执行流程重定向到你自己的代码,如可以使程序运行时调用你自己实现的malloc函数代替调用系统库中的malloc函数。
这里给出必要的代码。
...
#include <dlfcn.h>
...
int pthread_mutex_lock(pthread_mutex_t *mutex) {
pthread_t selfid = pthread_self();
lock_before(selfid, (uint64)mutex);
pthread_mutex_lock_f(mutex);
lock_after(selfid, (uint64)mutex);
}
int pthread_mutex_unlock(pthread_mutex_t *mutex) {
pthread_t selfid = pthread_self();
pthread_mutex_unlock_f(mutex);
unlock_after(selfid, (uint64)mutex);
}
static int init_hook() {
pthread_mutex_lock_f = dlsym(RTLD_NEXT, "pthread_mutex_lock"); //之后再代码中调用pthread_mutex_lock_f 就相当于调用pthread_mutex_lock系统调用,下同。
pthread_mutex_unlock_f = dlsym(RTLD_NEXT, "pthread_mutex_unlock");
}
...
int main(int argc,char **argv) {
init_hook();
...
}
dlsym(RTLD_NEXT, “pthread_mutex_lock”)的用处是,返回动态库中第一个名为pthread_mutex_lock的函数的指针。
当在main函数中使用了ini_hook后,以后在这个份原文件中调用pthread_mutex_lock和pthread_mutex_unlock时,就会调用我们自己定义的这两个函数。
六 启动线程检测
将死锁线程程序作为一个单独运行的线程独立运行。这个程序会循环检测所有已经加锁的线程,分析这个线程会不会和其他线程形成一个环。代码如下:
void check_dead_lock(void) {
int i = 0;
deadlock = 0;
for (i = 0;i < tg->num;i ++) {
if (deadlock == 1) break;
search_for_cycle(i);
}
if (deadlock == 0) {
printf("no deadlock\n");
}
}
static void *thread_routine(void *args) {
while (1) {
sleep(5);
check_dead_lock();
}
}
void start_check(void) {
tg = (struct task_graph*)malloc(sizeof(struct task_graph));
tg->num = 0;
tg->lockidx = 0;
pthread_t tid;
pthread_create(&tid, NULL, thread_routine, NULL);
}
int main(int argc,char **argv) {
...
start_check(); //启动线程检测程序
...
}
这样,当其他线程再调用各种锁时,如果出现了环,则会立即再终端中打印出可能会有死锁的提示,并打印出形成环的全部线程id。
七 我的代码
我编写了一个可以直接使用的死锁检测组件,感兴趣的朋友可以直接拿过去使用,代码链接是:https://github.com/q962875152/mycode/blob/master/deadlock.c