Free memory가 적어도 allocation이 가능한 이유

free에서 free 값은 작고 buff/cache는 큰 상황을 보셨을 수 있습니다. 사용되지 않는 RAM만 allocation에 쓸 수 있다면 곧 memory가 부족해질 것 같습니다. 하지만 kernel은 필요할 때 회수할 수 있는 memory도 사용 가능량을 추정할 때 고려합니다.

여기서는 Linux v6.12의 si_mem_available()과 procfs를 기준으로 MemAvailable의 계산을 살펴보겠습니다. 이 값은 앞으로 실행할 프로그램의 allocation 성공을 보증하는 수치가 아니라, swapping 없이 사용할 수 있는 memory에 대한 추정입니다.

free의 숫자를 /proc/meminfo로 되짚습니다

우선 조회만 하는 명령으로 같은 시점의 항목을 비교하시면 됩니다. 아래 명령은 설정을 변경하지 않습니다.

free -h
cat /proc/meminfo
cat /proc/buddyinfo

free의 기본 출력 단위는 보통 KiB이며, -h는 크기에 따라 단위를 바꾸어 표시합니다. /proc/meminfo에서 표기하는 kB는 이 문맥에서는 1,024 bytes 단위입니다. 서로 다른 단위의 화면을 그대로 빼거나 더하면 맞지 않는 것처럼 보일 수 있습니다.

책에서 설명하는 출력 항목의 대응 관계는 다음과 같습니다. free 자체의 version에 따라서도 표시 방식이 달라질 수 있으므로, 도구의 출력과 kernel의 원래 항목을 구분해서 읽으시는 편이 좋습니다.

출력 항목 주로 대응하는 항목과 의미
total MemTotal. Kernel이 사용 가능한 것으로 관리하는 physical memory이며, 장착된 RAM 전체와 같다고 보장하지 않습니다.
free MemFree. 현재 어떤 용도로도 사용하지 않는 memory입니다.
available MemAvailable. Reserve와 회수 가능량을 고려한 추정입니다.
used 책의 계산 기준에서는 MemTotal - MemAvailable입니다.
shared Shmem. tmpfs 등을 포함하는 shmem memory입니다. 모든 종류의 공유 mapping을 합한 수치가 아닙니다.
buff/cache 책의 계산 기준에서는 Buffers + Cached + SReclaimable입니다. 전부 즉시 회수할 수 있다는 뜻은 아닙니다.

Buffers는 block device의 buffer 용도 memory를 나타냅니다. Cached는 file cache의 관련 통계에서 swap cache와 buffer에 해당하는 부분 등을 제외한 값으로 계산됩니다. SReclaimable은 slab 중 회수 가능한 것으로 분류된 부분입니다. 이미 사용 중이라는 사실과 앞으로 회수할 수 있다는 사실은 모순되지 않습니다.

아래는 단위와 열의 관계를 비교하기 위해 만든 설명용 출력이며 실제 측정 결과가 아닙니다. free -k로 KiB 단위를 명시한 경우입니다. 같은 예시의 meminfo에서는 관련 필드만 골라 적었습니다.

               total   used   free  shared  buff/cache  available
Mem:           65536  24576  16384    1024       25088      40960
Swap:           8192   2048   6144

MemTotal:       65536 kB
MemFree:        16384 kB
MemAvailable:   40960 kB
Buffers:         512 kB
Cached:        20480 kB
Shmem:          1024 kB
SReclaimable:   4096 kB
SwapTotal:      8192 kB
SwapFree:       6144 kB

이 계산 기준에서 used는 65536 - 40960 = 24576 KiB, buff/cache는 512 + 20480 + 4096 = 25088 KiB입니다. used + free + buff/cache를 total과 같게 만드는 방식으로 역산해서는 안 됩니다. used가 available의 추정을 이용하기 때문입니다. free -h는 같은 크기를 사람이 읽기 쉬운 단위로 반올림해 표시하므로, 이미 반올림된 화면끼리 정확한 합계를 맞추려 하지 않는 편이 좋습니다.

Free pages에서도 reserve를 제외합니다

si_mem_available()은 먼저 모든 zone의 low watermark를 합합니다. 이를 여기서는 L이라고 하겠습니다. 이어 free pages에서 totalreserve_pages를 뺀 값을 계산의 출발점으로 삼습니다.

totalreserve_pages에는 zone watermark와 lowmem reserve에 관한 제약이 반영됩니다. 따라서 L과 totalreserve_pages를 같은 수치로 바꾸어 쓰시면 안 됩니다. 전자는 뒤에서 회수 가능량을 보수적으로 계산하는 데 쓰이고, 후자는 처음부터 userspace에 모두 내줄 수 없는 reserve를 제외하는 데 쓰입니다.

예를 들어 free가 40 MiB이고 이 계산에 적용되는 reserve가 20 MiB라면, 첫 항은 20 MiB입니다. 이는 설명을 위한 독립적인 수치이며 실제 장비에서 측정한 결과는 아닙니다.

Page cache도 전부 회수한다고 가정하지 않습니다

다음으로 NR_ACTIVE_FILE과 NR_INACTIVE_FILE을 더해 file LRU의 크기를 구합니다. 여기서 일정량을 남기고 나머지만 available에 더합니다. Cache를 전부 비우면 다시 읽는 I/O가 늘어나고 thrashing이 생길 수 있기 때문입니다.

남기는 양은 file LRU 크기의 절반과 L 중 작은 값입니다. 예를 들어 file LRU가 80 MiB이고 L이 12 MiB라면, 40 MiB와 12 MiB 중 작은 12 MiB를 남기고 68 MiB를 더합니다. 항상 절반을 남기는 것도, 항상 low watermark 전체를 남기는 것도 아닙니다.

Shmem은 file 형태로 접근할 수 있어도 이 계산에서 일반적인 clean file cache처럼 취급할 수 없습니다. 내용이 RAM에만 있다면 저장된 file data를 다시 읽는 방식으로 복구할 수 없기 때문입니다. LRU와 reclaim에서는 swap-backed anonymous memory 쪽의 특성을 가지며, Cached 화면의 숫자를 이 함수의 file LRU 값과 그대로 동일시하면 안 됩니다.

Reclaimable kernel memory를 더합니다

Linux v6.12 구현은 reclaimable slab뿐 아니라 NR_KERNEL_MISC_RECLAIMABLE도 포함합니다. Slab counter는 byte 기반 항목을 page 단위로 바꾸어 합산합니다. 서로 다른 단위의 counter를 그대로 더하지 않는다는 점도 source를 읽을 때 확인할 부분입니다.

이 kernel reclaimable 합계에서도 절반과 L 중 작은 값을 남깁니다. Reclaimable로 분류되었다고 해서 지금 당장 모든 object를 버릴 수 있는 것은 아닙니다. 마지막 결과가 음수이면 0으로 제한합니다.

다음은 실제 kernel code의 복사본이 아니라 계산 관계만 나타낸 수식입니다. F, R, C, K, L은 모두 같은 단위로 환산한 값입니다.

F = free memory
R = totalreserve_pages
C = active_file + inactive_file
K = reclaimable slab + other reclaimable kernel memory
L = sum of zone low watermarks

available = max(0,
    F - R + C - min(C / 2, L) + K - min(K / 2, L))
독립적인 예시로 계산한 MemAvailable. 빗금은 available에 더하지 않는 부분입니다.
독립적인 예시로 계산한 MemAvailable. 빗금은 available에 더하지 않는 부분입니다.

그림의 값에서는 free 40 MiB 중 20 MiB, file LRU 80 MiB 중 68 MiB, kernel reclaimable 40 MiB 중 28 MiB를 더해 116 MiB가 됩니다. 실제 함수는 page 단위 정수 연산으로 계산하므로, 임의의 byte 단위 실수 계산과 세부 반올림까지 같지는 않습니다.

실제 함수의 시작부터 반환까지 읽습니다

책의 Listing 14-4는 원본 두 쪽에 걸쳐 있습니다. 앞쪽의 선언과 뒷쪽의 계산을 연결해야 함수 전체가 됩니다. 책에서는 mm/page_alloc.c에 있던 구현을 다루지만, 여기서 확인한 Linux v6.12에서는 mm/show_mem.c에 있습니다.

아래는 Linux v6.12의 함수 전체입니다. Kernel 내부의 counter와 zone API를 사용하는 코드이므로 독립적인 userspace 프로그램으로 compile하는 예제가 아닙니다. 원본 파일의 GPL-2.0-only 표시와 출처를 함께 남겼습니다.

si_mem_available_v6_12.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Linux v6.12 mm/show_mem.c, complete si_mem_available() excerpt.
 * File copyright (C) 2008 Johannes Weiner <hannes@saeurebad.de>.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/show_mem.c
 * Kernel context required; this is not a standalone userspace program.
 */
long si_mem_available(void)
{
	long available;
	unsigned long pagecache;
	unsigned long wmark_low = 0;
	unsigned long reclaimable;
	struct zone *zone;

	for_each_zone(zone)
		wmark_low += low_wmark_pages(zone);

	/*
	 * Estimate the amount of memory available for userspace allocations,
	 * without causing swapping or OOM.
	 */
	available = global_zone_page_state(NR_FREE_PAGES) - totalreserve_pages;

	/*
	 * Not all the page cache can be freed, otherwise the system will
	 * start swapping or thrashing. Assume at least half of the page
	 * cache, or the low watermark worth of cache, needs to stay.
	 */
	pagecache = global_node_page_state(NR_ACTIVE_FILE) +
		global_node_page_state(NR_INACTIVE_FILE);
	pagecache -= min(pagecache / 2, wmark_low);
	available += pagecache;

	/*
	 * Part of the reclaimable slab and other kernel memory consists of
	 * items that are in use, and cannot be freed. Cap this estimate at the
	 * low watermark.
	 */
	reclaimable = global_node_page_state_pages(NR_SLAB_RECLAIMABLE_B) +
		global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);
	reclaimable -= min(reclaimable / 2, wmark_low);
	available += reclaimable;

	if (available < 0)
		available = 0;
	return available;
}

이 구현은 file LRU의 두 counter를 직접 더합니다. 책의 고전 구현처럼 NR_LRU_LISTS 배열을 먼저 채우는 형태와 달라도, active·inactive file pages를 모아 reserve를 제외한다는 계산 관계는 같습니다. Slab 쪽에는 global_node_page_state_pages()를 사용하고, miscellaneous reclaimable counter와 함께 page 단위로 계산합니다.

meminfo_proc_show()는 이렇게 얻은 page 단위 값을 /proc/meminfo의 단위로 표시합니다. 함수가 반환한 long을 이미 byte 수나 KiB 값으로 읽으시면 안 됩니다. 이 함수의 원본 전체 파일, 함께 제공하는 GPL-2.0-only license와 공식 license 파일도 확인하실 수 있습니다.

높은 order의 allocation에는 다른 질문이 필요합니다

MemAvailable이 충분해도 연속된 physical memory가 부족할 수 있습니다. Buddy allocator에서 order n의 block은 2^n개의 연속된 base pages입니다. Base page가 4 KiB이면 order 5는 32 pages, 즉 128 KiB입니다.

Order 0 요청은 높은 order의 block을 나누어서도 만족시킬 수 있습니다. 반대로 order가 높은 요청은 같은 총량의 free pages가 흩어져 있다는 사실만으로 만족시킬 수 없습니다. 필요한 node, zone, GFP 조건과 fragmentation을 함께 살펴보아야 합니다. 높은 order의 실패가 반드시 OOM kill로 이어지는 것도 아닙니다.

/proc/buddyinfo는 node와 zone별로 각 order의 free block 개수를 보여 줍니다. 각 열은 byte 수나 page 수가 아니라 해당 order의 block 개수입니다. 따라서 전체 크기를 계산하려면 열 n의 값에 2^n과 base page 크기를 곱해야 합니다.

다음 역시 실제 측정이 아닌 독립적인 예시입니다. 숫자 열은 왼쪽부터 order 0, 1, 2, ... 순서입니다.

Node 0, zone Normal  4 2 1 0 0 0 0 0 0 0 0
Node 1, zone Normal  0 0 0 1 0 0 0 0 0 0 0

4 KiB base page를 가정하면 node 0에는 (4 + 2*2 + 1*4) * 4 = 48 KiB, node 1에는 1 * 8 * 4 = 32 KiB가 표시되어 있습니다. Node 0의 총량이 더 크지만 이 표에는 order 3 block이 없습니다. Node 1에는 order 3 block이 하나 있습니다. 요청의 NUMA policy와 zone 조건을 무시하고 두 행을 합한 총량만 보시면 이런 차이를 놓치게 됩니다. Buddy의 최대 order 명칭과 configuration도 kernel version에 따라 확인해야 하며, 책의 MAX_ORDER - 1 표기를 모든 version의 고정 상수로 사용하지 않습니다.

이 표 역시 allocation 성공 판정기는 아닙니다. CPU별 cache, 동시 allocation과 migration, 요청의 제약 때문에 조회 직후에도 상태가 달라질 수 있습니다. 전체적인 여유를 볼 때는 MemAvailable, 연속성 문제를 의심할 때는 buddyinfo를 출발점으로 삼고, 실제 실패한 allocation의 조건으로 범위를 좁히시는 편이 좋습니다.

참고 자료