Memory · Linux v6.6 · source + diagram note

memblock

Linux v6.6의 RAM 등록·예약 구간·초기 할당과 buddy 이관을 구분합니다.

전체 흐름

memblock은 보통의 페이지 할당기가 준비되기 전, 물리 주소 구간을 관리한다. Linux v6.6의 memory 목록은 커널이 인식하는 RAM을, reserved 목록은 예약된 구간을 나타낸다. 예약된 RAM도 memory에 남을 수 있다. 따라서 RAM으로 등록됐다는 사실과 새 할당에 쓸 수 있다는 사실은 다르다.

그림 1. 두 목록과 할당 후보
memory커널이 인식하는 RAM 구간
reserved따로 관리하는 예약 구간
free 후보memory에서 reserved를 제외
할당 조건범위·정렬·node·flags를 추가 확인

두 목록은 같은 배열의 서로 다른 상태가 아니다. 예를 들어 RAM [0x1000, 0x9000)에 [0x3000, 0x5000)을 예약하면 RAM 기록은 그대로이며, 차집합은 [0x1000, 0x3000)과 [0x5000, 0x9000)이다. 숫자는 설명용이다.

소스 코드 위치

아래 코드는 Linux v6.6의 mm/memblock.c 기준이다. memblock_add()와 memblock_reserve()는 대상 목록만 달리하여 memblock_add_range()를 부른다. include/linux/memblock.h에는 자료구조와 반복자, arch/arm64/mm/init.c에는 ARM64 초기화 호출부가 있다.

다른 버전의 주석이나 도식을 그대로 적용하지 않는다. 특히 v6.6의 구간 추가는 배열 여유가 충분하면 카운트 전용 순회를 건너뛸 수 있고, 배열 확장 실패 시 오류를 돌려준다.

대표 코드

memblock_add(): memory 목록에 추가

mm/memblock.c, 727–735행

int __init_memblock memblock_add(phys_addr_t base, phys_addr_t size)
{
	phys_addr_t end = base + size - 1;

	memblock_dbg("%s: [%pa-%pa] %pS\n", __func__,
		     &base, &end, (void *)_RET_IP_);

	return memblock_add_range(&memblock.memory, base, size, MAX_NUMNODES, 0);
}

end는 디버그 출력용 마지막 주소다. 실제 범위 처리는 base와 size를 받은 memblock_add_range()가 맡는다. 반환값은 그대로 호출자에게 전달하므로 성공 로그처럼 보이는 호출 기록만으로 추가 성공을 판정하지 않는다.

memblock_reserve(): reserved 목록에 추가

mm/memblock.c, 871–879행

int __init_memblock memblock_reserve(phys_addr_t base, phys_addr_t size)
{
	phys_addr_t end = base + size - 1;

	memblock_dbg("%s: [%pa-%pa] %pS\n", __func__,
		     &base, &end, (void *)_RET_IP_);

	return memblock_add_range(&memblock.reserved, base, size, MAX_NUMNODES, 0);
}

대상이 &memblock.reserved인 점을 비교한다. 이 함수는 memory 배열에서 해당 RAM을 삭제하지 않는다. 구간을 경계에서 분리하는 memblock_isolate_range()나 제거하는 memblock_remove_range()와 혼동하지 않는다.

함수별 분석

memblock_add_range()는 요청 구간 중 기존 목록에 없는 부분만 추가하고, 조건이 맞는 이웃 구간을 병합한다. 빈 배열에는 첫 구간을 바로 기록한다. 이미 완전히 덮인 요청은 새로운 구간이 없어 성공으로 끝날 수 있다.

그림 2. add_range의 배열 용량에 따른 분기
빈 목록첫 구간과 total_size를 기록
여유가 충분함cnt * 2 + 1 <= max이면 바로 삽입
그 외필요 entry 수 계산 → 필요하면 배열 확장
삽입 후호환되는 이웃 구간 병합

모든 경로가 두 번 순회하는 것은 아니다. 카운트 경로에서 필요한 배열 확장이 실패하면 -ENOMEM을 반환한다. 인접하다는 이유만으로 node와 flags가 다른 구간까지 병합하지 않는다.

추가·예약·제거를 구분하는 예

같은 node와 flags의 목록에 [0x1000, 0x4000)이 있을 때 [0x3000, 0x6000)을 추가하면, 겹침을 중복 계산하지 않고 [0x1000, 0x6000)으로 합칠 수 있다. 이 설명은 한 목록 안의 추가·병합이다. RAM 목록과 예약 목록 사이를 병합한다는 뜻이 아니다.

반면 memblock_remove()는 memory에서, memblock_phys_free()는 reserved에서 구간을 제거한다. memblock_phys_free()가 예약을 해제했다고 그 자리에서 buddy 할당기에 페이지를 넘기는 것은 아니다.

구조체와 필드

그림 3. 구간 배열과 검색 조건
regions / cnt / max배열·현재 구간 수·수용 가능한 구간 수
base / size반열린 구간 [base, base + size)
node / flagsNUMA 및 구간 속성
current_limit / bottom_up접근 가능한 할당 상한 및 검색 방향

memory.total_size와 reserved.total_size는 각 목록의 합계다. 두 목록이 겹칠 수 있으므로 두 값을 더해서 전체 RAM 용량으로 해석하지 않는다.

current_limit은 접근 가능한 범위로 할당하는 경로에서 사용하는 상한이다. 모든 API의 명시적 주소 상한을 무조건 덮어쓰는 값은 아니다. bottom_up이 참이면 낮은 주소 쪽부터, 거짓이면 높은 주소 쪽부터 후보를 찾는다.

구간의 flags와 NUMA 조건도 검색에 영향을 준다. 단순히 두 목록의 차집합이 존재한다는 이유만으로 특정 크기·정렬의 할당이 성공한다고 판단하지 않는다.

v6.18.37 글과 비교할 때 달라진 점

같은 개념을 다루지만 v6.18.37 분석의 초기 계수와 예약 flag를 v6.6 코드에 그대로 대입하면 안 된다. 아래는 두 고정 버전의 mm/memblock.c에서 직접 확인한 차이다.

지점v6.6v6.18.37
빈 목록의 cnt초기 dummy entry를 세어 1; 첫 추가가 그 entry를 채운다.초기 0; 첫 추가 때 1로 바꾼다.
일반 초기 할당의 예약memblock_alloc_range_nid()가 memblock_reserve()를 호출한다.같은 경로에서 MEMBLOCK_RSRV_KERN flag로 예약한다.
겹침 앞쪽에 새 조각을 삽입할 때 flags 경고rbase > base 분기에서 새 flags와 기존 flags가 다르면 경고한다.새 flags가 MEMBLOCK_NONE이면 기존 flags와 달라도 해당 경고를 내지 않는다.

실행 단계와 상태 변화

그림 4. 예약과 페이지 할당기 이관의 경계
RAM 등록펌웨어/아키텍처 정보로 memory 구성
사용 영역 예약kernel·initrd 등 필요한 구간을 reserved에 기록
초기 할당조건에 맞는 free 범위를 찾고 예약
buddy 이관free_low_memory_core_early에서 남은 free page 해제

개념적인 단계다. 모든 아키텍처가 동일한 호출 순서를 갖는다는 뜻은 아니다. 예약되지 않은 범위를 페이지 경계에 맞추고, 해당 경로의 주소 상한도 적용한 뒤 buddy에 넘긴다.

memblock_free_all()은 남은 free 영역의 페이지를 buddy에 넘기는 초기화 경로다. 반대로 memblock_phys_free()는 reserved 기록을 제거하는 API다. 이름에 free가 있어도 같은 작업이 아니다.

CONFIG_ARCH_KEEP_MEMBLOCK을 쓰지 않는 구성에서는 초기화 뒤 memblock 자료구조를 폐기할 수 있다. 부팅 후의 일반 할당·해제를 이 초기 구간 목록만으로 설명하지 않는다.

불변 조건과 실패 사례

  • 한 목록의 겹치는 주소를 중복 합산하지 않는다. 병합은 주소 연속성뿐 아니라 node와 flags 호환 조건도 확인한다.
  • memblock_reserve()는 RAM 등록을 제거하는 연산이 아니다. 예약 제외 효과는 free-range 검색에서 확인한다.
  • region 배열 확장에는 별도의 저장 공간이 필요하다. memblock_allow_resize() 사용 시 아직 예약하지 않은 initrd 등과 겹치지 않도록 초기화 순서를 검토한다.
  • 추가 함수의 0 반환은 성공을 뜻하지만, 새 entry가 생겼다는 보장은 아니다. -ENOMEM 같은 실패는 호출자가 처리해야 한다.
  • memblock_phys_alloc*()의 물리 주소와 memblock_alloc*()의 가상 주소 반환을 혼용하지 않는다.

계측과 검증

memblock=debug 부팅 인자로 구간 변경과 할당 관련 로그를 살핀다. 출력은 호출 시점의 인자일 수 있으므로 반환값과 최종 목록도 대조한다. 이 글은 실제 보드를 부팅하여 검증한 결과가 아니라 고정 v6.6 소스에 근거한 설명이다.

earlycon은 해당 보드의 UART와 설정이 맞아야 초기 로그를 전달할 수 있다. 초기 RAM·예약 목록, 요청 크기와 정렬, 반환 주소 또는 오류를 함께 기록한다. 부팅이 끝난 뒤 켠 function tracer나 page_owner로 이미 끝난 초기 memblock 호출을 소급 관찰할 수 있다고 가정하지 않는다.

아키텍처별 호출 순서, CONFIG_ARCH_KEEP_MEMBLOCK, NUMA 및 메모리 맵 조건을 먼저 기록한다. 일반 tracefs 모듈 필터 명령은 이 초기 부팅 구간의 재현 절차를 대신하지 못한다.

추가 확인 사항

  • 예약 전후에 memory 목록이 그대로인데 free 범위만 달라지는 것을 설명할 수 있는가?
  • 추가 요청이 기존 구간에 완전히 포함될 때 cnt와 total_size는 어떻게 되는가?
  • 현재 배열 용량은 직접 삽입 조건을 만족하는가, 아니면 카운트·확장 경로가 필요한가?
  • 현재 보드의 mem_init 경로에서 어떤 free 범위가 buddy에 전달되고 어떤 예약이 남는가?