Linux v6.18.37 · 개념과 코드 읽기

buddy allocator: 큰 빈 블록을 반씩 나누기

이 코드는 어떤 문제를 푸나요?

요청한 크기의 빈 블록이 없다면 더 큰 블록을 쪼개서 사용할 수 있습니다. buddy의 order는 바이트 크기가 아니라 2의 몇 제곱 개 페이지인지를 나타냅니다. expand는 큰 블록의 앞부분을 계속 남겨 두면서 뒤쪽 절반들을 빈 목록에 돌려놓습니다.

읽을 범위: v6.18.37 · mm/page_alloc.c · expand 1740–1766행입니다. 아래에 이 범위의 원문과 각 줄의 설명을 실었습니다. 주제 전체의 흐름과 다른 경로는 기존 분석에서 함께 읽으실 수 있습니다.

먼저 알아둘 개념

order

order n의 블록은 2^n개 페이지입니다. 페이지가 4 KiB인 예에서 order 3은 8페이지, 32 KiB입니다. 페이지 크기는 커널 구성에 따라 달라집니다.

buddy

같은 크기로 나눈 두 절반은 서로 buddy입니다. 임의의 인접 빈 페이지 두 개가 항상 합쳐질 수 있는 것은 아니며 주소 정렬과 order가 맞아야 합니다.

migratetype

비슷한 이동 가능성을 가진 페이지를 모아 단편화를 줄이려는 분류입니다. 이 함수는 분할한 빈 절반을 해당 종류의 목록에 넣습니다.

처음 읽을 때

8페이지 블록에서 2페이지가 필요하다고 가정해 보세요. 8을 4+4로 나누고, 앞의 4를 다시 2+2로 나누면 요청한 2페이지와 빈 4·2페이지 블록이 남습니다.

더 깊이 살펴볼 때

디버깅용 guard page가 일반 free list에 추가되지 않을 때 nr_added가 어떻게 달라지는지 계산해 보세요. 반환값은 처음 큰 블록 크기가 아니라 실제 추가한 페이지 수입니다.

그림으로 보는 변화

같은 주소 축에서 order 3의 8페이지 블록을 order 2의 4페이지 두 개로 나누고, 앞 블록을 order 1의 2페이지 두 개로 나누어 요청할 2페이지와 빈 2·4페이지 블록을 남기는 그림.
4 KiB 페이지 8개를 4+4, 다시 2+2+4로 나누는 예시입니다. 각 줄은 같은 물리 주소 범위이며 오른쪽 화살표는 주소 증가, 아래쪽 화살표는 분할 순서입니다. guard page를 사용하지 않는 경우입니다.
buddy allocator: 큰 빈 블록을 반씩 나누기의 단계별 개념 그림
각 단계에 화살표 의미와 생략 범위를 표시했습니다. 주소·숫자 예제는 실제 장치 값을 뜻하지 않습니다.
1단계 설명

1단계 고정

GIF 원본 열기

1. 큰 빈 블록 선택

예시 high=3: 8페이지

화살표는 분할 순서를 뜻합니다. 예시는 low=1, guard page를 쓰지 않는 경우입니다.

2. 절반을 빈 목록에 반환

8 → 4 + 4, 뒤의 4를 free list에 추가

주소가 증가하는 쪽의 절반 page[size]를 반환합니다. 원래 시작 page는 유지합니다.

3. 필요한 크기까지 반복

앞의 4 → 2 + 2, 뒤의 2를 반환

남은 앞의 2페이지가 요청 크기입니다. 반환 페이지 수는 4+2=6입니다.

expand를 한 줄씩 읽기

줄 번호는 v6.18.37 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

static inline unsigned int expand(struct zone *zone, struct page *page, int low,
				  int high, int migratetype)
{
	unsigned int size = 1 << high;
	unsigned int nr_added = 0;

	while (high > low) {
		high--;
		size >>= 1;
		VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);

		/*
		 * Mark as guard pages (or page), that will allow to
		 * merge back to allocator when buddy will be freed.
		 * Corresponding page table entries will not be touched,
		 * pages will stay not present in virtual address space
		 */
		if (set_page_guard(zone, &page[size], high))
			continue;

		__add_to_free_list(&page[size], zone, high, migratetype, false);
		set_buddy_order(&page[size], high);
		nr_added += size;
	}

	return nr_added;
}
static inline unsigned int expand(struct zone *zone, struct page *page, int low,

zone의 큰 빈 블록 page를 low order까지 나누는 내부 함수입니다. 반환형이 정수인 것은 빈 목록에 추가한 페이지 수를 보고하기 때문입니다.

				  int high, int migratetype)

high는 현재 큰 블록의 order이고 migratetype은 나눈 빈 블록을 넣을 목록 종류입니다. low와 high의 단위는 바이트가 아닙니다.

	unsigned int size = 1 << high;

2의 high제곱으로 현재 블록의 페이지 수를 구합니다. high=3이면 size=8페이지입니다.

	unsigned int nr_added = 0;

빈 목록에 실제 추가한 페이지 수를 0에서 셉니다.

	while (high > low) {

현재 order가 필요한 order보다 큰 동안만 분할합니다. 같아지면 더 쪼개지 않습니다.

		high--;

한 번 분할하면 각 절반의 order는 1 줄어듭니다.

		size >>= 1;

새 절반의 페이지 수로 size를 갱신합니다. 뒤쪽 절반의 시작도 이 수로 찾습니다.

		VM_BUG_ON_PAGE(bad_range(zone, &page[size]), &page[size]);

page[size]가 예상한 zone 범위 안에 있는지 디버그 조건을 검사합니다. 잘못된 경계 계산으로 다른 zone의 메타데이터를 조작하면 안 됩니다.

		if (set_page_guard(zone, &page[size], high))

뒤쪽 절반을 디버깅용 guard 영역으로 지정할지 판단합니다. 성공하면 일반 할당용 빈 목록에 넣지 않습니다.

			continue;

guard로 지정된 절반은 아래 목록 추가와 nr_added 계산을 건너뛰고 다음 분할로 진행합니다.

		__add_to_free_list(&page[size], zone, high, migratetype, false);

뒤쪽 절반을 해당 order와 migratetype의 free list에 넣습니다. false는 목록의 tail에 추가하라는 선택이 아님을 나타냅니다.

		set_buddy_order(&page[size], high);

이 절반의 시작 페이지에 buddy order를 기록하여 이후 탐색·병합 시 크기를 알 수 있게 합니다.

		nr_added += size;

이번에 빈 목록에 추가한 절반의 페이지 수를 누적합니다.

	return nr_added;

실제로 빈 목록에 추가한 페이지 수를 호출자에게 돌려줍니다. guard로 남긴 페이지는 이 값에 들어가지 않습니다.

함께 생각해 볼 질문

size >>= 1은 메모리 내용을 이동하나요?

아닙니다. 블록의 페이지 수를 저장한 정수 size를 절반으로 줄입니다.

page[size]는 몇 바이트 뒤인가요?

struct page 배열에서 size번째 뒤의 페이지 메타데이터입니다. byte 포인터에 size바이트를 더한 표현이 아닙니다.

expand가 반환하는 것은 할당된 페이지 포인터인가요?

아닙니다. 빈 목록에 더한 페이지 수입니다. 남겨 둔 앞쪽 블록을 실제 할당으로 처리하는 일은 호출 경로가 이어서 합니다.

출처와 읽은 범위

Linux stable v6.18.37 · mm/page_alloc.c

해당 버전 원본 파일 · 기존 코드 분석 · 설명 원고

맨 위로 ↑