x86_64 and/or armv7-m mov 指令可以在运行中中断吗?

Can an x86_64 and/or armv7-m mov instruction be interrupted mid-operation?

我想知道何时需要为中断计数器使用原子类型或 volatile(或没什么特别的):

uint32_t uptime = 0;

// interrupt each 1 ms
ISR()
{
    // this is the only location which writes to uptime
    ++uptime;
}

void some_func()
{
    uint32_t now = uptime;
}

我个人认为volatile应该足够了并且保证无错误操作和一致性(递增值直到溢出)。

但我想到,当 moving/setting 个单独的位时,mov 指令可能会在操作中途中断,在 x86_64 and/or 上是否可能armv7-m?

例如 mov 指令将开始执行,设置 16 位,然后将被抢占,ISR 将 运行 将 uptime 增加一(并且可能改变所有位)然后 mov 指令将继续。我找不到任何可以向我保证工作顺序的 material。

这在 armv7-m 上是否也一样?

使用 sig_atomic_t 是始终获得无错误和一致结果的正确解决方案还是 "overkill"?

例如ARM7-M架构指定:

In ARMv7-M, the single-copy atomic processor accesses are:
• All byte accesses.
• All halfword accesses to halfword-aligned locations.
• All word accesses to word-aligned locations.

&uptime % 8 == 0 的断言是否足以保证这一点?

  1. 使用可变的。您的编译器不知道中断。它可能假设 ISR() 函数从未被调用(您的代码中是否有任何地方调用 ISR?)。这意味着 uptime 永远不会递增,这意味着 uptime 将始终为零,这意味着 uint32_t now = uptime; 可以安全地优化为 uint32_t now = 0;。使用 volatile uint32_t uptime。这样优化器就不会优化 uptime 了。
  2. 字长。 uint32_t 变量有 4 个字节。所以在 32 位处理器上需要 1 条指令来获取它的值,但在 8 位处理器上至少需要 4 条指令(通常)。所以在 32 位处理器上你不需要在加载 uptime 的值之前禁用中断,因为中断例程将在当前指令在处理器上执行之前或之后开始执行。处理器不能在指令中途中断路由,这是不可能的。在 8 位处理器上,我们需要在读取正常运行时间之前禁用中断,例如:

    禁用中断(); uint32_t 现在 = 正常运行时间; EnableInterrupts();

  3. C11 原子类型。我从未见过使用它们的真正嵌入式代码,仍在等待,我到处都看到 volatile 。这取决于您的编译器,因为编译器实现原子类型和 atomic_* 函数。这取决于编译器。是否 100% 确定从 atomic_t 变量读取时您的编译器将禁用 ISR() 中断?检查从 atomic_* 调用生成的程序集输出,您肯定会知道。 This 是一本好书。我希望 atomic* C11 类型适用于多个线程之间的并发,它可以随时切换执行上下文。在中断和正常上下文之间使用它可能会阻止您的 cpu,因为一旦您进入 IRQ,您只有在服务 IRQ 之后才能恢复正常执行,即。 some_func 设置互斥锁以读取 uptime,然后 IRQ 启动,如果互斥锁关闭,IRQ 将进入一个循环,这将导致无限循环。

  4. 例如 HAL_GetTick() 实现,来自 here, removed __weak macro and substituted __IO macro by volatile, those macros are defined in cmsis file:

static volatile uint32_t uwTick;

void HAL_IncTick(void)
{
  uwTick++;
}

uint32_t HAL_GetTick(void)
{
  return uwTick;
}

通常 HAL_IncTick() 每 1 毫秒从系统中断调用一次。

volatile 只是编译器的建议,应该存储值。通常对于这个单位,它存储在任何 CPU 寄存器中。但是如果编译器不会接受这个 space 因为它忙于其他操作,它将被忽略并传统上存储在内存中。这是主要规则。

那我们来看看架构。所有本机类型的本机 CPU 指令都是原子的。但是很多操作可以分成两步,这时应该将值从内存复制到内存。在那种情况下可以做一些 cpu 中断。但别担心,这是正常的。当值不会存储到准备好的变量中时,您可以将其理解为未完全提交的操作。

问题是当您使用的字长于 CPU 中实现的字时,例如 16 位或 8 位处理器中的 u32bit。在那种情况下,阅读和写作的价值将被分成许多步骤。那么它就确定了,然后一部分值被存储,另一部分不存储,你会得到错误的损坏值。

在这种情况下,禁用中断并不总是好的方法,因为这可能会花费很长时间。当然你可以使用锁定,但这也能起到同样的作用。 但是你可以制作一些结构,第一个字段作为数据,第二个字段作为适合架构的计数器。然后当你读取那个值时,你可以首先得到计数器作为第一个值,然后得到值,最后第二次得到计数器。当计数器不同时,你应该重复这个过程。 当然,它并不能保证一切都会正确,但通常它可以节省很多 cpu 周期。比如你会用16bit的附加计数器来校验,它是65536个值。然后当你第一次读取第二个计数器时,你的主进程必须被冻结很长的周期,在这个例子中它应该是 65536 错过的中断,为主计数器或任何其他存储值制造错误。

当然,如果您在 32 位架构中使用 32 位值,这不是问题,您不需要特别保护该操作,独立或架构。当然,除非体系结构将其所有操作都作为原子进行:)

示例代码:

struct
{
  ucint32_t value; //us important value
  int watchdog;  //for value secure, long platform depended, usually at least 32bits
} SecuredCounter;

ISR()
{
  // this is the only location which writes to uptime
  ++SecuredCounter.value;
  ++SecuredCounter.watchdog;
}

void some_func()
{
    uint32_t now = Read_uptime;
}

ucint32_t Read_uptime;
{
    int secure1; //length platform dependee
    ucint32_t value;
    int secure2;
    while (1) {
        longint secure1=SecuredCounter.watchdog;  //read first
        ucint32_t value=SecuredCounter.value;     //read value
        longint secure2=SecuredCounter.watchdog;  //read second, should be as first
        if (secure1==secure2) return value; //this is copied and should be proper
    };
};

不同的做法是制作两个相同的计数器,你应该在单一功能中增加它。在 read 函数中,您将两个值都复制到局部变量,并比较它们是否相同。如果是,那么值是正确的,return 单一个。如果不同,重复阅读。别担心,如果值不同,那么你的阅读功能就被打断了。机会很少,反复阅读后会再次发生。但如果发生这种情况,它就不可能停止循环。

您必须阅读每个单独核心 and/or 芯片的文档。 x86 是完全独立于 ARM 的东西,在两个系列中,每个实例都可能不同于任何其他实例,每次都可以并且应该期望是全新的设计。可能不是,但有时是。

注意事项如评论中所述。

typedef unsigned int uint32_t;

uint32_t uptime = 0;

void ISR ( void )
{
    ++uptime;
}
void some_func ( void )
{
    uint32_t now = uptime;
}

在我今天使用的工具的机器上:

Disassembly of section .text:

00000000 <ISR>:
   0:   e59f200c    ldr r2, [pc, #12]   ; 14 <ISR+0x14>
   4:   e5923000    ldr r3, [r2]
   8:   e2833001    add r3, r3, #1
   c:   e5823000    str r3, [r2]
  10:   e12fff1e    bx  lr
  14:   00000000    andeq   r0, r0, r0

00000018 <some_func>:
  18:   e12fff1e    bx  lr

Disassembly of section .bss:

00000000 <uptime>:
   0:   00000000    andeq   r0, r0, r0

这可能会有所不同,但如果有一天您在一台机器上发现某个工具出现问题,那么您可以认为这是一个问题。到目前为止,我们实际上还可以。因为 some_func 是死代码,所以读取被优化了。

typedef unsigned int uint32_t;

uint32_t uptime = 0;

void ISR ( void )
{
    ++uptime;
}
uint32_t some_func ( void )
{
    uint32_t now = uptime;
    return(now);
}

固定

00000000 <ISR>:
   0:   e59f200c    ldr r2, [pc, #12]   ; 14 <ISR+0x14>
   4:   e5923000    ldr r3, [r2]
   8:   e2833001    add r3, r3, #1
   c:   e5823000    str r3, [r2]
  10:   e12fff1e    bx  lr
  14:   00000000    andeq   r0, r0, r0

00000018 <some_func>:
  18:   e59f3004    ldr r3, [pc, #4]    ; 24 <some_func+0xc>
  1c:   e5930000    ldr r0, [r3]
  20:   e12fff1e    bx  lr
  24:   00000000    andeq   r0, r0, r0

由于像 mips 和 arm 这样的核心在默认情况下倾向于为未对齐的访问中止数据,我们可能会假设该工具不会为这样一个干净的定义生成未对齐的地址。但如果我们要谈论打包结构,那是另一个故事,你告诉编译器生成一个未对齐的访问,它会......如果你想要安全,请记住 ARM 中的 "word" 是 32 位,所以你可以断言变量的地址 AND 3.

x86 也会假设这样一个干净的定义会导致对齐的变量,但 x86 默认情况下没有数据错误问题,因此编译器更加自由......像我一样专注于 arm认为这是你的问题。

现在如果我这样做:

typedef unsigned int uint32_t;

uint32_t uptime = 0;

void ISR ( void )
{
    if(uptime)
    {
        uptime=uptime+1;
    }
    else
    {
        uptime=uptime+5;
    }
}
uint32_t some_func ( void )
{
    uint32_t now = uptime;
    return(now);
}

00000000 <ISR>:
   0:   e59f2014    ldr r2, [pc, #20]   ; 1c <ISR+0x1c>
   4:   e5923000    ldr r3, [r2]
   8:   e3530000    cmp r3, #0
   c:   03a03005    moveq   r3, #5
  10:   12833001    addne   r3, r3, #1
  14:   e5823000    str r3, [r2]
  18:   e12fff1e    bx  lr
  1c:   00000000    andeq   r0, r0, r0

并添加 volatile

00000000 <ISR>:
   0:   e59f3018    ldr r3, [pc, #24]   ; 20 <ISR+0x20>
   4:   e5932000    ldr r2, [r3]
   8:   e3520000    cmp r2, #0
   c:   e5932000    ldr r2, [r3]
  10:   12822001    addne   r2, r2, #1
  14:   02822005    addeq   r2, r2, #5
  18:   e5832000    str r2, [r3]
  1c:   e12fff1e    bx  lr
  20:   00000000    andeq   r0, r0, r0

两次读取导致两次读取。如果 read-modify-write 可以被打断,现在这里有一个问题,但我们假设这是一个 ISR,它不能吗?如果你要读一个 7,加一个 1 然后写一个 8 如果你在读后被一些也在修改正常运行时间的东西打断,那个修改的寿命有限,它的修改发生了,比如写了一个 5,那么这个 ISR 写一个8在上面,如果它。

如果 read-modify-write 在可中断代码中,那么 isr 可能会进入那里,它可能不会按您想要的方式工作。这是两个 reader 的两个作者,一个负责编写共享资源,另一个 read-only。否则你需要做更多没有内置到语言中的工作。

手臂机器注意事项:

typedef int __sig_atomic_t;
...
typedef __sig_atomic_t sig_atomic_t;

所以

typedef unsigned int uint32_t;
typedef int sig_atomic_t;
volatile sig_atomic_t uptime = 0;
void ISR ( void )
{
    if(uptime)
    {
        uptime=uptime+1;
    }
    else
    {
        uptime=uptime+5;
    }
}
uint32_t some_func ( void )
{
    uint32_t now = uptime;
    return(now);
}

不会改变结果。至少在那个系统上没有那个定义,需要检查其他 C 库 and/or 沙箱 headers 看看他们定义了什么,或者如果你不小心(经常发生)错误 headers被使用时,x6_64 headers 用于使用交叉编译器构建 arm 程序。看到 gcc 和 llvm 使主机与目标错误。

回到一个问题,根据您的评论,您似乎已经理解了该问题

typedef unsigned int uint32_t;
uint32_t uptime = 0;
void ISR ( void )
{
    if(uptime)
    {
        uptime=uptime+1;
    }
    else
    {
        uptime=uptime+5;
    }
}
void some_func ( void )
{
    while(uptime&1) continue;
}

即使您有一位作者和一位作者,评论中也指出了这一点 reader

00000020 <some_func>:
  20:   e59f3018    ldr r3, [pc, #24]   ; 40 <some_func+0x20>
  24:   e5933000    ldr r3, [r3]
  28:   e2033001    and r3, r3, #1
  2c:   e3530000    cmp r3, #0
  30:   012fff1e    bxeq    lr
  34:   e3530000    cmp r3, #0
  38:   012fff1e    bxeq    lr
  3c:   eafffffa    b   2c <some_func+0xc>
  40:   00000000    andeq   r0, r0, r0

它永远不会回头从内存中读取变量,除非有人破坏了事件处理程序中的寄存器,否则这可能是一个无限循环。

使正常运行时间不稳定:

00000024 <some_func>:
  24:   e59f200c    ldr r2, [pc, #12]   ; 38 <some_func+0x14>
  28:   e5923000    ldr r3, [r2]
  2c:   e3130001    tst r3, #1
  30:   012fff1e    bxeq    lr
  34:   eafffffb    b   28 <some_func+0x4>
  38:   00000000    andeq   r0, r0, r0

现在 reader 每次都读取一次。

同样的问题,不是在循环中,没有 volatile。

00000020 <some_func>:
  20:   e59f302c    ldr r3, [pc, #44]   ; 54 <some_func+0x34>
  24:   e5930000    ldr r0, [r3]
  28:   e3500005    cmp r0, #5
  2c:   0a000004    beq 44 <some_func+0x24>
  30:   e3500004    cmp r0, #4
  34:   0a000004    beq 4c <some_func+0x2c>
  38:   e3500001    cmp r0, #1
  3c:   03a00006    moveq   r0, #6
  40:   e12fff1e    bx  lr
  44:   e3a00003    mov r0, #3
  48:   e12fff1e    bx  lr
  4c:   e3a00007    mov r0, #7
  50:   e12fff1e    bx  lr
  54:   00000000    andeq   r0, r0, r0

两次测试之间的正常运行时间可能发生了变化。 volatile 解决了这个问题。

所以 volatile 不是通用的解决方案,将变量用于一种通信方式是理想的,需要使用单独的变量以另一种方式进行通信,一个作者一个或多个 readers per.

您做对了,您查阅了 chip/core

的文档

因此,如果对齐(在本例中为 32 位字)并且编译器选择了正确的指令,则中断不会中断事务。如果它是一个 LDM/STM 虽然你应该阅读文档(push 和 pop 也是 LDM/STM 伪指令)在一些 cores/architectures 那些可以被中断并重新启动因此我们被警告那些arm 文档中的情况。

简短回答,添加 volatile,并使每个变量只有一个编写器。并保持变量对齐。 (并在每次更改 chips/cores 时阅读文档,并定期反汇编以检查编译器是否按照您的要求进行操作)。不管它是来自同一供应商还是不同供应商的相同核心类型(另一个 cortex-m3),或者它是否完全不同 core/chip(avr、msp430、pic、x86、mips 等),从零,获取文档并阅读它们,检查编译器输出。

TL:DR: 如果对齐的 uint32_t 自然是原子的(它在 x86 和 ARM 上),则使用 volatile。您的代码在技术上将具有 C11 未定义行为,但实际实现将使用 volatile.

执行您想要的操作

或者使用 C11 stdatomic.hmemory_order_relaxed 如果你想告诉编译器你的意思。如果正确使用它,它将编译为与 x86 和 ARM 上的 volatile 相同的 asm。

(但是如果你真的需要它在单核 CPU 上有效地 运行,其中对齐的 uint32_t 的 load/store 不是原子的 "for free",例如只有 8 位寄存器,您可能宁愿禁用中断而不是让 stdatomic 回退到使用锁来序列化计数器的读写。)


在所有 CPU 架构 上,对于同一内核上的中断,整个指令始终是原子的。部分完成的指令在服务中断之前完成或丢弃(不提交它们的存储)。

对于单核,CPUs 始终保持 运行ning 指令的错觉,按程序顺序。这包括仅在指令之间的边界上发生的中断。 请参阅@supercat 在 上的单核答案。如果机器有 32 位寄存器,您可以安全地假设 volatile uint32_t 将用一条指令加载或存储。正如@old_timer 指出的那样,请注意 ARM 上未对齐的打包结构成员,但除非您使用 __attribute__((packed)) 或其他方式手动执行此操作,否则 x86 和 ARM 上的正常 ABI 可确保自然对齐。

来自未对齐操作数或窄总线的单个指令的多个总线事务仅对来自另一个核心或非CPU硬件设备的并发读+写有影响。 (例如,如果您要存储到设备内存)。

一些长运行ning x86 指令,如rep movsvpgatherdd 具有定义明确的方法来部分完成异常或中断:更新寄存器,因此重新运行宁指令做正确的事。但除此之外,一条指令要么有 运行,要么没有,即使是 "complex" 指令,如执行 read/modify/write 的内存目标 add。)IDK 如果任何人都曾提出 CPU 可以 suspend/result 跨越中断而不是取消它们的多步指令,但 x86 和 ARM 绝对不是那样。计算机体系结构研究论文中有很多奇怪的想法。但是似乎不太值得保留所有必要的微体系结构状态以在部分执行的指令中间恢复,而不是在从中断返回后重新解码它。

这就是为什么 AVX2 / AVX512 收集 总是 需要一个收集掩码,即使你想收集所有元素,以及为什么它们会破坏掩码(所以你必须重新设置它在下一次聚会之前再次向全体成员。


在您的情况下,您只需要存储(并在 ISR 之外加载)是原子的。您不需要 整个++uptime 是原子的。 你可以用 C11 stdatomic 来表达这个:

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

_Atomic uint32_t uptime = 0;

// interrupt each 1 ms
void ISR()
{
    // this is the only location which writes to uptime
    uint32_t tmp = atomic_load_explicit(&uptime, memory_order_relaxed);
    // the load doesn't even need to be atomic, but relaxed atomic is as cheap as volatile on machines with wide-enough loads
    atomic_store_explicit(&uptime, tmp+1, memory_order_relaxed);

    // some x86 compilers may fail to optimize to  add dword [uptime],1
    // but uptime+=1 would compile to  LOCK ADD (an atomic increment), which you don't want.
}

// MODIFIED: return the load result
uint32_t some_func()
{
    // this does need to be an atomic load
    // you typically get that by default with volatile, too
    uint32_t now = atomic_load_explicit(&uptime, memory_order_relaxed);
    return now;
}

volatile uint32_t 在 x86 和 ARM 上编译为完全相同的 asm。 我输入代码on the Godbolt compiler explorer。这就是 clang6.0 -O3 为 x86-64 所做的。 (对于 -mtune=bdver2,它使用 inc 而不是 add,但它知道 memory-destination inc 是 inc 仍然比 Intel 上的 add 差的少数情况之一 :)

ISR:                                    # @ISR
    add     dword ptr [rip + uptime], 1
    ret
some_func:                              # @some_func
    mov     eax, dword ptr [rip + uptime]
    ret
inc_volatile:           //  void func(){ volatile_var++; }
    add     dword ptr [rip + volatile_var], 1
    ret
不幸的是,

gcc 对 volatile_Atomic 使用单独的 load/store 指令。

    # gcc8.1 -O3 
    mov     eax, DWORD PTR uptime[rip]
    add     eax, 1
    mov     DWORD PTR uptime[rip], eax

至少这意味着在 gcc 或 clang 上使用 _Atomicvolatile _Atomic 没有任何缺点。

Plain uint32_t 没有任何一个限定符都不是真正的选择,至少对于读取端不是。您可能不希望编译器将 get_time() 提升出循环并在每次迭代中使用相同的时间。如果您确实需要,可以将其复制到本地。如果编译器 将其保存在寄存器中,这可能会导致额外的工作而没有任何好处(例如,跨函数调用编译器最容易从静态存储中重新加载)。但是,在 ARM 上,复制到本地可能实际上有帮助,因为这样它就可以相对于堆栈指针引用它,而不需要在另一个寄存器中保留静态地址或重新生成地址。 (由于其可变长度指令集,x86 可以使用一条大指令从静态地址加载。)


如果你想要更强的内存顺序,你可以使用atomic_signal_fence(memory_order_release);或其他任何东西(signal_fence而不是thread_fence) 告诉编译器你只关心 wrt 的排序。 运行ning 在 same CPU 上异步编码("in the same thread" 就像一个信号处理程序),所以它只需要阻止编译时重新排序,不发出任何内存屏障指令,如 ARM dmb.

例如在 ISR 中:

  uint32_t tmp = atomic_load_explicit(&idx,  memory_order_relaxed);
  tmp++;
  shared_buf[tmp] = 2;   // non-atomic
                         // Then do a release-store of the index
  atomic_signal_fence(memory_order_release);
  atomic_load_explicit(&idx, tmp, memory_order_relaxed);

然后 reader 加载 idx、运行 atomic_signal_fence(memory_order_acquire); 并从 shared_buf[tmp] 读取是安全的,即使 shared_buf 不是 _Atomic。 (假设您解决了环绕问题等。)