page를 얻는 함수 이름이 비슷해도 반환형과 policy는 다릅니다. 어떤 함수는 page descriptor를, 어떤 함수는 kernel virtual address를 반환합니다. node 번호를 받는 함수도 task의 NUMA policy를 따르는 함수와 같지 않으며, 연속한 block을 얻었다고 compound 수명이 자동으로 생기지도 않습니다.
여기서는 Linux v6.1의 실제 정의를 기준으로 이 계약을 나누겠습니다. allocator의 zone 탐색·watermark·lock은 allocation context 글에서 다루고, 이 글은 그 위의 진입 API에 집중합니다. code는 표시한 function의 전문이며 supporting kernel type과 macro가 필요한 kernel 내부 구현입니다. 실행 결과나 독립 user-space 프로그램은 아닙니다.
먼저 반환형과 policy를 고릅니다
| 요청의 의미 | 대표 API | 반환·실패 | 정책상 주의점 |
|---|---|---|---|
| order 크기의 page block | alloc_pages() |
struct page *, 실패는 NULL |
NUMA 구성에서는 현재 task policy도 선택합니다. |
| 명시 node 선호 | alloc_pages_node() |
struct page *, 실패는 NULL |
NUMA_NO_NODE를 기본 선호 node로 정규화하며 엄격한 THISNODE와는 다릅니다. |
| compound를 준비한 folio | folio_alloc()·__folio_alloc() |
struct folio *, 실패는 NULL |
COMP를 더하며 task policy를 거치는지는 wrapper와 구성에 달려 있습니다. |
| 직접 사용할 kernel 주소 | __get_free_pages()·get_zeroed_page() |
unsigned long, 실패는 0 |
HIGHMEM을 허용하는 descriptor API와 계약이 다릅니다. |
| byte 크기에 맞는 최소 page 수 | alloc_pages_exact() |
void *, 실패는 NULL |
뒤쪽 초과 page를 반환하고 free_pages_exact()와 짝을 이룹니다. |
| slab의 backing page | SLUB의 alloc_slab_page() |
struct slab *, 실패는 NULL |
object 하나의 allocation과 backing block의 확보는 다른 층입니다. |
| slab cache보다 큰 kmalloc | kmalloc_large() |
void *, 실패는 NULL |
order 기반 compound allocation과 accounting·sanitizer hook을 거칩니다. |
alloc_page()는 같은 gfp.h에 다음과 같이 정의된 macro입니다. 별도의 order 선택 algorithm이나 새로운 module-export 함수가 아닙니다.
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
public header에 이름이 보인다는 사실과 실제 out-of-line symbol, EXPORT_SYMBOL() 표시는 구별해야 합니다. 예를 들어 node wrapper들은 header inline이고 __alloc_pages()는 별도의 구현과 export를 가집니다.
명시 node wrapper는 task policy wrapper와 다릅니다
다음 세 함수는 include/linux/gfp.h의 전문입니다. SPDX는 GPL-2.0이며 들여쓰기만 정리했습니다.
static inline struct page *
__alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
warn_if_node_offline(nid, gfp_mask);
return __alloc_pages(gfp_mask, order, nid, NULL);
}
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,
unsigned int order)
{
if (nid == NUMA_NO_NODE)
nid = numa_mem_id();
return __alloc_pages_node(nid, gfp_mask, order);
}
static inline
struct folio *__folio_alloc_node(gfp_t gfp, unsigned int order, int nid)
{
VM_BUG_ON(nid < 0 || nid >= MAX_NUMNODES);
warn_if_node_offline(nid, gfp);
return __folio_alloc(gfp, order, nid, NULL);
}
이 경로는 preferred node를 주어 중앙 allocator를 부릅니다. NUMA_NO_NODE를 허용하는 바깥 wrapper와 유효·online node를 전제로 하는 안쪽 helper의 조건이 다릅니다. assertion이 있다는 이유로 invalid node를 전달해도 정상적인 NULL 실패로 처리된다고 기대해서는 안 됩니다. 또한 이 함수가 NULL nodemask를 전달해도 cpuset과 GFP의 zone 제한까지 사라지는 것은 아닙니다.
NUMA를 사용하지 않는 구성의 alloc_pages()와 folio_alloc()은 같은 header에 다음처럼 정의됩니다. 각각 function 전문이며 이 두 정의는 CONFIG_NUMA의 else 분기에 속합니다.
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_node(numa_node_id(), gfp_mask, order);
}
static inline struct folio *folio_alloc(gfp_t gfp, unsigned int order)
{
return __folio_alloc_node(gfp, order, numa_node_id());
}
반면 NUMA 구성에서는 mm/mempolicy.c의 다음 두 정의를 사용합니다. 이 파일의 SPDX는 GPL-2.0-only입니다.
struct page *alloc_pages(gfp_t gfp, unsigned order)
{
struct mempolicy *pol = &default_policy;
struct page *page;
if (!in_interrupt() && !(gfp & __GFP_THISNODE))
pol = get_task_policy(current);
/*
* No reference counting needed for current->mempolicy
* nor system default_policy
*/
if (pol->mode == MPOL_INTERLEAVE)
page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
else if (pol->mode == MPOL_PREFERRED_MANY)
page = alloc_pages_preferred_many(gfp, order,
policy_node(gfp, pol, numa_node_id()), pol);
else
page = __alloc_pages(gfp, order,
policy_node(gfp, pol, numa_node_id()),
policy_nodemask(gfp, pol));
return page;
}
struct folio *folio_alloc(gfp_t gfp, unsigned order)
{
struct page *page = alloc_pages(gfp | __GFP_COMP, order);
if (page && order > 1)
prep_transhuge_page(page);
return (struct folio *)page;
}
interrupt 밖이고 THISNODE가 아니어야 current task의 policy를 선택합니다. INTERLEAVE와 PREFERRED_MANY는 각자의 allocation helper로, 그 밖의 policy는 node와 nodemask를 구해 중앙 allocator로 이어집니다. 여기서 current task policy와 default policy를 읽는 경로에는 별도의 get/put이 없다는 주석도 있습니다. 이를 다른 task의 policy pointer나 임의의 장기 보관에도 적용되는 수명 규칙으로 확대해서는 안 됩니다.
Folio wrapper는 COMP를 실제로 더합니다
다음은 mm/page_alloc.c의 __folio_alloc() 전문입니다. SPDX는 GPL-2.0-only입니다.
struct folio *__folio_alloc(gfp_t gfp, unsigned int order, int preferred_nid,
nodemask_t *nodemask)
{
struct page *page = __alloc_pages(gfp | __GFP_COMP, order,
preferred_nid, nodemask);
if (page && order > 1)
prep_transhuge_page(page);
return (struct folio *)page;
}
NUMA policy를 거치는 folio_alloc()도 COMP를 더하지만, __folio_alloc()은 인수의 preferred node와 nodemask를 중앙 allocator에 직접 전달합니다. 따라서 둘을 무조건 같은 task-policy wrapper로 대체할 수는 없습니다. 이 tag에서는 성공한 결과의 order가 1보다 클 때 prep_transhuge_page()도 호출합니다. 단순히 반환 pointer를 cast한 것만으로 compound 준비를 대신하는 구조가 아닙니다.
주소를 반환하려면 HIGHMEM 계약을 바꿔야 합니다
다음 두 함수는 mm/page_alloc.c의 전문이며 SPDX는 GPL-2.0-only입니다.
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
page = alloc_pages(gfp_mask & ~__GFP_HIGHMEM, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
__get_free_pages()의 이 구현은 HIGHMEM bit를 제거하고, 얻은 descriptor를 page_address()로 바꿉니다. 실패 시 0인 주소 값이므로 descriptor-return API의 NULL과 반환형부터 다릅니다. HIGHMEM을 쓸 수 있는 descriptor를 얻은 뒤 적절히 mapping하는 경로와 혼동하지 않아야 합니다. get_zeroed_page()는 order 0과 ZERO 요구를 더하는 wrapper이며 서로 충돌하는 GFP action bit를 임의로 더해도 같은 초기화 계약이 유지된다고 가정해서는 안 됩니다.
Exact allocation은 초과 page를 되돌려 줍니다
4 KiB base page와 양수 요청 9 KiB를 예로 들면 필요한 것은 3 page입니다. 먼저 order-2인 4-page block을 얻은 뒤 마지막 page를 반환할 수 있습니다. 마지막 보유 page 안의 남는 byte를 다른 owner에게 나누어 준다는 뜻은 아닙니다.
다음은 alloc_pages_exact()와 make_alloc_exact()의 전문입니다. SPDX는 GPL-2.0-only입니다.
void *alloc_pages_exact(size_t size, gfp_t gfp_mask)
{
unsigned int order = get_order(size);
unsigned long addr;
if (WARN_ON_ONCE(gfp_mask & (__GFP_COMP | __GFP_HIGHMEM)))
gfp_mask &= ~(__GFP_COMP | __GFP_HIGHMEM);
addr = __get_free_pages(gfp_mask, order);
return make_alloc_exact(addr, order, size);
}
static void *make_alloc_exact(unsigned long addr, unsigned int order,
size_t size)
{
if (addr) {
unsigned long nr = DIV_ROUND_UP(size, PAGE_SIZE);
struct page *page = virt_to_page((void *)addr);
struct page *last = page + nr;
split_page_owner(page, 1 << order);
split_page_memcg(page, 1 << order);
while (page < --last)
set_page_refcounted(last);
last = page + (1UL << order);
for (page += nr; page < last; page++)
__free_pages_ok(page, 0, FPI_TO_TAIL);
}
return (void *)addr;
}
caller는 COMP와 HIGHMEM을 지정하지 않는 계약을 지켜야 합니다. 이 tag의 구현은 잘못 지정한 경우 warning 후 bit를 제거합니다. helper는 owner·memcg의 split 장부를 맞추고 보유할 뒤쪽 page들의 reference를 준비한 뒤, 초과 page를 개별 반환합니다. 단순히 반환 pointer에 필요한 byte 수만 붙인 것과는 다릅니다. 해제도 원래 size를 주는 free_pages_exact()를 사용해야 합니다.
Slab 공급과 large kmalloc은 서로 다른 변환입니다
v6.1 SLUB에서 확인하는 함수 이름은 alloc_slab_page()입니다. SPDX는 GPL-2.0이며 다음이 전문입니다.
static inline struct slab *alloc_slab_page(gfp_t flags, int node,
struct kmem_cache_order_objects oo)
{
struct folio *folio;
struct slab *slab;
unsigned int order = oo_order(oo);
if (node == NUMA_NO_NODE)
folio = (struct folio *)alloc_pages(flags, order);
else
folio = (struct folio *)__alloc_pages_node(node, flags, order);
if (!folio)
return NULL;
slab = folio_slab(folio);
__folio_set_slab(folio);
if (page_is_pfmemalloc(folio_page(folio, 0)))
slab_set_pfmemalloc(slab);
return slab;
}
node가 지정되지 않으면 alloc_pages(), 지정되면 __alloc_pages_node()를 사용합니다. 결과를 slab overlay로 해석하고 slab 표시와 pfmemalloc 상태를 설정합니다. 여기의 folio pointer cast를 folio_alloc() 호출과 동일시해서는 안 됩니다. 어느 metadata를 준비하는지는 실제 호출과 flag 설정으로 판단해야 합니다.
large-kmalloc 쪽은 mm/slab_common.c의 다음 두 함수로 확인할 수 있습니다. SPDX는 GPL-2.0입니다. 호출도에 보이는 kmalloc_order()라는 label을 이 v6.1 구현의 함수명으로 그대로 대입하지 않습니다.
void *kmalloc_large(size_t size, gfp_t flags)
{
void *ret = __kmalloc_large_node(size, flags, NUMA_NO_NODE);
trace_kmalloc(_RET_IP_, ret, size, PAGE_SIZE << get_order(size),
flags, NUMA_NO_NODE);
return ret;
}
static void *__kmalloc_large_node(size_t size, gfp_t flags, int node)
{
struct page *page;
void *ptr = NULL;
unsigned int order = get_order(size);
if (unlikely(flags & GFP_SLAB_BUG_MASK))
flags = kmalloc_fix_flags(flags);
flags |= __GFP_COMP;
page = alloc_pages_node(node, flags, order);
if (page) {
ptr = page_address(page);
mod_lruvec_page_state(page, NR_SLAB_UNRECLAIMABLE_B,
PAGE_SIZE << order);
}
ptr = kasan_kmalloc_large(ptr, size, flags);
/* As ptr might get tagged, call kmemleak hook after KASAN. */
kmemleak_alloc(ptr, size, 1, flags);
kmsan_kmalloc_large(ptr, size, flags);
return ptr;
}
요청 byte 수를 order로 올림하고 COMP를 더해 node wrapper로 확보합니다. 성공한 물리 범위는 slab-unreclaimable byte 통계에 반영되며, 그 뒤에도 KASAN·kmemleak·KMSAN hook이 있습니다. KASAN이 pointer tag를 바꿀 수 있어 kmemleak hook의 순서도 명시되어 있습니다. 반환받은 memory의 public 해제 API는 kfree()이며, backing page가 있다는 이유로 caller가 내부 반환 경로를 임의 선택하지 않습니다.
이렇게 나누면 비슷한 함수 이름에서 실제로 필요한 선택이 드러납니다. 반환형, NUMA policy의 적용 위치, compound 준비, 크기 반올림과 해제 짝을 먼저 정하고 그 아래의 zone 탐색·watermark·PCP/buddy 경로를 읽는 것이 순서입니다.
참고 자료
- Linux
v6.1, commit830b3c68c1fb1e9176028d02ef86f3cf76aa2476: public allocation header, NUMA policy wrapper. - Linux
v6.1: page/folio/address/exact allocation, SLUB backing allocation, large kmalloc와 kfree.