现象
1. 系统卡死现象
启动 soft_lock 组件之后,运行一段时间,ssh 将会断开,虚拟机 VNC console 按键没有响应。这个时间不定,有时候一启动就卡住,有时候正常运行几分钟后才出现问题。
2. 组件运行状态
在 soft_lock 组件和内核都在正常运行的时候,trace 日志还是能够正常打印,说明 eBPF 程序本身功能正常,问题出现在特定的执行路径上。
3. 性能分析工具异常
使用
perf top
抓取内核事件的时候,内核卡住时会增加 perf 进程,如图所示,这表明性能分析工具也受到了死锁的影响。
4. 调试难点
由于内核卡住时,perf 也无法采集内核火焰图,这对故障的原因无法深入排查,增加了问题定位的难度。同时,dmesg
以及 /var/log/messages
等日志文件也无法正常打印,无法通过日志分析问题。
环境
内核版本
# nkvers
############## Kylin Linux Version #################
Release:
Kylin Linux Advanced Server release V10 (Lance)
Kernel:
4.19.90-52.22.v2207.ky10.aarch64
Build:
Kylin Linux Advanced Server
release V10 (SP3) /(Lance)-aarch64-Build23/20230324
#################################################
eBPF 代码
内核态代码
SEC("kprobe/watchdog_timer_fn")
int trace_watchdog_timer(struct pt_regs *regs)
{
// ...
return 0;
}
SEC("kprobe/queued_spin_lock_slowpath")
int trace_native_queued_spin_lock_slowpath(struct pt_regs *regs)
{
// ...
return 0;
}
SEC("kretprobe/queued_spin_lock_slowpath")
int trace_native_queued_spin_lock_slowpath_ret(struct pt_regs *regs)
{
// ...
return 0;
}
具体代码参考 beepfd/core 项目的 soft_lock.c,当前内核态代码主要用于软锁死检测和内核调度性能监控,具体功能包括:
-
看门狗定时器监控 (
trace_watchdog_timer
)- 监控内核看门狗定时器函数
watchdog_timer_fn
的调用 - 用于检测系统是否出现软锁死(soft lockup)情况
- 当CPU长时间无法响应中断或调度时,看门狗会触发告警
- 监控内核看门狗定时器函数
-
自旋锁慢路径跟踪 (
trace_native_queued_spin_lock_slowpath
)- 监控自旋锁的慢路径获取过程
queued_spin_lock_slowpath
- 跟踪锁竞争激烈导致进入慢路径的情况
- 收集锁等待时间、竞争频率等性能指标
- 监控自旋锁的慢路径获取过程
-
自旋锁慢路径返回跟踪 (
trace_native_queued_spin_lock_slowpath_ret
)- 监控自旋锁慢路径函数的返回
- 计算锁持有时间和等待时间
- 分析锁的获取效率和系统瓶颈
自旋锁慢路径挂载点说明
在 Linux 内核中,根据不同的编译配置和运行环境,queued_spin_lock_slowpath
会有不同的实现:
-
queued_spin_lock_slowpath
- 通用的排队自旋锁慢路径入口函数
- 作为统一的函数名称,在不同配置下会指向不同的具体实现
- 是 eBPF kprobe 最常用的挂载点
-
native_queued_spin_lock_slowpath
- 原生(非虚拟化)环境下的排队自旋锁慢路径实现
- 在物理机或者支持硬件加速的虚拟化环境中使用
- 直接使用硬件的原子操作和内存屏障
- 性能最优,但不适用于半虚拟化环境
-
__pv_queued_spin_lock_slowpath
- 半虚拟化(Paravirtualized)环境下的排队自旋锁慢路径实现
- 在 Xen、KVM 等虚拟化环境中使用,通过 hypercall 与虚拟化层交互
- 可以避免在虚拟化环境中无效的自旋等待,提高整体性能
- 当内核配置
CONFIG_PARAVIRT_SPINLOCKS=y
时启用
复现
不同内核版本和模块配置测试结果
内核版本 | 模块 | 是否hang |
---|---|---|
6.8 | kprobe/watchdog_timer_fn、kprobe/__pv_queued_spin_lock_slowpath、kretprobe/__pv_queued_spin_lock_slowpath | 未hang |
4.19 | kprobe/watchdog_timer_fn、kprobe/queued_spin_lock_slowpath、kretprobe/queued_spin_lock_slowpath | hang |
4.19 | kprobe/watchdog_timer_fn | hang |
4.19 | kprobe/watchdog_timer_fn、kprobe/queued_spin_lock_slowpath | hang |
从测试结果可以看出:
- 内核版本差异:6.8 内核未出现 hang 问题,说明新版本内核可能已经修复了相关的死锁问题
- 模块影响:在 4.19 内核中,只要包含
kretprobe/queued_spin_lock_slowpath
,就会导致系统 hang - 关键模块定位:
kretprobe/queued_spin_lock_slowpath
是导致死锁的关键模块 - 安全模块:
kprobe/watchdog_timer_fn
单独运行时不会引起问题
这进一步确认了问题的根源在于 kretprobe 对 queued_spin_lock_slowpath
的探测机制存在缺陷。
分析
工具选择
kdump
kdump 是 Linux 内核的崩溃转储机制,通过配置适当的 sysctl 参数可以在系统出现死锁、软锁定、硬锁定等问题时自动生成 crash dump 文件。
关键 sysctl 参数配置
参数 | 作用 | 推荐值 | 说明 |
---|---|---|---|
kernel.sysrq | 启用魔术系统请求键 | 1 | 允许通过 Alt+SysRq+键 执行紧急操作 |
kernel.watchdog_thresh | 看门狗超时阈值(秒) | 10 | 检测系统无响应的时间限制 |
kernel.hung_task_panic | 挂起任务时是否 panic | 1 | 检测到任务长时间挂起时触发 panic |
kernel.perf_event_paranoid | perf 事件权限级别 | -1 | 允许非特权用户访问所有 perf 功能 |
kernel.kptr_restrict | 内核指针访问限制 | 0 | 允许读取内核指针,便于调试 |
kernel.softlockup_panic | 软锁定时是否 panic | 1 | 检测到 CPU 长时间无法调度时触发 |
kernel.panic_on_oops | 内核错误时是否 panic | 1 | 任何内核 oops 都触发 panic |
kernel.panic | panic 后重启延迟 | 10 | panic 后等待时间(秒) |
kernel.hardlockup_panic | 硬锁定时是否 panic | 1 | 检测到硬件级锁定时触发 |
针对 kretprobe 死锁问题的特殊配置
对于当前的 kretprobe 死锁问题,建议额外配置:
# 降低软锁定检测阈值,更快触发 dump
kernel.watchdog_thresh=5
# 启用更详细的调试信息
kernel.printk=8 4 1 7
基本信息
- 故障类型:软锁定(soft lockup)
- 受影响 CPU:CPU#0 卡死 11 秒
- 故障进程:sshd (PID: 18041)
- 内核版本:4.19.90-52.42.v2207.ky10.aarch64
故障调用链分析
sys_prctl # 系统调用入口
├── prctl_set_seccomp # 设置 seccomp 模式
├── do_seccomp # 执行 seccomp 操作
├── seccomp_set_mode_filter # 设置 seccomp 过滤器
├── bpf_prog_create_from_user # 从用户空间创建 BPF 程序
├── bpf_prepare_filter # 准备 BPF 过滤器
├── bpf_prog_select_runtime # 选择 BPF 运行时
├── bpf_int_jit_compile # BPF JIT 编译
├── kick_all_cpus_sync # 同步所有 CPU
└── smp_call_function_many ← **卡死位置**
卡死位置详细分析
pc : smp_call_function_many+0x348/0x3a0
lr : smp_call_function_many+0x308/0x3a0
程序计数器和链接寄存器都指向 smp_call_function_many
函数,说明 CPU 在这个函数内部陷入循环等待状态。
lockdep
lockdep 递归锁死检测分析
- 警告类型:
WARNING: possible recursive locking detected
- 内核版本:4.19.90-52.48.v2207.ky10.aarch64+debug #1
- 故障进程:swapper/1/0 (CPU1 的空闲进程)
锁冲突详细分析
冲突的锁对象:
锁地址:00000000056253b5 (&rp->lock){-.-.}
当前尝试获取位置:pre_handler_kretprobe+0xe4/0x530
已持有锁的位置:recycle_rp_inst+0xf8/0x278
死锁场景:
CPU0
----
lock(&rp->lock); ← recycle_rp_inst 中已经持有
lock(&rp->lock); ← pre_handler_kretprobe 中试图再次获取
*** DEADLOCK ***
锁持有状态分析
系统同时持有 3 个锁
#0: 00000000d4eaacda (rcu_node_0){..-.}
位置:rcu_process_callbacks+0x7ec/0x1868
#1: 00000000d386099e (&(kretprobe_table_locks[i].lock)){-.-.}
位置:kretprobe_hash_lock+0x80/0xb8
#2: 00000000056253b5 (&rp->lock){-.-.}
位置:recycle_rp_inst+0xf8/0x278
完整调用链分析
死锁触发路径:
arch_cpu_idle+0x3b4/0x730 # CPU 空闲状态
├── 中断处理
│ ├── el1_irq+0xbc/0x140 # ARM64 IRQ 处理入口
│ ├── gic_handle_irq+0x1ac/0x2e8 # GIC 中断控制器处理
│ ├── __handle_domain_irq+0x120/0x168 # 中断域处理
│ ├── irq_exit+0x2c4/0x528 # 中断退出处理
│ └── __do_softirq+0x91c/0x1304 # 软中断处理
├── RCU 回调处理
│ └── rcu_process_callbacks+0x7ec/0x1868 # RCU 回调处理 (持有 rcu_node_0 锁)
├── kretprobe 返回处理
│ ├── kretprobe_trampoline+0x70/0xc4 # kretprobe 跳板函数
│ ├── trampoline_probe_handler+0x2f0/0x5d0 # 跳板处理函数
│ └── recycle_rp_inst+0xf8/0x278 # 回收 kretprobe 实例 (持有 rp->lock)
└── 锁函数触发 kprobe
├── _raw_spin_lock+0xf4/0x118 # 自旋锁函数
├── queued_spin_lock_slowpath+0x0/0x6b0 # 触发 kprobe 探测点
├── kprobe_breakpoint_handler+0x400/0x548 # kprobe 断点处理
├── aggr_pre_handler+0xf8/0x190 # 聚合前置处理器
└── pre_handler_kretprobe+0xe4/0x530 # kretprobe 前置处理 (尝试获取 rp->lock)
递归锁死机制分析
问题根源:
- RCU 回调上下文:系统正在 RCU 软中断中处理回调
- kretprobe 实例回收:
recycle_rp_inst
函数持有&rp->lock
锁 - 锁函数被探测:在持有锁的状态下,又调用了被 kretprobe 监控的锁函数
- 递归获锁:
pre_handler_kretprobe
尝试获取同一个&rp->lock
锁
技术细节:
&rp->lock
:kretprobe 实例的保护锁,用于同步对 kretprobe 实例的访问recycle_rp_inst
:负责回收已使用的 kretprobe 实例,需要持有 rp->lockpre_handler_kretprobe
:kretprobe 的前置处理函数,也需要获取 rp->lock
关键问题分析
设计缺陷:
- 重入问题:kretprobe 监控了可能在其自身执行过程中被调用的函数
- 锁层次问题:没有正确的锁获取顺序,导致递归获锁
- 上下文冲突:软中断上下文中的 kretprobe 处理与普通上下文产生竞争
BZ#1844522 启用一些 kretprobes 可以触发内核 panic 使用以下 功能的 kretprobe 可能会导致 CPU 硬锁定: _raw_spin_lock _raw_spin_lock_irqsave _raw_spin_unlock_irqrestore queued_spin_lock_slowpath 因此,启用这些 kprobe 事件可能会遇到系统响应失败。在这种情况下会触发内核 panic。要解决这个问题,请避免为上述功能配置 kretprobes,并防止系统响应失败。
解决方案
bpftrace
bpftrace 的预防性解决方案
PR #534: Ban kprobes that can cause soft lockups
发现时间:2019年4月11日
问题来源:iovisor/bcc#2300
解决策略:主动禁止危险的 kretprobe 挂载点
问题发现过程
触发现象:
watchdog: BUG: soft lockup - CPU#8 stuck for 23s! [IOThreadPool0:2190014]
影响程度:
- 运行特定的 BPF 代码会导致系统完全不可用
- 需要重启系统才能恢复
- 在测试环境中多次复现(内核版本 4.16.18 和 5.0)
bpftrace 的解决方案
被禁止的 kretprobe 挂载点:
# bpftrace 测试结果
kretprobe:_raw_spin_lock is banned OK
kretprobe:queued_spin_lock_slowpath is banned OK ← 我们遇到的问题
kretprobe:_raw_spin_unlock_irqrestore is banned OK
kretprobe:_raw_spin_lock_irqsave is banned OK
代码逻辑:
/*
* Kernel functions that are unsafe to trace are excluded in the Kernel with
* `notrace`. However, the ones below are not excluded.
*/
const std::set<std::string> banned_kretprobes = {
"_raw_spin_lock", "_raw_spin_lock_irqsave", "_raw_spin_unlock_irqrestore",
"queued_spin_lock_slowpath",
};
void check_banned_kretprobes(std::string const& kprobe_name) {
if (banned_kretprobes.find(kprobe_name) != banned_kretprobes.end()) {
std::cerr << "error: kretprobe:" << kprobe_name << " can't be used as it might lock up your system." << std::endl;
exit(1);
}
}
关联内核补丁
[4.19,203/206] kretprobe: Prevent triggering kretprobe from within kprobe_flush_task
补丁信息:
- Commit ID: 9b38cc704e844e41d9cf74e647bff1d249512cb3
- 修复版本: Linux 4.19 稳定版补丁
- 补丁链接: patches.linaro.org/project/sta…
- 作者: Jiri Olsa jolsa@redhat.com
- 报告者: "Ziqian SUN (Zamir)" zsun@redhat.com
问题复现情况
触发场景:在 _raw_spin_lock_irqsave
上添加 retprobe 时出现锁死
lockdep 输出(与我们分析的问题完全一致):
============================================
WARNING: possible recursive locking detected
5.6.0-rc6+ #6 Not tainted
--------------------------------------------
sched-messaging/2767 is trying to acquire lock:
ffffffff9a492798 (&(kretprobe_table_locks[i].lock)){-.-.}, at: kretprobe_hash_lock+0x52/0xa0
but task is already holding lock:
ffffffff9a491a18 (&(kretprobe_table_locks[i].lock)){-.-.}, at: kretprobe_trampoline+0x0/0x50
*** DEADLOCK ***
stack backtrace:
kretprobe_trampoline+0x25/0x50
? _raw_spin_lock_irqsave+0x50/0x70
问题根本原因
死锁路径分析:
kprobe_flush_task
├── kretprobe_table_lock
├── raw_spin_lock_irqsave
└── _raw_spin_lock_irqsave ← 触发 kretprobe,安装 kretprobe_trampoline
→ kretprobe_table_locks 锁已被持有
kretprobe_trampoline
└── trampoline_handler
└── kretprobe_hash_lock(current, &head, &flags); ← 死锁!
核心问题:
- 递归触发:kretprobe 在自身的锁管理代码中触发了被监控的锁函数
- 锁重入:
kretprobe_table_locks
已被持有,但 trampoline 处理器试图再次获取 - 上下文混乱:中断上下文与普通上下文的锁竞争
修复方案
核心修复机制:引入 kprobe_busy
机制防止递归
新增函数:
struct kprobe kprobe_busy = {
.addr = (void *) get_kprobe,
};
void kprobe_busy_begin(void)
{
struct kprobe_ctlblk *kcb;
preempt_disable();
__this_cpu_write(current_kprobe, &kprobe_busy);
kcb = get_kprobe_ctlblk();
kcb->kprobe_status = KPROBE_HIT_ACTIVE;
}
void kprobe_busy_end(void)
{
__this_cpu_write(current_kprobe, NULL);
preempt_enable();
}
应用位置:
- kprobe_flush_task 中使用
kprobe_busy_begin/end
包围锁操作 - trampoline_handler 中使用
kprobe_busy
替代原有的递归保护机制
修复效果:
- 防止在 kprobe 处理代码中触发新的 kprobe
- 通过假的 probe 标记实现递归保护
- 确保 lockdep 的递归检查生效,阻止死锁路径
bpf_lock:内核的根本性解决方案(合并中)
Commit 046bbea5: bpf: Introduce bpf_lock
作者:Alexei Starovoitov ast@kernel.org
时间:2024年4月4日
目标:引入 BPF 程序安全锁机制,从根本上解决锁死问题
问题背景
这个 commit 尝试解决的核心问题正是我们遇到的:BPF 程序中的锁安全问题,包括:
- 递归死锁:BPF 程序在监控锁函数时自身产生的递归锁死
- 内存损坏:verifier 或 BPF 程序 bug 导致的锁结构体损坏
- ABBA 死锁:不同锁获取顺序导致的经典死锁问题
- 无限等待:缺乏超时机制导致的无限期等待
技术方案
该 commit 通过引入多层次安全机制来根本性解决 BPF 程序中的死锁问题:首先采用间接保护层设计,BPF 程序不再直接操作内核锁结构,而是通过 28 位 CPU ID + 4 位 per-CPU 锁 ID 的编码方式访问锁,避免了像 queued_spin_lock_slowpath
这样的递归锁死;其次使用MCS 锁算法替代传统自旋锁,该算法具有 NUMA 友好、FIFO 公平性和良好可扩展性的特点,天然避免了缓存行争用和递归获锁问题;同时内置完整的死锁检测机制,包括 AA 死锁检测(同一线程重复获取同一锁)和 ABBA 死锁检测(循环依赖检测),配合 1 秒超时机制防止无限等待;最后通过错误恢复和隔离机制,一旦检测到死锁就自动清理 bpf_lock_kern
并将 BPF 程序标记为 bad_lock
,后续锁操作直接失败,从而将有问题的程序隔离,避免对整个系统造成进一步损害。这种设计从根本上解决了 kretprobe 监控锁函数时产生的递归死锁问题,为 BPF 程序提供了安全可靠的锁机制。
根据本内核 commit 的思路 bpftrace 也实现了类似的机制,具体实现可以参考 bpftrace/bpftrace#3206。