blob: 922e9473bfa09c12e6ea5a040d7b3f3801724261 [file] [log] [blame] [edit]
// Copyright 2016 The Fuchsia 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 <fcntl.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/status.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <zircon/device/vfs.h>
#include <zircon/time.h>
#include <algorithm>
#include <limits>
#include <memory>
#include <fbl/algorithm.h>
#include <fbl/auto_call.h>
#include <fbl/string_piece.h>
#include <fs/debug.h>
#include <fs/vfs_types.h>
#include <safemath/checked_math.h>
#include "zircon/assert.h"
#ifdef __Fuchsia__
#include <lib/fidl-utils/bind.h>
#include <zircon/syscalls.h>
#include <utility>
#include <fbl/auto_lock.h>
#endif
#include "src/storage/minfs/file.h"
#include "src/storage/minfs/minfs_private.h"
#include "src/storage/minfs/unowned_vmo_buffer.h"
#include "src/storage/minfs/vnode.h"
namespace minfs {
File::File(Minfs* fs) : VnodeMinfs(fs) {}
#ifdef __Fuchsia__
// AllocateAndCommitData does the following operations:
// - Allocates data blocks,
// - Frees old data blocks (if this were overwritten),
// - Issues data and metadata writes,
// - Updates inode to reflect new size and modification time.
// Writes or fragments of a write may change inode's size, block_count or
// file block table (dnum, inum, dinum).
void File::AllocateAndCommitData(std::unique_ptr<Transaction> transaction) {
// Calculate the maximum number of data blocks we can update within one transaction. This is
// the smallest between half the capacity of the writeback buffer, and the number of direct
// blocks needed to touch the maximum allowed number of indirect blocks.
const uint32_t max_direct_blocks =
kMinfsDirect + (kMinfsDirectPerIndirect * Vfs()->Limits().GetMaximumMetaDataBlocks());
const uint32_t max_writeback_blocks = static_cast<blk_t>(Vfs()->WritebackCapacity() / 2);
const uint32_t max_blocks = std::min(max_direct_blocks, max_writeback_blocks);
fbl::Array<blk_t> allocated_blocks(new blk_t[max_blocks], max_blocks);
// Iterate through all relative block ranges and acquire absolute blocks for each of them.
while (true) {
blk_t expected_blocks = allocation_state_.GetTotalPending();
ZX_ASSERT(expected_blocks <= max_blocks);
if (expected_blocks == 0) {
if (GetInode()->size != allocation_state_.GetNodeSize()) {
GetMutableInode()->size = allocation_state_.GetNodeSize();
ValidateVmoTail(GetInode()->size);
}
// Since we may have pending reservations from an expected update, reset the allocation
// state. This may happen if the same block range is allocated and de-allocated (e.g.
// written and truncated) before the state is resolved.
ZX_ASSERT(allocation_state_.GetNodeSize() == GetInode()->size);
allocation_state_.Reset(allocation_state_.GetNodeSize());
ZX_DEBUG_ASSERT(allocation_state_.IsEmpty());
break;
}
blk_t bno_start, bno_count;
ZX_ASSERT(allocation_state_.GetNextRange(&bno_start, &bno_count) == ZX_OK);
ZX_ASSERT(bno_count <= max_blocks);
// Since we reserved enough space ahead of time, this should not fail.
ZX_ASSERT(BlocksSwap(transaction.get(), bno_start, bno_count, &allocated_blocks[0]) == ZX_OK);
// Enqueue each data block one at a time, as they may not be contiguous on disk.
UnownedVmoBuffer buffer(vmo());
for (blk_t i = 0; i < bno_count; i++) {
storage::Operation operation = {
.type = storage::OperationType::kWrite,
.vmo_offset = bno_start + i,
.dev_offset = allocated_blocks[i] + Vfs()->Info().dat_block,
.length = 1,
};
transaction->EnqueueData(operation, &buffer);
}
// Since we are updating the file in "chunks", only update the on-disk inode size
// with the portion we've written so far.
blk_t last_byte = (bno_start + bno_count) * Vfs()->BlockSize();
ZX_ASSERT(last_byte <= fbl::round_up(allocation_state_.GetNodeSize(), Vfs()->BlockSize()));
if (last_byte > GetInode()->size && last_byte < allocation_state_.GetNodeSize()) {
// If we have written past the end of the recorded size but have not yet reached the
// allocated size, update the recorded size to the last byte written.
GetMutableInode()->size = last_byte;
} else if (allocation_state_.GetNodeSize() <= last_byte) {
// If we have just written to the allocated inode size, update the recorded size
// accordingly.
GetMutableInode()->size = allocation_state_.GetNodeSize();
}
ValidateVmoTail(GetInode()->size);
// In the future we could resolve on a per state (i.e. reservation) basis, but since swaps
// are currently only made within a single thread, for now it is okay to resolve
// everything.
transaction->PinVnode(fbl::RefPtr(this));
}
// At this point there should not be any pending allocations. Following code block
// prints and asserts it.
if (allocation_state_.GetTotalPending() != 0) {
FX_LOGS(ERROR) << "Found modified blocks(" << allocation_state_.GetTotalPending()
<< ") after marking them clean";
for (auto modified_blocks = allocation_state_.cbegin();
modified_blocks != allocation_state_.cend(); ++modified_blocks) {
FX_LOGS(ERROR) << " bitoff:" << modified_blocks->bitoff
<< " bitlen:" << modified_blocks->bitlen;
}
ZX_ASSERT(allocation_state_.GetTotalPending() == 0);
}
InodeSync(transaction.get(), DirtyCacheEnabled() ? kMxFsSyncDefault : kMxFsSyncMtime);
Vfs()->CommitTransaction(std::move(transaction));
}
zx_status_t File::BlocksSwap(Transaction* transaction, blk_t start, blk_t count, blk_t* bnos) {
if (count == 0)
return ZX_OK;
VnodeMapper mapper(this);
VnodeIterator iterator;
zx_status_t status = iterator.Init(&mapper, transaction, start);
if (status != ZX_OK)
return status;
while (count > 0) {
const blk_t file_block = static_cast<blk_t>(iterator.file_block());
ZX_DEBUG_ASSERT(allocation_state_.IsPending(file_block));
blk_t old_block = iterator.Blk();
// TODO(fxbug.dev/51587): A value of zero for the block pointer has special meaning: the block
// is sparse or unmapped. We should add something for this magic constant and fix all places
// that currently hard code zero.
if (old_block == 0) {
GetMutableInode()->block_count++;
}
// For copy-on-write, swap the block out if it's a data block.
blk_t new_block = old_block;
Vfs()->BlockSwap(transaction, old_block, &new_block);
zx_status_t status = iterator.SetBlk(new_block);
if (status != ZX_OK)
return status;
*bnos++ = new_block;
bool cleared = allocation_state_.ClearPending(file_block, old_block != 0);
ZX_DEBUG_ASSERT(cleared);
// We have cleared pending bit for the block. Update the accounding for the
// dirty block.
Vfs()->SubtractDirtyBytes(Vfs()->BlockSize(), old_block != 0);
--count;
status = iterator.Advance();
if (status != ZX_OK)
return status;
}
return iterator.Flush();
}
#endif
void File::UpdateModificationTime() {
zx_time_t cur_time = GetTimeUTC();
GetMutableInode()->modify_time = cur_time;
}
blk_t File::GetBlockCount() const {
#ifdef __Fuchsia__
return GetInode()->block_count + allocation_state_.GetNewPending();
#else
return GetInode()->block_count;
#endif
}
uint64_t File::GetSize() const {
#ifdef __Fuchsia__
return allocation_state_.GetNodeSize();
#endif
return GetInode()->size;
}
void File::SetSize(uint32_t new_size) {
#ifdef __Fuchsia__
allocation_state_.SetNodeSize(new_size);
#else
GetMutableInode()->size = new_size;
#endif
}
void File::AcquireWritableBlock(Transaction* transaction, blk_t local_bno, blk_t old_bno,
blk_t* out_bno) {
bool using_new_block = (old_bno == 0);
#ifdef __Fuchsia__
allocation_state_.SetPending(local_bno, !using_new_block);
[[maybe_unused]] auto unused_ = Vfs()->AddDirtyBytes(Vfs()->BlockSize(), !using_new_block);
#else
if (using_new_block) {
Vfs()->BlockNew(transaction, out_bno);
GetMutableInode()->block_count++;
} else {
*out_bno = old_bno;
}
#endif
}
void File::DeleteBlock(PendingWork* transaction, blk_t local_bno, blk_t old_bno, bool indirect) {
// If we found a block that was previously allocated, delete it.
if (old_bno != 0) {
transaction->DeallocateBlock(old_bno);
GetMutableInode()->block_count--;
}
#ifdef __Fuchsia__
if (!indirect) {
if (allocation_state_.IsPending(local_bno)) {
Vfs()->SubtractDirtyBytes(Vfs()->BlockSize(), old_bno != 0);
}
// Remove this block from the pending allocation map in case it's set so we do not
// proceed to allocate a new block.
allocation_state_.ClearPending(local_bno, old_bno != 0);
}
#endif
}
#ifdef __Fuchsia__
void File::IssueWriteback(Transaction* transaction, blk_t vmo_offset, blk_t dev_offset,
blk_t block_count) {
// This is a no-op. The blocks are swapped later.
}
bool File::HasPendingAllocation(blk_t vmo_offset) {
return allocation_state_.IsPending(vmo_offset);
}
void File::CancelPendingWriteback() {
// Drop all pending writes, revert the size of the inode to the "pre-pending-write" size.
allocation_state_.Reset(GetInode()->size);
}
#endif
zx_status_t File::CanUnlink() const { return ZX_OK; }
fs::VnodeProtocolSet File::GetProtocols() const { return fs::VnodeProtocol::kFile; }
zx_status_t File::Read(void* data, size_t len, size_t off, size_t* out_actual) {
TRACE_DURATION("minfs", "File::Read", "ino", GetIno(), "len", len, "off", off);
FX_LOGS(DEBUG) << "minfs_read() vn=" << this << "(#" << GetIno() << ") len=" << len
<< " off=" << off;
fs::Ticker ticker(Vfs()->StartTicker());
auto get_metrics = fbl::MakeAutoCall(
[&ticker, &out_actual, this]() { Vfs()->UpdateReadMetrics(*out_actual, ticker.End()); });
Transaction transaction(Vfs());
return ReadInternal(&transaction, data, len, off, out_actual);
}
zx::status<uint32_t> File::GetRequiredBlockCount(size_t offset, size_t length) {
zx::status<blk_t> uncached = zx::error(ZX_ERR_INVALID_ARGS);
uncached = ::minfs::GetRequiredBlockCount(offset, length, Vfs()->BlockSize());
if (!DirtyCacheEnabled()) {
return uncached;
}
if (uncached.is_error()) {
return uncached;
}
return GetRequiredBlockCountForDirtyCache(offset, length, uncached.value());
}
zx::status<> File::CheckAndFlush(bool is_truncate, size_t length, size_t offset) {
auto status = ShouldFlush(is_truncate, length, offset);
if (status.is_error()) {
return zx::error(status.error_value());
}
if (!status.value()) {
return zx::ok();
}
return FlushCachedWrites();
}
zx::status<std::unique_ptr<Transaction>> File::GetTransaction(uint32_t reserve_blocks) {
std::unique_ptr<Transaction> transaction;
std::unique_ptr<CachedBlockTransaction> cached_transaction;
{
std::lock_guard<std::mutex> lock(cached_transaction_lock_);
cached_transaction = std::move(cached_transaction_);
if (!DirtyCacheEnabled()) {
ZX_ASSERT(cached_transaction == nullptr);
}
}
zx_status_t status;
if (!DirtyCacheEnabled() || cached_transaction == nullptr) {
if ((status = Vfs()->BeginTransaction(0, reserve_blocks, &transaction)) != ZX_OK) {
return zx::error(status);
}
} else {
if ((status = Vfs()->ContinueTransaction(reserve_blocks, std::move(cached_transaction),
&transaction)) != ZX_OK) {
// Failure here means that we ran out of space. Force flush pending writes
// and return the failure.
if (transaction != nullptr) {
[[maybe_unused]] auto error =
FlushTransaction(std::move(transaction), /*force_flush=*/true).status_value();
}
return zx::error(status);
}
}
return zx::ok(std::move(transaction));
}
zx_status_t File::Write(const void* data, size_t len, size_t offset, size_t* out_actual) {
TRACE_DURATION("minfs", "File::Write", "ino", GetIno(), "len", len, "off", offset);
FX_LOGS(DEBUG) << "minfs_write() vn=" << this << "(#" << GetIno() << ") len=" << len
<< " off=" << offset;
*out_actual = 0;
if (len == 0) {
return ZX_OK;
}
fs::Ticker ticker(Vfs()->StartTicker());
auto get_metrics = fbl::MakeAutoCall(
[&ticker, &out_actual, this]() { Vfs()->UpdateWriteMetrics(*out_actual, ticker.End()); });
auto new_size_or = safemath::CheckAdd(offset, len);
if (!new_size_or.IsValid() || new_size_or.ValueOrDie() > kMinfsMaxFileSize) {
return ZX_ERR_FILE_BIG;
}
// If this file's pending blocks have crossed a limit or if there are no free blocks in the
// filesystem, try to flush before we proceed.
if (auto status = CheckAndFlush(false, len, offset); status.is_error()) {
return status.error_value();
}
// Calculate maximum number of blocks to reserve for this write operation.
auto reserve_blocks_or = GetRequiredBlockCount(offset, len);
if (reserve_blocks_or.is_error()) {
return reserve_blocks_or.error_value();
}
size_t reserve_blocks = reserve_blocks_or.value();
zx_status_t status;
auto transaction_or = GetTransaction(reserve_blocks);
if (transaction_or.is_error()) {
return transaction_or.error_value();
}
std::unique_ptr<Transaction> transaction = std::move(transaction_or.value());
// We mark block that has writes pending only after we have enough blocks reserved through
// BeginTransaction or through ContinueTransaction.
if (DirtyCacheEnabled()) {
if (auto status = MarkRequiredBlocksPending(offset, len); status.is_error()) {
return status.error_value();
}
}
status =
WriteInternal(transaction.get(), static_cast<const uint8_t*>(data), len, offset, out_actual);
if (status != ZX_OK) {
return status;
}
if (*out_actual == 0) {
return ZX_OK;
}
// If anything was written, enqueue operations allocated within WriteInternal.
UpdateModificationTime();
return FlushTransaction(std::move(transaction)).status_value();
}
zx_status_t File::Append(const void* data, size_t len, size_t* out_end, size_t* out_actual) {
zx_status_t status = Write(data, len, GetSize(), out_actual);
*out_end = GetSize();
return status;
}
zx_status_t File::Truncate(size_t len) {
TRACE_DURATION("minfs", "File::Truncate");
if (len > kMinfsMaxFileSize) {
return ZX_ERR_INVALID_ARGS;
}
// TODO(unknown): Following can be optimized.
// - do not flush part of the file that will be truncated.
// - conditionally flush unaffected part if necessary.
if (auto status = FlushCachedWrites(); status.is_error()) {
return status.error_value();
}
fs::Ticker ticker(Vfs()->StartTicker());
auto get_metrics =
fbl::MakeAutoCall([&ticker, this] { Vfs()->UpdateTruncateMetrics(ticker.End()); });
std::unique_ptr<Transaction> transaction;
// Due to file copy-on-write, up to 1 new (data) block may be required.
size_t reserve_blocks = 1;
zx_status_t status;
if ((status = Vfs()->BeginTransaction(0, reserve_blocks, &transaction)) != ZX_OK) {
return status;
}
if ((status = TruncateInternal(transaction.get(), len)) != ZX_OK) {
return status;
}
// Force sync the inode to persistent storage: although our data blocks will be allocated
// later, the act of truncating may have allocated indirect blocks.
//
// Ensure our inode is consistent with that metadata.
UpdateModificationTime();
auto result = FlushTransaction(std::move(transaction), true);
ZX_ASSERT(result.is_ok());
return ZX_OK;
}
} // namespace minfs