| // Copyright 2020 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 <fbl/ref_counted_upgradeable.h> |
| #include <vm/page_queues.h> |
| #include <vm/scanner.h> |
| #include <vm/vm_cow_pages.h> |
| |
| // Rotation time is presently a constant and not adjustable. |
| constexpr zx_duration_t kMinMruRotateTime = ZX_SEC(10); |
| |
| PageQueues::PageQueues() { |
| for (uint32_t i = 0; i < PageQueueNumQueues; i++) { |
| list_initialize(&page_queues_[i]); |
| } |
| } |
| |
| PageQueues::~PageQueues() { |
| for (uint32_t i = 0; i < PageQueueNumQueues; i++) { |
| DEBUG_ASSERT(list_is_empty(&page_queues_[i])); |
| } |
| for (size_t i = 0; i < page_queue_counts_.size(); i++) { |
| DEBUG_ASSERT_MSG(page_queue_counts_[i] == 0, "i=%zu count=%zu", i, |
| page_queue_counts_[i].load()); |
| } |
| } |
| |
| void PageQueues::StartThreads() { |
| Thread* thread = Thread::Create( |
| "page-queue-mru-thread", |
| [](void*) -> int { |
| pmm_page_queues()->MruThread(); |
| return 0; |
| }, |
| nullptr, LOW_PRIORITY); |
| DEBUG_ASSERT(thread); |
| |
| thread->Resume(); |
| } |
| |
| void PageQueues::DisableAging() { |
| // Clear any previous signal. |
| aging_disabled_event_.Unsignal(); |
| if (disable_aging_.exchange(true)) { |
| // It is an error to call DisableAging twice in a row. |
| panic("Mismatched disable/enable pair"); |
| } |
| // Now that disable_aging_ is true, signal the aging thread. This ensures that it will wake up at |
| // least one more time and observe disable_aging_. |
| aging_event_.Signal(); |
| aging_disabled_event_.WaitDeadline(ZX_TIME_INFINITE, Interruptible::No); |
| // Now that aging_disabled_event_ has been signaled we know the aging thread is not in the middle |
| // of doing any aging, and so we can finally return. |
| } |
| |
| void PageQueues::EnableAging() { |
| if (!disable_aging_.exchange(false)) { |
| // It is an error to call EnableAging twice in a row. |
| panic("Mismatched disable/enable pair"); |
| } |
| // Now that aging is enabled, signal the aging thread in case there was a pending reason to age. |
| aging_event_.Signal(); |
| } |
| |
| void PageQueues::Dump() { |
| // Need to grab a copy of all the counts and generations. As the lock is needed to acquire the |
| // active/inactive counts, also hold the lock over the copying of the counts to avoid needless |
| // races. |
| uint64_t mru_gen; |
| uint64_t lru_gen; |
| size_t counts[kNumPagerBacked] = {}; |
| size_t inactive_count; |
| zx_time_t last_age_time; |
| ActiveInactiveCounts activeinactive; |
| { |
| Guard<CriticalMutex> guard{&lock_}; |
| mru_gen = mru_gen_.load(ktl::memory_order_relaxed); |
| lru_gen = lru_gen_.load(ktl::memory_order_relaxed); |
| inactive_count = |
| page_queue_counts_[PageQueuePagerBackedInactive].load(ktl::memory_order_relaxed); |
| for (uint32_t i = 0; i < kNumPagerBacked; i++) { |
| counts[i] = page_queue_counts_[PageQueuePagerBackedBase + i].load(ktl::memory_order_relaxed); |
| } |
| activeinactive = GetActiveInactiveCountsLocked(); |
| last_age_time = last_age_time_.load(ktl::memory_order_relaxed); |
| } |
| // Small arbitrary number that should be more than large enough to hold the constructed string |
| // without causing stack allocation pressure. |
| constexpr size_t kBufSize = 50; |
| // Start with the buffer null terminated. snprintf will always keep it null terminated. |
| char buf[kBufSize] __UNINITIALIZED = "\0"; |
| size_t buf_len = 0; |
| // This formats the counts of all buckets, not just those within the mru->lru range, even though |
| // any buckets not in that range should always have a count of zero. The format this generates is |
| // [active],[active],inactive,inactive,{last inactive},should-be-zero,should-be-zero |
| // Although the inactive and should-be-zero use the same formatting, they are broken up by the |
| // {last inactive}. |
| for (uint64_t i = 0; i < kNumPagerBacked; i++) { |
| PageQueue queue = gen_to_queue(mru_gen - i); |
| ASSERT(buf_len < kBufSize); |
| const size_t remain = kBufSize - buf_len; |
| int write_len; |
| if (i < kNumActiveQueues) { |
| write_len = |
| snprintf(buf + buf_len, remain, "[%zu],", counts[queue - PageQueuePagerBackedBase]); |
| } else if (i == mru_gen - lru_gen) { |
| write_len = |
| snprintf(buf + buf_len, remain, "{%zu},", counts[queue - PageQueuePagerBackedBase]); |
| } else { |
| write_len = snprintf(buf + buf_len, remain, "%zu,", counts[queue - PageQueuePagerBackedBase]); |
| } |
| // Negative values are returned on encoding errors, which we never expect to get. |
| ASSERT(write_len >= 0); |
| if (static_cast<uint>(write_len) >= remain) { |
| // Buffer too small, just use whatever we have constructed so far. |
| break; |
| } |
| buf_len += write_len; |
| } |
| zx_time_t current = current_time(); |
| timespec age_time = zx_timespec_from_duration(zx_time_sub_time(current, last_age_time)); |
| printf("pq: MRU generation is %" PRIu64 " set %ld.%lds ago, LRU generation is %" PRIu64 "\n", |
| mru_gen, age_time.tv_sec, age_time.tv_nsec, lru_gen); |
| printf("pq: Pager buckets %s evict first: %zu, %s active/inactive totals: %zu/%zu\n", buf, |
| inactive_count, activeinactive.cached ? "cached" : "live", activeinactive.active, |
| activeinactive.inactive); |
| } |
| |
| void PageQueues::MruThread() { |
| // Pretend that aging happens during startup to simplify the rest of the loop logic. |
| last_age_time_ = current_time(); |
| while (1) { |
| // Although we have a minimum queue rotation time, we do not want to simply sleep here as this |
| // would prevent us from being able to disable aging in a timely manner. |
| zx_status_t result = aging_event_.WaitDeadline( |
| zx_time_add_duration(last_age_time_.load(ktl::memory_order_relaxed), kMinMruRotateTime), |
| Interruptible::No); |
| |
| // Check if we should be disabling aging. |
| if (disable_aging_) { |
| aging_disabled_event_.Signal(); |
| // Aging is only disabled when running tests, so for simplicity for the logic we can just |
| // pretend to have aged. |
| last_age_time_ = current_time(); |
| continue; |
| } |
| |
| if (result != ZX_ERR_TIMED_OUT) { |
| // We have not reached the minimum rotation time yet, so ignore this wake up and continue |
| // waiting. |
| continue; |
| } |
| |
| // Make sure the accessed information has been harvested since the last time we aged, otherwise |
| // we are deliberately making the age information coarser, by effectively not using one of the |
| // queues, at which point we might as well not have bothered rotating. |
| // Currently this is redundant since we will explicitly harvest just after aging, however once |
| // there are additional aging triggers and harvesting is more asynchronous, this will serve as |
| // a synchronization point. |
| scanner_wait_for_accessed_scan(last_age_time_); |
| |
| RotatePagerBackedQueues(); |
| |
| // To emulate previous behavior of the system, force an accessed scan to happen now that the |
| // page queues have been rotated. Preserving the existing behavior is important, as there is |
| // presently a single active queue, and so we need to immediately pull any accessed pages back |
| // into that active queue to prevent them from being evicted. |
| scanner_wait_for_accessed_scan(ZX_TIME_INFINITE); |
| } |
| } |
| |
| void PageQueues::RotatePagerBackedQueues() { |
| VM_KTRACE_DURATION(2, "RotatePagerBackedQueues"); |
| // We want to increment mru_gen, but first may need to make space by incrementing lru gen. |
| if (mru_gen_.load(ktl::memory_order_relaxed) - lru_gen_.load(ktl::memory_order_relaxed) == |
| kNumPagerBacked - 1) { |
| // Process the LRU queue until we have at least one slot free. |
| ProcessLruQueue(mru_gen_.load(ktl::memory_order_relaxed) - (kNumPagerBacked - 2), false); |
| } |
| |
| // Now that we know there is space, can move the mru queue. |
| // Acquire the lock to increment the mru_gen_. This allows other queue logic to not worry about |
| // mru_gen_ changing whilst they hold the lock. |
| Guard<CriticalMutex> guard{&lock_}; |
| mru_gen_.fetch_add(1, ktl::memory_order_relaxed); |
| last_age_time_ = current_time(); |
| // Update the active/inactive counts. We could be a bit smarter here since we know exactly which |
| // active bucket might have changed, but this will work. |
| RecalculateActiveInactiveLocked(); |
| } |
| |
| ktl::optional<PageQueues::VmoBacklink> PageQueues::ProcessLruQueue(uint64_t target_gen, bool peek) { |
| // This assertion is <=, and not strictly <, since to evict a some queue X, the target must be |
| // X+1. Hence to preserve kNumActiveQueues, we can allow target_gen to become equal to the first |
| // active queue, as this will process all the non-active queues. |
| ASSERT(target_gen <= mru_gen_.load(ktl::memory_order_relaxed) - (kNumActiveQueues - 1)); |
| |
| const PageQueue mru_queue = mru_gen_to_queue(); |
| |
| // Processing the lru queue requires holding the page_queues_ lock_. The only other actions that |
| // require this lock are inserting or removing pages from the page queues. To ensure these actions |
| // can complete in a small bounded time kMaxQueueWork is chosen to be very small so that the lock |
| // will be regularly dropped. As processing the lru queue is not time critical and can be somewhat |
| // inefficient in its operation we err on the side of doing less work per lock acquisition. |
| static constexpr uint32_t kMaxQueueWork = 32; |
| |
| for (uint64_t lru = lru_gen_.load(ktl::memory_order_relaxed); lru < target_gen; |
| lru = lru_gen_.load(ktl::memory_order_relaxed)) { |
| VM_KTRACE_DURATION(2, "ProcessLruQueue"); |
| Guard<CriticalMutex> guard{&lock_}; |
| PageQueue queue = gen_to_queue(lru); |
| uint32_t work_remain = kMaxQueueWork; |
| while (!list_is_empty(&page_queues_[queue]) && work_remain > 0) { |
| work_remain--; |
| // Process the list from its notional oldest (tail) to notional newest (head) |
| vm_page_t* page = list_peek_tail_type(&page_queues_[queue], vm_page_t, queue_node); |
| PageQueue page_queue = |
| (PageQueue)page->object.get_page_queue_ref().load(ktl::memory_order_relaxed); |
| DEBUG_ASSERT(page_queue >= PageQueuePagerBackedBase); |
| // If the queue stored in the page does not match then we want to move it to its correct queue |
| // with the caveat that its queue could be invalid. The queue would be invalid if MarkAccessed |
| // had raced. Should this happen we know that the page is actually *very* old, and so we will |
| // fall back to the case of forcibly changing its age to the new lru gen. |
| if (page_queue != queue && queue_is_valid(page_queue, queue, mru_queue)) { |
| list_delete(&page->queue_node); |
| list_add_head(&page_queues_[page_queue], &page->queue_node); |
| } else if (peek) { |
| VmCowPages* cow = reinterpret_cast<VmCowPages*>(page->object.get_object()); |
| uint64_t page_offset = page->object.get_page_offset(); |
| DEBUG_ASSERT(cow); |
| |
| // We may be racing with destruction of VMO. As we currently hold our lock we know that our |
| // back pointer is correct in so far as the VmCowPages has not yet had completed running its |
| // destructor, so we know it is safe to attempt to upgrade it to a RefPtr. If upgrading |
| // fails we assume the page is about to be removed from the page queue once the VMO |
| // destructor gets a chance to run. |
| return VmoBacklink{fbl::MakeRefPtrUpgradeFromRaw(cow, lock_), page, page_offset}; |
| } else { |
| // Force it into our target queue, don't care about races. If we happened to access it at |
| // the same time then too bad. |
| PageQueue new_queue = gen_to_queue(lru + 1); |
| PageQueue old_queue = (PageQueue)page->object.get_page_queue_ref().exchange(new_queue); |
| DEBUG_ASSERT(old_queue >= PageQueuePagerBackedBase); |
| page_queue_counts_[old_queue].fetch_sub(1, ktl::memory_order_relaxed); |
| page_queue_counts_[new_queue].fetch_add(1, ktl::memory_order_relaxed); |
| list_delete(&page->queue_node); |
| list_add_head(&page_queues_[new_queue], &page->queue_node); |
| // We should only have performed this step to move from one inactive bucket to the next, |
| // so there should be no active/inactive count changes needed. |
| DEBUG_ASSERT(!queue_is_active(new_queue, mru_gen_to_queue())); |
| } |
| } |
| if (list_is_empty(&page_queues_[queue])) { |
| lru_gen_.store(lru + 1, ktl::memory_order_relaxed); |
| } |
| } |
| |
| return ktl::nullopt; |
| } |
| |
| void PageQueues::UpdateActiveInactiveLocked(PageQueue old_queue, PageQueue new_queue) { |
| // Short circuit the lock acquisition and logic if not dealing with active/inactive queues |
| if (!queue_is_pager_backed(old_queue) && !queue_is_pager_backed(new_queue)) { |
| return; |
| } |
| // This just blindly updates the active/inactive counts. If accessed scanning is happening, and |
| // used use_cached_queue_counts_ is true, then we could be racing and setting these to garbage |
| // values. That's fine as they will never get returned anywhere, and will get reset to correct |
| // values once access scanning completes. |
| PageQueue mru = mru_gen_to_queue(); |
| if (queue_is_active(old_queue, mru)) { |
| active_queue_count_--; |
| } else if (queue_is_inactive(old_queue, mru)) { |
| inactive_queue_count_--; |
| } |
| if (queue_is_active(new_queue, mru)) { |
| active_queue_count_++; |
| } else if (queue_is_inactive(new_queue, mru)) { |
| inactive_queue_count_++; |
| } |
| } |
| |
| void PageQueues::MarkAccessed(vm_page_t* page) { |
| Guard<CriticalMutex> guard{&lock_}; |
| |
| auto queue_ref = page->object.get_page_queue_ref(); |
| |
| // We need to check the current queue to see if it is in the pager backed range. Between checking |
| // this and updating the queue it could change, however it would only change as a result of |
| // MarkAccessedDeferredCount, which would only move it to another pager backed queue. No other |
| // change is possible as we are holding lock_. |
| if (queue_ref.load(ktl::memory_order_relaxed) < PageQueuePagerBackedInactive) { |
| return; |
| } |
| |
| PageQueue queue = mru_gen_to_queue(); |
| PageQueue old_queue = (PageQueue)queue_ref.exchange(queue, ktl::memory_order_relaxed); |
| // Double check again that this was previously pager backed |
| DEBUG_ASSERT(old_queue != PageQueueNone && old_queue >= PageQueuePagerBackedInactive); |
| if (old_queue != queue) { |
| page_queue_counts_[old_queue].fetch_sub(1, ktl::memory_order_relaxed); |
| page_queue_counts_[queue].fetch_add(1, ktl::memory_order_relaxed); |
| UpdateActiveInactiveLocked(old_queue, queue); |
| } |
| } |
| |
| void PageQueues::SetQueueLocked(vm_page_t* page, PageQueue queue) { |
| SetQueueBacklinkLocked(page, nullptr, 0, queue); |
| } |
| |
| void PageQueues::SetQueueBacklinkLocked(vm_page_t* page, void* object, uintptr_t page_offset, |
| PageQueue queue) { |
| DEBUG_ASSERT(page->state() == vm_page_state::OBJECT); |
| DEBUG_ASSERT(!page->is_free()); |
| DEBUG_ASSERT(!list_in_list(&page->queue_node)); |
| page->object.set_object(object); |
| page->object.set_page_offset(page_offset); |
| DEBUG_ASSERT(page->object.get_page_queue_ref().load(ktl::memory_order_relaxed) == PageQueueNone); |
| page->object.get_page_queue_ref().store(queue, ktl::memory_order_relaxed); |
| list_add_head(&page_queues_[queue], &page->queue_node); |
| page_queue_counts_[queue].fetch_add(1, ktl::memory_order_relaxed); |
| UpdateActiveInactiveLocked(PageQueueNone, queue); |
| } |
| |
| void PageQueues::MoveToQueueLocked(vm_page_t* page, PageQueue queue) { |
| MoveToQueueBacklinkLocked(page, nullptr, 0, queue); |
| } |
| |
| void PageQueues::MoveToQueueBacklinkLocked(vm_page_t* page, void* object, uintptr_t page_offset, |
| PageQueue queue) { |
| DEBUG_ASSERT(page->state() == vm_page_state::OBJECT); |
| DEBUG_ASSERT(!page->is_free()); |
| DEBUG_ASSERT(list_in_list(&page->queue_node)); |
| uint32_t old_queue = page->object.get_page_queue_ref().exchange(queue, ktl::memory_order_relaxed); |
| DEBUG_ASSERT(old_queue != PageQueueNone); |
| page->object.set_object(object); |
| page->object.set_page_offset(page_offset); |
| list_delete(&page->queue_node); |
| list_add_head(&page_queues_[queue], &page->queue_node); |
| page_queue_counts_[old_queue].fetch_sub(1, ktl::memory_order_relaxed); |
| page_queue_counts_[queue].fetch_add(1, ktl::memory_order_relaxed); |
| UpdateActiveInactiveLocked((PageQueue)old_queue, queue); |
| } |
| |
| void PageQueues::SetWired(vm_page_t* page) { |
| Guard<CriticalMutex> guard{&lock_}; |
| SetQueueLocked(page, PageQueueWired); |
| } |
| |
| void PageQueues::MoveToWired(vm_page_t* page) { |
| Guard<CriticalMutex> guard{&lock_}; |
| MoveToQueueLocked(page, PageQueueWired); |
| } |
| |
| void PageQueues::SetUnswappable(vm_page_t* page) { |
| Guard<CriticalMutex> guard{&lock_}; |
| SetQueueLocked(page, PageQueueUnswappable); |
| } |
| |
| void PageQueues::MoveToUnswappableLocked(vm_page_t* page) { |
| MoveToQueueLocked(page, PageQueueUnswappable); |
| } |
| |
| void PageQueues::MoveToUnswappable(vm_page_t* page) { |
| Guard<CriticalMutex> guard{&lock_}; |
| MoveToUnswappableLocked(page); |
| } |
| |
| void PageQueues::SetPagerBacked(vm_page_t* page, VmCowPages* object, uint64_t page_offset) { |
| Guard<CriticalMutex> guard{&lock_}; |
| SetQueueBacklinkLocked(page, object, page_offset, mru_gen_to_queue()); |
| } |
| |
| void PageQueues::MoveToPagerBacked(vm_page_t* page, VmCowPages* object, uint64_t page_offset) { |
| Guard<CriticalMutex> guard{&lock_}; |
| MoveToQueueBacklinkLocked(page, object, page_offset, mru_gen_to_queue()); |
| } |
| |
| void PageQueues::MoveToPagerBackedInactive(vm_page_t* page) { |
| Guard<CriticalMutex> guard{&lock_}; |
| MoveToQueueBacklinkLocked(page, page->object.get_object(), page->object.get_page_offset(), |
| PageQueuePagerBackedInactive); |
| } |
| |
| void PageQueues::SetUnswappableZeroFork(vm_page_t* page, VmCowPages* object, uint64_t page_offset) { |
| Guard<CriticalMutex> guard{&lock_}; |
| SetQueueBacklinkLocked(page, object, page_offset, PageQueueUnswappableZeroFork); |
| } |
| |
| void PageQueues::MoveToUnswappableZeroFork(vm_page_t* page, VmCowPages* object, |
| uint64_t page_offset) { |
| Guard<CriticalMutex> guard{&lock_}; |
| MoveToQueueBacklinkLocked(page, object, page_offset, PageQueueUnswappableZeroFork); |
| } |
| |
| void PageQueues::RemoveLocked(vm_page_t* page) { |
| // Directly exchange the old gen. |
| uint32_t old_queue = |
| page->object.get_page_queue_ref().exchange(PageQueueNone, ktl::memory_order_relaxed); |
| DEBUG_ASSERT(old_queue != PageQueueNone); |
| page_queue_counts_[old_queue].fetch_sub(1, ktl::memory_order_relaxed); |
| UpdateActiveInactiveLocked((PageQueue)old_queue, PageQueueNone); |
| page->object.set_object(nullptr); |
| page->object.set_page_offset(0); |
| list_delete(&page->queue_node); |
| } |
| |
| void PageQueues::Remove(vm_page_t* page) { |
| Guard<CriticalMutex> guard{&lock_}; |
| RemoveLocked(page); |
| } |
| |
| void PageQueues::RemoveArrayIntoList(vm_page_t** pages, size_t count, list_node_t* out_list) { |
| DEBUG_ASSERT(pages); |
| Guard<CriticalMutex> guard{&lock_}; |
| for (size_t i = 0; i < count; i++) { |
| DEBUG_ASSERT(pages[i]); |
| RemoveLocked(pages[i]); |
| list_add_tail(out_list, &pages[i]->queue_node); |
| } |
| } |
| |
| void PageQueues::BeginAccessScan() { |
| Guard<CriticalMutex> guard{&lock_}; |
| ASSERT(!use_cached_queue_counts_.load(ktl::memory_order_relaxed)); |
| cached_active_queue_count_ = active_queue_count_; |
| cached_inactive_queue_count_ = inactive_queue_count_; |
| use_cached_queue_counts_.store(true, ktl::memory_order_relaxed); |
| } |
| |
| void PageQueues::RecalculateActiveInactiveLocked() { |
| uint64_t active = 0; |
| uint64_t inactive = 0; |
| |
| uint32_t lru = lru_gen_.load(ktl::memory_order_relaxed); |
| uint32_t mru = mru_gen_.load(ktl::memory_order_relaxed); |
| |
| for (uint32_t index = lru; index <= mru; index++) { |
| uint64_t count = page_queue_counts_[gen_to_queue(index)].load(ktl::memory_order_relaxed); |
| if (queue_is_active(gen_to_queue(index), gen_to_queue(mru))) { |
| active += count; |
| } else { |
| // As we are only operating on pager backed queues, !active should imply inactive |
| DEBUG_ASSERT(queue_is_inactive(gen_to_queue(index), gen_to_queue(mru))); |
| inactive += count; |
| } |
| } |
| inactive += page_queue_counts_[PageQueuePagerBackedInactive].load(ktl::memory_order_relaxed); |
| |
| // Update the counts. |
| active_queue_count_ = active; |
| inactive_queue_count_ = inactive; |
| } |
| |
| void PageQueues::EndAccessScan() { |
| Guard<CriticalMutex> guard{&lock_}; |
| |
| ASSERT(use_cached_queue_counts_.load(ktl::memory_order_relaxed)); |
| |
| RecalculateActiveInactiveLocked(); |
| // Clear the cached counts. |
| cached_active_queue_count_ = 0; |
| cached_inactive_queue_count_ = 0; |
| use_cached_queue_counts_.store(false, ktl::memory_order_relaxed); |
| } |
| |
| PageQueues::PagerCounts PageQueues::GetPagerQueueCounts() const { |
| PagerCounts counts; |
| |
| // Grab the lock to prevent LRU processing, this lets us get a slightly less racy snapshot of the |
| // queue counts, although we may still double count pages that move after we count them. |
| // Specifically any parallel callers of MarkAccessed could move a page and change the counts, |
| // causing us to either double count or miss count that page. As these counts are not load bearing |
| // we accept the very small chance of potentially being off a few pages. |
| Guard<CriticalMutex> guard{&lock_}; |
| uint32_t lru = lru_gen_.load(ktl::memory_order_relaxed); |
| uint32_t mru = mru_gen_.load(ktl::memory_order_relaxed); |
| |
| counts.total = 0; |
| for (uint32_t index = lru; index <= mru; index++) { |
| // Distance to the MRU determines the bucket the count goes into, with 'newest' having a |
| // distance of 0 and 'oldest' having a distance of kNumPagerBacked - 1. |
| uint32_t age = mru - index; |
| uint64_t count = page_queue_counts_[gen_to_queue(index)].load(ktl::memory_order_relaxed); |
| if (age == 0) { |
| counts.newest = count; |
| } else if (age == kNumPagerBacked - 1) { |
| counts.oldest = count; |
| } |
| counts.total += count; |
| } |
| // Account the inactive queue length under |oldest|, since (inactive + oldest LRU) pages are |
| // eligible for reclamation first. |oldest| is meant to track pages eligible for eviction first. |
| uint64_t inactive_count = |
| page_queue_counts_[PageQueuePagerBackedInactive].load(ktl::memory_order_relaxed); |
| counts.oldest += inactive_count; |
| counts.total += inactive_count; |
| return counts; |
| } |
| |
| PageQueues::Counts PageQueues::QueueCounts() const { |
| Counts counts = {}; |
| |
| // Grab the lock to prevent LRU processing, this lets us get a slightly less racy snapshot of the |
| // queue counts. We may still double count pages that move after we count them. |
| Guard<CriticalMutex> guard{&lock_}; |
| uint32_t lru = lru_gen_.load(ktl::memory_order_relaxed); |
| uint32_t mru = mru_gen_.load(ktl::memory_order_relaxed); |
| |
| for (uint32_t index = lru; index <= mru; index++) { |
| counts.pager_backed[mru - index] = |
| page_queue_counts_[gen_to_queue(index)].load(ktl::memory_order_relaxed); |
| } |
| counts.pager_backed_inactive = |
| page_queue_counts_[PageQueuePagerBackedInactive].load(ktl::memory_order_relaxed); |
| counts.unswappable = page_queue_counts_[PageQueueUnswappable].load(ktl::memory_order_relaxed); |
| counts.wired = page_queue_counts_[PageQueueWired].load(ktl::memory_order_relaxed); |
| counts.unswappable_zero_fork = |
| page_queue_counts_[PageQueueUnswappableZeroFork].load(ktl::memory_order_relaxed); |
| return counts; |
| } |
| |
| bool PageQueues::DebugPageIsPagerBacked(const vm_page_t* page, size_t* queue) const { |
| PageQueue q = (PageQueue)page->object.get_page_queue_ref().load(ktl::memory_order_relaxed); |
| if (q >= PageQueuePagerBackedBase && q <= PageQueuePagerBackedLast) { |
| if (queue) { |
| *queue = queue_age(q, mru_gen_to_queue()); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| bool PageQueues::DebugPageIsPagerBackedInactive(const vm_page_t* page) const { |
| return page->object.get_page_queue_ref().load(ktl::memory_order_relaxed) == |
| PageQueuePagerBackedInactive; |
| } |
| |
| bool PageQueues::DebugPageIsUnswappable(const vm_page_t* page) const { |
| return page->object.get_page_queue_ref().load(ktl::memory_order_relaxed) == PageQueueUnswappable; |
| } |
| |
| bool PageQueues::DebugPageIsWired(const vm_page_t* page) const { |
| return page->object.get_page_queue_ref().load(ktl::memory_order_relaxed) == PageQueueWired; |
| } |
| |
| bool PageQueues::DebugPageIsUnswappableZeroFork(const vm_page_t* page) const { |
| return page->object.get_page_queue_ref().load(ktl::memory_order_relaxed) == |
| PageQueueUnswappableZeroFork; |
| } |
| |
| bool PageQueues::DebugPageIsAnyUnswappable(const vm_page_t* page) const { |
| return DebugPageIsUnswappable(page) || DebugPageIsUnswappableZeroFork(page); |
| } |
| |
| ktl::optional<PageQueues::VmoBacklink> PageQueues::PopUnswappableZeroFork() { |
| Guard<CriticalMutex> guard{&lock_}; |
| |
| vm_page_t* page = |
| list_peek_tail_type(&page_queues_[PageQueueUnswappableZeroFork], vm_page_t, queue_node); |
| if (!page) { |
| return ktl::nullopt; |
| } |
| |
| VmCowPages* cow = reinterpret_cast<VmCowPages*>(page->object.get_object()); |
| uint64_t page_offset = page->object.get_page_offset(); |
| DEBUG_ASSERT(cow); |
| MoveToQueueLocked(page, PageQueueUnswappable); |
| |
| // We may be racing with destruction of VMO. As we currently hold our lock we know that our |
| // back pointer is correct in so far as the VmCowPages has not yet had completed running its |
| // destructor, so we know it is safe to attempt to upgrade it to a RefPtr. If upgrading fails |
| // we assume the page is about to be removed from the page queue once the VMO destructor gets |
| // a chance to run. |
| return VmoBacklink{fbl::MakeRefPtrUpgradeFromRaw(cow, guard), page, page_offset}; |
| } |
| |
| ktl::optional<PageQueues::VmoBacklink> PageQueues::PeekPagerBacked(size_t lowest_queue) { |
| // Peek the tail of the inactive queue first. |
| while (true) { |
| // Process a single page each time to keep the critical section for the lock small. |
| Guard<CriticalMutex> guard{&lock_}; |
| if (list_is_empty(&page_queues_[PageQueuePagerBackedInactive])) { |
| break; |
| } |
| vm_page_t* page = |
| list_peek_tail_type(&page_queues_[PageQueuePagerBackedInactive], vm_page_t, queue_node); |
| |
| // Might need to fix up the queue for this page. |
| PageQueue page_queue = |
| (PageQueue)page->object.get_page_queue_ref().load(ktl::memory_order_relaxed); |
| |
| // The page is no longer inactive, we need to move this page out of the inactive queue. |
| // It's possible for MarkAccessed to race and change the queue again from under us, but the |
| // queue can't become PageQueuePagerBackedInactive since we need the lock for that. |
| if (page_queue != PageQueuePagerBackedInactive) { |
| // If page_queue is still valid, move it to that queue. Otherwise, this page is very old |
| // and should be moved to the lru queue and page counts should be updated accordingly. It's |
| // possible that the page is so old that the queues have wrapped again and its page_queue |
| // appears to be valid. However there isn't a way to distinguish that here, so respect the |
| // validity of the queue as returned by queue_is_valid. |
| if (queue_is_valid(page_queue, lru_gen_to_queue(), mru_gen_to_queue())) { |
| list_delete(&page->queue_node); |
| list_add_head(&page_queues_[page_queue], &page->queue_node); |
| } else { |
| PageQueue new_queue = lru_gen_to_queue(); |
| PageQueue old_queue = (PageQueue)page->object.get_page_queue_ref().exchange(new_queue); |
| page_queue_counts_[old_queue].fetch_sub(1, ktl::memory_order_relaxed); |
| page_queue_counts_[new_queue].fetch_add(1, ktl::memory_order_relaxed); |
| list_delete(&page->queue_node); |
| list_add_head(&page_queues_[new_queue], &page->queue_node); |
| } |
| } else { |
| // It's possible for MarkAccessed to race and change the queue from under us, i.e. if the page |
| // is accessed exactly when we're trying to evict it. Ignore that race, and let eviction win. |
| VmCowPages* cow = reinterpret_cast<VmCowPages*>(page->object.get_object()); |
| uint64_t page_offset = page->object.get_page_offset(); |
| DEBUG_ASSERT(cow); |
| |
| // We may be racing with destruction of VMO. As we currently hold our lock we know that our |
| // back pointer is correct in so far as the VmCowPages has not yet had completed running its |
| // destructor, so we know it is safe to attempt to upgrade it to a RefPtr. If upgrading fails |
| // we assume the page is about to be removed from the page queue once the VMO destructor gets |
| // a chance to run. |
| return VmoBacklink{fbl::MakeRefPtrUpgradeFromRaw(cow, guard), page, page_offset}; |
| } |
| } |
| |
| // Ignore any requests to evict from the active queues as this is never allowed. |
| lowest_queue = ktl::max(lowest_queue, kNumActiveQueues); |
| // The target gen is 1 larger than the lowest queue because evicting from queue X is done by |
| // attempting to make the lru queue be X+1. |
| return ProcessLruQueue(mru_gen_.load(ktl::memory_order_relaxed) - (lowest_queue - 1), true); |
| } |
| |
| PageQueues::ActiveInactiveCounts PageQueues::GetActiveInactiveCounts() const { |
| Guard<CriticalMutex> guard{&lock_}; |
| return GetActiveInactiveCountsLocked(); |
| } |
| |
| PageQueues::ActiveInactiveCounts PageQueues::GetActiveInactiveCountsLocked() const { |
| if (use_cached_queue_counts_.load(ktl::memory_order_relaxed)) { |
| return ActiveInactiveCounts{.cached = true, |
| .active = cached_active_queue_count_, |
| .inactive = cached_inactive_queue_count_}; |
| } else { |
| // With use_cached_queue_counts_ false the counts should have been updated to remove any |
| // negative values that might have been caused by races. |
| ASSERT(active_queue_count_ >= 0); |
| ASSERT(inactive_queue_count_ >= 0); |
| return ActiveInactiveCounts{.cached = false, |
| .active = static_cast<uint64_t>(active_queue_count_), |
| .inactive = static_cast<uint64_t>(inactive_queue_count_)}; |
| } |
| } |