代码之家  ›  专栏  ›  技术社区  ›  Lou

中断安全FIFO中的DMB指令

  •  2
  • Lou  · 技术社区  · 7 年前

    有关 this thread

    头索引必须是

    • 多次中断 (不是螺纹)

    移动FIFO磁头的功能与此类似(也会检查磁头在实际代码中是否溢出,但这是主要思想):

    #include <stdatomic.h>
    #include <stdint.h>
    
    #define FIFO_LEN 1024
    extern _Atomic int32_t _head;
    
    int32_t acquire_head(void)
    {
        while (1)
        {
            int32_t old_h = atomic_load(&_head);
            int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
    
            if (atomic_compare_exchange_strong(&_head, &old_h, new_h))
            {
                return old_h;
            }
        }
    }
    

    海湾合作委员会将 compile this 致:

    acquire_head:
            ldr     r2, .L8
    .L2:
            // int32_t old_h = atomic_load(&_head);
            dmb     ish
            ldr     r1, [r2]
            dmb     ish
    
            // int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
            adds    r3, r1, #1
            ubfx    r3, r3, #0, #10
    
            // if (atomic_compare_exchange_strong(&_head, &old_h, new_h))
            dmb     ish
    .L5:
            ldrex   r0, [r2]
            cmp     r0, r1
            bne     .L6
            strex   ip, r3, [r2]
            cmp     ip, #0
            bne     .L5
    .L6:
            dmb     ish
            bne     .L2
            bx      lr
    .L8:
            .word   _head
    

    这是一个没有OS/threads的裸机项目。这段代码用于记录FIFO,它不是时间关键型的,但我不希望头部的获取对我程序其余部分的延迟产生影响,因此我的问题是:

    • 我需要这些吗 dmb
    • 这些说明是否会造成明显的性能损失,或者我可以忽略这一点?
    • 如果中断发生在 dmb
    3 回复  |  直到 7 年前
        1
  •  6
  •   Peter Cordes    5 年前

    TL:是的, LL/SC (STREX/LDREX)与禁用中断相比,可以通过重试使原子RMW可中断,从而改善中断延迟。

    这可能是以吞吐量为代价的,因为显然在ARMv7上禁用/重新启用中断非常便宜(每个中断可能有1到2个周期) cpsid if / cpsie if ),特别是如果您可以无条件启用中断而不是保存旧状态。( Temporarily disable interrupts on ARM ).

    非常 稀有的只有当中断在另一个中断处理程序的LL/SC中间出现时才出现。


    不幸的是,像gcc这样的C11编译器对于单处理器系统或单线程代码没有特殊的case模式 . 因此,他们不知道如何使用代码生成器来利用这样一个事实,即在同一个内核上运行的任何东西都会在某一点上以程序顺序看到我们的所有操作,即使没有任何障碍。

    (无序执行和内存重新排序的主要规则是,它保留了单线程或单核以程序顺序运行指令的假象。)

    背靠背 dmb


    单核CPU上的原子

    之后 atomic_fetch_add ,而不是使用CAS模拟具有早期滚动的原子添加。(那么读者也必须戴上面具,但这很便宜。)

    memory_order_relaxed . 如果要针对中断处理程序重新排序保证,请使用 atomic_signal_fence 在没有asm障碍的情况下强制编译时排序,以防止运行时重新排序。

    // readers must also mask _head & (FIFO_LEN - 1) before use
    
    // Uniprocessor but with an atomic RMW:
    int32_t acquire_head_atomicRMW_UP(void)
    {
        atomic_signal_fence(memory_order_seq_cst);    // zero asm instructions, just compile-time
        int32_t old_h = atomic_fetch_add_explicit(&_head, 1, memory_order_relaxed);
        atomic_signal_fence(memory_order_seq_cst);
    
        int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
        return new_h;
    }
    

    On the Godbolt compiler explorer

    @@ gcc8.2 -O3 with your same options.
    acquire_head_atomicRMW:
        ldr     r3, .L4           @@ load the static address from a nearby literal pool
    .L2:
        ldrex   r0, [r3]
        adds    r2, r0, #1
        strex   r1, r2, [r3]
        cmp     r1, #0
        bne     .L2               @@ LL/SC retry loop, not load + inc + CAS-with-LL/SC
        adds    r0, r0, #1        @@ add again: missed optimization to not reuse r2
        ubfx    r0, r0, #0, #10
        bx      lr
    .L4:
        .word   _head
    

    法学/理学学士 _head . LDREX/STREX有编译器特定的内部函数,不过: Critical sections in ARM .

    这是安全的,因为 _Atomic int32_t _原子的 ).我会用的 uint32_t ,但我们得到的是相同的asm。


    从中断处理程序内部安全使用STREX/LDREX:

    ARM® Synchronization Primitives (2009年起)有一些关于管理LDREX/STREX的ISA规则的详细信息。运行LDREX会初始化“独占监视器”,以检测其他内核(或系统中的其他非CPU事物?我不知道)的修改。Cortex-M4是一个单核系统。

    总是 测试代码时失败(因此陷入重试循环),这可能就是问题所在。

    打断一下就行了 中止由LDREX启动的事务 clrex 为此,否则较旧的手臂将使用虚拟STREX到虚拟位置。

    看见 When is CLREX actually needed on ARM Cortex M7? 这和我要说的是一样的 在中断情况下,当线程之间不进行上下文切换时,通常不需要CLREX。

    (有趣的事实:最近关于这个链接问题的一个回答指出Cortex M7(或者通常是Cortex M?)会在中断时自动清除监视器,这意味着在中断处理程序中永远不需要clrex。下面的推理仍然适用于带有不跟踪地址的监视器的旧单核ARM CPU, unlike in multi-core CPUs .)

    但对于这个问题,你要换的东西 始终是中断处理程序的开始。你没有做先发制人的多任务。 只要STREX在低优先级中断中第一次失败,当您返回它时,就可以了。

    这里就是这种情况,因为更高优先级的中断只有在成功执行STREX(或者根本没有执行任何原子rmw)之后才会返回。

    所以我觉得即使不使用 克莱克斯 从内联asm,或在分派到C函数之前从中断处理程序。 手册中说,数据中止异常使监控器在体系结构上未定义,因此请确保至少在该处理程序中使用CLREX。

    如果在LDREX和STREX之间出现中断,则LL已将旧数据加载到寄存器中(可能还计算了一个新值),但由于STREX尚未运行,因此尚未将任何内容存储回内存。

    old_h 值,然后进行一次成功的STREX old_h + 1 . (除非 也会被中断,但这种推理是递归的)。这可能会在第一次循环中失败,但我不这么认为。即使如此,基于我链接的ARM文档,我也不认为存在正确性问题。文档提到,本地监视器可以像状态机一样简单,只跟踪LDREX和STREX指令,即使之前的指令是不同地址的LDREX,STREX也可以成功。假设Cortex-M4的实现过于简单,这就非常适合了。

    当CPU已经在监控前一个LDREX时,为同一地址运行另一个LDREX看起来应该没有效果。对服务器执行独占加载 不同的 地址会将监视器重置为打开状态,但对于这个地址,它始终是相同的地址(除非在其他代码中有其他原子?)

    然后(在做了一些其他事情之后),中断处理程序将返回,恢复寄存器并跳回低优先级中断的LL/SC循环的中间。

    回到低优先级中断时,STREX将失败,因为高优先级中断中的STREX重置监视器状态。那很好,我们 需要 它可能会失败,因为它会存储与在FIFO中占据其位置的高优先级中断相同的值。这个 cmp / bne 检测故障并再次运行整个循环。这次它成功了(除非被中断) 再一次

    因此,我认为我们可以在任何地方都不使用CLREX,因为中断处理程序总是在返回到它们中断的内容中间之前运行到完成。他们总是从一开始就开始。


    单作者版本

    或者,如果没有其他东西可以修改该变量,则根本不需要原子RMW,只需要一个纯原子加载,然后是新值的纯原子存储。( _原子的 为了读者的利益)。

    或者,如果没有任何其他线程或中断触及该变量,则不需要这样做 _原子的 .

    // If we're the only writer, and other threads can only observe:
    // again using uniprocessor memory order: relaxed + signal_fence
    int32_t acquire_head_separate_RW_UP(void) {
        atomic_signal_fence(memory_order_seq_cst);
        int32_t old_h = atomic_load_explicit(&_head, memory_order_relaxed);
    
        int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
        atomic_store_explicit(&_head, new_h, memory_order_relaxed);
        atomic_signal_fence(memory_order_seq_cst);
    
        return new_h;
    }
    
    acquire_head_separate_RW_UP:
        ldr     r3, .L7
        ldr     r0, [r3]          @@ Plain atomic load
        adds    r0, r0, #1
        ubfx    r0, r0, #0, #10   @@ zero-extend low 10 bits
        str     r0, [r3]          @@ Plain atomic store
        bx      lr
    

    这和我们在非原子方面得到的asm是一样的 head .

        2
  •  2
  •   0___________    7 年前

    您的代码不是以“裸机”的方式编写的。那些“通用”原子函数不知道读取或存储的值是否位于内部存储器中,或者它可能是一个硬件寄存器,位于远离内核的某处,通过总线连接,有时通过写/读缓冲器连接。

    在我看来,当您想以原子方式访问内存位置时,禁用中断就足够了。

    PS stdatomic在裸机uC开发中的应用非常罕见。

    保证M4 uC独占访问的最快方法是禁用和启用中断。

    __disable_irq();
    x++;
    __enable_irq();
    
      71        __ASM volatile ("cpsid i" : : : "memory");
    080053e8:   cpsid   i
     79         x++;
    080053ea:   ldr     r2, [pc, #160]  ; (0x800548c <main+168>)
    080053ec:   ldrb    r3, [r2, #0]
    080053ee:   adds    r3, #1
    080053f0:   strb    r3, [r2, #0]
      60        __ASM volatile ("cpsie i" : : : "memory");
    

    这两种指令只需额外花费2或4个时钟。

    它保证了原子性,并且不提供不必要的开销

        3
  •  0
  •   ensc    7 年前

    dmb 在以下情况下需要

    p1:
        str r5, [r1]
        str r0, [r2]
    

    p2:
        wait([r2] == 0)
        ldr r5, [r1]
    

    (来自 http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/Barrier_Litmus_Tests_and_Cookbook_A08.pdf ,第6.2.1节“弱序消息传递问题”)。

    在CPUI中,优化可以对上的指令重新排序 p1 所以你必须插入一个 dmb 在两家商店之间。

    dmb 这可能是由膨胀引起的 atomic_xxx() 哪一个可能 dmb 在开始和结束时。

    acquire_head:
            ldr     r2, .L8
            dmb     ish
    .L2:
            // int32_t old_h = atomic_load(&_head);
            ldr     r1, [r2]
    ...
            bne     .L5
    .L6:
            bne     .L2
            dmb     ish
            bx      lr
    

    没有别的 之间

    性能影响很难估计(您必须对代码进行基准测试,无论有无测试) dmb ). dmb 不消耗cpu周期;它只是停止了cpu内部的管道传输。

    推荐文章