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

11

我有一个用于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的用户(例如我在实现RX缓冲区时)知道该怎么做,仍然应该回答这个问题。


编辑(相关)(Jul/19):

volatile vs memory barrier for interrupts

基本上说,volatile不是必需的,因此这个问题就不存在了。


1
你的平台是否指定volatile可以使对象线程安全?因为在大多数平台上,这并不是真的。 - David Schwartz
它是线程安全的,不是因为volatile,而是因为只有一个线程,并且在我开始写之前检查中断是否被禁用,在此之后启用。因此,没有人同时搞砸的可能性。 - alx - recommends codidact
2
你甚至需要什么volatile - curiousguy
1
因为该变量在正常代码和中断中都被使用。只是在写入变量时,我确保没有其他人在使用它,但在任何其他时刻,该变量都在主循环和中断之间共享。 - alx - recommends codidact
1
我的理解是,严格来说,如果您通过非volatile指针访问具有volatile限定符的变量,则会调用未定义的行为。因此,即使不太可能实际上会引起问题,您对“普通”的memcpy()的使用也是可疑的。 - Jonathan Leffler
显示剩余5条评论
3个回答

7

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

不安全。通常情况下,memcpy()未被指定为能够正确处理volatile内存。
尽管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 断言“我以原子方式写入它”,但没有发布原子代码,这并不确定。
像下面这样的代码受益于 eusart.tx.msg_lenvolatile,但这还不足够。 volatile 保证每次都重新读取 .tx.msg_len 而非来自缓存。
while (eusart.tx.msg_len)
    ;
volatile

然而,对于.tx.msg_len的读取并没有被规定为原子操作。当.tx.msg_len == 256并且ISR触发时,减少.tx.msg_len,非ISR代码可能会将LSbyte(来自256的0)和MSbyte(来自255的0)看作0而不是255或256,因此在错误的时间结束循环。访问.tx.msg_len需要被指定为不可分割的(原子的),否则偶尔代码会神秘地失败。

while (eusart.tx.msg_len);也遭受无止境的循环问题。如果传输由于除了空闲之外的“某些原因”停止,while循环永远不会退出。

建议在检查或更改eusart.tx.msg_len, eusart.tx.msg_posn时阻塞中断。请查阅编译器对atomicvolatile的支持。

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() 可以成功地链式输入/输出而不会出现问题。顶级代码应该准备重新启动输出,如果它被阻塞。


另外,为什么要使用 while,而不是更明确的 for?如果它能节省一条指令,编译器不会优化索引并反转顺序吗? - alx - recommends codidact
unsigned 是不必要的:https://stackoverflow.com/a/54965630/6872717 - alx - recommends codidact
1
@CacahueteFrito 在 *memcpy_v() 函数中,编译器可以通过分析函数本身来推断 restrict 的继承关系,因此不需要使用 restrict - chux - Reinstate Monica
为什么使用while:只是完成相同任务的另一种方式。 - chux - Reinstate Monica
@CacahueteFrito 当然。 - chux - Reinstate Monica
显示剩余12条评论

2
C17标准第6.7.3节“类型限定符”中指出:

如果使用非volatile限定的lvalue引用已定义为volatile限定类型的对象,则其行为是未定义的。135)

135)即使这些对象在程序中从未被定义为对象(例如内存映射输入/输出地址处的对象),也适用于那些表现得像已定义为限定类型的对象。

因此,不安全。


2
标准缺乏任何方式,让程序员在使用普通指针访问存储区域的操作完成之前要求完成特定的volatile指针访问,并且也缺乏确保使用普通指针访问存储区域的操作直到执行某个特定的volatile指针访问后才能进行的方法。由于volatile操作的语义是实现定义的,标准的作者可能希望编译器编写者能够意识到客户需要这种语义的时候,并以与这些需求一致的方式指定它们的行为。不幸的是,这种情况并没有发生。
实现所需的语义将使用“流行的扩展”,例如clang的-fms-volatile模式,特定于编译器的内部函数或用某些非常低效的东西替换memcpy以使编译器无法支持这种语义而被淹没。

@CacahueteFrito:一些编译器,如MSVC(Microsoft)编译器,将避免在volatile操作之间重新排序任何操作,从我所知,通过使用指定的标志,Clang也可以被配置为行为类似。不过,我看到的clang文档对于承诺什么和不承诺什么有点模糊。 - supercat
按照标准,volatile 保证按照你编写的顺序执行:<C11: 5.1.2.3 程序执行>。 - alx - recommends codidact
1
@CacahueteFrito:标准只要求对“volatile”限定对象的操作相对于对其他“volatile”限定对象的操作进行排序。没有强制规定相对于非限定对象进行排序的标准手段。 - supercat
@CacahueteFrito:问题在于,如果像这样做memcpy(buffer, src1, 40); outptr = buffer; outlen = 40; while(outlen) ; memcpy(buffer, src2, 25); outptr = buffer; outlen = 25; while(outlen) ;[使用outptroutlen限定为volatile],没有办法强制编译器实际写入所有40个字节的src1,而不仅仅是不会被src2内容覆盖的部分。在某些平台上,这可能无关紧要,但在其他平台上,完整的复制对于正确性可能非常重要。 - supercat
让我们在聊天中继续这个讨论 - alx - recommends codidact
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接