带有 `volatile` 数组的 `memcpy((void *)dest, src, n)` 安全吗?
Is `memcpy((void *)dest, src, n)` with a `volatile` array safe?
我有一个用于 UART 的缓冲区,它是这样声明的:
union Eusart_Buff {
uint8_t b8[16];
uint16_t b9[16];
};
struct Eusart_Msg {
uint8_t msg_posn;
uint8_t msg_len;
union Eusart_Buff buff;
};
struct Eusart {
struct Eusart_Msg tx;
struct Eusart_Msg rx;
};
extern volatile struct Eusart eusart;
这里是填充缓冲区的函数(将使用中断发送):
void eusart_msg_transmit (uint8_t n, void *msg)
{
if (!n)
return;
/*
* The end of the previous transmission will reset
* eusart.tx.msg_len (i.e. ISR is off)
*/
while (eusart.tx.msg_len)
;
if (data_9b) {
memcpy((void *)eusart.tx.buff.b9, msg,
sizeof(eusart.tx.buff.b9[0]) * n);
} else {
memcpy((void *)eusart.tx.buff.b8, msg,
sizeof(eusart.tx.buff.b8[0]) * n);
}
eusart.tx.msg_len = n;
eusart.tx.msg_posn = 0;
reg_PIE1_TXIE_write(true);
}
在使用memcpy()
的那一刻,我知道没有其他人会使用缓冲区(原子),因为while
循环确保最后一条消息已经发送,因此中断被禁用。
以这种方式丢弃 volatile
以便我能够使用 memcpy()
是否安全,或者我是否应该创建一个可能被称为 memcpy_v()
的函数才安全?:
void *memcpy_vin(void *dest, const volatile void *src, size_t n)
{
const volatile char *src_c = (const volatile char *)src;
char *dest_c = (char *)dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
volatile void *memcpy_vout(volatile void *dest, const void *src, size_t n)
{
const char *src_c = (const char *)src;
volatile char *dest_c = (volatile char *)dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
volatile void *memcpy_v(volatile void *dest, const volatile void *src, size_t n)
{
const volatile char *src_c = (const volatile char *)src;
volatile char *dest_c = (volatile char *)dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
编辑:
如果我需要那些新功能,
鉴于我知道没有人会同时修改数组,使用 restrict
来(也许)帮助编译器优化(如果可以的话)是否有意义?
可能是这样(如果我错了请纠正我):
volatile void *memcpy_v(restrict volatile void *dest,
const restrict volatile void *src,
size_t n)
{
const restrict volatile char *src_c = src;
restrict volatile char *dest_c = dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
编辑 2(添加上下文):
void eusart_end_transmission (void)
{
reg_PIE1_TXIE_write(false); /* TXIE is TX interrupt enable */
eusart.tx.msg_len = 0;
eusart.tx.msg_posn = 0;
}
void eusart_tx_send_next_c (void)
{
uint16_t tmp;
if (data_9b) {
tmp = eusart.tx.buff.b9[eusart.tx.msg_posn++];
reg_TXSTA_TX9D_write(tmp >> 8);
TXREG = tmp;
} else {
TXREG = eusart.tx.buff.b8[eusart.tx.msg_posn++];
}
}
void __interrupt() isr(void)
{
if (reg_PIR1_TXIF_read()) {
if (eusart.tx.msg_posn >= eusart.tx.msg_len)
eusart_end_transmission();
else
eusart_tx_send_next_c();
}
}
虽然volatile
可能不需要是需要的(我问的在另一个问题中:),这个问题仍然应该在假设需要 volatile
的情况下回答,以便未来真正需要 volatile
的用户(例如我我实现了RX缓冲区),可以知道该怎么做了。
编辑(相关)(Jul/19):
基本上说不需要volatile
,因此这个问题就消失了。
Is memcpy((void *)dest, src, n)
with a volatile
array safe?
没有。在一般情况下,memcpy()
未指定与易失性内存一起正常工作。
OP的案例看起来可以扔掉volatile
,但发布的代码不足以确定。
如果代码想要memcpy()
volatile
内存,写辅助函数。
OP 的代码 restrict
在错误的地方。建议
volatile void *memcpy_v(volatile void *restrict dest,
const volatile void *restrict src, size_t n) {
const volatile unsigned char *src_c = src;
volatile unsigned char *dest_c = dest;
while (n > 0) {
n--;
dest_c[n] = src_c[n];
}
return dest;
}
编写自己的 memcpy_v()
的一个单一原因是编译器可以 "understand"/分析 memcpy()
并发出与预期截然不同的代码 - 甚至优化它,如果编译器认为不需要 副本。提醒自己编译器认为 memcpy()
操纵的内存是非易失性的。
然而 OP 错误地使用了 volatile struct Eusart eusart;
。访问 eusart
需要 volatile
不提供的保护。
在 OP 的情况下,代码可以将 volatile
放在缓冲区 上,然后使用 memcpy()
就好了。
剩下的问题是 OP 如何使用 eusart
的代码不足。使用 volatile
并不能解决 OP 的问题。 OP 确实断言 "I write to it atomically,",但没有发布 atomic
代码,这是不确定的。
像下面这样的代码使 eusart.tx.msg_len
成为 volatile
的好处,但这还不够。 volatile
确保 .tx.msg_len
不被缓存,而是每次都重新读取。
while (eusart.tx.msg_len)
;
然而 .tx.msg_len
的读取未指定为 atomic。当 .tx.msg_len == 256
和 ISR 触发时,递减 .tx.msg_len
,读取 LSbyte(256 中的 0)和 MSbyte(255 中的 0),非 ISR 代码可能会将 .tx.msg_len
视为0,而不是 255 或 256,因此在错误的时间结束了循环。 .tx.msg_len
的访问需要指定为不可分割的(原子的),否则,偶尔代码会莫名其妙地失败。
while (eusart.tx.msg_len);
也遭受无限循环的困扰。如果传输因 某些原因 而不是空的原因停止,则 while 循环永远不会退出。
建议改为在检查或更改时阻止中断 eusart.tx.msg_len, eusart.tx.msg_posn
。查看编译器对 atomic
或
的支持
size_t tx_msg_len(void) {
// pseudo-code
interrupt_enable_state = get_state();
disable_interrupts();
size_t len = eusart.tx.msg_len;
restore_state(interrupt_enable_state);
return len;
}
一般通信代码思路:
当非 ISR 代码读取或写入 eusart
时,请确保 ISR 不能 永远 更改 eusart
.
不要在步骤 #1 中长时间阻塞 ISR
。
不要假设基础 ISR()
将成功链接 input/output 而不会出现问题。顶层代码应该准备好在输出停止时重新启动输出。
标准没有任何方法可以让程序员可以要求在执行特定 volatile
指针访问之前完成通过普通指针访问存储区域的操作,并且也没有任何方法确保在执行某些特定的 volatile
指针访问之前,不会执行通过普通指针访问存储区域的操作。由于 volatile
操作的语义是实现定义的,标准的作者可能期望编译器编写者能够识别他们的客户何时可能需要这种语义,并以符合这些需求的方式指定他们的行为。不幸的是,这并没有发生。
实现您需要的语义要么使用 "popular extension",例如 clang 的 -fms-volatile
模式,一个特定于编译器的内部函数,要么用某些东西替换 memcpy
这是非常低效的,以至于淹没了编译器可以通过不支持这种语义获得的任何假定优势。
我有一个用于 UART 的缓冲区,它是这样声明的:
union Eusart_Buff {
uint8_t b8[16];
uint16_t b9[16];
};
struct Eusart_Msg {
uint8_t msg_posn;
uint8_t msg_len;
union Eusart_Buff buff;
};
struct Eusart {
struct Eusart_Msg tx;
struct Eusart_Msg rx;
};
extern volatile struct Eusart eusart;
这里是填充缓冲区的函数(将使用中断发送):
void eusart_msg_transmit (uint8_t n, void *msg)
{
if (!n)
return;
/*
* The end of the previous transmission will reset
* eusart.tx.msg_len (i.e. ISR is off)
*/
while (eusart.tx.msg_len)
;
if (data_9b) {
memcpy((void *)eusart.tx.buff.b9, msg,
sizeof(eusart.tx.buff.b9[0]) * n);
} else {
memcpy((void *)eusart.tx.buff.b8, msg,
sizeof(eusart.tx.buff.b8[0]) * n);
}
eusart.tx.msg_len = n;
eusart.tx.msg_posn = 0;
reg_PIE1_TXIE_write(true);
}
在使用memcpy()
的那一刻,我知道没有其他人会使用缓冲区(原子),因为while
循环确保最后一条消息已经发送,因此中断被禁用。
以这种方式丢弃 volatile
以便我能够使用 memcpy()
是否安全,或者我是否应该创建一个可能被称为 memcpy_v()
的函数才安全?:
void *memcpy_vin(void *dest, const volatile void *src, size_t n)
{
const volatile char *src_c = (const volatile char *)src;
char *dest_c = (char *)dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
volatile void *memcpy_vout(volatile void *dest, const void *src, size_t n)
{
const char *src_c = (const char *)src;
volatile char *dest_c = (volatile char *)dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
volatile void *memcpy_v(volatile void *dest, const volatile void *src, size_t n)
{
const volatile char *src_c = (const volatile char *)src;
volatile char *dest_c = (volatile char *)dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
编辑:
如果我需要那些新功能,
鉴于我知道没有人会同时修改数组,使用 restrict
来(也许)帮助编译器优化(如果可以的话)是否有意义?
可能是这样(如果我错了请纠正我):
volatile void *memcpy_v(restrict volatile void *dest,
const restrict volatile void *src,
size_t n)
{
const restrict volatile char *src_c = src;
restrict volatile char *dest_c = dest;
for (size_t i = 0; i < n; i++)
dest_c[i] = src_c[i];
return dest;
}
编辑 2(添加上下文):
void eusart_end_transmission (void)
{
reg_PIE1_TXIE_write(false); /* TXIE is TX interrupt enable */
eusart.tx.msg_len = 0;
eusart.tx.msg_posn = 0;
}
void eusart_tx_send_next_c (void)
{
uint16_t tmp;
if (data_9b) {
tmp = eusart.tx.buff.b9[eusart.tx.msg_posn++];
reg_TXSTA_TX9D_write(tmp >> 8);
TXREG = tmp;
} else {
TXREG = eusart.tx.buff.b8[eusart.tx.msg_posn++];
}
}
void __interrupt() isr(void)
{
if (reg_PIR1_TXIF_read()) {
if (eusart.tx.msg_posn >= eusart.tx.msg_len)
eusart_end_transmission();
else
eusart_tx_send_next_c();
}
}
虽然volatile
可能不需要是需要的(我问的在另一个问题中:,这个问题仍然应该在假设需要 volatile
的情况下回答,以便未来真正需要 volatile
的用户(例如我我实现了RX缓冲区),可以知道该怎么做了。
编辑(相关)(Jul/19):
基本上说不需要volatile
,因此这个问题就消失了。
Is
memcpy((void *)dest, src, n)
with avolatile
array safe?
没有。在一般情况下,memcpy()
未指定与易失性内存一起正常工作。
OP的案例看起来可以扔掉volatile
,但发布的代码不足以确定。
如果代码想要memcpy()
volatile
内存,写辅助函数。
OP 的代码 restrict
在错误的地方。建议
volatile void *memcpy_v(volatile void *restrict dest,
const volatile void *restrict src, size_t n) {
const volatile unsigned char *src_c = src;
volatile unsigned char *dest_c = dest;
while (n > 0) {
n--;
dest_c[n] = src_c[n];
}
return dest;
}
编写自己的 memcpy_v()
的一个单一原因是编译器可以 "understand"/分析 memcpy()
并发出与预期截然不同的代码 - 甚至优化它,如果编译器认为不需要 副本。提醒自己编译器认为 memcpy()
操纵的内存是非易失性的。
然而 OP 错误地使用了 volatile struct Eusart eusart;
。访问 eusart
需要 volatile
不提供的保护。
在 OP 的情况下,代码可以将 volatile
放在缓冲区 上,然后使用 memcpy()
就好了。
剩下的问题是 OP 如何使用 eusart
的代码不足。使用 volatile
并不能解决 OP 的问题。 OP 确实断言 "I write to it atomically,",但没有发布 atomic
代码,这是不确定的。
像下面这样的代码使 eusart.tx.msg_len
成为 volatile
的好处,但这还不够。 volatile
确保 .tx.msg_len
不被缓存,而是每次都重新读取。
while (eusart.tx.msg_len)
;
然而 .tx.msg_len
的读取未指定为 atomic。当 .tx.msg_len == 256
和 ISR 触发时,递减 .tx.msg_len
,读取 LSbyte(256 中的 0)和 MSbyte(255 中的 0),非 ISR 代码可能会将 .tx.msg_len
视为0,而不是 255 或 256,因此在错误的时间结束了循环。 .tx.msg_len
的访问需要指定为不可分割的(原子的),否则,偶尔代码会莫名其妙地失败。
while (eusart.tx.msg_len);
也遭受无限循环的困扰。如果传输因 某些原因 而不是空的原因停止,则 while 循环永远不会退出。
建议改为在检查或更改时阻止中断 eusart.tx.msg_len, eusart.tx.msg_posn
。查看编译器对 atomic
或
size_t tx_msg_len(void) {
// pseudo-code
interrupt_enable_state = get_state();
disable_interrupts();
size_t len = eusart.tx.msg_len;
restore_state(interrupt_enable_state);
return len;
}
一般通信代码思路:
当非 ISR 代码读取或写入
eusart
时,请确保 ISR 不能 永远 更改eusart
.不要在步骤 #1 中长时间阻塞
ISR
。不要假设基础
ISR()
将成功链接 input/output 而不会出现问题。顶层代码应该准备好在输出停止时重新启动输出。
标准没有任何方法可以让程序员可以要求在执行特定 volatile
指针访问之前完成通过普通指针访问存储区域的操作,并且也没有任何方法确保在执行某些特定的 volatile
指针访问之前,不会执行通过普通指针访问存储区域的操作。由于 volatile
操作的语义是实现定义的,标准的作者可能期望编译器编写者能够识别他们的客户何时可能需要这种语义,并以符合这些需求的方式指定他们的行为。不幸的是,这并没有发生。
实现您需要的语义要么使用 "popular extension",例如 clang 的 -fms-volatile
模式,一个特定于编译器的内部函数,要么用某些东西替换 memcpy
这是非常低效的,以至于淹没了编译器可以通过不支持这种语义获得的任何假定优势。