Skip to content

一次 AMP 死锁排查

症状

/user-test-sched 三明治计时测试中,StarryOS(hart 0)侧客户端 ioctl(AWAIT) 永久阻塞。死锁只在完整 RPC 流程(DELAY + ECHO 连续调用)中出现,单步调用正常。

rt-async(hart 1)侧正常完成所有消息处理。

架构背景

QEMU virt RISC-V 64 双核 AMP。hart 0 运行 StarryOS(Linux 兼容内核),hart 1 运行 rt-async(M-mode RTOS)。双核通过共享内存 ring buffer 通信,CLINT MSIP 用作 IPI 通知。

hart 0 (StarryOS)                    hart 1 (rt-async)
─────────────────                    ─────────────────
用户态程序
  │ call(DELAY) + IPI ─────────────────→ 接收请求
  │ call_poll(ECHO) + IPI ─────────────→ 处理 DELAY
  │ ioctl(AWAIT) ← 阻塞等待               处理 ECHO
  │                                        send response + IPI
  │ ←── 唤醒, 读响应 ←───────────────────────┘

根因:spin::Mutex 不关中断

代码状态

ioctl(AWAIT) 使用 PollSet 管理 waker,PollSet 内部用 spin::Mutex(不关中断)保护 waker 队列。IPI handler 中调用 PollSet::wake() 也要获取同一把锁。

死锁场景

Task (hart 0)                            IPI 中断 (hart 0)
─────────────────────────────────────────────────────────
IPC_POLLSET.register(waker)
  → self.0.lock() ✓ 获得锁
  → 正在写入 waker...
                                          IPI 到达!
                                          ipi_irq_handler()
                                            → IPC_POLLSET.wake()
                                              → self.0.lock() ☠ 死等

同 hart 上,中断 handler 等待被中断的代码释放锁——锁永远不会被释放,因为中断抢占了对端。

为什么单步调用不死锁

单步调用只触发一次 IPI。register()wake() 不太可能同时竞争同一把锁。连续 RPC(DELAY + ECHO)使 IPI 频率翻倍,竞争窗口显著增大。

修复:SpinNoIrq + 移除 IPC_PENDING

1. SpinNoIrq 替换 PollSet

kspin::SpinNoIrqlock() 时自动关中断,drop 时恢复。消除了同 hart 死锁:

rust
static IPC_WAKER: SpinNoIrq<Option<Waker>> = SpinNoIrq::new(None);

fn ipi_irq_handler() {
    if let Some(waker) = IPC_WAKER.lock().take() {
        waker.wake();
    }
}

2. 移除 IPC_PENDING,改为 ring buffer 直读

IPC_PENDING: AtomicBool 是 ring buffer read != write 的冗余拷贝。两个状态源意味着不一致的可能。移除后直接读 CH1 ring buffer 的 read/write 原子变量:

rust
fn ch1_has_pending() -> bool {
    let base = shm_vaddr();
    let r = unsafe {
        (*((base + amp::CH1_RING_READ_OFFSET) as *const AtomicUsize))
            .load(Ordering::Acquire)
    };
    let w = unsafe {
        (*((base + amp::CH1_RING_WRITE_OFFSET) as *const AtomicUsize))
            .load(Ordering::Acquire)
    };
    r != w
}

ioctl(AWAIT) 实现:

rust
poll_fn(|cx| {
    if ch1_has_pending() {
        return Poll::Ready(0usize);
    }
    let mut guard = IPC_WAKER.lock();
    // SpinNoIrq.lock() 已关 sstatus.SIE,register 和 re-check 原子
    if ch1_has_pending() {
        Poll::Ready(0usize)
    } else {
        *guard = Some(cx.waker().clone());
        Poll::Pending
    }
})

ring buffer 是唯一真相源,spurious wakeup 安全——被唤醒后直接检查 ring buffer,有数据就处理,没有就继续等。

3. intercom.rs IPI 策略

process_elastic 中每个 Notify 响应立即回 IPI(通过 process_allon_notify 回调)。竞争检查窗口内处理的消息同理:

rust
fn send_notify_ipi() {
    unsafe { chip_qemu_virt_rt::send_ipi_to_linux() };
}

pub fn process_elastic() -> usize {
    shm.set_busy();
    let mut total_count = 0;

    loop {
        let n = SERVER.process_all::<RtAsyncRpc, _, _>(
            |msg| handle_non_rpc(msg),
            || send_notify_ipi(),
        );
        total_count += n;

        if n > 0 { continue; }
        // 弹性自旋...
        break;
    }

    shm.clear_busy();
    core::sync::atomic::fence(Ordering::SeqCst);

    if SERVER.has_pending() || SERVER.has_urgent() {
        let n = SERVER.process_all::<RtAsyncRpc, _, _>(
            |msg| handle_non_rpc(msg),
            || send_notify_ipi(),
        );
        total_count += n;
    }
    total_count
}

改动总结

维度改动前改动后
中间标志IPC_PENDING: AtomicBool无(直接读 ring buffer)
waker 存储PollSet(spin::Mutex, 64槽)SpinNoIrq<Option<Waker>>
IRQ 安全性无(spin::Mutex 不关中断)SpinNoIrq 类型保证
IPI 策略逐通知回 IPI逐通知回 IPI(ring buffer 直读兜底)
spurious wakeup不安全(IPC_PENDING 可能误导)安全(ring buffer 是权威状态)

三条经验

1. 中断上下文共享的锁,必须关中断

这是嵌入式的常识,但容易被忽略——spin::Mutex 的 API 和 std::sync::Mutex 一模一样,看不出差异。在 bare metal 环境下,"锁"的语义比标准库多一个维度:IRQ 安全性。同 hart 上,中断 handler 等待被中断的代码释放锁——锁永远不会被释放。

2. 消除中间状态

IPC_PENDING 是对 ring buffer read != write 的冗余拷贝。两个状态源意味着不一致的可能。消除后 ring buffer 是唯一真相源,逻辑更简单,出错的表面积更小。

3. 类型系统是防止并发 bug 的最好工具

SpinNoIrq 把关中断编码在 lock() 语义里——不可能漏。比手动 NoPreemptIrqSave 靠人记忆可靠得多。