微信公众号:奔跑吧linux社区
本文节选自《奔跑吧linux内核》第二版卷1第6.3.2章
1.问题描述
对于RHEL发行版以及Ubuntu服务器版本,客户都报告了这样的问题。在一台至强服务器(使用x86_64处理器)上发现好几个正在运行的虚拟机发生了死锁,即虚拟机没有响应。这台服务器是基于NUMA架构的服务器,内建多个CPU节点和内存节点。在主机的Linux中开启了KSM机制和numad监控程序。虚拟机是基于KVM和QEMU构建的。
主机的Linux发行版是Ubuntu 16.04。主机的Linux内核版本是4.4.0-47-generic。
在虚拟机的Linux内核中发现softlockup的如下日志信息。
CPU: 3 PID: 22468 Comm: kworker/u32:2 Not tainted 4.4.0-47-generic #68-Ubuntu
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS
Ubuntu-1.8.2-1ubuntu1 04/01/2014
Workqueue: writeback wb_workfn (flush-252:0)
[<ffffffff81104388>] smp_call_function_many+0x1f8/0x260
[<ffffffff810727d5>] native_flush_tlb_others+0x65/0x150
[<ffffffff81072b35>] flush_tlb_page+0x55/0x90
2.问题分析
从虚拟机的Linux内核日志信息可知,虚拟机的vCPU3正在运行kworker内核线程,这个线程调用flush_tlb_page()函数刷新TLB,并调用smp_call_function_many()函数给其他vCPU发送IPI广播,最后一直在等待其他vCPU回应。smp_call_function_many()函数实现在kernel/smp.c中,其代码片段如下。
<kernel/smp.c>
void smp_call_function_many(const struct cpumask *mask,
smp_call_func_t func, void *info, bool wait)
{
...
for_each_cpu(cpu, cfd->cpumask) {
csd_lock(csd);
csd->func = func;
csd->info = info;
if (llist_add(&csd->llist, &per_cpu(call_single_queue, cpu)))
__cpumask_set_cpu(cpu, cfd->cpumask_ipi);
}
/* 给所有vCPU发送IPI广播*/
arch_send_call_function_ipi_mask(cfd->cpumask_ipi);
/* 等待其他vCPU返回*/
if (wait)
for_each_cpu(cpu, cfd->cpumask)
csd_lock_wait(csd);
}
我们在虚拟机的Linux内核中找不到特别多的有用信息,但是为什么vCPU会一直在等待TLB刷新呢?我们需要结合主机的Linux内核日志信息一起分析。
接下来分析主机Linux内核中的日志信息。我们可以使用kdump工具来抓取主机Linux发生的死锁或者softlockup中的内存快照(vmcore)。从主机的vmcore里,我们发现有几个线程一直在等待一个读写信号量。
下面是ksmd内核线程的函数调用栈,从第2个栈帧可以发现ksmd内核线程尝试申请一个读者类型的信号量,但是一直没有成功。对照其源代码,我们发现它在scan_get_next_rmap_item()函数中尝试申请一个读者类型的mm→mm