繁体   English   中英

在 C 代码的后面部分中丢弃 volatile 限定符

[英]Discarding the volatile qualifier in a later part of the C code

我有以下代码构造

int foo( volatile int *a)
{
     if(*a != VALID)
     {
          // suspend for few seconds 
          suspend();
          // check  again
          if(*a != VALID)
          {
               //error. data was unavailable 
               return -1; 
          }
     }
     // cast away the volatile. Does not accept volatile
     call_external_interface((int*)a) ; 

     return 0;
}

这里由“a”指向的 memory 位置是从外部源(通过 DMA 传输)填充的。

一旦可用,“a”指向的缓冲区就会通过不接受“volatile”的外部接口调用进行处理,因此会丢弃“volatile”。

假设只有在缓冲区被外部资源填充之前才需要“易失性”,因此无论数据是否可用,都会检查 memory 位置两次(并且指令不会被丢弃,因为它被声明为易失性)。

假设也是一旦缓冲区可用,它只是在内部处理,因此可以丢弃 volatile。

我可以确定只有在执行之前编码的指令之后才调用“call_external_interface”吗? 我需要一个明确的 memory 屏障吗?

处理器可以乱序执行。

关于如何使此代码安全的任何其他评论?

C 标准邀请实现提供与客户最佳服务一样强或弱的volatile语义(它明确将此类语义视为“实现定义的”)。 MSVC 历来使用的语义足够强大,可用于广泛的用途(后来被 Java 或 C# 等语言的设计者认为是有用的),并且仍然可以选择使用该行为。 在 MSVC 语义下,预计此代码的行为会很有用。

不幸的是,该标准没有提供程序员可以指示涉及非限定左值的操作不得在对volatile限定左值的访问中重新排序的方法,并且一些编译器将更高的优先级放在代码的效率上,这可以通过弱语义(在易失性访问之间自由地重新排序volatile访问),而不是最大化与为各种任务编写的代码的兼容性(将volatile访问视为对不透明函数的调用,或将读取和写入视为具有获取/释放语义)。

C 标准中的任何内容都不会禁止实现在foo执行的*a易失性读取之前重新排序由call_external_interface执行的volatile性限定的*a读取。 如果foo在循环中被调用,那么“聪明”的编译器可能会提升所有在call_external_interface中发生的对*a的访问,以便它们只发生一次,而不是在循环的每次迭代中重复。 使代码可靠工作的唯一方法是在对*a的 volatile 访问和任何非限定访问之间调用宏COMPILER_ORDERING_BARRIER ,并要求构建程序的人以适合所用编译器的方式定义宏。 请注意,在这种情况下,无需插入硬件 memory 屏障,并且一些编译器(如 MSVC)能够有效地处理代码,而无需任何编译器特定的语法,但其他编译器(如 gcc 或 Z2C55173DB7BC39754FFA4)据我所知,除了-O0之外没有其他优化选项会导致它们使用与 MSVC 兼容的语义处理代码。

目前还不太清楚,您的volatile int *a实际上也指向什么(DMA TCD 或 memory,其中数据也由 TCD.DESTADDR 中定义的 DMA 传输),以及谁在调用 foo()。

如果有 DMA 传输,您应该对 DMA 事件(例如主循环的 ISR 完成)做出反应以处理数据,而不是 DMA 传输的内容。 查看未完成的 DMA 传输的内容没有多大意义。

假设只有在缓冲区被外部资源填充之前才需要“易失性”,因此无论数据是否可用,都会检查 memory 位置两次(并且指令不会被丢弃,因为它被声明为易失性)。

假设也是一旦缓冲区可用,它只是在内部处理,因此可以丢弃 volatile。

C 语言规范定义的语义不支持这些假设。 它们可能适用于任何特定实现,也可能不适用,但如果您打算依赖 C 实现的详细信息,那么您需要找到一种方法来确定该实现提供的保证,超出语言规范,您可以依赖之上。 不受支持的假设会给人们带来麻烦。

我可以确定只有在执行之前编码的指令之后才调用“call_external_interface”吗?

一般来说,只要可观察的行为(包括 volatile 访问的模式)不变,C 规范中的任何内容都不会阻止其他操作围绕 volatile 访问重新排序或以其他方式优化。 特别是,如果suspend() function 对执行环境没有副作用,并且编译器有办法知道这一点,那么对它的调用可能会被省略。

假设编译器不能证明对call_external_interface()的调用对环境没有副作用,但是, *a的第一次读取肯定会首先执行。 如果该读取的结果表明如此,则也将执行第二次读取。 如果执行第二次读取并且其结果在if语句中表达的任何条件下测试无效,则 function 将在不调用call_external_interface()的情况下返回 -1。 为了表现出您的代码描述的外部可观察行为所需的一切。

我需要一个明确的 memory 屏障吗?

我看不出你认为会得到什么。

关于如何使此代码安全的任何其他评论?

这取决于您所说的“安全”是什么意思,并且从这个角度来看,取决于suspend()call_external_interface()函数的作用。 但是,强制类型转换通常具有轻微的不良代码气味,并且丢弃指针目标的类型限定符非常臭。 如果call_external_interface()不接受(指向)易失性数据并且无法修改为这样做,那么最安全的解决方案是foo()读取易失性数据并将其缓冲在非易失性存储中以供call_external_interface()使用.

或者,可能是 C 对于您想要表达的细节来说太高级了。 您可以考虑在程序集中重写foo()以更好地控制访问 memory 的方式和时间。

在许多情况下,这是一个非常糟糕的主意,我们应该避免它。 例子

void foo(volatile int *p)
{
    while(*p != 10);
}

void bar(int *p)
{
    while(*p != 10);
}

int goo(volatile int *p)
{
    foo(p);
}

int goo1(volatile int *p)
{
    bar((int *)p);
}

foo:
.L2:
        ldr     r3, [r0]
        cmp     r3, #10
        bne     .L2
        bx      lr
bar:
        ldr     r3, [r0]
        cmp     r3, #10
        bxeq    lr
.L8:
        b       .L8
goo:
.L10:
        ldr     r3, [r0]
        cmp     r3, #10
        bne     .L10
        bx      lr
goo1:
        ldr     r3, [r0]
        cmp     r3, #10
        bxeq    lr
.L15:
        b       .L15

在上面没有 volatile 的示例中,程序以死循环结束。

或者

void delay(unsigned delay)
{
    while(delay--);
}

int foo(int *a)
{
    int x = *a;
    delay(1000);
    return x + *a;
}

int bar(volatile int *a)
{
    int x = *a;
    delay(1000);
    return x + *a;
}

int goo(volatile int *a)
{
    return foo((int *)a);
}

int goo1(volatile int *a)
{
    return bar(a);
}

delay:
        cmp     r0, #0
        sub     r0, r0, #1
        bxeq    lr
.L3:
        subs    r0, r0, #1
        bxcc    lr
        subs    r0, r0, #1
        bxcc    lr
        b       .L3
foo:
        mov     r3, #1000
        ldr     r0, [r0]
.L10:
        subs    r3, r3, #1
        bne     .L10
        lsl     r0, r0, #1
        bx      lr
bar:
        mov     r3, #1000
        ldr     r2, [r0]
.L13:
        subs    r3, r3, #1
        bne     .L13
        ldr     r0, [r0]
        add     r0, r0, r2
        bx      lr
goo:
        mov     r3, #1000
        ldr     r0, [r0]
.L16:
        subs    r3, r3, #1
        bne     .L16
        lsl     r0, r0, #1
        bx      lr
goo1:
        mov     r3, #1000
        ldr     r2, [r0]
.L19:
        subs    r3, r3, #1
        bne     .L19
        ldr     r0, [r0]
        add     r0, r2, r0
        bx      lr

在这个没有 volatile 的示例中,object 只被读取一次 - 但它可以由 DMA 在后台更改。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM