blob: 45c4e9e4cf5be314e62bb9760f60c04e74e5012e [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "croslog/file_map_reader.h"
#include <algorithm>
#include <unistd.h>
#include "croslog/log_line_reader.h"
#include <base/check.h>
#include <base/check_op.h>
namespace croslog {
namespace {
// Size of chunk: 4 MB. This must be multiple of the page size.
// This is not constant for testing.
static uint64_t g_chunk_size_in_bytes = 4 * 1024 * 1024;
// Number of chunks to be allocated: 128 MB.
// This is not constant for testing.
static uint64_t g_allocate_chunk_size = 32;
// A dummy empty buffer. Used as an empty buffer.
static const uint8_t kEmptyBuffer[] = {};
std::pair<uint64_t, uint64_t> CalculateAppropriateBufferRange(
uint64_t request_pos, uint64_t request_length, uint64_t file_size) {
// Ensure the request parameters are valid. The caller must take care of it.
DCHECK_LE(request_pos + request_length, file_size);
// Assumption of this logic: the requested length is small enough to avoid
// request size from overruning of the allocated buffer range.
// The current limit is |g_chunk_size_in_bytes|, and this value is the limit
// in the case of g_allocate_chunk_size == 2. This can be larger when
// |g_allocate_chunk_size| > 2 (but currently it is not supported).
DCHECK_LE(request_length, g_chunk_size_in_bytes);
// Assumption of this logic: |g_allocate_chunk_size| must be larger than 2 to
// allocate a buffer across two chunks.
DCHECK_LE(2u, g_allocate_chunk_size);
// Assumption of this logic: |g_allocate_chunk_size| must be multiple of the
// page size, otherwise mmap fails.
DCHECK_EQ(0, g_chunk_size_in_bytes % sysconf(_SC_PAGE_SIZE));
// Calculate the aligned range of buffer with margins after and before.
// Each margin is about ((g_allocate_chunk_size / 2) * g_chunk_size_in_bytes)
// MB.
uint64_t buffer_length = g_allocate_chunk_size * g_chunk_size_in_bytes;
uint64_t buffer_start_pos =
request_pos - (request_pos % g_chunk_size_in_bytes);
// Add a margin before the requested range.
buffer_start_pos -=
std::min(buffer_start_pos,
(g_allocate_chunk_size / 2 - 1) * g_chunk_size_in_bytes);
// Adjust the length not to overrun the file size.
if ((buffer_start_pos + buffer_length) > file_size)
buffer_length = file_size - buffer_start_pos;
// Ensure the calculated range is valid.
DCHECK_LE(buffer_start_pos, request_pos);
DCHECK_LE(request_pos + request_length, buffer_start_pos + buffer_length);
return std::make_pair(buffer_start_pos, buffer_length);
}
} // anonymous namespace
// ============================================================================
// FileMapReaderDelegate declaration:
// Delegate to implement the logic to map the file or something compatible.
class FileMapReaderDelegate {
public:
virtual ~FileMapReaderDelegate() = default;
// Returns the current file size on the file system.
virtual int64_t GetCurrentFileSize() = 0;
// Do mmap and returns the allocated memory. Allocated memory is bigger than
// the requested size with mergins and includes the requested range.
// - On error, nullptr is returned
// - On success, an allocated memory is owned by the delegate
// An allocated memory is released on next DoMap call or dtor.
virtual std::pair<const uint8_t*, uint64_t> DoMap(int64_t request_pos,
int64_t request_length) = 0;
};
// ============================================================================
// FileMapReaderDelegateImpl implementation:
class FileMapReaderDelegateImpl : public FileMapReaderDelegate {
public:
explicit FileMapReaderDelegateImpl(base::File file)
: file_(std::move(file)) {}
FileMapReaderDelegateImpl(const FileMapReaderDelegateImpl&) = delete;
FileMapReaderDelegateImpl& operator=(const FileMapReaderDelegateImpl&) =
delete;
~FileMapReaderDelegateImpl() override = default;
int64_t GetCurrentFileSize() override { return file_.GetLength(); }
std::pair<const uint8_t*, uint64_t> DoMap(int64_t request_pos,
int64_t request_length) override {
DCHECK_GE(request_pos, 0);
DCHECK_GE(request_length, 0);
if (request_length == 0) {
// Returning an empty buffer without (re)mmapping, since mmap of an empty
// file fails.
mmap_.reset();
return std::make_pair(static_cast<const uint8_t*>(kEmptyBuffer), 0);
}
// Checks if the given range is invalid. It must be verified in the caller.
// Or probably a TOCTOU race has been happened until that check. In this
// case, the mmap bellow will fail.
DCHECK_LE(request_pos + request_length, file_.GetLength());
// Maps the file.
base::MemoryMappedFile::Region mmap_region;
mmap_region.offset = request_pos;
mmap_region.size = request_length;
mmap_ = std::make_unique<base::MemoryMappedFile>();
bool mmap_result = mmap_->Initialize(file_.Duplicate(), mmap_region);
if (!mmap_result) {
LOG(ERROR) << "Doing mmap failed.";
// Returning an empty buffer without (re)mmapping, since mmap of an empty
// file fails.
mmap_.reset();
return std::make_pair(nullptr, 0);
}
return std::make_pair(mmap_->data(), mmap_->length());
}
private:
base::File file_;
std::unique_ptr<base::MemoryMappedFile> mmap_;
};
// ============================================================================
// FileMapReaderDelegateImplMemoryReader implementation:
// This class maps and reads the logs on memory buffer. Used only in tests.
class FileMapReaderDelegateImplMemoryReader : public FileMapReaderDelegate {
public:
FileMapReaderDelegateImplMemoryReader(const uint8_t* buffer, uint64_t length)
: buffer_(buffer), buffer_length_(length) {}
int64_t GetCurrentFileSize() override { return buffer_length_; }
FileMapReaderDelegateImplMemoryReader(
const FileMapReaderDelegateImplMemoryReader&) = delete;
FileMapReaderDelegateImplMemoryReader& operator=(
const FileMapReaderDelegateImplMemoryReader&) = delete;
std::pair<const uint8_t*, uint64_t> DoMap(int64_t request_pos,
int64_t request_length) override {
DCHECK_GE(request_pos, 0);
DCHECK_GE(request_length, 0);
int64_t buffer_remaining_length =
std::max(buffer_length_ - request_pos, INT64_C(0));
return std::make_pair(buffer_ + request_pos,
std::min(request_length, buffer_remaining_length));
}
private:
const uint8_t* const buffer_ = nullptr;
const int64_t buffer_length_ = 0;
};
// ============================================================================
// FileMapReader::MappedBuffer implementation:
FileMapReader::MappedBuffer::MappedBuffer(const uint8_t* buffer,
uint64_t buffer_start,
uint64_t buffer_length)
: buffer_(buffer),
buffer_start_(buffer_start),
buffer_length_(buffer_length) {}
FileMapReader::MappedBuffer::~MappedBuffer() = default;
std::pair<const uint8_t*, uint64_t> FileMapReader::MappedBuffer::GetBuffer(
uint64_t start_pos, uint64_t length) const {
DCHECK(valid());
DCHECK_LE(buffer_start_, start_pos);
DCHECK_LE(start_pos + length, buffer_start_ + buffer_length_);
const uint8_t* buffer = buffer_ + (start_pos - buffer_start_);
const uint64_t buffer_remaining_length =
buffer_length_ - (start_pos - buffer_start_);
return std::make_pair(buffer, std::min(length, buffer_remaining_length));
}
// ============================================================================
// FileMapReader implementation:
// static
std::unique_ptr<FileMapReader> FileMapReader::CreateReader(base::File file) {
return std::make_unique<FileMapReader>(
std::make_unique<FileMapReaderDelegateImpl>(std::move(file)));
}
// static
uint64_t FileMapReader::GetChunkSizeInBytes() {
return g_chunk_size_in_bytes;
}
// static
std::unique_ptr<FileMapReader>
FileMapReader::CreateFileMapReaderDelegateImplMemoryReaderForTest(
const uint8_t* buffer, uint64_t length) {
return std::make_unique<FileMapReader>(
std::make_unique<FileMapReaderDelegateImplMemoryReader>(buffer, length));
}
// static
void FileMapReader::SetBlockSizesForTest(uint64_t chunk_size_in_bytes,
uint32_t allocate_chunks) {
CHECK_EQ(0, chunk_size_in_bytes % sysconf(_SC_PAGE_SIZE));
CHECK_LE(2u, allocate_chunks);
g_chunk_size_in_bytes = chunk_size_in_bytes;
g_allocate_chunk_size = allocate_chunks;
}
FileMapReader::FileMapReader(std::unique_ptr<FileMapReaderDelegate> delegate)
: delegate_(std::move(delegate)),
file_size_(delegate_->GetCurrentFileSize()) {
CHECK_GE(file_size_, 0);
}
FileMapReader::~FileMapReader() = default;
void FileMapReader::ApplyFileSizeExpansion() {
const int64_t current_file_size = delegate_->GetCurrentFileSize();
CHECK_GE(current_file_size, 0);
if (current_file_size == file_size_)
return;
// We assume the log files never shrink.
CHECK_LT(file_size_, current_file_size);
file_size_ = current_file_size;
}
std::unique_ptr<FileMapReader::MappedBuffer> FileMapReader::MapBuffer(
uint64_t request_pos, uint64_t request_length) {
DCHECK_LE(request_pos + request_length, file_size_);
// Ensure that the previous mapped buffer has already been freed.
DCHECK(!instantiated_mapped_buffer_);
// Reuse the previous mapped buffer if the request range is contained by the
// previous mapped range.
if ((buffer_ != nullptr) && (buffer_start_ <= request_pos) &&
((request_pos + request_length) <= (buffer_start_ + buffer_length_))) {
return CreateMappedBuffer();
}
// Just check if the log files never shrink just in case. But we don't update
// the file size here even if it's updated, for logic simplicity.
CHECK_LE(file_size_, delegate_->GetCurrentFileSize());
// Calculate the aligned range with enough margin.
const std::pair<uint64_t, uint64_t> region =
CalculateAppropriateBufferRange(request_pos, request_length, file_size_);
const uint64_t buffer_start_pos = region.first;
const std::pair<const uint8_t*, uint64_t> buffer =
delegate_->DoMap(buffer_start_pos, region.second);
buffer_ = buffer.first;
buffer_start_ = buffer_start_pos;
buffer_length_ = buffer.second;
// Checks if mmap failed. It fails when the file was shrunk from the
// previous file size check.
if (buffer.first != nullptr) {
// mmap succeeds.
DCHECK_LE(buffer_start_, request_pos);
DCHECK_LE(request_pos + request_length, buffer_start_ + buffer_length_);
}
return CreateMappedBuffer();
}
std::unique_ptr<FileMapReader::MappedBuffer>
FileMapReader::CreateMappedBuffer() {
// Ensure that the previous mapped buffer has already been freed.
// Note: The current implementation allows only one mapped buffer at the same
// time.
DCHECK(!instantiated_mapped_buffer_);
// Doesn't use std::make_unique due to the private constructor.
auto mapped_buffer = std::unique_ptr<MappedBuffer>(
new MappedBuffer(buffer_, buffer_start_, buffer_length_));
instantiated_mapped_buffer_ = mapped_buffer->weak_factory_.GetWeakPtr();
return mapped_buffer;
}
} // namespace croslog