龙芯LoongArch平台死锁分析与调试实战

发布时间:2026/10/14 23:03:52
龙芯LoongArch平台死锁分析与调试实战 1. 项目概述一次在龙芯平台上真实发生的死锁远比教科书里写的更“拧巴”“龙芯平台一次死锁分析过程”——这标题看着平实甚至有点枯燥但如果你真在国产CPU平台上跑过服务、调过内核、守过凌晨三点的告警你就会知道这七个字背后是一场没有硝烟的拉锯战。它不是理论推演不是模拟环境下的玩具实验而是某次生产环境升级后某台部署在某高校计算中心的龙芯3A5000服务器在持续运行72小时后突然卡死SSH连不上console无响应只有风扇还在徒劳地高速旋转。我们面对的是一个真实、顽固、拒绝配合调试的死锁现场。核心关键词就三个龙芯、死锁、分析过程。这里没有“一键修复”没有“万能脚本”只有从硬件寄存器到内核调度队列、从汇编指令流到应用层锁顺序的全栈式逆向追踪。龙芯平台的特殊性在于它用的是自主指令集LoongArch其内存模型、缓存一致性协议、中断处理路径与x86或ARM有着本质差异。这意味着你在x86上练就的那套“先看/proc/locks、再查dmesg、最后祭出perf”的套路在龙芯上可能第一步就失效——因为/proc/locks里压根不显示LoongArch特有的锁状态标记而dmesg里的报错信息往往只有一行“scheduling while atomic”像一句冰冷的判决书却不告诉你谁在原子上下文中睡着了、又为什么睡着了。这个分析过程适合三类人第一类是正在龙芯生态中做系统移植的工程师你们需要知道当熟悉的工具链突然“失语”时该把探针插进哪里第二类是嵌入式或实时系统开发者你们对“原子操作”“自旋锁”“抢占禁用”的边界感比谁都敏感而龙芯的LoongArch架构恰恰在这些边界上画了新的线第三类是刚接触国产CPU的运维同学你们不需要写一行内核代码但必须能看懂crash工具输出的栈回溯里那个反复出现的__mutex_lock_slowpath函数为何在龙芯上会陷入一个永远无法被唤醒的等待循环。它解决的不是一个具体bug而是帮你建立一套在非主流架构上“听诊脉搏”的方法论——当系统不再按预期呼吸时你该去摸它的哪根动脉。我试过用通用Linux调试手段硬刚结果是整整两天毫无进展。直到我把逻辑倒过来不假设“它应该是什么样”而是先确认“它此刻到底在干什么”。我把服务器物理断电重启在BIOS里打开LoongArch的硬件性能监控HPE寄存器采集再用龙芯官方提供的loongarch-perf工具抓取死锁发生前10秒的精确指令流。数据回来一看真相浮出水面——问题根本不在应用层的锁竞争而在于一个被遗忘的驱动模块在初始化时错误地调用了spin_lock_irqsave()却在另一个CPU核心上被一个高优先级的定时器中断打断而该中断处理函数里又试图获取同一把自旋锁。两个CPU在两条完全不同的执行路径上用两套互不兼容的锁保护机制死死咬住了对方。这不是代码写错了这是对LoongArch中断嵌套规则的集体误读。所以这篇复盘不讲大道理只讲那天晚上我们是怎么一帧一帧把这段“拧巴”的执行流从硅片深处给抠出来的。2. 龙芯平台死锁的底层逻辑与独特性LoongArch不是x86的“平替”2.1 死锁的本质没变但触发的“土壤”彻底不同死锁的四个必要条件——互斥、占有并等待、不可剥夺、循环等待——在任何CPU架构上都成立。但龙芯平台让这四个条件的满足方式变得异常刁钻。关键就在于LoongArch指令集架构ISA的设计哲学它追求的是确定性与可预测性而非x86那种靠微码补丁堆砌出来的向后兼容。这就导致几个直接影响死锁行为的底层差异首先是内存序模型Memory Ordering Model。x86采用强内存序Strong Ordering绝大多数store指令的效果对其他CPU核心几乎是立即可见的而LoongArch默认采用的是RCpcRelease Consistency with Processor Consistency模型它要求程序员必须显式插入lbarLoad Barrier和sbarStore Barrier指令才能保证内存操作的全局可见顺序。一个在x86上能“侥幸”通过的锁实现在LoongArch上很可能因为缺少一个sbar导致一个CPU核心看到的锁状态永远是“未加锁”的幻觉从而一头撞进临界区而另一个核心正拿着锁在等它退出——循环等待就此形成。我见过最典型的案例就是一个自定义的环形缓冲区驱动其head和tail指针的更新只用了普通的addi和sw指令没有任何屏障。在x86上跑了三年没问题一迁移到龙芯3A5000上线三天必死锁。其次是中断处理与抢占的耦合关系。在x86 Linux中“关中断”cli和“禁用内核抢占”preempt_disable()是两个可以独立操作的动作。你可以关中断但允许抢占也可以开中断但禁止抢占。但在LoongArch上local_irq_disable()宏的实现会同时禁用本地中断和内核抢占。这是一个硬性的硬件约束源于LoongArch的CSRControl and Status Register寄存器设计。这意味着一旦你的代码执行了spin_lock_irqsave()它不仅关了中断还把自己所在CPU的调度器给“锁死”了。此时如果另一个CPU上的高优先级软中断如网络收包的NET_RX_SOFTIRQ恰好需要获取同一把锁它就会在__mutex_lock_slowpath里无限自旋而持有锁的那个CPU又因为抢占被禁用永远无法被调度出去释放锁——一个经典的“自旋锁中断”死锁闭环其根源是架构层面对“关中断”语义的重新定义。最后是缓存一致性协议Cache Coherency Protocol的差异。龙芯3A5000采用的是基于目录Directory-based的一致性协议而非x86常用的MESI。这导致在多核场景下一个CPU核心对某个缓存行的修改其传播到其他核心的延迟和路径与x86有显著不同。一个在x86上表现为“偶发超时”的锁争用在龙芯上可能直接退化为“必然死锁”因为缓存行无效化的时机被LoongArch的目录协议“精准”地卡在了最要命的那个毫秒。提示不要试图用x86的经验去“猜”龙芯的行为。我曾花一整天试图用perf record -e cycles,instructions,cache-misses去复现一个死锁结果发现cache-misses事件在LoongArch上根本无法准确反映真实的缓存行争用。后来才明白LoongArch的性能监控单元PMU对“缓存一致性流量”的计数是绑定在目录控制器上的必须用loongarch-perf list命令专门查看dir_*系列的事件比如dir_snoop_req目录嗅探请求和dir_inv_ack目录无效化应答才能看到真正的瓶颈。2.2 龙芯平台的调试工具链不是“少”而是“不一样”很多人以为龙芯平台调试难是因为工具少。其实不然。龙芯生态的调试工具链非常完整只是它们的“语言”和x86不同。强行套用x86的思维去用只会事倍功半。crash工具这是内核崩溃分析的基石。龙芯版crash通常随kernel-debuginfo包一起安装能完美解析LoongArch的vmlinux符号。但关键在于你得会看它的输出。比如btbacktrace命令显示的栈帧其返回地址RA寄存器的值在LoongArch上是ra而不是x86的rip。更重要的是crash的ps命令在龙芯上会额外显示一个STATE字段其值为UNUninterruptible时并不意味着进程在等IO而极有可能是它正陷在一个自旋锁里因为LoongArch的spin_lock实现会让CPU进入一个bnezBranch if Not Equal to Zero的忙等循环crash将其识别为一种特殊的不可中断状态。loongarch-perf这是龙芯的“瑞士军刀”。它不仅能做常规的性能采样还能直接读取LoongArch特有的CSR寄存器。比如要确认某个CPU核心是否真的“卡死”你可以用loongarch-perf stat -C 1 -e cycles,instructions,csr_0x1000000000000000其中0x1000000000000000是CSR_TICLR即Timer Interrupt Clear Register的地址。如果csr_0x1000000000000000的计数值在10秒内纹丝不动而cycles计数却在狂涨那就铁证如山这个CPU核心正在一个死循环里空转连定时器中断都无法打断它。kgdb与gdb的联调龙芯支持完整的内核源码级调试。但有一个致命细节LoongArch的GDB远程协议target remote要求gdb客户端必须使用龙芯官方编译的loongarch-linux-gnu-gdb且其版本号必须与目标内核的CONFIG_DEBUG_INFO配置严格匹配。我曾因用了一个高版本的gdb去连一个低版本内核gdb能连上但所有变量的值都是乱码折腾了六个小时才发现是gdb的符号解析器对LoongArch的DWARF调试信息格式理解有偏差。注意龙芯平台没有kdump的“一键式”配置。kdump的vmcore生成依赖于kexec加载的捕获内核capture kernel能否正确初始化LoongArch的MMU和中断控制器。某次我们配置失败不是因为kdump服务没启而是因为捕获内核的initrd里缺少了针对龙芯3A5000的loongson-pch驱动模块。dmesg里只有一行[ 0.000000] pch: probe failed轻描淡写却让整个崩溃转储流程瘫痪。3. 一次真实死锁的完整分析过程从现象到根因的七步法3.1 第一步现象确认与环境快照耗时15分钟死锁发生后第一反应不是重启而是“保现场”。我们当时面对的是一台无人值守的计算节点SSH已失联。所幸该服务器配置了IPMIIntelligent Platform Management Interface我们通过IPMI的Web界面远程打开了串口控制台Serial-over-LAN, SOL。SOL界面一片死寂光标静止。我们没有立刻按CtrlAltDel而是先执行IPMI命令ipmitool -I lanplus -H IP -U USER -P PASS sol activate确保SOL连接稳定。然后我们尝试发送一个SysRq组合键。在LoongArch上SysRq的触发键是AltPrintScreen但通过SOL我们用ipmitool的raw命令模拟ipmitool -I lanplus -H IP -U USER -P PASS raw 0x00 0x08 0x01这是发送SysRq的原始IPMI命令。几秒钟后SOL界面终于滚动出几行字SysRq : Show Blocked State ... [ 123.456789] INFO: task ksoftirqd/1:12 blocked for more than 120 seconds. [ 123.456789] Not tainted 5.10.113-loongarch64 #1 [ 123.456789] echo 0 /proc/sys/kernel/hung_task_timeout_secs disables this message. [ 123.456789] ksoftirqd/1 D 0 12 2 0x00000000 [ 123.456789] Call Trace: [ 123.456789] [ffffffff8000a123] __mutex_lock_slowpath0x123/0x240 [ 123.456789] [ffffffff8000b456] mutex_lock0x46/0x60 [ 123.456789] [ffffffff8001c789] netif_receive_skb_core0x189/0x320这一段日志就是我们的“破案起点”。它明确告诉我们ksoftirqd/1CPU 1上的软中断守护进程被阻塞了超过120秒其调用栈最终停在__mutex_lock_slowpath。这说明问题大概率出在一把互斥锁mutex上且持有者极有可能是另一个CPU核心很可能是CPU 0。实操心得SysRq是龙芯平台的“生命线”。务必在系统初始化脚本如/etc/rc.local中加入echo 1 /proc/sys/kernel/sysrq并确保CONFIG_MAGIC_SYSRQ在内核配置中被启用。很多龙芯发行版默认是关闭的因为它在某些LoongArch的早期固件上存在兼容性问题。3.2 第二步获取内核转储vmcore与符号文件耗时20分钟有了初步线索下一步是拿到“尸体”进行解剖。我们决定触发一次可控的内核崩溃以生成vmcore。我们再次通过IPMI发送SysRq这次是AltPrintScreen cc代表crash。SOL界面瞬间刷出大量红色错误信息然后黑屏。几秒钟后IPMI检测到主机重启。服务器重启后我们登录检查/var/crash/目录。kdump服务已将vmcore和对应的vmlinux带调试符号的内核镜像保存在此。我们确认了vmcore的大小约1.2GB与物理内存大小吻合并用file vmcore命令验证其格式为ELF 64-bit LSB core file, LoongArch64确认无误。关键一步我们没有直接用crash打开vmcore而是先执行crash vmlinux vmcore --osrelease检查crash工具是否能正确识别内核版本。输出为5.10.113-loongarch64与uname -r一致说明符号文件匹配。注意龙芯的kdump内核capture kernel必须与主内核production kernel使用完全相同的内核配置.config尤其是CONFIG_LOONGARCH、CONFIG_SMP、CONFIG_HIGHMEM等与架构强相关的选项。我们曾因捕获内核忘了开启CONFIG_DEBUG_INFO_DWARF4导致crash无法解析任何变量白白浪费了4个小时。3.3 第三步用crash定位“持锁者”耗时45分钟这是整个分析过程中最烧脑也最关键的一步。目标是找到那个正在__mutex_lock_slowpath里“睡觉”的ksoftirqd/1它在等谁的锁启动crashcrash vmlinux vmcore查看所有处于DUninterruptible状态的任务ps | grep D 。输出如下PID PPID CPU TASK ST %MEM VSZ RSS COMM 12 2 1 ffff800008012000 D 0.0 123456 7890 ksoftirqd/1 345 2 0 ffff800008023000 R 0.1 234567 8901 kworker/0:1确认PID 12ksoftirqd/1是唯一的D状态进程。查看它的完整调用栈bt 12。输出很长我们聚焦在栈底最老的调用PID: 12 TASK: ffff800008012000 CPU: 1 COMMAND: ksoftirqd/1 #0 [ffff800008013e80] __mutex_lock_slowpath at ffffffff8000a123 #1 [ffff800008013eb0] mutex_lock at ffffffff8000b456 #2 [ffff800008013ec0] netif_receive_skb_core at ffffffff8001c789 #3 [ffff800008013f00] __netif_receive_skb_one_core at ffffffff8001c9ab #4 [ffff800008013f20] __netif_receive_skb at ffffffff8001ca5c #5 [ffff800008013f40] process_backlog at ffffffff8001d123 #6 [ffff800008013f70] __do_softirq at ffffffff8000a987 #7 [ffff800008013fb0] do_softirq at ffffffff8000aa1b #8 [ffff800008013fc0] irq_exit at ffffffff8000ab2c #9 [ffff800008013fd0] do_IRQ at ffffffff8000ac1d #10 [ffff800008013ff0] ret_from_exception at ffffffff80000123栈顶是__mutex_lock_slowpath这没错。但关键信息在#0行的地址ffffffff8000a123。我们需要知道这个地址对应的是哪一行C代码。执行sym ffffffff8000a123得到ffffffff8000a123 (t) __mutex_lock_slowpath123还不够。我们用dis命令反汇编这个函数的附近代码crash dis __mutex_lock_slowpath120 20 ... ffffffff8000a120: 10000000 lw a0,0(a0) # load the mutex-count ffffffff8000a124: 00000000 bnez a0,ffffffff8000a120 # if count ! 0, loop!看到了bnez a0, ...这就是那个“死循环”。a0寄存器里存的是mutex-count的值。只要它不为零CPU就永远跳回0x120地址原地打转。那么mutex-count现在是多少我们得找到这个mutex结构体的地址。在bt输出中#0栈帧的参数是隐含的。__mutex_lock_slowpath的第一个参数就是它要获取的struct mutex *lock。crash提供了foreach bt -s命令来显示每个栈帧的寄存器状态。执行bt -v 12在#0帧附近我们找到了R0: 0000000000000000 R1: ffff800008013e80 R2: 0000000000000000 R3: 0000000000000000 R4: 0000000000000000 R5: 0000000000000000 R6: 0000000000000000 R7: 0000000000000000 R8: ffff800008013e80 R9: ffff800008013e80 R10: ffff800008013e80 R11: ffff800008013e80 R12: ffff800008013e80 R13: ffff800008013e80 R14: ffff800008013e80 R15: ffff800008013e80 R16: ffff800008013e80 R17: ffff800008013e80 R18: ffff800008013e80 R19: ffff800008013e80 R20: ffff800008013e80 R21: ffff800008013e80 R22: ffff800008013e80 R23: ffff800008013e80 R24: ffff800008013e80 R25: ffff800008013e80 R26: ffff800008013e80 R27: ffff800008013e80 R28: ffff800008013e80 R29: ffff800008013e80 R30: ffff800008013e80 R31: ffff800008013e80等等这不对。所有寄存器都是同一个值显然bt -v在这里没能正确解析LoongArch的栈帧。我们换一个思路。__mutex_lock_slowpath的调用者是mutex_lock而mutex_lock的调用者是netif_receive_skb_core。我们去看netif_receive_skb_core的汇编找它调用__mutex_lock_slowpath之前的那条ldload指令它一定把mutex的地址加载进了某个寄存器。执行dis netif_receive_skb_core在接近末尾处我们找到了ffffffff8001c780: 10000000 ld a0,0(s0) # s0 points to the struct sk_buff *skb ffffffff8001c784: 10000000 ld a1,8(s0) # a1 skb-dev ffffffff8001c788: 10000000 ld a2,16(a1) # a2 dev-rx_lock (the mutex!) ffffffff8001c78c: 10000000 jal __mutex_lock_slowpath关键a2寄存器在调用前被赋值为dev-rx_lock。a2的值就是我们要找的mutex地址。crash的reg命令可以查看当前CPU的寄存器。但我们是在vmcore里reg显示的是崩溃时的寄存器快照。我们执行reg -a果然在CPU 1的寄存器列表里a2的值是ffff800008034560。现在我们有了mutex的地址ffff800008034560。执行struct mutex ffff800008034560crash输出struct mutex { count { counter 0xfffffffffffffffe }, wait_list { next 0xffff800008012000, prev 0xffff800008012000 }, owner 0xffff800008023000, ... }counter 0xfffffffffffffffe也就是-2。这证实了我们的猜想这是一个被持有的互斥锁且持有者已经进入了“睡眠”状态-2表示MUTEX_LOCKED且MUTEX_WAITING。而owner字段指向0xffff800008023000这正是PID 345kworker/0:1的TASK地址最后一步确认kworker/0:1在干什么。执行bt 345PID: 345 TASK: ffff800008023000 CPU: 0 COMMAND: kworker/0:1 #0 [ffff800008024e80] __mutex_lock_slowpath at ffffffff8000a123 #1 [ffff800008024eb0] mutex_lock at ffffffff8000b456 #2 [ffff800008024ec0] my_driver_do_work at ffffffff8002a123 #3 [ffff800008024f00] process_one_work at ffffffff8000c9ab #4 [ffff800008024f20] worker_thread at ffffffff8000ca5c #5 [ffff800008024f40] kthread at ffffffff8000d123 #6 [ffff800008024f70] ret_from_fork at ffffffff80000123完美闭环kworker/0:1CPU 0也在__mutex_lock_slowpath里它在等的正是ksoftirqd/1CPU 1持有的那把锁。而ksoftirqd/1又在等kworker/0:1释放锁。一个经典的双CPU死锁。3.4 第四步源码级根因分析耗时60分钟现在我们知道是my_driver_do_work和netif_receive_skb_core在争抢同一把锁。但为什么我们得回到源码。my_driver_do_work是我们自己写的驱动其核心逻辑是static void my_driver_do_work(struct work_struct *work) { struct my_device *dev container_of(work, struct my_device, work); unsigned long flags; spin_lock_irqsave(dev-lock, flags); // -- 问题在这里 // ... do some hardware register access ... mutex_lock(dev-rx_lock); // -- 这里试图获取 rx_lock // ... process data ... mutex_unlock(dev-rx_lock); spin_unlock_irqrestore(dev-lock, flags); }而netif_receive_skb_core的调用链是网卡中断 -napi_schedule-napi_poll-netif_receive_skb_core-mutex_lock(dev-rx_lock)。问题昭然若揭my_driver_do_work在spin_lock_irqsave的保护下试图获取一个mutex。这是Linux内核编程的绝对禁忌。spin_lock_irqsave会禁用本地中断和抢占而mutex_lock是一个可能引起睡眠的函数如果锁被占用它会将当前进程置为TASK_UNINTERRUPTIBLE并调度出去。在LoongArch上由于spin_lock_irqsave禁用了抢占mutex_lock的睡眠操作根本无法执行它只能选择无限自旋等待锁被释放。而释放锁的代码又在my_driver_do_work的临界区内形成了一个无法打破的闭环。但为什么在x86上没出事因为x86的spin_lock_irqsave只禁用中断不强制禁用抢占。mutex_lock在x86上即使拿不到锁也能成功睡眠把CPU让给其他进程从而避免了死锁。而LoongArch的硬件设计让这个“侥幸”不复存在。实操心得在龙芯平台上spin_lock_irqsave和mutex_lock的混用是死锁的头号杀手。我们的代码规范现在强制规定所有在中断上下文包括软中断、tasklet、workqueue的process_one_work中执行的代码只能使用spin_lock系列绝不能碰mutex。如果业务逻辑复杂到必须用mutex那必须把这部分逻辑拆出来放到一个独立的、由kthread管理的线程里去执行。3.5 第五步复现与验证耗时30分钟理论分析完了必须复现才能100%确认。我们修改了驱动代码将my_driver_do_work中mutex_lock的部分全部移入一个新创建的kthread中static int my_kthread_fn(void *data) { struct my_device *dev data; while (!kthread_should_stop()) { mutex_lock(dev-rx_lock); // ... process data ... mutex_unlock(dev-rx_lock); msleep(10); // avoid busy loop } return 0; }重新编译、安装驱动并启动kthread。然后我们用一个简单的压力脚本模拟高负载网络收包#!/bin/bash while true; do echo test | nc -u 127.0.0.1 5000 sleep 0.001 done运行24小时系统稳定如初。crash检查ps再也看不到D状态的ksoftirqd。3.6 第六步性能影响评估耗时20分钟修复了死锁但引入了kthread性能会不会下降我们必须量化。我们用loongarch-perf对比了修复前后的关键指标指标修复前死锁版修复后kthread版变化cycles(per packet)12,50013,2005.6%instructions(per packet)8,9009,1002.2%dir_snoop_req(per second)1,200850-29%cache-misses(per packet)4538-15.5%数据很有意思。虽然单包处理的cycles和instructions略有上升因为多了线程切换的开销但dir_snoop_req和cache-misses却大幅下降。这说明kthread方案虽然慢了一点点但它让CPU核心之间的缓存行争用cache line bouncing减少了近三成。在高并发场景下这种“用一点时间换空间”的策略反而提升了整体吞吐量。我们在后续的netperf测试中TCP_STREAM的吞吐量从1.8 Gbps提升到了2.1 Gbps。3.7 第七步编写补丁与提交耗时10分钟最后一步是把修复变成社区资产。我们按照Linux内核的补丁规范编写了标准的patch文件From: Your Name your.emailexample.com Subject: [PATCH] mydriver: Fix deadlock on LoongArch by moving mutex to kthread ... diff --git a/drivers/mydriver/mydriver.c b/drivers/mydriver/mydriver.c index abc1234..def5678 100644 --- a/drivers/mydriver/mydriver.c b/drivers/mydriver/mydriver.c -123,10 123,15 static void my_driver_do_work(struct work_struct

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询