| // Copyright 2021 The Fuchsia Authors |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #include <lib/boot-options/boot-options.h> |
| #include <lib/counters.h> |
| #include <lib/fit/defer.h> |
| #include <lib/zircon-internal/macros.h> |
| |
| #include <cassert> |
| #include <cstdint> |
| |
| #include <kernel/lockdep.h> |
| #include <ktl/algorithm.h> |
| #include <vm/loan_sweeper.h> |
| #include <vm/pmm.h> |
| #include <vm/scanner.h> |
| #include <vm/vm_cow_pages.h> |
| |
| #include "pmm_node.h" |
| |
| #include <ktl/enforce.h> |
| |
| KCOUNTER(sweep_count, "vm.reclamation.sweep_count") |
| KCOUNTER(sweep_looped, "vm.reclamation.sweep_looped") |
| KCOUNTER(sweep_pages_examined, "vm.reclamation.sweep_pages_examined") |
| KCOUNTER(sweep_pages_swept_to_loaned, "vm.reclamation.sweep_pages_swept_to_loaned") |
| KCOUNTER(sweep_page_chase_retried, "vm.reclamation.sweep_page_chase_retried") |
| KCOUNTER(sweep_page_chase_gave_up, "vm.reclamation.sweep_page_chase_gave_up") |
| |
| LoanSweeper::LoanSweeper() |
| : page_queues_(pmm_page_queues()), ppb_config_(pmm_physical_page_borrowing_config()) {} |
| |
| LoanSweeper::LoanSweeper(PageQueues* queues, PhysicalPageBorrowingConfig* config) |
| : page_queues_(queues), ppb_config_(config) {} |
| |
| void LoanSweeper::Init() { |
| num_arenas_ = pmm_num_arenas(); |
| fbl::AllocChecker ac; |
| arenas_ = ktl::make_unique<pmm_arena_info[]>(&ac, num_arenas_); |
| // This allocation happens super early, and only super early, so require it to succeed. If it |
| // doesn't succeed, most likely something has gone quite wrong quite early. |
| ASSERT(ac.check()); |
| |
| zx_status_t status = |
| pmm_get_arena_info(num_arenas_, /*i=*/0, &arenas_[0], num_arenas_ * sizeof(arenas_[0])); |
| // The only failures are caller bugs, but also check in release in case that changes. |
| ASSERT(status == ZX_OK); |
| |
| min_paddr_ = ktl::numeric_limits<paddr_t>::max(); |
| max_paddr_ = ktl::numeric_limits<paddr_t>::min(); |
| for (uint32_t i = 0; i < num_arenas_; ++i) { |
| auto& arena = arenas_[i]; |
| min_paddr_ = ktl::min(min_paddr_, arena.base); |
| max_paddr_ = ktl::max(max_paddr_, arena.base + arena.size - 1); |
| #if ZX_DEBUG_ASSERT_IMPLEMENTED |
| for (uint32_t j = 0; j < num_arenas_; ++j) { |
| auto& arena_2 = arenas_[j]; |
| if (&arena_2 != &arena) { |
| // Failing this assert would mean two arenas overlap on the same physical range, which would |
| // break assumptions elsewhere in LoanSweeper. |
| DEBUG_ASSERT(arena_2.base + arena_2.size - 1 < arena.base || |
| arena_2.base > arena.base + arena.size - 1); |
| } |
| } |
| #endif |
| } |
| next_start_paddr_ = min_paddr_; |
| } |
| |
| uint64_t LoanSweeper::ForceSynchronousSweep() { return SynchronousSweepInternal(); } |
| |
| // For now, we don't expect the number of loaned pages to typically exceed the number of non-loaned |
| // non-pinned pages (replaceable pages, roughly speaking) so it's reasonable enough for now to just |
| // sweep the pmm's page array looking for non-loaned non-pinned used pages when we have free loaned |
| // pages available. In the event that there are so many pinned pages that we run out of replaceable |
| // pages before we run out of loaned pages, we'll end up scanning the whole pmm page array and find |
| // nothing. In that event, we'll count the occurrence for now. |
| // |
| // Other than too many pages pinned to be able to make use of all loaned pages, we expect the |
| // density of replace-able pages to be high enough that sweeping in physical order is amortized |
| // reasonably efficient. |
| // |
| // We sweep from a starting offset that's persistent from the end of last sweep, since typically |
| // any sweeps due to low free pages will end early when we exhaust all loaned pages, and there's a |
| // better chance of finding replace-able non-loaned pages when we start from where we left off. |
| uint64_t LoanSweeper::SynchronousSweepInternal() { |
| // Sweep (up to) all the pages to find any VMO pages we can move to loaned physical pages, while |
| // we have any free loaned physical pages available. |
| // |
| // We iterate in physical page order because the info we need is in the pmm physical page array, |
| // not in VmCowPages. For now, there's no particular reason to expect a VmPageListNode to |
| // typically contain physically-contiguous pages, so we'd be jumping around in the pmm physical |
| // page array if we iterated in VmCowPages order. Non-sequential access is only done for pages we |
| // can probably replace with a loaned physical page. |
| sweep_count.Add(1); |
| pmm_arena_info_t* cached_arena = nullptr; |
| for (uint32_t i = 0; i < num_arenas_; ++i) { |
| auto& arena = arenas_[i]; |
| if (arena.base <= next_start_paddr_ && next_start_paddr_ < arena.base + arena.size) { |
| cached_arena = &arena; |
| break; |
| } |
| } |
| auto get_next_iter = [this, &cached_arena](paddr_t iter) { |
| DEBUG_ASSERT(IS_PAGE_ALIGNED(iter)); |
| iter += PAGE_SIZE; |
| if (cached_arena && iter < cached_arena->base + cached_arena->size) { |
| return iter; |
| } |
| pmm_arena_info_t* next_arena = nullptr; |
| pmm_arena_info_t* min_arena = &arenas_[0]; |
| for (uint32_t i = 0; i < num_arenas_; ++i) { |
| auto& arena = arenas_[i]; |
| if (arena.base >= iter && (!next_arena || arena.base < next_arena->base)) { |
| next_arena = &arena; |
| } |
| if (arena.base < min_arena->base) { |
| min_arena = &arena; |
| } |
| } |
| if (!next_arena) { |
| next_arena = min_arena; |
| } |
| cached_arena = next_arena; |
| return next_arena->base; |
| }; |
| bool ppb_enabled = ppb_config_->is_any_borrowing_enabled(); |
| uint64_t replaced_non_loaned_page_count = 0; |
| paddr_t iter; |
| auto set_next_start_addr = fit::defer([this, &iter] { next_start_paddr_ = iter; }); |
| Guard<Mutex> guard(&lock_); |
| bool first = true; |
| for (iter = next_start_paddr_; iter != next_start_paddr_ || first; iter = get_next_iter(iter)) { |
| first = false; |
| if (ppb_enabled) { |
| if (!pmm_count_loaned_free_pages()) { |
| return replaced_non_loaned_page_count; |
| } |
| } else { |
| if (!pmm_count_loaned_used_pages()) { |
| return replaced_non_loaned_page_count; |
| } |
| } |
| vm_page_t* page = paddr_to_vm_page(iter); |
| DEBUG_ASSERT(page); |
| DEBUG_ASSERT(page->paddr() == iter); |
| sweep_pages_examined.Add(1); |
| // We're willing to try a limited number of times to chase down a non-loaned page as it moves |
| // between VmCowPages, but limit the iteration count since it's not critical that we replace |
| // every single non-loaned page we iterate over, as there should typically be plenty of |
| // non-loaned replaceable pages to use up all the loaned pages. |
| bool replaced = false; |
| const uint32_t kMaxPageChaseIterations = 3; |
| uint32_t page_try_ordinal; |
| for (page_try_ordinal = 0; page_try_ordinal < kMaxPageChaseIterations; ++page_try_ordinal) { |
| if (page_try_ordinal != 0) { |
| sweep_page_chase_retried.Add(1); |
| } |
| // These are approximate checks, as we're not holding the PageQueues lock or the pmm lock |
| // continuously until we replace the pagel. |
| if (page->state() != vm_page_state::OBJECT) { |
| // next page |
| break; |
| } |
| if (ppb_enabled == pmm_is_loaned(page)) { |
| // next page |
| break; |
| } |
| // That's enough pre-checking to filter out most pages that won't work. Now try to find the |
| // owning VmCowPages and replace this page with a loaned page (or non-loaned page). |
| // |
| // Despite the efforts of GetCowWithReplaceablePage, we may still find below that a returned |
| // VmCowPages doesn't have the page any more, which is the reason for the directly enclosing |
| // loop. |
| auto maybe_vmo_backlink = |
| pmm_page_queues()->GetCowWithReplaceablePage(page, /*owning_cow=*/nullptr); |
| if (!maybe_vmo_backlink) { |
| // There may not be a backlink if page already became FREE or if the page state wasn't |
| // immediately consistent with the page being replaceable (without any waiting). |
| // |
| // next page |
| break; |
| } |
| auto& vmo_backlink = maybe_vmo_backlink.value(); |
| // Else GetCowWithReplaceablePage wouldn't have set the optional backlink. |
| DEBUG_ASSERT(vmo_backlink.cow_container); |
| auto& cow_container = vmo_backlink.cow_container; |
| // TODO(fxbug.dev/99890): Implement way to replace loaned with non-loaned that supports |
| // delayed allocations. |
| ASSERT(ppb_enabled); |
| // vmo_backlink.offset is offset in cow |
| zx_status_t replace_result = cow_container->ReplacePageWithLoaned(page, vmo_backlink.offset); |
| if (replace_result == ZX_ERR_NOT_FOUND) { |
| // No longer owned by cow or no longer replaceable. Go around again to figure out which and |
| // continue chasing it down. We limit the iteration count however, since it's not critical |
| // that we catch up with the page here, and we don't want to get stuck on a page that's |
| // moving super often, or pinning/unpinning super often. Counters track times where we |
| // tried more than once, and times when we tried max times and still didn't replace the |
| // page. |
| // |
| // this page again |
| continue; |
| } |
| if (replace_result == ZX_ERR_NO_MEMORY) { |
| // Out of pages of the appropriate type, so don't try the next page. |
| return replaced_non_loaned_page_count; |
| } |
| if (replace_result != ZX_OK) { |
| // Not replaceable after all. |
| // |
| // next page |
| break; |
| } |
| // The page has been replaced with a different page that doesn't have loan_cancelled set. |
| replaced = true; |
| // next page |
| break; |
| } // page chase loop |
| if (page_try_ordinal == kMaxPageChaseIterations) { |
| sweep_page_chase_gave_up.Add(1); |
| } |
| if (replaced && ppb_enabled) { |
| ++replaced_non_loaned_page_count; |
| sweep_pages_swept_to_loaned.Add(1); |
| } |
| } |
| if (iter == next_start_paddr_) { |
| sweep_looped.Add(1); |
| } |
| return replaced_non_loaned_page_count; |
| } |