| // 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" |
| |
| 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 |