LLFIO  v2.00 late alpha
llfio_v2_xxx::algorithm::shared_fs_mutex::memory_map< Hasher, HashIndexSize, SpinlockType > Class Template Reference

Many entity memory mapped shared/exclusive file system based lock. More...

#include "memory_map.hpp"

Inheritance diagram for llfio_v2_xxx::algorithm::shared_fs_mutex::memory_map< Hasher, HashIndexSize, SpinlockType >:
llfio_v2_xxx::algorithm::shared_fs_mutex::shared_fs_mutex

Classes

struct  _entity_idx
 

Public Types

using entity_type = shared_fs_mutex::entity_type
 The type of an entity id.
 
using entities_type = shared_fs_mutex::entities_type
 The type of a sequence of entities.
 
using hasher_type = Hasher< entity_type::value_type >
 The type of the hasher being used.
 
using spinlock_type = SpinlockType
 The type of the spinlock being used.
 

Public Member Functions

 memory_map (const memory_map &)=delete
 No copy construction.
 
memory_mapoperator= (const memory_map &)=delete
 No copy assignment.
 
 memory_map (memory_map &&o) noexcept
 Move constructor.
 
memory_mapoperator= (memory_map &&o) noexcept
 Move assign.
 
const file_handlehandle () const noexcept
 Return the handle to file being used for this lock.
 
virtual void unlock (entities_type entities, unsigned long long) noexcept final
 Unlock a previously locked sequence of entities.
 
entity_type entity_from_buffer (const char *buffer, size_t bytes, bool exclusive=true) noexcept
 Generates an entity id from a sequence of bytes.
 
template<typename T >
entity_type entity_from_string (const std::basic_string< T > &str, bool exclusive=true) noexcept
 Generates an entity id from a string.
 
entity_type random_entity (bool exclusive=true) noexcept
 Generates a cryptographically random entity id.
 
void fill_random_entities (span< entity_type > seq, bool exclusive=true) noexcept
 Fills a sequence of entity ids with cryptographic randomness. Much faster than calling random_entity() individually.
 
result< entities_guardlock (entities_type entities, deadline d=deadline(), bool spin_not_sleep=false) noexcept
 Lock all of a sequence of entities for exclusive or shared access.
 
result< entities_guardlock (entity_type entity, deadline d=deadline(), bool spin_not_sleep=false) noexcept
 Lock a single entity for exclusive or shared access.
 
result< entities_guardtry_lock (entities_type entities) noexcept
 Try to lock all of a sequence of entities for exclusive or shared access.
 
result< entities_guardtry_lock (entity_type entity) noexcept
 Try to lock a single entity for exclusive or shared access.
 

Static Public Member Functions

static result< memory_mapfs_mutex_map (const path_handle &base, path_view lockfile) noexcept
 

Protected Member Functions

virtual result< void > _lock (entities_guard &out, deadline d, bool spin_not_sleep) noexcept final
 

Static Protected Member Functions

static span< _entity_idx_hash_entities (_entity_idx *entity_to_idx, entities_type &entities)
 

Detailed Description

template<template< class > class Hasher = QUICKCPPLIB_NAMESPACE::algorithm::hash::fnv1a_hash, size_t HashIndexSize = 4096, class SpinlockType = QUICKCPPLIB_NAMESPACE::configurable_spinlock::shared_spinlock<>>
class llfio_v2_xxx::algorithm::shared_fs_mutex::memory_map< Hasher, HashIndexSize, SpinlockType >

Many entity memory mapped shared/exclusive file system based lock.

Template Parameters
HasherA STL compatible hash algorithm to use (defaults to fnv1a_hash)
HashIndexSizeThe size in bytes of the hash index to use (defaults to 4Kb)
SpinlockTypeThe type of spinlock to use (defaults to a SharedMutex concept spinlock)

This is the highest performing filing system mutex in LLFIO, but it comes with a long list of potential gotchas. It works by creating a random temporary file somewhere on the system and placing its path in a file at the lock file location. The random temporary file is mapped into memory by all processes using the lock where an open addressed hash table is kept. Each entity is hashed into somewhere in the hash table and its individual spin lock is used to implement the exclusion. As with byte_ranges, each entity is locked individually in sequence but if a particular lock fails, all are unlocked and the list is randomised before trying again. Because this locking implementation is entirely implemented in userspace using shared memory without any kernel syscalls, performance is probably as fast as any many-arbitrary-entity shared locking system could be.

As it uses shared memory, this implementation of shared_fs_mutex cannot work over a networked drive. If you attempt to open this lock on a network drive and the first user of the lock is not on this local machine, errc::no_lock_available will be returned from the constructor.

  • Linear complexity to number of concurrent users up until hash table starts to get full or hashed entries collide.
  • Sudden power loss during use is recovered from.
  • Safe for multithreaded usage of the same instance.
  • In the lightly contended case, an order of magnitude faster than any other shared_fs_mutex algorithm.

Caveats:

  • No ability to sleep until a lock becomes free, so CPUs are spun at 100%.
  • Sudden process exit with locks held will deadlock all other users.
  • Exponential complexity to number of entities being concurrently locked.
  • Exponential complexity to concurrency if entities hash to the same cache line. Most SMP and especially NUMA systems have a finite bandwidth for atomic compare and swap operations, and every attempt to lock or unlock an entity under this implementation is several of those operations. Under heavy contention, whole system performance very noticeably nose dives from excessive atomic operations, things like audio and the mouse pointer will stutter.
  • Sometimes different entities hash to the same offset and collide with one another, causing very poor performance.
  • Memory mapped files need to be cache unified with normal i/o in your OS kernel. Known OSs which don't use a unified cache for memory mapped and normal i/o are QNX, OpenBSD. Furthermore, doing normal i/o and memory mapped i/o to the same file needs to not corrupt the file. In the past, there have been editions of the Linux kernel and the OS X kernel which did this.
  • If your OS doesn't have sane byte range locks (OS X, BSD, older Linuxes) and multiple objects in your process use the same lock file, misoperation will occur.
  • Requires handle::current_path() to be working.
Todo:
memory_map::_hash_entities needs to hash x16, x8 and x4 at a time to encourage auto vectorisation

Member Function Documentation

◆ fs_mutex_map()

template<template< class > class Hasher = QUICKCPPLIB_NAMESPACE::algorithm::hash::fnv1a_hash, size_t HashIndexSize = 4096, class SpinlockType = QUICKCPPLIB_NAMESPACE::configurable_spinlock::shared_spinlock<>>
static result<memory_map> llfio_v2_xxx::algorithm::shared_fs_mutex::memory_map< Hasher, HashIndexSize, SpinlockType >::fs_mutex_map ( const path_handle base,
path_view  lockfile 
)
inlinestaticnoexcept

Initialises a shared filing system mutex using the file at lockfile.

Errors returnable
Awaiting the clang result<> AST parser which auto generates all the error codes which could occur, but a particularly important one is errc::no_lock_available which will be returned if the lock is in use by another computer on a network.
198  {
199  LLFIO_LOG_FUNCTION_CALL(0);
200  try
201  {
203  file_handle temph;
204  // Am I the first person to this file? Lock everything exclusively
205  auto lockinuse = ret.try_lock_range(_initialisingoffset, 2, file_handle::lock_kind::exclusive);
206  if(lockinuse.has_error())
207  {
208  if(lockinuse.error() != errc::timed_out)
209  {
210  return std::move(lockinuse).error();
211  }
212  // Somebody else is also using this file, so try to read the hash index file I ought to use
213  lockinuse = ret.lock_range(_lockinuseoffset, 1, file_handle::lock_kind::shared); // inuse shared access, blocking
214  if(!lockinuse)
215  {
216  return std::move(lockinuse).error();
217  }
218  byte buffer[65536];
219  memset(buffer, 0, sizeof(buffer));
220  OUTCOME_TRYV(ret.read(0, {{buffer, 65535}}));
221  path_view temphpath(reinterpret_cast<filesystem::path::value_type *>(buffer));
222  result<file_handle> _temph(in_place_type<file_handle>);
224  // If temp file doesn't exist, I am on a different machine
225  if(!_temph)
226  {
227  // Release the exclusive lock and tell caller that this lock is not available
228  return errc::no_lock_available;
229  }
230  temph = std::move(_temph.value());
231  // Map the hash index file into memory for read/write access
232  OUTCOME_TRY(temphsection, section_handle::section(temph, HashIndexSize));
233  OUTCOME_TRY(temphmap, map_handle::map(temphsection, HashIndexSize));
234  // Map the path file into memory with its maximum possible size, read only
235  OUTCOME_TRY(hsection, section_handle::section(ret, 65536, section_handle::flag::read));
236  OUTCOME_TRY(hmap, map_handle::map(hsection, 0, 0, section_handle::flag::read));
237  return memory_map(std::move(ret), std::move(temph), std::move(lockinuse.value()), std::move(hmap), std::move(temphmap));
238  }
239 
240  // I am the first person to be using this (stale?) file, so create a new hash index file in /tmp
242  OUTCOME_TRY(_temph, file_handle::random_file(tempdirh));
243  temph = std::move(_temph);
244  // Truncate it out to the hash index size, and map it into memory for read/write access
245  OUTCOME_TRYV(temph.truncate(HashIndexSize));
246  OUTCOME_TRY(temphsection, section_handle::section(temph, HashIndexSize));
247  OUTCOME_TRY(temphmap, map_handle::map(temphsection, HashIndexSize));
248  // Write the path of my new hash index file, padding zeros to the nearest page size
249  // multiple to work around a race condition in the Linux kernel
250  OUTCOME_TRY(temppath, temph.current_path());
251  char buffer[4096];
252  memset(buffer, 0, sizeof(buffer));
253  size_t bytes = temppath.native().size() * sizeof(*temppath.c_str());
254  file_handle::const_buffer_type buffers[] = {{reinterpret_cast<const byte *>(temppath.c_str()), bytes}, {reinterpret_cast<const byte *>(buffer), 4096 - (bytes % 4096)}};
255  OUTCOME_TRYV(ret.truncate(65536));
256  OUTCOME_TRYV(ret.write({buffers, 0}));
257  // Map for read the maximum possible path file size, again to avoid race problems
258  OUTCOME_TRY(hsection, section_handle::section(ret, 65536, section_handle::flag::read));
259  OUTCOME_TRY(hmap, map_handle::map(hsection, 0, 0, section_handle::flag::read));
260  /* Take shared locks on inuse. Even if this implementation doesn't implement
261  atomic downgrade of exclusive range to shared range, we're fully prepared for other users
262  now. The _initialisingoffset remains exclusive to prevent double entry into this init routine.
263  */
264  OUTCOME_TRY(lockinuse2, ret.lock_range(_lockinuseoffset, 1, file_handle::lock_kind::shared));
265  lockinuse = std::move(lockinuse2); // releases exclusive lock on all three offsets
266  return memory_map(std::move(ret), std::move(temph), std::move(lockinuse.value()), std::move(hmap), std::move(temphmap));
267  }
268  catch(...)
269  {
270  return error_from_exception();
271  }
272  }
Exclude only those requesting an exclusive lock on the same inode.
file_handle::io_result< file_handle::size_type > read(file_handle &self, file_handle::extent_type offset, std::initializer_list< file_handle::buffer_type > lst, deadline d=deadline()) noexcept
Definition: file_handle.hpp:553
static result< file_handle > file(const path_handle &base, path_view_type path, mode _mode=mode::read, creation _creation=creation::open_existing, caching _caching=caching::all, flag flags=flag::none) noexcept
Exclude those requesting any kind of lock on the same inode.
Ability to read and write (READ_CONTROL|FILE_READ_DATA|FILE_READ_ATTRIBUTES|FILE_READ_EA|FILE_WRITE_D...
static result< map_handle > map(size_type bytes, bool zeroed=false, section_handle::flag _flag=section_handle::flag::readwrite) noexcept
static result< file_handle > random_file(const path_handle &dirpath, mode _mode=mode::write, caching _caching=caching::temporary, flag flags=flag::none) noexcept
Definition: file_handle.hpp:143
bool is_valid() const noexcept
True if the handle is valid (and usually open)
Definition: handle.hpp:285
static result< section_handle > section(file_handle &backing, extent_type maximum_size, flag _flag) noexcept
Create a memory section backed by a file.
Cache reads and writes of data and metadata so they complete immediately, only sending any updates to...
Cache reads only. Writes of data and metadata do not complete until reaching storage (O_SYNC)...
If filesystem entry exists that is used, else one is created.
const path_handle & storage_backed_temporary_files_directory() noexcept
Returns a reference to an open handle to a verified temporary directory where files created are store...
const path_handle & memory_backed_temporary_files_directory() noexcept
Returns a reference to an open handle to a verified temporary directory where files created are store...
Filesystem entry must already exist.

The documentation for this class was generated from the following file: