<kernel v5.0>
Migration
non-movable 페이지의 migration 지원
커널은 전통적으로 movable LRU 페이지에 대해서만 migration을 지원해왔다. 최근 임베디드 시스템인 WbOS, android 등에서 많은 수의 non-movable 페이지들을 사용하여 왔다. 이러한 non-movable 페이지들이 많이 사용되면서 high order 할당에 문제가 생기는 리포트들이 보고되어왔다. 따라서 이러한 문제점들을 제거하기 위해 몇개의 노력을 해왔었지만 (예를 들면 압축 알고리즘 개선, slub fallback 시 0 order 할당, reserved 메모리, vmalloc, …) 여전히 non-movable 페이지들이 많이 사용되면 장기적으로는 효과가 없었다.
이 번에는 non-movable 페이지들을 movable이 가능하도록 지원하기 위해 아래 패치를 통해 드라이버(zram, GPU memory, …)들에 (*isolate_page), (*migratepage) 등의 후크 함수를 구현할 수 있도록 하였다. 이러한 후크 함수가 지원되는 드라이버의 경우 커널이 non-movable로 분류할지라도 이러한 드라이버를 통해서 migration 할 수 있게 하였다.
다음 그림은 페이지 유형별 isolation 및 migration 지원 여부를 보여준다.
migrate_pages()
mm/migrate.c
/* * migrate_pages - migrate the pages specified in a list, to the free pages * supplied as the target for the page migration * * @from: The list of pages to be migrated. * @get_new_page: The function used to allocate free pages to be used * as the target of the page migration. * @put_new_page: The function used to free target pages if migration * fails, or NULL if no special handling is necessary. * @private: Private data to be passed on to get_new_page() * @mode: The migration mode that specifies the constraints for * page migration, if any. * @reason: The reason for page migration. * * The function returns after 10 attempts or if no pages are movable any more * because the list has become empty or no retryable pages exist any more. * The caller should call putback_movable_pages() to return pages to the LRU * or free list only if ret != 0. * * Returns the number of pages that were not migrated, or an error code. */
int migrate_pages(struct list_head *from, new_page_t get_new_page, free_page_t put_new_page, unsigned long private, enum migrate_mode mode, int reason) { int retry = 1; int nr_failed = 0; int nr_succeeded = 0; int pass = 0; struct page *page; struct page *page2; int swapwrite = current->flags & PF_SWAPWRITE; int rc; if (!swapwrite) current->flags |= PF_SWAPWRITE; for(pass = 0; pass < 10 && retry; pass++) { retry = 0; list_for_each_entry_safe(page, page2, from, lru) { retry: cond_resched(); if (PageHuge(page)) rc = unmap_and_move_huge_page(get_new_page, put_new_page, private, page, pass > 2, mode, reason); else rc = unmap_and_move(get_new_page, put_new_page, private, page, pass > 2, mode, reason); switch(rc) { case -ENOMEM: /* * THP migration might be unsupported or the * allocation could've failed so we should * retry on the same page with the THP split * to base pages. * * Head page is retried immediately and tail * pages are added to the tail of the list so * we encounter them after the rest of the list * is processed. */ if (PageTransHuge(page) && !PageHuge(page)) { lock_page(page); rc = split_huge_page_to_list(page, from); unlock_page(page); if (!rc) { list_safe_reset_next(page, page2, lru); goto retry; } } nr_failed++; goto out; case -EAGAIN: retry++; break; case MIGRATEPAGE_SUCCESS: nr_succeeded++; break; default: /* * Permanent failure (-EBUSY, -ENOSYS, etc.): * unlike -EAGAIN case, the failed page is * removed from migration page list and not * retried in the next outer loop. */ nr_failed++; break; } } } nr_failed += retry; rc = nr_failed; out: if (nr_succeeded) count_vm_events(PGMIGRATE_SUCCESS, nr_succeeded); if (nr_failed) count_vm_events(PGMIGRATE_FAIL, nr_failed); trace_mm_migrate_pages(nr_succeeded, nr_failed, mode, reason); if (!swapwrite) current->flags &= ~PF_SWAPWRITE; return rc; }
최대 10번을 시도하여 migrate 스캐너가 isolation한 페이지를 unmap 한 후 free 스캐너가 isolation한 free 페이지로 migration한다.
- 코드 라인 11~15에서 현재 태스크가 swap write를 지원하지 않는 경우 migration을 하는 동안만 swap write를 지원하도록 플래그를 추가한다.
- 코드 라인 17~18에서 최대 반복 횟수를 10번으로 제한을 한다.
- 코드 라인 20에서 인수로 전달받은 @from 리스트의 페이지들 만큼 루프를 돈다.
- 코드 라인 24~31에서 huge 페이지 또는 일반 페이지의 unmap과 move를 수행한다. 10번 시도 중 4번째 시도 부터는 force 값을 1로 하여 full sync 모드에서 writeback 페이지들도 강제로 writeback이 끝날 때까지 대기하도록 강제한다.
- 코드 라인 33~56에서 migration 결과가 메모리 부족인 경우 처리를 중단한다. 만일 TransHuge 페이지인 경우에 한해 페이지를 split 한 후 retry한다.
- 코드 라인 57~59에서 migration을 다시 시도해야 하는 경우이다.
- 코드 라인 60~62에서 migration 결과가 성공한 경우이다.
- 코드 라인 63~72에서 migration 결과가 실패한 경우이다.
- 코드 라인 77에서 10번을 시도하고 완료하였거나 메모리 부족으로 처리를 완료하고자 도달하는 out 레이블이다.
- 코드 라인 84~85에서 현재 태스크에 설정해둔 swap writing 을 원래 상태로 돌려놓는다.
일반 페이지의 Migration
unmap_and_move()
mm/migrate.c -1/2-
/* * Obtain the lock on page, remove all ptes and migrate the page * to the newly allocated page in newpage. */
static ICE_noinline int unmap_and_move(new_page_t get_new_page, free_page_t put_new_page, unsigned long private, struct page *page, int force, enum migrate_mode mode, enum migrate_reason reason) { int rc = MIGRATEPAGE_SUCCESS; struct page *newpage; if (!thp_migration_supported() && PageTransHuge(page)) return -ENOMEM; newpage = get_new_page(page, private); if (!newpage) return -ENOMEM; if (page_count(page) == 1) { /* page was freed from under us. So we are done. */ ClearPageActive(page); ClearPageUnevictable(page); if (unlikely(__PageMovable(page))) { lock_page(page); if (!PageMovable(page)) __ClearPageIsolated(page); unlock_page(page); } if (put_new_page) put_new_page(newpage, private); else put_page(newpage); goto out; } rc = __unmap_and_move(page, newpage, force, mode); if (rc == MIGRATEPAGE_SUCCESS) set_page_owner_migrate_reason(newpage, reason);
migrate 스캐너가 isolation한 페이지를 unmap한 후 free 스캐너가 isolation한 free 페이지로 migration한다.
- 코드 라인 10~11에서 thp(Transparent Huge Page) migration이 지원하지 않을 때 thp 에 대해 -ENOMEM을 반환한다.
- 코드 라인 13~15에서 @get_new_page 함수를 통해 free 페이지를 가져온다.
- compaction 시에는 @get_new_page에 compaction_alloc() 함수를 사용하여 free 스캐너가 관리하는 리스트에서 선두의 free 페이지를 가져온다.
- 코드 라인 17~32에서 page가 이미 free 된 경우 페이지의 active 및 unevictable 플래그를 클리어한다.
- 코드 라인 21~26에서 낮은 확률로 non-lru movable 페이지이지만 드라이버에 (*isolate_page) 후크 함수의 구현이 없는 경우 isolated 플래그를 클리어한다.
- 코드 라인 27~31에서 @put_new_page 함수가 주어진 경우 이 함수를 통해 free 페이지를 다시 되돌려 놓고, 지정되지 않은 경우 페이지 를 버디 시스템으로 돌려 놓는다.
- compaction 시에는 @put_new_page에 compaction_free() 함수를 사용하여 free 스캐너가 관리하는 리스트로 되돌려 놓는다.
- 코드 라인 34~36에서 매핑을 푼 후 page를 newpage로 migration 한다. migration이 성공한 경우 디버그를 위해 페이지 owner에 reason을 기록해둔다.
mm/migrate.c -2/2-
out: if (rc != -EAGAIN) { /* * A page that has been migrated has all references * removed and will be freed. A page that has not been * migrated will have kepts its references and be * restored. */ list_del(&page->lru); /* * Compaction can migrate also non-LRU pages which are * not accounted to NR_ISOLATED_*. They can be recognized * as __PageMovable */ if (likely(!__PageMovable(page))) mod_node_page_state(page_pgdat(page), NR_ISOLATED_ANON + page_is_file_cache(page), -hpage_nr_pages(page)); } /* * If migration is successful, releases reference grabbed during * isolation. Otherwise, restore the page to right list unless * we want to retry. */ if (rc == MIGRATEPAGE_SUCCESS) { put_page(page); if (reason == MR_MEMORY_FAILURE) { /* * Set PG_HWPoison on just freed page * intentionally. Although it's rather weird, * it's how HWPoison flag works at the moment. */ if (set_hwpoison_free_buddy_page(page)) num_poisoned_pages_inc(); } } else { if (rc != -EAGAIN) { if (likely(!__PageMovable(page))) { putback_lru_page(page); goto put_new; } lock_page(page); if (PageMovable(page)) putback_movable_page(page); else __ClearPageIsolated(page); unlock_page(page); put_page(page); } put_new: if (put_new_page) put_new_page(newpage, private); else put_page(newpage); } return rc; }
- 코드 라인 1~19에서 out: 레이블이다. migration 결과가 -EAGAIN이 아닌 경우 페이지를 LRU 리스트에서 분리한다. 그리고 높은 확률로 non-lru movable 페이지가 아닌 경우 nr_isolated_anon 또는 nr_isolated_file 카운터를 감소시킨다.
- 코드 라인 26~36에서 migrate가 성공한 경우 페이지의 참조 카운터를 감소시켜 버디 시스템에 돌려보낸다.
- 코드 라인 37~42에서 migrate 결과가 -EAGAIN인 경우 movable 매핑된 페이지가 아니면 LRU 리스트에 되돌려 놓는다.
- 코드 라인 44~50에서 migrate 실패한 경우의 처리이다. non-lru movable free 페이지에 대해서는 파일 시스템의 (*putback_page) 후크 함수를 통해 원 위치로 돌려놓고, lru movable인 경우 원래 있었던 위치인 LRU 리스트로 되돌린다.
- 코드 라인 52~56에서 put_new: 레이블이다. @put_new_page가 주어진 경우 해당 함수를 통해 free 페이지를 다시 되돌려 놓는다. 그렇지 않은 경우 버디 시스템으로 되돌려 놓는다.
__unmap_and_move()
mm/migrate.c -1/3-
static int __unmap_and_move(struct page *page, struct page *newpage, int force, enum migrate_mode mode) { int rc = -EAGAIN; int page_was_mapped = 0; struct anon_vma *anon_vma = NULL; bool is_lru = !__PageMovable(page); if (!trylock_page(page)) { if (!force || mode == MIGRATE_ASYNC) goto out; /* * It's not safe for direct compaction to call lock_page. * For example, during page readahead pages are added locked * to the LRU. Later, when the IO completes the pages are * marked uptodate and unlocked. However, the queueing * could be merging multiple pages for one bio (e.g. * mpage_readpages). If an allocation happens for the * second or third page, the process can end up locking * the same page twice and deadlocking. Rather than * trying to be clever about what pages can be locked, * avoid the use of lock_page for direct compaction * altogether. */ if (current->flags & PF_MEMALLOC) goto out; lock_page(page); } if (PageWriteback(page)) { /* * Only in the case of a full synchronous migration is it * necessary to wait for PageWriteback. In the async case, * the retry loop is too short and in the sync-light case, * the overhead of stalling is too much */ switch (mode) { case MIGRATE_SYNC: case MIGRATE_SYNC_NO_COPY: break; default: rc = -EBUSY; goto out_unlock; } if (!force) goto out_unlock; wait_on_page_writeback(page); }
page를 unmapping 하고 newpage에 migration한다.
- 코드 라인 7에서 lru movable 페이지 여부를 is_lru에 담는다.
- true=lru movable페이지
- false=non-lru movable 페이지
- 코드 라인 9~30에서 에서 page에 대한 lock을 획득한다. 만일 락 획득 시도가 실패한 경우 @force가 0이거나 async migrate 모드인 경우 -EAGAIN 에러로 함수를 종료한다. 그리고 pfmemalloc상황에서 호출된 경우 migration이 이중으로 동작할 필요 없다.
- 코드 라인 32~50에서 page가 파일시스템에 writeback 중인 경우 sync 또는 sync_no_copy인 경우 -EAGAIN 에러를 반환한다. 단 @force가 강제된 경우 writeback이 완료될 때까지 대기한다. 그 외의 async 및 sync_light 모드의 경우 -EBUSY 에러를 반환한다.
mm/migrate.c -2/3-
. /* * By try_to_unmap(), page->mapcount goes down to 0 here. In this case, * we cannot notice that anon_vma is freed while we migrates a page. * This get_anon_vma() delays freeing anon_vma pointer until the end * of migration. File cache pages are no problem because of page_lock() * File Caches may use write_page() or lock_page() in migration, then, * just care Anon page here. * * Only page_get_anon_vma() understands the subtleties of * getting a hold on an anon_vma from outside one of its mms. * But if we cannot get anon_vma, then we won't need it anyway, * because that implies that the anon page is no longer mapped * (and cannot be remapped so long as we hold the page lock). */ if (PageAnon(page) && !PageKsm(page)) anon_vma = page_get_anon_vma(page); /* * Block others from accessing the new page when we get around to * establishing additional references. We are usually the only one * holding a reference to newpage at this point. We used to have a BUG * here if trylock_page(newpage) fails, but would like to allow for * cases where there might be a race with the previous use of newpage. * This is much like races on refcount of oldpage: just don't BUG(). */ if (unlikely(!trylock_page(newpage))) goto out_unlock; if (unlikely(!is_lru)) { rc = move_to_new_page(newpage, page, mode); goto out_unlock_both; } /* * Corner case handling: * 1. When a new swap-cache page is read into, it is added to the LRU * and treated as swapcache but it has no rmap yet. * Calling try_to_unmap() against a page->mapping==NULL page will * trigger a BUG. So handle it here. * 2. An orphaned page (see truncate_complete_page) might have * fs-private metadata. The page can be picked up due to memory * offlining. Everywhere else except page reclaim, the page is * invisible to the vm, so the page can not be migrated. So try to * free the metadata, so the page can be freed. */ if (!page->mapping) { VM_BUG_ON_PAGE(PageAnon(page), page); if (page_has_private(page)) { try_to_free_buffers(page); goto out_unlock_both; } } else if (page_mapped(page)) { /* Establish migration ptes */ VM_BUG_ON_PAGE(PageAnon(page) && !PageKsm(page) && !anon_vma, page); try_to_unmap(page, TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS); page_was_mapped = 1; } if (!page_mapped(page)) rc = move_to_new_page(newpage, page, mode); if (page_was_mapped) remove_migration_ptes(page, rc == MIGRATEPAGE_SUCCESS ? newpage : page, false);
- 코드 라인 15~16에서 KSM(Kernel Shared Memory)을 제외한 anon 페이지인 경우anon_vma를구해온다.
- 코드 라인 29~32에서 낮은 확률로 non-lru movable 페이지인 경우 페이지를 new 페이지로 옮기고 매핑 제거 루틴을 수행할 필요가 없으므로 곧장 out_unlock_both: 레이블로 이동한다.
- 코드 라인 46~59에서 다음은 일반적이지 않은 코너 케이스에 해당하는 처리이다. 만일 anon 페이지이면서 별도의 버퍼를 사용하는(예: ksm) 경우 free 버퍼를 제거하고 함수를 빠져나간다. 그 외의 경우 이 페이지로 매핑된 모든 페이지 테이블에서 매핑을 해제하게 한다.
- 코드 라인 61~62에서 페이지 테이블에 매핑되지 않은 페이지인 경우 페이지를 new 페이지로 옮긴 후 매핑 제거 루틴을 수행할 필요 없이 곧바로 out_unlock_both 레이블로 이동한다.
- 코드 라인 64~66에서 페이지가 매핑되었었던 경우 기존 페이지에 연결된 모든 매핑을 새 페이지로 옮긴다.
mm/migrate.c -3/3-
out_unlock_both: unlock_page(newpage); out_unlock: /* Drop an anon_vma reference if we took one */ if (anon_vma) put_anon_vma(anon_vma); unlock_page(page); out: /* * If migration is successful, decrease refcount of the newpage * which will not free the page because new page owner increased * refcounter. As well, if it is LRU page, add the page to LRU * list in here. Use the old state of the isolated source page to * determine if we migrated a LRU page. newpage was already unlocked * and possibly modified by its owner - don't rely on the page * state. */ if (rc == MIGRATEPAGE_SUCCESS) { if (unlikely(!is_lru)) put_page(newpage); else putback_lru_page(newpage); } return rc; }
- 코드 라인 1~2에서 out_unlock_both: 레이블에서는 새 페이지에 대한 lock을 먼저 release 한다.
- 코드 라인 3~7에서 out_unlock: 레이블에서는 기존 페이지에 대해 lock을 release 한다. 만일 anon 페이지인 경우 anon_vma에 대한 사용이 완료되었으므로 참조카운터를 감소시킨다.
- 코드 라인 8~23에서 migration이 성공한 경우 사용 중으로 바뀐 newpage의 사용을 완료시킨다. lru movable 페이지인 경우에는 LRU 리스트로 되돌린다.
Non-lru movable 페이지 여부 확인
PageMovable()
mm/compaction.c
int PageMovable(struct page *page) { struct address_space *mapping; VM_BUG_ON_PAGE(!PageLocked(page), page); if (!__PageMovable(page)) return 0; mapping = page_mapping(page); if (mapping && mapping->a_ops && mapping->a_ops->isolate_page) return 1; return 0; } EXPORT_SYMBOL(PageMovable);
non-lru movable 페이지 여부를 반환한다.
- 코드 라인 6~7에서 non-lru movable 페이지가 아닌 경우 0을 반환한다.
- 코드 라인 9~13에서 매핑된 페이지의 드라이버에 (*isolate_page) 후크 함수가 지원되는 경우 1을 반환하고, 그렇지 않은 경우 0을 반환한다.
__PageMovable()
static __always_inline int __PageMovable(struct page *page) { return ((unsigned long)page->mapping & PAGE_MAPPING_FLAGS) == PAGE_MAPPING_MOVABLE; }
non-lru movable 페이지 여부를 반환한다.
writeback 완료까지 대기
wait_on_page_writeback()
include/linux/pagemap.h
/* * Wait for a page to complete writeback */ static inline void wait_on_page_writeback(struct page *page) { if (PageWriteback(page)) wait_on_page_bit(page, PG_writeback); }
page가 writeback이 완료될 때까지 기다린다.
새 페이지로 이동
move_to_new_page()
mm/migrate.c
/* * Move a page to a newly allocated page * The page is locked and all ptes have been successfully removed. * * The new page will have replaced the old page if this function * is successful. * * Return value: * < 0 - error code * MIGRATEPAGE_SUCCESS - success */
static int move_to_new_page(struct page *newpage, struct page *page, enum migrate_mode mode) { struct address_space *mapping; int rc = -EAGAIN; bool is_lru = !__PageMovable(page); VM_BUG_ON_PAGE(!PageLocked(page), page); VM_BUG_ON_PAGE(!PageLocked(newpage), newpage); mapping = page_mapping(page); if (likely(is_lru)) { if (!mapping) rc = migrate_page(mapping, newpage, page, mode); else if (mapping->a_ops->migratepage) /* * Most pages have a mapping and most filesystems * provide a migratepage callback. Anonymous pages * are part of swap space which also has its own * migratepage callback. This is the most common path * for page migration. */ rc = mapping->a_ops->migratepage(mapping, newpage, page, mode); else rc = fallback_migrate_page(mapping, newpage, page, mode); } else { /* * In case of non-lru page, it could be released after * isolation step. In that case, we shouldn't try migration. */ VM_BUG_ON_PAGE(!PageIsolated(page), page); if (!PageMovable(page)) { rc = MIGRATEPAGE_SUCCESS; __ClearPageIsolated(page); goto out; } rc = mapping->a_ops->migratepage(mapping, newpage, page, mode); WARN_ON_ONCE(rc == MIGRATEPAGE_SUCCESS && !PageIsolated(page)); } /* * When successful, old pagecache page->mapping must be cleared before * page is freed; but stats require that PageAnon be left as PageAnon. */ if (rc == MIGRATEPAGE_SUCCESS) { if (__PageMovable(page)) { VM_BUG_ON_PAGE(!PageIsolated(page), page); /* * We clear PG_movable under page_lock so any compactor * cannot try to migrate this page. */ __ClearPageIsolated(page); } /* * Anonymous and movable page->mapping will be cleard by * free_pages_prepare so don't reset it here for keeping * the type to work PageAnon, for example. */ if (!PageMappingFlags(page)) page->mapping = NULL; } out: return rc; }
페이지를 새로 할당 받은 페이지로 migration 한다.
- 코드 라인 6에서 movable 페이지가 lru 리스트에서 관리되는 페이지인지 여부를 알아온다.
- 코드 라인 11~28에서 lru movable 페이지의 migration을 수행한다. 처리 유형은 다음 3가지이다.
- A) anon 페이지의 migration
- B) swap 캐시 및 파일 캐시 페이지의 (*migratepage)를 사용한 migration
- C) swap 캐시 및 파일 캐시 페이지의 (*migratepage)가 없을 때 사용한 fallback migration
- 코드 라인 29~45에서 non-lru movable 페이지의 migration을 수행한다. 처리 유형은 1 가지이다.
- D) non-lru 페이지 migration을 수행한다. 단 파일 시스템에 (*migratepages) 후크가 구현되지 않은 경우 isolated 플래그를 제거하고 성공을 반환한다.
- 코드 라인 51~69에서 migration이 성공한 경우 기존 페이지는 이제 free 페이지가 된 경우이다. non-lru 페이지였던 경우 isolated 플래그를 제거한다. 그리고 매핑되었던 경우 매핑을 제거한다.
A) lru movable – anon 페이지의 migration
migrate_page()
mm/migrate.c
/* * Common logic to directly migrate a single LRU page suitable for * pages that do not use PagePrivate/PagePrivate2. * * Pages are locked upon entry and exit. */
int migrate_page(struct address_space *mapping, struct page *newpage, struct page *page, enum migrate_mode mode) { int rc; BUG_ON(PageWriteback(page)); /* Writeback must be complete */ rc = migrate_page_move_mapping(mapping, newpage, page, mode, 0); if (rc != MIGRATEPAGE_SUCCESS) return rc; if (mode != MIGRATE_SYNC_NO_COPY) migrate_page_copy(newpage, page); else migrate_page_states(newpage, page); return MIGRATEPAGE_SUCCESS; } EXPORT_SYMBOL(migrate_page);
lru movable 페이지를 새로 할당 받은 페이지로 매핑을 migration하고 copy 한다.
- 코드 라인 9~12에서 페이지를 migrate 한다.
- 코드 라인 14~17에서 MIGRATE_SYNC_NO_COPY 모드에서는 페이지 디스크립터 정보만 옮기지만, 그 외의 모드에서는 cpu에서 페이지 프레임 복사도 추가하여 수행한다.
B) swap 캐시 및 파일 캐시 페이지의 (*migratepage)를 사용한 migration
다음 루틴에서 지원한다.
- mm/shmem.c – migrate_page()
- mm/swap_state.c – migrate_page()
- fs/block_dev.c – buffer_migrate_page_norefs()
- fs/ubifs/file.c – ubifs_migrate_page()
- fs/ext2/inode.c – buffer_migrate_page()
- fs/btrfs/disk-io.c – btree_migratepage()
- fs/f2fs/checkpoint.c – f2fs_migrate_page()
- fs/xfs/xfs_aops.c – iomap_migrate_page()
- fs/hugetlbfs/inode.c – hugetlbfs_migrate_page()
- fs/nfs/file.c – nfs_migrate_page()
- …
C) fallback migrate
fallback_migrate_page()
mm/migrate.c
/* * Default handling if a filesystem does not provide a migration function. */
static int fallback_migrate_page(struct address_space *mapping, struct page *newpage, struct page *page, enum migrate_mode mode) { if (PageDirty(page)) { /* Only writeback pages in full synchronous migration */ switch (mode) { case MIGRATE_SYNC: case MIGRATE_SYNC_NO_COPY: break; default: return -EBUSY; } return writeout(mapping, page); } /* * Buffers may be managed in a filesystem specific way. * We must have no buffers or drop them. */ if (page_has_private(page) && !try_to_release_page(page, GFP_KERNEL)) return -EAGAIN; return migrate_page(mapping, newpage, page, mode); }
파일 시스템이 migration 기능을 지원하지 못할 때 default 호출되어 페이지를 migration 한다.
- 코드 라인 4~14에서 ditty 페이지이면서 MIGRATE_SYNC 또는 MIGRATE_SYNC_NO_COPY 모드로 동작하는 경우에는 페이지를 기록하여 dirty 상태를 클리어한 후 리턴한다. 그 외의 모드의 경우 -EBUSY를 반환한다.
- 코드 라인 20~22에서 private 페이지인 경우 파일 시스템 등이 생성한 해당 페이지에 대한 메타 데이터를 제거한다.
- 코드 라인 24에서 page를 새 페이지로 migration한다.
D) non-lru 페이지 migration
다음 루틴에서 지원한다.
- mm/zsmalloc.c – zs_page_migrate()
- mm/balloon_compaction.c – balloon_page_migrate()
- drivers/virtio/virtio_balloon.c – virtballoon_migratepage()
매핑 이동
migrate_page_move_mapping()
이 함수는 다음 루틴에서 호출되어 사용된다.
- mm/migrate.c – migrate_page()
- mm/migrate.c – __buffer_migrate_page()
- fs/iomap.c – iomap_migrate_page()
- fs/ubifs/file.c – ubifs_migrate_page()
- fs/f2fs/data.c – f2fs_migrate_page()
- fs/aio.c – aio_migratepage()
mm/migrate.c -1/2-
/* * Replace the page in the mapping. * * The number of remaining references must be: * 1 for anonymous pages without a mapping * 2 for pages with a mapping * 3 for pages with a mapping and PagePrivate/PagePrivate2 set. */
int migrate_page_move_mapping(struct address_space *mapping, struct page *newpage, struct page *page, enum migrate_mode mode, int extra_count) { XA_STATE(xas, &mapping->i_pages, page_index(page)); struct zone *oldzone, *newzone; int dirty; int expected_count = expected_page_refs(page) + extra_count; if (!mapping) { /* Anonymous page without mapping */ if (page_count(page) != expected_count) return -EAGAIN; /* No turning back from here */ newpage->index = page->index; newpage->mapping = page->mapping; if (PageSwapBacked(page)) __SetPageSwapBacked(newpage); return MIGRATEPAGE_SUCCESS; } oldzone = page_zone(page); newzone = page_zone(newpage); xas_lock_irq(&xas); if (page_count(page) != expected_count || xas_load(&xas) != page) { xas_unlock_irq(&xas); return -EAGAIN; } if (!page_ref_freeze(page, expected_count)) { xas_unlock_irq(&xas); return -EAGAIN; } /* * Now we know that no one else is looking at the page: * no turning back from here. */ newpage->index = page->index; newpage->mapping = page->mapping; page_ref_add(newpage, hpage_nr_pages(page)); /* add cache reference */ if (PageSwapBacked(page)) { __SetPageSwapBacked(newpage); if (PageSwapCache(page)) { SetPageSwapCache(newpage); set_page_private(newpage, page_private(page)); } } else { VM_BUG_ON_PAGE(PageSwapCache(page), page); }
페이지의 매핑을 newpage로 migration 한다.
- 코드 라인 10~22에서 매핑되지 않은 anon 페이지의 경우 새 페이지에 인덱스와 매핑 정보를 옮긴다. 그리고 SwapBacked 플래그가 설정된 경우 제거한다. 만일 참조 카운터가 expected_count와 다른 경우 -EAGAIN을 반환하고 그렇지 않은 경우 success를 반환한다.
- 코드 라인 27에서 xas array 락을 획득한다.
- 코드 라인 28~31에서 참조 카운터가 expected_count가 아니면 -EAGAIN을 반환한다.
- 코드 라인 33~36에서 참조 카운터를 0으로 리셋한다. 리셋 전 값이 expected_count가 아닌 경우 -EAGAIN을 반환한다.
- 코드 라인 42~44에서 매핑된 페이지의 새 페이지에 인덱스, 매핑, 참조카운터를 옮긴다.
- 코드 라인 45~53에서 SwapBacked 플래그를 옮긴다. 그리고 SwapCache 플래그도 옮기고 private 데이터에 저장된 값도 옮긴다.
mm/migrate.c -2/2-
/* Move dirty while page refs frozen and newpage not yet exposed */ dirty = PageDirty(page); if (dirty) { ClearPageDirty(page); SetPageDirty(newpage); } xas_store(&xas, newpage); if (PageTransHuge(page)) { int i; for (i = 1; i < HPAGE_PMD_NR; i++) { xas_next(&xas); xas_store(&xas, newpage + i); } } /* * Drop cache reference from old page by unfreezing * to one less reference. * We know this isn't the last reference. */ page_ref_unfreeze(page, expected_count - hpage_nr_pages(page)); xas_unlock(&xas); /* Leave irq disabled to prevent preemption while updating stats */ /* * If moved to a different zone then also account * the page for that zone. Other VM counters will be * taken care of when we establish references to the * new page and drop references to the old page. * * Note that anonymous pages are accounted for * via NR_FILE_PAGES and NR_ANON_MAPPED if they * are mapped to swap space. */ if (newzone != oldzone) { __dec_node_state(oldzone->zone_pgdat, NR_FILE_PAGES); __inc_node_state(newzone->zone_pgdat, NR_FILE_PAGES); if (PageSwapBacked(page) && !PageSwapCache(page)) { __dec_node_state(oldzone->zone_pgdat, NR_SHMEM); __inc_node_state(newzone->zone_pgdat, NR_SHMEM); } if (dirty && mapping_cap_account_dirty(mapping)) { __dec_node_state(oldzone->zone_pgdat, NR_FILE_DIRTY); __dec_zone_state(oldzone, NR_ZONE_WRITE_PENDING); __inc_node_state(newzone->zone_pgdat, NR_FILE_DIRTY); __inc_zone_state(newzone, NR_ZONE_WRITE_PENDING); } } local_irq_enable(); return MIGRATEPAGE_SUCCESS; } EXPORT_SYMBOL(migrate_page_move_mapping);
- 코드 라인 2~6에서 Dirty 플래그가 있는 경우 새 페이지에 옮기고, 기존 페이지는 제거한다.
- 코드 라인8에서 xas xarray에 새페이지를 저장한다. 기존에는 radix tree를 사용했었는데 xarray 데이터 구조로 변경하였다.
- 코드 라인 9~16에서 thp인 경우 소속된 각 페이지를 xas xarray에 저장한다.
- 코드 라인 23에서 페이지의 참조 카운터를 페이지 수 만큼 지정한다.
- 코드 라인 25에서 xas array 락을 release 한다.
- 코드 라인 38~51 존이 변경된 경우 관련 카운터들 값을 증감한다.
다음 그림은 페이지 유형에 따라 기존 페이지에서 새 페이지로 관련 속성들을 복사하는 모습을 보여준다.
Rmap walk를 통한 페이지 migration
remove_migration_ptes()
mm/migrate.c
/* * Get rid of all migration entries and replace them by * references to the indicated page. */
static void remove_migration_ptes(struct page *old, struct page *new) { struct rmap_walk_control rwc = { .rmap_one = remove_migration_pte, .arg = old, }; if (locked) rmap_walk_locked(new, &rwc); else rmap_walk(new, &rwc); }
@old 페이지에 연결된 모든 매핑을 @new 페이지로 옮긴다.
- 코드 라인 3~6에서 기존 페이지를 참고하는 모든 vma를 찾기위해 rmap walk를 사용하도록 준비한다.
- 코드 라인 8~11에서 rmap walk를 통해 관련 vma에 연결된 기존 매핑을 제거하고 새 페이지에 매핑을 옮긴다.
remove_migration_pte()
mm/migrate.c
/* * Restore a potential migration pte to a working pte entry */
static bool remove_migration_pte(struct page *page, struct vm_area_struct *vma, unsigned long addr, void *old) { struct page_vma_mapped_walk pvmw = { .page = old, .vma = vma, .address = addr, .flags = PVMW_SYNC | PVMW_MIGRATION, }; struct page *new; pte_t pte; swp_entry_t entry; VM_BUG_ON_PAGE(PageTail(page), page); while (page_vma_mapped_walk(&pvmw)) { if (PageKsm(page)) new = page; else new = page - pvmw.page->index + linear_page_index(vma, pvmw.address); #ifdef CONFIG_ARCH_ENABLE_THP_MIGRATION /* PMD-mapped THP migration entry */ if (!pvmw.pte) { VM_BUG_ON_PAGE(PageHuge(page) || !PageTransCompound(page), page); remove_migration_pmd(&pvmw, new); continue; } #endif get_page(new); pte = pte_mkold(mk_pte(new, READ_ONCE(vma->vm_page_prot))); if (pte_swp_soft_dirty(*pvmw.pte)) pte = pte_mksoft_dirty(pte); /* * Recheck VMA as permissions can change since migration started */ entry = pte_to_swp_entry(*pvmw.pte); if (is_write_migration_entry(entry)) pte = maybe_mkwrite(pte, vma); if (unlikely(is_zone_device_page(new))) { if (is_device_private_page(new)) { entry = make_device_private_entry(new, pte_write(pte)); pte = swp_entry_to_pte(entry); } else if (is_device_public_page(new)) { pte = pte_mkdevmap(pte); flush_dcache_page(new); } } else flush_dcache_page(new); #ifdef CONFIG_HUGETLB_PAGE if (PageHuge(new)) { pte = pte_mkhuge(pte); pte = arch_make_huge_pte(pte, vma, new, 0); set_huge_pte_at(vma->vm_mm, pvmw.address, pvmw.pte, pte); if (PageAnon(new)) hugepage_add_anon_rmap(new, vma, pvmw.address); else page_dup_rmap(new, true); } else #endif { set_pte_at(vma->vm_mm, pvmw.address, pvmw.pte, pte); if (PageAnon(new)) page_add_anon_rmap(new, vma, pvmw.address, false); else page_add_file_rmap(new, false); } if (vma->vm_flags & VM_LOCKED && !PageTransCompound(new)) mlock_vma_page(new); if (PageTransHuge(page) && PageMlocked(page)) clear_page_mlock(page); /* No need to invalidate - it was non-present before */ update_mmu_cache(vma, pvmw.address, pvmw.pte); } return true; }
@old 페이지에 연결된 모든 매핑을 @new 페이지로 옮긴다.
- 코드 라인 4~15에서 old 페이지에 대한 매핑이 모두 제거될 때까지 반복한다.
- Rmap -3- (PVMW) | 문c
- 코드 라인 16~20에서 migration될 새 페이지를 구한다.
- 코드 라인 24~28에서 pmd에 매핑된 thp 엔트리인 경우 pmd 엔트리로의 migration을 수행한 후 계속한다.
- 코드 라인 31~34에서 새 페이지를 사용하기로 참조 카운터를 증가시킨다. 그런 후 이 페이지에 대한 pte 엔트리를 준비한다. 기존 pte 엔트리의 soft dirty 상태도 옮긴다.
- 코드 라인 39~41에서 swap 엔트리가 migration 가능한 swap 엔트리에서 write 속성의 vma를 사용하면 pte 속성에 write 속성을 부가한다.
- migration이 시작한 이후로 permission이 변경될 수 있는데 VMA를 다시 write 속성을 조사하여 변경된 경우 추가한다.
- 코드 라인 43~52에서 새 페이지에 대한 데이터 캐시를 flush 한다.
- private 페이지를 운영하는 존 디바이스인 경우 새 페이지에 대한 pte 엔트리를 가져온다.
- private 페이지가 아닌 존 디바이스인 경우는 새 페이지에 대한 pte 엔트리를 가져오고 추가로 새 페이지에 대한 데이터 캐시를 flush한다.
- 코드 라인 55~62에서 새 페이지가 huge 페이지인 경우 정규 매핑을 추가하고, rmap에도 추가한다.
- 코드 라인 63~72에서 huge 페이지가 아닌 경우 정규 매핑을 추가하고, rmap에도 추가한다.
- 코드 라인 73~74에서 VM_LOCKED 플래그를 가진 vma에 thp 및 hugetlbfs 페이지가 아닌 경우에만 새 페이지에 mlock 플래그 설정을 한다.
- 코드 라인 76~77에서 만일 새 thp에 mlocked 플래그가 설정된 경우 클리어한다.
- 참고로 thp는 mlock 설정을 할 수 없다.
- 코드 라인 80에서 캐시 flush가 필요한 아키텍처에서 flush를 수행한다.
- ARMv6 이상에서는 아무런 동작도 수행하지 않고, ARMv6 미만에서 cache coherent를 위한 flush 루틴들이 동작한다.
페이지 프레임 복사
migrate_page_copy()
mm/migrate.c
void migrate_page_copy(struct page *newpage, struct page *page) { if (PageHuge(page) || PageTransHuge(page)) copy_huge_page(newpage, page); else copy_highpage(newpage, page); migrate_page_states(newpage, page); } EXPORT_SYMBOL(migrate_page_copy);
기존 페이지 프레임을 새 페이지 프레임으로 복사한다. 또한 관련 페이지 디스크립터도 복사한다.
- 코드 라인 3~4에서 huge 페이지 또는 thp인 경우의 복사 루틴을 수행한다.
- 코드 라인 5~6에서 그렇지 않은 일반 페이지의 경우의 복사 루틴을 수행한다.
- 코드 라인 8에서 페이지 디스크립터도 복사한다.
copy_huge_page()
mm/migrate.c
static void copy_huge_page(struct page *dst, struct page *src) { int i; int nr_pages; if (PageHuge(src)) { /* hugetlbfs page */ struct hstate *h = page_hstate(src); nr_pages = pages_per_huge_page(h); if (unlikely(nr_pages > MAX_ORDER_NR_PAGES)) { __copy_gigantic_page(dst, src, nr_pages); return; } } else { /* thp page */ BUG_ON(!PageTransHuge(src)); nr_pages = hpage_nr_pages(src); } for (i = 0; i < nr_pages; i++) { cond_resched(); copy_highpage(dst + i, src + i); } }
@src huge 페이지 프레임을 @dst로 복사한다.
- 코드 라인 6~14에서 hugetlbfs 페이지이면서 최대 order 페이지 수보다 큰 페이지인 경우 단순히 페이지 구조체 포인터를 증가시키는 것으로 정확히 동작하는 것을 보장하지 못한다. 따라서 페이지 디스크립터의 포인터를 정확히 처리하기 위해 별도의 함수에서 처리한다.
- 코드 라인 15~19에서 thp에 대한 페이지 수를 알아온다.
- 코드 라인 21~24에서 페이지 수만큼 순회하며 @src+i의 페이지 프레임을 @dst+i로 복사한다.
__copy_gigantic_page()
mm/migrate.c
/* * Gigantic pages are so large that we do not guarantee that page++ pointer * arithmetic will work across the entire page. We need something more * specialized. */
static void __copy_gigantic_page(struct page *dst, struct page *src, int nr_pages) { int i; struct page *dst_base = dst; struct page *src_base = src; for (i = 0; i < nr_pages; ) { cond_resched(); copy_highpage(dst, src); i++; dst = mem_map_next(dst, dst_base, i); src = mem_map_next(src, src_base, i); } }
최대 order 페이지 수 보다 큰 @src 페이지 프레임을 @dst로 복사한다.
mem_map_next()
mm/internal.h
/* * Iterator over all subpages within the maximally aligned gigantic * page 'base'. Handle any discontiguity in the mem_map. */
static inline struct page *mem_map_next(struct page *iter, struct page *base, int offset) { if (unlikely((offset & (MAX_ORDER_NR_PAGES - 1)) == 0)) { unsigned long pfn = page_to_pfn(base) + offset; if (!pfn_valid(pfn)) return NULL; return pfn_to_page(pfn); } return iter + 1; }
@iter 페이지의 다음 페이지를 구해 반환한다. 최대 order 페이지 수를 초과하는 페이지의 경우 섹션별로 관리되는 mem_map의 경계를 초과할 수 있어 잘못된 주소가 나올 가능성이 있으므로 그럴 때마다 페이지 디스크립터의 주소를 정확히 재산출한다.
- 코드 라인 4~9에서 낮은 확률로 offset이 최대 order 페이지 수(default: 1024 = 4M(4K 페이지)) 단위로 정렬된 경우에는 page 디스크립터 포인터를 증가시키면 mem_map의 경계를 초과할 수 있으므로 pfn 값을 먼저 구한 후 pfn_to_page() 함수를 사용하여 page 디스크립터를 다시 구한다.
- 코드 라인 10에서 다음 페이지 디스크립터를 반환한다.
copy_highpage()
include/linux/highmem.h
static inline void copy_highpage(struct page *to, struct page *from) { char *vfrom, *vto; vfrom = kmap_atomic(from); vto = kmap_atomic(to); copy_page(vto, vfrom); kunmap_atomic(vto); kunmap_atomic(vfrom); }
@from 페이지 프레임을 @to로 복사한다.
- 코드 라인 5~6에서 32비트 시스템에서 highmem 페이지인 경우 fixmap을 사용하여 임시로 매핑하도록 한다.
- 코드 라인 7에서 아키텍처가 지원하는 가장 빠른 방법으로 페이지 프레임을 복사한다.
- 코드 라인 8~9에서 fixmap에 임시 매핑한 페이지의 매핑을 해제한다.
페이지 디스크립터 정보 복사
migrate_page_states()
mm/migrate.c
/* * Copy the page to its new location */
void migrate_page_states(struct page *newpage, struct page *page) { int cpupid; if (PageError(page)) SetPageError(newpage); if (PageReferenced(page)) SetPageReferenced(newpage); if (PageUptodate(page)) SetPageUptodate(newpage); if (TestClearPageActive(page)) { VM_BUG_ON_PAGE(PageUnevictable(page), page); SetPageActive(newpage); } else if (TestClearPageUnevictable(page)) SetPageUnevictable(newpage); if (PageWorkingset(page)) SetPageWorkingset(newpage); if (PageChecked(page)) SetPageChecked(newpage); if (PageMappedToDisk(page)) SetPageMappedToDisk(newpage); /* Move dirty on pages not done by migrate_page_move_mapping() */ if (PageDirty(page)) SetPageDirty(newpage); if (page_is_young(page)) set_page_young(newpage); if (page_is_idle(page)) set_page_idle(newpage); /* * Copy NUMA information to the new page, to prevent over-eager * future migrations of this same page. */ cpupid = page_cpupid_xchg_last(page, -1); page_cpupid_xchg_last(newpage, cpupid); ksm_migrate_page(newpage, page); /* * Please do not reorder this without considering how mm/ksm.c's * get_ksm_page() depends upon ksm_migrate_page() and PageSwapCache(). */ if (PageSwapCache(page)) ClearPageSwapCache(page); ClearPagePrivate(page); set_page_private(page, 0); /* * If any waiters have accumulated on the new page then * wake them up. */ if (PageWriteback(newpage)) end_page_writeback(newpage); copy_page_owner(page, newpage); mem_cgroup_migrate(page, newpage); } EXPORT_SYMBOL(migrate_page_states);
기존 페이지 디스크립터 내용을 새 페이지의 디스크립터로 옮긴다.
- 코드 라인 5~6에서 PG_error 플래그를 옮긴다.
- 코드 라인 7~8에서 PG_referenced 플래그를 옮긴다.
- 코드 라인 9~10에서 PG_uptodate 플래그를 옮긴다.
- 코드 라인 11~15에서 PG_active 플래그 또는 PG_unevictable 플래그를 옮기고, 기존 페이지에서는 제거한다.
- 코드 라인 16~17에서 PG_workingset 플래그를 옮긴다.
- 코드 라인 18~19에서 PG_checked 플래그를 옮긴다.
- 코드 라인 20~21에서 PG_mappedtodist 플래그를 옮긴다.
- 코드 라인 24~25에서 PG_dirty 플래그를 옮긴다.
- 코드 라인 27~28에서 Young 플래그를 옮긴다.
- 32bit 시스템의 경우 page 구조체가 아닌 page_ext 구조체에 존재한다.
- 코드 라인 29~30에서 Idle 플래그를 옮긴다.
- 32bit 시스템의 경우 page 구조체가 아닌 page_ext 구조체에 존재한다.
- 코드 라인 36~37에서 cpupid 정보를 옮기고, 기존 페이지에는 -1을 대입한다.
- 코드 라인 39에서 ksm 페이지이면서, 복사할 대상이 statble 노드 매핑된 경우 stable 노드 매핑 정보를 제거한다.
- KSM(Kernel Same page Merging)이 application이 주소 공간에서 같은 내용을 가진 페이지를 스캔하여 한 페이지로 merge 시킬 수 있도록 하는 기능이다.
- 참고: How to use the Kernel Samepage Merging feature | kernel.org
- 코드 라인 44~45에서 기존 페이지의 SwapCache 정보를 제거한다.
- 코드 라인 46~47에서 기존 페이지의 Private 플래그를 제거하고, p->private에 0을 대입한다.
- 코드 라인 53~54에서 새 페이지에 Writeback 플래그가 설정된 경우 lru의 tail로 rotate하고, 관련 태스크를 깨워 즉각 회수가 가능하도록 한다.
- 코드 라인 56에서 page owner 정보를 옮긴다.
- 코드 라인 58에서 memcg 정보를 옮긴다.
참고
- Zoned Allocator -1- (물리 페이지 할당-Fastpath) | 문c
- Zoned Allocator -2- (물리 페이지 할당-Slowpath) | 문c
- Zoned Allocator -3- (Buddy 페이지 할당) | 문c
- Zoned Allocator -4- (Buddy 페이지 해지) | 문c
- Zoned Allocator -5- (Per-CPU Page Frame Cache) | 문c
- Zoned Allocator -6- (Watermark) | 문c
- Zoned Allocator -7- (Direct Compact) | 문c
- Zoned Allocator -8- (Direct Compact-Isolation) | 문c
- Zoned Allocator -9- (Direct Compact-Migration) | 문c – 현재 글
- Zoned Allocator -10- (LRU & pagevec) | 문c
- Zoned Allocator -11- (Direct Reclaim) | 문c
- Zoned Allocator -12- (Direct Reclaim-Shrink-1) | 문c
- Zoned Allocator -13- (Direct Reclaim-Shrink-2) | 문c
- Zoned Allocator -14- (Kswapd) | 문c