分析core不是一件容易的事情。试想,一个系统运行了很长一段时间,在这段时间里,系统会积累大量正常、甚至不正常的状态。这个时候若是系统忽然出现了一个问题,那这个问题十有八九跟长时间积累下来的状态有关系。分析core,就是分析出问题时,系统产生的“快照”,追溯历史,找出问题发生源头。这有点像是从案发现场,推导案发通过同样。java
今天这个“案件”,咱们从soft lockup提及。python
soft lockup是内核实现的夯机自我诊断功能。这个功能的实现,和线程的优先级有关系。linux
这里咱们假设有三个线程A、B、和C。他们的优先级关系是A<B<C。这意味着C优先于B执行,B优先于A执行。这个优先级关系,若是倒过来叙述,就会产生一个规则:若是C不能执行,那么B也没有办法执行,若是B不能执行,那基本上A也无法执行。缓存
soft lockup实际上就是对这个规则的实现:soft lockup使用一个内核定时器(C线程),周期性地检查,watchdog(B线程)有没有正常运行。若是没有,那就意味着普通线程(A线程)也没有办法正常运行。这时内核定时器(C线程)会输出相似上图中的soft lockup记录,来告诉用户,卡在cpu上的,有问题的线程的信息。架构
具体到这个“案件”,卡在cpu上的线程是python,这个线程正在刷新tlb缓存。函数
若是咱们对全部夯机问题的调用栈作一个统计的话,咱们确定会发现,tlb和ipi是一对如影随行的老搭档。其实这不是偶然的。系统中,相对于内存,tlb是处理器本地的cache。这样的共享内存和本地cache的架构,必然会提出一致性的要求。若是每一个处理器的tlb“各自为政”的话,那系统确定会乱套。知足tlb一致性的要求,本质上来讲只须要一种操做,就是刷新本地tlb的同时,同步地刷新其余处理器的tlb。系统正是靠tlb和ipi这对老搭档的完美配合来完成这个操做的。oop
这个操做自己的代价是比较大的。一方面,为了不产生竞争,线程在刷新本地tlb的时候,会停掉抢占。这就致使一个结果:其余的线程,固然包括watchdog线程,没有办法被调度执行(soft lockup)。另一方面,为了要求其余cpu同步地刷新tlb,当前线程会使用ipi和其余cpu同步进展,直到其余cpu也完成刷新为止。其余cpu若是迟迟不配合,那么当前线程就会死等。this
为何其余cpu不配合去刷新tlb呢?理论上来讲,ipi是中断,中断的优先级是很高的。若是有cpu不配合去刷新tlb,基本上有两种可能:一种是这个cpu刷新了tlb,可是作到一半也卡住了;另一种是,它根本没有办法响应ipi中断。spa
经过查看系统中全部占用cpu的线程,能够看到cpu基本上在作三件事情:idle,正在刷新tlb,和正在运行java程序。其中idle的cpu,确定能在须要的时候,响应ipi并刷新tlb。而正在刷新tlb的cpu,由于停掉了抢占,且在等待其余cpu完成tlb刷新,因此在重复输出soft lockup记录。这里问题的关键,是运行java的cpu,这个咱们在下一节讲。线程
java线程运行在0号cpu上,这个线程的调用栈,满满的都是故事。咱们能够简单地把线程调用栈分为上下两部分。下边的是system call调用栈,是java从系统调用进入内核的执行记录。上边的是中断栈,java在执行系统调用的时候,正好有一个中断进来,因此这个cpu临时去处理了中断。在linux内核中,中断和系统调用使用的是不一样的内核栈,因此咱们能够看到第二列,上下两部分地址是不连续的。
分析中断处理这部分调用栈,从下往上,咱们首先会发现,netoops函数触发了缺页异常。缺页异常其实就是给系统一个机会,把指令踩到的虚拟地址,和真正想要访问的物理机之间的映射关系给创建起来。可是有些虚拟地址,这种映射根本就是不存在的,这些地址就是非法地址(坑)。若是指令踩到这样的地址,会有两种后果,segment fault(进程)和oops(内核)。
很显然netoops踩到了非法地址,使得系统进入了oops逻辑。系统进入oops逻辑,作的第一件事情就是禁用中断。这个很是好理解。oops逻辑要作的事情是保存现场,它固然不但愿,中断在这个时候破坏问题现场。
接下来,为了保存现场的须要,netoops再一次被调用,而后这个函数在几条指令以后,等在了spinlock上。要拿到这个spinlock,netoops必需要等它当前的owner线程释放它。这个spinlock的owner是谁呢?其实就是当前线程。换句话说,netoops拿了spinlock,回过头来又去要这个spinlock,致使当前线程死锁了本身。
验证上边的结论,咱们固然能够去读代码。可是有另一个技巧。咱们能够看到netoops函数在踩到非法地址的时候,指令rip地址是ffffffff8137ca64,而在尝试拿spinlock的时候,rip是ffffffff8137c99f。很显然拿spinlock在踩到非法地址以前。虽然代码里的跳转指令,让这种判断不是那么的准确,可是大部分状况下,这个技巧是颇有用的。
这个线程进入死锁的根本缘由是,缺页异常在错误的时间发生在了错误的地点。对netoops函数的汇编和源代码进行分析,咱们会发现,缺页发生在ffffffff8137ca64这条指令,而这条指令是inline函数utsname的指令。下图中框出来的四条指令,就是编译后的utsname函数。
而utsname函数的源代码其实就一行。
return ¤t->nsproxy->uts_ns->name;
这行代码经过当前进程的task_struct指针current,访问了uts namespace相关的内容。这一行代码,之因此会编译成截图中的四条汇编指令,是由于gs寄存器的0xcbc0项,保存的就是current指针。这四条汇编指令作的事情分别是,取current指针,读nsproxy项,读uts_ns项,以及计算name的地址。第三条指令踩到非法地址,是由于nsproxy这个值为空值。
咱们能够在两个地方验证nsproxy为空这个结论。第一个地方是读取当前进程task_sturct的nsproxy项。另一个是看缺页异常的时候,保存下来的rax寄存器的值。保存下来的rax寄存器值能够在图三中看到,下边是从task_struct里读出来的nsproxy值。
那么,为何当前进程task_struct这个结构的nsproxy这一项为空呢?咱们能够回头看一下,java线程调用栈的下半部份内容。这部分调用栈其实是在执行exit系统调用,也就是说进程正在退出。实际上参考代码,咱们能够肯定,这个进程已经处于僵尸(zombie)状态了。于是nsproxy相关的资源,已经被释放了。
最后咱们简单看一下nsproxy的访问规则。规则一共有三条,netoops踩到空指针的缘由,某种意义上来讲,是由于它间接地违背了第三条规则。netoops经过utsname访问进程的namespace,由于它在中断上下文,因此并不算是访问当前的进程,也就是说它应该查空。另外我加亮的部分,进一步佐证了上一小节的结论。
/*
* the namespaces access rules are:
*
* 1. only current task is allowed to change tsk->nsproxy pointer or
* any pointer on the nsproxy itself
*
* 2. when accessing (i.e. reading) current task's namespaces - no
* precautions should be taken - just dereference the pointers
*
* 3. the access to other task namespaces is performed like this
* rcu_read_lock();
* nsproxy = task_nsproxy(tsk);
* if (nsproxy != NULL) {
* / *
* * work with the namespaces here
* * e.g. get the reference on one of them
* * /
* } / *
* * NULL task_nsproxy() means that this task is
* * almost dead (zombie)
* * /
* rcu_read_unlock();
*
*/
最后咱们复原一下案发通过。开始的时候,是java进程退出。java退出须要完成不少步骤。当它立刻就要完成本身使命的时候,一个中断打断了它。这个中断作了一系列的动做,以后调用了netoops函数。netoops函数拿了一个锁,而后回头去访问java的一个被释放掉的资源,这触发了一个缺页。由于访问的是非法地址,因此这个缺页致使了oops。oops过程禁用了中断,而后调用netoops函数,netoops须要再次拿锁,可是这个锁已经被本身拿了,这是典型的死锁。再后来其余cpu尝试同步刷新tlb,由于java进程关闭了中断并且死锁了,它根本收不到其余cpu发来的ipi消息,因此其余cpu只能不断的报告soft lockup错误。