什么是extern volatile指针?

14

什么是extern volatile指针。

extern volatile uint32 *ptr;

在这里,*ptr的行为将是什么? 这实际上是什么意思?

那么,它应该在什么时候使用?

我已经尝试通过谷歌搜索了解它,但没有得到令人满意的答案,在这种组合中没有太多信息。


你没有认真查看,可以参考https://dev59.com/aWkw5IYBdhLWcg3wY5jq。 - Mikhail
Extern和volatile没有关联。你有一个volatile指针,它恰好在其他地方被定义。extern限定符不会改变volatile指针的语义。 - Mat
我在我正在工作的项目中看到了extern volatile *ptr的使用,但由于高度的数据安全原因,我无法提供代码以供参考。此外,由于产品非常广泛,所以我无法理解它的用途。 - Arti
volatile obj 告诉编译器在程序/作用域中读取 obj 的值时,需要从其内存位置读取。通常,编译器会将小变量(int、指针)读入 CPU 寄存器,并在下一次顺序引用该值时使用该寄存器(为了提高效率)。使用 volatile 可以使编译器停止这种行为。 - Déjà vu
有人能回答一下,考虑到“extern volatile”吗? 我不是在寻找单独的定义。 比如,如果我省略volatile,系统的行为会是什么样子,使用'extern volatile'时的行为又是什么样子。 任何实现示例都将是有帮助的。 - Arti
4个回答

20

无论是extern还是volatile关键字都可以单独考虑。每个关键字的作用不会与另一个关键字交互,因此每个关键字的解释都可以独立详细说明,如下所示。

extern告诉编译器ptr的实际定义在另一个模块(另一个.c)中。 基本上,编译器处理ptr的方式没有太多变化 - 使用extern只是告诉编译器它不必为ptr在内存中保留一些空间,因为它在另一个.c中完成,并且其实际内存位置稍后将由链接器给出。

  extern uint32 *ptr;
如果省略 extern,编译器将不会发出警告。 然而,稍后,链接器在尝试链接所有目标模块以构建最终可执行程序时,将抛出一个错误,说“ptr 被定义了两次”(因为它已经在另一个 .c 中被定义了)。
  uint32 *ptr;
volatile 告诉编译器,ptr 所在的内存位置可能会被一些外部事件改变或修改,因此(编译器)不应该依赖某些效率优化,比如考虑在几个顺序的 C 语句的作用域内 ptr 的值不会改变。这样的事件可能是异步中断,在执行上述作用域时发生,并修改了 ptr 的值。

通常情况下(在优化后的 C 虚拟汇编代码的注释中),REGx 是 CPU 寄存器,我们对变量 y 不感兴趣...

  int x = 10;

  int func() {
    int y;              // REG4
    printf("%d\n", x);  // Set REG3 = memory(x) and display x
    x += 2;             // Add 2 to REG3
    y = x * x;          // REG4 = REG3 * REG3
    printf("%d %d\n", x, y); // Do printf(..., REG3, REG4)
    x += 5;             // REG3 = REG3 + 5
                        // memory(x) = REG3 (save register to memory)
    return y;           // return REG4
  }

应该显示10、12、144。为了提高效率(内存访问比寄存器访问更昂贵),编译器将x的值存储在内部CPU寄存器(REG3)中,并在func中安全使用它,直到最后将x的新值(它是全局变量)存储到x的内存位置。最后x为17。

但想象一下程序比这更复杂,并且每分钟都有一个时钟中断会将x减去10。如果在中断发生时...

  void inter_call_by_timer_every_minute() {
     x -= 10;
  }

func中的printf("%d\n", x);语句后面发生了什么?funcx存储在REG3(10)中,然后加2(12),最后加5(17)并将REG3的结果存储到x内存位置(17)。这是错误的,因为编译器优化隐藏了中断效应(-10),由于它在最后忽略了来自REG3到内存(x)的值的减法。正确的结果是:x最初为10,在func的第一个printf之后,中断将其减去10(0),然后添加2,再添加5。结果为7。

添加volatile

  volatile int x = 10;

使用该选项将使编译器避免在 func 中对 x 进行优化。

  int func() {
    int y;              // REG4
    printf("%d\n", x);  // display memory(x)
    x += 2;             // memory(x) += 2
    y = x * x;          // REG4 = memory(x) * memory(x)
    printf("%d %d\n", x, y); // Do printf(..., memory(x), REG4)
    x += 5;             // memory(x) += 5
    return y;           // return REG4
  }

每次都从内存中读取x的值。结果,在第一个printf之后被来自inter_call_by_timer_every_minute的中断打断,此时x == 7.


2
需要注意的是,在C标准中没有保证像+=这样的运算符是原子性的,而x * x肯定不是原子性的。因此,尽管volatile确实告诉编译器不要缓存读取,但它对原子性不会有任何影响。此外,在这种情况下,添加volatile可能会导致y最终成为任何整数的平方,如果在x * x的两次读取之间发生了x -= 10。这甚至可能比跳过单个计时器减少还要糟糕。 - vgru

2

extern关键字用于声明一个全局变量,该变量在其他地方定义(也就是说,在某个其他的.c文件中定义)。

例如,在一个项目中有两个.c文件a.cb.c。在a.c文件中定义了一个全局变量,并且可以在该文件中定义的所有函数中访问该变量。如果我们想要在第二个文件b.c中访问相同的全局变量,则应在b.c中将该变量声明为extern

下面是a.c文件的内容:

int flag = 0;
int main()
{
    .......
    func1();
    printf("\nflag value is %d\n", flag).
    ....... 
}

以下是.c文件的内容:
extern int flag;
void func1();
{
    .....
    flag = 10;
    .....
}

volatile 关键字用于告诉编译器在生成可执行指令时避免进行任何优化。

该关键字通常用于多线程程序和嵌入式系统中,以确保变量的准确性和一致性。
int flag = 0;
int main()
{
    while(flag == 0);
    printf("\nflag value is %d\n", flag);
    return 0;

}

考虑上面的程序,所有编译器都会将 while(flag == 0); 优化为 while(1);。因为在代码中,在该 while 循环之前没有任何地方更新了 flag 的值。所以,如果该变量的值由其他硬件更新,则它永远不会反映在程序执行中。因此,如果我们像下面这样将该变量声明为 volatile,编译器将不会对该变量进行任何优化,并且该程序的行为将如预期一样。
volatile int flag = 0;

但如果程序变量的值没有被其他硬件更新的方式,那么不需要将该变量声明为volatile。因为对于易失性变量,CPU需要执行每个访问该变量的指令的I/O操作。这种影响性能的情况需要考虑一个永远不会被其他硬件更新的变量。


2
我将通过关键词来解释。 extern 表示变量定义在定义范围之外的某处使用。例如,它可以在头文件中定义,并在 .c 文件中使用。 volatile 表示修改该变量应与外部世界保持一致。这意味着所有更新都应提交到主内存中,以便其他共享相同执行空间的线程可以看到。

0

Extern表示它在其他地方已被定义 - 可能是在头文件中。 Volatile是向编译器提供信息,表明它不应尝试优化此内容。


2
通常情况下不会在头文件中定义变量,这是不常见的做法,而且大多数时候也是不好的实践。你可能会在头文件中找到extern关键字,而实际的定义则在相应的C文件中。 - Lundin

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