为什么即使使用volatile关键字,编译器也会因strncmp()而优化掉共享内存读取?

这是一个程序foo.c ,它将数据写入共享内存。

 #include  #include  #include  #include  #include  #include  #include  #include  int main() { key_t key; int shmid; char *mem; if ((key = ftok("ftok", 0)) == -1) { perror("ftok"); return 1; } if ((shmid = shmget(key, 100, 0600 | IPC_CREAT)) == -1) { perror("shmget"); return 1; } printf("key: 0x%x; shmid: %d\n", key, shmid); if ((mem = shmat(shmid, NULL, 0)) == (void *) -1) { perror("shmat"); return 1; } sprintf(mem, "hello"); sleep(10); sprintf(mem, "exit"); return 1; } 

这是另一个程序bar.c ,它从同一共享内存中读取数据。

 #include  #include  #include  #include  #include  #include  #include  #include  int main() { key_t key; int shmid; volatile char *mem; if ((key = ftok("ftok", 0)) == -1) { perror("ftok"); return 1; } if ((shmid = shmget(key, sizeof (int), 0400 | IPC_CREAT)) == -1) { perror("shmget"); return 1; } printf("key: 0x%x; shmid: %d\n", key, shmid); if ((mem = shmat(shmid, NULL, 0)) == (void *) -1) { perror("shmat"); return 1; } printf("looping ...\n"); while (strncmp((char *) mem, "exit", 4) != 0) ; printf("exiting ...\n"); return 0; } 

我首先在一个终端运行编写程序。

 touch ftok && gcc foo.c -o foo && ./foo 

当编写器程序仍在运行时,我在另一个终端中运行读取器程序。

 gcc -O1 bar.c -o bar && ./bar 

读者程序进入无限循环。 看起来优化器已经优化了以下代码

  while (strncmp((char *) mem, "exit", 4) != 0) ; 

  while (1) ; 

因为它在循环中看不到任何东西,它可以在读取一次后修改mem的数据。

但出于这个原因,我正确认为memvolatile ; 防止编译器优化它。

 volatile char *mem; 

为什么编译器仍然优化了mem的读取?

顺便说一句,我找到了一个有效的解决方案。 有效的解决方案是修改

  while (strncmp((char *) mem, "exit", 4) != 0) ; 

  while (mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't') ; 

为什么编译器优化了strncmp((char *) mem, "exit", 4) != 0但是没有优化离开mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't' mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't' mem[0] != 'e' || mem[1] != 'x' || mem[2] != 'i' || mem[3] != 't'即使char *mem在两种情况下都被声明为volatile

通过写(char *)mem你告诉strncmp函数它实际上不是一个易失性缓冲区。 事实上, strncmp和其他C库函数并不适用于易失性缓冲区。

实际上,您需要修改代码,以便在易失性缓冲区上不使用C库函数。 您的选择包括:

  • 编写自己的替代C库函数,使用volatile缓冲区。
  • 使用适当的内存屏障。

你选择了第一个选项; 但想想如果其他进程在你的四次读取之间修改了内存会发生什么。 为了避免这类问题,您需要使用第二个选项,即进程间内存屏障 – 在这种情况下,缓冲区不再需要是volatile ,您可以返回使用C库函数。 (编译器必须假设屏障检查可能会更改缓冲区)。

6.7.3类型限定符

6 […]如果尝试通过使用具有非volatile限定类型的左值来引用使用volatile限定类型定义的对象,则行为未定义。 133)

133)这适用于那些行为就好像用限定类型定义的对象,即使它们实际上从未被定义为程序中的对象(例如内存映射输入/输出地址处的对象)。

这正是您在代码中观察到的内容。 编译器基本上是在“行为未定义”的狂野自由下优化代码。

换句话说,不可能将strncmp直接正确应用于易失性数据。

您可以做的是实现自己的比较,不丢弃volatile限定符(这是您已经完成的),或使用一些易失strncmp知方法将易失性数据复制到非易失性存储中,并将strncmp应用于后者。