Art of Pr0gr4m

[Linux Kernel 5] NUMA (Non-Uniform Memory Access) 본문

IT/Linux Kernel

[Linux Kernel 5] NUMA (Non-Uniform Memory Access)

pr0gr4m 2020. 5. 13. 16:32

NUMA는 Non-Uniform Memory Access의 약자로, 멀티 프로세서 시스템에서 메모리에 접근하는 시간이 메모리와 프로세서간의 상대적 위치에 따라 달라지는 메모리 설계 방법이다.

CPU의 속도가 메모리 접근 시간에 비해 월등히 빠르기 때문에, 자연스레 data starvation 현상이 나타난다.

NUMA는 시스템 상의 모든 CPU가 같은 메모리에 접근하는 대신, 각 CPU별로 접근할 수 있는 메모리 영역을 나누어 자신이 이용할 수 있는 메모리 공간에 대해서 다른 CPU의 메모리 접근에 관계없이 빠르게 접근하여 이를 개선한다.

 

NUMA의 concept에 대한 더 자세한 내용은 다음 링크들을 참고하고, 이번 포스트에서는 리눅스에서 NUMA를 어떻게 구현하고 있는지 알아본다.

 

https://ko.wikipedia.org/wiki/%EB%B6%88%EA%B7%A0%EC%9D%BC_%EA%B8%B0%EC%96%B5_%EC%9E%A5%EC%B9%98_%EC%A0%91%EA%B7%BC

 

불균일 기억 장치 접근 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 불균일 기억 장치 접근(Non-Uniform Memory Access, NUMA)는 멀티프로세서 시스템에서 사용되고 있는 컴퓨터 메모리 설계 방법중의 하나로, 메모리에 접근하는 시간이 ��

ko.wikipedia.org

https://www.kernel.org/doc/html/latest/vm/numa.html

 

What is NUMA? — The Linux Kernel documentation

What is NUMA? This question can be answered from a couple of perspectives: the hardware view and the Linux software view. From the hardware perspective, a NUMA system is a computer platform that comprises multiple components or assemblies each of which may

www.kernel.org

 

 

1. x86 NUMA init

 

NUMA 시스템을 초기화하는 함수들은 다음과 같다.

/**
 * x86_numa_init - Initialize NUMA
 *
 * Try each configured NUMA initialization method until one succeeds.  The
 * last fallback is dummy single node config encompassing whole memory and
 * never fails.
 */
void __init x86_numa_init(void)
{
	if (!numa_off) {
#ifdef CONFIG_ACPI_NUMA
		if (!numa_init(x86_acpi_numa_init))
			return;
#endif
#ifdef CONFIG_AMD_NUMA
		if (!numa_init(amd_numa_init))
			return;
#endif
	}
 
	numa_init(dummy_numa_init);
}
 
static int __init numa_init(int (*init_func)(void))
{
	int i;
	int ret;
 
	for (i = 0; i < MAX_LOCAL_APIC; i++)
		set_apicid_to_node(i, NUMA_NO_NODE);
 
	nodes_clear(numa_nodes_parsed);
	nodes_clear(node_possible_map);
	nodes_clear(node_online_map);
	memset(&numa_meminfo, 0, sizeof(numa_meminfo));
	WARN_ON(memblock_set_node(0, ULLONG_MAX, &memblock.memory,
				  MAX_NUMNODES));
	WARN_ON(memblock_set_node(0, ULLONG_MAX, &memblock.reserved,
				  MAX_NUMNODES));
	/* In case that parsing SRAT failed. */
	WARN_ON(memblock_clear_hotplug(0, ULLONG_MAX));
	numa_reset_distance();
 
	ret = init_func();
	if (ret < 0)
		return ret;
 
	/*
	 * We reset memblock back to the top-down direction
	 * here because if we configured ACPI_NUMA, we have
	 * parsed SRAT in init_func(). It is ok to have the
	 * reset here even if we did't configure ACPI_NUMA
	 * or acpi numa init fails and fallbacks to dummy
	 * numa init.
	 */
	memblock_set_bottom_up(false);
 
	ret = numa_cleanup_meminfo(&numa_meminfo);
	if (ret < 0)
		return ret;
 
	numa_emulation(&numa_meminfo, numa_distance_cnt);
 
	ret = numa_register_memblks(&numa_meminfo);
	if (ret < 0)
		return ret;
 
	for (i = 0; i < nr_cpu_ids; i++) {
		int nid = early_cpu_to_node(i);
 
		if (nid == NUMA_NO_NODE)
			continue;
		if (!node_online(nid))
			numa_clear_node(i);
	}
	numa_init_array();
 
	return 0;
}
 
/**
 * dummy_numa_init - Fallback dummy NUMA init
 *
 * Used if there's no underlying NUMA architecture, NUMA initialization
 * fails, or NUMA is disabled on the command line.
 *
 * Must online at least one node and add memory blocks that cover all
 * allowed memory.  This function must not fail.
 */
static int __init dummy_numa_init(void)
{
	printk(KERN_INFO "%s\n",
	       numa_off ? "NUMA turned off" : "No NUMA configuration found");
	printk(KERN_INFO "Faking a node at [mem %#018Lx-%#018Lx]\n",
	       0LLU, PFN_PHYS(max_pfn) - 1);
 
	node_set(0, numa_nodes_parsed);
	numa_add_memblk(0, 0, PFN_PHYS(max_pfn));
 
	return 0;
}

결국 초기화의 메인 루틴은 numa_init 함수이다.

set_apicid_to_node 함수는 __apicid_to_node테이블을 NUMA_NO_NODE(-1)으로 초기화한다.

nodes_clear 함수들은 NUMA 노드를 관리하는 비트맵들을 초기화한다.

numa_reset_distance 함수는 NUMA distance table을 초기화한다. (NUMA distance를 모르겠다면 위의 링크를 다시 읽고와야 한다.)

init_func는 x86_numa_init 함수에서 시스템 정보에 따라 인자로 전달한 x86_acpi_numa_init이나 amd_numa_init을 실행하여 system specific한 정보들을 초기화한다.

memblock_set_bottom_up은 memblock의 bottom_up allocation direction을 해제한다.

numa_cleanup_meminfo는 numa_meminfo를 초기화하고, numa_emulation은 NUMA 노드들을 에뮬레이트한다.

numa_register_memblks는 노드 정보를 설정하고 alloc_node_data 함수를 호출하여 NUMA 노드 데이터를 관리하는 struct pglist_data를 할당한다.

마지막으로 numa_init_array는 node_online_map에 있는 NUMA 노드들을 대상으로 numa_set_node를 호출해 cpu에 NUMA 노드를 설정한다.

 

 

 

2. struct pglist_data

 

NUMA의 각 메모리 Node들은 struct pglist_data로 관리한다. 해당 구조체의 정의는 다음과 같다.

/*
 * On NUMA machines, each NUMA node would have a pg_data_t to describe
 * it's memory layout. On UMA machines there is a single pglist_data which
 * describes the whole memory.
 *
 * Memory statistics and page replacement data structures are maintained on a
 * per-zone basis.
 */
struct bootmem_data;
typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES];
	struct zonelist node_zonelists[MAX_ZONELISTS];
	int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
	struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
	struct page_ext *node_page_ext;
#endif
#endif
#if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT)
	/*
	 * Must be held any time you expect node_start_pfn,
	 * node_present_pages, node_spanned_pages or nr_zones to stay constant.
	 *
	 * pgdat_resize_lock() and pgdat_resize_unlock() are provided to
	 * manipulate node_size_lock without checking for CONFIG_MEMORY_HOTPLUG
	 * or CONFIG_DEFERRED_STRUCT_PAGE_INIT.
	 *
	 * Nests above zone->lock and zone->span_seqlock
	 */
	spinlock_t node_size_lock;
#endif
	unsigned long node_start_pfn;
	unsigned long node_present_pages; /* total number of physical pages */
	unsigned long node_spanned_pages; /* total size of physical page
					     range, including holes */
	int node_id;
	wait_queue_head_t kswapd_wait;
	wait_queue_head_t pfmemalloc_wait;
	struct task_struct *kswapd;	/* Protected by
					   mem_hotplug_begin/end() */
	int kswapd_order;
	enum zone_type kswapd_classzone_idx;
 
	int kswapd_failures;		/* Number of 'reclaimed == 0' runs */
 
#ifdef CONFIG_COMPACTION
	int kcompactd_max_order;
	enum zone_type kcompactd_classzone_idx;
	wait_queue_head_t kcompactd_wait;
	struct task_struct *kcompactd;
#endif
	/*
	 * This is a per-node reserve of pages that are not available
	 * to userspace allocations.
	 */
	unsigned long		totalreserve_pages;
 
#ifdef CONFIG_NUMA
	/*
	 * node reclaim becomes active if more unmapped pages exist.
	 */
	unsigned long		min_unmapped_pages;
	unsigned long		min_slab_pages;
#endif /* CONFIG_NUMA */
 
	/* Write-intensive fields used by page reclaim */
	ZONE_PADDING(_pad1_)
	spinlock_t		lru_lock;
 
#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
	/*
	 * If memory initialisation on large machines is deferred then this
	 * is the first PFN that needs to be initialised.
	 */
	unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */
 
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
	struct deferred_split deferred_split_queue;
#endif
 
	/* Fields commonly accessed by the page reclaim scanner */
 
	/*
	 * NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.
	 *
	 * Use mem_cgroup_lruvec() to look up lruvecs.
	 */
	struct lruvec		__lruvec;
 
	unsigned long		flags;
 
	ZONE_PADDING(_pad2_)
 
	/* Per-node vmstats */
	struct per_cpu_nodestat __percpu *per_cpu_nodestats;
	atomic_long_t		vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;

node_zones는 해당 노드가 관리하는 Zone의 배열이며, Zone의 개수는 nr_zones에 저장된다.

node_start_pfn은 메모리 맵에서 해당 물리 메모리가 시작하는 PFN을 나타낸다.

node_present_pages는 노드에 할당된 물리 페이지 수를 나타낸다.

node_spanned_pages는 노드의 물리 페이지 사이즈를 나타낸다.

node_id는 변수명대로 node id를 나타낸다.

min_unmapped_pages와 min_slab_pages는 node memory reclaim의 가동 여부를 위한 변수이다.

first_deferred_pfn은 대용량 메모리 머신이 메모리 지연 초기화를 할 때 해당 PFN부터 초기화하기 위하여 사용한다.

__lruvec은 LRU 리스트를 관리하기 위한 배열(벡터)이다.

 

 

 

3. NUMA API

 

NUMA 데이터를 다루기 위한 API들은 다음과 같다.

#ifdef CONFIG_USE_PERCPU_NUMA_NODE_ID
DECLARE_PER_CPU(int, numa_node);
 
#ifndef numa_node_id
/* Returns the number of the current Node. */
static inline int numa_node_id(void)
{
	return raw_cpu_read(numa_node);
}
#endif
 
#ifndef set_numa_node
static inline void set_numa_node(int node)
{
	this_cpu_write(numa_node, node);
}
#endif
 
#ifndef set_cpu_numa_node
static inline void set_cpu_numa_node(int cpu, int node)
{
	per_cpu(numa_node, cpu) = node;
}
#endif
 
#else	/* !CONFIG_USE_PERCPU_NUMA_NODE_ID */
 
/* Returns the number of the current Node. */
#ifndef numa_node_id
static inline int numa_node_id(void)
{
	return cpu_to_node(raw_smp_processor_id());
}
#endif
 
extern int _node_numa_mem_[MAX_NUMNODES];
 
#ifndef set_numa_mem
static inline void set_numa_mem(int node)
{
	this_cpu_write(_numa_mem_, node);
	_node_numa_mem_[numa_node_id()] = node;
}
#endif
 
#ifndef node_to_mem_node
static inline int node_to_mem_node(int node)
{
	return _node_numa_mem_[node];
}
#endif
 
#ifndef numa_mem_id
/* Returns the number of the nearest Node with memory */
static inline int numa_mem_id(void)
{
	return raw_cpu_read(_numa_mem_);
}
#endif
 
#ifndef cpu_to_mem
static inline int cpu_to_mem(int cpu)
{
	return per_cpu(_numa_mem_, cpu);
}
#endif
 
#ifndef set_cpu_numa_mem
static inline void set_cpu_numa_mem(int cpu, int node)
{
	per_cpu(_numa_mem_, cpu) = node;
	_node_numa_mem_[cpu_to_node(cpu)] = node;
}
#endif
 
#else	/* !CONFIG_HAVE_MEMORYLESS_NODES */
 
#ifndef numa_mem_id
/* Returns the number of the nearest Node with memory */
static inline int numa_mem_id(void)
{
	return numa_node_id();
}
#endif
 
#ifndef node_to_mem_node
static inline int node_to_mem_node(int node)
{
	return node;
}
#endif
 
#ifndef cpu_to_mem
static inline int cpu_to_mem(int cpu)
{
	return cpu_to_node(cpu);
}
#endif

numa_node_id는 cpu가 소속된 node id를 반환한다.

set_numa_node는 cpu에 node를 등록한다.

XXX_numa_mem_XXX 관련 자료들에는 직접 접근하지 않고, set_numa_mem(), numa_mem_id(), cpu_to_mem()을 사용해야 한다.

numa_mem_id는 CPU에 설정된 numa 메모리의 node id를 반환한다.

numa_node 변수는 각 CPU가 소속된 node id를 저장한다.

_node_nume_mem 변수는 배열에 각 노드의 node id 혹은 메모리가 없는 노드의 경우 fallback node id를 저장한다.

_numa_mem_변수는 cpu가 소속된 node id 혹은 메모리가 없는 노드의 경우 fallback node id를 저장한다.

 

 

4. NUMA Memory Policy

 

NUMA 메모리에서 사용하는 정책은 다음과 같다.

/*
 * Both the MPOL_* mempolicy mode and the MPOL_F_* optional mode flags are
 * passed by the user to either set_mempolicy() or mbind() in an 'int' actual.
 * The MPOL_MODE_FLAGS macro determines the legal set of optional mode flags.
 */
 
/* Policies */
enum {
	MPOL_DEFAULT,
	MPOL_PREFERRED,
	MPOL_BIND,
	MPOL_INTERLEAVE,
	MPOL_LOCAL,
	MPOL_MAX,	/* always last member of enum */
};
 
/* Flags for set_mempolicy */
#define MPOL_F_STATIC_NODES	(1 << 15)
#define MPOL_F_RELATIVE_NODES	(1 << 14)
 
/*
 * MPOL_MODE_FLAGS is the union of all possible optional mode flags passed to
 * either set_mempolicy() or mbind().
 */
#define MPOL_MODE_FLAGS	(MPOL_F_STATIC_NODES | MPOL_F_RELATIVE_NODES)
 
/* Flags for get_mempolicy */
#define MPOL_F_NODE	(1<<0)	/* return next IL mode instead of node mask */
#define MPOL_F_ADDR	(1<<1)	/* look up vma using address */
#define MPOL_F_MEMS_ALLOWED (1<<2) /* return allowed memories */
 
/* Flags for mbind */
#define MPOL_MF_STRICT	(1<<0)	/* Verify existing pages in the mapping */
#define MPOL_MF_MOVE	 (1<<1)	/* Move pages owned by this process to conform
				   to policy */
#define MPOL_MF_MOVE_ALL (1<<2)	/* Move every page to conform to policy */
#define MPOL_MF_LAZY	 (1<<3)	/* Modifies '_MOVE:  lazy migrate on fault */
#define MPOL_MF_INTERNAL (1<<4)	/* Internal flags start here */
 
#define MPOL_MF_VALID	(MPOL_MF_STRICT   | 	\
			 MPOL_MF_MOVE     | 	\
			 MPOL_MF_MOVE_ALL)
 
/*
 * Internal flags that share the struct mempolicy flags word with
 * "mode flags".  These flags are allocated from bit 0 up, as they
 * are never OR'ed into the mode in mempolicy API arguments.
 */
#define MPOL_F_SHARED  (1 << 0)	/* identify shared policies */
#define MPOL_F_LOCAL   (1 << 1)	/* preferred local allocation */
#define MPOL_F_MOF	(1 << 3) /* this policy wants migrate on fault */
#define MPOL_F_MORON	(1 << 4) /* Migrate On protnone Reference On Node */

MPOL_DEFAULT는 시스템 디폴트 메모리 정책을 사용하게 만들도록 API 내부에서 사용하는 모드이다.

MPOL_PREFERRED는 선호하는 노드를 지정하여 CPU에서 해당 노드에 우선할당하도록 하는 정책이다.

MPOL_BIND는 노드를 지정하여 해당 노드에서만 메모리를 할당할 수 있도록 하는 정책이다.

MPOL_INTERLEAVE는 여러 노드를 지정하여 해당 interleave 노드들에 번갈아가며 메모리를 할당하는 정책이다.

MPOL_LOCAL은 로컬 노드를 우선 할당하도록 하는 정책이다.

/*
 * run-time system-wide default policy => local allocation
 */
static struct mempolicy default_policy = {
	.refcnt = ATOMIC_INIT(1), /* never free it */
	.mode = MPOL_PREFERRED,
	.flags = MPOL_F_LOCAL,
};

시스템에서 default 정책은 위와 같이 로컬 정책이다.

 

메모리 정책과 관련된 API들은 다음과 같다.

struct mempolicy *get_task_policy(struct task_struct *p)
{
	struct mempolicy *pol = p->mempolicy;
	int node;
 
	if (pol)
		return pol;
 
	node = numa_node_id();
	if (node != NUMA_NO_NODE) {
		pol = &preferred_node_policy[node];
		/* preferred_node_policy is not initialised early in boot */
		if (pol->mode)
			return pol;
	}
 
	return &default_policy;
}
 
/* Return the node id preferred by the given mempolicy, or the given id */
static int policy_node(gfp_t gfp, struct mempolicy *policy,
								int nd)
{
	if (policy->mode == MPOL_PREFERRED && !(policy->flags & MPOL_F_LOCAL))
		nd = policy->v.preferred_node;
	else {
		/*
		 * __GFP_THISNODE shouldn't even be used with the bind policy
		 * because we might easily break the expectation to stay on the
		 * requested node and not break the policy.
		 */
		WARN_ON_ONCE(policy->mode == MPOL_BIND && (gfp & __GFP_THISNODE));
	}
 
	return nd;
}
 
static int mpol_new_interleave(struct mempolicy *pol, const nodemask_t *nodes)
{
	if (nodes_empty(*nodes))
		return -EINVAL;
	pol->v.nodes = *nodes;
	return 0;
}
 
static int mpol_new_preferred(struct mempolicy *pol, const nodemask_t *nodes)
{
	if (!nodes)
		pol->flags |= MPOL_F_LOCAL;	/* local allocation */
	else if (nodes_empty(*nodes))
		return -EINVAL;			/*  no allowed nodes */
	else
		pol->v.preferred_node = first_node(*nodes);
	return 0;
}
 
static int mpol_new_bind(struct mempolicy *pol, const nodemask_t *nodes)
{
	if (nodes_empty(*nodes))
		return -EINVAL;
	pol->v.nodes = *nodes;
	return 0;
}
 
/*
 * mpol_set_nodemask is called after mpol_new() to set up the nodemask, if
 * any, for the new policy.  mpol_new() has already validated the nodes
 * parameter with respect to the policy mode and flags.  But, we need to
 * handle an empty nodemask with MPOL_PREFERRED here.
 *
 * Must be called holding task's alloc_lock to protect task's mems_allowed
 * and mempolicy.  May also be called holding the mmap_semaphore for write.
 */
static int mpol_set_nodemask(struct mempolicy *pol,
		     const nodemask_t *nodes, struct nodemask_scratch *nsc)
{
	int ret;
 
	/* if mode is MPOL_DEFAULT, pol is NULL. This is right. */
	if (pol == NULL)
		return 0;
	/* Check N_MEMORY */
	nodes_and(nsc->mask1,
		  cpuset_current_mems_allowed, node_states[N_MEMORY]);
 
	VM_BUG_ON(!nodes);
	if (pol->mode == MPOL_PREFERRED && nodes_empty(*nodes))
		nodes = NULL;	/* explicit local allocation */
	else {
		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;
	}
 
	if (nodes)
		ret = mpol_ops[pol->mode].create(pol, &nsc->mask2);
	else
		ret = mpol_ops[pol->mode].create(pol, NULL);
	return ret;
}
 
/*
 * 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)
{
	struct mempolicy *policy;
 
	pr_debug("setting mode %d flags %d nodes[0] %lx\n",
		 mode, flags, nodes ? nodes_addr(*nodes)[0] : NUMA_NO_NODE);
 
	if (mode == MPOL_DEFAULT) {
		if (nodes && !nodes_empty(*nodes))
			return ERR_PTR(-EINVAL);
		return NULL;
	}
	VM_BUG_ON(!nodes);
 
	/*
	 * MPOL_PREFERRED cannot be used with MPOL_F_STATIC_NODES or
	 * MPOL_F_RELATIVE_NODES if the nodemask is empty (local allocation).
	 * All other modes require a valid pointer to a non-empty nodemask.
	 */
	if (mode == MPOL_PREFERRED) {
		if (nodes_empty(*nodes)) {
			if (((flags & MPOL_F_STATIC_NODES) ||
			     (flags & MPOL_F_RELATIVE_NODES)))
				return ERR_PTR(-EINVAL);
		}
	} else if (mode == MPOL_LOCAL) {
		if (!nodes_empty(*nodes) ||
		    (flags & MPOL_F_STATIC_NODES) ||
		    (flags & MPOL_F_RELATIVE_NODES))
			return ERR_PTR(-EINVAL);
		mode = MPOL_PREFERRED;
	} else if (nodes_empty(*nodes))
		return ERR_PTR(-EINVAL);
	policy = kmem_cache_alloc(policy_cache, GFP_KERNEL);
	if (!policy)
		return ERR_PTR(-ENOMEM);
	atomic_set(&policy->refcnt, 1);
	policy->mode = mode;
	policy->flags = flags;
 
	return policy;
}
 

get_task_policy는 인자로 전달받은 태스크의 메모리 정책을 반환한다.

policy_node는 인자로 전달받은 정책이 preferred인 경우 preferred로 지정된 노드 번호를 반환한다.

mpol_new는 새로운 mempolicy 구조체를 할당하고 초기화한다.

mpol_set_nodemask는 mempolicy 구조체에 연결되는 노드의 정보 등을 설정한다.

mpol_new_xxx는 각 정책에 따라 mempolicy에 노드들을 설정하는데, 이는 mempolicty_operations에 등록하여 mpol_set_nodemask에서 mpol_ops.create를 통해 호출된다.