Art of Pr0gr4m

[Linux Kernel 5] Memory Zone 본문

IT/Linux Kernel

[Linux Kernel 5] Memory Zone

pr0gr4m 2020. 5. 12. 11:01

이번 포스트에서는 메모리 할당 정책의 기반이 되는 메모리 존에 대해서 알아본다.

 

 

 

1. Memory Zone

 

리눅스 커널은 물리 메모리의 주소 영역을 나눠 Zone으로 관리한다.

Zone 타입은 다음과 같다.

enum zone_type {
	/*
	 * ZONE_DMA and ZONE_DMA32 are used when there are peripherals not able
	 * to DMA to all of the addressable memory (ZONE_NORMAL).
	 * On architectures where this area covers the whole 32 bit address
	 * space ZONE_DMA32 is used. ZONE_DMA is left for the ones with smaller
	 * DMA addressing constraints. This distinction is important as a 32bit
	 * DMA mask is assumed when ZONE_DMA32 is defined. Some 64-bit
	 * platforms may need both zones as they support peripherals with
	 * different DMA addressing limitations.
	 *
	 * Some examples:
	 *
	 *  - i386 and x86_64 have a fixed 16M ZONE_DMA and ZONE_DMA32 for the
	 *    rest of the lower 4G.
	 *
	 *  - arm only uses ZONE_DMA, the size, up to 4G, may vary depending on
	 *    the specific device.
	 *
	 *  - arm64 has a fixed 1G ZONE_DMA and ZONE_DMA32 for the rest of the
	 *    lower 4G.
	 *
	 *  - powerpc only uses ZONE_DMA, the size, up to 2G, may vary
	 *    depending on the specific device.
	 *
	 *  - s390 uses ZONE_DMA fixed to the lower 2G.
	 *
	 *  - ia64 and riscv only use ZONE_DMA32.
	 *
	 *  - parisc uses neither.
	 */
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	/*
	 * Normal addressable memory is in ZONE_NORMAL. DMA operations can be
	 * performed on pages in ZONE_NORMAL if the DMA devices support
	 * transfers to all addressable memory.
	 */
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	/*
	 * A memory area that is only addressable by the kernel through
	 * mapping portions into its own address space. This is for example
	 * used by i386 to allow the kernel to address the memory beyond
	 * 900MB. The kernel will set up special mappings (page
	 * table entries on i386) for each page that the kernel needs to
	 * access.
	 */
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
	ZONE_DEVICE,
#endif
	__MAX_NR_ZONES
 
};

 

 

2. ZONE_DMA & ZONE_DMA32

 

DMA를 사용하는 디바이스의 주소 버스가 시스템의 물리 메모리 주소에 전부 접근할 수 없는 경우 사용하는 영역이다.

예를 들어 16bit 주소 버스를 갖는 디바이스가 32bit 주소로 할당된 물리 메모리에 접근하려고 하는 경우 디바이스가 메모리에 제대로 접근할 수 없다.

이 경우 64kB(2^16)의 주소를 ZONE_DMA 영역에 할당받아 사용한다.

ZONE_DMA32의 경우 64bit 시스템에서 32bit 주소를 사용하는 DMA 장치를 위해 사용한다.

(kmalloc의 flag에 GFP_DMA32를 사용하여 ZONE_DMA32 영역에 메모리를 할당할 수 있다.)

 

반대로, 32bit 주소 버스를 갖는 디바이스가 32bit 메모리 시스템에 접근하는 경우 해당 Zone을 사용할 필요 없이, ZONE_NORMAL 영역에 메모리를 할당받으면 된다.

(시스템 specific한 내용은 위 주석을 참고하면 된다.)

 

 

3. ZONE_NORMAL & ZONE_HIGHMEM

 

물리 메모리와 가상 주소가 1:1로 매핑되어 사용할 수 있는 일반적인 주소 영역이다.

알다시피 시스템은 메모리 공간을 유저 공간과 커널 공간으로 나눠 사용한다.

(크기 구성은 system과 커널 설정에 따라 다르다.)

이 중 커널 공간의 일부 영역을 사용하여 배치하는 물리 메모리 영역이다.

 

32bit 시스템의 경우엔 대부분의 커널공간이 1G이기 때문에 크기가 매우 제한적이다.

따라서 ZONE_NORMAL로 관리하지 못하는 나머지 상위 메모리 영역을 ZONE_HIGHMEM으로 관리한다.

예를 들어 896M 까지의 영역을 ZONE_NORMAL로 설정하고,

896M ~ 1024M 영역에 1G 이상 영역에 대한 매핑 정보를 두어

1G 이상의 영역을 ZONE_HIGHMEM으로 구성한다.

 

64bit 시스템의 경우 모든 물리 메모리를 1:1 매핑이 가능하기 때문에 굳이 ZONE_HIGHMEM을 사용할 필요 없이 모든 영역을 ZONE_NORMAL로 다룬다.

 

 

4. ZONE_MOVABLE

 

메모리 단편화를 완화하기 위해 사용하며, 메모리 핫플러그를 지원하는데도 사용한다.

추후에 포스팅할 버디시스템에서 페이지를 할당하다보면 당연하지만 메모리 단편화가 생긴다.

일반적으로 유저 공간 페이지들은 movable하기 때문에 memory compaction 작업에 지장이 없다.

하지만 커널 공간의 페이지들은 대부분 non-movable 페이지이기 때문에 compaction 작업에 지장이 생긴다.

이런 경우를 대비하여 NUMA 노드의 일부 영역을 ZONE_MOVABLE 영역으로 할당하여,

해당 공간의 페이지들은 movable하게 설정해 연속적인 메모리 세그먼트를 만들기 용이하게 한다.

그 외에도 메모리 핫플러그 사용 시 유동적인 메모리 노드를 ZONE_MOVABLE 영역에 할당하여 사용한다.

ZONE_MOVABLE 영역은 시스템에 설치된 최상위 zone (보통 ZONE_HIGHMEM이나 ZONE_NORMAL)의 일부를 할당한다.

 

 

5. ZONE_DEVICE

 

대용량 메모리를 사용하는 디바이스(heterogeneous memory device, persistent memory device)에서 사용하기 위한 영역이다. 자세한 내용은 링크를 참고한다.

 

 

 

6. struct zone

 

커널이 zone을 관리하기 위한 구조체 struct zone의 정의는 다음과 같다.

struct zone {
	/* Read-mostly fields */
 
	/* zone watermarks, access with *_wmark_pages(zone) macros */
	unsigned long _watermark[NR_WMARK];
	unsigned long watermark_boost;
 
	unsigned long nr_reserved_highatomic;
 
	/*
	 * We don't know if the memory that we're going to allocate will be
	 * freeable or/and it will be released eventually, so to avoid totally
	 * wasting several GB of ram we must reserve some of the lower zone
	 * memory (otherwise we risk to run OOM on the lower zones despite
	 * there being tons of freeable ram on the higher zones).  This array is
	 * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
	 * changes.
	 */
	long lowmem_reserve[MAX_NR_ZONES];
 
#ifdef CONFIG_NUMA
	int node;
#endif
	struct pglist_data	*zone_pgdat;
	struct per_cpu_pageset __percpu *pageset;
 
#ifndef CONFIG_SPARSEMEM
	/*
	 * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
	 * In SPARSEMEM, this map is stored in struct mem_section
	 */
	unsigned long		*pageblock_flags;
#endif /* CONFIG_SPARSEMEM */
 
	/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
	unsigned long		zone_start_pfn;
 
	/*
	 * spanned_pages is the total pages spanned by the zone, including
	 * holes, which is calculated as:
	 * 	spanned_pages = zone_end_pfn - zone_start_pfn;
	 *
	 * present_pages is physical pages existing within the zone, which
	 * is calculated as:
	 *	present_pages = spanned_pages - absent_pages(pages in holes);
	 *
	 * managed_pages is present pages managed by the buddy system, which
	 * is calculated as (reserved_pages includes pages allocated by the
	 * bootmem allocator):
	 *	managed_pages = present_pages - reserved_pages;
	 *
	 * So present_pages may be used by memory hotplug or memory power
	 * management logic to figure out unmanaged pages by checking
	 * (present_pages - managed_pages). And managed_pages should be used
	 * by page allocator and vm scanner to calculate all kinds of watermarks
	 * and thresholds.
	 *
	 * Locking rules:
	 *
	 * zone_start_pfn and spanned_pages are protected by span_seqlock.
	 * It is a seqlock because it has to be read outside of zone->lock,
	 * and it is done in the main allocator path.  But, it is written
	 * quite infrequently.
	 *
	 * The span_seq lock is declared along with zone->lock because it is
	 * frequently read in proximity to zone->lock.  It's good to
	 * give them a chance of being in the same cacheline.
	 *
	 * Write access to present_pages at runtime should be protected by
	 * mem_hotplug_begin/end(). Any reader who can't tolerant drift of
	 * present_pages should get_online_mems() to get a stable value.
	 */
	atomic_long_t		managed_pages;
	unsigned long		spanned_pages;
	unsigned long		present_pages;
 
	const char		*name;
 
#ifdef CONFIG_MEMORY_ISOLATION
	/*
	 * Number of isolated pageblock. It is used to solve incorrect
	 * freepage counting problem due to racy retrieving migratetype
	 * of pageblock. Protected by zone->lock.
	 */
	unsigned long		nr_isolate_pageblock;
#endif
 
#ifdef CONFIG_MEMORY_HOTPLUG
	/* see spanned/present_pages for more description */
	seqlock_t		span_seqlock;
#endif
 
	int initialized;
 
	/* Write-intensive fields used from the page allocator */
	ZONE_PADDING(_pad1_)
 
	/* free areas of different sizes */
	struct free_area	free_area[MAX_ORDER];
 
	/* zone flags, see below */
	unsigned long		flags;
 
	/* Primarily protects free_area */
	spinlock_t		lock;
 
	/* Write-intensive fields used by compaction and vmstats. */
	ZONE_PADDING(_pad2_)
 
	/*
	 * When free pages are below this point, additional steps are taken
	 * when reading the number of free pages to avoid per-cpu counter
	 * drift allowing watermarks to be breached
	 */
	unsigned long percpu_drift_mark;
 
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
	/* pfn where compaction free scanner should start */
	unsigned long		compact_cached_free_pfn;
	/* pfn where async and sync compaction migration scanner should start */
	unsigned long		compact_cached_migrate_pfn[2];
	unsigned long		compact_init_migrate_pfn;
	unsigned long		compact_init_free_pfn;
#endif
 
#ifdef CONFIG_COMPACTION
	/*
	 * On compaction failure, 1<<compact_defer_shift compactions
	 * are skipped before trying again. The number attempted since
	 * last failure is tracked with compact_considered.
	 */
	unsigned int		compact_considered;
	unsigned int		compact_defer_shift;
	int			compact_order_failed;
#endif
 
#if defined CONFIG_COMPACTION || defined CONFIG_CMA
	/* Set to true when the PG_migrate_skip bits should be cleared */
	bool			compact_blockskip_flush;
#endif
 
	bool			contiguous;
 
	ZONE_PADDING(_pad3_)
	/* Zone statistics */
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];
	atomic_long_t		vm_numa_stat[NR_VM_NUMA_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

zone_start_pfn은 해당 zone이 시작하는 PFN을 나타낸다.

spanned_pages는 zone이 가질 수 있는 총 page 수를 나타낸다.

present_pages는 zone에 존재하는 물리 page 수를 나타낸다.

managed_pages는 present_pages 중 buddy system으로 관리되고 있는 페이지의 수를 나타낸다.

페이지 할당이나 vm 스캔에는 이 managed_pages가 사용된다.

해당 값들을 사용할 때 사용하는 lock은 주석을 참고한다.

free_area는 buddy system에서 사용하는 다양한 사이즈의 free area들이다.

compact_xxx 멤버들은 memory compation에 사용되는 멤버들이다.

 

과거 메모리 정책이 간단했을때 구버전 커널에서는 zone_table 배열로 메모리 존을 구성했다.

현재 Zone은 NUMA 노드마다 존재한다.

즉, NUMA 노드 별로 메모리를 관리해야 하며 이는 struct pglist_data 구조체로 관리한다.

zone_pgdat 멤버는 자신을 포함하고 있는 pglist_data를 가리킨다.

 

참고로 NUMA에 대한 내용은 다음 포스트를 참고한다.