当前位置:首页>面试真题>嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计

嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计

  • 2026-07-21 03:40:54
嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计

嵌入式面试真题第 10 题:高优化等级下共享状态可见性、内存模型与系统级同步设计

在这里插入图片描述

问题

在一个带有 RTOS 的嵌入式系统中,某个共享状态在调试版本或 -O0 下工作正常,但切换到 -O2-O3、LTO,或迁移到带数据 Cache、多核 CPU、DMA 和总线主设备的新平台后,消费者偶发看不到生产者刚刚写入的数据,或者出现“标志位已经变化,但数据体仍是旧值”“中断已经发生,任务仍在空转”“DMA 已完成,CPU 读取的缓冲区却没有更新”“单核正常,多核失效”等问题。

生产者可能来自按键扫描任务、设备驱动中断、协议接收线程、DMA、另一个 CPU 核、协处理器或外部总线主设备;消费者可能是状态机、控制任务、日志线程、UI、音视频处理线程或安全监控任务。共享对象也不一定只是一个布尔标志,它可能是一组状态位、计数器、描述符、环形缓冲区索引、消息结构体、双缓冲区、控制块或一段 DMA 共享内存。

请不要把问题简化为“加一个 volatile”。需要从 C/C++ 抽象机、编译器优化、寄存器分配、CPU 乱序执行、Store Buffer、Cache 一致性、DMA 非一致性、访问原子性、内存屏障和 RTOS 调度唤醒等层面解释现象,并设计一套能够覆盖任务与任务、ISR 与任务、核与核、CPU 与 DMA、CPU 与 MMIO 设备之间通信的通用同步方案。还需要说明如何选择 atomic、临界区、互斥锁、信号量、事件、队列、任务通知、Ring Buffer、双缓冲和 seqlock 等机制,以及如何验证方案在高优化等级和压力条件下仍然正确。

回答

结论:-O3 通常不是“把一个本来正确的程序优化坏了”,而是把原有的未定义行为、数据竞争、发布顺序缺失、原子性不足、Cache 维护遗漏或唤醒机制缺失暴露出来。volatile 只约束特定对象的编译器可观察访问,它不等价于原子操作,不构成互斥,不建立跨线程的 happens-before 关系,不自动刷新或失效 Cache,也不会把一个轮询任务变成可靠的事件同步方案。

通用解法不是围绕某个全局变量不断叠加补丁,而是先识别通信边界,再同时解决四个问题:

  1. 1. 原子性:一次读改写是否可能被中断、抢占或另一个核打断。
  2. 2. 可见性:生产者写入的内容何时能被消费者观察到。
  3. 3. 有序性:数据体和发布标志、描述符和 doorbell、DMA buffer 和完成位之间的先后关系是否被保证。
  4. 4. 等待与唤醒:消费者是忙等、阻塞等待还是事件驱动;生产者能否在正确上下文安全地唤醒消费者。

工程上应优先采用“单一所有者 + 消息传递 + RTOS 同步原语”的设计。只有在性能、延迟或内存约束确实要求共享内存时,才使用明确的 atomic acquire/release、短临界区、SPSC Ring Buffer、双缓冲或 seqlock,并为 DMA 与非一致性 Cache 增加显式 clean/invalidate 和所有权转移协议。

总体架构

任务生产者
同步边界 / 通信 API
ISR 生产者
其他 CPU 核
DMA / 外部总线主设备
MMIO 设备
通信模型选择
消息传递\nQueue / Mailbox / Notification
共享标量\nAtomic / Critical Section
共享数据流\nSPSC Ring Buffer
快照数据\nDouble Buffer / Seqlock
设备共享内存\nDMA Ownership + Cache Maintenance
消费者任务 / 状态机
Ordering\nAcquire / Release / Barrier
Wakeup\nSemaphore / Event / IRQ / IPI
Observability\nTrace / Counter / Timeout

这套架构的核心不是规定所有场景都必须使用同一个 API,而是把通信问题放在一个统一框架里处理。生产者和消费者必须通过明确的同步边界交换所有权或发布状态,而不是默认“写了全局变量,另一个上下文自然就会看到”。

共享状态的可靠交付可以抽象为以下协议:

生产者准备数据
    -> 确保数据写入完成
    -> 以 release 语义发布状态或描述符
    -> 触发事件、通知、中断或 doorbell
    -> 消费者被唤醒
    -> 以 acquire 语义读取发布状态
    -> 读取与该状态关联的数据
    -> 处理完成后归还所有权或更新消费位置

如果通信对象是 DMA buffer,还需要在“确保数据写入完成”和“消费者读取数据”之间插入与 Cache 属性相匹配的 clean、invalidate 或同步 API;如果对象是 MMIO 寄存器,还要使用平台规定的 I/O accessor 和 barrier,而不是普通指针访问。

机制与 Linux/RTOS/开源实现的对应关系

本文机制
Linux 或开源实现
能否直接使用
主要参考价值
编译器可见访问
Linux READ_ONCE() / WRITE_ONCE()
Linux 内核可直接使用;裸机和 RTOS 只能参考语义
防止编译器合并、拆分或重复构造特定共享访问,但不单独提供完整同步。
内存顺序
Linux smp_load_acquire()smp_store_release()smp_mb();C11 <stdatomic.h>
可直接使用对应平台 API
用 release 发布数据,用 acquire 消费数据,建立跨执行上下文的顺序。
单值原子更新
C11 atomic、GCC __atomic_*、Zephyr atomic_*
支持 C11 或对应 RTOS 时可直接使用
计数器、状态位、引用计数、一次性状态转换。
临界区与锁
FreeRTOS critical section、Zephyr spinlock/mutex、RT-Thread mutex/critical
可直接使用
保护多字段不变量、复合状态更新和短时共享资源。
事件唤醒
FreeRTOS task notification/event group/semaphore、Zephyr semaphore/poll、RT-Thread event/semaphore
可直接使用
避免轮询,形成可靠的生产者到消费者通知路径。
消息传递
FreeRTOS Queue、Zephyr k_msgq/mailbox、RT-Thread mailbox/message queue
可直接使用
把数据和同步合并为一个内核对象,降低共享变量数量。
数据流
Linux kfifo、Zephyr ring_buf、FreeRTOS Stream/Message Buffer
视系统而定
单生产者单消费者、固定内存、批量搬运、claim/finish、背压。
快照读取
Linux seqlock/seqcount、双缓冲、RCU 思路
多数小系统需自实现或封装
写少读多、不允许读者阻塞、需要一致快照。
DMA 一致性
Linux DMA API、CMSIS Cache API、芯片 HAL Cache API
必须按平台直接使用
所有权转移、Cache line 对齐、clean/invalidate、描述符顺序。
调试验证
GCC/Clang 汇编输出、ThreadSanitizer、Linux Kernel Concurrency Sanitizer、RTOS trace
部分可直接使用
发现数据竞争、验证优化后指令、观察唤醒与延迟。

先建立正确的问题模型

很多“优化后变量失效”的讨论把编译器、寄存器和 Cache 混成一个原因。实际上它们位于不同层级,必须分别诊断。

源代码中的共享变量
C/C++ 抽象机与数据竞争规则
编译器优化\n常量传播 / Load Hoisting / Store Elimination
机器指令与寄存器分配
CPU 执行\n乱序 / 推测 / Store Buffer
Cache 与一致性域
片上互连 / 内存控制器
SRAM / DDR / 外设 / DMA
RTOS 调度与唤醒

一个症状可能由多个层级共同触发。例如:

  • • 源代码存在任务间无同步读写,语言层面已经是数据竞争。
  • • -O0 每次循环都碰巧从内存加载,所以看起来可用。
  • • -O3 把 load 提出循环,寄存器里永远保留旧值。
  • • 把变量改成 volatile 后,CPU 确实重新发起 load,但另一个核的写入尚未通过一致性协议可见,或者数据体与标志位顺序不匹配。
  • • 再加 barrier 后,任务仍然延迟很高,因为它依旧在轮询,没有正确使用事件唤醒。

因此,任何修复都应该回答:问题在哪一层,修复了哪一种保证,是否还缺少其他层的保证。

为什么 -O0 正常而 -O3 失效

-O0 只是偶然保留了更多内存访问

在未优化构建中,编译器通常会生成更多栈读写和内存 load/store,局部变量也更少长时间驻留寄存器。调试器观察变量更容易,执行速度更慢,任务切换和中断时序也不同。这些特征会掩盖并发缺陷,但不构成任何标准保证。

下面的代码没有同步语义:

staticuint32_t g_ready;
staticuint8_t g_payload[64];

voidproducer(void)
{
    fill_payload(g_payload, sizeof(g_payload));
    g_ready = 1U;
}

voidconsumer(void)
{
while (g_ready == 0U) {
/* Busy wait. */
    }

    use_payload(g_payload, sizeof(g_payload));
}

对编译器而言,如果它无法从当前线程的可见控制流推导出 g_ready 会被合法修改,循环中的 load 可能被提升到循环外,甚至整个循环被变换。对多线程 C/C++ 程序而言,无同步并发读写本身就可能构成数据竞争;一旦进入未定义行为,不能再用“源码看起来每次都读了”推断机器代码。

高优化会做哪些合法变换

编译器可能进行以下变换:

  1. 1. Load Hoisting:把循环内不变的 load 提到循环外。
  2. 2. Common Subexpression Elimination:复用已经读取的值,不再次加载。
  3. 3. Dead Store Elimination:删除后续一定被覆盖、且对单线程可观察行为无影响的写入。
  4. 4. Store Merging:把多个相邻写合并成更宽的 store。
  5. 5. Load/Store Reordering:在不破坏抽象机单线程语义的前提下调整访问顺序。
  6. 6. Scalar Replacement:把结构体字段拆成寄存器中的独立标量。
  7. 7. Interprocedural Optimization 与 LTO:跨函数甚至跨编译单元证明某些对象不会变化。
  8. 8. Loop Unswitching/Unrolling:复制循环并把条件外提,使错误表现与 -O0 完全不同。

这些优化不是异常行为。真正的问题是程序没有通过 atomic、锁、事件或平台同步 API 告诉编译器“这里存在合法的跨上下文通信”。

优化等级还会改变时序

即使最终生成的访问顺序没有明显错误,-O3 也会改变:

  • • 临界区持续时间;
  • • 中断响应窗口;
  • • 任务运行速度和抢占点;
  • • Ring Buffer 被填满或耗尽的速度;
  • • DMA 与 CPU 的相对完成时刻;
  • • 日志输出对调度的扰动;
  • • 看门狗、超时和重试窗口。

因此,“加日志后问题消失”“单步调试不复现”“关闭优化正常”都是典型的竞态或时序敏感信号,而不是证明硬件一定有故障。

CPU 寄存器到底扮演什么角色

“变量被 CPU 缓存在寄存器里”是常见说法,但需要更精确。

编译器负责把抽象变量映射到寄存器、栈或内存。只要语言规则允许,它可以让一个值在多个指令周期甚至整个循环期间驻留寄存器。CPU 并不知道这个寄存器原来对应 C 语言中的哪个全局变量,它只执行编译器生成的指令。

内存CPU 寄存器优化器源代码内存CPU 寄存器优化器源代码loop[优化后的循-环]while (flag == 0) {}生成一次 load flagflag = 0比较寄存器值

如果机器码没有再次加载内存,Cache 是否一致已经不重要,因为 CPU 根本没有发起新的内存访问。此时应通过正确的语言级同步原语修复,而不是尝试手工清 Cache。

volatile 可以迫使实现保留对该 volatile 对象的可观察访问,但它仍然没有自动建立“先写数据体,再发布标志”的跨线程顺序,也不保证复合操作原子。

volatile 能做什么,不能做什么

合理用途

在典型嵌入式 C 实现中,volatile 主要用于:

  • • 访问具有副作用的 MMIO 寄存器;
  • • 表达值可能被当前控制流之外的硬件或异步上下文改变;
  • • 配合特定编译器、ABI 和平台约定访问设备状态;
  • • 保证某些调试、轮询或时序相关访问不会被完全省略。

但即便是 MMIO,也应优先使用芯片 SDK、CMSIS、Linux readl()/writel() 或平台封装,因为寄存器宽度、访问顺序、端序、posted write 和 barrier 需求并不由 volatile 本身解决。

明确不能提供的保证

需求
volatile
 是否保证
正确机制
每次表达式都按实现要求访问对象
通常可以
volatile
 或平台 accessor
counter++
 不被并发打断
atomic fetch-add 或锁
结构体整体一致读取
锁、双缓冲、seqlock
多核之间建立发布/获取顺序
release/acquire 或锁
让 DMA 写入自动使 CPU D-Cache 失效
DMA sync / cache invalidate
让 CPU 写回自动对 DMA 可见
cache clean / DMA sync
避免忙等浪费 CPU
semaphore、event、notification、queue
防止多个生产者同时破坏 Ring Buffer
MPSC 算法或外部串行化
保证 MMIO 写已经到达设备
平台 I/O barrier、read-back、DSB 等
建立任务间 happens-before
RTOS 同步原语、atomic、锁

GCC 官方文档还特别提醒,volatile 访问与普通非 volatile 访问之间的顺序不能简单依赖;如果需要把普通数据写入和 volatile doorbell 绑定,必须使用适当的编译器/CPU barrier 或平台 I/O API。

volatile 与 atomic 不是互斥关系

少数场景中对象既具有设备可变性,又需要原子访问,但不能简单写成 _Atomic volatile 就认为所有问题结束。MMIO 寄存器通常不允许编译器生成通用原子 read-modify-write 指令;某些原子指令会访问总线多次,可能破坏设备语义。设备寄存器应遵循硬件手册规定的 set/clear 寄存器、独占访问能力和 barrier 要求。

四类保证必须同时考虑

原子性

原子性回答“一次操作是否不可分割”。以下表达式即使底层整数宽度与 CPU 字长相同,也未必是单个原子操作:

counter++;
flags |= MASK;
state = next_state(state);
object.field_a = x;
object.field_b = y;

counter++ 通常包含 load、add、store。中断或另一个核可能在中间插入,造成更新丢失。结构体多字段更新更不可能天然原子。

可见性

可见性回答“一个执行上下文的写何时能被另一个上下文看到”。它涉及:

  • • 编译器是否真的发出访问;
  • • CPU 是否把 store 留在 Store Buffer;
  • • Cache 一致性域是否覆盖双方;
  • • DMA 是否绕过 CPU Cache;
  • • 共享内存属性是否配置正确;
  • • 是否执行了适当的 Cache 维护和同步指令。

有序性

有序性回答“消费者看到发布标志时,是否一定也能看到之前的数据”。典型错误如下:

生产者源码顺序:写 payload -> 写 ready
消费者源码顺序:读 ready  -> 读 payload

如果没有 release/acquire 或锁,编译器和 CPU 都可能使消费者观察到 ready == 1,但 payload 的某些写尚不可见。

等待与唤醒

即使数据同步完全正确,忙等仍可能造成:

  • • 低优先级生产者得不到调度,消费者反而一直占用 CPU;
  • • 功耗升高;
  • • 系统实时性恶化;
  • • 单核优先级反转式“自锁”;
  • • 超时逻辑难以统一;
  • • 无法统计事件丢失和积压。

所以系统级方案通常应使用事件、任务通知、信号量或队列阻塞等待,而不是无限轮询。

编译器屏障、CPU 屏障与同步原语的区别

Syntax error in textmermaid version 11.4.1

编译器屏障

编译器屏障只约束编译器,不一定生成 CPU 指令。它可以防止普通内存访问跨越某个点被编译器重排,但不能迫使 Store Buffer 排空,也不能让另一个 CPU 核的 Cache 自动更新。

CPU 内存屏障

CPU 屏障约束处理器对内存访问的观察顺序。不同架构的屏障能力不同:ARM 常见 DMBDSBISB;RISC-V 有 fence;x86 的普通内存模型更强,但仍不能把编译器屏障和 CPU 屏障混为一谈。

同步原语

互斥锁、信号量、队列等 RTOS 原语通常同时承担多个职责:

  • • 串行化访问;
  • • 包含必要的编译器和 CPU 内存顺序;
  • • 管理任务阻塞和唤醒;
  • • 在部分 RTOS 中处理优先级继承;
  • • 提供超时和错误码。

因此,在没有极端性能需求时,优先使用经过验证的 RTOS 原语,比手写 barrier + 标志位更稳健。

Acquire/Release:最常用的发布与消费模型

假设生产者准备一份数据,然后发布 ready;消费者观察到 ready 后读取数据。C11 atomic 可以表达如下:

#include<stdatomic.h>
#include<stdbool.h>
#include<stddef.h>
#include<stdint.h>

#define PAYLOAD_SIZE 64U

typedefstruct {
uint32_t sequence;
uint8_t data[PAYLOAD_SIZE];
payload_t;

staticpayload_t g_payload;
staticatomic_bool g_ready = ATOMIC_VAR_INIT(false);

voidproducer_publish(constuint8_t *src, size_t len)
{
size_t copy_len = len < PAYLOAD_SIZE ? len : PAYLOAD_SIZE;

    g_payload.sequence++;
for (size_t i = 0U; i < copy_len; ++i) {
        g_payload.data[i] = src[i];
    }

    atomic_store_explicit(&g_ready, true, memory_order_release);
}

boolconsumer_try_read(payload_t *out)
{
if (!atomic_load_explicit(&g_ready, memory_order_acquire)) {
returnfalse;
    }

    *out = g_payload;
    atomic_store_explicit(&g_ready, false, memory_order_release);
returntrue;
}

release store 保证它之前的普通写不会在同步意义上跑到发布之后;与之匹配的 acquire load 保证消费者在观察到该发布后,后续普通读能够看到发布前的数据。

这段示例仍然只适合“至多一个未消费对象”的简单协议。若生产者可能覆盖尚未消费的数据,需要队列、双缓冲、序号或背压;若有多个生产者或多个消费者,需要更强的并发算法或外部锁。

不应默认使用 memory_order_relaxed

relaxed atomic 只保证该原子对象本身的读改写原子性,不给其他普通数据建立顺序。它适合统计计数器、独立标志或不参与数据发布的引用计数局部步骤,但不适合单独发布一段 payload。

不必滥用全序一致

memory_order_seq_cst 最容易理解,但在部分弱内存序架构上成本更高。系统设计先以正确性为目标,可以从 seq_cst 起步,再在有测量依据时改为 acquire/release。不要为了“看起来快”直接使用 relaxed。

不同通信边界的风险矩阵

通信边界
主要风险
推荐主机制
额外要求
同一任务内部
逻辑顺序、可重入
普通变量
无并发时不需要同步。
任务与任务,单核
抢占、数据竞争、优先级反转
Queue、mutex、event、notification
需要超时和优先级策略。
ISR 与任务,单核
ISR 上下文限制、丢事件、非原子复合更新
FromISR API、notification、semaphore、SPSC ring
ISR 不阻塞;必要时请求调度。
核与核,硬件一致性 Cache
编译器和 CPU 重排、伪共享
atomic acquire/release、spinlock、IPI
Cache line 对齐,避免独立热点共享一行。
核与核,非一致性共享内存
Cache 不一致、所有权混乱
mailbox + cache maintenance + barrier
明确谁 clean、谁 invalidate。
CPU 与 DMA
DMA 绕过 D-Cache、描述符顺序
DMA API、双缓冲、descriptor ownership
buffer 对齐、长度向 Cache line 扩展。
CPU 与 MMIO
posted write、设备访问顺序、副作用
readl/writel 或 HAL accessor、I/O barrier
遵循芯片手册,必要时 read-back。
CPU 与协处理器/FPGA
协议不同步、doorbell 早于数据
共享描述符 + release + doorbell
版本、序号、CRC、超时恢复。

系统级首选方案:单一所有者与消息传递

最稳健的设计是让每组业务状态只有一个任务拥有。ISR、其他任务和外设不直接修改状态机内部变量,而是提交事件。状态迁移只发生在所有者任务中。

Other TasksState Owner TaskEvent QueueISR / DriverOther TasksState Owner TaskEvent QueueISR / Driverenqueue BUTTON_EDGE / RX_DONE / TIMEOUTenqueue START / STOP / CONFIG_UPDATEwake upvalidate eventupdate state and related data atomically in task contextpublish result / response event

这种结构有几个直接收益:

  1. 1. 状态机本体不需要被多个上下文并发写。
  2. 2. 事件携带参数和时间戳,避免“只改一个标志但丢失上下文”。
  3. 3. 队列天然记录积压,可设置容量和溢出策略。
  4. 4. 消费者阻塞等待,不需要忙轮询。
  5. 5. 单元测试可以直接向状态机注入事件。
  6. 6. 后续从单核迁移到多核时,通信边界已经清晰。

事件结构建议

typedefenum {
    APP_EVENT_BUTTON_EDGE = 0,
    APP_EVENT_RX_READY,
    APP_EVENT_DMA_DONE,
    APP_EVENT_TIMEOUT,
    APP_EVENT_CONFIG_UPDATE,
    APP_EVENT_SHUTDOWN
app_event_type_t;

typedefstruct {
app_event_type_t type;
uint32_t source;
uint32_t timestamp;
union {
struct {
uint32_t pin;
uint32_t level;
        } button;
struct {
void *buffer;
size_t length;
uint32_t sequence;
        } data;
struct {
uint32_t code;
uint32_t detail;
        } error;
    } payload;
app_event_t;

事件要么完整复制到队列,要么只传递受所有权协议保护的指针。不能把指向 ISR 栈、临时局部变量或即将被复用 DMA buffer 的裸指针放入队列。

RTOS 同步原语如何选

选择表

原语
最适合的场景
数据传递
计数能力
ISR 可用性
主要风险
Task Notification
一个任务的轻量事件、位图或计数
少量整数/位
可配置
通常有 FromISR API
目标任务固定,复杂消息表达能力有限。
Binary Semaphore
完成事件、资源可用通知
0/1
通常支持 give from ISR
连续多次事件可能合并。
Counting Semaphore
资源数量、累计事件
通常支持
只有数量,没有事件参数。
Event Group/Flags
多条件组合等待
位图
位级
ISR 支持受实现限制
不适合计数;清位时机要明确。
Queue/Message Queue
带参数事件、命令、工作项
以队列深度计
通常支持非阻塞 ISR 发送
拷贝成本、队列满策略。
Mailbox
指针或固定消息的异步交付
依实现
依实现
生命周期和所有权。
Mutex
任务间共享复杂对象
ISR 不可用
死锁、长临界区、优先级反转。
Critical Section
极短的本核共享更新
适合短路径
关中断时间过长会破坏实时性。
Spinlock
多核极短共享更新
平台相关
持锁者被抢占或阻塞会严重浪费 CPU。

任务通知

当 ISR 只需要唤醒一个固定任务,并传递位图、计数或一个整数值时,任务通知通常比队列更轻。典型做法是 ISR 只完成最小硬件处理,然后通知驱动任务执行后半部。

ISR:
    clear interrupt source
    capture minimal status
    notify driver task
    request context switch if a higher-priority task was woken

Driver task:
    block on notification
    drain hardware/FIFO
    perform protocol parsing
    send high-level event to state owner

队列

队列适合事件参数不可丢失、顺序重要、需要背压的场景。队列满时必须预先定义策略:

  • • 丢最新事件;
  • • 丢最旧事件;
  • • 合并同类事件;
  • • 提升消费者优先级;
  • • 记录过载并进入降级;
  • • 对关键事件使用独立保留通道。

不能在 ISR 中无限等待队列空间。

互斥锁

互斥锁适合任务间保护需要维持多字段不变量的对象。持锁区内不得执行不确定时长的 I/O、日志格式化、动态内存分配或等待另一个事件,否则容易造成优先级反转和长尾延迟。

临界区

单核 MCU 上,极短的 ISR/任务共享更新可以通过关中断或提升中断屏蔽级别保护。但临界区只应覆盖真正不可分割的几条操作。

uint32_t key = platform_enter_critical();
shared->head = new_head;
shared->count++;
platform_exit_critical(key);

临界区不能被当成“任何并发问题都关中断”的万能方案。多核系统中关闭本核中断并不能阻止另一个核访问共享内存。

ISR 与任务同步的通用流程

硬件中断
读取并清除最小状态
数据量小?
FromISR Queue/Notification
写入预分配 SPSC Ring 或 DMA Descriptor
发布 producer index
FromISR Semaphore/Notification
请求必要的任务切换
任务被唤醒
批量消费并做复杂处理

ISR 设计原则:

  1. 1. 不阻塞,不获取普通 mutex。
  2. 2. 不进行长时间格式化、协议解析或大块复制。
  3. 3. 先处理硬件必须的清中断顺序。
  4. 4. 使用 RTOS 明确提供的 ISR-safe API。
  5. 5. 事件计数不可丢时,使用计数通知、队列或 Ring Buffer,而不是单一布尔标志。
  6. 6. 若一次中断可能对应多个硬件项,任务被唤醒后应 drain 到空,而不是假设一次通知只处理一个项。
  7. 7. 记录溢出、覆盖、队列满和最大延迟指标。

为什么布尔标志会丢事件

ISR 触发 1: flag = true
ISR 触发 2: flag = true
任务运行:   只观察到一次 true

如果业务关心两次独立事件,布尔变量没有计数能力。应改用计数信号量、队列、单调序号或生产者索引。

SPSC Ring Buffer:共享内存数据流的首选结构

当生产者和消费者各只有一个,且数据量较大、频率较高时,单生产者单消费者 Ring Buffer 可以避免每个数据项都进入内核队列。

write payload

release store head

acquire load head

read payload

release store tail

acquire load tail

Producer\nISR / DMA completion / Task
Ring Buffer
Head
Consumer Task
Tail

关键不变量:

  • • 生产者只写 head,消费者只写 tail
  • • 生产者先填数据,再 release 发布新 head
  • • 消费者 acquire 读取 head 后,才能读取对应数据。
  • • 消费者处理完数据,再 release 更新 tail
  • • 生产者 acquire 读取 tail 后,才能复用已释放空间。
  • • 索引宽度和回绕算法必须经过证明。
  • • 多生产者或多消费者不能直接复用 SPSC 实现。

简化的 C11 SPSC 示例

#include<stdatomic.h>
#include<stdbool.h>
#include<stddef.h>
#include<stdint.h>

#define RING_CAPACITY 128U

typedefstruct {
uint32_t value;
uint32_t timestamp;
ring_item_t;

typedefstruct {
ring_item_t items[RING_CAPACITY];
atomic_size_t head;
atomic_size_t tail;
spsc_ring_t;

staticsize_tring_next(size_t index)
{
return (index + 1U) % RING_CAPACITY;
}

boolspsc_ring_push(spsc_ring_t *ring, constring_item_t *item)
{
size_t head = atomic_load_explicit(&ring->head, memory_order_relaxed);
size_t next = ring_next(head);
size_t tail = atomic_load_explicit(&ring->tail, memory_order_acquire);

if (next == tail) {
returnfalse;
    }

    ring->items[head] = *item;
    atomic_store_explicit(&ring->head, next, memory_order_release);
returntrue;
}

boolspsc_ring_pop(spsc_ring_t *ring, ring_item_t *item)
{
size_t tail = atomic_load_explicit(&ring->tail, memory_order_relaxed);
size_t head = atomic_load_explicit(&ring->head, memory_order_acquire);

if (tail == head) {
returnfalse;
    }

    *item = ring->items[tail];
    atomic_store_explicit(&ring->tail, ring_next(tail), memory_order_release);
returntrue;
}

该示例只表达核心顺序,实际工程还应考虑:

  • • 是否允许在 ISR 中调用所选 atomic 实现;
  • • size_t 原子操作是否 lock-free;
  • • Cache line 对齐与伪共享;
  • • 32 位索引在长时间运行后的回绕;
  • • 批量 claim/commit;
  • • 水位统计和溢出策略;
  • • DMA 直接写入时的 Cache 维护;
  • • 消费者阻塞唤醒机制。

Zephyr ring_buf 的 claim/finish API、Linux kfifo 以及 FreeRTOS Stream Buffer 都可以作为接口设计参考。需要注意,某个组件支持 SPSC 并不意味着它自动适用于 SMP 或 DMA;仍要阅读其并发和内存顺序约束。

双缓冲:把生产和消费空间分离

双缓冲适合一帧、一块或一组数据在生产完成后整体交付的场景,例如传感器扫描结果、图像行块、音频帧、控制参数快照、网络报文批次或推理输入。

生产完成

release 发布索引

消费者 acquire 获取索引

消费完成

归还生产者

生产完成

BufferAWrite
BufferAReady
Swap
BufferARead
BufferAFree
BufferBWrite
BufferBReady
BufferBRead
BufferBFree

双缓冲不能只用一个无保护的 active_index。必须明确:

  • • 哪个 buffer 属于生产者;
  • • 哪个 buffer 属于消费者;
  • • 生产完成后如何原子交换索引;
  • • 消费者未完成时生产者是否允许覆盖;
  • • 如果生产速度高于消费速度,是阻塞、丢帧还是覆盖旧帧;
  • • DMA 完成时由谁做 invalidate;
  • • 索引发布是否具备 release/acquire 语义。

对于不能丢帧的场景,双缓冲可能不足,应扩展为 N-buffer 或描述符队列。对于允许只保留最新状态的 UI、遥测和控制参数,双缓冲可以采用“最新值覆盖”策略,但需要增加版本号,消费者可检测是否跳帧。

Seqlock/Seqcount:写少读多的一致快照

当多个读者需要低延迟读取一组相关字段,写者很少且写操作短,seqlock 或 sequence counter 是一种可参考的方案。

基本思想:

  1. 1. 写者开始更新前把序号变为奇数。
  2. 2. 写者完成所有字段后把序号变为下一个偶数。
  3. 3. 读者先读序号,再复制数据,最后再次读序号。
  4. 4. 如果两次序号不同或序号为奇数,说明读取期间发生了写入,读者重试。
ReaderShared DataSequenceWriterReaderShared DataSequenceWriteralt[seq1 ==seq2 andeven][changedor odd]seq = oddupdate field A/B/Cseq = even (release)read seq1 (acquire)copy A/B/Cread seq2 (acquire)accept snapshotretry

适用条件:

  • • 写操作很短;
  • • 读者可以重试;
  • • 读者不能在复制过程中产生不可撤销副作用;
  • • 共享对象不包含在读取期间可能释放的裸指针,除非另有生命周期机制;
  • • 写者之间仍需互斥,seqcount 本身通常不解决多写者并发。

不适合:

  • • 写者长期持有更新状态;
  • • 硬实时读者不能接受不确定次数重试;
  • • 数据中包含复杂资源所有权;
  • • 读操作本身非常大,重试成本高;
  • • 写入频率接近或高于读取频率。

Linux seqlock/seqcount 文档和实现可以作为原理参考,但在 MCU 上不应只复制几行代码而忽略架构 barrier、写者串行化和对象生命周期。

多字段状态为什么需要不变量保护

考虑以下对象:

typedefstruct {
uint32_t state;
uint32_t error_code;
uint32_t timestamp;
uint8_t payload[32];
status_snapshot_t;

如果生产者按顺序更新字段,消费者可能读到:

  • • 新 state + 旧 error_code
  • • 新 timestamp + 半更新 payload
  • • 已复位状态 + 上一周期错误码;
  • • 指针已经更新,但其目标对象尚未初始化完成。

这类问题不是把每个字段都声明为 atomic 就能自然解决,因为业务需要的是整体不变量。常见正确方案:

  1. 1. mutex 保护整个对象;
  2. 2. 在短临界区内复制快照;
  3. 3. 双缓冲后原子发布索引;
  4. 4. seqlock 让读者验证一致性;
  5. 5. 通过消息队列直接复制完整结构体;
  6. 6. 使用不可变对象,构造完成后发布指针,并通过引用计数/RCU 处理生命周期。

选择标准不是字段数量,而是读写频率、实时性、是否允许重试、对象大小和生命周期复杂度。

DMA 与 CPU Cache:最容易被误判为 volatile 的问题

为什么 CPU 看不到 DMA 新数据

在非一致性 DMA 平台上,DMA 直接写物理内存,CPU 可能仍持有该区域旧的 D-Cache line。DMA 完成中断只能说明 DMA 引擎完成了总线事务,不意味着 CPU Cache 自动失效。

write new data

read without invalidate

DMA Engine
Physical Memory
CPU Core
D-Cache: old copy
DMA Complete IRQ

此时把 buffer 声明为 volatile 只会让 CPU 按代码重新执行 load,但 load 仍可能命中旧 Cache line。

CPU 到 DMA 的发送方向

CPU 填充待发送 buffer 后,如果数据仍停留在 D-Cache 的脏行中,DMA 从内存读到的可能是旧内容。因此交付给 DMA 前通常需要 clean/write-back,并保证描述符发布顺序正确。

DMA 到 CPU 的接收方向

DMA 写入内存后,CPU 消费前通常需要 invalidate。不同平台 DMA API 可能在 map/sync/unmap 中完成这些动作;裸机和 RTOS 项目常由芯片 HAL 或 CMSIS Cache API承担。

通用所有权协议

CPU owns TX buffer:
    CPU writes payload
    cache clean payload/descriptor
    release barrier
    set descriptor OWN = DMA
    ring DMA doorbell

DMA owns TX buffer:
    CPU must not modify payload

DMA completes:
    hardware clears OWN / raises interrupt
    CPU acquire observes completion
    CPU may reuse buffer

DMA owns RX buffer:
    CPU has prepared and invalidated buffer as required

DMA completes RX:
    CPU receives completion
    invalidate RX cache lines
    acquire barrier
    CPU reads payload

Cache line 对齐与范围扩展

Cache 维护通常按 Cache line 粒度执行。若 buffer 起止地址未对齐,invalidate 可能连带丢弃同一 Cache line 中其他对象的脏数据。工程上应:

  • • buffer 起始地址按 Cache line 对齐;
  • • buffer 长度向上取整到 Cache line;
  • • 描述符与频繁变化的控制字段独立放置;
  • • 不让 DMA buffer 与普通可写对象共享同一 Cache line;
  • • 使用 linker section 或静态内存池集中管理 DMA 区域;
  • • 阅读芯片关于 cacheable、shareable、device memory 属性的要求。

不要假设所有 Cortex-M 都没有 Cache

Cortex-M7、部分 Cortex-M33/M55 平台以及带外部存储控制器的 SoC 可能具有 I-Cache/D-Cache。即使单核,CPU 与 DMA 之间仍可能不一致。迁移平台时必须重新审查内存属性和 DMA API,而不是沿用低端 MCU 的经验。

多核与 Cache 一致性

一致性不等于顺序正确

硬件 Cache coherence 通常保证同一 Cache line 的副本最终按协议一致,但它不替代编译器和 CPU 内存顺序。一个核可能先让发布标志可见,另一个核随后读取到标志,却尚未按预期观察到数据体。

因此多核共享内存仍需 acquire/release、锁或内核同步原语。

伪共享

两个逻辑上无关、但高频写入的变量落在同一 Cache line,会导致 Cache line 在核之间反复迁移。

Cache line 0:
    producer_head   // CPU0 frequently writes
    consumer_tail   // CPU1 frequently writes

即使算法完全正确,性能也可能剧烈下降。应把不同核写入的热点字段按 Cache line 分离:

#define CACHE_LINE_SIZE 64U

typedefstruct {
_Alignas(CACHE_LINE_SIZE) atomic_size_t producer_head;
uint8_t producer_pad[CACHE_LINE_SIZE - sizeof(atomic_size_t)];
_Alignas(CACHE_LINE_SIZE) atomic_size_t consumer_tail;
ring_index_t;

实际 Cache line 大小和对齐能力必须由平台确认,不能硬编码后忽略其他架构。

IPI 与共享内存发布

核间通信常见流程:

CPU0 writes message
CPU0 release-publishes descriptor
CPU0 triggers IPI/doorbell
CPU1 handles IPI
CPU1 acquire-loads descriptor
CPU1 reads message

不能先触发 IPI 再写数据,除非硬件协议明确提供等价顺序保证。

MMIO:设备寄存器不是普通内存

MMIO 访问可能具有副作用:

  • • 读状态寄存器会清除中断;
  • • 写 1 清零;
  • • 某些寄存器要求特定宽度访问;
  • • 写入通过总线桥 posted,CPU 指令完成但设备尚未收到;
  • • 配置寄存器和启动寄存器存在严格顺序;
  • • 设备内存类型不允许缓存、合并或推测访问。

典型错误

regs->descriptor = desc_addr;
regs->control = START_BIT;

如果 descriptor 的普通内存内容尚未对设备可见,或者两次 MMIO 写被平台以不符合设备要求的方式排序,设备可能读取到旧描述符。

推荐方式

  • • 使用供应商 HAL、CMSIS 或 OS 提供的寄存器访问函数;
  • • 在写 doorbell 前使用适当的内存/设备 barrier;
  • • 对 posted write 按手册要求读回状态寄存器;
  • • 区分 DMBDSBISB 的用途;
  • • 不使用通用原子 RMW 指令修改不支持该语义的设备寄存器;
  • • 把寄存器访问顺序封装在驱动内部,不让业务层直接操作裸寄存器。

CMSIS 提供 __DMB()__DSB()__ISB() 等 intrinsic,但使用哪一种必须由处理器架构和设备编程模型决定。屏障越强不代表设计越正确,滥用全屏障只会掩盖协议不清并增加开销。

Linux READ_ONCE()/WRITE_ONCE() 的正确借鉴方式

Linux 使用 READ_ONCE() 和 WRITE_ONCE() 标记可能被并发访问的对象,主要防止编译器对特定访问进行不希望的合并、拆分或重构。它们很有参考价值,但不能被误解为:

READ_ONCE + WRITE_ONCE = 完整线程同步

Linux 内核通常还会配合:

  • • smp_store_release() / smp_load_acquire()
  • • spinlock、mutex、RCU、seqlock;
  • • wait queue、completion;
  • • DMA API;
  • • 设备 I/O accessor;
  • • 架构相关 barrier。

在 RTOS 或裸机中,不能只手写一个强制 volatile cast 的宏并声称等价于 Linux 语义。应优先使用编译器和平台已经定义的 atomic/barrier API。

开源组件与方案详解

Linux Kernel Memory Barriers

Linux 官方 memory barriers 文档系统说明了编译器重排、CPU 重排、SMP barrier 配对、设备访问、Cache 与 DMA 等问题。其最重要的工程启示是:屏障必须成对、必须围绕明确的共享协议使用,不能孤立地在“看起来不放心”的地方插入全屏障。

可参考点:

  • • acquire 和 release 是方向性的单向屏障;
  • • CPU-CPU 和 CPU-device 的同步语义不同;
  • • barrier 不保证某个硬件动作已经完成,完成语义可能需要 DSB、状态读回或设备专用机制;
  • • 对共享环形缓冲区,数据发布与索引发布必须绑定;
  • • 单个 CPU 的源码顺序不能推导另一个 CPU 的观察顺序。

Linux kfifo

kfifo 是固定容量 FIFO/Ring Buffer 实现。其适合借鉴的不是 API 名称,而是:

  • • 2 的幂容量简化索引;
  • • 单生产者单消费者可以降低锁需求;
  • • 多生产者/多消费者需要外部串行化;
  • • 读写索引和数据区有明确职责;
  • • 批量读写减少同步和函数调用开销。

Linux seqlock/seqcount

Linux seqcount 适合写少读多的一致快照。借鉴时要同时理解:

  • • sequence 只用于检测并发写;
  • • 写者仍需串行化;
  • • 读者可能重试;
  • • 不能安全保护会被释放的指针生命周期;
  • • 实时场景要评估写者抢占导致读者长时间重试的问题。

FreeRTOS Task Notification

FreeRTOS 任务通知将通知状态直接存放在任务控制块中,适合一个或少量固定接收任务的轻量事件、计数或位图同步。它可以替代部分二值/计数信号量和事件位场景,但不能替代包含复杂 payload、需要多个消费者或需要独立队列深度的消息队列。

可参考点:

  • • ISR 使用专用 FromISR API;
  • • 通知动作可以设置位、递增计数或覆盖/不覆盖值;
  • • 接收端可以阻塞等待;
  • • 必须定义重复通知是否合并、累计或覆盖。

FreeRTOS Queue、Stream Buffer 与 Message Buffer

  • • Queue 适合固定大小消息和命令;
  • • Stream Buffer 适合字节流,通常按单写者单读者设计;
  • • Message Buffer 在字节流上增加消息边界;
  • • 大 payload 可考虑传递内存池对象指针,但必须定义所有权和释放路径。

Zephyr Atomic Services

Zephyr 官方 atomic API 面向线程和 ISR,适合单个机器字的原子读改写。文档明确说明其 API 需要在硬件必要时提供完整内存屏障。借鉴时仍要注意:原子变量只能保护自身,不会自动让一组业务字段成为一致快照。

Zephyr Message Queue

k_msgq 使用固定大小消息和内部 Ring Buffer,可由线程和 ISR 异步发送。其设计体现了几个通用原则:

  • • 队列容量固定,可预测内存;
  • • ISR 不得在队列满时阻塞;
  • • 大消息完整拷贝会线性增加临界路径成本;
  • • 大对象更适合传递指针或句柄,但必须管理生命周期;
  • • 队列满时要有明确过载策略。

Zephyr Ring Buffer

Zephyr ring_buf 提供 copy API 和 claim/finish 零拷贝接口。其并发文档指出,单生产者单消费者可以在不同上下文并发使用;多生产者或多消费者需要外部保护;在 SMP 上仍需确保数据写在索引发布前可见,使用内核同步原语可获得必要的屏障。

这正是通用 Ring Buffer 设计的核心:数据区的写入和索引发布不是同一个动作,必须建立顺序。

RT-Thread IPC

RT-Thread 提供信号量、互斥量、事件、邮箱和消息队列。对于 MCU 工程,可以按以下方式映射:

  • • 中断到线程的单次完成通知:信号量;
  • • 多位条件组合:事件集;
  • • 传递固定宽度值或指针:邮箱;
  • • 传递结构化消息:消息队列;
  • • 任务间保护共享对象:互斥量;
  • • 极短 ISR/线程共享字段:中断锁或平台原子 API。

不要因为 RTOS 已提供 IPC,就同时保留一套无同步全局标志。否则系统会出现“双重事实源”:事件已经进入队列,但全局标志仍是旧值,或反之。

CMSIS-Core Barrier 与 Cache API

CMSIS-Core 提供处理器 intrinsic 和部分 Cache 操作接口,可用于 Cortex-M/A/R 平台。需要把它们放在平台抽象层中,而不是散落在业务代码:

platform_memory_barrier_release()
platform_memory_barrier_acquire()
platform_dcache_clean(addr, len)
platform_dcache_invalidate(addr, len)
platform_dma_prepare_tx(buffer, len)
platform_dma_complete_rx(buffer, len)

这样可以根据芯片系列、MPU/MMU 属性、Cache line 大小和 DMA 一致性能力替换实现,并便于审计。

一套通用同步抽象

为了避免业务代码直接依赖特定 RTOS,可建立小型 OSAL/同步抽象层。

/**
 * @brief Posts an event from task context.
 *
 * @param[in] channel Event channel handle.
 * @param[in] event Event to enqueue.
 * @param[in] timeout_ms Maximum wait time in milliseconds.
 *
 * @return 0 on success, or a negative error code.
 */

intsync_event_post(sync_channel_t *channel,
constsync_event_t *event,
uint32_t timeout_ms)
;

/**
 * @brief Posts an event from interrupt context.
 *
 * @param[in] channel Event channel handle.
 * @param[in] event Event to enqueue.
 * @param[out] higher_priority_woken Whether a higher-priority task was woken.
 *
 * @return 0 on success, or a negative error code.
 */

intsync_event_post_isr(sync_channel_t *channel,
constsync_event_t *event,
bool *higher_priority_woken)
;

/**
 * @brief Waits for the next event.
 *
 * @param[in] channel Event channel handle.
 * @param[out] event Received event.
 * @param[in] timeout_ms Maximum wait time in milliseconds.
 *
 * @return 0 on success, or a negative error code.
 */

intsync_event_wait(sync_channel_t *channel,
sync_event_t *event,
uint32_t timeout_ms)
;

抽象层至少要统一:

  • • task 与 ISR API 区分;
  • • 超时单位和永久等待常量;
  • • 队列满、超时、删除、上下文错误返回码;
  • • 是否触发调度;
  • • 内存屏障语义;
  • • 统计计数器;
  • • 静态分配和动态分配策略。

不要把 OSAL 设计成最小公分母,导致无法表达任务通知、事件位或 ISR 唤醒。可以提供通用接口,同时允许性能关键模块使用受控的原生后端。

典型错误模式与修复

错误 1:共享布尔变量轮询

while (!g_event) {
}

问题:编译器优化、忙等、事件合并、优先级调度和功耗。

修复:使用任务通知、信号量或队列;如果必须短时自旋,使用 atomic acquire 并设置严格超时,之后进入阻塞等待。

错误 2:数据体普通写,volatile 标志发布

payload = new_payload;
ready = 1U/* volatile */

问题:没有跨上下文 release/acquire 关系,DMA/多核 Cache 也未处理。

修复:使用 atomic release 发布、acquire 消费,或直接使用 RTOS 队列/锁。

错误 3:ISR 修改复杂状态机

问题:任务同时修改、ISR 运行时间变长、难以锁定、难以测试。

修复:ISR 只采集最小状态并发送事件,状态机由单一任务拥有。

错误 4:关中断保护多核共享变量

问题:只关闭当前核中断,其他核仍可并发访问。

修复:使用 spinlock、atomic 或核间消息;必要时结合本地中断屏蔽保护“本核 ISR + 其他核”的混合竞争。

错误 5:DMA 完成后直接读 buffer

问题:CPU D-Cache 仍是旧数据。

修复:按平台 DMA API 执行同步/invalidate,并遵守所有权协议。

错误 6:队列里传递栈变量地址

问题:生产者返回后对象失效,消费者读取悬空指针。

修复:队列复制值,或从固定内存池分配对象并在消费后归还。

错误 7:多生产者共用 SPSC Ring Buffer

问题:两个生产者竞争同一个 head,覆盖数据。

修复:外部 mutex/critical section 串行化生产者,或使用经过验证的 MPSC 队列。

错误 8:把 atomic 当业务事务

问题:每个字段都原子,但字段组合仍可能不一致。

修复:保护业务不变量,使用锁、双缓冲、seqlock 或完整消息。

错误 9:随处插入 DMB/DSB

问题:没有明确配对协议,可能仍然错误且性能下降。

修复:先画出生产者/消费者时序和所有权,再在发布/消费边界选择最小充分屏障。

错误 10:优化等级作为“修复开关”

问题:-O0 仅改变症状,发布版本仍可能失败。

修复:所有测试配置都保持正确同步,并在 -O2/-O3/LTO 下做压力验证。

设计决策树

不能

单个计数/状态位

多字段不变量

能,写少读多

不能

连续数据流

DMA Buffer

MMIO

需要跨上下文传递信息
能否让单一任务拥有状态?
消息传递 / 事件驱动
对象是什么?
Atomic
读者能否重试?
Seqlock / Double Buffer
Mutex / Critical Section
SPSC?
SPSC Ring + Notification
验证过的 MPSC/MPMC Queue 或外部锁
Ownership + Cache Maintenance + Descriptor Queue
Platform I/O Accessor + Barrier

决策原则

  1. 1. 能传消息就不共享复杂可变状态。
  2. 2. 能由单一任务拥有就不让 ISR 修改业务状态机。
  3. 3. atomic 只用于小而明确的同步变量。
  4. 4. 锁用于保护不变量,不用于包围慢 I/O。
  5. 5. Ring Buffer 只在生产者/消费者模型明确时使用。
  6. 6. DMA 与 MMIO 必须按设备内存模型处理。
  7. 7. 每个阻塞点必须有超时、错误码和观测指标。

性能与实时性分析

正确性优先,但同步方案也要满足实时预算。

应量化的指标

T_isr_max        = ISR 最长执行时间
T_wakeup_p99     = 事件发布到任务开始运行的 P99 延迟
T_lock_hold_max  = 锁最长持有时间
T_queue_wait_max = 队列最长等待时间
N_queue_peak     = 峰值积压深度
N_drop_total     = 丢弃事件总数
N_retry_peak     = seqlock 最大重试次数
T_dma_sync_max   = cache clean/invalidate 最大耗时

队列容量估算

若峰值事件到达率为 R_in,消费者在最长不可运行窗口 T_block 内不能处理事件,安全余量为 N_margin

N_queue_min >= ceil(R_in * T_block) + N_margin

如果长期平均到达率高于消费率,增加队列只能推迟溢出,必须降低生产速率、批量处理、增加消费者能力或实施背压。

Ring Buffer 容量估算

B_ring_min >= R_data_peak * (T_sched_tail + T_service_jitter + T_margin)

对于 DMA Ping-Pong Buffer,还要保证一个 buffer 的处理时间小于另一个 buffer 被填满的时间,否则双缓冲会持续覆盖或溢出。

锁的实时风险

高优先级任务等待低优先级任务持有的 mutex 时,若 RTOS 支持优先级继承,可降低无界优先级反转风险。但优先级继承不能修复:

  • • 持锁期间等待 I/O;
  • • 锁顺序不一致导致死锁;
  • • ISR 错误获取 mutex;
  • • 多个嵌套锁形成长阻塞链;
  • • 锁保护范围过大。

调试与定位流程

第一步:确认共享边界

列出所有读者、写者和执行上下文:

对象
写者
读者
上下文
是否可能并发
当前同步
state
控制任务、ISR
UI、日志
task/ISR
RX buffer
DMA
协议任务
device/task
中断标志
ring head
ISR
工作任务
ISR/task
volatile
config snapshot
配置任务
多个业务任务
task/task
mutex

任何“写者不确定”的对象都不能继续靠猜测修复。

第二步:查看优化后的机器码

建议保留以下构建产物:

gcc -O0 -S -fverbose-asm module.c -o module.O0.s
gcc -O3 -S -fverbose-asm module.c -o module.O3.s
gcc -O3 -flto -c module.c -o module.o
objdump -drwC firmware.elf > firmware.dis

检查:

  • • 循环中是否仍有 load;
  • • load/store 宽度是否变化;
  • • 是否合并为宽访问;
  • • atomic 是否调用库函数;
  • • barrier 是否生成预期指令;
  • • LTO 是否内联并删除路径;
  • • ISR 和任务是否访问同一实际符号。

第三步:验证对象布局和对齐

  • • 查看 linker map;
  • • 确认对象未被放入错误的 cacheable/non-cacheable 区域;
  • • 检查 DMA buffer 是否按 Cache line 对齐;
  • • 检查结构体 packing 是否导致非对齐访问;
  • • 检查 bitfield 是否被并发修改;
  • • 确认共享变量没有同名静态副本。

第四步:加入低扰动观测

优先使用:

  • • ITM/ETM/SWO;
  • • RTOS trace hooks;
  • • GPIO 时间戳;
  • • 环形 trace buffer;
  • • 单调序号和丢失计数;
  • • 硬件 watchpoint;
  • • DMA 描述符快照。

大量串口 printf 会改变调度和时序,只能作为辅助。

第五步:构造压力条件

  • • 提高中断频率;
  • • 缩短任务周期;
  • • 增加随机抢占;
  • • 同时运行 Cache/DDR 压力;
  • • 调整任务优先级;
  • • 开启/关闭 LTO 对比;
  • • 在多核间迁移任务;
  • • 改变 DMA buffer 对齐;
  • • 重复运行数小时并统计极端值。

第六步:在主机侧使用并发检测工具

与硬件无关的状态机和队列封装可以移植到 Linux 主机测试,并使用 ThreadSanitizer:

clang -O1 -g -fsanitize=thread test_sync.c -lpthread -o test_sync
./test_sync

ThreadSanitizer 不能发现 DMA Cache 一致性或 MMIO 顺序错误,但能帮助发现普通线程数据竞争。

验证方案与验收标准

编译矩阵

至少覆盖:

维度
建议配置
优化等级
-O0
-Og-O2-O3
LTO
关闭、开启
编译器
GCC、Clang(条件允许)
架构
单核、SMP 或目标多核配置
Cache
启用目标配置;必要时做对照实验
调试宏
日志关闭、最小日志、完整 trace

功能测试

  1. 1. 连续发布单个事件,确认不丢失。
  2. 2. 短时间突发超过平均处理速率,验证队列满策略。
  3. 3. 连续两次相同事件,确认不会被布尔标志合并。
  4. 4. 多字段快照循环写读,验证不出现混合版本。
  5. 5. Ring Buffer 在回绕点、高低水位和满/空边界测试。
  6. 6. 任务超时、取消和关闭流程测试。
  7. 7. DMA TX/RX 在非 Cache line 整数长度下测试。
  8. 8. 多核同时访问热点变量,验证正确性和性能。
  9. 9. 重启、异常中断和错误恢复过程中验证所有权归还。

压力测试

运行时长                >= 目标可靠性窗口
事件总数                >= 10^8 或项目定义数量
事件丢失                = 0,除非策略明确允许
重复消费                = 0
顺序错误                = 0
快照校验失败            = 0
Ring Buffer 越界        = 0
锁超时/死锁             = 0
DMA 数据 CRC 错误       = 0
队列最大深度            < 配置容量,并保留安全余量
唤醒 P99/P99.9 延迟     < 实时预算

允许丢帧的系统也必须区分“策略性丢帧”和“无声的数据竞争”。每次丢弃应有计数、原因和可观测状态。

故障注入

为了验证恢复能力,可以主动注入:

  • • 延迟生产者或消费者;
  • • 强制队列满;
  • • 随机跳过一次通知;
  • • 在发布前后插入调度点;
  • • 模拟 DMA 超时和错误中断;
  • • 修改序号造成 discontinuity;
  • • 在多核上随机迁移任务;
  • • 强制 seqlock 写者延长;
  • • 让 buffer 地址跨 Cache line 边界;
  • • 在系统高负载下触发低功耗进入/退出。

验收重点不是“永远不出错”,而是出错时能够超时、报警、复位通信通道或重新建立所有权,不能永久死循环。

一套推荐的分层实现

Business State Machine
Typed Event API
OSAL Queue / Notification / Mutex
Atomic & Barrier Abstraction
Architecture Port\nARM / RISC-V / x86
DMA Buffer Manager
Cache Maintenance Port
Trace & Metrics
Stress Test / Fault Injection

业务层

  • • 只处理类型化事件;
  • • 不直接操作 ISR 标志;
  • • 不直接清 Cache;
  • • 不直接插入架构屏障;
  • • 状态由单一任务拥有。

OSAL 层

  • • 封装 queue、event、notification、mutex;
  • • 区分 ISR 与 task API;
  • • 统一 timeout 和错误码;
  • • 提供静态分配能力;
  • • 集成统计和 trace hook。

原子与屏障层

  • • 优先映射 C11 atomic 或 RTOS 官方 atomic;
  • • 只暴露明确的 relaxed/acquire/release 语义;
  • • 禁止业务层直接使用随意的内联汇编;
  • • 为不支持 lock-free 的宽类型提供锁回退。

DMA Buffer Manager

  • • 固定内存池;
  • • Cache line 对齐;
  • • 明确 FREE/CPU_OWNED/DMA_OWNED/READY 状态;
  • • 集中执行 clean/invalidate;
  • • 管理描述符和 payload 生命周期;
  • • 统计超时、错误和 buffer 饥饿。

Trace 与指标

  • • 事件序号;
  • • 发布和消费时间戳;
  • • 队列深度高水位;
  • • 溢出计数;
  • • 锁持有时间;
  • • DMA 同步耗时;
  • • 快照重试次数;
  • • 最后一次错误上下文。

示例:从错误轮询改为事件驱动

原始设计

staticvolatilebool g_button_changed;
staticuint32_t g_button_state;

voidbutton_isr(void)
{
    g_button_state = read_button_gpio();
    g_button_changed = true;
}

voidbutton_task(void *argument)
{
    (void)argument;

for (;;) {
if (g_button_changed) {
            g_button_changed = false;
            button_state_machine(g_button_state);
        }
    }
}

问题包括:

  • • 连续边沿会合并;
  • • g_button_state 与 g_button_changed 没有完整发布协议;
  • • 任务忙等;
  • • ISR 与任务同时读写标志;
  • • 抖动去除、时间戳和来源信息缺失;
  • • 未来扩展多个按键时全局变量迅速失控。

事件驱动设计

typedefstruct {
uint32_t pin;
uint32_t level;
uint32_t timestamp;
button_event_t;

voidbutton_isr(void)
{
button_event_t event = {
        .pin = read_pending_button_pin(),
        .level = read_button_gpio(),
        .timestamp = platform_get_isr_timestamp(),
    };
bool higher_priority_woken = false;

    clear_button_interrupt(event.pin);

if (button_event_post_isr(&event, &higher_priority_woken) != 0) {
        button_stats_record_drop(event.pin);
    }

    platform_yield_from_isr(higher_priority_woken);
}

voidbutton_task(void *argument)
{
button_event_t event;

    (void)argument;

for (;;) {
if (button_event_wait(&event, BUTTON_WAIT_FOREVER) != 0) {
continue;
        }

        button_debounce_process(&event);
        button_state_machine_process(&event);
    }
}

这里真正解决问题的不是删除 volatile,而是改变了通信模型:ISR 生产不可变事件,队列承担数据交付和唤醒,状态机由一个任务拥有,事件溢出可统计。

示例:DMA 接收双缓冲

Consumer TaskBuffer ManagerDMA ISRDMAConsumer TaskBuffer ManagerDMA ISRDMAwrite buffer Ncompletion interruptmark N completenotifyacquire completed bufferinvalidate cache rangereturn pointer + length + sequencevalidate CRC and consumerelease bufferre-arm descriptor

推荐状态机:

FREE -> CPU_PREPARED -> DMA_OWNED -> DMA_COMPLETE -> CPU_OWNED -> FREE

任何非法跳转都应记录错误。例如 CPU 不得读取 DMA_OWNED buffer,DMA 不得写 CPU_OWNED buffer。

示例:配置快照发布

配置项较多、读取频繁、更新很少时,可以使用不可变快照 + 原子指针发布:

Config Task:
    allocate new snapshot
    validate all fields
    calculate derived parameters
    release-publish pointer
    retire old snapshot after readers are safe

Reader Tasks:
    acquire-load pointer
    read immutable snapshot without lock

难点不在原子指针,而在旧对象何时释放。小系统可以采用:

  • • 双缓冲,只有两个静态对象;
  • • 所有读者在固定调度点确认版本;
  • • 引用计数;
  • • 短 mutex 保护指针获取和引用增加;
  • • 类 RCU grace period。

若生命周期无法可靠证明,直接使用 mutex 复制快照通常更安全。

面试中应如何回答

可以按以下顺序组织:

  1. 1. 先定性:-O3 暴露了同步缺失或未定义行为,-O0 正常不代表程序正确。
  2. 2. 分层解释:编译器可能把 load 提出循环并让值驻留寄存器;CPU 还可能乱序;Cache 一致性只覆盖特定主设备;DMA 可能绕过 Cache。
  3. 3. 明确 volatile 边界:它不提供原子性、互斥、happens-before、Cache 维护和任务唤醒。
  4. 4. 提出四个同步目标:原子性、可见性、有序性、等待/唤醒。
  5. 5. 给出系统架构:状态单一所有者,ISR/其他任务通过 queue、notification、event 或 semaphore 发送事件。
  6. 6. 共享内存优化:单值用 atomic;多字段用锁、双缓冲或 seqlock;数据流用 SPSC Ring;DMA 加所有权和 Cache clean/invalidate。
  7. 7. 给出验证方法:检查汇编、构建优化矩阵、压力测试、序号/CRC/trace、主机 ThreadSanitizer、DMA 对齐和多核测试。
  8. 8. 最后强调:屏障必须围绕明确协议成对使用,不能靠随处加 volatile 或 DMB 碰运气。

适合现场表达的精简回答

-O3 下共享变量“不更新”,首先应判断是否存在没有同步语义的并发访问。优化器可能把循环中的 load 提到循环外,值长期留在寄存器;即使每次都重新 load,CPU 还可能通过 Store Buffer、乱序和 Cache 让另一个上下文观察到不同顺序。若写者是 DMA,CPU Cache 甚至可能一直保存旧副本。volatile 只能限制对特定对象的编译器优化,不能提供原子性、release/acquire、有序发布、Cache clean/invalidate 或任务唤醒。

系统级方案是让状态机由单一任务拥有,ISR 和其他任务通过 RTOS queue、task notification、event 或 semaphore 发送事件。确需共享内存时,单个标量使用 atomic;多字段状态使用 mutex、临界区、双缓冲或 seqlock;连续数据流使用带 release/acquire 索引发布的 SPSC Ring Buffer;DMA buffer 使用明确所有权、Cache line 对齐和 clean/invalidate。最后在 -O2/-O3/LTO、高并发、Cache 开启和长时间压力条件下,用序号、CRC、trace 和汇编检查验证不丢事件、不读混合快照、不出现越界或死锁。

已知事实与工程判断边界

已知客观事实

  • • 编译器可以在不破坏语言抽象机可观察行为的前提下重排、合并或删除普通内存访问。
  • • 无同步的线程间冲突访问在 C/C++ 内存模型下可能构成数据竞争和未定义行为。
  • • volatile 不等价于 C11 atomic,也不建立线程间 release/acquire 关系。
  • • CPU Cache 一致性和 DMA 一致性是不同问题;非一致性 DMA 需要平台规定的同步或 Cache 维护。
  • • RTOS 队列、信号量、任务通知等同步原语用于建立阻塞/唤醒和跨上下文通信。
  • • 内存屏障提供顺序约束,但不自动定义对象所有权、队列容量和错误恢复策略。

基于事实的工程分析

  • • 对大多数 MCU/RTOS 业务状态机,单一所有者和消息传递通常比共享全局变量更易验证。
  • • 对短小、高频、单生产者单消费者的数据流,SPSC Ring Buffer 往往能在可预测内存和低开销之间取得较好平衡。
  • • 对 DMA 与多核系统,应把 Cache 维护和 barrier 封装进平台层,避免业务代码直接依赖具体指令。
  • • 对安全关键或高可靠系统,应把队列溢出、超时、序号断裂和非法所有权跳转视为可诊断故障,而不是静默忽略。

这些工程判断仍需结合目标 CPU、编译器、RTOS、Cache 属性、DMA 一致性和实时预算验证,不能机械套用。

参考链接

  • • Linux kernel memory barriers[1]
  • • Linux atomic types[2]
  • • Linux seqlock and seqcount[3]
  • • Linux kfifo API[4]
  • • Linux DMA API HOWTO[5]
  • • Linux volatile considered harmful[6]
  • • GCC Volatiles[7]
  • • GCC Atomic Builtins[8]
  • • FreeRTOS task notifications[9]
  • • FreeRTOS queues[10]
  • • FreeRTOS stream and message buffers[11]
  • • Zephyr Atomic Services[12]
  • • Zephyr Message Queues[13]
  • • Zephyr Ring Buffers[14]
  • • Zephyr synchronization services[15]
  • • RT-Thread inter-thread synchronization[16]
  • • CMSIS-Core intrinsic functions[17]
  • • CMSIS-Core cache functions[18]
  • • Arm memory ordering white paper[19]

最终结论

一句话概括:高优化等级下的“共享变量失效”不是一个 volatile 语法题,而是一个跨越语言内存模型、编译器、CPU、Cache、DMA、RTOS 和业务所有权的系统同步题。可靠设计应以明确的生产者/消费者协议为基础,同时提供原子性、可见性、有序性和等待/唤醒保证;优先使用单一所有者与消息传递,必要时再用经过证明的 atomic、锁、Ring Buffer、双缓冲、seqlock 和 DMA Cache 维护机制。

引用链接

[1] Linux kernel memory barriers:https://docs.kernel.org/core-api/wrappers/memory-barriers.html
[2]Linux atomic types:https://docs.kernel.org/core-api/wrappers/atomic_t.html
[3]Linux seqlock and seqcount:https://docs.kernel.org/locking/seqlock.html
[4]Linux kfifo API:https://docs.kernel.org/core-api/kernel-api.html
[5]Linux DMA API HOWTO:https://docs.kernel.org/core-api/dma-api-howto.html
[6]Linux volatile considered harmful:https://docs.kernel.org/process/volatile-considered-harmful.html
[7]GCC Volatiles:https://gcc.gnu.org/onlinedocs/gcc/Volatiles.html
[8]GCC Atomic Builtins:https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html
[9]FreeRTOS task notifications:https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/03-Direct-to-task-notifications/01-Task-notifications
[10]FreeRTOS queues:https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/01-Queues
[11]FreeRTOS stream and message buffers:https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/04-Stream-and-message-buffers/01-Stream-and-message-buffers
[12]Zephyr Atomic Services:https://docs.zephyrproject.org/latest/kernel/services/other/atomic.html
[13]Zephyr Message Queues:https://docs.zephyrproject.org/latest/kernel/services/data_passing/message_queues.html
[14]Zephyr Ring Buffers:https://docs.zephyrproject.org/latest/kernel/data_structures/ring_buffers.html
[15]Zephyr synchronization services:https://docs.zephyrproject.org/latest/kernel/services/synchronization/index.html
[16]RT-Thread inter-thread synchronization:https://www.rt-thread.io/document/site/programming-manual/ipc1/ipc1/
[17]CMSIS-Core intrinsic functions:https://arm-software.github.io/CMSIS_6/latest/Core/group__intrinsic__CPU__gr.html
[18]CMSIS-Core cache functions:https://arm-software.github.io/CMSIS_6/latest/Core/group__cache__functions__m7.html
[19]Arm memory ordering white paper:https://developer.arm.com/documentation/102376/latest/

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-07-21 12:03:12 HTTP/2.0 GET : https://15386.cn/a/470224.html
  2. 运行时间 : 0.158659s [ 吞吐率:6.30req/s ] 内存消耗:4,571.30kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=6662cedc86a8b39086c572d5fcc9e47c
  1. /yingpanguazai/ssd/ssd1/www/no.15386.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/no.15386.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/no.15386.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/no.15386.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/no.15386.cn/runtime/temp/97c957f747c268aee476c4e16775dd7c.php ( 12.06 KB )
  140. /yingpanguazai/ssd/ssd1/www/no.15386.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000616s ] mysql:host=127.0.0.1;port=3306;dbname=no_15386;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000905s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.011891s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.015978s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000919s ]
  6. SELECT * FROM `set` [ RunTime:0.004992s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000648s ]
  8. SELECT * FROM `article` WHERE `id` = 470224 LIMIT 1 [ RunTime:0.005623s ]
  9. UPDATE `article` SET `lasttime` = 1784606592 WHERE `id` = 470224 [ RunTime:0.014171s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 65 LIMIT 1 [ RunTime:0.016518s ]
  11. SELECT * FROM `article` WHERE `id` < 470224 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000786s ]
  12. SELECT * FROM `article` WHERE `id` > 470224 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001418s ]
  13. SELECT * FROM `article` WHERE `id` < 470224 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.003819s ]
  14. SELECT * FROM `article` WHERE `id` < 470224 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.006181s ]
  15. SELECT * FROM `article` WHERE `id` < 470224 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.001450s ]
0.160473s