| // Copyright 2022 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 "src/storage/f2fs/f2fs.h" |
| |
| namespace f2fs { |
| |
| Page::Page(FileCache *file_cache, pgoff_t index) : file_cache_(file_cache), index_(index) {} |
| |
| VnodeF2fs &Page::GetVnode() const { return file_cache_->GetVnode(); } |
| |
| VmoManager &Page::GetVmoManager() const { return file_cache_->GetVmoManager(); } |
| |
| FileCache &Page::GetFileCache() const { return *file_cache_; } |
| |
| Page::~Page() { |
| ZX_DEBUG_ASSERT(IsWriteback() == false); |
| ZX_DEBUG_ASSERT(InTreeContainer() == false); |
| ZX_DEBUG_ASSERT(InListContainer() == false); |
| ZX_DEBUG_ASSERT(IsDirty() == false); |
| ZX_DEBUG_ASSERT(IsLocked() == false); |
| } |
| |
| void Page::RecyclePage() { |
| // Since active Pages are evicted only when having strong references, |
| // it is safe to call InContainer(). |
| if (InTreeContainer()) { |
| ZX_ASSERT(VmoOpUnlock() == ZX_OK); |
| file_cache_->Downgrade(this); |
| } else { |
| delete this; |
| } |
| } |
| |
| F2fs *Page::fs() const { return file_cache_->fs(); } |
| |
| bool Page::SetDirty() { |
| SetUptodate(); |
| // No need to make dirty Pages for orphan files. |
| if (!file_cache_->IsOrphan() && |
| !flags_[static_cast<uint8_t>(PageFlag::kPageDirty)].test_and_set(std::memory_order_acquire)) { |
| VnodeF2fs &vnode = GetVnode(); |
| SuperblockInfo &superblock_info = fs()->GetSuperblockInfo(); |
| vnode.SetDirty(); |
| vnode.IncreaseDirtyPageCount(); |
| if (vnode.IsNode()) { |
| superblock_info.IncreasePageCount(CountType::kDirtyNodes); |
| } else if (vnode.IsDir()) { |
| superblock_info.IncreasePageCount(CountType::kDirtyDents); |
| superblock_info.IncreaseDirtyDir(); |
| } else if (vnode.IsMeta()) { |
| superblock_info.IncreasePageCount(CountType::kDirtyMeta); |
| superblock_info.SetDirty(); |
| } else { |
| superblock_info.IncreasePageCount(CountType::kDirtyData); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| bool Page::ClearDirtyForIo() { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| VnodeF2fs &vnode = GetVnode(); |
| if (IsDirty()) { |
| ClearFlag(PageFlag::kPageDirty); |
| vnode.DecreaseDirtyPageCount(); |
| SuperblockInfo &superblock_info = fs()->GetSuperblockInfo(); |
| if (vnode.IsNode()) { |
| superblock_info.DecreasePageCount(CountType::kDirtyNodes); |
| } else if (vnode.IsDir()) { |
| superblock_info.DecreasePageCount(CountType::kDirtyDents); |
| superblock_info.DecreaseDirtyDir(); |
| } else if (vnode.IsMeta()) { |
| superblock_info.DecreasePageCount(CountType::kDirtyMeta); |
| } else { |
| superblock_info.DecreasePageCount(CountType::kDirtyData); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| zx_status_t Page::GetPage() { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| auto committed_or = VmoOpLock(); |
| ZX_ASSERT(committed_or.is_ok()); |
| if (!committed_or.value()) { |
| ZX_DEBUG_ASSERT(!IsDirty()); |
| ZX_DEBUG_ASSERT(!IsWriteback()); |
| ClearUptodate(); |
| } |
| return committed_or.status_value(); |
| } |
| |
| void Page::Invalidate() { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| ClearDirtyForIo(); |
| ClearColdData(); |
| ClearUptodate(); |
| } |
| |
| bool Page::IsUptodate() const { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| if (!TestFlag(PageFlag::kPageUptodate)) { |
| return false; |
| } |
| return !GetVmoManager().IsPaged() || IsDirty() || IsWriteback(); |
| } |
| |
| bool Page::SetUptodate() { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| return SetFlag(PageFlag::kPageUptodate); |
| } |
| |
| void Page::ClearUptodate() { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| // block_addr_ is valid only when the uptodate flag is set. |
| block_addr_ = kNullAddr; |
| ClearFlag(PageFlag::kPageUptodate); |
| } |
| |
| void Page::WaitOnWriteback() { |
| if (IsWriteback()) { |
| fs()->ScheduleWriter(); |
| } |
| WaitOnFlag(PageFlag::kPageWriteback); |
| } |
| |
| bool Page::SetWriteback() { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| bool ret = SetFlag(PageFlag::kPageWriteback); |
| if (!ret) { |
| fs()->GetSuperblockInfo().IncreasePageCount(CountType::kWriteback); |
| } |
| return ret; |
| } |
| |
| void Page::ClearWriteback() { |
| if (IsWriteback()) { |
| fs()->GetSuperblockInfo().DecreasePageCount(CountType::kWriteback); |
| ClearFlag(PageFlag::kPageWriteback); |
| WakeupFlag(PageFlag::kPageWriteback); |
| } |
| } |
| |
| bool Page::SetCommit() { return SetFlag(PageFlag::kPageCommit); } |
| |
| void Page::ClearCommit() { ClearFlag(PageFlag::kPageCommit); } |
| |
| bool Page::SetSync() { return SetFlag(PageFlag::kPageSync); } |
| |
| void Page::ClearSync() { ClearFlag(PageFlag::kPageSync); } |
| |
| void Page::SetColdData() { |
| ZX_DEBUG_ASSERT(IsLocked()); |
| ZX_DEBUG_ASSERT(!IsWriteback()); |
| SetFlag(PageFlag::kPageColdData); |
| } |
| |
| zx::result<> Page::SetBlockAddr(block_t addr) { |
| if (IsLocked() && TestFlag(PageFlag::kPageUptodate)) { |
| block_addr_ = addr; |
| return zx::ok(); |
| } |
| return zx::error(ZX_ERR_UNAVAILABLE); |
| } |
| |
| bool Page::ClearColdData() { |
| if (IsColdData()) { |
| ClearFlag(PageFlag::kPageColdData); |
| return true; |
| } |
| return false; |
| } |
| |
| zx_status_t Page::VmoOpUnlock(bool evict) { |
| ZX_DEBUG_ASSERT(InTreeContainer()); |
| // |evict| can be true only when the Page is clean or subject to invalidation. |
| if (((!IsDirty() && !file_cache_->IsOrphan()) || evict) && IsVmoLocked()) { |
| WaitOnWriteback(); |
| ClearFlag(PageFlag::kPageVmoLocked); |
| return GetVmoManager().UnlockVmo(index_, evict); |
| } |
| return ZX_OK; |
| } |
| |
| zx::result<bool> Page::VmoOpLock() { |
| ZX_DEBUG_ASSERT(InTreeContainer()); |
| ZX_DEBUG_ASSERT(IsLocked()); |
| if (!SetFlag(PageFlag::kPageVmoLocked)) { |
| return GetVmoManager().CreateAndLockVmo(index_, addr_ ? nullptr : &addr_); |
| } |
| return zx::ok(true); |
| } |
| |
| zx_status_t Page::Read(void *data, uint64_t offset, size_t len) { |
| if (unlikely(offset + len > Size())) { |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| if (uint8_t *addr = GetAddress<uint8_t>(); addr) { |
| std::memcpy(data, &addr[offset], len); |
| return ZX_OK; |
| } |
| return GetVmoManager().Read(data, index_ * Size() + offset, len); |
| } |
| |
| zx_status_t Page::Write(const void *data, uint64_t offset, size_t len) { |
| if (unlikely(offset + len > Size())) { |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| if (uint8_t *addr = GetAddress<uint8_t>(); addr) { |
| std::memcpy(&addr[offset], data, len); |
| return ZX_OK; |
| } |
| return GetVmoManager().Write(data, index_ * Size() + offset, len); |
| } |
| |
| void LockedPage::Zero(size_t start, size_t end) const { |
| if (start < end && end <= Page::Size()) { |
| page_->Write(kZeroBuffer_.data(), start, end - start); |
| } |
| } |
| |
| zx::result<> LockedPage::SetVmoDirty() { |
| if (!page_->IsDirty() && !page_->IsWriteback() && page_->GetVmoManager().IsPaged()) { |
| size_t start_offset = safemath::CheckMul(page_->GetKey(), kBlockSize).ValueOrDie(); |
| if (auto dirty_or = page_->GetVmoManager().DirtyPages(*page_->fs()->vfs(), start_offset, |
| start_offset + kBlockSize); |
| dirty_or.is_error()) { |
| ZX_DEBUG_ASSERT(dirty_or.error_value() == ZX_ERR_NOT_FOUND); |
| return dirty_or.take_error(); |
| } |
| } |
| return zx::ok(); |
| } |
| |
| bool LockedPage::SetDirty() { return page_->SetDirty(); } |
| |
| FileCache::FileCache(VnodeF2fs *vnode, VmoManager *vmo_manager) |
| : vnode_(vnode), vmo_manager_(vmo_manager) {} |
| |
| FileCache::~FileCache() { |
| Reset(); |
| { |
| std::lock_guard tree_lock(tree_lock_); |
| ZX_DEBUG_ASSERT(page_tree_.is_empty()); |
| } |
| } |
| |
| void FileCache::Downgrade(Page *raw_page) { |
| // We can downgrade multiple Pages simultaneously. |
| fs::SharedLock tree_lock(tree_lock_); |
| // Resurrect |this|. |
| raw_page->ResurrectRef(); |
| fbl::RefPtr<Page> page = fbl::ImportFromRawPtr(raw_page); |
| // Leak it to keep alive in FileCache. |
| [[maybe_unused]] auto leak = fbl::ExportToRawPtr(&page); |
| raw_page->ClearActive(); |
| recycle_cvar_.notify_all(); |
| } |
| |
| F2fs *FileCache::fs() const { return GetVnode().fs(); } |
| |
| zx_status_t FileCache::AddPageUnsafe(const fbl::RefPtr<Page> &page) { |
| if (page->InTreeContainer()) { |
| return ZX_ERR_ALREADY_EXISTS; |
| } |
| page_tree_.insert(page.get()); |
| return ZX_OK; |
| } |
| |
| zx::result<std::vector<LockedPage>> FileCache::GetPages(const pgoff_t start, const pgoff_t end) { |
| std::lock_guard tree_lock(tree_lock_); |
| std::vector<LockedPage> locked_pages(end - start); |
| auto exist_pages = GetLockedPagesUnsafe(start, end); |
| uint32_t exist_pages_index = 0, count = 0; |
| for (pgoff_t index = start; index < end; ++index, ++count) { |
| if (exist_pages_index < exist_pages.size() && |
| exist_pages[exist_pages_index]->GetKey() == index) { |
| locked_pages[count] = std::move(exist_pages[exist_pages_index]); |
| ++exist_pages_index; |
| } else { |
| locked_pages[count] = GetNewPage(index); |
| } |
| |
| if (auto ret = locked_pages[count]->GetPage(); ret != ZX_OK) { |
| return zx::error(ret); |
| } |
| } |
| |
| return zx::ok(std::move(locked_pages)); |
| } |
| |
| zx::result<std::vector<LockedPage>> FileCache::FindPages(const pgoff_t start, const pgoff_t end) { |
| std::lock_guard tree_lock(tree_lock_); |
| auto pages = GetLockedPagesUnsafe(start, end); |
| for (auto &page : pages) { |
| if (auto ret = page->GetPage(); ret != ZX_OK) { |
| return zx::error(ret); |
| } |
| } |
| |
| return zx::ok(std::move(pages)); |
| } |
| |
| zx::result<std::vector<LockedPage>> FileCache::GetPages(const std::vector<pgoff_t> &page_offsets) { |
| std::lock_guard tree_lock(tree_lock_); |
| if (page_offsets.empty()) { |
| return zx::ok(std::vector<LockedPage>(0)); |
| } |
| |
| auto locked_pages = GetLockedPagesUnsafe(page_offsets); |
| uint32_t count = 0; |
| for (pgoff_t index : page_offsets) { |
| if (index != kInvalidPageOffset) { |
| if (!locked_pages[count]) { |
| locked_pages[count] = GetNewPage(index); |
| } |
| |
| if (zx_status_t ret = locked_pages[count]->GetPage(); ret != ZX_OK) { |
| return zx::error(ret); |
| } |
| } |
| ++count; |
| } |
| |
| return zx::ok(std::move(locked_pages)); |
| } |
| |
| LockedPage FileCache::GetNewPage(const pgoff_t index) { |
| fbl::RefPtr<Page> page; |
| if (GetVnode().IsNode()) { |
| page = fbl::MakeRefCounted<NodePage>(this, index); |
| } else { |
| page = fbl::MakeRefCounted<Page>(this, index); |
| } |
| ZX_ASSERT(AddPageUnsafe(page) == ZX_OK); |
| auto locked_page = LockedPage(std::move(page)); |
| locked_page->SetActive(); |
| return locked_page; |
| } |
| |
| zx_status_t FileCache::GetPage(const pgoff_t index, LockedPage *out) { |
| LockedPage locked_page; |
| std::lock_guard tree_lock(tree_lock_); |
| auto locked_page_or = GetPageUnsafe(index); |
| if (locked_page_or.is_error()) { |
| locked_page = GetNewPage(index); |
| } else { |
| locked_page = std::move(*locked_page_or); |
| } |
| if (auto ret = locked_page->GetPage(); ret != ZX_OK) { |
| return ret; |
| } |
| *out = std::move(locked_page); |
| return ZX_OK; |
| } |
| |
| zx_status_t FileCache::FindPage(const pgoff_t index, fbl::RefPtr<Page> *out) { |
| std::lock_guard tree_lock(tree_lock_); |
| auto locked_page_or = GetPageUnsafe(index); |
| if (locked_page_or.is_error()) { |
| return locked_page_or.error_value(); |
| } |
| if (auto ret = (*locked_page_or)->GetPage(); ret != ZX_OK) { |
| return ret; |
| } |
| *out = (*locked_page_or).release(); |
| return ZX_OK; |
| } |
| |
| zx::result<LockedPage> FileCache::GetLockedPageFromRawUnsafe(Page *raw_page) { |
| auto page = fbl::MakeRefPtrUpgradeFromRaw(raw_page, tree_lock_); |
| if (page == nullptr) { |
| // Wait for it to be resurrected when it is being recycled. |
| recycle_cvar_.wait(tree_lock_); |
| return zx::error(ZX_ERR_SHOULD_WAIT); |
| } |
| // Try to make LockedPage from |page|. |
| // If |page| has been already locked, it waits for it to be unlock and returns ZX_ERR_SHOULD_WAIT. |
| auto locked_page_or = GetLockedPage(std::move(page)); |
| if (locked_page_or.is_error()) { |
| return zx::error(ZX_ERR_SHOULD_WAIT); |
| } |
| // Here, Page::ref_count should not be less than one. |
| return zx::ok(std::move(*locked_page_or)); |
| } |
| |
| zx::result<LockedPage> FileCache::GetPageUnsafe(const pgoff_t index) { |
| while (true) { |
| auto raw_ptr = page_tree_.find(index).CopyPointer(); |
| if (raw_ptr != nullptr) { |
| if (raw_ptr->IsActive()) { |
| auto locked_page_or = GetLockedPageFromRawUnsafe(raw_ptr); |
| if (locked_page_or.is_error()) { |
| continue; |
| } |
| return zx::ok(std::move(*locked_page_or)); |
| } |
| auto page = fbl::ImportFromRawPtr(raw_ptr); |
| LockedPage locked_page(std::move(page)); |
| locked_page->SetActive(); |
| ZX_DEBUG_ASSERT(locked_page->IsLastReference()); |
| return zx::ok(std::move(locked_page)); |
| } |
| break; |
| } |
| return zx::error(ZX_ERR_NOT_FOUND); |
| } |
| |
| zx::result<LockedPage> FileCache::GetLockedPage(fbl::RefPtr<Page> page) { |
| if (page->TryLock()) { |
| tree_lock_.unlock(); |
| { |
| // If |page| is already locked, wait for it to be unlocked. |
| // Ensure that the references to |page| drop before |tree_lock_|. |
| // If |page| is the last reference, it enters Page::RecyclePage() and |
| // possibly acquires |tree_lock_|. |
| LockedPage locked_page(std::move(page)); |
| } |
| // It is not allowed to acquire |tree_lock_| with locked Pages. |
| tree_lock_.lock(); |
| return zx::error(ZX_ERR_SHOULD_WAIT); |
| } |
| LockedPage locked_page(std::move(page), false); |
| return zx::ok(std::move(locked_page)); |
| } |
| |
| zx_status_t FileCache::EvictUnsafe(Page *page) { |
| if (!page->InTreeContainer()) { |
| return ZX_ERR_NOT_FOUND; |
| } |
| // Before eviction, check if it requires VMO_OP_UNLOCK |
| // since Page::RecyclePage() tries VMO_OP_UNLOCK only when |page| keeps in FileCache. |
| ZX_ASSERT(page->VmoOpUnlock(true) == ZX_OK); |
| page_tree_.erase(*page); |
| return ZX_OK; |
| } |
| |
| std::vector<LockedPage> FileCache::GetLockedPagesUnsafe(pgoff_t start, pgoff_t end) { |
| std::vector<LockedPage> pages; |
| auto current = page_tree_.lower_bound(start); |
| while (current != page_tree_.end() && current->GetKey() < end) { |
| if (!current->IsActive()) { |
| LockedPage locked_page(fbl::ImportFromRawPtr(current.CopyPointer())); |
| locked_page->SetActive(); |
| pages.push_back(std::move(locked_page)); |
| } else { |
| auto prev_key = current->GetKey(); |
| auto locked_page_or = GetLockedPageFromRawUnsafe(current.CopyPointer()); |
| if (locked_page_or.is_error()) { |
| current = page_tree_.lower_bound(prev_key); |
| continue; |
| } |
| pages.push_back(std::move(*locked_page_or)); |
| } |
| ++current; |
| } |
| return pages; |
| } |
| |
| std::vector<LockedPage> FileCache::GetLockedPagesUnsafe(const std::vector<pgoff_t> &page_offsets) { |
| std::vector<LockedPage> pages(page_offsets.size()); |
| if (page_tree_.is_empty()) { |
| return pages; |
| } |
| |
| uint32_t index = 0; |
| while (index < page_offsets.size()) { |
| if (page_offsets[index] == kInvalidPageOffset) { |
| ++index; |
| continue; |
| } |
| auto current = page_tree_.find(page_offsets[index]); |
| if (current == page_tree_.end()) { |
| ++index; |
| continue; |
| } |
| if (!current->IsActive()) { |
| // No reference to |current|. It is safe to make a reference. |
| LockedPage locked_page(fbl::ImportFromRawPtr(current.CopyPointer())); |
| locked_page->SetActive(); |
| pages[index] = std::move(locked_page); |
| } else { |
| auto locked_page_or = GetLockedPageFromRawUnsafe(current.CopyPointer()); |
| if (locked_page_or.is_error()) { |
| continue; |
| } |
| pages[index] = std::move(*locked_page_or); |
| } |
| ++index; |
| } |
| return pages; |
| } |
| |
| std::vector<LockedPage> FileCache::CleanupPagesUnsafe(pgoff_t start, pgoff_t end) { |
| std::vector<LockedPage> pages = GetLockedPagesUnsafe(start, end); |
| for (auto &page : pages) { |
| EvictUnsafe(page.get()); |
| page->Invalidate(); |
| } |
| return pages; |
| } |
| |
| std::vector<LockedPage> FileCache::InvalidatePages(pgoff_t start, pgoff_t end, bool zero) { |
| std::vector<LockedPage> pages; |
| { |
| std::lock_guard tree_lock(tree_lock_); |
| pages = CleanupPagesUnsafe(start, end); |
| if (zero) { |
| // Make sure that all pages in the range are zeroed. |
| vmo_manager_->ZeroBlocks(*vnode_->fs()->vfs(), start, end); |
| } |
| } |
| return pages; |
| } |
| |
| void FileCache::ClearDirtyPages() { |
| std::vector<LockedPage> pages; |
| { |
| std::lock_guard tree_lock(tree_lock_); |
| pages = GetLockedPagesUnsafe(); |
| // Let kernel evict the pages if |this| is running on paged vmo. |
| vmo_manager_->AllowEviction(*vnode_->fs()->vfs()); |
| } |
| // Clear the dirty flag of all Pages. |
| for (auto &page : pages) { |
| page->ClearDirtyForIo(); |
| } |
| } |
| |
| void FileCache::Reset() { |
| std::vector<LockedPage> pages; |
| { |
| std::lock_guard tree_lock(tree_lock_); |
| pages = CleanupPagesUnsafe(); |
| } |
| vmo_manager_->Reset(); |
| } |
| |
| std::vector<bool> FileCache::GetDirtyPagesInfo(pgoff_t index, size_t max_scan) { |
| std::vector<bool> read_blocks; |
| read_blocks.reserve(max_scan); |
| |
| // Set bits in |read_blocks| which requires read IOs. |
| fs::SharedLock tree_lock(tree_lock_); |
| auto current = page_tree_.find(index); |
| for (size_t i = 0; i < max_scan; ++i) { |
| read_blocks.push_back(true); |
| if (current != page_tree_.end() && current->GetKey() == index + i) { |
| if (current->IsDirty() || current->IsWriteback()) { |
| // no read IOs for dirty or writeback pages which are uptodate and pinned. |
| read_blocks[i] = false; |
| } |
| ++current; |
| } |
| } |
| return read_blocks; |
| } |
| |
| std::vector<LockedPage> FileCache::GetLockedDirtyPages(const WritebackOperation &operation) { |
| std::vector<LockedPage> pages; |
| pgoff_t nwritten = 0; |
| |
| std::lock_guard tree_lock(tree_lock_); |
| auto current = page_tree_.lower_bound(operation.start); |
| // Get Pages from |operation.start| to |operation.end|. |
| while (nwritten <= operation.to_write && current != page_tree_.end() && |
| current->GetKey() < operation.end) { |
| LockedPage locked_page; |
| auto raw_page = current.CopyPointer(); |
| if (raw_page->IsActive()) { |
| if (raw_page->IsDirty()) { |
| auto prev_key = raw_page->GetKey(); |
| auto locked_page_or = GetLockedPageFromRawUnsafe(raw_page); |
| if (locked_page_or.is_error()) { |
| current = page_tree_.lower_bound(prev_key); |
| continue; |
| } |
| // Re-check if it is dirty after locking. |
| if (locked_page_or->IsDirty()) { |
| locked_page = std::move(*locked_page_or); |
| } |
| } |
| ++current; |
| } else { |
| ++current; |
| ZX_DEBUG_ASSERT(!raw_page->IsLocked()); |
| ZX_DEBUG_ASSERT(!raw_page->IsWriteback()); |
| if (raw_page->IsDirty()) { |
| ZX_DEBUG_ASSERT(raw_page->IsLastReference()); |
| locked_page = LockedPage(fbl::ImportFromRawPtr(raw_page)); |
| } else if (operation.bReleasePages || !vnode_->IsActive()) { |
| // For inactive Pages, try to evict clean Pages if operation.bReleasePages is set or if |
| // their vnodes are inactive(closed). |
| fbl::RefPtr<Page> evicted = fbl::ImportFromRawPtr(raw_page); |
| EvictUnsafe(raw_page); |
| } |
| } |
| if (!locked_page) { |
| continue; |
| } |
| if (!operation.if_page || operation.if_page(locked_page.CopyRefPtr()) == ZX_OK) { |
| locked_page->SetActive(); |
| pages.push_back(std::move(locked_page)); |
| ++nwritten; |
| } else { |
| auto page_ref = locked_page.release(); |
| // It prevents |page| from entering RecyclePage() and |
| // keeps |page| alive in FileCache. |
| [[maybe_unused]] auto leak = fbl::ExportToRawPtr(&page_ref); |
| } |
| } |
| return pages; |
| } |
| |
| } // namespace f2fs |