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/

http://egloos.zum.com/rousalome/v/10002613

http://egloos.zum.com/rousalome/v/10002618

+ Recent posts