【问题标题】:Is `memcpy((void *)dest, src, n)` with a `volatile` array safe?带有 `volatile` 数组的 `memcpy((void *)dest, src, n)` 安全吗?
【发布时间】:2019-07-24 15:19:53
【问题描述】:

我有一个用于 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 for variable that is only read in ISR?,这个仍然应该在需要volatile 的假设下回答问题,以便真正需要volatile 的未来用户(例如我在实现RX 缓冲区时)可以知道该怎么做。


编辑(相关)(7 月 19 日):

volatile vs memory barrier for interrupts

基本上说volatile 是不需要的,因此这个问题就消失了。

【问题讨论】:

  • 您的平台是否指定volatile 使对象成为线程安全的?因为在大多数平台上,这不是真的。
  • 它是线程安全的不是因为volatile,而是因为只有一个线程,而且在我开始写之前检查中断是否被禁用,然后再启用。所以有人在同一时间胡闹的可能性为 0。
  • 你还需要volatile 做什么?
  • 因为该变量用于普通代码和中断。只是在写它的那一刻,我证明没有其他人在使用它,但在任何其他时刻,该变量在主循环和中断之间共享。
  • 我的理解是,严格来说,如果您通过非易失性指针访问具有volatile 限定符的变量,则会调用未定义的行为。因此,您对“普通”memcpy() 的使用是可疑的,即使实际上不太可能造成麻烦。

标签: c casting interrupt volatile memcpy


【解决方案1】:

memcpy((void *)dest, src, n)volatile 数组是否安全?

没有。在一般情况下,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() 的一个唯一原因是编译器可以“理解”/分析memcpy() 并发出与预期截然不同的代码——甚至优化它,如果编译器认为副本不是需要。提醒自己编译器认为memcpy() 操作的内存是非易失性的。


然而 OP 错误地使用了volatile struct Eusart eusart;。访问eusart 需要volatile 不提供的保护。

在 OP 的情况下,代码可以将volatile 放在缓冲区上,然后使用memcpy() 就好了。

剩下的问题在于 OP 如何使用eusart 的代码很少。使用 volatile 并不能解决 OP 的问题。 OP 确实断言“我以原子方式写入它”,但没有发布 atomic 代码,这是不确定的。


eusart.tx.msg_len 成为volatile 的代码如下所示,但这还不够。 volatile 确保 .tx.msg_len 不会被缓存,而是每次都重新读取。

while (eusart.tx.msg_len)
    ;

.tx.msg_len 的读取并未指定为原子。当.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;
}

一般通信代码思路:

  1. 当非 ISR 代码读取或写入 eusart 时,请确保 ISR永远无法更改 eusart

  2. 不要在第 1 步中长时间阻止 ISR

  3. 不要假设底层ISR() 将成功链接输入/输出而不会出现问题。顶级代码应准备好在停止时重新启动输出。

【讨论】:

  • 我将添加更多代码,以便您了解完整的上下文:)
  • 你为什么不把restrict 也放在src_cdest_c 中呢?那里不需要它,对吧?
  • 另外,当for 更明确时,为什么要使用while?如果保存一条指令,编译器是否无法优化索引并反转顺序?
  • unsigned 是不必要的:stackoverflow.com/a/54965630/6872717
  • @CacahueteFrito Re restrict 作为编译器不需要,通过单独分析*memcpy_v(),可以推断出restrict的继承。
【解决方案2】:

该标准缺乏任何方法,程序员可以要求通过普通指针访问存储区域的操作在执行特定的volatile 指针访问之前完成,并且也缺乏任何方法来确保操作直到执行了某些特定的volatile 指针访问之后,才执行通过普通指针访问存储区域的操作。由于volatile 操作的语义是实现定义的,标准的作者可能期望编译器编写者能够识别他们的客户何时可能需要这种语义,并以符合这些需求的方式指定他们的行为。不幸的是,这并没有发生。

实现您需要的语义要么使用“流行扩展”,例如 clang 的 -fms-volatile 模式,一种编译器特定的内在函数,要么将 memcpy 替换为效率极低的东西任何假设的优势编译器都可以通过不支持这种语义获得。

【讨论】:

  • 我不完全理解第一部分。你是说volatile 不是原子的吗?据我了解volatile,这只是意味着不允许编译器删除看似不必要的代码,对吧?
  • 另外,我从来没有在严肃的事情上使用过 clang(总是 GCC);您能否提供一个链接以获取有关该 -fms-volatile 的信息?
  • @CacahueteFrito:一些编译器,如 MSVC (Microsoft) 编译器将避免重新排序跨 volatile 操作的任何操作,据我所知,Clang 可以配置为通过使用类似的行为指示的标志。我看到的 clang 文档对于到底什么是承诺和不承诺有点含糊,唉。
  • 按照标准,volatile 保证遵循您编写它的顺序:.
  • @CacahueteFrito:标准只要求volatile-qualified 对象上的操作相对于volatile-qualified 对象上的其他操作进行排序。没有强制相对于非限定对象排序的标准方法..
猜你喜欢
  • 2018-11-27
  • 1970-01-01
  • 1970-01-01
  • 2011-01-24
  • 2013-02-15
  • 2020-03-10
  • 2017-01-10
相关资源
最近更新 更多