问题表现
应用白屏/卡住/应用进程的某条或多条线程cpu占用接近100%,目前arm64 UOS云电脑(host机器为华为鲲鹏920)中,易连/浏览器/资源管理器/向日葵等都有这个问题
问题的直接原因
cpu(疑似)在用户空间重复执行同一条指令,该指令为普通的非特权指令,且指令语义并非重复执行自己
分析过程(以浏览器为例)
1. 从top看浏览器进程(pid为11104)两条线程(tid为11104和11110)cpu接近占满且基本都在用户空间运行
2. 针对这种情况,往往需要通过perf查看热点函数进一步定位,以11104为例,通过perf top -t 11104看到热点函数在v8::internal::Assembler::Assembler,第一感觉看起来是在这个函数死循环了
如果你是第一次分析此问题,此时正确的方式是翻看v8::internal::Assembler::Assembler的代码
Assembler::Assembler(const AssemblerOptions& options,
std::unique_ptr<AssemblerBuffer> buffer)
: AssemblerBase(options, std::move(buffer)),
unresolved_branches_(),
constpool_(this) {
veneer_pool_blocked_nesting_ = 0;
Reset();
#if defined(V8_OS_WIN)
if (options.collect_win64_unwind_info) {
xdata_encoder_ = std::make_unique<win64_unwindinfo::XdataEncoder>(*this);
}
#endif
}
子函数的代码这里就不贴了,因为分析代码这种方式对于本问题并不适用,每次出现这个问题的时候"死循环"的位置都不一样,并且由于我们也没有浏览器的代码,所以高效且精准的方式应该是结合汇编和代码来一起看(如果可以的话可以确定下chromium的具体tag),如何得到热点汇编呢?应该通过perf找到热点ip寄存器的值(通常会有很多,可通过它们的值定位到具体函数的具体位置),然后再反编译对应模块的代码来分析,具体步骤如下:
sudo sysctl -w kernel.perf_event_max_sample_rate=4000 //调整下采样频率上限
sudo perf record -t 11104 -F 3999 //用3999的频率采样,这也是为了避免跟tick周期重复产生干扰
sudo perf script -F ip,dso,sym //获取ip地址和符号以及模块信息
理论上获取的ip会相对分散但一定会收敛到某个循环内,然而事实大相径庭:
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
aaaad1574000 v8::internal::Assembler::Assembler (/usr/share/browser/browser)
用户空间所有的ip都在同一地址0xaaaad1574000,而内核地址则是分散的:
ffff0000080818dc __softirqentry_text_start ([kernel.kallsyms])
ffff000008a2be80 inet_gro_receive ([kernel.kallsyms])
ffff00000860cc30 virtqueue_get_buf_ctx ([kernel.kallsyms])
赶紧看一下0xaaaad1574000处的指令到底是什么指令
ctyun@0000000g-3F3F8cwE6I:~/cong$ cat /proc/11104/maps | grep "/usr/share/browser/browser"
aaaacc30d000-aaaacf431000 r--p 00000000 fd:02 1714852 /usr/share/browser/browser
aaaacf431000-aaaad73c4000 r-xp 03124000 fd:02 1714852 /usr/share/browser/browser
aaaad73c4000-aaaad743e000 rw-p 0b0b7000 fd:02 1714852 /usr/share/browser/browser
aaaad743e000-aaaad7be2000 r--p 0b131000 fd:02 1714852 /usr/share/browser/browser
/usr/share/browser/browser的基地址为0xaaaacc30d000,得出偏移为0x5267000,反编译/usr/share/browser/browser并定位到0x5267000偏移处:
看到二进制中的指令为:
.text:0000000005267000 F4 4F 02 A9 STP X20, X19, [SP,#0x30+var_10]
指令的语义为:将寄存器X20和X19存到sp+0x20处,就是一个普通的保存上下文的操作,怎么会一直在执行这条指令呢,难道内存被篡改了?
写了一个获取进程内存的工具print_memory,用于获取指定进程指定地址指定大小的内存,并按字节输出:
./print_memory 11104 0xaaaad1573ff8 40
pid:11104
addr:0xaaaad1573ff8
len:40
FF 03 01 D1 F5 0B 00 F9 F4 4F 02 A9 FD 7B 03 A9 FD C3 00 91 68 2E 03 D0 08 61 42 F9 F3 03 00 AA 08 01 40 F9 E8 07 00 F9
0xaaaad1573ff8是函数头的地址,可以看到这片内存跟二进制中的一样,未被篡改,而根据指令的语义,执行完这条指令以后一定会继续执行下一条指令,这无法解释。于是提出了如下几点怀疑并分别进行了验证:
1,perf时间太短,刚被调度到准备执行又被perf的中断打断?
延长perf时间,perf结果数百万次,结果依旧
2,perf工具本身的结果就不可靠?
int main(){
while(1){
asm("nop");
}
return 0;
}
用上面的demo跑了一下,然后用同样的办法perf可以得到"相对正确"的结果,ip为:
......
400558
400558
400558
400558
400558
400554
400558
400558
400558
400558
400558
400558
400558
400558
400558
......
看下汇编:
0000000000400554 <main>:
400554: d503201f nop
400558: 17ffffff b 400554 <main>
40055c: d503201f nop
大部分都会是0x400558,只有少数0x400554,猜测应该跟cpu的pipeline有关,又改了下代码:
int main(){
while(1){
asm("nop");
asm("nop");
asm("nop");
asm("nop");
}
return 0;
}
再用同样的方式perf,得到的结果为:
400558
400558
400554
400554
400558
400558
400558
400554
400554
400558
400558
400558
400558
400558
400554
400558
......
400560
......
对应的汇编代码为:
0000000000400554 <main>:
400554: d503201f nop
400558: d503201f nop
40055c: d503201f nop
400560: d503201f nop
400564: 17fffffc b 400554 <main>
大部分是0x400554和0x400558,有少数0x400560,没有0x40055c,大概率是跟cpu pipeline有关,上述实验说明,perf的结果还是相对比较可靠的,如此小的循环也可以perf出相对正确的结果
3,内核的bug导致perf的结果出现假象?
有可能,但没有直接的证据,需要阅读perf的源码看下其原理,理论上其基于pmu中断,以此来采样结果相对还是可靠的,而且perf被大量使用,如果有bug早应该被修复了才对,所以看起来也不太像。如果是内核bug导致,那必然是在出问题的进程的内核路径上产生的问题
4,cpu的bug?
看起来不太像,首先程序如果进入内核态,perf的结果相对正常,只有在用户态会这样,理论上cpu的状态对于内核态和用户态区别应该不大,但也不是完全没有区别,至少有些寄存器会不一样,所以没有办法完全排除cpu的问题——最好是由华为的人来验证,最直接的方式就是拿硬件调试器看看到底cpu上正在执行什么指令
5,host/kvm的问题?
就在本文档的时候,其中一个线程(tid 11104)已经恢复了,所以用另外一个线程(tid 11110)来验证一下,这里首先得确认11110这个线程是不是同样的问题,也是用上述同样的方式,发现是同样的问题,只是ip不一样而已:
aaaad60d3fd4 blink::EventWithCallback::EventWithCallback (/usr/share/browser/browser)
.text:0000000009DC6FD4 FF 83 01 D1 SUB SP, SP, #0x60
连上host机器,找到对应的kvm进程,从host上看cpu占用和guest的情况基本一致:
host:
guest:
尝试从host上perf:
sudo perf kvm --host --guest record -t 3286693
发现跟guest上的结果一致,问题点均为0xaaaad60d3fd4,host和guest的的perf结果一致又增加了一个可疑点,如果是内核bug,那是host内核bug还是guest内核bug?这又得看看perf kvm的实现原理了,阶段性总结一下目前的怀疑点:
1,内核的bug(host或guest)
2,硬件问题
为了说服自己这不是一个应用程序的问题,于是用gdb看了下上下文:
跟二进制里面的依旧是一致的
索性直接continue,发现在另外一个地方又卡住了,然后再continue又恢复正常了。。。
综上
目前怀疑的点有两个:
1,内核bug——看perf代码,我方可进行
——review内核的改动和patch,需要uos方来进行,我方可参与
2,硬件问题——硬件调试器调试确认,需要华为协助,我方可参与