如何在共享内存的同一区域上工作的两个进程之间共享锁?

时间:2014-04-01 21:36:44

标签: c++ multithreading memory ipc shared-memory

我想知道如何做到这一点(使用C ++ 98)。这是我的方案:我有进程A和进程B.进程A在共享内存中分配一个大缓冲区并将其拆分为固定数量的块。然后它使用一系列这样的结构来表示每个块:

struct Chunk
{
    Lock lock; //wrapper for pthread_attr_t and pthread_mutex_t
    char* offset; //address of the beginning of this chunk in the shared memory buffer
};

构造时的锁定是这样的:

pthread_mutexattr_init(&attrs);
pthread_mutexattr_setpshared(&attrs, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(&lock,&attrs); //lock is pthread_mutex_t and attrs is pthread_mutexattr_t

调用时锁定方法执行此操作:

pthread_mutex_lock(&lock);

在上面创建上面的" Chunk" s时,它会使用placement new,如下所示:

char* mem; //pointer to the shared memory
Chunks[i] = new (mem) Chunk; //for i = {1..num chunks}
mem += sizeof(Chunk);

然后它分配偏移并继续通过它的生命周期写入缓冲区的其余部分。每次在与上述某个对应的块中写入时,它都会抓取块锁并在完成后释放它。

现在进程B出现并将相同的共享内存缓冲区映射到内存中,并尝试检索这样的块:

Chunk** chunks = reinterpret_cast<Chunk**)(mem); //mem being the pointer into the shared memory

然后它尝试通过扫描不同的块来操作共享内存,并在需要时尝试使用锁。

当我运行时,我遇到了奇怪的崩溃,其中块**是垃圾,我想知道Lock是否也可以在整个过程中工作,或者是否有任何其他警告我只是在简单的步骤中忽略以上?是否有足够的SHARED pthread attr或者我是否需要使用完全不同的锁?

2 个答案:

答案 0 :(得分:0)

当您将共享内存区域拉入进程时,它通常不会与访问共享内存的其他进程位于同一虚拟地址。因此,您不能只将原始指针存储到共享内存中,并希望它们能够有效地工作。

因此,在您的情况下,即使Chunk位于共享内存中,每个块中的offset指针在任何其他进程中都没有意义。

一种解决方案是使用共享内存块开头的偏移量

struct Chunk {
    pthread_mutex_t  lock;
    size_t           offset;
};

char *base; // base address of shared memory
char *mem;  // end of in-use shared memory

Chunk *chunks = reinterpret_cast<Chunk *>(mem);  // pointer to array in shared memory
for (int i = 0; i < num_chunks; i++) {
    // initialize the chunks
    new(mem) Chunk;
    mem += sizeof(Chunk); }
// set the offsets to point at some memory
for (int i = 0; i < num_chunks; i++) {
    chunks[i].offset = mem - base;
    mem += CHUNK_SIZE; // how much memory to allocate for each chunk?
}

现在在B中你可以做到

Chunk *chunks = reinterpret_cast<Chunk *>(base);

但在任一过程中,要访问块的数据,您需要base + chunks[i].offset

或者,您可以使用package that manages the shared memory allocation for you and ensures that it gets mapped at the same address in every process

答案 1 :(得分:0)

除了我的评论,我还提供了一个非常基本的offset_ptr实现。我同意将一个简单的项目依赖于像boost这样的东西可能是一种矫枉过正,但即使达到某个项目大小,你决定切换到一个更严肃的库集合,也值得包装像偏移指针这样的关键事物。环绕可以帮助您集中偏移处理程序代码,也可以使用断言来保护自己。一个简单的offset_ptr模板不能处理所有情况仍然比手工编码的偏移量+指针操纵器代码要好得多,这些代码可以在任何地方进行复制粘贴,而且它的基本实现不会尝试全面:

template <typename T, typename OffsetInt=ptrdiff_t>
class offset_ptr
{
    template <typename U, typename OI> friend class offset_ptr;
public:
    offset_ptr() : m_Offset(0) {}
    offset_ptr(T* p)
    {
        set_ptr(p);
    }
    offset_ptr(offset_ptr& other)
    {
        set_ptr(other.get_ptr());
    }
    template <typename U, typename OI>
    offset_ptr(offset_ptr<U,OI>& other)
    {
        set_ptr(static_cast<T*>(other.get_ptr()));
    }
    offset_ptr& operator=(T* p)
    {
        set_ptr(p);
        return *this;
    }
    offset_ptr& operator=(offset_ptr& other)
    {
        set_ptr(other.get_ptr());
        return *this;
    }
    template <typename U, typename OI>
    offset_ptr& operator=(offset_ptr<U,OI>& other)
    {
        set_ptr(static_cast<T*>(other.get_ptr()));
        return *this;
    }
    T* operator->()
    {
        assert(m_Offset);
        return get_ptr();
    }
    const T* operator->() const
    {
        assert(m_Offset);
        return get_ptr();
    }
    T& operator*()
    {
        assert(m_Offset);
        return *get_ptr();
    }
    const T& operator*() const
    {
        assert(m_Offset);
        return *get_ptr();
    }
    operator T* ()
    {
        return get_ptr();
    }
    operator const T* () const
    {
        return get_ptr();
    }

private:
    void set_ptr(const T* p)
    {
        m_Offset = p ? OffsetInt((char*)p - (char*)this) : OffsetInt(0);
    }
    T* get_ptr() const
    {
        return m_Offset ? (T*)((char*)this + m_Offset) : (T*)nullptr;
    }

private:
    OffsetInt m_Offset;
};

offset_ptr<int> p;
int x = 5;

struct TestStruct
{
    int member;
    void func()
    {
        printf("%s(%d)\n", __FUNCTION__, member);
    }
};

TestStruct ts;
offset_ptr<TestStruct> pts;

int main()
{
    p = &x;
    *p = 6;
    printf("%d\n", x);

    ts.member = 11;
    if (!pts)
        printf("pts is null\n");
    pts = &ts;
    if (pts)
        pts->func();
    pts = nullptr;
    if (!pts)
        printf("pts is null again\n");

    // this will cause an assert because pts is null
    pts->func();
    return 0;
}

它可能包含一些操作符函数,如果你不习惯实现指针的话,这些函数会很难写,但是与一个像boost这样的复杂lib的完全成熟的指针实现相比,这真的很简单,不是吗?它使指针(偏移)操纵器代码更好!没有理由不至少使用这样的包装!

包装锁和其他东西也很有成效,即使你在没有外部库的情况下自己动手也是如此,因为在使用裸本机api的锁写入/使用2-3-4次后,它会使你的代码看起来更好如果您使用带有ctor / destructor / lock()/ unlock()的简单包装器,更不用说可以使用断言保护的集中锁定/解锁代码,有时您可以将调试信息输入到锁类(如id用于调试死锁的最后一个更衣室线程更容易......)。

因此,即使您在没有std或boost的情况下滚动代码,也不要省略包装。