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

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

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

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

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

먼저 알아둘 개념

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로 지정된 절반이 일반 free list에 들어가지 않는 이유와 set_page_guard의 migratetype 인자를 살펴보세요. v6.6 expand는 void 함수이므로 상태 변경을 추적해야 하며 최신 버전의 반환값 설명을 적용하지 않습니다.

그림으로 보는 변화

같은 주소 축에서 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.6 원문 기준입니다. 주석·빈 줄을 포함한 함수 전체를 먼저 보고, 그 아래에서 각 줄을 설명합니다.

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

	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, migratetype))
			continue;

		add_to_free_list(&page[size], zone, high, migratetype);
		set_buddy_order(&page[size], high);
	}
}
static inline void expand(struct zone *zone, struct page *page,

zone의 큰 빈 블록 page를 나누는 내부 함수입니다. v6.6의 반환형은 void이므로 추가한 페이지 수를 반환하지 않습니다.

	int low, int high, int migratetype)

low는 필요한 order, high는 현재 블록의 order이며 migratetype은 빈 목록의 종류를 지정합니다. order의 단위는 바이트가 아닙니다.

	unsigned long size = 1 << high;

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

	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, migratetype))

뒤쪽 절반을 디버깅용 guard 영역으로 지정할지 판단합니다. 이 버전은 migratetype도 넘겨 guard 페이지 관련 처리를 진행합니다.

			continue;

guard로 지정된 절반은 아래의 일반 빈 목록 추가를 건너뜁니다. v6.6에는 nr_added 카운터가 없습니다.

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

뒤쪽 절반을 해당 order와 migratetype의 빈 목록에 추가합니다. 앞쪽 절반은 같은 page 시작점을 유지하며 계속 나눌 수 있습니다.

		set_buddy_order(&page[size], high);

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

함께 생각해 볼 질문

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

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

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

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

expand가 할당된 페이지 포인터를 반환하나요?

v6.6에서는 반환형이 void입니다. 기존 page의 앞쪽 블록을 남기고 뒤의 절반들을 빈 목록에 추가하며, 상위 경로가 그 앞쪽 블록의 할당을 이어서 처리합니다.

출처와 읽은 범위

Linux stable v6.6 · mm/page_alloc.c

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

맨 위로 ↑