본문 바로가기
Kernel/Memory Management

compound page 정리

by hyeyoo 2022. 10. 5.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.
 

struct page 메모

struct page에 대한 간단한 노트 참고로 64비트 리눅스는 LP64를 사용한다. 이 글에서 자료형의 크기는 LP64에 따라서 서술되었다. Introuction struct page는 페이지 프레임 하나 (보통 4096 바이트)에 대한 정

hyeyoo.com

앞선 글에서는 struct page가 어떻게 생겼는지 간단하게 살펴봤다. compound page에서는 이 자료구조가 어떻게 처리되는지 알아보자.

What is compound page

일반적인 non-compound page (order == 0)와는 달리 compound page는 2^n개의 페이지 프레임을 하나의 페이지로 취급한 것이다. 아래 사진은 order-2 compound page를 그림으로 나타내 보았다. compound page에서는 가장 맨 앞에 있는 page가 head page이고, 그 외의 모든 페이지는 tail page라고 한다.

    pages = alloc_pages(GFP_KERNEL | __GFP_COMP, 2);

compound page를 얻는 방법은 간단하다. 버디 할당자에서 할당할 때 __GFP_COMP라는 gfp flag를 넘기면 된다.

    pages = alloc_pages(GFP_KERNEL, 2); /* no __GFP_COMP */

만약 __GFP_COMP를 넘기지 않고 그냥 order > 0 page를 할당하면, 이건 compound page가 아니다. [1] 물론 order == 2로 할당했으므로 물리적으로 연속적인 페이지 프레임 4개를 할당받은 건 맞는데, 커널이 보기에는 order-0 page 4개이지 compound page라고 여기지는 않는다.

커널은 어떤 page가 compound page인지 아닌지를 판단하기 위해 어떤 트릭을 사용하는지 살펴보자.

 

struct page {
[...]
		struct {	/* Tail pages of compound page */
			unsigned long compound_head;	/* Bit zero is set */

			/* First tail page only */
			unsigned char compound_dtor;
			unsigned char compound_order;
			atomic_t compound_mapcount;
			atomic_t compound_pincount;
#ifdef CONFIG_64BIT
			unsigned int compound_nr; /* 1 << compound_order */
#endif
		};
[...]
} _struct_page_alignment;

 

compound page의 tail page들은 위의 필드들을 사용하니 참고하자. (second tail page 제외)

그림을 그려봤다. 어떤 페이지가 head page라면 PG_head 플래그가 켜져있어야 하고, tail page라면 compound_head의 비트 0이 1이어야 한다. 따라서 compound_head와 같은 오프셋을 갖는 필드의 비트 0이 1이면 compound page가 아니더라도 tail page라고 착각할 수 있다.

struct page {
[...]
		struct {	/* Page table pages */
			unsigned long _pt_pad_1;	/* compound_head */
			pgtable_t pmd_huge_pte; /* protected by page->ptl */
			unsigned long _pt_pad_2;	/* mapping */
			union {
				struct mm_struct *pt_mm; /* x86 pgds only */
				atomic_t pt_frag_refcount; /* powerpc */
			};
#if ALLOC_SPLIT_PTLOCKS
			spinlock_t *ptl;
#else
			spinlock_t ptl;
#endif
		};
[...]
} _struct_page_alignment;

예를 들어서 어떤 page table page의 struct page에서 어떤 이유로 _pt_pad_1 필드를 사용하게 되었다면 반드시 비트 0이 항상 0이도록 보장해야 한다. (최소 2 byte로 align된 포인터를 저장한다던가 등등) 또한 tail page는 head page의 주소를 compound_head로 가리킨다.

PageCompound()

static __always_inline int PageCompound(struct page *page)
{
	return test_bit(PG_head, &page->flags) ||
	       READ_ONCE(page->compound_head) & 1;
}

어떤 페이지가 compound page (head or tail)인지 확인하는 데에 사용되는 함수다. head page (PG_head set)이거나 tail page (compound_head & 1) 인지를 확인한다.

PageHead()

static __always_inline int PageHead(struct page *page)
{
	PF_POISONED_CHECK(page);
	return test_bit(PG_head, &page->flags) && !page_is_fake_head(page);
}

PageHead()는 PG_head 플래그를 확인해서 이 페이지가 head page인지를 확인한다. page_is_fake_head()는 HugeTLB에서 메모리 overhead를 줄이기 위한 부분인데 이 글에서는 자세히 설명하지 않는다.

PageTail()

static __always_inline int PageTail(struct page *page)
{
	return READ_ONCE(page->compound_head) & 1 || page_is_fake_head(page);
}

PageTail()은 compound_head의 비트 0을 확인해서 이 페이지가 tail page인지를 확인한다.

First and second tail page

compound page에서 first tail page는 특별하다. head page의 struct page에 모든 메타데이터를 넣을 수 없기 때문에 compound page에서는 first tail page의 struct page에 몇가지 메타데이터를 저장한다. 이러한 이유로 order-0 compound page는 만들 수 없다. (최소 order >= 1 이어야 한다.)

THP의 compound page는 order > 1이기 때문에 second tail page도 별도의 용도로 사용하는데 이 글에서 자세히 다루지는 않는다.

preparing compound page in buddy

compound page에 어떤 메타데이터를 저장하는지 보기 위해서 buddy의 일부분을 살펴보자. buddy 할당자에서는 order > 0이고 (order == 0인 경우에는 무시) __GFP_COMP 플래그가 켜진 경우 compound page 초기화 루틴을 실행한다.

 

static void prep_new_page(struct page *page, unsigned int order, gfp_t gfp_flags,
							unsigned int alloc_flags)
{
	post_alloc_hook(page, order, gfp_flags);

	if (order && (gfp_flags & __GFP_COMP))
		prep_compound_page(page, order);

	/*
	 * page is set pfmemalloc when ALLOC_NO_WATERMARKS was necessary to
	 * allocate the page. The expectation is that the caller is taking
	 * steps that will free more memory. The caller should avoid the page
	 * being used for !PFMEMALLOC purposes.
	 */
	if (alloc_flags & ALLOC_NO_WATERMARKS)
		set_page_pfmemalloc(page);
	else
		clear_page_pfmemalloc(page);
}

 

/*
 * Higher-order pages are called "compound pages".  They are structured thusly:
 *
 * The first PAGE_SIZE page is called the "head page" and have PG_head set.
 *
 * The remaining PAGE_SIZE pages are called "tail pages". PageTail() is encoded
 * in bit 0 of page->compound_head. The rest of bits is pointer to head page.
 *
 * The first tail page's ->compound_dtor holds the offset in array of compound
 * page destructors. See compound_page_dtors.
 *
 * The first tail page's ->compound_order holds the order of allocation.
 * This usage means that zero-order pages may not be compound.
 */

 

친절하게 주석도 있다. first tail page에는 compound_dtor라는 page destructor의 index와 compound_order로 compound page의 order를 저장한다고 한다.

compound_dtor, compound_page_dtors

compound_page_dtor * const compound_page_dtors[NR_COMPOUND_DTORS] = {
	[NULL_COMPOUND_DTOR] = NULL,
	[COMPOUND_PAGE_DTOR] = free_compound_page,
#ifdef CONFIG_HUGETLB_PAGE
	[HUGETLB_PAGE_DTOR] = free_huge_page,
#endif
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
	[TRANSHUGE_PAGE_DTOR] = free_transhuge_page,
#endif
};

 

first tail page의 page->compound_dtor에는 compound_page_dtors 배열에 대한 인덱스가 저장된다. 배열의 원소는 각각 페이지가 해제될 때 호출되어야 할 callback 함수이다. compound_dtor은 기본적으로 COMPOUND_PAGE_DTOR로 설정하지만 이후에 THP나 HugeTLB에서 필요에 따라 바꿔준다.

 

void free_compound_page(struct page *page)
{
	mem_cgroup_uncharge(page_folio(page));
	free_the_page(page, compound_order(page));
}

 

free_compound_page()는 memcg uncharging을 수행하고 first tail page에 기록된 order를 free_the_page()로 넘겨서 해제한다.

 

static void prep_compound_head(struct page *page, unsigned int order)
{
	set_compound_page_dtor(page, COMPOUND_PAGE_DTOR);
	set_compound_order(page, order);
	atomic_set(compound_mapcount_ptr(page), -1);
	atomic_set(compound_pincount_ptr(page), 0);
}

static void prep_compound_tail(struct page *head, int tail_idx)
{
	struct page *p = head + tail_idx;

	p->mapping = TAIL_MAPPING;
	set_compound_head(p, head);
}

void prep_compound_page(struct page *page, unsigned int order)
{
	int i;
	int nr_pages = 1 << order;

	__SetPageHead(page);
	for (i = 1; i < nr_pages; i++)
		prep_compound_tail(page, i);

	prep_compound_head(page, order);
}

 

prep_compound_page()는 head page에 PG_head를 설정하고, 각각 prep_compound_head()와 prep_compound_tail()로 head/tail page의 struct page를 초기화한다.

_mapcount of compound page

예전에는 compound page는 전체를 사용자 공간에 매핑하거나 아예 매핑하지 않거나 둘 중 하나만 가능했었다. 하지만 [2]에 의해서 compound page의 일부 sub-page들만 사용자 공간에 매핑할 수 있게 되면서 _mapcount가 조금 바뀌었다. 

[2]가 도입될 당시의 diff를 좀 가져와서 살펴보자.

diff --git a/include/linux/mm.h b/include/linux/mm.h
index dad667d99304..33cb3aa647a6 100644
--- a/include/linux/mm.h
+++ b/include/linux/mm.h
@@ -393,6 +393,19 @@ static inline int is_vmalloc_or_module_addr(const void *x)
 
 extern void kvfree(const void *addr);
 
+static inline atomic_t *compound_mapcount_ptr(struct page *page)
+{
+	return &page[1].compound_mapcount;
+}
+
+static inline int compound_mapcount(struct page *page)
+{
+	if (!PageCompound(page))
+		return 0;
+	page = compound_head(page);
+	return atomic_read(compound_mapcount_ptr(page)) + 1;
+}
+
 /*
  * The atomic page->_mapcount, starts from -1: so that transitions
  * both from it and to it can be tracked, using atomic_inc_and_test
@@ -405,8 +418,16 @@ static inline void page_mapcount_reset(struct page *page)
 
 static inline int page_mapcount(struct page *page)
 {
+	int ret;
 	VM_BUG_ON_PAGE(PageSlab(page), page);
-	return atomic_read(&page->_mapcount) + 1;
+	ret = atomic_read(&page->_mapcount) + 1;
+	/*
+	 * Positive compound_mapcount() offsets ->_mapcount in every page by
+	 * one. Let's substract it here.
+	 */
+	if (compound_mapcount(page))
+	       ret += compound_mapcount(page) - 1;
+	return ret;
 }
 
 static inline int page_count(struct page *page)
@@ -888,7 +909,7 @@ static inline pgoff_t page_file_index(struct page *page)
  */
 static inline int page_mapped(struct page *page)
 {
-	return atomic_read(&(page)->_mapcount) >= 0;
+	return atomic_read(&(page)->_mapcount) + compound_mapcount(page) >= 0;
 }

 

compound page 전체에 대한 mapping 개수는 compound_mapcount()를 통해서 first tail page의 _mapcount를 저장하며, page_mapcount()는 compound page의 일부 sub-page에 대한 _mapcount와 compound page 전체에 대한 _mapcount 를 더한다.

예를 들어서 사진의 compound page는 왼쪽에는 compound page 전체가 매핑되었지만 오른쪽에는 second tail page만 매핑되었다. 이러한 경우에 second tail page는 2번 매핑된다고 보는 것이다. (타당해 보인다.)

_refcount of compound page

이전 글에서 알아봤듯 non-compound page (order-0) 에 대한 _refcount는 작업 도중 페이지가 해제되는 것을 방지하는 데에 사용된다. compound page에서는 tail page의 _refcount를 사용하지 않고 head page의 _refcount만 사용한다. [3] 다시 말해서 compound page 전체를 해제하거나, 해제하지 않거나 둘 중 하나만 하는 것이다.

get_page(), put_page()를 보면 이것이 명확하다.

compound_pincount

우린 이제 buddy에서 prep_compound_{head,tail}()에서 하는 일의 대부분을 이해했다. 그런데 first tail page에는 compound_mapcount 말고도 compound_pincount라는 값을 설정하는데, 이건 뭘까?

질문에 대한 답은 [4], [5]에서 찾아볼 수 있었다. 아직 GUP ( = get_user_pages() ) 쪽은 잘 모르지만... 요지는  get_user_pages()에서는 페이지의 해제를 막기 위해 _refcount를 1씩 증가시키지만, _refcount를 증가시켰다고 해서 GUP로 pinning을 했는지는 알 수 없기 때문에, explicit하게 GUP_PIN_COUNTING_BIAS (1024)만큼 _refcount를 증가시켜서 _refcount >= 1024인 경우에 페이지가 누군가에 의해 pinned 되었다는걸 표시하는 것이다.

하지만 HugeTLB에서 쓰는 아주 큰 compound page에서는 explicit pinning count가 부족할 수 있기 때문에 compound_pincount를 사용한다.

따라서 누군가 1024번 GUP를 수행하면 order-0 page에서는 _refcount >= 1024이므로 누군가 explicit하게 pinning을 했나? 하고 착각할 수 있지만 (false positive, but it's acceptable according to [4]) compound page에서는 그러한 false positive가 발생하지 않는다.

References

[1] An introduction to compound pages, LWN.net 

[2] THP refcount redesign, lore 

[3] struct page, the Linux physical page frame data structure, Oracle Linux Blog  

[4] pin_user_pages() and related calls, kernel documentation    

[5] Explicit pinning of user-space pages  

TODO

page_mapcount() 최신 코드 반영

'Kernel > Memory Management' 카테고리의 다른 글

Page Frame Reclamation and Swapout (v2.4.22)  (0) 2022.12.24
Process Address Space  (0) 2022.11.05
struct page 메모  (2) 2022.09.14
Virtual Memory: vmalloc(), vm_map_ram() 분석  (0) 2022.07.13
Direct Map Fragmentation 문제  (0) 2022.05.11

댓글