GDB 死锁分析

2017-11-20 language linux c/cpp

pthread 是 POSIX 标准的多线程库,其源码位于 glibc 中的 Native POSIX Thread Library, NPTL 目录下,大部分的应用都是基于 pthread 来实现多线程的并行与同步管理。

简介

通过线程可以提高调度效率,包括了更加轻量级的上下文切换,避免不必要的 mm_switch ,在 Linux 中 pthread 所提供的同步机制核心要依赖与内核的 futex 机制。

在用户空间会执行变量的原子增加操作,一般是 CAS 操作,如果没有冲突,那么就会立即返回,不会发生上下文切换。

只有发生冲突后,才会调用 futex 系统调用,然后切换到内核态。

nptl 的实现会通过 futex 中的值标识来表示锁的状态,包括了:A) 0 锁空闲;B) 1 没有 waiter ,解锁之后无需调用 futex_wake ;C) 2 有 waiter ,那么解锁之后需要调用 futex_wake 。

pthread_mutex_lock()   nptl/pthread_mutex_lock.c
 |-__pthread_mutex_lock()
   |-LLL_MUTEX_LOCK()    最主要的实现函数,也就是lll_lock()的宏定义
     |-__lll_lock()
       |-atomic_compare_and_exchange_bool_acq()	尝试从0变为1,成功返回0(获得锁返回),否则返回>0
         |-__lll_lock_wait() 返回的是非0进入阻塞,会调用futex并将值设置为2

上述真实的代码是在汇编文件中实现,对于 C 代码可以参考 lowlevellock.c 中的实现。

void __lll_lock_wait (int *futex, int private)
{
	/* 非第一个线程会阻塞在这里 */
	if (*futex == 2)  
		lll_futex_wait (futex, 2, private); /* Wait if *futex == 2.  */
 
	/* 第一个线程会阻塞在这里 */
	while (atomic_exchange_acq (futex, 2) != 0)
		lll_futex_wait (futex, 2, private); /* Wait if *futex == 2.  */
}

__lll_lock_wait() 函数中,第一个没有获取锁的线程会进入 while 循环,并将 futex 赋值成为 2 ,等待 lock 被释放后成为 0 ,这第一个 waiter 被唤醒,atomic_exchange_acq() 则会赋予 futex 继续是 2,但是返回 0 跳出获取到 lock 。

pthread_mutex_unlock()
 |-__pthread_mutex_unlock()
   |-__pthread_mutex_unlock_usercnt() 做一些恢复owner nusers的操作
     |-lll_unlock()   // 将futex值赋为0,并对oldval比较,如果是2,说明有waiter,则futex_wake,1则不需要
	   |-lll_futex_wake()

另外,内核中的 futex 模块的 waiter 队列是 FIFO 的,根据参数 mutex unlock 后只会 wake up 一个 waiter 。

示例

如下是一个会产生死锁的示例。

#include <unistd.h>
#include <pthread.h>

struct foobar {
    pthread_mutex_t mutex1;
    pthread_mutex_t mutex2;
};

void *thread1(void *arg)
{
    struct foobar *d;

    d = (struct foobar *)arg;
    while (1) {
        pthread_mutex_lock(&d->mutex1);
        sleep(1);
        pthread_mutex_lock(&d->mutex2);

        pthread_mutex_unlock(&d->mutex2);
        pthread_mutex_unlock(&d->mutex1);
    }
}

void *thread2(void *arg)
{
    struct foobar *d;

    d = (struct foobar *)arg;
    while (1) {
        pthread_mutex_lock(&d->mutex2);
        sleep(1);
        pthread_mutex_lock(&d->mutex1);

        pthread_mutex_unlock(&d->mutex1);
        pthread_mutex_unlock(&d->mutex2);
    }
}

int main(void)
{
    pthread_t tid[2];
    struct foobar data = {
        PTHREAD_MUTEX_INITIALIZER,
        PTHREAD_MUTEX_INITIALIZER
    };

    pthread_create(&tid[0], NULL, &thread1, &data);
    pthread_create(&tid[1], NULL, &thread2, &data);

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

    return 0;
}

注意,如果使用了 -O2 参数 + strip 操作,那么对于一些已经优化的符号 (static inline) 在使用 gdb 的时候就可能不存在。

那么需要添加 -rdynamic 参数,那么即使执行了上面的两个操作,那么仍然可以通过 gdb 使用。

不过,因为优化之后可能导致符号对应的地址与原函数不匹配,有可能是在函数的末尾,那么,对于 gdb 来说,就是设置了断点,但是可能不生效。

此时,只能通过如下方法查看其反汇编代码。

(gdb) disassemble /r 0x401365,0x401370
(gdb) info break
(gdb) delete 1            # 删除序号为1的断点,如果不加参数,则删除所有
(gdb) break *(0x400990)   # 根据地址设置断点

无符号

在发行版本中,一般会将调试信息删除,但是,因为 mutex 的数据结构是固定的,所以仍然可以通过 gdb 进行查看。

__lll_lock_wait() 所在的帧处,对于 x86_64 可以通过 p *(pthread_mutex_t*)$rdi 查看,而 x86_32 可以通过 p *(pthread_mutex_t*)$ebx 查看。

注意,如果没有 debuginfo 包,一般会报 No symbol table is loaded. 的错误,也就是对应的 pthread_mutex_t 符号没有加载,那么可以通过如下方式查看。

(gdb) print *((int*)($rdi))                # lock字段
$4 = 2
(gdb) print *((unsigned int*)($rdi)+1)     # count字段
$5 = 0
(gdb) print *((int*)($rdi)+2)              # owner字段
$6 = 12275

然后可以通过 /proc/<PID>/maps 确定其所属的地址空间,基本确定发生死锁的是本二进制,还是在库中。