메모리 관리

C/C++와 같은 프로그래밍 언어와 달리, MicroPython은 자동 메모리 관리를 지원하여 메모리 관리 세부 사항을 개발자로부터 숨깁니다. 자동 메모리 관리는 메모리의 할당과 해제를 자동으로 관리하기 위해 운영 체제나 애플리케이션에서 사용하는 기법입니다. 이를 통해 객체에 할당된 메모리를 해제하는 것을 잊어버리는 등의 문제가 사라집니다. 또한 자동 메모리 관리는 이미 해제된 메모리를 사용하는 치명적인 문제도 방지합니다. 자동 메모리 관리에는 여러 형태가 있으며, 그중 하나가 가비지 컬렉션(garbage collection, GC)입니다.

가비지 컬렉터는 일반적으로 두 가지 역할을 합니다.

  1. 사용 가능한 메모리에 새 객체를 할당합니다.

  2. 사용하지 않는 메모리를 해제합니다.

GC 알고리즘에는 여러 가지가 있지만, MicroPython은 메모리 관리를 위해 Mark and Sweep 정책을 사용합니다. 이 알고리즘은 힙을 순회하며 살아 있는 모든 객체를 표시하는 마크(mark) 단계와, 힙을 따라가며 표시되지 않은 모든 객체를 회수하는 스윕(sweep) 단계로 구성됩니다.

MicroPython의 가비지 컬렉션 기능은 gc 내장 모듈을 통해 사용할 수 있습니다:

>>> x = 5
>>> x
5
>>> import gc
>>> gc.enable()
>>> gc.mem_alloc()
1312
>>> gc.mem_free()
2071392
>>> gc.collect()
19
>>> gc.disable()
>>>

gc.disable()를 호출한 경우에도 gc.collect()로 컬렉션을 수동으로 실행할 수 있습니다.

C 코드에서의 MicroPython 메모리

“Python 힙”에서 메모리를 할당하는 C 코드(즉, m_malloc(), m_malloc0(), m_free() 등의 함수)를 작성할 때는 가비지 컬렉터를 염두에 두어야 합니다.

가비지 컬렉터의 마크 단계는 다음 루트(root)에서 시작하여 힙 메모리를 가리키는 살아 있는 포인터를 탐색합니다:

  • 메인 Python 런타임(또는 REPL)의 스택.

  • 네이티브 운영 체제 스레드나 태스크 위에 Python 스레드를 구현하는 포트의 경우, 각 “Python 스레드”의 스택.

  • 매크로 MP_REGISTER_ROOT_POINTER를 사용하여 C 코드에서 정의한 “루트 포인터”. 이것이 Python 힙에 대한 정적 범위 포인터를 두는 권장 방법입니다.

  • m_tracked_calloc(), m_tracked_realloc, m_tracked_free() 함수로 만든 추적 할당. 이 특수 함수들은 가비지 컬렉터가 항상 살아 있다고 간주하는 메모리 블록을 할당할 수 있게 해줍니다. C의 메모리 할당과 유사하게, 이 메모리는 m_tracked_free() 호출이나 소프트 리셋으로만 해제됩니다. 각 추적 할당에는 약간의 메모리 사용량과 런타임 오버헤드가 따릅니다. 이 기능은 모든 포트에서 기본적으로 활성화되어 있지는 않습니다.

그런 다음 가비지 컬렉터는 루트 포인터가 가리키는 모든 메모리를 재귀적으로 스캔하고 표시하며, 모든 주소를 다 처리할 때까지 이 과정을 반복합니다. 이것만으로도 MicroPython 런타임이 여전히 사용 중인 모든 Python 객체를 찾기에 충분합니다.

그러나 다음 메모리는 가비지 컬렉터가 스캔하지 않으며, 따라서 조기에 해제될 수 있습니다:

  • 힙 메모리에 대한 포인터를 담고 있는 정적(static) 또는 전역(global) C 변수.

  • 할당된 버퍼의 “머리(head)”(즉, m_malloc()이 반환한 정확한 주소)를 가리키지 않고, 대신 할당된 버퍼 내부의 주소를 가리키는 포인터(예: 중첩된 구조체에 대한 포인터). 성능상의 이유로, 가비지 컬렉터는 이러한 경우 이를 둘러싼 버퍼를 표시하지 않습니다.

  • Python 코드를 실행하지 않거나 “Python 스레드”로 수동 등록되지 않은 스레드나 RTOS 태스크의 스택(네이티브 스레드나 태스크를 지원하는 포트의 경우).

이러한 상황에서 use-after-free를 방지하는 방법:

  • 추적 할당 API m_tracked_calloc(), m_tracked_realloc(), m_tracked_free()를 사용합니다.

  • 정적 변수에 포인터를 저장하는 대신 루트 포인터를 등록합니다(위 참조).

  • 코드를 재구성합니다. 예를 들어, 관련 포인터들을 정적 변수에 두는 대신, Python 코드가 그 포인터들을 모두 보유하는 싱글톤 Python 객체(C로 구현됨)를 초기화하는 API를 두는 방식입니다.

참고

소프트 리셋은 항상 Python 힙을 비우고 모든 메모리를 해제합니다. 소프트 리셋 후에는 힙에 대한 어떤 포인터도 보유하지 않는 것이 중요한데, 그러한 포인터는 해제된 메모리를 가리키는 댕글링 포인터(dangling pointer)가 되기 때문입니다.

일부 포트는 “C 힙”도 지원하며(C 동적 메모리 할당 참조), 이 경우 표준 C 함수 malloc 등을 호출하여 소프트 리셋 후에도 유효하게 유지되는 메모리를 할당할 수 있습니다.

객체 모델

모든 MicroPython 객체는 mp_obj_t 데이터 타입으로 참조됩니다. 이는 보통 워드 크기(즉, 대상 아키텍처에서의 포인터와 같은 크기)이며, 일반적으로 32비트(STM32, RP2, nRF, Unix x86) 또는 64비트(Unix x64)일 수 있습니다. 특정 객체 표현의 경우 워드 크기보다 클 수도 있는데, 예를 들어 OBJ_REPR_D는 32비트 아키텍처에서 64비트 크기의 mp_obj_t를 가집니다.

mp_obj_t는 정수, 부동소수점, 타입, dict 또는 클래스 인스턴스와 같은 MicroPython 객체를 나타냅니다. 불리언과 작은 정수 같은 일부 객체는 그 값이 mp_obj_t 값 안에 직접 저장되어 추가 메모리를 필요로 하지 않습니다. 그 외의 객체는 값이 메모리의 다른 곳(예: 가비지 컬렉션 대상 힙)에 저장되며, 해당 mp_obj_t는 그 메모리를 가리키는 포인터를 담습니다. mp_obj_t의 일부는 어떤 타입의 객체인지를 알려주는 태그입니다.

사용 가능한 표현에 대한 구체적인 세부 사항은 py/mpconfig.h를 참조하세요.

포인터 태깅(Pointer tagging)

포인터는 워드 정렬되어 있으므로, mp_obj_t에 저장될 때 이 객체 핸들의 하위 비트는 0이 됩니다. 예를 들어 32비트 아키텍처에서는 하위 2비트가 0이 됩니다:

********|********|********|******00

이 비트들은 태그를 저장하기 위한 용도로 예약되어 있습니다. 태그는 해당 정보를 객체에 저장하기 위한 새 필드를 도입하는 대신 추가 정보를 저장하는데, 새 필드 도입은 비효율적일 수 있습니다. MicroPython에서 태그는 우리가 작은 정수, 인터닝된(작은) 문자열, 또는 구체 객체(concrete object) 중 무엇을 다루고 있는지를 알려주며, 각각에 서로 다른 의미 체계가 적용됩니다.

작은 정수의 경우 매핑은 다음과 같습니다:

********|********|********|*******1

여기서 별표(*)는 실제 정수 값을 담습니다. 인터닝된 문자열이나 즉치 객체(immediate object)(예: True)의 경우 mp_obj_t 값의 레이아웃은 각각 다음과 같습니다:

********|********|********|*****010

********|********|********|*****110

반면 위의 어느 것도 아닌 구체 객체는 다음과 같은 형태를 취합니다:

********|********|********|******00

여기서 별표는 메모리 내 구체 객체의 주소에 해당합니다.

객체의 할당

작은 정수의 값은 mp_obj_t 안에 직접 저장되며 힙이나 다른 곳이 아닌 제자리(in-place)에 할당됩니다. 따라서 작은 정수의 생성은 힙에 영향을 주지 않습니다. 이미 텍스트 데이터가 다른 곳에 저장되어 있는 인터닝된 문자열과, None, False, True 같은 즉치 값도 마찬가지입니다.

구체 객체에 해당하는 그 외의 모든 것은 힙에 할당되며, 그 객체 구조는 객체의 타입을 저장하기 위한 필드가 객체 헤더에 예약되도록 되어 있습니다.

+++++++++++
+         +
+ type    + object header
+         +
+++++++++++
+         + object items
+         +
+         +
+++++++++++

힙의 가장 작은 할당 단위는 블록이며, 그 크기는 머신 워드 4개입니다(32비트 머신에서는 16바이트, 64비트 머신에서는 32바이트). 힙에 할당되는 또 다른 구조는 각 블록에서의 객체 할당을 추적합니다. 이 구조를 비트맵(bitmap)이라고 합니다.

../_images/bitmap.png

비트맵은 블록이 “비어 있는지(free)” 또는 “사용 중(in use)”인지를 추적하며, 각 블록의 이 상태를 추적하는 데 2비트를 사용합니다.

마크-스윕 가비지 컬렉터는 힙에 할당된 객체를 관리하며, 비트맵을 활용하여 여전히 사용 중인 객체를 표시하기도 합니다. 이러한 세부 사항의 전체 구현은 py/gc.c 를 참조하세요.

할당: 힙 레이아웃

힙은 풀(pool) 단위의 블록들로 구성되도록 배열됩니다. 블록은 서로 다른 속성을 가질 수 있습니다:

  • ATB(allocation table byte, 할당 테이블 바이트): 설정되어 있으면 해당 블록은 일반 블록입니다

  • FREE: 비어 있는 블록

  • HEAD: 블록 체인의 머리

  • TAIL: 블록 체인의 꼬리에 위치

  • MARK : 표시된 머리 블록

  • FTB(finaliser table byte, 종료자 테이블 바이트): 설정되어 있으면 해당 블록에 종료자(finaliser)가 있습니다