본문 바로가기
Kernel/Memory Management

struct page 메모

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

struct page에 대한 간단한 노트

참고로 64비트 리눅스는 LP64를 사용한다. 이 글에서 자료형의 크기는 LP64에 따라서 서술되었다.

Introuction

struct page는 페이지 프레임 하나 (보통 4096 바이트)에 대한 정보를 나타내며 64비트 환경에서 일반적으로 64 바이트이다. struct page는 부팅 과정에서 동적으로 할당된다. 부팅이 끝나면 64/4096 = 1.56%의 메모리가 struct page에 사용된다.

memory model & struct page <-> page frame number conversion

물리 메모리는 페이지 프레임들의 집합이기 때문에 struct page는 배열의 형태로 관리하는데,어떤 물리 메모리 모델(FLATMEM, SPARSEMEM)를 [1], [2] 사용하냐에 따라서 이 배열에 접근하는 방식이 다르다.

/* include/asm-generic/memory_model.h# */
/*
 * supports 3 memory models.
 */
#if defined(CONFIG_FLATMEM)

#ifndef ARCH_PFN_OFFSET
#define ARCH_PFN_OFFSET		(0UL)
#endif

#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)

 

FLATMEM에서는 물리 메모리를 (중간에 조금 비어있을 수 있지만) 하나의 연속적인 배열로 보기 때문에 PFN을 인덱스로 하는 mem_map가 전역변수로 존재한다. 따라서 mem_map의 주소에 PFN를 더하면 struct page를 구할 수 있고, 반대로 struct page의주소에서 mem_map의 주소를 빼면 PFN을 구할 수 있다.

#elif defined(CONFIG_SPARSEMEM)
/*
 * Note: section's mem_map is encoded to reflect its start_pfn.
 * section[i].section_mem_map == mem_map's address - start_pfn;
 */
#define __page_to_pfn(pg)					\
({	const struct page *__pg = (pg);				\
	int __sec = page_to_section(__pg);			\
	(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	\
})

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})
#endif /* CONFIG_FLATMEM/SPARSEMEM */

 

SPARSEMEM, SPARSEMEM_VMEMMAP에서는 물리 메모리가 연속된 하나의 배열이 아닌, 다수의 연속된 물리 메모리 배열이 물리 주소 공간에 흩어져있는 것으로 본다. 이 때 연속된 물리 메모리 배열 하나를 "섹션"이라고 하고, 이 섹션에 대한 struct page 배열을 struct mem_section으로 관리한다.

SPARSEMEM에서는 pfn <-> struct page 변환을 위해서 struct page/pfn으로 struct mem_section을 구한 후, struct mem_section에 pfn을 더하거나, struct page의 주소에 mem_section을 뺀다.

#elif defined(CONFIG_SPARSEMEM_VMEMMAP)

/* memmap is virtually contiguous.  */
#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

 

SPARSEMEM_VMEMMAP도 SPARSEMEM과 같이 struct mem_section으로 struct page 배열을 관리하지만 변환 방식이 조금 다른데, SPARSE_VMEMMAP은 FLATMEM에서 mem_map이 물리적으로 연속적이었던 것과 유사하게, "가상 주소 상으로 연속되어있는" vmemmap을 사용한다. vmemmap은 커널 주소 공간의 일정 부분을 차지하며, vmemmap 영역 중 실제로 물리 메모리가 존재하는 구간만 struct page의 배열과 연결되어있다.

관련 매크로로 virt_to_page() (가상 주소(direct mapping 영역 상의 주소)에 해당하는 struct page를 구함), page_address() (struct page의 direct mapping 영역 상의 주소를 구함) 등이 있다.

struct page 구조체

struct page는 64비트 프로세서에서 보통 64 바이트인데, 그 중 24바이트 (flags (8), mapcount (4), refcount (4), memcg_data (8))는 모든 struct page에 대해서 공통적으로 사용되고, 나머지 40바이트는 (= 5 words) 용도에 따라서 다양하게 사용할 수 있도록 복잡한 union으로 되어있다.

flags

struct page {
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */

 

첫 번째 워드는 flags 필드이다. flags는 말 그대로 이 페이지 프레임의 상태를 나타내는 플래그이다. 근데 플래그 용도로만 사용하지는 않고, 비트의 일부는 이 프레임이 어느 node, 어느 zone에 속하는지 등을 나타내는 데에 사용된다.

 include/linux/page-flags-layout.h의 주석을 참고해보자.

/*
 * page->flags layout:
 *
 * There are five possibilities for how page->flags get laid out.  The first
 * pair is for the normal case without sparsemem. The second pair is for
 * sparsemem when there is plenty of space for node and section information.
 * The last is when there is insufficient space in page->flags and a separate
 * lookup is necessary.
 *
 * No sparsemem or sparsemem vmemmap: |       NODE     | ZONE |             ... | FLAGS |
 *      " plus space for last_cpupid: |       NODE     | ZONE | LAST_CPUPID ... | FLAGS |
 * classic sparse with space for node:| SECTION | NODE | ZONE |             ... | FLAGS |
 *      " plus space for last_cpupid: | SECTION | NODE | ZONE | LAST_CPUPID ... | FLAGS |
 * classic sparse no space for node:  | SECTION |     ZONE    | ... | FLAGS |
 */

 

아, 참고로 flags 필드는 매우 한정적인 자원이다. 이 필드에 node/zone에 대한 정보를 저장하기 때문에, 비트를 하나 더 쓸  때마다 지원 가능한 최대 노드의 수/존의 수가 줄어든다.

64비트에서는 이 필드가 8바이트라서 비트가 남지만, 리눅스는 32비트 프로세서도 지원하기 때문에 새로운 비트를 사용하기는 어렵다. 대신 일부 플래그는 특정 플래그가 사용된 경우에만 사용할 수 있기 때문에, 다른 목적으로 재사용할 수 있다. [6]

subsystem-specific five words

그 다음 5 워드는 용도에 따라서 다양하게 사용된다. 어떤 페이지는 사용자 프로세스, 파일시스템, 페이지 테이블, 슬랩, vmalloc, 커널 스택 등등 아주 다양한 용도로 사용될 수 있기 때문에 용도에 따라서 사용할 수 있도록 union으로 되어있다.

모든 용도를 설명하는 건 이 글의 범위를 넘어가므로 생략한다. 참고로 slab은 특이하게도 struct page를 사용하지만 구조체의 정의 자체는 struct slab으로 mm/slab.h에 되어있다. 다른 구조체 같지만 struct page와 같은 메모리를 사용한다.

	/*
	 * Five words (20/40 bytes) are available in this union.
	 * WARNING: bit 0 of the first word is used for PageTail(). That
	 * means the other users of this union MUST NOT use the bit to
	 * avoid collision and false-positive PageTail().
	 */
	union {
    		/* 용도에 따라서 5 워드를 맞게 사용 */
    		[...]
	};
	union {		/* This union is 4 bytes in size. */
		/*
		 * If the page can be mapped to userspace, encodes the number
		 * of times this page is referenced by a page table.
		 */
		atomic_t _mapcount;

		/*
		 * If the page is neither PageSlab nor mappable to userspace,
		 * the value stored here may help determine what this page
		 * is used for.  See page-flags.h for a list of page types
		 * which are currently stored here.
		 */
		unsigned int page_type;
	};

 

그 다음 4바이트는 _mapcount와 page_type의 union이다. unsigned int/atomic_t는 64/32비트에서 모두 4바이트이다. 이 페이지 프레임이 사용자 프로세스에 매핑된 경우 _mapcount는 몇 개의 매핑이 존재하는지를 나타내며, 슬랩 페이지인 경우 (page->flags & PG_slab)에는 별도의 용도로 사용된다.

만약 페이지를 할당한 다음 _mapcount 필드를 다른 용도로 사용한다면  free하기 전에 page_mapcount_reset()으로 0으로 초기화해야한다.

그 외에는 이 페이지가 어떤 용도로 사용되는지 확인하는 데에 사용된다. 필자가 이해하기로는 flags에 비트가 부족해서 일부 플래그를 기록하는 데에 page_type가 사용된 것으로 보인다.

	/* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */
	atomic_t _refcount;

 

그 다음 4바이트 _refcount는 이 페이지를 얼마나 사용하는지 나타낸다. 이제 막 할당된 페이지는 reference count가 1이고, 이 카운트가 0이 되면 페이지 프레임이 free된다.

#ifdef CONFIG_MEMCG
	unsigned long memcg_data;
#endif

 

그 다음 8바이트 memcg_data는 memory cgroup에서 데이터를 저장하기 위한 필드이다. memcg는 다양한 종류의 메모리를 accounting하기 때문에 별도의 워드를 사용하는 것으로 보인다.

	/*
	 * On machines where all RAM is mapped into kernel address space,
	 * we can simply calculate the virtual address. On machines with
	 * highmem some memory is mapped into kernel virtual memory
	 * dynamically, so we need a place to store that address.
	 * Note that this field could be 16 bits on x86 ... ;)
	 *
	 * Architectures with slow multiplication can define
	 * WANT_PAGE_VIRTUAL in asm/page.h
	 */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;			/* Kernel virtual address (NULL if
					   not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
	int _last_cpupid;
#endif
} _struct_page_alignment;

 

자, 아까 struct page가 64바이트라고 했는데 이미 64바이트가 이미 모두 사용되었다. 사실 크기는 configuration에 따라서 가변적이다. 나머지 12바이트(virtual, _last_cpuid)는 일반적으로 포함되는 필드는 아니다.

virtual 필드는 이 페이지 프레임이 어떤 커널 가상 주소로 매핑되어있는지 나타내는 데에 사용된다. 64비트 환경 같은 경우에는 모든 물리 메모리가 가상 주소를 갖기 때문에 필요하지 않지만, HIGHMEM을 사용하는 경우에는 kmap()/kmap_atomic()/kmap_local_page() 등의 API로 가상 주소를 만들어주어야 하므로 virtual 필드에 이 주소를 저장한다. 아니면 주석에서 말하는 것처럼 pfn <-> page 변환이 느린 경우에도 virtual 필드에 저장할 수 있다.

_last_cpupid는 말 그대로 마지막으로 페이지 프레임 사용한 CPU, PID를 기록하는 필드이다. configuration에 따라 flags 필드에 기록되거나 별도의 필드에 존재한다.

alignment requirements

SLUB을 사용하는 경우 freelist, counters 필드에 대하여 double-width CAS 명령어를 사용하기 때문에 freelist 필드가 (6.0 기준) double-word aligned되어 있어야 한다. 정렬이 안되어있으면 General Protection Fault가 발생할 수 있다.

따라서 CONFIG_HAVE_ALIGNED_STRUCT_PAGE=y인 경우에는 _struct_page_alignment가 다음과 같이 정의된다.

#ifdef CONFIG_HAVE_ALIGNED_STRUCT_PAGE
#define _struct_page_alignment	__aligned(2 * sizeof(unsigned long))
#else
#define _struct_page_alignment
#endif

constraints

리눅스는 struct page의 크기를 늘리지 않기 위해서 다양한 노력을 한다. 그러다보니 이 구조체는 몇 가지 제약 사항이 있다.

must not use lower two bits of mapping field

page가 anonymous page나 pagecache page로 사용되는 경우 mapping 필드가 존재한다. (struct page에서 네 번째 word에 해당) 이 페이지 프레임이 사용자 프로세스에 매핑된 경우에는 mapping 필드가 anon_vma를 가리키고, 페이지 캐시에 사용된 경우에는 struct address_space를 가리킨다.

include/linux/page-flags.h의 주석을 보면 mapping 필드의 하위 2비트는 플래그로 사용됨을 알 수 있다. 다른 용도로 사용되는 페이지 프레임 (예를 들어 슬랩)이 mapping 필드와 오프셋이 같은 필드의 하위 2비트를 사용할 경우 compaction 코드가 어떤 페이지 프레임을 movable page로 착각할 수 있다. 얼마 전에 개발 중 실제로 이런 경우가 생겼었다.

 

 

/* include/linux/page-flags.h */

/*
 * On an anonymous page mapped into a user virtual memory area,
 * page->mapping points to its anon_vma, not to a struct address_space;
 * with the PAGE_MAPPING_ANON bit set to distinguish it.  See rmap.h.
 *
 * On an anonymous page in a VM_MERGEABLE area, if CONFIG_KSM is enabled,
 * the PAGE_MAPPING_MOVABLE bit may be set along with the PAGE_MAPPING_ANON
 * bit; and then page->mapping points, not to an anon_vma, but to a private
 * structure which KSM associates with that merged page.  See ksm.h.
 *
 * PAGE_MAPPING_KSM without PAGE_MAPPING_ANON is used for non-lru movable
 * page and then page->mapping points to a struct movable_operations.
 *
 * Please note that, confusingly, "page_mapping" refers to the inode
 * address_space which maps the page from disk; whereas "page_mapped"
 * refers to user virtual address space into which the page is mapped.
 */
#define PAGE_MAPPING_ANON	0x1
#define PAGE_MAPPING_MOVABLE	0x2
#define PAGE_MAPPING_KSM	(PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)
#define PAGE_MAPPING_FLAGS	(PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE)

must not use bit zero of compound_page field

어떤 페이지 프레임이 compound page의 head인지 tail인지를 [5] 나타낼 때 compound_head 필드 (struct page의 2번째 워드에 해당)의 bit 0이 사용된다. 따라서 compound_head와 오프셋이 같은 필드는 비트 0을 사용해선 안된다.

_mapcount and mapping should be zero at free

페이지 프레임을 해제할 때 버디는 몇 가지 체크를 하는데, _mapcount 필드와 mapping 필드는 모두 0이어야 한다.

이 필드들을 사용한다면 앞서 언급한대로 _mapcount는 page_mapcount_reset()으로 초기화하고, mapping 필드는 NULL로 설정해준 후 해제하자. 

References

[1] Physical Memory Model — The Linux Kernel documentation

 

Physical Memory Model — The Linux Kernel documentation

Physical memory in a system may be addressed in different ways. The simplest case is when the physical memory starts at address 0 and spans a contiguous range up to the maximal address. It could be, however, that this range contains small holes that are no

www.kernel.org

[2] Memory: the flat, the discontiguous, and the sparse (Korean) | hacklog (sjp38.github.io)

 

Memory: the flat, the discontiguous, and the sparse (Korean) | hacklog

LWN 의 “Memory: the flat, the discontiguous, and the sparse” 라는 제목의 글의 한글 번역입니다. 원문은 LWN 에서 볼 수 있습니다: https://lwn.net/Articles/789304/ May 27, 2019 이 기사는 Mike Rapoport 에 의해 기여되었

sjp38.github.io

[3] Minimizing struct page overhead (oracle.com)

 

Minimizing struct page overhead

Discussion on how to improve Linux memory management efficiency.

orasites-prodapp.cec.ocp.oraclecloud.com

[4] MemoryTypes - Linux Kernel Newbies

 

MemoryTypes - Linux Kernel Newbies

KernelNewbies: MemoryTypes (2021-10-12 14:25:49에 MatthewWilcox가(이) 마지막으로 수정)

kernelnewbies.org

[5] An introduction to compound pages [LWN.net]

 

An introduction to compound pages [LWN.net]

Did you know...?LWN.net is a subscriber-supported publication; we rely on subscribers to keep the entire operation going. Please help out by buying a subscription and keeping LWN on the net. By Jonathan Corbet November 11, 2014 Your editor was digging thro

lwn.net

[6] struct page, the Linux physical page frame data structure (oracle.com)

 

struct page, the Linux physical page frame data structure

Gain insight into the Linux physical page frame data structure struct page and how to safely use various fields in the structure.

orasites-prodapp.cec.ocp.oraclecloud.com

 

반응형

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

struct page 메모  (0) 2022.09.14
Virtual Memory: vmalloc(), vm_map_ram() 분석  (0) 2022.07.13
Direct Map Fragmentation 문제  (0) 2022.05.11
KFENCE: Kernel Electric-Fence  (2) 2022.04.17
KASAN: Kernel Address SANitizer  (0) 2022.04.09
Virtual Memory: Transparent Huge Pages  (0) 2022.03.23

댓글0