3

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

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

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 <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

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'char *memvolatile

4

2 回答 2

6

6.7.3 类型限定符

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

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

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

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

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

于 2016-12-09T01:44:39.730 回答
4

通过编写(char *)mem,您告诉strncmp函数它实际上不是易失性缓冲区。事实上,strncmp其他 C 库函数并非设计用于在 volatile 缓冲区上工作。

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

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

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

于 2016-12-09T02:01:05.417 回答