简体   繁体   English

在共享 memory 中移动 boost::interprocess::string

[英]move boost::interprocess::string in shared memory

I wanted to implement, some queue of messages (based on vector) to handling in some way data from the network and to do this I used shared memory to save messages and I encountered an issue related to it, the thing is that my code works well when I run it first time, when I want to run it once again I get segfaut when I want to assign a new value to string in my queue in shared memory, actually in my case when I want to move it (the same problem exists when I want to copy it).我想实现,一些消息队列(基于向量)以某种方式处理来自网络的数据,为此我使用共享 memory 来保存消息,我遇到了一个与之相关的问题,问题是我的代码有效好吧,当我第一次运行它时,当我想再次运行它时,当我想为共享 memory 中的队列中的字符串分配一个新值时,我得到了 segfaut,实际上在我想移动它的情况下(同样的问题当我想复制它时存在)。 The problem doesn't exist when SSO is working, so when I have small string enough.当 SSO 工作时,问题不存在,所以当我有足够小的字符串时。 What did I wrong?我做错了什么?

#include <atomic>
#include <exception>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <boost/interprocess/containers/vector.hpp>
#include <boost/interprocess/managed_shared_memory.hpp>

namespace bip = boost::interprocess;

struct BadSharedMemoryAccess final : public std::exception
{
    BadSharedMemoryAccess(std::string&& msg):
        msg_{std::move(msg)}
{}

virtual const char* what() const noexcept
{
    return msg_.c_str();
}

private:
    std::string msg_;
};

struct Message
{
    bip::string message_;
};

template<typename Alloc>
class MyCustomData final
{
public:
    using allocator_type = typename Alloc::template rebind<Message>::other;

    MyCustomData(std::size_t number_of_messages, Alloc alloc = {}) :
        init_add_index_{0},
        init_handle_index_{-1},
        messages_{number_of_messages, alloc}
    {}

public:
    uint_fast64_t init_add_index_;
    int_fast64_t init_handle_index_;
    std::vector<Message, Alloc> messages_;
//    bip::vector<data::Message, Alloc> messages_;
};

template<typename DataType, typename DataAllocator>
class SharedMemory
{
public:
    template<typename... Args>
    SharedMemory(std::string const& shm_segment_name, std::size_t const segment_size,
        std::string const& shm_object_name, Args&&... args) :
            shm_object_name_{shm_object_name}
    {
        std::cout << "attempt to allocate space for shared memory segment " << shm_segment_name
              << ", size: ." << segment_size << std::endl;
        setSharedMemorySize(shm_segment_name, segment_size);

        DataAllocator const allocInstance{shm_.get_segment_manager()};
        data_ = shm_.find_or_construct<DataType>(shm_object_name.c_str())(std::forward<Args>(args)..., allocInstance);
        if (data_)
            std::cout << "shared memory segment has been allocated" << std::endl;
        else
            std::cout << "shared memory has not been constructed or founded" << std::endl;
    }

    virtual ~SharedMemory()
    {
        std::cout << "shared memory segment will be closed." << std::endl;
    }

    void setSharedMemorySize(std::string const& shm_segment_name, std::size_t const segment_size)
    {
        auto page_size = bip::mapped_region::get_page_size();
        auto const page_increase_rate{2};
        while (page_size < segment_size)
        {
            page_size *= page_increase_rate;
        }

        std::cout <<"seting page size: " << page_size << std::endl;
        shm_ = bip::managed_shared_memory{bip::open_or_create, shm_segment_name.c_str(), page_size};
        std::cout << "space for shared memory has been successfully allocated." << std::endl;
    }

    DataType& getData()
    {
        if (not data_)
            throw BadSharedMemoryAccess{"cannot access " + shm_object_name_};
        return *data_;
    }

protected:
    DataType* data_;

private:
    std::string const shm_object_name_;
    bip::managed_shared_memory shm_;
};

namespace sharable
{
    using DataAllocator = bip::allocator<Message, bip::managed_shared_memory::segment_manager>;
    template<typename Alloc>
    using DataType = MyCustomData<Alloc>;
}

int main()
{
    std::size_t const max_number_of_elements_in_container{1000000};
    auto shmem_data = std::make_shared<SharedMemory<MyCustomData<sharable::DataAllocator>, sharable::DataAllocator>>(
        "SHM_SEGMENT", sizeof(MyCustomData<sharable::DataAllocator>) +
            (max_number_of_elements_in_container * sizeof(Message) * 2),
        "SHM_CONTAINER", max_number_of_elements_in_container);

    std::vector<bip::string> feed{max_number_of_elements_in_container};
    for (std::size_t i = 0; i < max_number_of_elements_in_container; ++i)
    {
        std::string s{"blablabla11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111" + std::to_string(i)};
        feed[i] = s.c_str();
    }

    auto& data = shmem_data->getData();
    auto& shmem_vec = data.messages_;
    std::cout << "addr: " << shmem_vec.data() << std::endl;
    for (std::size_t i = 0; i < max_number_of_elements_in_container; ++i)
    {
//        if (i == 0)
//            std::cout << "msg: " << shmem_vec[i].message_ << std::endl;
        auto msg = feed[i];
        shmem_vec[i].message_ = std::move(msg);
    }
    return 0;
}
  1. You are not using a shared-memory allocator for the strings.您没有为字符串使用共享内存分配器。 In that sense your question is the same as circular_buffer and managed_mapped_file segmentation fault .从这个意义上说,您的问题与circular_buffer 和 managed_mapped_file segmentation fault相同。 You might want to read that for a general intro.您可能想阅读它以获得一般介绍。

  2. Your example complicates things by wrapping the strings into your own structs.您的示例通过将字符串包装到您自己的结构中来使事情复杂化。 That means you get a lot of tedious work passing around allocators.这意味着您会在分配器之间进行大量繁琐的工作。 For the "uses_allocator" approach which - in combination with scoped_allocator_adaptor - can alleviate some of that pain, see eg making non-shared copies of boost::interprocess shared memory objects .对于“uses_allocator”方法,结合scoped_allocator_adaptor可以减轻一些痛苦,请参见制作 boost::interprocess shared memory objects 的非共享副本

  3. Reading the rest of your code, I'm a bit confused.阅读您的代码的rest,我有点困惑。 Why would you template your SharedMemory type with an allocator?为什么要使用分配器对SharedMemory类型进行模板化? I mean, the SharedMemory should be the single point responsible for chosing and passing the right allocator, right?我的意思是, SharedMemory应该是负责选择和传递正确分配器的单点,对吧? How could it work with an externally provided allocator.它如何与外部提供的分配器一起工作。

  4. There are typedefs that are unused, you make a new segment for each object even though it might be from the same shared memory (mapping the same pages into memory multiple times).有未使用的 typedef,您为每个 object 创建一个新段,即使它可能来自同一个共享 memory(将相同的页面映射到 memory 多次)。 Yet you somehow think it's important to share ownership of one such an instance ( make_shared ).然而,您不知何故认为共享这样一个实例( make_shared )的所有权很重要。

  5. The size calculations are just wrong: they only take into account the size of your Message struct, not the allocated string data.大小计算是错误的:它们只考虑您的Message结构的大小,而不是分配的字符串数据。 You seem to have forgotten that mapped memory is virtual memory too.您似乎忘记了映射的 memory 也是虚拟 memory。 The underlying storage will be able to allocate sparsely.底层存储将能够稀疏分配。 So, why not reserve a generous amount of memory, and just respond when you run out?那么,何不预留大量的memory,用完就回复呢?

  6. You're talking about and coding (some) move semantics, but then you write:您正在谈论和编码(某些)移动语义,但随后您写道:

     for (std::size_t i = 0; i < max_number_of_elements_in_container; ++i) { auto msg = feed[i]; shmem_vec[i].message_ = std::move(msg); }

    That's confused.这很困惑。 What good is the move (if it worked, see below) if you first make an explicit copy anyways:如果您首先制作一个明确的副本,那么移动有什么好处(如果有效,请参见下文):

     auto msg = feed[i];
  7. These are worrying signs:这些是令人担忧的迹象:

     uint_fast64_t init_add_index_; int_fast64_t init_handle_index_;

    It looks as if you may be planning to use these concurrently from multiple processes/threads².看起来您可能正计划从多个进程/线程中同时使用这些²。 In such case, you must add synchronization OR use atomic<> types at the very least.在这种情况下,您必须添加同步或至少使用atomic<>类型。

Summarizing it looks to me that you may be trying so hard to hide complexity that you accidentally increased it.总结一下,在我看来,您可能非常努力地隐藏复杂性,以至于您不小心增加了它。

On Moving在移动

You ask about "moving shared string in shared memory".您询问“在共享内存中移动共享字符串”。 For this part of the question, let's assume you actually had your strings allocating in shared memory.对于这部分问题,假设您实际上将字符串分配在共享 memory 中。

Looking at how moving strings works it's not hard to see that moving strings inside shared memory will work exactly like moving them inside the heap would work: the object address will be different, but the internal pointer to allocated memory will be the same.看看移动字符串是如何工作的,不难看出在共享 memory中移动字符串的工作方式与在堆内移动它们的工作方式完全相同:object 地址会不同,但指向分配的 ZCD69B4957F06CD8298D7BF3D691 的内部指针将是相同的。

However, the code does something else : It doesn't move inside shared memory.但是,代码做了其他事情:它不会在共享 memory内部移动。 It attempts to move from heap to shared memory.它尝试堆移动共享 memory。 This will obviously not be safe since the objects in shared memory cannot usefully point to anything outside the shared memory segment (any other process would invoke undefined behaviour indirecting through such a pointer).这显然是不安全的,因为共享 memory 中的对象无法有效地指向共享 memory 段之外的任何内容(任何其他进程都会通过这样的指针间接调用未定义的行为)。

As often, in C++ you're partly your own to prevent accidents like this: C++11 basic_string<>::swap specifies通常,在 C++ 中,您部分地是自己来防止这样的事故: C++11 basic_string<>::swap指定

The behavior is undefined if Allocator does not propagate on swap and the allocators of *this and other are unequal.如果Allocator不在交换上传播并且*thisother的分配器不相等,则行为未定义。

The move-constructor is specified to have complexity:移动构造函数被指定为具有复杂性:

constant.持续的。 If alloc is given and alloc.= other,get_allocator(), then linear如果给出了alloc并且alloc.= other,get_allocator(),那么线性

Note that the semantics of allocators when copying/moving containers ( basic_string<> is a container, similar to std::vector<> ) is even more involved:请注意,复制/移动容器( basic_string<>是一个容器,类似于std::vector<> )时分配器的语义更加复杂:

在此处输入图像描述

What To Do?该怎么办?

All in all, if you're lucky the move won't compile because the allocators are of incompatible types and none is supplied (eg by the uses_allocator protocol).总而言之,如果你幸运的话,这个动作不会编译,因为分配器是不兼容的类型并且没有提供(例如通过 uses_allocator 协议)。

If you're less lucky, it will compile but it will (fortunately) not perform the move because it detects that the allocators are "not equal" and hence it falls back to copying the storage.如果你不那么幸运,它会编译,但它不会(幸运地)执行移动,因为它检测到分配器“不相等”,因此它回退到复制存储。

If you're absolutely unlucky, you chose a configuration where the types are compatible and the allocators are not configured to safely propagate on container move/copy, or another circumstance leads the allocators to fail to detect "incompatibility"¹, and you end up with UB.如果您绝对不走运,您选择了类型兼容且分配器未配置为在容器移动/复制时安全传播的配置,或者其他情况导致分配器无法检测到“不兼容”¹,您最终会与 UB。

In this case there's a much easier option: you know you can't move .在这种情况下,有一个更简单的选择:你知道你不能移动 Hence, don't request a move .因此,不要要求搬家

Risk averted.规避风险。

Some Code To Heal Our Wounds治愈我们伤口的一些代码

After breaking down a lot of the complexity in the code and question, let's get constructive and show what we can do to fix things:在分解了代码和问题中的许多复杂性之后,让我们变得有建设性并展示我们可以做些什么来解决问题:

#include <exception>
#include <iomanip>
#include <iostream>
#include <random>

#include <boost/interprocess/allocators/allocator.hpp>
#include <boost/interprocess/containers/string.hpp>
#include <boost/interprocess/containers/vector.hpp>
#include <boost/interprocess/managed_shared_memory.hpp>

namespace bip = boost::interprocess;

struct BadSharedMemoryAccess final : std::runtime_error {
    BadSharedMemoryAccess(std::string msg) : std::runtime_error{ std::move(msg) } {}
};

That's the prelude.这就是前奏。 Now, let's state our intentions:现在,让我们 state 我们的意图:

using Segment = bip::managed_shared_memory;
template <typename U> using Alloc = bip::allocator<U, Segment::segment_manager>;

This makes it easy to refer to (and maybe switch out) the segment and its allocators.这使得引用(并且可能切换出)段及其分配器变得容易。

using Message       = bip::string;
using Feed          = bip::vector<Message>;
using SharedMessage = bip::basic_string<char, std::char_traits<char>, Alloc<char> >;
using SharedFeed    = bip::vector<SharedMessage, Alloc<SharedMessage> >;

Simply define our domain entities.只需定义我们的域实体。 By using bip::string / bip::vector for heap and shared allocation versions we get the best interop between the two;通过对堆和共享分配版本使用bip::string / bip::vector ,我们可以获得两者之间的最佳互操作性;

class MyCustomData final {
  public:
    using allocator_type = SharedFeed::allocator_type;

    MyCustomData(std::size_t capacity, allocator_type alloc)
        : messages_(capacity, SharedMessage(alloc), alloc) // don't brace initlaize
    { }

    auto&       messages()       { return messages_; }
    auto const& messages() const { return messages_; }

  private:
    uint_fast64_t init_add_index_ = 0;
    int_fast64_t  init_handle_index_ = -1;
    SharedFeed messages_;
};

For now, dropped the virtual destructor, and the Message struct that simply wrapped a bip::string for convenience.现在,为了方便起见,删除了virtual析构函数和简单包装bip::stringMessage结构。

template <typename T> class SharedMemory final {
  public:
    template <typename... Args>
    SharedMemory(std::string const& shm_segment_name,
                 std::size_t const segment_size,
                 std::string const& shm_object_name,
                 Args&&... args)
        : shm_ { bip::open_or_create, shm_segment_name.c_str(), segment_size }
    {
        data_ = shm_.find_or_construct<T>
            (shm_object_name.c_str())
            (std::forward<Args>(args)...,
             shm_.get_segment_manager())
            ;

        if (!data_) throw BadSharedMemoryAccess {"cannot access " + shm_segment_name + "/" + shm_object_name};
    }

    T const& get() const { return *data_; }
    T&       get()       { return *data_; }

    auto free() const { return shm_.get_free_memory(); }
  protected:
    T* data_;

  private:
    Segment shm_;
};

It strikes me that SharedMemory has too many responsibilities: on the one hand it tries to be a "smart-reference" for shared objects, and on the other hand it "manages a segment". SharedMemory有太多的职责让我感到震惊:一方面它试图成为共享对象的“智能引用”,另一方面它“管理一个段”。 This leads to problems if you actually wanted to have multiple objects in a segment.如果您实际上想在一个段中拥有多个对象,这会导致问题。 Consider splitting into Shared::Segment and Shared::Object<T> .考虑拆分为Shared::SegmentShared::Object<T>

Feed generate_heap_feed(size_t n) {
    Feed feed;
    feed.reserve(n);
    for (size_t i = 0; i < n ; ++i) {
        feed.emplace_back(
            "blablabla11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
            + std::to_string(i));
    }
    return feed;
}

Extracted the test-feed generator from main .main中提取了 test-feed 生成器。

int main() {
    static constexpr std::size_t capacity { 1000000 };
    static constexpr auto estimate = 300ull << 20; // 300 MiB (<< 10 kilo, << 20 mebi, << 30 gibi)

Replaced the misguided calculations³ with a generous estimate.用慷慨的估计取代了误导的计算³。 See measurements below.请参阅下面的测量值。

    using SharedData = SharedMemory<MyCustomData>;
    SharedData shmem_data("SHM_SEGMENT", estimate, "SHM_CONTAINER", capacity);
    std::cout << "Free: " << shmem_data.free() << "\n";

Nice and readable.很好读。 On my system prints "Free: 282572448" on first run.在我的系统上第一次运行时打印"Free: 282572448"

    Feed const feed      = generate_heap_feed(capacity);
    SharedFeed& shm_feed = shmem_data.get().messages();

Now we have our feeds side by side, let's copy:现在我们有我们的提要并排,让我们复制:

    // copy feed from heap to shm
    auto const n = std::min(feed.size(), shm_feed.size());
    std::copy_n(feed.begin(), n, shm_feed.begin());

    std::cout << "Copied: " << n << "\n";
    std::cout << "Free: " << shmem_data.free() << "\n";

That's all.就这样。 We don't try to move, because we know that can't work.我们不会尝试移动,因为我们知道那是行不通的。 bip::basic_string correctly knows how to copy between incompatible allocators. bip::basic_string正确地知道如何在不兼容的分配器之间进行复制。 No sweat.没有汗水。

For good measure let's print some diagnostic information:为了更好地衡量,让我们打印一些诊断信息:

    {
        // check some random samples
        std::default_random_engine prng{std::random_device{}()};
        auto pick = [&] { return std::uniform_int_distribution<>(0, n-1)(prng); };

        for (auto index : {pick(), pick(), pick(), pick()}) {
            std::string_view a = feed.at(index);
            std::string_view b = shm_feed.at(index);
            std::cout << "Message #" << index
                << (a == b? " OK":" FAIL")
                << " " << std::quoted(b) << std::endl;
        }
    }
}

See it Live On Coliru⁴在 Coliru 上观看直播⁴

Prints, eg:打印,例如: 在此处输入图像描述

Especially note the filesize measurements ( --apparent-size vs. the size on disk).特别注意文件大小测量( --apparent-size与磁盘上的大小)。 This confirms my point about sparse allocation.这证实了我关于稀疏分配的观点。 Even if you reserved 100TB, the effective size of the SHM_CONTAINER would still be 182MiB.即使您保留了 100TB,SHM_CONTAINER 的有效大小仍然是 182MiB。

BONUS SECTIONS奖金部分

Scoped Allocator Adaptors范围分配器适配器

Simply replacing one line:只需替换一行:

template <typename U> using Alloc = bip::allocator<U, Segment::segment_manager>;

with

template <typename U> using Alloc = boost::container::scoped_allocator_adaptor<
    bip::allocator<U, Segment::segment_manager> >;

does the trick, unlocking magical allocator propagation, eg from vector to string when constructing its elements (with emplace or assign ).做到这一点,解锁神奇的分配器传播,例如在构造元素时从向量到字符串(使用emplaceassign )。 So we can simplify the copy_n even more from:所以我们可以进一步简化copy_n

// copy feed from heap to shm
auto const n = std::min(feed.size(), shm_feed.size());
std::copy_n(feed.begin(), n, shm_feed.begin());

std::cout << "Copied: " << n << "\n";

to simply:简单地说:

shm_feed.assign(feed.begin(), feed.end());
std::cout << "Copied: " << shm_feed.size() << "\n";

It has exactly the same allocation behaviour as before.它具有与以前完全相同的分配行为。 See it Live On Coliru as well.也可以在 Coliru上看到它。

Polymorphic Allocators (c++17)多态分配器 (c++17)

This would not change a thing, fundamentally, except:从根本上说,这不会改变任何事情,除了:

  • it would make Feed/SharedFeed and Message/SharedMessage share the same static type它会使 Feed/SharedFeed 和 Message/SharedMessage 共享相同的 static 类型
  • it would have the scoped-allocator behaviour as before by default默认情况下,它将具有与以前一样的作用域分配器行为

However, until we get proper support for fancy pointers in the standard, this is a pipe dream:然而,除非我们在标准中得到对花哨指针的适当支持,否则这是一个 pipe 的梦想:

Making Message Struct Again?再次制作Message结构?

Well.出色地。 More like "struggle again".更像是“再战”。 I admit that I hate writing allocator-aware data types.我承认我讨厌编写可识别分配器的数据类型。 This is no doubt not optimal, but it is the minimal thing I could do to make things working:这无疑不是最佳的,但它是我可以做的最小的事情来使事情正常工作:

template <typename Alloc>
struct BasicMessage {
    // pre-c++17:
    //  using allocator_type = typename Alloc::template rebind<char>::other;
    using allocator_type = typename std::allocator_traits<Alloc>::template rebind_alloc<char>;

    BasicMessage(std::allocator_arg_t, allocator_type alloc)
        : _msg(alloc) { }

    template <typename T1, typename... T,
             typename = std::enable_if_t<
                    not std::is_same_v<std::allocator_arg_t, std::decay_t<T1> >
                 >
        >
    explicit BasicMessage(T1&& a, T&&... init)
        : _msg(std::forward<T1>(a), std::forward<T>(init)...) { }

    template <typename OtherAlloc>
    BasicMessage(BasicMessage<OtherAlloc> const& other, allocator_type alloc)
        : _msg(other.message().begin(), other.message().end(), alloc) { }

    template <typename OtherAlloc, typename OM = BasicMessage<OtherAlloc> >
    std::enable_if_t<
        not std::is_same_v<allocator_type, typename OM::allocator_type>,
        BasicMessage&>
    operator=(BasicMessage<OtherAlloc> const& other) {
        _msg.assign(other.message().begin(), other.message().end());
        return *this;
    }

    template <typename OtherAlloc>
    BasicMessage(std::allocator_arg_t, allocator_type alloc, BasicMessage<OtherAlloc> const& other)
        : _msg(other.message().begin(), other.message().end(), alloc) { }

    BasicMessage(BasicMessage const&) = default;
    BasicMessage(BasicMessage&&) = default;
    BasicMessage& operator=(BasicMessage const&) = default;
    BasicMessage& operator=(BasicMessage&&) = default;

    auto& message() const { return _msg; }
    auto& message()       { return _msg; }
  private:
    bip::basic_string<char, std::char_traits<char>, allocator_type> _msg;
};

using Message       = BasicMessage<std::allocator<char> >;
using Feed          = bip::vector<Message>;
using SharedMessage = BasicMessage<Alloc<char> >;
using SharedFeed    = bip::vector<SharedMessage, Alloc<SharedMessage> >;

On the bright side, that still uses the "magic assign" due to the scoped_allocator_adaptor fix introduced above.从好的方面来说,由于上面介绍的 scoped_allocator_adaptor 修复,它仍然使用“魔术分配”。 Perhaps if that wasn't desired, you could get away with a little bit less complexity.也许如果这不是我们所希望的,你可以减少一点复杂性。

With minor interface changes elsewhere:在其他地方进行了较小的界面更改:

: messages_(capacity, SharedMessage(std::allocator_arg, alloc), alloc) // don't brace initlaize

and

    std::string_view a = feed.at(index).message();
    std::string_view b = shm_feed.at(index).message();

it all still works, see Live On Coliru这一切仍然有效,请参阅Live On Coliru


¹ not standardese, hence the scare-quotes ¹ 不是标准的,因此是吓人的引号

² I'm suspecting you may be trying to implement the Disruptor Pattern ² 我怀疑您可能正在尝试实施 Disruptor Pattern

³ see Estimating size required for memory mapped boost rtree ³ 请参阅memory 映射的 boost rtree 所需的估计大小

⁴ replaced managed_shared_memory with manage_mapped_file and reduced capacities`because of Coliru limitations ⁴ 用manage_mapped_file替换managed_shared_memory并减少容量`因为 Coliru 限制

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM