问题说明

  线上服务器运行过程中突然失联,没有自动重启,维护人员进入机房后,接上显示器和鼠标均没反应。重新上电后,没有发现core文件,

检查messages日志没有什么发现,系统coredump设置也没问题。现场最近升级过一个安全版本,可能与这个版本有关系,但是没有core文件,

无法确认问题根本原因。

      维护人员重新启动所有版本,不到一天三台机器有两台又出现异常,其中一台有core文件,打开vmcore-dmesg.txt文件,发现有进程发生死锁了。

      从vmcore-dmesg.txt获取的异常信息如下:

                                                图1 系统挂死日志

 

                                         图2 top阻塞堆栈信息

 

死锁分析

  首先确认死锁是在Sys_open函数调用序列上,调用pid_revalidate函数时出现的。

  通过反汇编地址,代码如下:

  crash> dis -l ffffffff8124ebad
  /usr/src/debug/kernel-3.10.0-327.22.2.el7/linux-3.10.0-327.22.2.el7.x86_64/fs/proc/internal.h: 106
  0xffffffff8124ebad <pid_revalidate+93>: mov    0x468(%rbx),%rdi

  

  内核代码如下:

          图3  阻塞代码

 

  结合堆栈来看,top进程应该是阻塞在task_lock函数中,该函数获取进程的alloc_lock锁被阻塞。该锁用于保护进程数据结构的fs,files,

mm,group_info,以及comm等数据。

             图4  阻塞函数task_lock

 

  为什么top进程被阻塞呢?top进程调用pid_revalidate函数获取alloc_lock失败失败原因是什么?我们从被阻塞进程的堆栈信息开始分析。 

再次把top进程的堆栈打印出来,把要分析的重要寄存器用红线标记出来。

 

            图5  top进程堆栈回溯

 

 RDI寄存器信息:

             图6  RDI寄存器信息

 

   RDI寄存器保存了进程ID为1127的一个进程(准确说是一个线程)的一个成员地址,从下图运算结果,我们可以知道,

RDI寄存器保存的是进程数据结构中的alloc_lock地址,这个alloc_lock是top进程要获取的锁吗?我们稍后分析证明,先

查看一下其他的寄存器信息。

              图7  task_struct成员alloc_lock偏移

 

R9寄存器保存地址的内存信息:

  通过读取R9寄存器保存地址的内容,发现地址0xffff88130be58020开始,其内容为/proc/38484/task/1127/stat,

其中进程1127 正是寄存器RDI保存的成员地址所属的进程。我们离真相又进了一步。

 

       图8  R9寄存器指向的内存信息

 

   这个时候,pid 1127线程在做什么事情呢? 打印1127线程的堆栈信息如下:

 

             图9  PID 1127线程堆栈回溯

 

  pid 1127线程堆栈信息显示,该进程在调用xxx_sys_open函数(劫持了系统sys_open函数)时调用了get_mm_exe_file函数。

 

get_mm_exe_file函数定义如下:

 1 struct file *get_mm_exe_file(struct mm_struct *mm)
 2 {
 3     struct file *exe_file;
 4 
 5     /* We need mmap_sem to protect against races with removal of exe_file */
 6     down_read(&mm->mmap_sem);
 7     exe_file = mm->exe_file;
 8     if (exe_file)
 9         get_file(exe_file);
10     up_read(&mm->mmap_sem);
11     return exe_file;
12 }

get_mm_exe_file调用堆栈:
get_mm_exe_file
  -->down_read ## kernel/rwsem.c
    -->__down_read ## arch/alpha/include/asm/rwsem.h
      -->call_rwsem_down_read_failed ## arch/x86/lib/rwsem.S
        -->rwsem_down_read_failed ## lib/rwsem.c
          -->schedule
            -->__schedule

   从get_mm_exe_file调用堆栈分析,这一过程并没有对task->alloc_lock上锁。再回到1127进程的堆栈,并结合模块代码分析,

发现在调用get_mm_exe_file函数之前,已经获取了alloc_lock锁,代码如下:

 1 static struct file *task_exefile(struct task_struct *task)
 2 {
 3     struct file *exe_file = NULL;
 4     struct mm_struct *mm;
 5 
 6     task_lock(task);  // 获取task alloc_lock锁 
 7     mm = task->mm;
 8     if (mm) {
 9         if (!(task->flags & PF_KTHREAD))
10         {
11             if(get_mm_exe_file_ptr)
12             {
13                 exe_file = get_mm_exe_file_ptr(mm);  //获取进程可执行文件
14             }
15             else
16             {
17                 printk(ERR "get_mm_exe_file_ptr(%p) maybe NULL\n", get_mm_exe_file_ptr);                
18             }
19         }
20     }
21     task_unlock(task);
22     return exe_file;
23 }

 

  自此,系统产生死锁的脉络终于清晰了:

  pid 1127进程打开文件时被业务内核模块劫持,走到task_exefile函数时,获取了task->alloc_lock锁;然后调用get_mm_exe_file函数时,

主动进行进程调度,可能已经让出cpu;此时另外一个pid为22335的进程(也就是top进程),需要获取task->alloc_lock锁,由于pid 1127线程

长时间没有释放alloc_lock锁,导致pid 22335进程阻塞。最终引起系统异常复位。

 

  那么问题来了,怎么证明pid 22335进程希望获取pid 1127线程的alloc_lock锁呢?我们还是从pid 22335进程的堆栈backtrace分析。

 从上述堆栈分析,top进程阻塞在 task_dumpable函数中,该函数由pid_revalidate函数调用。

 

  反汇编pid_revalidate函数并分析如下:

 

地址为0xffff88169600ae00的task_struct  PID和state信息如下:

  

 

 

  

    结合堆栈分析,rdi和r13寄存器保存了同一个值0xffff88169600b600,该值就是task_struct的成员编译alloc_lock值。

通过计算可以得到task值为0xffff88169600ae00(如图6所示)。

 

  由此,我们可以明确得到问题原因:

  在业务内核模块调用task_exefile函数时持有 PID 1127线程的alloc_lock锁,且处于TASK_UNINTERRUPTIBLE状态,

top进程在获取PID 1127线程的alloc_lock锁时,就被阻塞了。

 

 

您的支持是对博主最大的鼓励👍,感谢您的认真阅读。

本博客持续更新,欢迎您关注和交流!


本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。