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.h,S/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 与伪共享防护
msg 与 reply 强制按 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_status、ns_slots、ns_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 API (agent_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。
三种情况:
- 无 D-Cache(如低端 M0+):什么都不用做,宏退化。
- 有 D-Cache 且共享区可缓存:每次跨核访问前后必须配对 Clean/Invalidate。
- 共享区配置为 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. 代码中的简化点与升级方向
- 槽位一一对应 (
tfm_spe_mailbox.cL339-L344):直接用 NSPE 槽位索引作 SPE 槽位索引,未做动态空闲槽搜索。多并发时效率受限,但正确性无碍(empty_slots位图保证不冲突)。 - 空句柄回复第一个槽 (L420-L428):
handle == 0时默认回 slot 0。单并发够用;多并发存在被伪造的风险(注释已提示)。 - 消息校验是占位 (
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_status、ns_slots、ns_slot_count,见 platform_spe_mailbox.c 与 tfm_hal_mailbox.h 的 secure_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_CRYPTO(secure_fw/partitions/crypto/tfm_crypto.yaml)TFM_ATTEST(初始认证)TFM_ITS/TFM_PS(内部/受保护存储)TFM_FWU(固件更新)、TFM_SP_PLATFORM(平台控制)TFM_NS_MAILBOX_AGENT(ns_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_status、ns_slots)指向 NSPE 队列;它自己的spe_mailbox_queue与vectors[]都是安全内存里的静态变量 (tfm_spe_mailbox.cL42-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_slots 和 msg、replied_slots 一样,都是直接写在共享内存里 的字段;mailbox 中断(门铃)不携带任何数据,只负责唤醒对端去共享内存里读。
15.1 两个通道分开看
| 通道 | 内容 | 机制 |
|---|---|---|
| 共享内存 | msg、reply、pend_slots、replied_slots |
直接读写共享 RAM |
| 门铃(mailbox 中断) | 不携带任何数据 | 只触发对端中断 |
15.2 代码证据
NSPE 置位(tfm_ns_mailbox.c 的 mailbox_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.c 的 tfm_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_status、ns_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.h 的 MAILBOX_SHARED_BASE/SIZE):
mailbox_init_t目录 (platform_spe_mailbox.cL20-L32):status/slot_count/slots/spinlock_addr四个字段,仅握手阶段使用。ns_mailbox_queue_t队列本体 (tfm_ns_mailbox.hL75-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_t(tfm_hal_mailbox.h L26-L38)------empty_slots、本地 msg 副本、ns_slot_idx、msg_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_ALIGN(tfm_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_t、vectors[] |
❌ 不可访问 | ✅ 独占 | SAU/MPU/PPC 安全隔离(NSPE 物理上碰不到) |
三个层次的访问规则:
- 共享区:双方可访问,但按字段单向分工 (见第 3 节表格)------
msg只有 NSPE 写、reply只有 SPE 写、pend_slots只有 NSPE 置位、replied_slots只有 SPE 置位。能访问 ≠ 能乱写,角色在 ABI 层面固定。 - SPE 私有区:NSPE 永远不可见 ------这是安全核心。SAU/MPU/PPC 把 S_CODE/S_DATA 标为安全属性,NSPE(非安全状态)访问直接触发 fault。NSPE 唯一能"看到"SPE 的窗口就是共享区里的
status/slots。 - NSPE 私有区:SPE 不主动访问 ------即使安全侧理论上能读 NS 内存(Arm 安全/非安全内存模型允许 Secure 状态读 Non-Secure),SPE 也只通过
spm_memcpy这一个受控入口读 NSPE 的 msg/reply,绝不直接摸 NSPE 的 RTOS 任务数据结构。这是软件层面的安全纪律(纵深防御)。
为什么这样设计(一句话):共享区是唯一的"外交窗口",窗口里只放传输协议数据;窗口两侧各自的内存都是主权领土,非所有者不可进入------NSPE 靠硬件隔离防 SPE 数据泄漏,SPE 靠硬件隔离 + 软件纪律双保险防 NSPE 篡改。