| // 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 |
| |
| #ifndef ZIRCON_KERNEL_VM_INCLUDE_VM_PAGE_QUEUES_H_ |
| #define ZIRCON_KERNEL_VM_INCLUDE_VM_PAGE_QUEUES_H_ |
| |
| #include <lib/fit/result.h> |
| #include <lib/page/size.h> |
| #include <sys/types.h> |
| #include <zircon/listnode.h> |
| |
| #include <fbl/algorithm.h> |
| #include <fbl/macros.h> |
| #include <kernel/event.h> |
| #include <kernel/lockdep.h> |
| #include <kernel/mutex.h> |
| #include <kernel/semaphore.h> |
| #include <ktl/array.h> |
| #include <ktl/optional.h> |
| #include <ktl/variant.h> |
| #include <vm/debug_compressor.h> |
| #include <vm/page.h> |
| |
| class VmCowPages; |
| |
| // Allocated pages that are part of the cow pages in a VmObjectPaged can be placed in a page queue. |
| // The page queues provide a way to |
| // * Classify and group pages across VMO boundaries |
| // * Retrieve the VMO that a page is contained in (via a back reference stored in the vm_page_t) |
| // Once a page has been placed in a page queue its queue_node becomes owned by the page queue and |
| // must not be used until the page has been Remove'd. It is not sufficient to call list_delete on |
| // the queue_node yourself as this operation is not atomic and needs to be performed whilst holding |
| // the PageQueues::lock_. |
| class PageQueues { |
| public: |
| // The number of reclamation queues is slightly arbitrary, but to be useful you want at least 3 |
| // representing |
| // * Very new pages that you probably don't want to evict as doing so probably implies you are in |
| // swap death |
| // * Slightly old pages that could be evicted if needed |
| // * Very old pages that you'd be happy to evict |
| // With two active queues 8 page queues are used so that there is some fidelity of information in |
| // the inactive queues. Additional queues have reduced value as sufficiently old pages quickly |
| // become equivalently unlikely to be used in the future. |
| static constexpr size_t kNumReclaim = 8; |
| |
| // Two active queues are used to allow for better fidelity of active information. This prevents |
| // a race between aging once and needing to collect/harvest age information. |
| static constexpr size_t kNumActiveQueues = 2; |
| |
| // The amount of pages that will have to move around the queues before the active/inactive |
| // ratio is re-checked. This therefore represents how much error the active ratio aging process |
| // might have, or how delayed the MRU generation might be. |
| // In the worst case once the active ratio is triggered this value is how much page data needs to |
| // then change queues before the aging process happens. |
| static constexpr size_t kActiveInactiveErrorMargin = (2 * MB) / kPageSize; |
| |
| static_assert(kNumReclaim > kNumActiveQueues, "Needs to be at least one non-active queue"); |
| |
| // In addition to active and inactive, we want to consider some of the queues as 'oldest' to |
| // provide an additional way to limit eviction. Presently the processing of the LRU queue to make |
| // room for aging is not integrated with the Evictor, and so will not trigger eviction, therefore |
| // to have a non-zero number of pages ever appear in an oldest queue for eviction the last two |
| // queues are considered the oldest. |
| static constexpr size_t kNumOldestQueues = 2; |
| static_assert(kNumOldestQueues + kNumActiveQueues <= kNumReclaim); |
| |
| // Number of different isolate queues that are available. Different isolate queues allow for |
| // separating isolate pages into different buckets such that more nuanced choices on what page to |
| // reclaim can be made. |
| // We use 2 queues to separate "Don't Need" pages (high reclamation priority, index 0) from |
| // standard aged pages (standard reclamation priority, index 1). |
| static constexpr size_t kIsolateQueueDontNeed = 0; |
| static constexpr size_t kIsolateQueueStandard = 1; |
| static constexpr size_t kNumIsolateQueues = 2; |
| |
| static_assert(kIsolateQueueDontNeed < kNumIsolateQueues); |
| static_assert(kIsolateQueueStandard < kNumIsolateQueues); |
| static_assert(kIsolateQueueDontNeed < kIsolateQueueStandard); |
| |
| static constexpr zx_duration_mono_t kDefaultMinMruRotateTime = ZX_SEC(5); |
| static constexpr zx_duration_mono_t kDefaultMaxMruRotateTime = ZX_SEC(5); |
| |
| // This is presently an arbitrary constant, since the min and max mru rotate time are currently |
| // fixed at the same value, meaning that the active ratio can not presently trigger, or prevent, |
| // aging. |
| static constexpr uint64_t kDefaultActiveRatioMultiplier = 0; |
| |
| PageQueues(); |
| ~PageQueues(); |
| |
| DISALLOW_COPY_ASSIGN_AND_MOVE(PageQueues); |
| |
| // All Set operations places a page, which must not currently be in a page queue, into the |
| // specified queue. The backlink information of |object| and |page_offset| must be specified and |
| // valid. If the page is either removed from the referenced object, or moved to a different |
| // offset, the backlink information must be updated either by calling ChangeObjectOffsetLocked, or |
| // removing the page completely from the queues. |
| |
| void SetWired(vm_page_t* page, VmCowPages* object, uint64_t page_offset); |
| // |skip_reclaim| controls whether reclaiming the page should be forcibly skipped regardless of |
| // whether anonymous pages are considered reclaimable in general. |
| void SetAnonymous(vm_page_t* page, VmCowPages* object, uint64_t page_offset, |
| bool skip_reclaim = false); |
| void SetReclaim(vm_page_t* page, VmCowPages* object, uint64_t page_offset); |
| void SetPagerBackedDirty(vm_page_t* page, VmCowPages* object, uint64_t page_offset); |
| void SetAnonymousZeroFork(vm_page_t* page, VmCowPages* object, uint64_t page_offset); |
| void SetHighPriority(vm_page_t* page, VmCowPages* object, uint64_t page_offset); |
| |
| // All Move operations change the queue that a page is considered to be in, but do not change the |
| // object or offset backlink information. The page must currently be in a valid page queue. |
| |
| void MoveToWired(vm_page_t* page); |
| // |skip_reclaim| controls whether reclaiming the page should be forcibly skipped regardless of |
| // whether anonymous pages are considered reclaimable in general. |
| void MoveToAnonymous(vm_page_t* page, bool skip_reclaim = false); |
| void MoveToReclaim(vm_page_t* page); |
| void MoveToReclaimDontNeed(vm_page_t* page); |
| void MoveToPagerBackedDirty(vm_page_t* page); |
| void MoveToHighPriority(vm_page_t* page); |
| |
| // If a page is presently in the anonymous (or reclaim queue, depending if anonymous pages are |
| // reclaimable) moves it to the appropriate anonymous zero fork queue (exact queue depends on |
| // whether zero forks are reclaimable or not). If the page is not in the anonymous queue then it |
| // is not modified. |
| void MoveAnonymousToAnonymousZeroFork(vm_page_t* page); |
| |
| // Indicates that page has failed a compression attempted, and moves it to a separate queue to |
| // prevent it from being considered part of the reclaim set, which makes it neither active nor |
| // inactive. The specified page must be in the page queues, but if not presently in a reclaim |
| // queue this method will do nothing. |
| // TODO(https://fxbug.dev/42138396): Determine whether/how pages are moved back into the reclaim |
| // pool and either further generalize this to support pager backed, or specialize FailedReclaim to |
| // be explicitly only anonymous. |
| void CompressFailed(vm_page_t* page); |
| |
| // Changes the backlink information for a page and should only be called by the page owner under |
| // its lock (that is the VMO lock). The page must currently be in a valid page queue. |
| void ChangeObjectOffset(vm_page_t* page, VmCowPages* object, uint64_t page_offset); |
| void ChangeObjectOffsetArray(vm_page_t** pages, VmCowPages* object, uint64_t* offsets, |
| size_t count); |
| |
| // Externally locked variant of CHangeObjectOffset that can be used for more efficient batch |
| // operations. In addition to the annotated lock_, the VMO lock of the owner is also required to |
| // be held. |
| void ChangeObjectOffsetLockedList(vm_page_t* page, VmCowPages* object, uint64_t page_offset) |
| TA_REQ(list_lock_); |
| |
| // Removes the page from any page list and returns ownership of the queue_node. |
| void Remove(vm_page_t* page); |
| // Batched version of Remove that also places all the pages in the specified list |
| void RemoveArrayIntoList(vm_page_t** page, size_t count, VmPageDoublyLinkedList* out_list); |
| |
| // Tells the page queue this page has been accessed, and it should have its position in the queues |
| // updated. |
| void MarkAccessed(vm_page_t* page); |
| |
| // Provides access to the underlying lock, allowing _Locked variants to be called. Use of this is |
| // highly discouraged as the underlying lock is a CriticalMutex which disables preemption. |
| // Preferably *Array variations should be used, but this provides a higher performance mechanism |
| // when needed. |
| Lock<CriticalMutex>* get_lock() TA_RET_CAP(list_lock_) { return &list_lock_; } |
| |
| // Used to identify the reason that aging is triggered, mostly for debugging and informational |
| // purposes. |
| enum class AgeReason { |
| // Aging occurred due to the maximum timeout being reached before any other reason could trigger |
| Timeout, |
| // The allowable ratio of active versus inactive pages was exceeded. |
| ActiveRatio, |
| // An explicit call to RotatePagerBackedQueues caused aging. This would typically occur due to |
| // test code or via the kernel debug console. |
| Manual, |
| }; |
| static const char* string_from_age_reason(PageQueues::AgeReason reason); |
| |
| // Performs a manually requested aging event. This ignores usual aging triggers / restrictions and |
| // waits, if necessary, for aging to be possible and then performs it. |
| // Only for tests and debugging. |
| void RotateReclaimQueues(); |
| |
| // Used to represent and return page backlink information acquired whilst holding the page queue |
| // lock. As a VMO may not destruct while it has pages in it, the cow RefPtr will always be valid, |
| // although the page and offset contained here are not synchronized and must be separately |
| // validated before use. This can be done by acquiring the returned vmo's lock and then validating |
| // that the page is still contained at the offset. |
| struct VmoBacklink { |
| fbl::RefPtr<VmCowPages> cow; |
| vm_page_t* page = nullptr; |
| uint64_t offset = 0; |
| }; |
| |
| // Moves a page from from the anonymous zero fork queue into the anonymous queue and returns |
| // the backlink information. If the zero fork queue is empty then a nullopt is returned, otherwise |
| // if it has_value the vmo field may be null to indicate that the vmo is running its destructor |
| // (see VmoBacklink for more details). |
| ktl::optional<VmoBacklink> PopAnonymousZeroFork(); |
| |
| // Looks at the isolate queues and returns backlink information of the first page found. If the |
| // isolate queue is empty then LRU queues up to |lowest_queue| epochs from the most recent will be |
| // processed to attempt to fill the isolate list. If no page was found a nullopt is returned, |
| // otherwise if it has_value the vmo field may be null to indicate that the vmo is running its |
| // destructor (see VmoBacklink for more details). If a page is returned its location in the |
| // reclaim queue is not modified. |
| ktl::optional<VmoBacklink> PeekIsolate(size_t lowest_queue); |
| |
| // Can be called while the |page| is known to be in the loaned state. This method checks if it is |
| // in the page queues, and if so returns a reference to the cow pages that owns it. |
| // The page must be 'owned' by the caller, in so far as the page->state() is guaranteed to not be |
| // changing. |
| ktl::optional<VmoBacklink> GetCowForLoanedPage(vm_page_t* page); |
| |
| // Helper struct to group reclaimable queue length counts returned by GetReclaimCounts. |
| struct ReclaimCounts { |
| size_t total = 0; |
| size_t newest = 0; |
| size_t oldest = 0; |
| }; |
| |
| // Returns just the reclaim queue counts. Called from the zx_object_get_info() syscall. |
| ReclaimCounts GetReclaimQueueCounts() const; |
| |
| // Helper struct to group queue length counts returned by QueueCounts. |
| struct Counts { |
| ktl::array<size_t, kNumReclaim> reclaim = {0}; |
| size_t reclaim_isolate = 0; |
| size_t pager_backed_dirty = 0; |
| size_t anonymous = 0; |
| size_t wired = 0; |
| size_t anonymous_zero_fork = 0; |
| size_t failed_reclaim = 0; |
| size_t high_priority = 0; |
| |
| bool operator==(const Counts& other) const { |
| return reclaim == other.reclaim && reclaim_isolate == other.reclaim_isolate && |
| pager_backed_dirty == other.pager_backed_dirty && anonymous == other.anonymous && |
| wired == other.wired && anonymous_zero_fork == other.anonymous_zero_fork && |
| failed_reclaim == other.failed_reclaim && high_priority == other.high_priority; |
| } |
| bool operator!=(const Counts& other) const { return !(*this == other); } |
| }; |
| |
| Counts QueueCounts() const; |
| |
| struct ActiveInactiveCounts { |
| // Pages that would normally be available for eviction, but are presently considered active and |
| // so will not be evicted. |
| size_t active = 0; |
| // Pages that are available for eviction due to not presently being considered active. |
| size_t inactive = 0; |
| |
| bool operator==(const ActiveInactiveCounts& other) const { |
| return active == other.active && inactive == other.inactive; |
| } |
| bool operator!=(const ActiveInactiveCounts& other) const { return !(*this == other); } |
| }; |
| ActiveInactiveCounts GetActiveInactiveCounts() const; |
| |
| void Dump() TA_EXCL(lock_); |
| |
| // Returns a global count of all pages compressed at the point of LRU change. This is a global |
| // method and will include stats from every PageQueues that has been instantiated. |
| static uint64_t GetLruPagesCompressed(); |
| |
| // Enables reclamation of anonymous pages by causing them to be placed into the reclaimable queue |
| // instead of the dedicated anonymous queue. The |zero_forks| parameter controls whether the |
| // anonymous zero forks should also go into the general reclaimable queue or not. |
| // Any pages already placed into the anonymous queues will be moved over, and there is no way to |
| // disable this once enabled. |
| void EnableAnonymousReclaim(bool zero_forks); |
| |
| // Returns whether or not the reclaim queues only include pager backed pages or not. |
| bool ReclaimIsOnlyPagerBacked() const { return !anonymous_is_reclaimable_; } |
| |
| // Returns true if the page is in an isolate queue. |
| static bool IsPageReclaimable(const vm_page_t* page) { |
| return page->object.get_page_queue_ref().load(ktl::memory_order_relaxed) == |
| PageQueueReclaimIsolate; |
| } |
| |
| // These query functions are marked Debug as it is generally a racy way to determine a pages state |
| // and these are exposed for the purpose of writing tests or asserts against the pagequeue. |
| |
| // This takes an optional output parameter that, if the function returns true, will contain the |
| // index of the queue that the page was in. |
| bool DebugPageIsReclaim(const vm_page_t* page, size_t* queue = nullptr) const; |
| bool DebugPageIsReclaimIsolate(const vm_page_t* page) const; |
| bool DebugPageIsPagerBackedDirty(const vm_page_t* page) const; |
| bool DebugPageIsAnonymous(const vm_page_t* page) const; |
| bool DebugPageIsAnonymousZeroFork(const vm_page_t* page) const; |
| bool DebugPageIsAnyAnonymous(const vm_page_t* page) const; |
| bool DebugPageIsWired(const vm_page_t* page) const; |
| bool DebugPageIsHighPriority(const vm_page_t* page) const; |
| |
| // These methods are public so that the scanner can call. Once the scanner is an object that can |
| // be friended, and not a collection of anonymous functions, these can be made private. |
| |
| // Creates any threads for queue management. This needs to be done separately to construction as |
| // there is a recursive dependency where creating threads will need to manipulate pages, which |
| // will call back into the page queues. |
| // Delaying thread creation is fine as these threads are purely for aging and eviction management, |
| // which is not needed during early kernel boot. |
| // Failure to start the threads may cause operations such as RotatePagerBackedQueues to block |
| // indefinitely as they might attempt to offload work to a nonexistent thread. This issue is only |
| // relevant for unittests that may wish to avoid starting the threads for some tests. |
| // It is the responsibility of the caller to only call this once, otherwise it will panic. |
| void StartThreads(zx_duration_mono_t min_mru_rotate_time, zx_duration_mono_t max_mru_rotate_time); |
| |
| // Initializes and starts the debug compression, which attempts to immediately compress a random |
| // subset of pages added to the page queues. It is an error to call this if there is no compressor |
| // or if not running in debug mode. |
| void StartDebugCompressor(); |
| |
| // Sets the active ratio multiplier. |
| void SetActiveRatioMultiplier(uint32_t multiplier); |
| |
| // Describes any action to take when processing the LRU queue. This is applied to pages that would |
| // otherwise have to be moved from the old LRU queue into the isolate queue. |
| enum class LruAction { |
| None, |
| EvictOnly, |
| CompressOnly, |
| EvictAndCompress, |
| }; |
| void SetLruAction(LruAction action); |
| |
| // Controls to enable and disable the active aging system. These must be called alternately and |
| // not in parallel. That is, it is an error to call DisableAging twice without calling EnableAging |
| // in between. Similar for EnableAging. |
| void DisableAging() TA_EXCL(lock_); |
| void EnableAging() TA_EXCL(lock_); |
| |
| // Register an Event that will be signalled every time aging occurs. This can be used to know if |
| // if PeekReclaim might now return items (due to aging having occurred) where it had previously |
| // ceased. |
| // Only a single Event may be registered at a time and the Event is assumed to live as long as the |
| // PageQueues object. A nullptr can be passed in to unregister an Event, otherwise it is an error |
| // to attempt to register over the top of an existing event. |
| void SetAgingEvent(Event* event); |
| |
| // Debug methods to retrieve a reference to any lru and mru threads. These are intended for use |
| // during tests / debugging and hence bypass the lock normally needed to read these members. It is |
| // up to the caller to know if these objects are alive or not. |
| Thread* DebugGetLruThread() TA_NO_THREAD_SAFETY_ANALYSIS { return lru_thread_; } |
| Thread* DebugGetMruThread() TA_NO_THREAD_SAFETY_ANALYSIS { return mru_thread_; } |
| |
| private: |
| // Specifies the indices for both the page_queues_ and the page_queue_counts_ |
| enum PageQueue : uint8_t { |
| PageQueueNone = 0, |
| PageQueueAnonymous, |
| PageQueueWired, |
| PageQueueHighPriority, |
| PageQueueAnonymousZeroFork, |
| PageQueuePagerBackedDirty, |
| PageQueueFailedReclaim, |
| PageQueueReclaimIsolate, |
| PageQueueReclaimBase, |
| PageQueueReclaimLast = PageQueueReclaimBase + kNumReclaim - 1, |
| PageQueueNumQueues, |
| }; |
| |
| // Ensure that the reclaim queue counts are always at the end. |
| static_assert(PageQueueReclaimLast + 1 == PageQueueNumQueues); |
| |
| // The page queue index, unlike the full generation count, needs to be able to fit inside a |
| // uint8_t in the vm_page_t. |
| static_assert(PageQueueNumQueues < 256); |
| |
| // Converts free running generation to reclaim queue. |
| static constexpr PageQueue gen_to_queue(uint64_t gen) { |
| return static_cast<PageQueue>((gen % kNumReclaim) + PageQueueReclaimBase); |
| } |
| |
| // Checks if a candidate reclaim page queue would be valid given a specific lru and mru |
| // queue. |
| static constexpr bool queue_is_valid(PageQueue page_queue, PageQueue lru, PageQueue mru) { |
| DEBUG_ASSERT(page_queue >= PageQueueReclaimBase); |
| if (lru <= mru) { |
| return page_queue >= lru && page_queue <= mru; |
| } else { |
| return page_queue <= mru || page_queue >= lru; |
| } |
| } |
| |
| // Returns whether this queue is reclaimable, and hence can be active or inactive. If this |
| // returns false then it is guaranteed that both |queue_is_active| and |queue_is_inactive| would |
| // return false. |
| static constexpr bool queue_is_reclaim(PageQueue page_queue) { |
| // We check against the the Isolate queue and not the base queue so that accessing a page can |
| // move it from the Isolate list into the LRU queues. To keep this case efficient we require |
| // that the Isoalte queue be directly before the LRU queues. |
| static_assert(PageQueueReclaimIsolate + 1 == PageQueueReclaimBase); |
| |
| // Ensure that the Dirty queue comes before the smallest queue that would return true for this |
| // function. This function is used for computing active/inactive sets for the purpose of |
| // eviction, and dirty pages cannot be evicted. The Dirty queue also needs to come before the |
| // Isolate queue so that MarkAccessed does not try to move the page to the MRU queue on |
| // access. |
| static_assert(PageQueuePagerBackedDirty < PageQueueReclaimIsolate); |
| |
| return page_queue >= PageQueueReclaimIsolate; |
| } |
| |
| // Calculates the age of a queue against a given mru, with 0 meaning page_queue==mru |
| // This is only meaningful to call on reclaimable queues. |
| static constexpr uint queue_age(PageQueue page_queue, PageQueue mru) { |
| DEBUG_ASSERT(page_queue >= PageQueueReclaimBase); |
| if (page_queue <= mru) { |
| return mru - page_queue; |
| } else { |
| return (static_cast<uint>(kNumReclaim) - page_queue) + mru; |
| } |
| } |
| |
| // Returns whether the given page queue would be considered active against a given mru. |
| // This is valid to call on any page queue, not just reclaimable ones, and as such this returning |
| // false does not imply the queue is inactive. |
| static constexpr bool queue_is_active(PageQueue page_queue, PageQueue mru) { |
| if (page_queue < PageQueueReclaimBase) { |
| return false; |
| } |
| return queue_age(page_queue, mru) < kNumActiveQueues; |
| } |
| |
| // Returns whether the given page queue would be considered inactive against a given mru. |
| // This is valid to call on any page queue, not just reclaimable ones, and as such this returning |
| // false does not imply the queue is active. |
| static constexpr bool queue_is_inactive(PageQueue page_queue, PageQueue mru) { |
| // The Isolate queue does not have an age, and so we cannot call queue_age on it, but it should |
| // definitely be considered part of the inactive set. |
| if (page_queue == PageQueueReclaimIsolate) { |
| return true; |
| } |
| if (page_queue < PageQueueReclaimBase) { |
| return false; |
| } |
| return queue_age(page_queue, mru) >= kNumActiveQueues; |
| } |
| |
| PageQueue mru_gen_to_queue() const { |
| return gen_to_queue(mru_gen_.load(ktl::memory_order_relaxed)); |
| } |
| |
| PageQueue lru_gen_to_queue() const { |
| return gen_to_queue(lru_gen_.load(ktl::memory_order_relaxed)); |
| } |
| |
| // This processes the LRU queue with the goal of increasing the lru_gen_ to the target_gen. It |
| // achieves this by walking all the pages in the queue and doing one of the following: |
| // 1. For pages that have a newest accessed time and are in the wrong queue, are moved into the |
| // correct queue. |
| // 2. For pages that are in the correct queue, they are moved to the Isolate queue. |
| // An optional limit for the number of pages to Isolate can be provided and if reached this will |
| // return early even if target_gen has not been reached. |
| void ProcessLruQueue(uint64_t target_gen, ktl::optional<size_t> isolate); |
| |
| // Peek the isolate Isolate lists and return the first page, or a nullopt if the list is empty. |
| ktl::optional<VmoBacklink> PeekIsolateList() TA_EXCL(lock_); |
| |
| // Helpers for adding and removing to the queues. All of the public Set/Move/Remove operations |
| // are convenience wrappers around these. |
| void RemoveLockedList(vm_page_t* page) TA_REQ(list_lock_); |
| void SetQueueBacklinkLockedList(vm_page_t* page, void* object, uintptr_t page_offset, |
| PageQueue queue) TA_REQ(list_lock_); |
| void MoveToQueueLockedList(vm_page_t* page, PageQueue queue) TA_REQ(list_lock_); |
| void MoveToIsolateLockedList(vm_page_t* page, size_t isolate_queue_index) TA_REQ(list_lock_); |
| // Potentially calls |CheckActiveRatioAgingLocked| based on the kActiveInactiveErrorMargin. |
| // |pages| indicates how many pages might have changed queue, and hence how much the ratio could |
| // have changed by. |
| void MaybeCheckActiveRatioAging(size_t pages) TA_EXCL(lock_); |
| void MaybeCheckActiveRatioAgingLocked(size_t pages) TA_REQ(lock_); |
| |
| // Internal helper for shutting down any threads created in |StartThreads|. |
| void StopThreads(); |
| |
| // Entry point for the thread that will performing aging and increment the mru generation. |
| void MruThread(); |
| |
| // Checks if the active ratio has exceeded the threshold to cause aging, and if so signals the |
| // event. |
| void CheckActiveRatioAgingLocked() TA_REQ(lock_); |
| |
| // Checks if there is any pending age reason and either returns the reason, or a suggestion on |
| // how long to wait before checking again. This timeout does not take into account that other |
| // changes, namely the active ratio, could cause aging to be necessary before that timeout. |
| ktl::variant<AgeReason, zx_instant_mono_t> GetAgeReasonLocked() const TA_REQ(lock_); |
| |
| // Synchronizes with any outstanding aging. This is intended to allow a reclamation process to |
| // ensure it is not racing with, and falsely failing to reclaim, the aging thread due to |
| // scheduling or other delays. |
| void SynchronizeWithAging() TA_EXCL(lock_); |
| |
| // Helper that performs an instance of aging recording the reason, increment the mru generation |
| // and performing any needed notifications / triggers. |
| // The caller is responsible for only calling this if aging is possible, i.e. if |
| // CanIncrementMruGen is true. |
| void IncrementMruGenLocked(AgeReason age_reason) TA_REQ(lock_); |
| |
| // Attempts to perform aging with the given |age_reason|. This will either perform aging, or make |
| // non-zero progress towards being able to successfully age. Must be called with lock_ held, and |
| // takes ownership of the lock acquisition and releases it. |
| void TryAgingLocked(AgeReason age_reason, Guard<CriticalMutex>::Adoptable&& adopt); |
| |
| // Helper method that calculates whether the current active ratio would trigger aging. |
| bool IsActiveRatioTriggeringAging() TA_REQ(lock_); |
| |
| void LruThread(); |
| void MaybeTriggerLruProcessingLocked() TA_REQ(lock_); |
| bool NeedsLruProcessingLocked() const TA_REQ(lock_); |
| |
| // Returns true if a page is both in one of the Reclaim queues, and succeeds the passed in |
| // validator, which takes a fbl::RefPtr<VmCowPages>. |
| template <typename F> |
| bool DebugPageIsSpecificReclaim(const vm_page_t* page, F validator, size_t* queue) const; |
| |
| // Returns true if a page is both in the specified |queue|, and succeeds the passed in validator, |
| // which takes a fbl::RefPtr<VmCowPages>. |
| template <typename F> |
| bool DebugPageIsSpecificQueue(const vm_page_t* page, PageQueue queue, F validator) const; |
| |
| // Records that |pages| have potentially changed queue impacting the active/inactive ratio, and |
| // returns |true| if checking the active ratio can be skipped. |
| bool RecordActiveRatioSkips(size_t pages) { |
| // Add the pages to the skip count and check if our specific addition caused the count to cross |
| // the threshold. This prevents a thundering herd of threads all noticing once the count passes |
| // the threshold. |
| uint64_t old_count = lazy_active_ratio_aging_skips_.fetch_add(pages); |
| if (unlikely(old_count < kActiveInactiveErrorMargin && |
| old_count + pages >= kActiveInactiveErrorMargin)) { |
| // Reset the skips counter to zero. This possibly loses some counts, but as the active ratio |
| // has not yet been checked, this is fine. |
| lazy_active_ratio_aging_skips_ = 0; |
| return false; |
| } |
| return true; |
| } |
| |
| // Internal helper for MarkAccessed that handles the case where the page might be in the Isolate |
| // queue. Pages in the isolate queue must actually be moved out of the list, and this requires |
| // taking the lock. |
| void MarkAccessedMaybeIsolate(vm_page_t* page); |
| |
| // When holding the PageQueue lock, and performing an operation on an arbitrary number of pages, |
| // the "operation batch size" controls the number of pages for which the lock will be held before |
| // checking for contention and potentially releasing the lock if contended, allowing other |
| // operations to proceed. |
| static constexpr size_t kOpBatchSize = 64; |
| |
| // Helper that checks if iterations is at a multiple of the kOpBatchSize, and if so whether or not |
| // the lock is presently contested and hence should be yielded. |
| bool BatchOpShouldDropLock(size_t iterations) const { |
| if ((iterations % kOpBatchSize) == 0) { |
| return list_lock_.lock().IsContested(); |
| } |
| return false; |
| } |
| |
| // Returns whether or not it is permissible to increase the mru generation, or if the lru |
| // generation would need incrementing first. This is marked as requiring lock_ as, even though the |
| // annotation is not exercised, the return value of this method is only useful / non-racy if the |
| // mru/lru generations cannot change, which requires holding the lock to guarantee. |
| bool CanIncrementMruGenLocked() const TA_REQ(lock_) { |
| return mru_gen_.load(ktl::memory_order_relaxed) - lru_gen_.load(ktl::memory_order_relaxed) < |
| kNumReclaim - 1; |
| } |
| |
| // Similar to |CanIncrementMruGenLocked|, but for the lru. |
| bool CanIncrementLruGenLocked() const TA_REQ(lock_) { |
| return mru_gen_.load(ktl::memory_order_relaxed) - lru_gen_.load(ktl::memory_order_relaxed) > |
| kNumActiveQueues; |
| } |
| |
| // The list_lock_ is used to protect the linked lists queues as these cannot be implemented with |
| // atomics. A few related members are also protected with this lock, such as the isolate_cursor. |
| // The purpose of this separate spinlock, compared to the general lock_, is so that latency |
| // sensitive operations, such as adding / removing pages, that only need to modify the list, can |
| // happen without false contention with other page queues operations. |
| DECLARE_CRITICAL_MUTEX(PageQueues) mutable list_lock_; |
| |
| // General lock used to protect all logic and members that are not part of critical latency |
| // sensitive operations. |
| // Where both locks need to be acquired, lock_ must be acquired prior to the list_lock_. |
| DECLARE_CRITICAL_MUTEX(PageQueues) mutable lock_; |
| |
| // Externally supplied event that we should signal anytime aging occurs. |
| Event* aging_event_ TA_GUARDED(lock_) = nullptr; |
| |
| // Records whether or not the active aging via the mru thread should be disabled or not. |
| bool aging_disabled_ TA_GUARDED(lock_) = false; |
| |
| // Time at which the mru_gen_ was last incremented. |
| ktl::atomic<zx_instant_mono_t> last_age_time_ = ZX_TIME_INFINITE_PAST; |
| // Reason the last aging event happened, this is purely for informational/debugging purposes. |
| // Initialized to Timeout as a somewhat arbitrary choice. |
| AgeReason last_age_reason_ TA_GUARDED(lock_) = AgeReason::Timeout; |
| // Tracks whether the active ratio has been tripped and should contribute as an aging trigger. |
| // This is stored as a boolean so that it is sticky in the advent of a race with additional |
| // modifications to the page queues. Were this not sticky then, in the absence of a debounce |
| // threshold, we could repeatedly trigger the active ratio on and off, causing the aging thread |
| // to repeatedly wake up, miss the trigger, and do nothing. |
| bool active_ratio_triggered_ TA_GUARDED(lock_) = false; |
| // Used to signal the mru thread that it should wake up and check if the mru generation needs |
| // incrementing. This must be signaled if active_ratio_triggered_ transitions false->true or if |
| // the lru_gen_ is incremented. Over signalling is safe, just less efficient. Only the MruThread |
| // is permitted to wait on this. |
| AutounsignalEvent mru_event_; |
| // Used to signal the lru thread that it should wake up and check if the lru queue needs |
| // processing. This must be signaled if the mru_gen_ is modified such that NeedsLruProcessLocked() |
| // becomes true. Over signalling is safe, just less efficient. Only the LruThread is permitted to |
| // wait on this. |
| AutounsignalEvent lru_event_; |
| |
| // What to do with pages when processing the LRU queue. |
| LruAction lru_action_ TA_GUARDED(lock_) = LruAction::None; |
| |
| // The page queues are placed into an array, indexed by page queue, for consistency and uniformity |
| // of access. This does mean that the list for PageQueueNone does not actually have any pages in |
| // it, and should always be empty. |
| // The reclaimable queues are the more complicated as, unlike the other categories, pages can be |
| // in one of the queues, and can move around. The reclaimable queues themselves store pages that |
| // are roughly grouped by their last access time. The relationship is not precise as pages are not |
| // moved between queues unless it becomes strictly necessary. This is in contrast to the queue |
| // counts that are always up to date. |
| // |
| // What this means is that the vm_page::page_queue index is always up to do date, and the |
| // page_queue_counts_ represent an accurate count of pages with that vm_page::page_queue index, |
| // but counting the pages actually in the linked list may not yield the correct number. |
| // |
| // New reclaimable pages are always placed into the queue associated with the MRU generation. If |
| // they get accessed the vm_page_t::page_queue gets updated along with the counts. At some point |
| // the LRU queue will get processed (see |ProcessIsolateAndLruQueues|) and this will cause pages |
| // to get relocated to their correct list. |
| // |
| // Consider the following example: |
| // |
| // LRU MRU LRU MRU LRU MRU LRU MRU MRU LRU |
| // | | | | | | | | | | |
| // | | Insert A | | Age | | Touch A | | Age | | |
| // V v Queue=2 v v Queue=2 v v Queue=3 v v Queue=3 v v |
| // [][ ][ ][] -------> [][ ][a][] -------> [][ ][a][ ] -------> [][ ][a][ ] -------> [ ][ ][a][] |
| // |
| // At this point page A, in its vm_page_t, has its queue marked as 3, and the page_queue_counts |
| // are {0,0,1,0}, but the page itself remains in the linked list for queue 2. If the LRU queue is |
| // then processed to increment it we would do. |
| // |
| // MRU LRU MRU LRU MRU LRU |
| // | | | | | | |
| // | | Move LRU | | Move LRU | | |
| // V v Queue=3 v v Queue=3 v v |
| // [ ][ ][a][] -------> [ ][][a][] -------> [][ ][][a] |
| // |
| // In the second processing of the LRU queue it gets noticed that the page, based on |
| // vm_page_t::page_queue, is in the wrong queue and gets moved into the correct one. |
| // |
| // For specifics on how LRU and MRU generations map to LRU and MRU queues, see comments on |
| // |lru_gen_| and |mru_gen_|. |
| ktl::array<VmPageDoublyLinkedList, PageQueueNumQueues> page_queues_ TA_GUARDED(list_lock_); |
| |
| // When a page is in the PageQueueReclaimIsolate state, instead of being in the page_queues_ list |
| // it is in, potentially one of several different, isolate_queues_ lists. This is just an |
| // implementation simplification as there's no need to 'save' the memory of the unused list_node_t |
| // in the other page_queues_ array. Note that PageQueueReclaimIsolate state is not a queue index, |
| // i.e. a page will have the PageQueueReclaimIsolate state irrespective of which isolate_queues_ |
| // list it is in. |
| // Pages in the PageQueueReclaimIsolate state are always exactly in the isolate_queues_ list, and |
| // similarly any page in the isolate_queues_ list is exactly in the PageQueueReclaimIsolate state. |
| // In this way the isolate_queues_ are considered reclaimable, and are part of active/inactive |
| // tracking, but do not support the MarkAccessed fastpath. |
| ktl::array<VmPageDoublyLinkedList, kNumIsolateQueues> isolate_queues_ TA_GUARDED(list_lock_); |
| |
| // The generation counts are monotonic increasing counters and used to represent the effective age |
| // of the oldest and newest reclaimable queues. The page queues themselves are treated as a fixed |
| // size circular buffer that the generations map onto (see definition of |gen_to_queue|).This |
| // means all pages in the system have an age somewhere in [lru_gen_, mru_gen_] and so the lru and |
| // mru generations cannot drift apart by more than kNumReclaim, otherwise there would not be |
| // enough queues. |
| // A pages age being between [lru_gen_, mru_gen_] is not an invariant as MarkAccessed can race and |
| // mark pages as being in an invalid queue. This race will get noticed by ProcessLruQueues and |
| // the page will get updated at that point to have a valid queue. Importantly, whilst pages can |
| // think they are in a queue that is invalid, only valid linked lists in the page_queues_ will |
| // ever have pages in them. This invariant is easy to enforce as the page_queues_ are updated |
| // under a lock. |
| // These are atomic so they can be safely read without the lock held, however they are always |
| // modified with the lock hold. |
| ktl::atomic<uint64_t> lru_gen_ = 0; |
| ktl::atomic<uint64_t> mru_gen_ = kNumReclaim - 1; |
| |
| // Tracks the counts of pages in each queue in O(1) time complexity. As pages are moved between |
| // queues, the corresponding source and destination counts are decremented and incremented, |
| // respectively. |
| // |
| // The first entry of the array is left special: it logically represents pages not in any queue. |
| // For simplicity, it is initialized to zero rather than the total number of pages in the system. |
| // Consequently, the value of this entry is a negative number with absolute value equal to the |
| // total number of pages in all queues. This approach avoids unnecessary branches when updating |
| // counts. |
| ktl::array<ktl::atomic<size_t>, PageQueueNumQueues> page_queue_counts_ = {}; |
| |
| // Count for how many pages have moved queue without us recalculating the active ratio. This is a |
| // RelaxedAtomic to allow for completely skipping lock acquisition in MarkAccessed, except when |
| // the ratio actually needs to be recalculated. |
| RelaxedAtomic<uint64_t> lazy_active_ratio_aging_skips_ = 0; |
| |
| // Tracks the number of consecutive ProcessLruQueue iterations that skipped sweeping |
| // due to active unloans. |
| RelaxedAtomic<uint32_t> consecutive_skipped_sweeps_ = 0; |
| |
| // Track the mru and lru threads and have a signalling mechanism to shut them down. |
| ktl::atomic<bool> shutdown_threads_ = false; |
| Thread* mru_thread_ TA_GUARDED(lock_) = nullptr; |
| Thread* lru_thread_ TA_GUARDED(lock_) = nullptr; |
| |
| // Debug compressor is only available when debug asserts are also enabled. This ensures it can |
| // never have an impact on production builds. |
| #if DEBUG_ASSERT_IMPLEMENTED |
| ktl::unique_ptr<VmDebugCompressor> debug_compressor_ TA_GUARDED(list_lock_); |
| #endif |
| |
| // Queue rotation parameters. These are not locked as they are only read by the mru thread, and |
| // are set before the mru thread is started. |
| zx_duration_mono_t min_mru_rotate_time_; |
| zx_duration_mono_t max_mru_rotate_time_; |
| |
| // Determines if anonymous zero page forks are placed in the zero fork queue or in the reclaimable |
| // queue. |
| RelaxedAtomic<bool> zero_fork_is_reclaimable_ = false; |
| |
| // Determines if anonymous pages are placed in the reclaimable queues, or in their own non aging |
| // anonymous queues. |
| RelaxedAtomic<bool> anonymous_is_reclaimable_ = false; |
| |
| // Current active ratio multiplier. |
| int64_t active_ratio_multiplier_ TA_GUARDED(lock_) = 0; |
| }; |
| |
| #endif // ZIRCON_KERNEL_VM_INCLUDE_VM_PAGE_QUEUES_H_ |