自制死锁检测组件

本文介绍了如何自制死锁检测组件,通过构建有向图来检测线程间的资源等待关系,以此判断是否出现死锁。文章详细阐述了组件的框架设计,包括函数hook、线程和资源的数据结构以及环的检测方法。最后,文中提供了测试代码来验证组件的正确性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

死锁

死锁,简单地说就是一组线程或进程互相之间通过互斥量去竞争资源而导致全部进入永久阻塞的状态。如下图所示,首先线程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形成环。之所以这么设计,主要还是考虑到多个线程可能同时在等待一个锁,而一个线程也可能同时拥有多个锁,将vertexresource_lock分开,有助于简化我们的代码,否则某个锁释放时要调整vertex之间的指向关系的话,逻辑上就会变得比较复杂。

我们将所有资源放在一个task_graph结构体中:

struct
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值