本文分析基于linux内核4.19.195. 在linux的实际使用中,使用hugetlbfs进行大页内存分配是一种常用的优化方法,而且我们知道,hugetlbfs能够根据需要,将不同numa节点上的内存分配给用户态程序。hugetlbfs常见的用法是,在内核启动完成后,通过写/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages接口预留适当数量的大页,然后通过hugetlbfs获得大页。那么,为了实现“将不同numa节点上的内存分配给用户态程序”,在写/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages接口预留大页的时候,均匀的在各个numa节点上预留内存,是一个比较合理的做法,当然内核也确实是这么干的。如下面步骤的输出所示(我的机器上有两个numa节点):
[root@localhost hugepages-2048kB]
0
[root@localhost hugepages-2048kB]
[root@localhost node]
1
[root@localhost node]
0
[root@localhost hugepages-2048kB]
[root@localhost node]
1
[root@localhost node]
1
[root@localhost node]
[root@localhost node]
5
[root@localhost node]
5
[root@localhost node]
/sys/devices/system/node
可以看到,hugepage被均匀的分散到了2个numa节点中。但是,内核是怎么实现的呢? 我们知道,写/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages接口,最终会调用到内核的hugetlb_sysctl_handler函数。
int hugetlb_sysctl_handler(struct ctl_table *table, int write,
void __user *buffer, size_t *length, loff_t *ppos)
{
return hugetlb_sysctl_handler_common(false, table, write,
buffer, length, ppos);
}
static int hugetlb_sysctl_handler_common(bool obey_mempolicy,
struct ctl_table *table, int write,
void __user *buffer, size_t *length, loff_t *ppos)
{
struct hstate *h = &default_hstate;
unsigned long tmp = h->max_huge_pages;
int ret;
if (!hugepages_supported())
return -EOPNOTSUPP;
ret = proc_hugetlb_doulongvec_minmax(table, write, buffer, length, ppos,
&tmp);
if (ret)
goto out;
if (write)
ret = __nr_hugepages_store_common(obey_mempolicy, h,
NUMA_NO_NODE, tmp, *length);
out:
return ret;
}
主要是在函数__nr_hugepages_store_common中实现的大页预留。
static ssize_t __nr_hugepages_store_common(bool obey_mempolicy,
struct hstate *h, int nid,
unsigned long count, size_t len)
{
int err;
NODEMASK_ALLOC(nodemask_t, nodes_allowed, GFP_KERNEL | __GFP_NORETRY);
if (hstate_is_gigantic(h) && !gigantic_page_supported()) {
err = -EINVAL;
goto out;
}
if (nid == NUMA_NO_NODE) {
if (!(obey_mempolicy &&
init_nodemask_of_mempolicy(nodes_allowed))) {
NODEMASK_FREE(nodes_allowed);
nodes_allowed = &node_states[N_MEMORY];
}
} else if (nodes_allowed) {
count += h->nr_huge_pages - h->nr_huge_pages_node[nid];
init_nodemask_of_node(nodes_allowed, nid);
} else
nodes_allowed = &node_states[N_MEMORY];
h->max_huge_pages = set_max_huge_pages(h, count, nodes_allowed);
if (nodes_allowed != &node_states[N_MEMORY])
NODEMASK_FREE(nodes_allowed);
return len;
out:
NODEMASK_FREE(nodes_allowed);
return err;
}
进入__nr_hugepages_store_common函数,首先是获取nodes_allowed这个nodemask_t,然后调用set_max_huge_pages函数完成大页的预留动作。按照调用流程分析,当我们通过写/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages接口完成大页的预留时,会走进if (nid == NUMA_NO_NODE) 分支并将nodes_allowed设置为&node_states[N_MEMORY],也就是说,会从所有有内存的numa节点分配内存。 接下来进入函数set_max_huge_pages。该函数会调用alloc_pool_huge_page(h, nodes_allowed);完成内存的分析,我们来看alloc_pool_huge_page函数。
#define for_each_node_mask_to_alloc(hs, nr_nodes, node, mask) \
for (nr_nodes = nodes_weight(*mask); \
nr_nodes > 0 && \
((node = hstate_next_node_to_alloc(hs, mask)) || 1); \
nr_nodes--)
static int get_valid_node_allowed(int nid, nodemask_t *nodes_allowed)
{
if (!node_isset(nid, *nodes_allowed))
nid = next_node_allowed(nid, nodes_allowed);
return nid;
}
static int hstate_next_node_to_alloc(struct hstate *h,
nodemask_t *nodes_allowed)
{
int nid;
VM_BUG_ON(!nodes_allowed);
nid = get_valid_node_allowed(h->next_nid_to_alloc, nodes_allowed);
h->next_nid_to_alloc = next_node_allowed(nid, nodes_allowed);
return nid;
}
static int alloc_pool_huge_page(struct hstate *h, nodemask_t *nodes_allowed)
{
struct page *page;
int nr_nodes, node;
gfp_t gfp_mask = htlb_alloc_mask(h) | __GFP_THISNODE;
for_each_node_mask_to_alloc(h, nr_nodes, node, nodes_allowed) {
page = alloc_fresh_huge_page(h, gfp_mask, node, nodes_allowed);
if (page)
break;
}
if (!page)
return 0;
put_page(page);
return 1;
}
重点在for_each_node_mask_to_alloc宏定义。重点关注get_valid_node_allowed函数调用的第一个参数,传入的是h->next_nid_to_alloc,并且,在调用get_valid_node_allowed之后,立马更新了h->next_nid_to_alloc。也就是说,每次都是从h->next_nid_to_alloc这个numa节点开始去分配大页,每次分配之后,都会更新这个变量。依靠这个变量的不断更新,内核实现了从各个numa节点上均匀分配内存的功能。
|