文章目录
死锁
死锁,简单地说就是一组线程或进程互相之间通过互斥量去竞争资源而导致全部进入永久阻塞的状态。如下图所示,首先线程1获取了互斥锁mutex1,线程2获取了互斥锁mutex2,线程3获取了互斥锁mutex3;此时,线程1想要获取mutex2,则它将阻塞直至线程2释放mutex2;线程2又想要获取mutex3,则它将阻塞直至线程3释放mutex3;线程3又想要获取mutex1,则它将阻塞直至线程1释放mutex1。此时死锁就产生了,其中任何一方都获取不到想要的资源,同时又不释放自己已经获取的资源,这种资源分配关系形成了一个环。
也因此,如果把线程间的资源等待关系描述成一个有向图,则图中是否存在环路就可以作为检测死锁的依据。
组件框架
首先我们需要利用dlsym()
函数对pthread_mutex_lock()
和pthread_mutex_unlock()
进行hook,在源代码中重新定义这两个函数,在加锁和解锁时记录资源的调用关系。pthread_mutex_lock()
和pthread_mutex_unlock()
都位于动态共享库中,程序启动时已经被加载到内存中,而dlsym()
函数会借助符号表从动态共享库所在的内存中提取出这些函数的地址。如下所示:
typedef int (*pthread_mutex_lock_ptr)(pthread_mutex_t* mutex);
pthread_mutex_lock_ptr __pthread_mutex_lock;
typedef int (*pthread_mutex_unlock_ptr)(pthread_mutex_t* mutex);
pthread_mutex_unlock_ptr __pthread_mutex_unlock;
int pthread_mutex_lock(pthread_mutex_t* mutex) {
__pthread_mutex_lock(mutex);
}
int pthread_mutex_unlock(pthread_mutex_t* mutex) {
__pthread_mutex_unlock(mutex);
}
void hook() {
__pthread_mutex_lock = dlsym(RTLD_NEXT, "pthread_mutex_lock");
__pthread_mutex_unlock = dlsym(RTLD_NEXT, "pthread_mutex_unlock");
}
这样一来,我们的目标代码中调用hook()之后,再去调用pthread_mutex_lock()
和pthread_mutex_unlock()
实际上是先进入了我们上面定义的这两个函数,然后通过__pthread_mutex_lock()
和__pthread_mutex_unlock()
才真正进入到pthread库中的加锁和解锁函数。
于是我们在pthread_mutex_lock()
和pthread_mutex_unlock()
中做手脚就行了:
- thread1在加锁前,如果该锁已被thread2占用,则建立从节点thread1到节点thread2之间的一条边,将该锁目前的请求计数加1(请求计数即拥有该锁的线程数+正在等待该锁的线程数);
- thread1在获取到锁后,记录为该锁当前的拥有者(使用线程ID标记),将节点thread1到节点thread2之间的边取消;
- thread1在释放锁后,将该锁目前的请求计数减1;
之后,只需要通过遍历各个thread之间的边,看看是否存在环,就知道是否有死锁。
因此,修改后的pthread_mutex_lock()
和pthread_mutex_unlock()
框架如下:
int pthread_mutex_lock(pthread_mutex_t* mutex) {
pthread_t tid = pthread_self();
before_lock(/*...*/);
int ret = __pthread_mutex_lock(mutex);
after_lock(/*...*/);
return ret;
}
int pthread_mutex_unlock(pthread_mutex_t* mutex) {
int ret = __pthread_mutex_unlock(mutex);
after_unlock(/*...*/);
return ret;
}
我们可以将线程封装成图中的节点vertex
,并且将所有的vertex
串成链表vertex_list
,而将锁封装成资源resource_lock
,将所有的resource_lock
串成链表lock_list
。
struct vertex {
struct vertex* next;
pthread_t tid; // 每个线程是一个节点 vertex,因此用线程 id 作为节点的 id
struct resource_lock* waitting; // waitting 不为 NULL时,指向当前正在等待释放的锁资源,通过 resource_lock 可以获取到 holder 从而构建图
int visited; // 遍历时用于标记是否访问过
};
struct resource_lock {
struct resource_lock* next;
struct vertex* holder; // 持有当前锁的 vertex
pthread_mutex_t* lock; // 该锁资源的 mutx 地址
int used; // 有多少线程当前在持有该资源或等待该资源
};
每个resource_lock
如果被占用的话(成功获取到锁),就将其占用者记录为holder
;当一个vertex
想要获取某个resource_lock
前,先记录该资源为自己的waitting
对象,待真正获取到该锁后再将自己标记为holder
,然后将waitting
置空(取消图的边)。在该vertex
释放该resource_lock
之前,其他想要获取该锁的线程都会阻塞。
数据结构之间的关系画图表示如下:
图中的1,2,3三条边构成了一个环。这里,各个vertex
之间其实是间接通过resource_lock
形成环。之所以这么设计,主要还是考虑到多个线程可能同时在等待一个锁,而一个线程也可能同时拥有多个锁,将vertex
与resource_lock
分开,有助于简化我们的代码,否则某个锁释放时要调整vertex
之间的指向关系的话,逻辑上就会变得比较复杂。
我们将所有资源放在一个task_graph
结构体中:
struct