从子进程中的共享内存中读取指向共享内存的指针时出现分段错误

时间:2014-08-20 23:54:51

标签: c fork shared-memory mmap

概览

我有一个程序,需要在多个进程之间拥有共享状态(可能是80,我正在处理80个内核的服务器上一个令人尴尬的并行问题)。理想情况下,我能够以这样的方式分配这个共享内存,以便扩展这些进程之间的共享状态量。

我怀疑它失败了因为指针没有指向实际内存,所以如果一个进程中mmap的返回值是0xDEADBEEF,那并不意味着0xDEADBEEF将指向另一个进程中的相同内存部分。但是,我对C编程几乎一无所知,因此怀疑可能很容易出错。

有人能告诉我我的怀疑是否正确吗?如果是这样,我应该为共享状态做些什么呢?服务器每个数据集至少需要18天而不使用所有核心,并且我们有相当多的数据集,所以放弃并行计算并不是一个真正的选择。但是,我愿意从进程切换到线程或类似的东西,如果这会有所帮助(我不知道如何在C中做到这一点)。在此先感谢您的帮助。

下面是一些工作和非工作的示例代码,以及gdb的结果。

borked.h

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>

// Expandable array, doubles memory allocation when it runs out of space.
struct earr {
    int *vals;
    int capacity;
    int length;
};

void *shared_calloc(size_t nmemb, size_t size) {
    void *mem = mmap(NULL, nmemb * size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    return memset(mem, 0, nmemb * size);
}

void shared_free_size(void *mem, size_t nmemb, size_t size) {
    if(mem) munmap(mem, nmemb * size);
}

struct earr *create_earr() {
    struct earr *a = shared_calloc(1, sizeof(struct earr));
    a->length = 0;
    a->capacity = 16;
    a->vals = shared_calloc(a->capacity, sizeof(int));
    return a;
}

void earr_expand(struct earr *a) {
    int *new_vals = shared_calloc(a->capacity * 2, sizeof(int));
    memcpy(new_vals, a->vals, a->capacity * sizeof(int));
    a->vals = new_vals;
    a->capacity *= 2;
}

void earr_insert(struct earr *a, int val) {
    if(a->length >= a->capacity) earr_expand(a);
    a->vals[a->length] = val;
    a->length++;
}

int earr_lookup(struct earr *a, int index) {
    return a->vals[index];
}

working.c

#include "borked.h"

int main(void) {
    struct earr *a = create_earr();
    int i;
    pid_t pid;
    int size = 0x10000;
    for(i = 0; i < size; i++) {
        earr_insert(a, i);
    }
    for(i = 0; i < size; i++) {
        earr_lookup(a, i);
    }
    return EXIT_SUCCESS;
}

broken.c

#include "borked.h"

int main(void) {
    struct earr *a = create_earr();
    int i;
    pid_t pid;
    int size = 0x10000;
    if(0 == (pid = fork())) {
        for(i = 0; i < size; i++) {
            earr_insert(a, i);
        }
    } else {
        int status;
        waitpid(pid, &status, 0);
        for(i = 0; i < size; i++) {
            earr_lookup(a, i);
        }
    }
    return EXIT_SUCCESS;
}

GDB调试

$ gdb broken
...
(gdb) run
Starting program /path/to/broken

Program received signal SIGSEGV, Segmentation Fault
0x08048663 in earr_lookup (a=0xb7fda000, index=0) at /path/to/borked.h:46
46      return a->vals[index];
(gdb) x/3x 0xb7fda000
0xb7fda000: 0xb7da6000  0x00010000  0x00010000
(gdb) x/x 0xb7da6000
0xb7da6000: Cannot access memory at address 0xb7da6000

2 个答案:

答案 0 :(得分:3)

你的方法确实被打破了。当其中一个子进程最终调用earr_expand以增加数组的大小并最终调用mmap时,生成的新映射仅存在于该子进程中 - 它不会传播到父进程或其他子进程。

您需要使用其他技术,例如在fork()之前预先分配全部内存,或者使用POSIX共享内存。

答案 1 :(得分:0)

是的,共享内存的地址因处理过程而异。

解决方案通常是使用索引而不是指针。在OP的示例代码中,可以使用具有C99灵活数组成员的结构来描述共享内存映射,类似于

struct shared_mmap {
    unsigned int  size;
    unsigned int  used;
    int           data[];
};

当然,data元素类型本身可以是一种结构。

但是,使用mremap()增加匿名内存映射将为您提供进程本地新页面;新页面不会在进程之间共享。您需要使用文件备份(而不是匿名共享内存),或者更好,POSIX shared memory

(如果你有足够的RAM可用,文件支持不一定要慢,因为文件页面通常位于页面缓存中。文件支持具有允许计算停止并继续使用相同或更高的有趣属性。甚至不同数量的进程,显然取决于共享内存映射结构。如果使用MAP_SHARED | MAP_NORESERVE,则可以使用比可用RAM + swap大得多的内存映射 - 但是如果磁盘空间或配额用完,您的过程将收到(并死于)SIGBUS信号。)

虽然在x86和x86-64上int加载和存储是原子的,但您最有可能最终需要(pthread) mutexes和/或atomic built-ins(由GCC-4.7及更高版本提供) ;较旧的GCC和ICC等人提供遗留sync built-ins

最后,如果你有一组无序的int s,我会考虑在内存映射中使用固定大小(536,870,912字节,或2 32 位,在Linux中)和原子位运算符:

#include <limits.h>

#define UINT_BITS (CHAR_BIT * sizeof (unsigned int))
#define ULONG_BITS (CHAR_BIT * sizeof (unsigned long))

/* Pointer to shared memory of (1<<UINT_BITS)/CHAR_BIT chars */
static unsigned long *shared;

static inline unsigned int shared_get(unsigned int i)
{
    return !!__atomic_load_n(shared + (i / ULONG_BITS), 1UL << (i % ULONG_BITS), __ATOMIC_SEQ_CST);
}

static inline void shared_set(unsigned int i)
{
    __atomic_fetch_or(shared + (i / ULONG_BITS), 1UL << (i % ULONG_BITS), __ATOMIC_SEQ_CST);
}

static inline void shared_clear(unsigned int i)
{
    __atomic_fetch_nand(shared + (i / ULONG_BITS), 1UL << (i % ULONG_BITS), __ATOMIC_SEQ_CST);
}

在任何情况下,内存映射长度必须是sysconf(_SC_PAGESIZE)的倍数,您应该在运行时对其进行评估。

(在所有已知架构中它都是2的幂,所以当前2个 21 和更大的合理大功率是所有当前支持的Linux架构上页面大小的倍数。)< / p>

有问题吗?