TF-M SPE Mailbox IPC 交互流程与 Cache 维护详解

1. IPC 的实质:三个核心问题

抛开代码,双核 IPC 的本质是三个问题:

1. 数据怎么传? ------ 共享内存。

两个核挂在同一个总线/存储上,有一块双方都能访问的 RAM。A 核写进去,B 核读出来。

2. 对方怎么知道有数据了? ------ 中断(Notification)。

共享内存是"哑"的,写进去对方不会自动知道。需要一条"门铃"通道:一方写完数据后,用硬件机制(IPC 外设、专用寄存器等)触发对方的中断,对方在 ISR 里被唤醒去取数据。

3. 数据会不会读错? ------ 一致性(Cache + 原子性)。

  • Cache 一致性问题:共享内存若被各自 D-Cache 缓存,A 写进内存的值可能只留在 A 的 cache 里;B 读时命中自己 cache 的旧值 → 读到脏数据。解决:写后 Clean、读前 Invalidate,或把共享区配成 uncacheable。
  • 原子性问题:状态位图(pend/replied)是"读-改-写整个 32 位字",两核同时操作会互相覆盖。解决:跨核自旋锁。

因此 TF-M 的 Mailbox = 共享内存(数据结构) + 双向门铃(IPC 中断) + 一致性手段(fence/cache 操作 + spinlock) 三者组合。


2. 整体架构与角色

  • 协议层(ABI) :共享数据结构定义在 interface/include/multi_core/tfm_mailbox.hS/NS 两侧必须使用同一份头文件编译
  • NSPE 侧(如 D25,非安全主核)tfm_ns_mailbox 库 + RTOS 适配层 + HAL 层,位于 interface/src/multi_core/tfm_ns_mailbox.c
  • SPE 侧(如 N22,安全核)ns_agent_mailbox 分区(非安全代理),secure_fw/partitions/ns_agent_mailbox/ns_agent_mailbox.c + tfm_spe_mailbox.c,接入 tfm_rpc 框架。

3. 共享内存 ABI(S/NS 共用)

复制代码
ns_mailbox_queue_t (NSPE 分配,共享区)
├── status: mailbox_status_t   ← 状态位图
│     ├── pend_slots           NSPE 已提交、待 SPE 处理
│     └── replied_slots        SPE 已回复、待 NSPE 取走
├── slots[N]: mailbox_slot_t   ← 每个槽位一个 PSA 调用
│     ├── msg   (MAILBOX_ALIGN)  NSPE 写请求
│     └── reply (MAILBOX_ALIGN)  SPE 写回复
└── slots_ns[N]                ← NSPE 私有(owner/reply/woken,SPE 不可见)

关键数据结构(interface/include/multi_core/tfm_mailbox.h):

c 复制代码
/* 每个槽位 = 一个进行中的 PSA 调用 */
struct mailbox_slot_t {
    struct mailbox_msg_t   msg    MAILBOX_ALIGN;   /* NSPE 写请求 */
    struct mailbox_reply_t reply  MAILBOX_ALIGN;   /* SPE 写回复 */
} MAILBOX_ALIGN;

/* 队列状态:两张位图 */
struct mailbox_status_t {
    mailbox_queue_status_t pend_slots;     /* 位i=1 → slot[i] 有待处理的请求 */
    mailbox_queue_status_t replied_slots;  /* 位i=1 → slot[i] 已有回复可取 */
} MAILBOX_ALIGN;

/* 请求消息 */
struct mailbox_msg_t {
    uint32_t call_type;          /* MAILBOX_PSA_*: FRAMEWORK_VERSION/VERSION/CONNECT/CALL/CLOSE */
    struct psa_client_params_t params;  /* 对应 PSA API 的参数联合体 */
    int32_t  client_id;          /* NS 调用者 ID(可选) */
};

谁写什么、谁读什么固定单向(杜绝竞争的关键设计):

共享对象 写入方 读取方
msg NSPE SPE
reply SPE NSPE
pend_slots NSPE 置位 SPE 读+清
replied_slots SPE 置位 NSPE 读+清

NSPE 私有槽位信息(owner 任务句柄、唤醒标志)在 slots_ns[] 中,SPE 永远不碰。

3.1 MAILBOX_ALIGN 与伪共享防护

msgreply 强制按 cache line 对齐分隔(MAILBOX_ALIGN,默认 MAILBOX_CACHE_LINE_SIZE = 32B)。

  • 问题:若 msg 与 reply 同处一条 cache line,NSPE 刷 msg、SPE 刷 reply 会互相使对方缓存行失效,产生**伪共享(false sharing)**抖动。
  • 对策:对齐后各自独占 cache line。
  • 可关闭 :当两侧都无 dcache 或共享区配置为 uncacheable(MAILBOX_IS_UNCACHED_S/NS==1)时,宏退化为编译器默认对齐,节省内存。

4. 完整交互流程

4.1 启动握手(一次性)

复制代码
NSPE(主核,先启动)                      SPE
    │                                    │  SPM+分区初始化
    │◄────── IPC 中断(INIT 通知) ────────│  tfm_mailbox_hal_init()
    │  读 mailbox_init_t(status/slots/   │
    │  slot_count + 自定义 spinlock_addr) │
    │  tfm_ns_mailbox_init()              │
    │──── IPC 中断(READY 确认) ─────────►│
    │                                    │  双方就绪,进入 PSA 通信

SPE 侧入口 ns_agent_mailbox_entry()

c 复制代码
void ns_agent_mailbox_entry(void)
{
    boot_ns_core();                        // 启动/等待 NS 核
    if (tfm_inter_core_comm_init()) {      // → tfm_mailbox_init()
        psa_panic();
    }
    mailbox_enable_interrupts();           // 使能 MAILBOX_IRQ
    while (1) {
        signals = psa_wait(PSA_WAIT_ANY, PSA_BLOCK);   // IPC 分区模型:等信号
        ...tfm_rpc_client_call_handler(active_signal);
        ...tfm_rpc_client_call_reply();    // 处理异步回复
    }
}

tfm_inter_core_comm_init()tfm_mailbox_init()tfm_spe_mailbox.c L476-L507),最关键的是 tfm_mailbox_hal_init(&spe_mailbox_queue) 完成握手,将 NSPE 队列地址(ns_statusns_slotsns_slot_count)挂接进 SPE 侧 secure_mailbox_queue_t

4.2 一次 PSA 调用(同步请求/回复时序)

复制代码
NSPE 任务                               SPE ns_agent_mailbox
│  psa_call()                           │
│  [tfm_ns_mailbox_client_call]         │
│  ① acquire_empty_slot() 取空槽位      │
│  ② 填 slots[idx].msg                 │
│     MAILBOX_CLEAN_CACHE(msg)          │  ← 写后刷缓存
│  ③ 临界区置 pend_slots 位             │
│  ④ IPC 中断 ───────────────────────► │
│                                       │  ⑤ ISR → psa_wait 信号
│                                       │     tfm_rpc_client_call_handler()
│                                       │  ⑥ tfm_mailbox_handle_msg()
│                                       │     - 临界区读 pend_slots
│                                       │     - MAILBOX_INVALIDATE_CACHE(msg) ← 读前失效
│                                       │     - tfm_mailbox_dispatch()
│                                       │       同步:mailbox_direct_reply
│                                       │       异步:等分区处理完再回
│                                       │  ⑦ 写 slots[idx].reply
│                                       │     MAILBOX_CLEAN_CACHE(reply)  ← 写后刷
│                                       │  ⑧ 临界区置 replied_slots
│                                       │     通知对端
│◄────── IPC 中断 ──────────────────────│
│  ⑨ tfm_ns_mailbox_wake_reply_owner_isr()
│     - MAILBOX_INVALIDATE_CACHE(reply)  ← 读前失效
│     - 拷贝 return_val,唤醒任务        │
│  任务继续执行                          │

4.3 NSPE 侧发请求(代码要点)

tfm_ns_mailbox_client_call()tfm_ns_mailbox.c L169-L207):

c 复制代码
ret = mailbox_tx_client_req(call_type, params, client_id, &slot_idx);
mailbox_wait_reply(slot_idx);
ret = mailbox_rx_client_reply(slot_idx, &reply_buf);

mailbox_tx_client_req()(L100-L153)的四个动作:

c 复制代码
idx = acquire_empty_slot(...);            // ① 从 empty_slots 位图抢一个空闲槽
msg_ptr = &mailbox_queue_ptr->slots[idx].msg;
msg_ptr->call_type = call_type;           // ② 填 msg:调用类型+参数+client_id
memcpy(&msg_ptr->params, params, ...);
MAILBOX_CLEAN_CACHE(msg_ptr, sizeof(*msg_ptr));   // ③ 写后刷缓存

critical_section = tfm_ns_mailbox_hal_enter_critical();  // ④ 跨核临界区
set_queue_slot_pend(mailbox_queue_ptr, idx);       //    置 pend_slots 位
tfm_ns_mailbox_hal_exit_critical(critical_section);

tfm_ns_mailbox_hal_notify_peer();         // ⑤ 拉门铃:触发 SPE 中断

顺序严谨性:先填 msg 并 Clean → 再置 pend_slots 位 → 最后发中断。保证 SPE 收到中断时 msg 内容在内存中已可见;顺序反了,SPE 看到 pend 位去读 msg 可能读到旧数据。

4.4 SPE 侧收请求、处理(代码要点)

tfm_mailbox_handle_msg()tfm_spe_mailbox.c L304-L404)核心循环:

c 复制代码
pend_slots = get_nspe_queue_pend_status(ns_status);   // ① 读前 Invalidate + 临界区

for (idx = 0; idx < spe_mailbox_queue.ns_slot_count; idx++) {
    if (!(pend_slots & (1 << idx))) continue;         // 只看挂起的槽

    clear_spe_queue_empty_status(idx);                // ② 占住 SPE 侧槽位
    msg_ptr = &spe_mailbox_queue.queue[idx].msg;
    MAILBOX_INVALIDATE_CACHE(&spe_mailbox_queue.ns_slots[idx].msg, sizeof(*msg_ptr));
    spm_memcpy(msg_ptr, &spe_mailbox_queue.ns_slots[idx].msg, sizeof(*msg_ptr));
    //                                                          ↑ ③ 读前失效 + 拷到安全内存
    status = tfm_mailbox_dispatch(msg_ptr, idx, &reply_slots);  // ④ 分发
}
...
clear_nspe_queue_pend_status(ns_status, pend_slots);   // ⑤ 清所有 pend 位
set_nspe_queue_replied_status(ns_status, reply_slots); // ⑥ 置 replied 位
tfm_mailbox_hal_notify_peer();                         // ⑦ 拉门铃回 NSPE

细节 :SPE 把 msg 拷贝到自己的安全内存(spm_memcpy)再处理,而非直接读共享内存。目的:一是安全隔离(防 TOCTOU:处理过程中 NSPE 可能改槽位);二是槽位能尽快复用。

分发 tfm_mailbox_dispatch()(L214-L299),MAILBOX_PSA_CALL 分支:

c 复制代码
case MAILBOX_PSA_CALL:
    ret = local_copy_vects(params, idx, &control);   // 把 in/out 向量拷到本地 vectors[]
    ...
    client_params.ns_client_id_stateless = client_id;
    client_params.p_invecs = vectors[idx].in_vec;
    client_params.p_outvecs = vectors[idx].out_vec;
    psa_ret = tfm_rpc_psa_call(handle, control, &client_params, mb_msg_handle);
    if (psa_ret != PSA_SUCCESS) sync = true;         // 出错则改同步
    break;

这里体现 FF-M Agent 机制 的本质:普通安全分区的 psa_call() 会阻塞调用者等结果,但 NS Agent 邮箱分区必须尽快响应 NSPE 中断、不能阻塞------所以使用非阻塞的 Agent APIagent_psa_call,见 secure_fw/spm/include/ffm/mailbox_agent_api.h)。服务完成后由 SPM 通过 ASYNC_MSG_REPLY 信号回叫分区。

4.5 同步 vs 异步回复

call_type 模式 说明
FRAMEWORK_VERSION / VERSION 同步 立即回复
PSA_CALL 异步(IPC 后端) 由安全分区处理后,经 RPC reply 回调 → tfm_mailbox_reply_msg() 回复
PSA_CONNECT / PSA_CLOSE 异步 同上
出错/参数非法 强制转同步 立即回错误码

同步路径tfm_mailbox_dispatch() 末尾:

c 复制代码
if (sync) {
    *reply_slots |= (1 << idx);              // 记入本批 reply 位图
    mailbox_direct_reply(idx, (uint32_t)psa_ret);
}

mailbox_direct_reply()(L145-L175):

c 复制代码
if (vectors[idx].in_use && result == PSA_SUCCESS) {
    for (int i = 0; i < vectors[idx].out_len; i++) {
        vectors[idx].original_out_vec[i].len = vectors[idx].out_vec[i].len;
    }                                        // 把 outvec 实际长度写回 NSPE
}
vectors[idx].in_use = false;
reply_ptr = get_nspe_reply_addr(idx);        // 定位 NSPE 槽位 reply
spm_memcpy(&reply_ptr->return_val, &ret_result, sizeof(...));
MAILBOX_CLEAN_CACHE(reply_ptr, sizeof(*reply_ptr));  // 写 reply 后刷缓存
mailbox_clean_queue_slot(idx);               // 释放 SPE 槽位

异步路径 :服务分区处理完后,SPM 经 ASYNC_MSG_REPLY 信号唤醒 agent 分区,tfm_rpc_client_call_reply() 取出 handle->client_data(即当初的 mb_msg_handle)调用 rpc_ops.reply()mailbox_reply()(L455-L463):

c 复制代码
static void mailbox_reply(const void *owner, int32_t ret)
{
    mailbox_msg_handle_t handle = MAILBOX_MSG_NULL_HANDLE;
    if (owner) handle = *((mailbox_msg_handle_t *)owner);  // 从 owner 取回句柄
    (void)tfm_mailbox_reply_msg(handle, ret);              // 按句柄定位槽位再回复
}

tfm_mailbox_reply_msg()(L407-L443)通过 handle → idx 定位槽位,走同一个 mailbox_direct_reply(),然后单独置 replied 位、单独 notify_peer()

为什么句柄 = 索引 + 1? (L119-L138)0 保留为 MAILBOX_MSG_NULL_HANDLE(表示"回复第一个槽位",单并发场景简化),真实槽位句柄 ≥1,天然区分有效/无效。

4.6 NSPE 取回复(代码要点)

NSPE 被门铃唤醒后,OS 模式 ISR 调 tfm_ns_mailbox_wake_reply_owner_isr()(L211-L244):读 replied_slots → 对每个已回复槽位置 is_woken 标志并唤醒 owner 任务。任务醒来后 mailbox_rx_client_reply()(L146-L165):

c 复制代码
MAILBOX_INVALIDATE_CACHE(&slot->reply, sizeof(slot->reply));  // 读 reply 前失效
*reply = slot->reply.return_val;                              // 取返回值
set_queue_slot_empty(idx);                                    // 槽位重新可用

防止误唤醒机制mailbox_wait_reply(),L247-L263):任务被唤醒后先检查 is_woken 标志确认"真的是我的回复到了",否则继续睡------因为中断可能来自别的槽位/任务。


5. Cache 一致性维护细节

5.1 基本原理

共享内存若被 dcache 缓存,一侧写入只停留在 cache 中,对侧读到的是陈旧数据。必须遵循:

写后刷(Clean/Flush)→ 对侧读前失效(Invalidate)

5.2 宏定义与降级条件

SPE 侧(tfm_spe_mailbox.c L29-L38):

c 复制代码
#if !defined(__DCACHE_PRESENT) || (__DCACHE_PRESENT == 0U) || (MAILBOX_IS_UNCACHED_S == 1)
#define MAILBOX_CLEAN_CACHE(addr, size) __DSB()                 /* 退化为仅屏障 */
#define MAILBOX_INVALIDATE_CACHE(addr, size) do {} while (0)
#else
#define MAILBOX_CLEAN_CACHE(addr, size) SCB_CleanDCache_by_Addr((addr), (size))
#define MAILBOX_INVALIDATE_CACHE(addr, size) SCB_InvalidateDCache_by_Addr((addr), (size))
#endif

NSPE 侧(tfm_ns_mailbox.h L33-L42)同构,条件为 MAILBOX_IS_UNCACHED_NS

三种情况:

  1. 无 D-Cache(如低端 M0+):什么都不用做,宏退化。
  2. 有 D-Cache 且共享区可缓存:每次跨核访问前后必须配对 Clean/Invalidate。
  3. 共享区配置为 uncacheable (推荐):cache 不介入共享内存,宏退化为仅 __DSB()/fence 内存屏障(保证顺序、防重排),最简单最安全。

5.3 四个关键配对点

# 位置 操作 目的
1 NSPE 写 msg 后(tfm_ns_mailbox.c L131-L133) CLEAN_CACHE(msg) 请求内容推到物理内存
2 SPE 读 msg 前(tfm_spe_mailbox.c L357-L358) INVALIDATE_CACHE(msg) 丢弃可能残留的旧缓存行
3 SPE 写 reply 后(tfm_spe_mailbox.c L164-L165) CLEAN_CACHE(reply) 返回值推到物理内存
4 NSPE 读 reply 前(tfm_ns_mailbox.c L156-L158) INVALIDATE_CACHE(reply) 丢弃旧缓存行再读

status 位图同样配对:SPE 读 pend 前 Invalidate(L93-L99),置 replied 后 Clean(L101-L107)。

5.4 注意坑

CLEAN_CACHE 不能替代内存屏障。即使共享区 uncacheable,fence/__DSB() 仍必须存在------否则编译器可能把"填 msg"与"置 pend 位"重排。这也是退化分支仍保留 __DSB() 的原因。

5.5 推荐做法

将共享内存区域配置为 uncacheable (PMP/总线属性),两侧只需 fence,无需 cache 操作。锁变量(spinlock)也必须位于 uncacheable 区,否则 AMOSWAP 自旋可能失效。


6. 并发与同步

6.1 为什么必须 spinlock(读-改-写竞争)

pend_slots 是 32 位整字。D 置 bit2 的同时 SPE 清 bit2:

复制代码
NSPE: 读 pend_slots(0b0100) → 置 bit2 → 写回 0b1100
SPE : 读 pend_slots(0b0100) → 清 bit2 → 写回 0b0000   ← 覆盖了 NSPE 的写!

无互斥时 SPE 的最后写会把 NSPE 刚置的位抹掉,请求丢失。所以 SPE 读 pend(L310-L319)进临界区,清 pend/置 replied(L396-L401)也在临界区内。

6.2 软件自旋锁(RISC-V AMOSWAP 骨架)

platform/ext/target/demo/board/mailbox/platform_multicore.c

c 复制代码
static inline void spin_lock_acquire(volatile uint32_t *lock)
{
    __asm volatile(
        "1: li   t0, 1\n"
        "   amoswap.w.aq t1, t0, (%0)\n"   /* 原子交换:把 lock 置1,返回旧值 */
        "   bnez t1, 1b\n");               /* 旧值非0 → 已被占用,继续自旋 */
}

static inline void spin_lock_release(volatile uint32_t *lock)
{
    __asm volatile("amoswap.w.rl x0, x0, (%0)\n");  /* 写0释放 */
}

要点:

  • AMOSWAP(读-改-写原子指令)保证"检查-加锁"不可分割;不用原子指令则两核可能同时看到锁为 0 同时进临界区。
  • .aq(acquire)/.rl(release)语义承担内存屏障职责:加锁后的访问不能重排到加锁前,释放前的访问不能重排到释放后。
  • 锁变量必须在 uncacheable 区域。
  • 忙等(自旋)而非信号量:临界区只有几十个周期,自旋不涉及任务切换,开销最小。

6.3 锁地址同步

锁变量地址由握手时通过 mailbox_init_t.spinlock_addr 一次性同步(platform_spe_mailbox.c L25-L35 扩展结构体)。注意mailbox_init_t 是 S/NS 共用 ABI,加字段必须两端同步修改,否则结构体布局错位导致内存损坏。

6.4 NSPE 侧内部同步

  • empty_slots 位图由 tfm_ns_mailbox_os_spin_lock 保护。
  • 任务等待回复用信号量 + is_woken 标志(区分中断唤醒与误唤醒)。
  • 跨核状态字置位/清位在 tfm_ns_mailbox_hal_enter/exit_critical() 内完成。

7. 数据流全景图(含缓存点标注)

复制代码
NSPE 任务                                  共享内存                            SPE ns_agent_mailbox
  │                                          │                                   │
  │ 取空槽 idx                                │                                   │
  │ 填 msg ── CLEAN ──────────────────────►  msg                                │
  │ 临界区: 置 pend_slots[i] ──────────────► pend_slots                          │
  │ 门铃(IPC中断) ────────────────────────────▶                                 │
  │                                          │                                   │ ISR→psa_wait→handler
  │                                          │                       临界区: 读 pend_slots
  │                                          │ msg ◄────── INVALIDATE ───────── │ 拷贝 msg 到安全内存
  │                                          │                                   │ dispatch→agent_psa_call
  │                                          │                                   │  [同步]立即回 / [异步]等分区
  │                                          │ reply ◄────── CLEAN ─────────────│ 写 reply.return_val
  │                                          │ replied_slots ◄───── 临界区置位 ──│
  │◄────────────────────────────────────────── 门铃(IPC中断)                     │
  │ 读 replied_slots[i](临界区)             │                                   │
  │ reply ── INVALIDATE ──► 取 return_val     │                                   │
  │ 唤醒任务 → psa_call() 返回                 │                                   │

8. 代码中的简化点与升级方向

  1. 槽位一一对应tfm_spe_mailbox.c L339-L344):直接用 NSPE 槽位索引作 SPE 槽位索引,未做动态空闲槽搜索。多并发时效率受限,但正确性无碍(empty_slots 位图保证不冲突)。
  2. 空句柄回复第一个槽 (L420-L428):handle == 0 时默认回 slot 0。单并发够用;多并发存在被伪造的风险(注释已提示)。
  3. 消息校验是占位check_mailbox_msg,L178-L184):直接返回成功。生产环境应校验 call_type 合法、params 指针范围、vector 数量等。

9. 移植清单(双核通信专项)

SPE 侧

  • platform_multicore.h/.c:IPC 通道/中断号/魔数(与 NSPE 约定)
  • platform_spe_mailbox.c
    • tfm_mailbox_hal_init():握手时序 + ns_init 地址校验
    • mailbox_init_t.spinlock_addr 扩展(与 NSPE 侧同步修改!)
    • notify_peer / enter/exit_critical
  • tfm_hal_multi_core.c:空操作适配(NSPE 先启动场景)
  • ns_agent_mailbox 分区纳入构建(CMake)
  • mailbox ISR 注册 → tfm_rpc_client_call_handler()
  • 共享区 uncacheable 配置(PMP/总线属性)

NSPE 侧

  • tfm_ns_mailbox_hal_*:握手接收端 + 通知 + spinlock
  • tfm_ns_mailbox_os_*:RTOS 信号量/消息队列/任务句柄
  • mailbox ISR:tfm_ns_mailbox_wake_reply_owner_isr()
  • tfm_multi_core_psa_ns_api.c 等库搬运
  • 专用 mailbox 线程(tfm_ns_mailbox_thread_runner

共同

  • 共享内存布局一致(MAILBOX_SHARED_BASE
  • mailbox_init_t 结构体两端同步(含 spinlock_addr)
  • IPC 中断号约定一致
  • Cache 策略一致(uncacheable 或 CLEAN/INVALIDATE 配对)

10. SPE 侧 HAL 接口速查

接口 职责 位置
tfm_mailbox_hal_init() 握手,挂接 NSPE 队列地址 platform/include/tfm_hal_mailbox.h
tfm_mailbox_hal_notify_peer() 触发对端中断(门铃) 同上
tfm_mailbox_hal_enter/exit_critical() 跨核互斥(硬件锁或 AMOSWAP) 同上
tfm_rpc_register_ops() 注册 handle_req/reply/process_new_msg 回调 ns_agent_mailbox_rpc.c

11. 关键参考文件

用途 位置
共享 ABI 定义 interface/include/multi_core/tfm_mailbox.h
NS 侧 API interface/include/multi_core/tfm_ns_mailbox.h
NS 侧实现 interface/src/multi_core/tfm_ns_mailbox.c
SPE 侧实现 secure_fw/partitions/ns_agent_mailbox/tfm_spe_mailbox.c
SPE 侧分区入口 secure_fw/partitions/ns_agent_mailbox/ns_agent_mailbox.c
RPC 回调层 secure_fw/partitions/ns_agent_mailbox/ns_agent_mailbox_rpc.c
Agent API secure_fw/spm/include/ffm/mailbox_agent_api.h
SPE 侧 HAL platform/include/tfm_hal_mailbox.h
多核 HAL platform/include/tfm_hal_multi_core.h
移植骨架 platform/ext/target/demo/board/mailbox/*
双核通信文档 platform/ext/target/demo/board/DUAL_CORE_COMMUNICATION.md
设计文档 docs/design_docs/multi-cpu/mailbox_design.rst

12. 答疑:SPE 与 NSPE 是否都是"客户端"访问共享内存?

不是。"客户端"一词需分两层理解,无论哪一层,SPE 与 NSPE 都不是对称的"双客户端"关系。

12.1 内存访问层面 ------ 双方是对等节点,但访问单向不对称

共享邮箱不是"谁都能读写"的一整块内存,而是按字段严格分工:

共享对象 写入方 读取方
slots[i].msg NSPE SPE
slots[i].reply SPE NSPE
status.pend_slots NSPE 置位 SPE 读+清
status.replied_slots SPE 置位 NSPE 读+清

而且队列本身由 NSPE 分配ns_mailbox_queue_t 在 NS 侧创建),SPE 侧只有握手时拿到的指针(ns_statusns_slotsns_slot_count,见 platform_spe_mailbox.ctfm_hal_mailbox.hsecure_mailbox_queue_t)。所以内存归属是:NSPE 是队列所有者,SPE 只是持指针的访问者

12.2 PSA Client-Server 模型层面 ------ NSPE 是 Client,SPE 是 Server

  • NSPE 是真正的 PSA 客户端 :调用 psa_call()/psa_connect()/psa_close()
  • SPE 提供安全服务(crypto / attestation / ITS 等),是服务端。
  • 中间的 ns_agent_mailbox 分区叫 NS Agent(非安全代理) :它代表 NSPE 客户端 去调用 SPM 服务,因此 API 名为 agent_psa_call(),调用时把 client_id 传给 SPM(见 secure_fw/spm/core/mailbox_agent_api.c)。它"代理"的是 NS 客户端身份,而不是自己当客户端。

一句话总结:传输层(mailbox)双方是对等的消息对端、角色单向固定;服务层 NSPE 是 Client、SPE 是 Server,SPE 里的 agent 分区只是代传客户端的请求。


13. 答疑:SPE 的"SP 分区"是什么?如何划分?在共享内存之外吗?

13.1 什么是 SP 分区

TF-M 基于 PSA FF-M(Firmware Framework for M) 框架,安全侧(SPE)并非一个大而全的固件,而是拆成多个安全分区(Secure Partition,SP) ,每个分区是独立隔离的组件,提供一类服务:

  • TFM_SP_CRYPTOsecure_fw/partitions/crypto/tfm_crypto.yaml
  • TFM_ATTEST(初始认证)
  • TFM_ITS / TFM_PS(内部/受保护存储)
  • TFM_FWU(固件更新)、TFM_SP_PLATFORM(平台控制)
  • TFM_NS_MAILBOX_AGENTns_agent_mailbox.yaml)------ 即本分析的 mail 分区

每个分区通过一个 manifest(yaml)描述自身,构建工具生成"加载信息",SPM 启动时加载并隔离它们。

13.2 分区如何"划分"(两套维度)

维度一:信任等级 (manifest 的 "type" 字段)

  • PSA-ROT(PSA Root of Trust):最可信,可访问底层隔离硬件。crypto、attestation、ns_agent_mailbox 都是 PSA-ROT。
  • APP-ROT(Application RoT):可信度较低,只能通过标准 PSA API 交互。

维度二:执行模型 (manifest 的 "model" 字段)

  • IPC 模型 :分区是独立线程,用 psa_wait/psa_get/psa_reply 消息式处理请求。ns_agent_mailbox 是 IPC 模型ns_agent_mailbox_entry()while(1)psa_wait(PSA_WAIT_ANY, PSA_BLOCK) 等信号)。
  • SFN 模型 :分区只是一组被 SPM 直接调用的函数,无独立线程(如 crypto 是 "model": "SFN")。

物理隔离划分 :每个分区在 manifest 声明 stack_size,SPM 从安全数据区的运行时池分配独立栈;隔离边界由 MPU/SAU/PPC 等硬件强制,分区之间不能互访内存,只能经 SPM 授权的 API 通信。

13.3 分区内存在哪?------ 在共享内存之外

参考 platform/ext/target/demo/board/partition/region_defs.h 的 N22 内存布局:

复制代码
N22_SRAM (256 KB)
├── S_CODE   (0x20000000)     安全代码段        ← SPE 代码
├── S_DATA   (一半 SRAM)      安全数据段        ← 分区代码/栈/加载信息池
│     ├── PART_INFOLIST / PART_INFORAM / SERV_INFORAM   ← SPM 运行时分区池
│     └── 各分区自己的栈(stack_size)
└── MAILBOX_SHARED_BASE  (SRAM 末尾 4KB)       ← 唯一与 NSPE 共享的区域
      └── mailbox 队列 + spinlock
  • 共享内存只是一小块专用的 mailbox 传输通道MAILBOX_SHARED_BASE/SIZE,SRAM 末尾,uncacheable),里面只有队列和锁。
  • 所有 SP 分区(含 ns_agent_mailbox)的代码、数据、栈都在安全私有内存(S_CODE/S_DATA)里,不在共享区。
  • ns_agent_mailbox 分区"接触"共享内存的方式,只是它内部持有两个指针(ns_statusns_slots)指向 NSPE 队列;它自己的 spe_mailbox_queuevectors[] 都是安全内存里的静态变量tfm_spe_mailbox.c L42-L55)。

这正是安全设计的关键:NSPE 只看得见共享区,永远碰不到安全内存里的分区数据;SPE 通过安全隔离保证共享区里只能放 mailbox 数据。


14. 答疑:Cache 一致性与原子性是什么关系?

14.1 各自解决什么问题

  • Cache 一致性 :解决"写的数据看不看得见"------空间维度。write-back 模式下,A 核写的数据先留在自己的 D-Cache,物理 RAM 不立即更新;B 核读自己的 cache 或物理内存都可能拿到旧值。破坏者是 cache 缓冲与指令重排。
  • 原子性 :解决"读-改-写会不会被撕裂 "------时间维度。pend_slots 的置位/清位底层是"读整个 32 位字 → 改一位 → 写回"三条指令,两核交叉执行会互相覆盖。破坏者是两核并发修改同一个字。

14.2 对比

Cache 一致性 原子性
解决的问题 写的数据看不看得见(空间维度) 改的操作会不会被撕裂(时间维度)
破坏者 write-back cache / 指令重排 两核并发读-改-写同一个字
对策 Clean / Invalidate / uncacheable 原子指令 + 自旋锁
不解决的后果 读到旧数据 状态位丢失、请求丢失

14.3 为什么必须同时解决

  • 就算 cache 完全一致(数据随时可见),没有锁,"读-改-写"仍可能被另一核插入 → 原子性问题照样存在
  • 就算有完美原子指令,没有 Clean/Invalidate,A 写的数据 B 看不见 → Cache 问题照样存在

两者互相独立、谁也不能替代谁。这也是第 3 节"谁写、谁读固定单向"设计的动机:msg 只有 NSPE 写、reply 只有 SPE 写,每个对象单一写方从设计上消灭了大半竞争;只有两核都改的状态位图(pend_slots/replied_slots)才需要锁。

14.4 生活化比喻

  • Cache 像桌上的草稿纸 :算完答案先写在自己草稿纸上(write-back,没抄到白板);对端看白板(物理内存)还是旧内容,或对端自己桌上也有一张旧草稿纸(自己的 cache 副本)直接当答案。Clean = 抄到白板,Invalidate = 撕掉旧草稿纸强制看白板。
  • 原子性像两人改同一份文档 :各自复制整份改不同行,再整份覆盖回去,后覆盖的冲掉先覆盖的。锁 = 谁先拿到谁改,改完才放开。

15. 答疑:pend_slots 是用 mailbox 传递的吗?

不是。 pend_slotsmsgreplied_slots 一样,都是直接写在共享内存里 的字段;mailbox 中断(门铃)不携带任何数据,只负责唤醒对端去共享内存里读。

15.1 两个通道分开看

通道 内容 机制
共享内存 msgreplypend_slotsreplied_slots 直接读写共享 RAM
门铃(mailbox 中断) 不携带任何数据 只触发对端中断

15.2 代码证据

NSPE 置位(tfm_ns_mailbox.cmailbox_tx_client_req(),L121-L133):

c 复制代码
critical_section = tfm_ns_mailbox_hal_enter_critical();
set_queue_slot_pend(mailbox_queue_ptr, idx);   // ① 写共享内存 status.pend_slots
tfm_ns_mailbox_hal_exit_critical(critical_section);

tfm_ns_mailbox_hal_notify_peer();              // ② 之后才拉门铃(中断,无参数)

顺序是先写共享内存 → 最后拉门铃,中断里没有槽位号或状态位。

SPE 读位图(tfm_spe_mailbox.ctfm_mailbox_handle_msg(),L310-L321):

c 复制代码
pend_slots = get_nspe_queue_pend_status(ns_status);   // ← 直接从共享内存读

get_nspe_queue_pend_status() 就是 ns_status->pend_slots(L93-L99),ns_status 是握手时从共享内存挂接的指针。

15.3 完整链路

复制代码
NSPE: 填 msg(共享内存) → 置 pend_slots(共享内存) → 拉门铃
                                        ↓(中断只喊"快来看")
SPE ISR: 被唤醒 → 读 pend_slots(共享内存) → 才知道哪个槽位有新请求

中断是"门铃",共享内存才是"信箱"。 中断的作用不是传 pend_slots,而是触发对方去共享内存里读。

15.4 为什么这样设计

  • 批量通知:SPE 一次被唤醒可处理多个 pend 位,而中断一次只触发一次;若位图放中断载荷,N 个请求要打 N 次带数据的门铃。
  • 中断载荷极宝贵:核间中断通常只能带一两个字,放不下位图 + 状态。
  • 职责分离:中断管时序/唤醒,共享内存管数据/状态,各自简单。

16. 答疑:mailbox 的"指针传递"传什么?

只有一种指针:mailbox_init_t 结构体的地址,且只在握手阶段传一次。 之后所有通信走共享内存 + 门铃,不再传指针。

16.1 通道载荷清单

通道载荷 内容 时机
指针send_msg_ptr/fetch_msg_ptr mailbox_init_t 结构体地址 仅握手,一次
数据字send_msg_data 魔数:NS_MAILBOX_INIT_ENABLE / S_MAILBOX_READY / PSA_CLIENT_CALL_REPLY_MAGIC 握手 + 每次通知

16.2 指针指向什么

mailbox_init_t 是共享队列的"目录"(platform_spe_mailbox.c L20-L32):

c 复制代码
struct mailbox_init_t {
    struct mailbox_status_t *status;   /* 共享状态:pend_slots/replied_slots 位掩码 */
    uint32_t slot_count;               /* 槽位数量 */
    struct mailbox_slot_t  *slots;     /* 槽位数组(msg/reply) */
    uint32_t spinlock_addr;            /* 自旋锁地址 */
};

D25(NSPE)在共享内存里分配整个队列,把"目录"也放共享内存,只把目录地址经 mailbox 通道传给 N22。N22 收到后解引用挂接(L44-L47):

c 复制代码
platform_mailbox_fetch_msg_ptr((void **)&ns_init);   // 收到的是 mailbox_init_t 的地址
s_queue->ns_status    = ns_init->status;             // 解引用,拿到真正的指针
s_queue->ns_slot_count = ns_init->slot_count;
s_queue->ns_slots     = ns_init->slots;

16.3 为什么这样设计

  • 通道只有单字载荷 :放不下 status + slots + slot_count + 锁地址。
  • 打包成结构体放共享内存,通道只传"目录在哪"------一个 word 就够了
  • 握手完成后双方都知道队列地址(ns_statusns_slots),之后每次 PSA 调用只走共享内存 + 门铃(send_msg_data 的魔数),不再传指针。

16.4 注意:握手方向

DUAL_CORE_COMMUNICATION.md §3.1 画的"D25 读 mailbox_init_t"方向有误------实际代码(platform_spe_mailbox.c)是 D25 构造并发出指针、N22 接收,与标准 TF-M 一致(NSPE 分配队列、SPE 持指针访问)。


17. 答疑:整个 IPC 的 RAM 如何划分?为什么?

17.1 总体布局:三块区域

复制代码
D25 (NSPE)                         N22 (SPE)
┌──────────────────┐     ┌──────────────────────────────┐
│ NSPE 私有区       │     │ SPE 私有区                    │
│ · RTOS 任务/信号量│     │ · S_CODE / S_DATA(分区池)   │
│ · 无(队列在共享区)│     │ · secure_mailbox_queue_t    │
└────────┬─────────┘     │ · vectors[] 本地副本         │
         │               │ · SP 分区代码/栈             │
         │  共享传输区    └──────────┬───────────────────┘
         └───────┬──────────────────┘
                 ▼
        MAILBOX_SHARED_BASE (4KB, uncacheable)
        ├── mailbox_init_t(目录,仅握手用)
        ├── ns_mailbox_queue_t(队列本体)
        └── spinlock(锁变量)

17.2 每块包含什么(代码依据)

① 共享传输区 (唯一两核都能碰的区域,region_defs.hMAILBOX_SHARED_BASE/SIZE):

  • mailbox_init_t 目录platform_spe_mailbox.c L20-L32):status/slot_count/slots/spinlock_addr 四个字段,仅握手阶段使用。
  • ns_mailbox_queue_t 队列本体tfm_ns_mailbox.h L75-L95),cache line 对齐排布:
    • status(pend_slots + replied_slots,位图)
    • slots[N](每槽 = msg + reply,各占一个 cache line)
    • slots_ns[N](owner / reply / is_woken,NS 私有
    • empty_slots(空槽位图,NS 私有)、is_full
  • spinlock 变量:必须放共享区,否则 AMOSWAP 自旋失效。

② SPE 私有区secure_mailbox_queue_ttfm_hal_mailbox.h L26-L38)------empty_slots、本地 msg 副本、ns_slot_idxmsg_handle 等处理现场;vectors[] 向量快照(tfm_spe_mailbox.c L42-L55);各 SP 分区代码/栈。注意其中 ns_status/ns_slots 是指向共享区的指针,其余全在安全内存。

③ NSPE 私有区:D25 自己的 RTOS 任务栈、信号量等。队列本体不放这------因为队列必须在共享区。

17.3 五个关键划分决策

决策 1:队列放共享区、处理状态放私有区------共享"传输物",不共享"处理现场"。

  • ns_mailbox_queue_t 是 NSPE 写、SPE 读的消息载体,必须双方可见。
  • secure_mailbox_queue_t 是 SPE 处理过程的私有中间状态,NSPE 不需要也不允许看。
  • 安全隔离的根本要求:NSPE 只看得见共享区,SPE 一切内部状态必须留在 S_DATA,否则 NSPE 可篡改 agent 分区逻辑。

决策 2:SPE 把 msg 拷贝到私有内存再处理------防 TOCTOU + 安全边界。

tfm_spe_mailbox.c L357-L359 的 spm_memcpy

  • 不拷贝的话,SPE 处理长请求期间 NSPE 可改写槽位,读到前后不一致的参数 → 拷成快照。
  • SPM 只允许 SPE 通过"拷贝"这一受控动作读 NS 内存,之后处理全在安全内存进行,NSPE 再也碰不到。
  • vectors[] 同理,把 in/out 向量拷成快照再传给 SPM。共享区只是传输口,不是工作区。

决策 3:status 与 slots 分离------允许 NS 侧改槽位数而不重编 SPE。

tfm_mailbox.h L139-L144 注释:"status is separated from slots to allow flexible allocation of slots... safe to change number of slots on non-secure side without rebuild of TF-M." SPE 只通过指针 + 握手传入的 slot_count 访问,两侧镜像解耦。

决策 4:共享区按 cache line 对齐------防伪共享。

MAILBOX_ALIGNtfm_mailbox.h L29-L47):

  • msg 与 reply 各占一个 cache line,NSPE 只写 msg、SPE 只写 reply;若共享一行,一方 Clean/Invalidate 会反复使对方缓存行失效 → 伪共享抖动。
  • 共享区 uncacheable(MAILBOX_IS_UNCACHED_S/NS=1)时对齐自动退化省内存------cache 不参与,无需防伪共享。

决策 5:目录单独放一块、不直接传队列------单字载荷放不下。

门铃通道只有单字载荷(见 §16),队列含多项信息传不完 → 打包成目录放共享区,通道只传目录地址。目录与数据分离:通道传"元信息的位置",元信息里放"真实数据的位置"。

17.4 内存开销估算(默认 N=1)

内容 大小 位置
mailbox_init_t 16 B 共享区
status(pend+replied) 8 B(对齐后 32 B) 共享区
slots[1](msg+reply) 2×32 B cache line 共享区
slots_ns[1] ~32 B 共享区
empty_slots / is_full ~8 B 共享区
spinlock 4 B 共享区
共享区合计 ~128 B(4KB 配额绰绰有余)
secure_mailbox_queue_t N×~40 B SPE 私有
vectors[] N×~100 B SPE 私有

共享区只需几百字节,留 4KB 是为对齐余量与未来扩槽位;其余全部留在私有区,把暴露面压到最小。

17.5 划分原则总结

能共享的只有"传输物"(队列+锁+目录),一切"处理现场"(本地副本、向量快照、SPE 状态)留在私有区;共享区内部再按 cache 行为(对齐/uncacheable)与更新频率(status 独立)细分。 主线三条:安全隔离、cache 一致性、握手解耦。

17.6 区域可访问性矩阵(谁看得见谁)

不是所有区域双方都能访问。只有共享传输区是双方可见的;两个私有区各自只有所有者能访问,靠硬件强制隔离。

区域 内容 NSPE SPE 强制机制
NSPE 私有区 RTOS 任务/信号量、NS 栈堆 ✅ 独占 ❌ 不主动访问(NS 内存) NS 内存安全属性 + SPE 仅经受控 spm_memcpy 拷贝
共享传输区 mailbox_init_t、队列、spinlock ✅ 读写 ✅ 读写 双方约定地址(握手同步)+ uncacheable 属性
SPE 私有区 S_CODE/S_DATA、分区池、secure_mailbox_queue_tvectors[] ❌ 不可访问 ✅ 独占 SAU/MPU/PPC 安全隔离(NSPE 物理上碰不到)

三个层次的访问规则:

  1. 共享区:双方可访问,但按字段单向分工 (见第 3 节表格)------msg 只有 NSPE 写、reply 只有 SPE 写、pend_slots 只有 NSPE 置位、replied_slots 只有 SPE 置位。能访问 ≠ 能乱写,角色在 ABI 层面固定。
  2. SPE 私有区:NSPE 永远不可见 ------这是安全核心。SAU/MPU/PPC 把 S_CODE/S_DATA 标为安全属性,NSPE(非安全状态)访问直接触发 fault。NSPE 唯一能"看到"SPE 的窗口就是共享区里的 status/slots
  3. NSPE 私有区:SPE 不主动访问 ------即使安全侧理论上能读 NS 内存(Arm 安全/非安全内存模型允许 Secure 状态读 Non-Secure),SPE 也只通过 spm_memcpy 这一个受控入口读 NSPE 的 msg/reply,绝不直接摸 NSPE 的 RTOS 任务数据结构。这是软件层面的安全纪律(纵深防御)。

为什么这样设计(一句话):共享区是唯一的"外交窗口",窗口里只放传输协议数据;窗口两侧各自的内存都是主权领土,非所有者不可进入------NSPE 靠硬件隔离防 SPE 数据泄漏,SPE 靠硬件隔离 + 软件纪律双保险防 NSPE 篡改。

相关推荐
元岳数字人小元19 小时前
极简运维体系,数字人一体机适配中小场景升级
运维·人工智能·开源·人机交互·交互·源代码管理
见山是山-见水是水21 小时前
鸿蒙Panel 滑动面板完全指南:半屏/全屏切换、拖拽交互与地图应用
华为·交互·harmonyos
ZYJCSZKJ2 天前
AI数字人直播系统的技术架构与多方言交互实现方案——基于广西区域商业场景的工程实践
人工智能·架构·交互
柳叶方舟6 天前
Nature Medicine IF=50.0 | 公众医疗助手LLM可靠性存隐忧:真人交互表现未优于对照组,现有评估基准失效
论文阅读·人工智能·深度学习·交互·健康医疗
dwm888888887 天前
FluidVoice 智能语音交互落地实战指南
交互
人工智能研究所8 天前
GitHub 10k+ Star:用自然语言描述系统,AI 帮你生成可交互架构图
人工智能·github·交互·自然语言·系统架构图·archify
circuitsosk9 天前
构建高可用AI后端服务:REST API设计、数据库交互及异步任务编排经验总结
数据库·人工智能·python·交互·fastapi·数据库连接池·rest api
CDN3609 天前
俄语区跨境站点优化实战:CDN+Nginx 解决 H5 白屏、交互延迟、链路卡顿问题
运维·nginx·交互·h5 加速·nginx 轻交互调优
元岳数字人小元13 天前
数字人开源技术优势解析,助力智能交互普及落地
运维·人工智能·开源·人机交互·交互