死锁问题分析

@

定义

所谓死锁,就是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,它们都将无法推进下去,此时,系统处于死锁(Dead Lock)状态或系统产生了死锁,这些永远在相互等待的进程称之为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下将会因为永远分配不到所必需的资源而无法继续运行下去,这就产生了死锁这一特殊现象。

产生死锁的原因

竞争资源引起进程死锁

在两个或多个任务中,如果每个任务都锁定了其他任务试图锁定的资源,则此时将会造成这些任务的永久阻塞,从而出现死锁。

进程推进顺序不当引起死锁

由于进程在运行中具有异步性特征,这可能使P1、P2、P3三个进程在获取S1、S2、S3三个资源时会按下述两种顺序向前推进。

(1)合法的进程推进顺序:P1:Release(S1);Request(S3);=>P2:Release(S2);Request(S1);=>P3:Release(S3);Request(S2)。此时,这三个进程的推进顺序时合法的,不会引起进程死锁。

(2)非法的进程推进顺序:P1:Request(S3);Release(S1);=>P2:Request(S1);Release(S2);=>P3:Request(S2);Release(S3)。此时,当上述三个进程再向前推进时,P1会因为资源S3已被P3占用而阻塞,P2会因资源S1被P1占用而阻塞,P3会因资源S2被P2占用而阻塞,于是发生进程死锁。

嵌套锁

函数 A 持有锁 L,调用函数 B,函数 B 又尝试获取锁 L(且该锁不是递归锁),这个时候会发生死锁。解决方法是使用递归锁。

资源耗尽

线程在等待一个永远不会被释放的资源(如信号量计数为 0,或者等待一个已经退出的线程 Join)。

死锁产生的必要条件

虽然进程在运行的过程中可能会发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:

(1)互斥条件:指进程对所分配到的资源进行排他性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

(2)请求和保持条件:指进程已经保持至少一个资源,但又提出新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但又对自己已获得的其他资源保持不放。

(3)不剥夺条件:指进程已获得的资源,在未使用完之前不能被剥夺,只能在使用完时由自己释放。

(4)环路等待条件:指在发生死锁的时候,必然会存在一个进程——资源的环形链,即进程集合\(\{P_0,P_1,\cdots,P_n\}\)中的\(P_0\)正在等待一个\(P_1\)所占用的资源,\(P_1\)正在等待一个\(P_2\)所占用的资源,...,\(P_n\)正在等待已被\(P_0\)所占用的资源。

处理死锁的基本方法

在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。目前处理死锁的方法可归纳为以下四种:

(1)预防死锁:方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个来预防发生死锁。常见的死锁预防方法有如下几种:

打破互斥条件,即允许进程同时访问某些资源。

打破不可抢占条件,即允许进程强行从占有者那里夺取某些资源。这种预防死锁的方法实现起来困难,会降低系统性能。

打破请求和保持条件,可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要地全部资源。如果某个进程所需要地全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有在系统能够满足当前进程地全部资源需求时,才一次性地将所申请地资源全部分配给该进程。由于运行地进程已占有了它所需地全部资源,所以不会发生占有资源又申请资源地现象,因此不会发生死锁。但是,这种策略也有一些缺点,例如,在许多情况下,一个进程在执行之前不可能知道它所需地全部资源。这是由于进程在执行时是动态的,不可预测的;另外,该策略的资源利用率低,无论所分配资源何时用到,一个进程只有在占有所需的全部资源后才会执行。即使有些资源最后才被该进程用到一次,但该进程在生存期间却一直占有它们,造成长期占着不用的状况;此外,该策略也降低了进程的并发性,因为资源有限,又加上存在浪费,因此使得能分配到所需全部资源的进程个数就必然少了。

打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请、占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。这种策略和前面的策略相比,资源的利用率和系统吞吐量都有很大提高,但是也存在一些缺点,例如限制了进程对资源的请求,同时给系统中所有的资源合理编号也是件困难的事,并增加了系统开销;其次,为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,从而增加了进程对资源的占用时间。

(2)避免死锁

该方法同样属于事先预防的策略,但它并不需要事先采取各种限制措施去破坏产生死锁的四个必要条件,而是在资源的动态分配过程中用某种方法避免发生死锁。

代表性的死锁避免方法有“有序资源分配法”:在该算法中,首先按某种规则将系统中的所有资源同意编号,然后,进程必须以上升的次序来申请这些资源。即,系统要求申请进程按照以下规则来进行:

1)进程对它所必须使用的且属于同一类的所有资源,必须一次申请完毕。

2)进程在申请不同类资源时,必须按各类资源的编号来依次申请。

(3)检测死锁
这种方法并不需要事先采取任何强制性措施,也不必检查系统是否已经进入不安全区,此方法允许系统在运行过程中发生死锁。但可通过系统所设置的检测机构及时地检测出死锁地发生,并精确确定与死锁有关地进程和资源,然后采取适当措施,从系统中将已发生地死锁清除掉。

(4)解除死锁
这是与检测死锁配套地一种措施。当检测到系统中已发生死锁时,需要将进程从死锁状态中解脱出来。常用地实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态地进程,使之转为就绪状态,以继续运行。死锁地检测和消除措施,有可能使系统获得较好地资源利用率和吞吐量,但在实现难度上最大。常用的死锁解除方法有以下几种:

1)撤销陷入死锁地全部进程

2)逐个撤销陷于死锁的进程,直到死锁不存在

3)从陷于死锁地进程中逐个强迫放弃所占用地资源,直至死锁消失

4)从另外一些进程那里强行剥夺足够数量地资源分配给死锁进程,以解除死锁状态

实验

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <ctype.h>
#include <pthread.h>

#define LOOP_TIMES 100    /* 定义程序循环执行地次数为10000次 */

/* 初始化互斥锁 pthread_mutex_t mutex1 */
pthread_mutex_t mutex1=PTHREAD_MUTEX_INITIALIZER;
/* 初始化互斥锁 pthread_mutex_t mutex2 */
pthread_mutex_t mutex2=PTHREAD_MUTEX_INITIALIZER;

void *thread_worker(void *);    /* 声明函数thread_worker() */
void critical_section(int thread_num, int i);   /* 声明函数critical_section() */
int main()
{
    int rtn, i;
    /* 声明并初始化存放子线程ID地变量 */
    pthread_t Pthread_id=0;
    /* 调用函数pthread_create()创建子线程 */
    rtn=pthread_create(&Pthread_id,NULL,thread_worker,NULL);

    if(rtn!=0) {
        printf("pthread_create ERROR!\n");
        return -1;
    }

    /* 在主线程中循环执行critical_section()函数 */
    for(i=0;i<LOOP_TIMES;i++) {
        pthread_mutex_lock(&mutex1);    /* 对第1个互斥锁上锁 */
        pthread_mutex_lock(&mutex2);    /* 对第2个互斥锁上锁 */
        critical_section(1,i);  /* 执行critical_section(1,i)函数 */
        pthread_mutex_unlock(&mutex2);
        pthread_mutex_unlock(&mutex1);
    }
	pthread_mutex_destroy(&mutex1);
	pthread_mutex_destroy(&mutex2);
	return 0;
}

void critical_section(int thread_num,int i)
{
    printf("thread %d: %d\n",thread_num,i);
}

void *thread_worker(void *p)
{
	int i;
	/* 在子线程循环执行critical_section()函数 */
	for(i=0;i<LOOP_TIMES;i++) {
		pthread_mutex_lock(&mutex2);    /* 对第2个互斥锁上锁 */
        pthread_mutex_lock(&mutex1);    /* 对第1个互斥锁上锁 */
        critical_section(2,i);  /* 执行critical_section(2,i)函数 */
        pthread_mutex_unlock(&mutex2);
        pthread_mutex_unlock(&mutex1);
	}
}

测试结果

$ gcc thread_deadlock.c -o thread_deadlock
$ ./thread_deadlock 
thread 1: 0
thread 1: 1
thread 1: 2
thread 1: 3
thread 1: 4
thread 1: 5

如何排查死锁

top

top查看进程状态

top - 13:58:44 up  2:26,  5 users,  load average: 0.00, 0.00, 0.00
Tasks: 197 total, 1 running, 196 sleep, 0 d-sleep, 0 stopped, 0 zombie
%Cpu(s):  0.1 us,  0.0 sy,  0.0 ni, 99.8 id,  0.0 wa,  0.1 hi,  0.0 si,  0.0 st 
MiB Mem :   3890.2 total,   1703.2 free,   1349.9 used,   1125.8 buff/cache     
MiB Swap:   4096.0 total,   4096.0 free,      0.0 used.   2540.3 avail Mem 

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                                                              
  22602 dev       20   0   10752   1664   1540 S   0.0   0.0   0:00.00 thread_deadlock

线程状态多为 S (Sleeping) 或 D (Uninterruptible Sleep),而不是 R (Running)。

gdb

  • 通过pid 进程序
$ sudo gdb -p 22602

查看所有线程堆栈

(gdb) thread apply all bt

Thread 2 (Thread 0x7f1f7bd4f6c0 (LWP 22603) "thread_deadlock"):
#0  0x00007f1f7bde68d0 in ?? () from /usr/lib/libc.so.6
#1  0x00007f1f7bded0c4 in pthread_mutex_lock () from /usr/lib/libc.so.6
#2  0x000055c7072772ee in thread_worker (p=0x0) at thread_deadlock.c:53
#3  0x00007f1f7bde997a in ?? () from /usr/lib/libc.so.6
#4  0x00007f1f7be6d2bc in ?? () from /usr/lib/libc.so.6

Thread 1 (Thread 0x7f1f7bd50740 (LWP 22602) "thread_deadlock"):
#0  0x00007f1f7bde68d0 in ?? () from /usr/lib/libc.so.6
#1  0x00007f1f7bded0c4 in pthread_mutex_lock () from /usr/lib/libc.so.6
#2  0x000055c70727721e in main () at thread_deadlock.c:32

寻找卡在 pthread_mutex_lock, sem_wait, futex 等函数上的线程

strace

sudo strace -f -p 43501
strace: Process 43501 attached with 2 threads
[pid 43502] futex(0x5557514a2080, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 43501] futex(0x5557514a20c0, FUTEX_WAIT_PRIVATE, 2, NULL

从strace的输出可以看到两个线程无限期地阻塞在 futex(...) 系统调用上,且永远不会醒来。

代码检查

ThreadSanitizer (TSan)

是 data race 检测工具,用于检测数据争用,上面这个死锁的例子不能说明,可以使用下面这个例子:

#include <pthread.h>
#include <stdio.h>
 
int Global;
 
void *Thread1(void *x) {
  Global++;
  return NULL;
}
 
void *Thread2(void *x) {
  Global--;
  return NULL;
}
 
int main() {
  pthread_t t[2];
  pthread_create(&t[0], NULL, Thread1, NULL);
  pthread_create(&t[1], NULL, Thread2, NULL);
  pthread_join(t[0], NULL);
  pthread_join(t[1], NULL);
}
$ g++ simple_race.cc -fsanitize=thread -fPIE -pie -g
$ ./a.out
==================
WARNING: ThreadSanitizer: data race (pid=36687)
  Read of size 4 at 0x55ca18af605c by thread T2:
    #0 Thread2 /home/dev/temp/simple_race.c:12 (a.out+0x1239) (BuildId: 718176325124af798e2e4ffca107f5cba1b9b78e)
    #1 <null> <null> (libtsan.so.2+0x541b9) (BuildId: f29521f558650bcc384c0178d8c6d0fd49466e29)

  Previous write of size 4 at 0x55ca18af605c by thread T1:
    #0 Thread1 /home/dev/temp/simple_race.c:7 (a.out+0x11f8) (BuildId: 718176325124af798e2e4ffca107f5cba1b9b78e)
    #1 <null> <null> (libtsan.so.2+0x541b9) (BuildId: f29521f558650bcc384c0178d8c6d0fd49466e29)

  Location is global 'Global' of size 4 at 0x55ca18af605c (a.out+0x405c)

  Thread T2 (tid=36690, running) created by main thread at:
    #0 pthread_create <null> (libtsan.so.2+0x5fb47) (BuildId: f29521f558650bcc384c0178d8c6d0fd49466e29)
    #1 main /home/dev/temp/simple_race.c:19 (a.out+0x12cc) (BuildId: 718176325124af798e2e4ffca107f5cba1b9b78e)

  Thread T1 (tid=36689, finished) created by main thread at:
    #0 pthread_create <null> (libtsan.so.2+0x5fb47) (BuildId: f29521f558650bcc384c0178d8c6d0fd49466e29)
    #1 main /home/dev/temp/simple_race.c:18 (a.out+0x12ab) (BuildId: 718176325124af798e2e4ffca107f5cba1b9b78e)

SUMMARY: ThreadSanitizer: data race /home/dev/temp/simple_race.c:12 in Thread2
==================

Valgrind

Helgrind 是 Valgrind 工具集中的一个检测 数据争用 的工具。它查找被多个 (POSIX) 线程访问的内存位置,但找不到一致使用的 (pthread_mutex_lock) 锁。此类位置表示线程之间缺少同步,并可能导致难以找到的时序相关问题。它对任何使用 pthread 的程序都很有用。

参考

死锁问题分析的利器——valgrind的DRD和Helgrind

posted @ 2026-03-01 16:23  main_c  阅读(8)  评论(0)    收藏  举报  来源