从策略和实践,带你掌握死锁检测

本文分享自华为云社区《掌握死锁检测:策略和最佳实践》,作者: Lion Long。

一、背景:死锁产生原因

死锁,是指多个线程或者进程在运行过程中因争夺资源而造成的一种僵局,当进程或者线程处于这种僵持状态,若无外力作用,它们将无法再向前推进。

如下图所示,线程 A 想获取线程 B 的锁,线程 B 想获取线程 C 的锁,线程 C 想获取线程 D 的锁,线程 D 想获取线程 A 的锁,从而构建了一个资源获取环。


如果有两个及以上的CPU占用率达到100%时,极可能是程序进入死锁状态。
死锁的存在是因为有资源获取环的存在,所以只要能检测出资源获取环,就等同于检测出死锁的存在。

1.1、构建一个死锁

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex4 = PTHREAD_MUTEX_INITIALIZER;

void *thread_funcA(void *arg)
{
    pthread_mutex_lock(&mutex1);
    sleep(1);
    pthread_mutex_lock(&mutex2);

    printf("funcA --> \n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}

void *thread_funcB(void *arg)
{
    pthread_mutex_lock(&mutex2);
    sleep(1);
    pthread_mutex_lock(&mutex3);

    printf("funcB --> \n");

    pthread_mutex_unlock(&mutex3);
    pthread_mutex_unlock(&mutex2);
}

void *thread_funcC(void *arg)
{
    pthread_mutex_lock(&mutex3);
    sleep(1);
    pthread_mutex_lock(&mutex4);

    printf("funcC --> \n");

    pthread_mutex_unlock(&mutex4);
    pthread_mutex_unlock(&mutex3);
}

void *thread_funcD(void *arg)
{
    pthread_mutex_lock(&mutex4);
    sleep(1);
    pthread_mutex_lock(&mutex1);

    printf("funcD --> \n");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex4);
}
int main()
{
    pthread_t tid[4] = { 0 };
    
    pthread_create(&tid[0], NULL, thread_funcA, NULL);
    pthread_create(&tid[1], NULL, thread_funcB, NULL);
    pthread_create(&tid[2], NULL, thread_funcC, NULL);
    pthread_create(&tid[3], NULL, thread_funcD, NULL);

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_join(tid[2], NULL);
    pthread_join(tid[3], NULL);

    return 0;
}

二、使用hook检测死锁

hook使用场景:

(1)实现自己的协议栈,通过hook posix api。

2.1、dlsym()函数

获取共享对象或可执行文件中符号的地址。

函数原型:

复制代码
#include <dlfcn.h>

void *dlsym(void *handle, const char *symbol);

#define _GNU_SOURCE
#include <dlfcn.h>

void *dlvsym(void *handle, char *symbol, char *version);

// Link with -ldl.

描述:

函数dlsym()接受dlopen()返回的动态加载共享对象的"句柄"以及以空结尾的符号名,并返回该符号加载到内存中的地址。如果在指定对象或加载对象时dlopen()自动加载的任何共享对象中找不到该符号,dlsym()将返回NULL。(dlsym()执行的搜索是通过这些共享对象的依赖关系树进行的广度优先搜索。)
由于符号的值实际上可能是NULL(因此,dlsym()的NULL返回值不必指示错误),因此测试错误的正确方法是调用dlerror()以清除任何旧的错误条件,然后调用dlsym。
handle中可以指定两个特殊的伪句柄:

代码 含义
RTLD_DEFAULT 使用默认共享对象搜索顺序查找所需符号的第一个匹配项。搜索将包括可执行文件及其依赖项中的全局符号,以及使用RTLD_GLOBAL标志动态加载的共享对象中的符号。
RTLD_NEXT 在当前对象之后,按搜索顺序查找所需符号的下一个匹配项。这允许在另一个共享对象中为函数提供包装,例如,预加载共享对象中的函数定义可以查找并调用另一共享对象中提供的"真实"函数(或者在预加载有多层的情况下,函数的"下一个"定义)。

函数dlvsym()的作用与dlsym()相同,但使用版本字符串作为附加参数。
返回值:

成功时,这些函数返回与符号关联的地址。

失败时,返回NULL;可以使用dlerror()诊断错误的原因。

2.2、pthread_self()函数

获取调用线程的ID。

函数原型:

复制代码
#include <pthread.h>

pthread_t pthread_self(void);

// Compile and link with -pthread.

说明:

函数的作用是返回调用线程的ID。这与创建此线程的pthread_create()调用中*thread中返回的值相同。
返回值:
此函数始终成功,返回调用线程的ID。

2.3、实现步骤

(1)构建函数指针

(2)定义与目标函数一样的类型

复制代码
typedef int(*pthread_mutex_lock_t)(pthread_mutex_t *mutex);
typedef int(*pthread_mutex_unlock_t)(pthread_mutex_t *mutex);

pthread_mutex_lock_t    pthread_mutex_lock_f;
pthread_mutex_unlock_t    pthread_mutex_unlock_f;

(3)具体函数实现,函数名与目标函数名一致

复制代码
int pthread_mutex_lock(pthread_mutex_t *mutex)
{
    pthread_t selfid = pthread_self();

    printf("pthread_mutex_lock: %ld, %p\n", selfid, mutex);
    // ...
    return 0;
}

int pthread_mutex_unlock(pthread_mutex_t *mutex)
{
    pthread_t selfid = pthread_self();

    printf("pthread_mutex_unlock: %ld, %p\n", selfid, mutex);
    // ...
    return 0;
}

(4)调用dlsym()函数,即钩子。

复制代码
int init_hook()
{
    pthread_mutex_lock_f = dlsym(RTLD_NEXT, "pthread_mutex_lock");
    pthread_mutex_unlock_f = dlsym(RTLD_NEXT, "pthread_mutex_unlock");
    // ...
    return 0;
}

2.4、示例代码

复制代码
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.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    pthread_mutex_lock_f;
pthread_mutex_unlock_t    pthread_mutex_unlock_f;

int pthread_mutex_lock(pthread_mutex_t *mutex)
{
    pthread_t selfid = pthread_self();

    pthread_mutex_lock_f(mutex);
    printf("pthread_mutex_lock: %ld, %p\n", selfid, mutex);
    
    return 0;
}

int pthread_mutex_unlock(pthread_mutex_t *mutex)
{
    pthread_t selfid = pthread_self();

    pthread_mutex_unlock_f(mutex);
    printf("pthread_mutex_unlock: %ld, %p\n", selfid, mutex);

    return 0;
}

int init_hook()
{
    pthread_mutex_lock_f = dlsym(RTLD_NEXT, "pthread_mutex_lock");
    pthread_mutex_unlock_f = dlsym(RTLD_NEXT, "pthread_mutex_unlock");
    return 0;
}

#if 1 // debug

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex4 = PTHREAD_MUTEX_INITIALIZER;

void *thread_funcA(void *arg)
{
    pthread_mutex_lock(&mutex1);
    sleep(1);
    pthread_mutex_lock(&mutex2);

    printf("funcA --> \n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
}

void *thread_funcB(void *arg)
{
    pthread_mutex_lock(&mutex2);
    sleep(1);
    pthread_mutex_lock(&mutex3);

    printf("funcB --> \n");

    pthread_mutex_unlock(&mutex3);
    pthread_mutex_unlock(&mutex2);
}

void *thread_funcC(void *arg)
{
    pthread_mutex_lock(&mutex3);
    sleep(1);
    pthread_mutex_lock(&mutex4);

    printf("funcC --> \n");

    pthread_mutex_unlock(&mutex4);
    pthread_mutex_unlock(&mutex3);
}

void *thread_funcD(void *arg)
{
    pthread_mutex_lock(&mutex4);
    sleep(1);
    pthread_mutex_lock(&mutex1);

    printf("funcD --> \n");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex4);
}
int main()
{

    init_hook();

    pthread_t tid[4] = { 0 };
    
    pthread_create(&tid[0], NULL, thread_funcA, NULL);
    pthread_create(&tid[1], NULL, thread_funcB, NULL);
    pthread_create(&tid[2], NULL, thread_funcC, NULL);
    pthread_create(&tid[3], NULL, thread_funcD, NULL);

    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_join(tid[2], NULL);
    pthread_join(tid[3], NULL);

    return 0;
}

#endif

缺点:这种方式在少量锁情况下还可以分析,在大量锁使用的情况,分析过程极为困难。

三、使用图算法检测死锁

死锁检测可以利用图算法,检测有向图是否有环。

3.1、图的构建

(1)矩阵

指向 1 指向 2 指向 3 指向 ...
节点 1
节点 2
节点 3
节点 ...

(2)邻接表

数据结构原理示意图:

"图"连接:

3.2、图的使用

先新增节点再新增边。

(1)每创建一个线程,新增一个节点;注意,不是线程创建的时候就要加节点(有些线程不会用到锁),而是线程调用锁(以互斥锁为例,pthread_mutex_lock() )的时候才添加节点。

(2)线程加锁(以互斥锁为例,pthread_mutex_lock() )的时候,并且检测到锁已经占用,则新增一条边。

(3)移除边,调用锁(以互斥锁为例,pthread_mutex_lock() )前,如果此时锁没有被占用,并且该边存在,则移除边。

(4)移除节点是在解锁之后。
三个原语操作:

(1)加锁之前的操作,lock_before();

(2)加锁之后的操作,lock_after();

(3)解锁之后的操作,unlock_after();

3.3、示例代码

代码比较长,为了避免篇幅较长,不利于阅读,这里没有贴上。如果需要,可以联系博主,或者关注微信公众号 《Lion 莱恩呀》 获取。

总结

死锁的产生是因为多线程之间存在交叉申请锁的情况,因争夺资源而造成的一种僵局。

hook使用:

(1)定义与目标函数一样的类型;

(2)具体函数实现,函数名与目标函数名一致;

(3)调用dlsym()函数,初始化hook。
死锁检测可以使用图算法,通过检测有向图是否有环判断是否有死锁。

点击关注,第一时间了解华为云新鲜技术~

相关推荐
bigear_码农5 天前
python异步协程async调用过程图解
开发语言·python·线程·进程·协程
敲上瘾13 天前
高并发内存池(四):内存释放原理与实现
c++·算法·缓存·线程·高并发内存池·池化技术
Thomas_Cai1 个月前
Python的线程、进程与协程
python·线程·进程·协程
SuperHeroWu71 个月前
【HarmonyOS NEXT】EventHub和Emitter的使用场景与区别
华为·线程·harmonyos·鸿蒙·eventhub·emitter·事件广播
stackY、2 个月前
【Linux】:封装线程
线程
郑州吴彦祖7722 个月前
《深入解析Java synchronized死锁:从可重入锁到哲学家就餐问题》
java·线程·synchronized
charlie1145141912 个月前
从0开始的操作系统手搓教程21:进程子系统的一个核心功能——简单的进程切换
汇编·学习·操作系统·线程·进程·手搓教程
magic 2452 个月前
深入理解Java多线程编程:从基础到高级应用
java·开发语言·线程
SuperHeroWu72 个月前
【HarmonyOS Next】鸿蒙应用进程和线程详解
华为·线程·进程·harmonyos·鸿蒙
小沈同学呀2 个月前
SpringBoot中使用 ThreadLocal 进行多线程上下文管理及其注意事项
java·spring boot·后端·线程·thread·threadlocal