通过Mmap共享内存传递可变长度C字符串

时间:2018-07-05 15:38:09

标签: c++ linux g++ shared-memory mmap

假设我有一个进程A和一个进程B,并且进程A希望通过shm_open()+ mmap()共享内存将C字符串传递给进程B。

最有效的延迟方式是什么?

post的答案表明,在C ++ 11之后,std :: atomic是在共享内存上共享数据的正确方法。

但是,我看不到如何写这样的东西来编写C字符串:

struct Buffer {
std::atomic<uint32_t> length;
std::atomic<char*> str;
} __attribute__((packed));

鉴于我以这种方式创建了共享内存:

class SHM {
    char* _ptr;
public:
    SHM() {
        const auto handle = shm_open("myTest", O_RDWR|O_CREAT, 0666);
        const auto size =  4 * 1024 * 1024;
        if (-1 == ftruncate(handle, size)) {
            throw;
        }
        _ptr = (char*)mmap(0,size , PROT_READ | PROT_WRITE, MAP_SHARED, handle, 0);

        if(_ptr == MAP_FAILED){
            throw;
        }

        int rc = fchmod(handle, 0666);
        if (rc == -1) {
            throw;
        }
    }

    // assume to caller will do buffer.size.store(someLength, std::memory_order_release); after filling up Buffer::str
    Buffer& getBuffer() noexcept {
        return *reinrepret_cast<Buffer*>(_ptr);
    }

    Buffer& read() {
        auto& buffer = *reinrepret_cast<Buffer*>(_ptr);
        while (buffer.size.load(std::memory_order_acquire) > 0) {
            buffer.str.load(std::memory_order_relaxed);
            return buffer;
        }
    }
};

SHM::getBuffer()的调用者如何通过char正确写入Buffer :: str char,以便进程B可以调用SHM::read()进行检索?

buffer.str.load(std :: memory_order_relaxed)是否真正自动地正确加载?我对此表示怀疑,因为它甚至都不知道长度。

这适用于Linux,X86-64,GCC 7。

谢谢。

1 个答案:

答案 0 :(得分:1)

这是单生产者-单消费者情况的工作草图(生产者/消费者线程是否来自同一进程无关紧要),无需等待:

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <utility>
#include <cstring>
#include <string>
#include <atomic>

class SingleProducerSingleConsumerIndexes {
    std::atomic<uint64_t> produced_ = {};
    std::atomic<uint64_t> consumed_ = {};

public: // Producer interface.
    uint64_t produced() {
        auto consumed = consumed_.load(std::memory_order_acquire); // Syncronizes with store 2.
        auto produced = produced_.load(std::memory_order_relaxed);
        if(produced != consumed || !produced)
            return produced;
        // Entire buffer was consumed. Rewind.
        produced_.store(0, std::memory_order_release); // Store 1.
        consumed_.store(0, std::memory_order_relaxed); // Store 3.
        return 0;
    }

    void produce(uint64_t end) {
        produced_.store(end, std::memory_order_release); // Store 1.
    }

public: // Consumer interface.
    std::pair<uint64_t, uint64_t> available() const {
        auto produced = produced_.load(std::memory_order_acquire); // Syncronizes with store 1.
        auto consumed = consumed_.load(std::memory_order_relaxed);
        // min handles the case of store 3 not visible yet.
        return {std::min(produced, consumed), produced};
    }

    void consume(uint64_t end) {
        consumed_.store(end, std::memory_order_release); // Store 2.
    }
};

class SharedMemoryStrings {
    void* p_;
    static constexpr int size = 4 * 1024 * 1024;
    static constexpr int buffer_size = size - sizeof(SingleProducerSingleConsumerIndexes);
public:
    SharedMemoryStrings() {
        auto handle = ::shm_open("/another-test", O_RDWR|O_CREAT, 0666);
        if(-1 == ::ftruncate(handle, size))
            throw;
        p_ = ::mmap(0,size , PROT_READ | PROT_WRITE, MAP_SHARED, handle, 0);
        ::close(handle);
        if(p_ == MAP_FAILED)
            throw;
    }

    ~SharedMemoryStrings() {
        ::munmap(p_, size);
    }

    void produce(std::string const& s) {
        auto* indexes = static_cast<SingleProducerSingleConsumerIndexes*>(p_);
        auto produced = indexes->produced();
        uint64_t new_end = produced + sizeof(uint64_t) + s.size();
        if(new_end > buffer_size)
            throw; // Out of buffer space.

        auto* buffer = reinterpret_cast<char*>(indexes + 1) + produced;
        uint64_t size = s.size();
        memcpy(buffer, &size, sizeof size);
        buffer += sizeof size;
        memcpy(buffer, s.data(), s.size());

        indexes->produce(new_end);
    }

    bool try_consume(std::string& s) {
        auto* indexes = static_cast<SingleProducerSingleConsumerIndexes*>(p_);
        auto available = indexes->available();
        auto consumed = available.first;
        auto produced = available.second;
        if(consumed == produced)
            return false; // No data available.

        auto* buffer = reinterpret_cast<char const*>(indexes + 1) + consumed;
        uint64_t size;
        memcpy(&size, buffer, sizeof size);
        buffer += sizeof size;
        // Reuse the string to minimize memory allocations.
        s.assign(buffer, size);

        indexes->consume(consumed + sizeof(uint64_t) + size);
        return true;
    }
};

int main(int ac, char** av) {
    if(ac > 1) {
        // Producer.
        SharedMemoryStrings a;
        for(int i = 1; i < ac; ++i)
            a.produce(av[i]);
    }
    else {
        // Consumer.
        SharedMemoryStrings a;
        for(std::string s;;) { // Busy-wait loop.
            if(a.try_consume(s)) // Reuse the string to minimize memory allocations.
                printf("%s\n", s.c_str());
            // else // Potential optimization.
            //     _mm_pause();
        }
    }
}

注意:

  • 编译类似g++ -o test -W{all,extra,error} -std=gnu++11 -O3 -DNDEBUG -march=native -pthread -lrt test.cc的代码。假设此来源称为test.cc

  • 不使用任何参数./test启动使用者。具有参数的生产者,例如./test hello world。开始顺序无关紧要。

  • 这是单生产者单消费者解决方案。它是 free-free (生产者和消费者的调用按固定数量的指令完成,没有循环),这比 free-locking 更好(不能保证在一定数量的指令中完成)。不能那么快。

  • 在x86-64上,它们会获取并释放原子负载,并将编译结果存储到普通mov指令中,因为当前的x86-64内存模型太强大了。但是,使用std::atomic和特定的内存顺序可确保编译器不会对指令重新排序。并且还可以确保代码可以在内存模型较弱的体系结构上编译并正常工作,并在必要时插入适当的屏障,volatile可能无法做到。例如,像PowerPC。使用volatile与使用std::memory_order_relaxed相同。参见the assembly comparison

  • produced_.store(end, std::memory_order_release);确保生产者线程创建的所有先前存储(memcpy进入共享内存)在{{1 }}。请参阅http://preshing.com/20130823/the-synchronizes-with-relation/,以全面了解该主题。另外std::memory_order说得最好:

      

    produced_.load(std::memory_order_acquire);使用此内存顺序的加载操作在受影响的内存位置上执行获取操作:在此加载之前,无法重新排序当前线程中的读取或写入。在其他线程中释放相同原子变量的所有写操作在当前线程中可见。

         

    memory_order_acquire具有此内存顺序的存储操作执行释放操作:在此存储之后,无法对当前线程中的任何读取或写入进行重新排序。当前线程中的所有写操作在获得相同原子变量的其他线程中可见,并且将依赖项带入原子变量的写操作在使用相同原子的其他线程中可见。

  • 生产者检测消费者何时消耗了所有可用数据。在这种情况下,生产者将缓冲区倒回开始。这样做是为了避免处理环形缓冲区的缓冲区包装。如果使用者不能足够快地处理消息,则无论如何缓冲区最终将变满。

  • 它从不调用memory_order_release构造函数。它依赖于一个事实,即一个新文件被零初始化,这就是构造函数将要做的事情。在更复杂的场景中,如果刚刚创建了文件,则需要调用共享数据的构造函数。这可以通过以下方法完成:首先创建一个具有唯一名称的临时文件(如果该文件尚不存在),然后将该文件映射到内存中并调用构造函数。然后将该临时文件重命名为最终名称(SingleProducerSingleConsumerIndexes是原子的)。如果由于文件已存在而重命名失败,请删除该临时文件,然后重新开始。

  • 使用方忙于等待,以将等待时间降至最低。如果您希望使用者在等待时阻塞,则可以添加一个进程共享的互斥量和条件变量来实现。不过,唤醒线程要等待内核中的条件变量(Linux中为futex)需要几微秒的时间。那将需要调用rename构造函数来进行所有必需的初始化(例如,初始化一个健壮的自适应过程共享互斥量和一个过程共享条件变量)。