1. NUMA
최근 Linux Kernel system은 NUMA로 memory 할당 및 관리를 진행하며 NUMA로 진행된 이후 복잡한 메모리 관리가 필수불가결이 되었다.
NUMA란 CPU core의 효율성을 극대화하여 높은 성능을위해 나누어 관리하는 개념으로 여기서 메모리 관리가 잘못되면 메모리가 충분함에도 메모리 부족 등의 문제가 발생할 수 있기 때문이다.
1) 메모리 정책
- Interleave
순차적으로 할당하는 방법 부족시 fallback 사용
- Bind
NUMA별 자기 영역에서만 메모리 할당이 가능 부족시에도 fallback 사용 x
- Preferred
해당 테스크에 우선적으로 권장 노드를 사용하나 부족시에는 fallback 사용
- Default
해당 태스크가 동작하는 cpu가 속한 노드를 우선적으로 사용하며 메모리 부족 시 fallback 사용
2) fallback
fallback이란 메모리 할당을 진행할 때 메모리 부족 현상이 발생해서 대체하는 메모리를 말한다. 보통 fallback path로 메모리 fail시 다음 순위로 할당할 영역을 미리 setup 해둔다. 그리고 다음 메모리를 결정할 때는 미리 정해진 우선순위를 통해서 다음 메모리를 결정하는데 우선순위 리스트를 zonelist(=fallback list) 라고 한다.
여기서 NUMA는 zonelist를 2개를 사용하는데 zonlist[0]은 전체 노드에 대상으로 우선순위를 만든 것이고 zonelist[1]은 현재 NUMA-node에 대해서만 우선순위를 만든 것이다.
fallback의 zone list에는 크게 2가지 정렬방법이 존재하며 가장 적합한 빠른 할당이가능한 node순으로 우선순위를 정하는 방법과 zone type별로 우선순위를 결정하는 방법이다.
- zone 우선(zone type별로 우선순위 결정)
zone 우선 시에는 아래 순서로 할당한다.
ZONE_MAVABLE > ZONE_HIGHMEM > ZONE_NORMAL > ZONE_DMA32 | ZONE_DMA
2. NUMA 정책 설정
1) 초기화
mm/mempolicy.c
void __init numa_policy_init(void) { nodemask_t interleave_nodes; unsigned long largest = 0; int nid, prefer = 0; policy_cache = kmem_cache_create("numa_policy", sizeof(struct mempolicy), 0, SLAB_PANIC, NULL); sn_cache = kmem_cache_create("shared_policy_node", sizeof(struct sp_node), 0, SLAB_PANIC, NULL); for_each_node(nid) { preferred_node_policy[nid] = (struct mempolicy) { .refcnt = ATOMIC_INIT(1), .mode = MPOL_PREFERRED, .flags = MPOL_F_MOF | MPOL_F_MORON, .nodes = nodemask_of_node(nid), }; } /* * Set interleaving policy for system init. Interleaving is only * enabled across suitably sized nodes (default is >= 16MB), or * fall back to the largest node if they're all smaller. */ nodes_clear(interleave_nodes); for_each_node_state(nid, N_MEMORY) { unsigned long total_pages = node_present_pages(nid); /* Preserve the largest node */ if (largest < total_pages) { largest = total_pages; prefer = nid; } /* Interleave this node? */ if ((total_pages << PAGE_SHIFT) >= (16 << 20)) node_set(nid, interleave_nodes); } /* All too small, use the largest */ if (unlikely(nodes_empty(interleave_nodes))) node_set(prefer, interleave_nodes); if (do_set_mempolicy(MPOL_INTERLEAVE, 0, &interleave_nodes)) pr_err("%s: interleaving failed\n", __func__); check_numabalancing_enable(); } |
위 fucntion은 최초로 policy를 setup하는 function으로 NUMA 메모리 정책 초기화를 위해 사용한다.
위 function 중 nodes_clear(interleave_nodes);를 볼 수 있는데 이는 초기 NUMA node를 모두 Interleave정책으로 할당을 진행하는 fucntion이다.
include/linux/nodemask.h
#define nodes_clear(dst) __nodes_clear(&(dst), MAX_NUMNODES) static inline void __nodes_clear(nodemask_t *dstp, unsigned int nbits) { bitmap_zero(dstp->bits, nbits); } |
__nodes_clear()를 보면 위와같이 파라미터로 입력받은 정책에 대해서 초기화 진행을 한다.
2) 정책 설정
위와 같이 초기화가 진행된 다음에 정책을 변경 즉 set하는 과정을 살펴보면 아래와 같다.
NUMA memory정책은 위에서 본 바와 같이 Interleave, Bind, Preferred, Default가 존재한다. 그리고 set은 do_set_mempolicy()을 사용한다.
mm/mempolicy.c
static long do_set_mempolicy(unsigned short mode, unsigned short flags, nodemask_t *nodes) { struct mempolicy *new, *old; NODEMASK_SCRATCH(scratch); int ret; if (!scratch) return -ENOMEM; new = mpol_new(mode, flags, nodes); if (IS_ERR(new)) { ret = PTR_ERR(new); goto out; } ret = mpol_set_nodemask(new, nodes, scratch); if (ret) { mpol_put(new); goto out; } task_lock(current); old = current->mempolicy; current->mempolicy = new; if (new && new->mode == MPOL_INTERLEAVE) current->il_prev = MAX_NUMNODES-1; task_unlock(current); mpol_put(old); ret = 0; out: NODEMASK_SCRATCH_FREE(scratch); return ret; } |
위 함수를 살펴보면 크게 두 부분으로 나눌 수 있다. 먼저 set을진행하기위한 새로운 정책에 대한 메모리 할당을 진행하고 이후에 할당된 메모리에 한하여 새 정책을 set하고 current policy를 변경해준다.
/* * This function just creates a new policy, does some check and simple * initialization. You must invoke mpol_set_nodemask() to set nodes. */ static struct mempolicy *mpol_new(unsigned short mode, unsigned short flags, nodemask_t *nodes) |
먼저 mpol_new()을 통해서 새로운 정책을 위한 메모리 영역 할당을 진행한다.
static int mpol_set_nodemask(struct mempolicy *pol, const nodemask_t *nodes, struct nodemask_scratch *nsc) { int ret; /* * Default (pol==NULL) resp. local memory policies are not a * subject of any remapping. They also do not need any special * constructor. */ if (!pol || pol->mode == MPOL_LOCAL) return 0; /* Check N_MEMORY */ nodes_and(nsc->mask1, cpuset_current_mems_allowed, node_states[N_MEMORY]); VM_BUG_ON(!nodes); if (pol->flags & MPOL_F_RELATIVE_NODES) mpol_relative_nodemask(&nsc->mask2, nodes, &nsc->mask1); else nodes_and(nsc->mask2, *nodes, nsc->mask1); if (mpol_store_user_nodemask(pol)) pol->w.user_nodemask = *nodes; else pol->w.cpuset_mems_allowed = cpuset_current_mems_allowed; ret = mpol_ops[pol->mode].create(pol, &nsc->mask2); return ret; } |
그리고 mpol_set_nodemask를 통해서 직접 set을 진행한다. 해당 함수는 mpol_new를 통해 새로 할당한 mempolicy struct에 대해서 mask 값을 셋업하는 작업을 진행함으로써 실제 set하고자하는 mempolicy로 만들어주는 역할을 진행한다.
3. 페이지 할당
기본적으로 Buddy시스템에서 할당을진행할 때 NUMA인지 아닌지로 나누어 동작한다. NUMA가 아닌 경우 바로 __alloc_pages_node()를 통해서 페이지 할당을 진행하지만 NUMA인 경우 어떤 노드를 사용할지 정하는 작업이 우선적으로 필요하다.
그래서 NUMA인 경우에는 alloc_pages_current()를 통해서 zonelist 등을 확인하고 먼저 어떤 노드를 사용할지를 결정한다.
이후에 __alloc_pages_node()를 호출함으로써 페이지 할당을 진행한다.
여기서 __alloc_pages_node()역시 크게 두가지 방법으로 나뉘는데 페이지 할당을 진행할 때 아무 문제 없이 한 번에 진행되는 경우를 fastpath라고 하고
만약에 메모리 부족 등의 이슈로 메모리를 다시 다른 곳에서 땡겨오는 등의 작업을 거쳐서 메모리 할당을 진행하는 경우를 slowpath라고 한다.
1) fastpath 페이지 할당
fastpath할당은 alloc_pages_node()를 통해서 한 번에 문제 없이 할당되는 케이스를 말한다. alloc_pages_node()를 따라가면 아래와 같이 실제 할당을 진행하는 function을 볼 수 있다.
mm/page_alloc.c
struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid, nodemask_t *nodemask) { struct page *page; unsigned int alloc_flags = ALLOC_WMARK_LOW; gfp_t alloc_gfp; /* The gfp_t that was actually used for allocation */ struct alloc_context ac = { }; /* * There are several places where we assume that the order value is sane * so bail out early if the request is out of bound. */ if (unlikely(order >= MAX_ORDER)) { WARN_ON_ONCE(!(gfp & __GFP_NOWARN)); return NULL; } gfp &= gfp_allowed_mask; /* * Apply scoped allocation constraints. This is mainly about GFP_NOFS * resp. GFP_NOIO which has to be inherited for all allocation requests * from a particular context which has been marked by * memalloc_no{fs,io}_{save,restore}. And PF_MEMALLOC_PIN which ensures * movable zones are not used during allocation. */ gfp = current_gfp_context(gfp); alloc_gfp = gfp; if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac, &alloc_gfp, &alloc_flags)) return NULL; /* * Forbid the first pass from falling back to types that fragment * memory until all local zones are considered. */ alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp); /* First allocation attempt */ page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac); if (likely(page)) goto out; alloc_gfp = gfp; ac.spread_dirty_pages = false; /* * Restore the original nodemask if it was potentially replaced with * &cpuset_current_mems_allowed to optimize the fast-path attempt. */ ac.nodemask = nodemask; page = __alloc_pages_slowpath(alloc_gfp, order, &ac); out: if (memcg_kmem_enabled() && (gfp & __GFP_ACCOUNT) && page && unlikely(__memcg_kmem_charge_page(page, gfp, order) != 0)) { __free_pages(page, order); page = NULL; } trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype); return page; } |
먼저 여기서 flag값들을 살펴보면 의미는 다음과 같다.
- ALLOC WATER MARK
Linux시스템에 있어 워터마크는 시스템에 남은 메모리가 얼마인지를 알기위한 지표이다.
아래와 같이 잔여 메모리에 따라서 서로 다른 워터마크 defined값을 가진다.
#define ALLOC_WMARK_MIN 잔여 메모리가 너무 부족해서 시스템 구동에 문제 #define ALLOC_WMARK_LOW 잔여 메모리가 적음 #define ALLOC_WMARK_HIGH 잔여 메모리가 충분함. #define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */ |
- gfp 플래그
Linux 에서는 메모리 할당을 진행할 때 어느 Zone 영역을 할당해라 등의 할당시에 사용자가 원하는 할당을 유도하기 위해 flag값을 이용한다.
그리고 이 때 사용되는 flag값이 바로 gfp 플래그 값이다.
gfp는 위 alloc은 물론 kmalloc()등을 수행할 때도 인자로 넣어줌으로써 사용자가 원하는 할당을 유도할 수 있다.
#define GFP_ATOMIC 메모리 할당 중에 sleep할 수 없음 #define GFP_KERNEL %ZONE_NORMAL이나 lower zone에 1:1 매핑요청 #define GFP_KERNEL_ACCOUNT #define GFP_NOWAIT filesystem call back이나 I/O를 통해서 진행 #define GFP_NOIO slab 영역 할당 x #define GFP_NOFS file system interface x #define GFP_USER DMA등 H/W에서 사용하는 버퍼할당을 위한 목적 #define GFP_DMA ZONE_DMA 사용 #define GFP_DMA32 __GFP_DMA32 #define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM) #define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE | \ HIGHUSER의 경우 userspace에서의 할당이다. __GFP_SKIP_KASAN_POISON) #define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | \ __GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM) TANSHUGE의 경우에는 THP 할당을 지원한다. #define GFP_TRANSHUGE (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM) |
여기서 참고로 THP 할당은 Transparent Huge Pages라고 한다.
Linux system은 page크기를 4KB로 default한다. 하지만 이경우 데이터베이스와 같이 엄청난 크기의 데이터를 처리할 때 너무 많은 페이지가 생기게 된다.
이러한 문제를 해결하기위해 Huge Page라는 개념을 만들어서 리눅스 부팅과정에서 default 4KB에서 2GB와 같이 페이지단위를 변경할 수 있다.
하지만 부팅 과정에서 파라미터를 변경하는 작업이 불편하기 때문에 이를 자동화한 기술이 있는데 이 기술이 바로 THP이다.
다시 위 __alloc_pages function을 살펴보면 해당 function은 기본적으로 워터마크와 gfp플래그 설정을 진행하고 alloc를 통해서 할당을 진행하고 있다.
여기서 바로 성공하면 fastpath가 되는 것이고 만약 실패하는 경우 아래와 같이
page = __alloc_pages_slowpath(alloc_gfp, order, &ac); |
를 통해서 다시 할당을 시도하고 있다. (slowpath)
- ref
코드로 배우는 ARM64 Linux Kernel
https://www.kernel.org/doc/html/latest/vm/numa.html
https://elixir.bootlin.com/linux/v5.14.16/source/
'OS > Linux' 카테고리의 다른 글
[Linux Kernel] Kernel 분석(v5.14.16) - 워터마크 (0) | 2022.05.29 |
---|---|
[Linux Kernel] Kernel 분석(v5.14.16) - NUMA, Zone Allocation (2) (0) | 2022.05.09 |
[Linux Kernel] Kernel 분석(v5.14.16) - pcp (0) | 2022.03.26 |
[Linux Kernel] Kernel 분석(v5.14.16) - Buddy (2) (0) | 2022.03.07 |
[Linux Kernel] Kernel 분석(v5.14.16) - Buddy (1) (0) | 2022.02.26 |