MemoryMapping
authorTom Jackson <tjackson@fb.com>
Thu, 21 Feb 2013 00:13:00 +0000 (16:13 -0800)
committerJordan DeLong <jdelong@fb.com>
Tue, 19 Mar 2013 00:07:50 +0000 (17:07 -0700)
Summary: MemoryMapping is a C++ wrapper object around mmap. It works with `folly::File`s, and provides bitwise-range access for reading and writing files.

Test Plan: Unit test

Reviewed By: lucian@fb.com

FB internal diff: D452384

folly/MemoryMapping.cpp [new file with mode: 0644]
folly/MemoryMapping.h [new file with mode: 0644]
folly/test/MemoryMappingTest.cpp [new file with mode: 0644]

diff --git a/folly/MemoryMapping.cpp b/folly/MemoryMapping.cpp
new file mode 100644 (file)
index 0000000..617d800
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2013 Facebook, Inc.
+ *
+ * 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.
+ */
+
+#include "folly/MemoryMapping.h"
+#include "folly/Format.h"
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <system_error>
+
+DEFINE_int64(mlock_max_size_at_once, 1 << 20,  // 1MB
+             "Maximum bytes to mlock/munlock/munmap at once "
+             "(will be rounded up to PAGESIZE)");
+
+namespace folly {
+
+/* protected constructor */
+MemoryMapping::MemoryMapping()
+  : mapStart_(nullptr)
+  , mapLength_(0)
+  , locked_(false) {
+}
+
+MemoryMapping::MemoryMapping(File file, off_t offset, off_t length)
+  : mapStart_(nullptr)
+  , mapLength_(0)
+  , locked_(false) {
+
+  init(std::move(file), offset, length, PROT_READ, false);
+}
+
+void MemoryMapping::init(File file,
+                         off_t offset, off_t length,
+                         int prot,
+                         bool grow) {
+  off_t pageSize = sysconf(_SC_PAGESIZE);
+  CHECK_GE(offset, 0);
+
+  // Round down the start of the mapped region
+  size_t skipStart = offset % pageSize;
+  offset -= skipStart;
+
+  file_ = std::move(file);
+  mapLength_ = length;
+  if (mapLength_ != -1) {
+    mapLength_ += skipStart;
+
+    // Round up the end of the mapped region
+    mapLength_ = (mapLength_ + pageSize - 1) / pageSize * pageSize;
+  }
+
+  // stat the file
+  struct stat st;
+  CHECK_ERR(fstat(file_.fd(), &st));
+  off_t remaining = st.st_size - offset;
+  if (mapLength_ == -1) {
+    length = mapLength_ = remaining;
+  } else {
+    if (length > remaining) {
+      if (grow) {
+        PCHECK(0 == ftruncate(file_.fd(), offset + length))
+          << "ftructate() failed, couldn't grow file";
+        remaining = length;
+      } else {
+        length = remaining;
+      }
+    }
+    if (mapLength_ > remaining) mapLength_ = remaining;
+  }
+
+  if (length == 0) {
+    mapLength_ = 0;
+    mapStart_ = nullptr;
+  } else {
+    unsigned char* start = static_cast<unsigned char*>(
+      mmap(nullptr, mapLength_, prot, MAP_SHARED, file_.fd(), offset));
+    PCHECK(start != MAP_FAILED)
+      << " offset=" << offset
+      << " length=" << mapLength_;
+    mapStart_ = start;
+    data_.reset(start + skipStart, length);
+  }
+}
+
+namespace {
+
+off_t memOpChunkSize(off_t length) {
+  off_t chunkSize = length;
+  if (FLAGS_mlock_max_size_at_once <= 0) {
+    return chunkSize;
+  }
+
+  chunkSize = FLAGS_mlock_max_size_at_once;
+  off_t pageSize = sysconf(_SC_PAGESIZE);
+  off_t r = chunkSize % pageSize;
+  if (r) {
+    chunkSize += (pageSize - r);
+  }
+  return chunkSize;
+}
+
+/**
+ * Run @op in chunks over the buffer @mem of @bufSize length.
+ *
+ * Return:
+ * - success: true + amountSucceeded == bufSize (op success on whole buffer)
+ * - failure: false + amountSucceeded == nr bytes on which op succeeded.
+ */
+bool memOpInChunks(std::function<int(void*, size_t)> op,
+                   void* mem, size_t bufSize,
+                   size_t& amountSucceeded) {
+  // unmap/mlock/munlock take a kernel semaphore and block other threads from
+  // doing other memory operations. If the size of the buffer is big the
+  // semaphore can be down for seconds (for benchmarks see
+  // http://kostja-osipov.livejournal.com/42963.html).  Doing the operations in
+  // chunks breaks the locking into intervals and lets other threads do memory
+  // operations of their own.
+
+  size_t chunkSize = memOpChunkSize(bufSize);
+  size_t chunkCount = bufSize / chunkSize;
+
+  char* addr = static_cast<char*>(mem);
+  amountSucceeded = 0;
+
+  while (amountSucceeded < bufSize) {
+    size_t size = std::min(chunkSize, bufSize - amountSucceeded);
+    if (op(addr + amountSucceeded, size) != 0) {
+      return false;
+    }
+    amountSucceeded += size;
+  }
+
+  return true;
+}
+
+}  // anonymous namespace
+
+bool MemoryMapping::mlock(LockMode lock) {
+  size_t amountSucceeded = 0;
+  locked_ = memOpInChunks(::mlock, mapStart_, mapLength_, amountSucceeded);
+  if (locked_) {
+    return true;
+  }
+
+  auto msg(folly::format(
+    "mlock({}) failed at {}",
+    mapLength_, amountSucceeded).str());
+
+  if (lock == LockMode::TRY_LOCK && (errno == EPERM || errno == ENOMEM)) {
+    PLOG(WARNING) << msg;
+  } else {
+    PLOG(FATAL) << msg;
+  }
+
+  // only part of the buffer was mlocked, unlock it back
+  if (!memOpInChunks(::munlock, mapStart_, amountSucceeded, amountSucceeded)) {
+    PLOG(WARNING) << "munlock()";
+  }
+
+  return false;
+}
+
+void MemoryMapping::munlock(bool dontneed) {
+  if (!locked_) return;
+
+  size_t amountSucceeded = 0;
+  if (!memOpInChunks(::munlock, mapStart_, mapLength_, amountSucceeded)) {
+    PLOG(WARNING) << "munlock()";
+  }
+  if (mapLength_ && dontneed &&
+      ::madvise(mapStart_, mapLength_, MADV_DONTNEED)) {
+    PLOG(WARNING) << "madvise()";
+  }
+  locked_ = false;
+}
+
+void MemoryMapping::hintLinearScan() {
+  advise(MADV_SEQUENTIAL);
+}
+
+MemoryMapping::~MemoryMapping() {
+  if (mapLength_) {
+    size_t amountSucceeded = 0;
+    if (!memOpInChunks(::munmap, mapStart_, mapLength_, amountSucceeded)) {
+      PLOG(FATAL) << folly::format(
+        "munmap({}) failed at {}",
+        mapLength_, amountSucceeded).str();
+    }
+  }
+}
+
+void MemoryMapping::advise(int advice) const {
+  if (mapLength_ && ::madvise(mapStart_, mapLength_, advice)) {
+    PLOG(WARNING) << "madvise()";
+  }
+}
+
+WritableMemoryMapping::WritableMemoryMapping(File file, off_t offset, off_t length) {
+  init(std::move(file), offset, length, PROT_READ | PROT_WRITE, true);
+}
+
+}  // namespace folly
diff --git a/folly/MemoryMapping.h b/folly/MemoryMapping.h
new file mode 100644 (file)
index 0000000..244727b
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2013 Facebook, Inc.
+ *
+ * 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.
+ */
+
+#ifndef FOLLY_MEMORYMAPPING_H_
+#define FOLLY_MEMORYMAPPING_H_
+
+#include "folly/FBString.h"
+#include "folly/File.h"
+#include "folly/Range.h"
+#include <glog/logging.h>
+#include <boost/noncopyable.hpp>
+
+namespace folly {
+
+/**
+ * Maps files in memory (read-only).
+ *
+ * @author Tudor Bosman (tudorb@fb.com)
+ */
+class MemoryMapping : boost::noncopyable {
+ public:
+  /**
+   * Lock the pages in memory?
+   * TRY_LOCK  = try to lock, log warning if permission denied
+   * MUST_LOCK = lock, fail assertion if permission denied.
+   */
+  enum class LockMode {
+    TRY_LOCK,
+    MUST_LOCK
+  };
+  /**
+   * Map a portion of the file indicated by filename in memory, causing a CHECK
+   * failure on error.
+   *
+   * By default, map the whole file.  length=-1: map from offset to EOF.
+   * Unlike the mmap() system call, offset and length don't need to be
+   * page-aligned.  length is clipped to the end of the file if it's too large.
+   *
+   * The mapping will be destroyed (and the memory pointed-to by data() will
+   * likely become inaccessible) when the MemoryMapping object is destroyed.
+   */
+  explicit MemoryMapping(File file,
+                         off_t offset=0,
+                         off_t length=-1);
+
+  virtual ~MemoryMapping();
+
+  /**
+   * Lock the pages in memory
+   */
+  bool mlock(LockMode lock);
+
+  /**
+   * Unlock the pages.
+   * If dontneed is true, the kernel is instructed to release these pages
+   * (per madvise(MADV_DONTNEED)).
+   */
+  void munlock(bool dontneed=false);
+
+  /**
+   * Hint that these pages will be scanned linearly.
+   * madvise(MADV_SEQUENTIAL)
+   */
+  void hintLinearScan();
+
+  /**
+   * Advise the kernel about memory access.
+   */
+  void advise(int advice) const;
+
+  /**
+   * A bitwise cast of the mapped bytes as range of values. Only intended for
+   * use with POD or in-place usable types.
+   */
+  template<class T>
+  Range<const T*> asRange() const {
+    CHECK(mapStart_);
+    size_t count = data_.size() / sizeof(T);
+    return Range<const T*>(static_cast<const T*>(
+                             static_cast<const void*>(data_.data())),
+                           count);
+  }
+
+  /**
+   * A range of bytes mapped by this mapping.
+   */
+  Range<const uint8_t*> range() const {
+    return {data_.begin(), data_.end()};
+  }
+
+  /**
+   * Return the memory area where the file was mapped.
+   */
+  ByteRange data() const {
+    return range();
+  }
+
+  bool mlocked() const {
+    return locked_;
+  }
+
+ protected:
+  MemoryMapping();
+
+  void init(File file,
+            off_t offset, off_t length,
+            int prot,
+            bool grow);
+
+  File file_;
+  void* mapStart_;
+  off_t mapLength_;
+  bool locked_;
+  Range<uint8_t*> data_;
+};
+
+/**
+ * Maps files in memory for writing.
+ *
+ * @author Tom Jackson (tjackson@fb.com)
+ */
+class WritableMemoryMapping : public MemoryMapping {
+ public:
+  explicit WritableMemoryMapping(File file,
+                                 off_t offset = 0,
+                                 off_t length = -1);
+  /**
+   * A bitwise cast of the mapped bytes as range of mutable values. Only
+   * intended for use with POD or in-place usable types.
+   */
+  template<class T>
+  Range<T*> asWritableRange() const {
+    CHECK(mapStart_);
+    size_t count = data_.size() / sizeof(T);
+    return Range<T*>(static_cast<T*>(
+                       static_cast<void*>(data_.data())),
+                     count);
+  }
+
+  /**
+   * A range of mutable bytes mapped by this mapping.
+   */
+  Range<uint8_t*> writableRange() const {
+    return data_;
+  }
+};
+
+}  // namespace folly
+
+#endif /* FOLLY_MEMORYMAPPING_H_ */
diff --git a/folly/test/MemoryMappingTest.cpp b/folly/test/MemoryMappingTest.cpp
new file mode 100644 (file)
index 0000000..662c54c
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2013 Facebook, Inc.
+ *
+ * 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.
+ */
+
+#include <gtest/gtest.h>
+#include "folly/MemoryMapping.h"
+
+namespace folly {
+
+TEST(MemoryMapping, Basic) {
+  File f = File::temporary();
+  {
+    WritableMemoryMapping m(f.fd(), 0, sizeof(double));
+    double volatile* d = m.asWritableRange<double>().data();
+    *d = 37 * M_PI;
+  }
+  {
+    MemoryMapping m(f.fd(), 0, 3);
+    EXPECT_EQ(0, m.asRange<int>().size()); //not big enough
+  }
+  {
+    MemoryMapping m(f.fd(), 0, sizeof(double));
+    const double volatile* d = m.asRange<double>().data();
+    EXPECT_EQ(*d, 37 * M_PI);
+  }
+}
+
+TEST(MemoryMapping, DoublyMapped) {
+  File f = File::temporary();
+  // two mappings of the same memory, different addresses.
+  WritableMemoryMapping mw(f.fd(), 0, sizeof(double));
+  MemoryMapping mr(f.fd(), 0, sizeof(double));
+
+  double volatile* dw = mw.asWritableRange<double>().data();
+  const double volatile* dr = mr.asRange<double>().data();
+
+  // Show that it's truly the same value, even though the pointers differ
+  EXPECT_NE(dw, dr);
+  *dw = 42 * M_PI;
+  EXPECT_EQ(*dr, 42 * M_PI);
+  *dw = 43 * M_PI;
+  EXPECT_EQ(*dr, 43 * M_PI);
+}
+
+} // namespace folly