从策略和实践,带你掌握死锁检测
本文分享自华为云社区《掌握死锁检测:策略和最佳实践》,作者: 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。
死锁检测可以使用图算法,通过检测有向图是否有环判断是否有死锁。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
启动速度提升 10 倍:Apache Dubbo 静态化方案深入解析
作者:华钟明 文章摘要: 本文整理自有赞中间件技术专家、Apache Dubbo PMC 华钟明的分享。本篇内容主要分为五个部分: -GraalVM直面 Java 应用在云时代的挑战 -Dubbo享受AOT带来的技术红利 -Dubbo Native Image的实践和示例 -Dubbo集成Native Image的原理和思考 -Dubbo在Native Image技术的未来规划 GraalVM 直面 Java 应用在云时代的挑战 云计算时代比较显著的特点包括: 基于云计算的基础设施,我们的应用能够在云上快速、轻松且高效地做到弹性。尤其是无状态的应用,能够轻易地基于同一个镜像构建实例,当然也能轻易地收缩多余的实例,实现弹性伸缩容。 基于容器化技术,系统资源被切分的更细,资源的利用也变得更优。 基于云计算的开发平台,应用部署更加容易,应用开发更加敏捷。 那么在云计算时代,Java 应用存在哪些问题呢? 冷启动速度较慢。 Java 应用启动需要经历包括 JVM 的初始化、类加载等过程,导致启动速度相较于其他语言来说是处于劣势的。 应用预热时间过长,无法立即达到性能峰值。 比如如果没有对应用做...
- 下一篇
RAG (检索增强生成)技术详解:揭秘基于垂直领域专有数据的Chatbots是如何实现的
编者按:相信很多人都对Chatbots背后的技术原理很感兴趣,其实Chatbots并非通过“魔法”与我们交流,而是依靠一种被称为检索增强生成(RAG)的技术。 文章详细梳理了 RAG 技术的具体实现原理。首先,RAG 将用户输入的问题与知识库中的私有数据进行匹配,获取相关知识片段。然后,通过预训练的大语言模型,用提取到的知识片段来增强对问题的回答生成过程。在知识提取步骤,借助词向量的相似度找到与用户提出的问题最匹配的内容。生成回答时,直接向语言模型提供增强知识来指导其产出更符合语境的回答。 RAG 技术看似神奇,但其本质是结合了检索和生成两个子任务的一种系统工程,而每个子任务又都有明确的技术原理支撑。作为 AI 开发者,理解这一工作流程尤为重要。相信本文有助于读者进一步掌握 RAG 的技术原理,从而更好地运用 Chatbots 为用户创造更多价值。 以下是译文,enjoy! 作者 | Cory Zue 编译 | 岳扬 🚢🚢🚢欢迎小伙伴们加入AI技术软件及技术交流群,追踪前沿热点,共探技术难题~ 目录 01 什么是检索增强生成(RAG)技术? 02 让我们从后往前看:给LLM额外的...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- Linux系统CentOS6、CentOS7手动修改IP地址
- 2048小游戏-低调大师作品
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- MySQL8.0.19开启GTID主从同步CentOS8