公司的某款PCIE加速卡产品,一直在各类基于x86 CPU的台式机/服务器上都运行良好;前一段时间,有客户将该款加速卡产品应用到了某款鲲鹏服务器上,在进行性能压测时出现异常,系统日志中大量出现这两类告警信息:“list_del corruption, ffffa02030de1868->next is LIST_POISON1 (dead000000000100)”,“list_add double add: new=ffffa02030de1868, prev=ffffa02030de1868, next=ffffa020306a0610”。
考虑到该问题只出现在鲲鹏服务器,推测和鲲鹏处理器的特定差异性(与x86 CPU相比)有一定关系,这里笔者首先从鲲鹏服务器的架构差异性等相关背景知识开始逐步介绍定位过程。
1 背景知识
鲲鹏处理器是华为公司自主研发的一系列高性能服务器处理器,基于ARM64架构,特别是ARM v8架构进行研发,并在此基础上进行了优化和创新,以满足数据中心等高性能计算场景的需求。同时,华为在鲲鹏服务器上对 Linux 内核的编译进行了优化,以确保内核能够充分发挥鲲鹏处理器的性能优势。
1.1 ARM64架构与弱一致性内存模型
ARM64 架构采用了一种称为“弱一致性内存模型”(Weakly-Ordered Memory Model)的内存模型。这种模型允许处理器和编译器在一定程度上重新排序内存操作,以提高性能。
弱一致性内存模型允许以下几种类型的内存操作重新排序:
1)写-写重排序:两个写操作可以重新排序。
2)读-读重排序:两个读操作可以重新排序。
3)写-读重排序:写操作可以在读操作之前执行,但读操作不能在写操作之前执行。
在这种情况下,程序员需要使用内存屏障(Memory Barriers)来显式地控制内存操作的顺序,以确保程序的正确性。
进一步阅读参考建议:
《内存一致性问题—内存屏障》
1.2 编译器优化导致的乱序访问
编译器在优化代码时,为了提高执行效率,可能会对指令进行重新排序。这种重排在不改变程序单线程执行结果的前提下,通过调整指令顺序来更好地利用处理器的流水线、缓存等资源。然而,在多线程或并发执行环境中,这种指令重排可能会导致内存访问顺序与程序员的预期不一致,从而引发问题。
1.3 鲲鹏服务器linux内核的编译优化措施
鲲鹏服务器的linux内核在编译时,通常会设置较高的GCC编译器优化级别,如-O2或-O3,以提升代码的执行效率。这些优化级别会对代码进行更多的分析和重排,从而生成更高效的机器码。
此外,还会使用一些特定的编译器选项来进一步提升代码的执行效率,如-fomit-frame-pointer、-march、-mtune等。这些选项能够针对特定的处理器架构进行优化,生成更高效的机器码。
2 原因分析
2.1 系统告警日志
系统压测中message告警日志片段如下:
[2240.087542] list_del corruption, ffffa02030de1868->next is LIST_POISON1 (dead000000000100)
[2240.095779] WARNING: CPU: 25 PID: 2074 at lib/list_debug.c:47 __list_del_entry_valid+0x60/0xc8
[2240.160279]CPU:25 PID:2074 Comm: Kdump: loaded Tainted: G W OE 4.19.90-0.240102.uos.aarch64 #1
[ 2240.171094] Hardware name: PowerLeader PR210K/BC82AMDQ, BIOS 7.15 06/20/2024
[ 2240.178109] pstate: 40c00009 (nZcv daif +PAN +UAO)
[ 2240.182876] pc : __list_del_entry_valid+0x60/0xc8
[ 2240.187558] lr : __list_del_entry_valid+0x60/0xc8
[ 2240.192239] sp : ffffa020314f7cf0
… … … …(省略无关信息)
[ 2240.274844] Call trace:
[ 2240.277281] __list_del_entry_valid+0x60/0xc8
[ 2240.281619] xxDataLinkGet+0x74/0x118 [xxdrv](从空闲队列分配资源节点)
[ 2240.286130] xx_InstMsgProc+0x98/0x380 [xxdrv]
[ 2240.290642] xx_rcv_msg+0xd4/0x480 [xxdrv]
[ 2240.294894] xx_pollTskProc+0x798/0x10d8 [xxdrv]
[ 2240.299575] kthread+0x134/0x138
[ 2240.302788] ret_from_fork+0x10/0x18
… … … …(省略无关信息)
[2252.155899] list_add double add: new=ffffa02030de1868, prev=ffffa02030de1868, next=ffffa020306a0610.
[2252.165001] WARNING: CPU: 25 PID: 2074 at lib/list_debug.c:31 __list_add_valid+0x9c/0xa8
[ 2252.228981] CPU: 25 PID: 2074 Comm: Kdump: loaded Tainted: G W OE 4.19.90-0.240102.uos.aarch64 #1
[ 2252.239798] Hardware name: PowerLeader PR210K/BC82AMDQ, BIOS 7.15 06/20/2024
[ 2252.246813] pstate: 40c00009 (nZcv daif +PAN +UAO)
[ 2252.251580] pc : __list_add_valid+0x9c/0xa8
[ 2252.255745] lr : __list_add_valid+0x9c/0xa8
[ 2252.259909] sp : ffffa020314f7d00
.. … … …(省略无关信息)
[ 2252.342512] Call trace:
[ 2252.344948] __list_add_valid+0x9c/0xa8
[ 2252.348768] xxDataLinkPut+0x64/0xb8 [xxdrv](释放资源节点到空闲队列)
[ 2252.353194] xxDataSemLnkPut+0x28/0x48 [xxdrv]
[ 2252.357793] xxDataSemLnkPutEx+0x70/0x128 [xxdrv]
[ 2252.362650] xx_pollTskProc+0x31c/0x10d8 [xxdrv]
[ 2252.367331] kthread+0x134/0x138
[ 2252.370546] ret_from_fork+0x10/0x18
… … … …(省略无关信息)
2.2 问题调用栈分析
从系统日志看,两类异常告警均出现在加速卡驱动xxdrv的同一调度线程xx_pollTskProc中,其中:
1)调用链1:xx_pollTskProc=>(…)xxDataLinkGet=>__list_del_entry_valid,对应调度线程的加速计算请求入口和(list队列)资源节点分配流程,“list_del corruption, ffffa02030de1868->next is LIST_POISON1 (dead000000000100)”告警表示从该list队列分配的节点next值异常为空,即该节点出现重复分配问题。
2)调用链2:xx_pollTskProc=>(…)xxDataLinkPut=>__list_add_valid,对应调度线程的加速计算结果响应和(list队列)资源节点释放流程,“list_add double add: new=ffffa02030de1868, prev=ffffa02030de1868, next=ffffa020306a0610”告警表示释放回该list队列的new节点与队列中之前已经释放的prev为同一节点,即出现节点重复释放问题。
补充说明:驱动程序中,调度线程xx_pollTskProc使用同一个list队列来管理(分配/释放)用于记录加速卡计算请求/响应上下文信息的节点资源,每个资源节点与加速卡上的计算节点一一对应,上述1/2的调用链告警信息意味着该list队列资源节点的分配与释放操作过程中出现了重复分配和重复释放异常问题;由于该list队列的节点管理是在同一个调度线程中进行管理,似乎与CPU的多核心多线程同步调用机制问题无关,问题显得非常费解。
补充参考:linux内核源码中“ __list_add_valid”和“__list_del_entry_valid”函数的相关代码片段:
/*
* Check that the data structures for the list manipulations are reasonably
* valid. Failures here indicate memory corruption (and possibly an exploit
* attempt).
*/
bool __list_add_valid(struct list_head *new, struct list_head *prev,
struct list_head *next)
{
if (CHECK_DATA_CORRUPTION(prev == NULL,
"list_add corruption. prev is NULL.\\n") ||
CHECK_DATA_CORRUPTION(next == NULL,
"list_add corruption. next is NULL.\\n") ||
CHECK_DATA_CORRUPTION(next->prev != prev,
"list_add corruption. next->prev should be prev (%px), but was %px. (next=%px).\\n",
prev, next->prev, next) ||
CHECK_DATA_CORRUPTION(prev->next != next,
"list_add corruption. prev->next should be next (%px), but was %px. (prev=%px).\\n",
next, prev->next, prev) ||
CHECK_DATA_CORRUPTION(new == prev || new == next,
"list_add double add: new=%px, prev=%px, next=%px.\\n",
new, prev, next))
return false;
return true;
}
EXPORT_SYMBOL(__list_add_valid);
bool __list_del_entry_valid(struct list_head *entry)
{
struct list_head *prev, *next;
prev = entry->prev;
next = entry->next;
if (CHECK_DATA_CORRUPTION(next == NULL,
"list_del corruption, %px->next is NULL\\n", entry) ||
CHECK_DATA_CORRUPTION(prev == NULL,
"list_del corruption, %px->prev is NULL\\n", entry) ||
CHECK_DATA_CORRUPTION(next == LIST_POISON1,
"list_del corruption, %px->next is LIST_POISON1 (%px)\\n",
entry, LIST_POISON1) ||
CHECK_DATA_CORRUPTION(prev == LIST_POISON2,
"list_del corruption, %px->prev is LIST_POISON2 (%px)\\n",
entry, LIST_POISON2) ||
CHECK_DATA_CORRUPTION(prev->next != entry,
"list_del corruption. prev->next should be %px, but was %px\\n",
entry, prev->next) ||
CHECK_DATA_CORRUPTION(next->prev != entry,
"list_del corruption. next->prev should be %px, but was %px\\n",
entry, next->prev))
return false;
return true;
}
EXPORT_SYMBOL(__list_del_entry_valid);
2.3原因追踪
从前述2.2的异常调用栈分析来看,由于是在同一个调度线程中进行对同一list资源队列进行分配释放管理,问题应该与CPU的多核心多线程同步调用机制无关;而同一驱动程序在x86系统上又能正常工作,表明代码逻辑也问题无关。
于是,问题分析的重点开始落到鲲鹏CPU架构的差异性上,特别是“背景知识”部分提到的ARM64架构的弱一致性内存机制和内核编译器优化可能导致的指令乱序执行问题上。
接着,开始反复分析与list队列资源节点操作,特别是驱动中与加速卡计算节点进行同步调度的函数代码,查看是否存在因指令乱序执行导致资源节点乱序的问题。
功夫不费有心人,对疑似可能乱序的函数代码进行经过多次尝试(分析/修改/测试),并对修订后的代码进行长时间压测验证,最终确认了问题原因。
3 问题分析与修订
这里直接分享问题相关点代码片段(省去无关细节)的问题分析和修订过程。
3.1 加速计算请求函数xx_doMsgReq
原因分析:xx_doMsgReq函数管理计算请求节点队列ring0,每发送一个加速计算请求报文到加速卡,会对该请求循环队列ring0进行移位(ring->tail++),同时同步设置加速卡的对应寄存器(调用tx_ring_start),问题就出在ring0队列移位指令与加速卡寄存器操作指令之间(在鲲鹏CPU上)可能存在乱序,一旦出现乱序,驱动层的资源节点与加速卡上的计算节点就会出现错位,再加上双方的循环队列会周期性回转( ring->block_num=>0),请求节点和响应节点(参见2.3.2)就会因队列错位而出现资源节点的重复读取与释放,对应出现2.2部分的告警信息。
修正方法:在上述可能出现乱序的指令之间增加内存屏蔽指令(mb),防止编译器/CPU的乱序并行优化/执行。
int xx_doMsgReq(…)
{
.. … … …(省略无关代码)
req = (void *)(ring->virt_addr + ring->tail * ring->block_size);
.. … … …(省略无关代码)
ring->tail++; //请求循环队列ring0
if(ring->tail >= ring->block_num) ring->tail=0;
mb(); //修订:新增内存屏蔽指令
tx_ring_start(…, ring);
.. … … …(省略无关信息)
}
int tx_ring_start(…)
{
.. … … …(省略无关代码)
RSP_REG_WRITE(…, ring->block_size * ring->tail);
.. … … …(省略无关代码)
}
3.2 加速卡计算结果处理函数xx_instan_read
原因分析:xx_instan_read函数会周期性地检查和读取加速卡的计算响应报文队列ring1,一旦读到某个计算响应结果,会对响应循环队列ring1进行移位(ring->head++),同时同步设置加速卡的对应寄存器(调用ring_head_reg),同理,一旦ring1循环队列移位指令与加速卡寄存器操作的指令之间出现乱序,请求节点(参见2.3.1)和响应节点就会出现错位和重复读取与释放,对应出现2.2部分的告警信息。
修正方法:类似3.1。
int xx_instan_read(…)
{
.. … … …(省略无关代码)
ring->head++;//响应循环队列ring1
if(ring->head >= ring->block_num) ring->head=0;
mb(); //修订:新增内存屏蔽指令
ring_head_reg(…, ring->block_size * ring->head);
.. … … …(省略无关代码)
}
4 问题总结
笔者后来还尝试将该PCIE加速卡和修订前的驱动部署在纯ARM64架构的RK35XX主板上进行压力测试验证,并没有重现问题,也就是说该问题只在鲲鹏服务器上出现。最后对整个问题定位过程要点总结一下:
评论前必须登录!
注册