我没有找到太多关于非原子操作的材料。
假设我有一个32位处理器,我想在一个64位变量中计算微秒数。每微秒都会有一个中断更新该变量。调度程序是非抢占式的。将会有一个函数用来清除变量,另一个函数用来读取它。由于这是一个32位处理器,因此访问将是非原子性的。是否有一种“标准”或惯用的方法来处理这个问题,以便读取函数不会得到半更新的值?
// ==========
// Do this:
// ==========
// global volatile variables for use in ISRs
volatile uint64_t u1;
volatile uint64_t u2;
volatile uint64_t u3;
int main()
{
// main loop
while (true)
{
uint64_t u1_copy;
uint64_t u2_copy;
uint64_t u3_copy;
// use atomic access guards to copy out the volatile variables
// 1. Save the current interrupt state
const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;
// 2. Turn interrupts off
interrupts_off();
// copy your volatile variables out
u1_copy = u1;
u2_copy = u2;
u3_copy = u3;
// 3. Restore the interrupt state to what it was before disabling it.
// This leaves interrupts disabled if they were previously disabled
// (ex: inside an ISR where interrupts get disabled by default as it
// enters--not all ISRs are this way, but many are, depending on your
// device), and it re-enables interrupts if they were previously
// enabled. Restoring interrupt state rather than enabling interrupts
// is the right way to do it, and it enables this atomic access guard
// style to be used both inside inside **and** outside ISRs.
INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;
// Now use your copied variables in any calculations
}
}
// ==========
// NOT this!
// ==========
volatile uint64_t u1;
volatile uint64_t u2;
volatile uint64_t u3;
int main()
{
// main loop
while (true)
{
// 1. Save the current interrupt state
const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;
// 2. Turn interrupts off
interrupts_off();
// Now use your volatile variables in any long calculations
// - This is not as good as using copies! This would leave interrupts
// off for an unnecessarily long time, introducing a ton of jitter
// into your measurements and code.
// 3. Restore the interrupt state to what it was before disabling it.
INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;
}
}
在更新易失性变量时,尽量减少中断关闭的时间,只在更新过程中禁用它们。
// global volatile variables for use in ISRs
volatile uint64_t u1;
volatile uint64_t u2;
volatile uint64_t u3;
int main()
{
// main loop
while (true)
{
// Do calculations here, **outside** the atomic access interrupt guards
const uint32_t INTERRUPT_STATE_BAK = INTERRUPT_STATE_REGISTER;
interrupts_off();
// quickly update your variables and exit the guards
u1 = 1234;
u2 = 2345;
u3 = 3456;
INTERRUPT_STATE_REGISTER = INTERRUPT_STATE_BAK;
}
}
doAtomicRead()
:确保在不关闭中断的情况下进行原子读取!与上述示例中使用的原子访问保护相比,另一种选择是重复读取变量,直到它不再改变,这表明在你只读取了部分字节后,变量没有被更新。
以下是该方法。@Brendan和@chux-ReinstateMonica和我在@chux-ReinstateMonica's answer下讨论了一些想法。
#include <stdint.h> // UINT64_MAX
#define MAX_NUM_ATOMIC_READ_ATTEMPTS 3
// errors
#define ATOMIC_READ_FAILED (UINT64_MAX)
/// @brief Use a repeat-read loop to do atomic-access reads of a
/// volatile variable, rather than using atomic access guards which
/// disable interrupts.
uint64_t doAtomicRead(const volatile uint64_t* val)
{
uint64_t val_copy;
uint64_t val_copy_atomic = ATOMIC_READ_FAILED;
for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i++)
{
val_copy = *val;
if (val_copy == *val)
{
val_copy_atomic = val_copy;
break;
}
}
return val_copy_atomic;
}
doAtomicRead()
函数,但这次附有详细的解释性注释。我还展示了一个被注释掉的微小变体,可能在某些情况下会有帮助,如注释中所解释的那样。/// @brief Use a repeat-read loop to do atomic-access reads of a
/// volatile variable, rather than using atomic access guards which
/// disable interrupts.
///
/// @param[in] val Ptr to a volatile variable which is updated
/// by an ISR and needs to be read atomically.
/// @return A copy of an atomic read of the passed-in variable,
/// if successful, or sentinel value ATOMIC_READ_FAILED if the max number
/// of attempts to do the atomic read was exceeded.
uint64_t doAtomicRead(const volatile uint64_t* val)
{
uint64_t val_copy;
uint64_t val_copy_atomic = ATOMIC_READ_FAILED;
// In case we get interrupted during this code block, and `val` gets updated
// in that interrupt's ISR, try `MAX_NUM_ATOMIC_READ_ATTEMPTS` times to get
// an atomic read of `val`.
for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i++)
{
val_copy = *val;
// An interrupt could have fired mid-read while doing the **non-atomic**
// read above, updating the 64-bit value in the ISR and resulting in
// 32-bits of the old value in the 64-bit variable being wrong now
// (since the whole 64-bit value has just been updated with a new
// value), so verify the read above with a new read again.
//
// Caveat:
//
// Note that this method is **not _always_** foolproof, as technically
// the interrupt could fire off and run again during this 2nd read,
// causing a very rare edge-case where the exact same incorrect value
// gets read again, resulting in a false positive where it assigns an
// erroneous value to `val_copy_atomic`! HOWEVER, that is for **you or
// I** to design and decide as the architect.
//
// Is it _possible_ for the ISR to really fire off again immediately
// after returning? Or, would that never happen because we are
// guaranteed some minimum time gap between interrupts? If the former,
// you should read the variable again a 3rd or 4th time by uncommenting
// the extra code block below in order to check for consistency and
// minimize the chance of an erroneous `val_copy_atomic` value. If the
// latter, however, and you know the ISR won't fire off again for at
// least some minimum time value which is large enough for this 2nd
// read to occur **first**, **before** the ISR gets run for the 2nd
// time, then you can safely say that this 2nd read is sufficient, and
// you are done.
if (val_copy == *val)
{
val_copy_atomic = val_copy;
break;
}
// Optionally delete the "if" statement just above and do this instead.
// Refer to the long "caveat" note above to see if this might be
// necessary. It is only necessary if your ISR might fire back-to-back
// with essentially zero time delay between each interrupt.
// for (size_t j = 0; j < 4; j++)
// {
// if (val_copy == *val)
// {
// val_copy_atomic = val_copy;
// break;
// }
// }
}
return val_copy_atomic;
}
*val
,而不是两次。以下是优化后的代码:
[这是我最喜欢的版本:]
uint64_t doAtomicRead(const volatile uint64_t* val)
{
uint64_t val_copy_new;
uint64_t val_copy_old = *val;
uint64_t val_copy_atomic = ATOMIC_READ_FAILED;
for (size_t i = 0; i < MAX_NUM_ATOMIC_READ_ATTEMPTS; i++)
{
val_copy_new = *val;
if (val_copy_new == val_copy_old)
{
// no change in the new reading, so we can assume the read was not
// interrupted during the first reading
val_copy_atomic = val_copy_new;
break;
}
// update the old reading, to compare it with the new reading in the
// next iteration
val_copy_old = val_copy_new;
}
return val_copy_atomic;
}
doAtomicRead()
的示例:// global volatile variable shared between ISRs and main code
volatile uint64_t u1;
// Inside your function: "atomically" read and copy the volatile variable
uint64_t u1_copy = doAtomicRead(&u1);
if (u1_copy == ATOMIC_READ_FAILED)
{
printf("Failed to atomically read variable `u1`.\n");
// Now do whatever is appropriate for error handling; examples:
goto done;
// OR:
return;
// etc.
}
doAtomicRead()
技巧可以很好地工作。doAtomicRead()
函数的效率,将其更新为执行这个操作。除非你错过了2^32次计数,唯一可能出现的撕裂是当低位卷绕时,高位会被递增。这就像检查整个值,但重试的频率会更低。
演示一个基本且高效的无锁 SPSC(单生产者单消费者)环形缓冲队列,在 C 中(也适用于 C++)。
该队列仅在 SPSC 上下文中无锁工作,比如在需要从中断服务例程(ISR)向主循环发送数据的裸机微控制器上。
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
my_var_copy = my_var;
}
*val
一次,在第一次之前多加载一次。 - Gabriel StaplesMAX_NUM_ATOMIC_READ_ATTEMPTS
=3时,GCC -Os
仍然完全展开循环,因此read1=read2
不需要任何指令成本。将值设置为300或其他可见编译器创建循环,在这种情况下,2个ldrd
比ldrd
+ 2个mov
少一些指令,但差别不大。 - Peter Cordestodo
注释,但在搞砸之后重新开始编辑,并且显然从Godbolt复制/粘贴了错误代码块的一部分,这个代码块只是之前看起来类似于我一直在查看的那个。不确定为什么您想保留具有2个读取的版本并将其显示为第一个。差别不大,所以我认为未来的读者可能会因为同一算法的多个实现而陷入困境;它们都易于阅读。当然,这是您的答案,因此由您决定,这只是我的意见/印象。 - Peter Cordes在ISR内部,通常会防止后续中断的发生(除非有更高优先级,但这时计数通常不会受影响),因此只需简单地执行 count_of_microseconds ++;
在ISR之外,要访问(读取或写入) count_of_microseconds
,需要具有中断保护或原子访问的功能。
当无法使用 atomic
但可以使用解释控制:*1
volatile uint64_t count_of_microseconds;
...
saved_interrupt_state();
disable_interrupts();
uint64_t my_count64 = count_of_microseconds;
restore_interrupt_state();
// now use my_count64
atomic_ullong count_of_microseconds;
...
unsigned long long my_count64 = count_of_microseconds;
// now use my_count64
自C89以来,使用volatile
与count_of_microseconds
。
[更新]
无论使用哪种方法(包括此答案或其他答案)在非ISR代码中读写计数器,我建议将读写代码封装在一个辅助函数中,以隔离这一关键操作集。
*1 <stdatomic.h>
自C11起可用,并且未定义__STDC_NO_ATOMICS__
。
#if __STDC_VERSION__ >= 201112
#ifndef __STDC_NO_ATOMICS__
#include <stdatomic.h>
#endif
#endif
new_high = counter[1]; do { old_high = new_high; low = counter[0]; new_high = counter[1]; } while (old_high != new_high);
。 - Brendandisable(); read; enable()
,因为中断可能已经被禁用,最好的方法是恢复状态而不是启用它。 - chux - Reinstate Monica很高兴听到“读两次”方法是可行的。我之前有些怀疑,不知道为什么。与此同时,我想出了这个:
struct
{
uint64_t ticks;
bool toggle;
} timeKeeper = {0};
void timeISR()
{
ticks++;
toggle = !toggle;
}
uint64_t getTicks()
{
uint64_t temp = 0;
bool startToggle = false;
do
{
startToggle = timeKeeper.toggle;
temp = timekeeper.ticks;
} while (startToggle != timeKeeper.toggle);
return temp;
}
ticks
已经是一个计数器了。所以这本质上就变成了“读两次”。 - Nate Eldredgevolatile
,所以你没有强制编译器使汇编读取两次。(使用32位原子操作实现64位原子计数器)。 - Peter Cordes
ATOMIC_LLONG_LOCK_FREE
来了解。如果它不是无锁定的,则会非常昂贵。 - phuclv