diff --git a/Makefile.am b/Makefile.am index 8c5592cf..07d42611 100644 --- a/Makefile.am +++ b/Makefile.am @@ -160,6 +160,10 @@ UTIL_FILES = \ matrix.ipp \ memory.cpp \ memory.hpp \ + memory/buffer/circular.cpp \ + memory/buffer/circular.hpp \ + memory/buffer/paged.cpp \ + memory/buffer/paged.hpp \ memory/system.cpp \ memory/system.hpp \ net/address.cpp \ @@ -356,7 +360,7 @@ libutil_a_CXXFLAGS = $(AM_CXXFLAGS) AM_DEFAULT_SOURCE_EXT = .cpp -AM_LDFLAGS = $(BOOST_LDFLAGS) +AM_LDFLAGS = $(BOOST_LDFLAGS) -lrt bin_PROGRAMS = \ tools/json-clean \ @@ -401,6 +405,8 @@ TEST_BIN = \ test/introspection \ test/ip \ test/json_types \ + test/memory/buffer/circular \ + test/memory/buffer/paged \ test/maths \ test/matrix \ test/md2 \ diff --git a/memory/buffer/circular.cpp b/memory/buffer/circular.cpp new file mode 100644 index 00000000..b3eca448 --- /dev/null +++ b/memory/buffer/circular.cpp @@ -0,0 +1,145 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2015 Danny Robson + */ + +#include "./circular.hpp" + +#include "../system.hpp" +#include "../../except.hpp" +#include "../../raii.hpp" +#include "../../maths.hpp" +#include "../../random.hpp" + +#include +#include +#include +#include +#include + +#include + +using util::memory::buffer::circular; + + +/////////////////////////////////////////////////////////////////////////////// +// generate a random string that could be used as a path leaf +// +// it looks a lot like a shitty tmpnam replacement because it is. we can't use +// tmpnam without security warnings being emitted by binutils linker, despite +// using it safely in this particular scenario. +static void +tmpname (std::string &str, size_t length) +{ + static const char alphanum[] = + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789"; + + str.resize (length); + std::generate_n (str.begin (), + length, + [&] (void) { return util::choose (alphanum); }); +} + + +/////////////////////////////////////////////////////////////////////////////// +circular::circular (size_t bytes) +{ + bytes = max (bytes, sizeof (value_type)); + bytes = align (bytes, pagesize ()); + + int fd = -1; + + constexpr size_t RETRIES = 128; + constexpr size_t NAME_LENGTH = 16; + std::string name (NAME_LENGTH, '\0'); + + // keep generating paths and attempting to create the shm backing. we may + // fall through this loop upon failure, so be sure to check the validity + // of the fd at the end. + for (size_t i = 0; fd < 0 && i < RETRIES; ++i) { + tmpname (name, NAME_LENGTH); + name[0] = '/'; + + fd = shm_open (name.c_str (), O_EXCL | O_CREAT | O_TRUNC | O_RDWR, 0600); + } + + if (fd < 0) + throw std::runtime_error ("unable to generate shm name"); + + // setup a desctructor for the shm data. mmap retains a reference, so do + // this whether we succeed or fail in the next phase. + util::scoped_function raii ([&name] (void) { shm_unlink (name.c_str ()); }); + + // embiggen to the desired size + ftruncate (fd, bytes); + + // pre-allocate a sufficiently large virtual memory block. it doesn't + // matter much what flags we use because we'll just be overwriting it + // shortly. + m_begin = reinterpret_cast (mmap (nullptr, bytes * 2, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0)); + if (MAP_FAILED == m_begin) + errno_error::throw_code (); + + // preemptively setup an unmapping object in case the remapping fails + util::scoped_function unmapper ([this, bytes] (void) { munmap (m_begin, bytes); }); + + // overwrite the map with two adjacent copies of the memory object. this + // must be a shared mapping for the values to propogate across segments. + auto prot = PROT_READ | PROT_WRITE; + auto flag = MAP_FIXED | MAP_SHARED; + + m_begin = reinterpret_cast (mmap (m_begin, bytes, prot, flag, fd, 0)); + m_end = reinterpret_cast (mmap (m_begin + bytes, bytes, prot, flag, fd, 0)); + + if (m_begin == MAP_FAILED || m_end == MAP_FAILED) + errno_error::throw_code (); + + // all went well, disarm the failsafe + unmapper.clear (); +} + + +//----------------------------------------------------------------------------- +circular::~circular () +{ + auto res = munmap (m_begin, 2 * (m_end - m_begin)); + (void)res; + CHECK_ZERO (res); +} + + +/////////////////////////////////////////////////////////////////////////////// +char* +circular::begin (void) +{ + return m_begin; +} + + +//----------------------------------------------------------------------------- +char* +circular::end (void) +{ + return m_end; +} + + +/////////////////////////////////////////////////////////////////////////////// +size_t +circular::size (void) const +{ + return m_end - m_begin; +} diff --git a/memory/buffer/circular.hpp b/memory/buffer/circular.hpp new file mode 100644 index 00000000..32fe4891 --- /dev/null +++ b/memory/buffer/circular.hpp @@ -0,0 +1,55 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2015 Danny Robson + */ + +#ifndef __UTIL_MEMORY_BUFFER_CIRCULAR_HPP +#define __UTIL_MEMORY_BUFFER_CIRCULAR_HPP + +#include + +namespace util { namespace memory { namespace buffer { + // buffer size is advisory and will likely depend on page size. the user + // must check the size after creation if this field is important for + // their usage. + class circular { + public: + using value_type = char; + + circular (size_t bytes); + ~circular (); + + circular (const circular&) = delete; + circular (circular&&) = delete; + circular& operator= (const circular&) = delete; + circular& operator= (circular&&) = delete; + + char& operator[] (size_t); + const char& operator[] (size_t) const; + + char* begin (void); + char* end (void); + + const char* begin (void) const; + const char* end (void) const; + + size_t size (void) const; + + private: + char *m_begin, *m_end; + + }; +} } } + +#endif diff --git a/memory/buffer/paged.cpp b/memory/buffer/paged.cpp new file mode 100644 index 00000000..3bb5db71 --- /dev/null +++ b/memory/buffer/paged.cpp @@ -0,0 +1,166 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2015 Danny Robson + */ + +#include "./paged.hpp" + +#include "../system.hpp" +#include "../../memory.hpp" +#include "../../types/casts.hpp" +#include "../../except.hpp" + +#include + +using util::memory::buffer::paged; + + +/////////////////////////////////////////////////////////////////////////////// +paged::paged (size_t bytes, size_t _window): + m_window (align (_window, pagesize ())) +{ + // reserve the address region with no access permissions + m_begin = reinterpret_cast ( + mmap (nullptr, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0) + ); + + if (m_begin == MAP_FAILED) + errno_error::throw_code (); + + // remap the initial window with read/write permissions + m_cursor = m_begin + align (min (m_window, bytes), pagesize ()); + if (MAP_FAILED == mmap (m_begin, + m_cursor - m_begin, + PROT_READ | PROT_WRITE, + MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, 0, 0)) + errno_error::throw_code (); + + // record the nominal end address + m_end = m_begin + align (bytes, pagesize ()); +} + + +//----------------------------------------------------------------------------- +paged::~paged () +{ + // ignore errors in production; we don't want to double throw. + auto res = munmap (m_begin, m_end - m_begin); + (void)res; + CHECK_ZERO (res); +} + + +/////////////////////////////////////////////////////////////////////////////// +char* +paged::begin (void) +{ + return m_begin; +} + + +//----------------------------------------------------------------------------- +char* +paged::end (void) +{ + return m_end; +} + + +/////////////////////////////////////////////////////////////////////////////// +void +paged::access (char *cursor) +{ + if (cursor < m_cursor) + release (cursor); + else + commit (cursor); +} + + +//----------------------------------------------------------------------------- +void +paged::commit (char *cursor) +{ + // bail if it's already mapped + if (cursor <= m_cursor) + return; + + if (cursor > m_end || cursor < m_begin) + throw std::out_of_range ("invalid commit cursor"); + + // bump the request up to page aligned and tack on a little to amortize + // syscall overheads + cursor = align (cursor, pagesize ()) + m_window; + cursor = min (cursor, m_end); + + if (MAP_FAILED == mmap (m_cursor, + cursor - m_cursor, + PROT_READ | PROT_WRITE, + MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, + 0, 0)) + errno_error::throw_code (); + + m_cursor = cursor; +} + + +//----------------------------------------------------------------------------- +void +paged::release (char *desired) +{ + if (desired > m_end || desired < m_begin) + throw std::out_of_range ("invalid release cursor"); + + align (desired, pagesize ()); + + // bail if the region is alread unmapped, or if it's not sufficiently + // behind the current cursor. + if (desired >= m_cursor || sign_cast (m_cursor - desired) < m_window) + return; + + desired += m_window; + + if (MAP_FAILED == mmap (desired, + m_end - desired, + PROT_NONE, + MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, + 0, 0)) + errno_error::throw_code (); + + m_cursor = desired; +} + + +/////////////////////////////////////////////////////////////////////////////// +size_t +paged::size (void) const +{ + return m_cursor - m_begin; +} + + +//----------------------------------------------------------------------------- +size_t +paged::capacity (void) const +{ + return m_end - m_begin; +} + + +//----------------------------------------------------------------------------- +size_t +paged::window (void) const +{ + return m_window; +} diff --git a/memory/buffer/paged.hpp b/memory/buffer/paged.hpp new file mode 100644 index 00000000..0befbb38 --- /dev/null +++ b/memory/buffer/paged.hpp @@ -0,0 +1,59 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2015 Danny Robson + */ + +#ifndef __UTIL_MEMORY_BUFFER_PAGED_HPP +#define __UTIL_MEMORY_BUFFER_PAGED_HPP + +#include + +namespace util { namespace memory { namespace buffer { + class paged { + public: + using value_type = char; + + paged (size_t bytes, size_t window); + ~paged (); + + paged (const paged&) = delete; + paged (paged &&) = delete; + paged& operator= (const paged&) = delete; + paged& operator= (paged &&) = delete; + + char* begin (void); + char* end (void); + + const char* cbegin (void); + const char* cend (void); + + const char* begin (void) const; + const char* end (void) const; + + void access (char*); + + size_t size (void) const; + size_t capacity (void) const; + size_t window (void) const; + + private: + void commit (char*); + void release (char*); + + char *m_begin, *m_end, *m_cursor; + size_t m_window; + }; +} } } + +#endif diff --git a/test/memory/buffer/circular.cpp b/test/memory/buffer/circular.cpp new file mode 100644 index 00000000..6c92eed7 --- /dev/null +++ b/test/memory/buffer/circular.cpp @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Copyright 2015 Danny Robson + */ + +#include "memory/buffer/circular.hpp" +#include "tap.hpp" + +#include + +/////////////////////////////////////////////////////////////////////////////// +int +main (void) +{ + util::TAP::logger tap; + + // provoke usage of the smallest size buffer we can get away with so we + // might detect caching issues or similar. + constexpr size_t CAPACITY = 1; + util::memory::buffer::circular buffer (CAPACITY); + + // zero fill to ensure our value setting tests don't accidentall succeed + std::fill_n (buffer.begin (), buffer.size () * 2, 0); + + // sanity check we haven't accidentally mapped an empty region + tap.expect_neq (buffer.begin (), buffer.end (), "non-zero sized region"); + + // check a near overrun is replicated + buffer.end ()[0] = 1; + tap.expect_eq (buffer.begin ()[0], buffer.end ()[0], "near overrun is replicated"); + + // check a far overrun is replicated + buffer.end ()[buffer.size () - 1] = 2; + tap.expect_eq (buffer.begin ()[buffer.size () - 1], + buffer.end ()[buffer.size () - 1], + "far overrun is replicated"); + + return tap.status (); +} diff --git a/test/memory/buffer/paged.cpp b/test/memory/buffer/paged.cpp new file mode 100644 index 00000000..38070b3f --- /dev/null +++ b/test/memory/buffer/paged.cpp @@ -0,0 +1,100 @@ +#include "tap.hpp" +#include "memory/buffer/paged.hpp" +#include "debug.hpp" +#include "except.hpp" + +#include +#include + +/////////////////////////////////////////////////////////////////////////////// +sigjmp_buf fault_jmp; + + +template +bool +has_fault (const volatile T* addr) +{ + if (sigsetjmp (fault_jmp, 1) == 0) { + *addr; + return false; + } else { + return true; + } +} + + +//----------------------------------------------------------------------------- +static bool fault_seen; +static void *fault_address; + +void +segv_handler (int num, siginfo_t *info, void *cookie) +{ + CHECK_EQ (num, SIGSEGV); + + (void)num; + (void)cookie; + + fault_seen = true; + fault_address = info->si_addr; + + siglongjmp (fault_jmp, SIGSEGV); +} + + + +/////////////////////////////////////////////////////////////////////////////// +int +main (void) +{ + util::TAP::logger tap; + + // setup a trap to record SEGV events + struct sigaction newhandler {}; + newhandler.sa_sigaction = segv_handler; + newhandler.sa_flags = SA_SIGINFO; + + auto err = sigaction (SIGSEGV, &newhandler, nullptr); + if (err) + util::errno_error::throw_code (); + + // initialise a partially unmapped buffer. the tests assume that the + // window is substantially less than half the capacity (so that probing + // the centre doesn't trigger a mapping overlapping the end). + constexpr size_t CAPACITY = 16 * 1024 * 1024; + constexpr size_t WINDOW = 1024 * 1024; + + util::memory::buffer::paged buffer (CAPACITY, WINDOW); + + typedef decltype(buffer)::value_type value_type; + const value_type *first = buffer.begin (); + const value_type *last = buffer.end () - 1; + const value_type *centre = buffer.begin () + buffer.capacity () / 2; + const value_type *window = centre + buffer.window () - 1; + + // ensure correct initial mappings + tap.expect (!has_fault (first), "first is initially valid"); + tap.expect ( has_fault (last), "last is intially invalid"); + + // allocate half the buffer and check mappings + buffer.access (const_cast (centre)); + + tap.expect (!has_fault (first), "first remains valid after commit"); + tap.expect (!has_fault (centre), "centre is valid after partial commit"); + tap.expect (!has_fault (window), "centre window is valid after partial commit"); + tap.expect ( has_fault (last), "last is invalid after partial commit"); + + // allocate the entire buffer and check the last address + buffer.access (const_cast (last)); + + tap.expect (!has_fault (last), "last is valid after total commit"); + + // unmap the buffer and check centre and last are invalid + buffer.access (const_cast (first)); + + tap.expect (!has_fault (first), "first value remains valid after release"); + tap.expect ( has_fault (centre), "centre is invalid after release"); + tap.expect ( has_fault (last), "last is invalid after release"); + + return tap.status (); +}