为什么即使使用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
的数据。
但出于这个原因,我正确认为mem
是volatile
; 防止编译器优化它。
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
应用于后者。