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

时间:2019-03-02 23:38:01

标签: c casting interrupt 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 for variable that is only read in ISR?)< s>,仍然需要假设volatile来回答这个问题,以便将来真正需要volatile的用户(例如,当我实现RX缓冲区时我)知道该怎么做。

2 个答案:

答案 0 :(得分:3)

  

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_lenvolatile的好处,但这还不够。 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;
}

一般通信代码构想:

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

  2. 在步骤#1中不要长时间阻塞ISR

  3. 不要假设基础ISR()将成功地将输入/输出链接而不会打h。如果输出停顿,应该准备顶层代码以重新启动输出。

答案 1 :(得分:0)

该标准缺少程序员可以要求在执行特定的volatile指针访问之前完成通过普通指针访问存储区域的操作的任何方法,并且缺乏确保以下内容的任何方法:直到执行了某些特定的volatile指针访问之后,才会执行通过普通指针访问存储区域的操作。由于volatile操作的语义是实现定义的,因此该标准的作者可能希望编译器作者会识别其客户何时需要此类语义,并以与那些需求一致的方式指定其行为。不幸的是,这没有发生。

实现所需的语义将要么使用“流行扩展名”,例如clang的-fms-volatile模式,特定于编译器的内在函数,要么将memcpy替换为如此可怕的东西在不支持这种语义的情况下,编译器可能无法获得优势。