OSX / 메모리 관리 1 : 가상메모리

메모리 성능 관리 지침

도입

메모리는 모든 프로그램이 사용하는 중요한 시스템 자원이다. 프로그램은 실행되기 전에 반드시 메모리로 로드되어야 하고, 실행되는 동안에는 명시적으로나 묵시적으로 추가적인 메모리를 할당받아 프로그램에서 사용하는 정보를 담고, 조작할 수 있게 한다. 프로그램의 코드와 데이터를 위한 메모리상의 공간을 만드는 작업은 시간과 비용이 드는 일이며, 따라서 이러한 작업은 시스템의 전체 성능에 영향을 끼친다. 메모리를 많이 사용하는 일을 피하기는 어렵지만, 메모리 사용으로 시스템에 다른 부분에 영향을 끼치는 일은 최소화하는 몇 가지 방법은 있다.

가상메모리 시스템

효율적인 메모리관리는 iOS 및 OSX에서 좋은 성능을 내는 코드를 작성하는데 매우 중요한 부분 중 하나이다. 메모리를 적게 쓰면 앱의 메모리 점유율이 낮아지고 그만큼 CPU점유 시간의 총량도 적어진다. 적절하게 앱을 튜닝하기 위해서는 그 아래에 있는 시스템이 어떻게 메모리를 관리하는지를 알아둘 필요가 있다.

OSX와 iOS는 운영체제에 완전히 통합되어 (임의로 중지시킬 수 없는) 가상 메모리 체계를 갖추고 있다. 이는 항시 작동하는 것으로 두 플랫품은 32비트 프로세스마다 4기가 바이트의 메모리 주소를 할당할 수 있다. 또한 OSX는 64비트 프로세스에 대해 18엑사바이트(18000기가)의 메모리 주소를 할당할 수 있다. 4기가 혹은 그 이상의 램을 장착한 컴퓨터에서는 시스템은 하나의 프로세스에 이 만큼만의 메모리를 할당할 수 있다.

프로세스에게 4기가 혹은 18엑사바이트의 메모리 주소 공간을 제공하기 위해 OSX는 현재 사용되지 않는 데이터를 하드디스크에 저장한다. 메모리가 가득차면 지금 쓰지 않는 데이터들은 지금 쓸 데이터를 위해서 하드디스크로 옮겨진다. 이렇게 메모리의 부족분을 위해 사용되도록 할당된 하드디스크 영역은 "backing memory"라고 부른다.

OSX는 이렇게 "backing memroy"를 지원하지만, iOS는 그렇지 못하다. iOS에서 변경되지 않는 데이터가 플래시메모리에 기록되어 있다면, 이 데이터는 메모리에서 제거되고, 필요할 때 다시 읽어들이게 된다. 반대로 변경될 수 있는 데이터는 OS에 의해 절대 임의로 제거되지 않는다. 대신 가용램의 용량이 특정 수치 이하로 떨어지면 시스템은 실행중인 앱에게 경고를 보내고 새로운 데이터를 위한 공간을 확보하기 위해 쓰지 않는 메모리를 해제할 것은 요구한다. 앱이 충분한 크기의 메모리를 비워내지 못하면 해당 앱은 종료된다.

다른 대부분의 UNIX계열 시스템과는 달리, OSX는 하드디스크의 스왑공간을 다로 파티션으로 할당하지 않는다. 대신, 부트 파티션의 사용가능한 전체 공간을 사용한다. 따라서 하드디스크의 여유 공간이 매우 부족해지면 시스템의 전체 성능이 급격히 나빠질 수 있다.

가상메모리

가상메모리는 물리적인 메모리 용량의 제한으로부터 OS가 벗어날 수 있도록 도와준다. 가상에모리 관리자는 각각의 프로세스에 대해 논리적인 가상메모리 주소를 생성하고, 이를 일정한 크기의 메모리 조각-'page'라 부른다-으로 나눈다. 프로세서와 프로세서의 메모리 관리유닛(MMU)은 프로그램의 논리적 메모리 주소 공간을 실제 램의 주소에 대응시기 위해 페이지 테이블을 유지관리한다. 프로그램의 코드가 메모리의 특정 주소에 액세스할 때 이 MMU는 논리적 주소값을 램의 실제 하드웨어 주소로 변환한다. 이 변환은 자동으로 일어나며, 실행 중인 앱은 이에 대해서는 알아차리지 못한다.

어떤 프로그램에 관해 그 논리적 주소 공간내의 임의의 주소는 항상 접근이 가능하다. 하지만 앱이 물리적인 램에 존재하지 않는 페이지의 주소를 참조하고자 하면 'page fault'가 발생한다. 이렇게 되면, 가상메모리 관리자는 특별한 페이지폴트 핸들러를 호출해서 해당 오류에 즉각 대응하게 된다. 그러면 이 핸들러는 실행중인 코드를 잠시 멈추고, 필요한 페이지내 정보를 하드디스크로부터 읽어와 램에 적재한 후 페이지 테이블의 내용을 갱신하고 다시 코드를 실행시킨다. 이러한 작업을 페이징이라고 한다.

만약 물리적 메모리에 사용가능한 페이지가 없다면, 핸들러는 우선 기존의 페이지를 해제해서 새로운 페이지를 위한 공간을 만들어야 한다. 시스템이 페이지를 해제하는 방법은 플랫폼마다 다른데, OSX에서는 가상 메모리 시스템은 지원저장소에 페이지의 내용을 기록한다. 지원저장소(backing store)는 메모리 페이지의 사본을 보관하는 하드디스크 기반의 저장소이다. 램의 데이터를 지원저장소로 옮기는 일을 페이징아웃이라고 한다.(스와핑아웃이라고 하기도 한다.) 그리고 지원저장소로부터 다시 램으로 데이터를 옮기는 일을 페이징인이라고 한다. iOS에서는 지원저장소가 없으며 페이지는 디스크로 기록되지 않는다. 대신에 "읽기전용" 페이지는 수시로 필요할 때마다 디스크로부터 읽어들여진다.

iOS와 OSX의 페이지 크기는 4Kbytes이다. 페이지폴트가 발생할 때마다 시스템은 디스크로부터 4킬로바이트씩 읽어들이게 된다. 시스템이 너무 많은 양의 페이지 폴트를 만나서 실제 코드를 처리하는 것보다 페이지를 읽고 쓰는데 많은 시간을 할애하게되면 디스크 스레싱이 일어나게 된다.

어떤 종류의 페이지를 막론하고 스레싱이 생기게되면 시스템은 대부분의 시간을 디스크 액세스에 할애하게 되므로 시스템 전체 성능이 매우 나빠진다. 디스크로부터 페이지를 읽어들이는 데는 아주 많은 시간이 소비되고, 이는 램에서 직접 읽는것보다는 훨씬훨씬훨씬훨씬 느리다. 만약 시스템이 다른 페이지를 읽기 전에 기존에 열린 페이지를 디스크에 써야 한다면 상황은 더욱 나빠진다.

가상 메모리 시스템을 좀 더 자세히

프로세스의 논리 주소 공간은 메모리에 대해 맵핑된 영역으로 구성된다. 각각의 맵핑된 메모리 영역에 포함된 가상메모리 페이지의 개수는 시스템이 알고 있다. 각 영역은 상속이나 쓰기 보호, 연결됨 등에 대산 고유한 속성을 가지고 있다. wired 속성은 페이지 아웃될 수 없다는 속성이다. 각각의 영역의 페이지 수는 알려져 있기 때문에, 영역들은 페이지-정렬로 된다. 즉, 영역의 시작 번지는 그 영역의 첫 페이지의 시작번지가 되고, 끝 부분 역시 페이지의 주소를 통해 알게된다.

커널은 각각의 논리 주소 공간의 영역에 대해 VM객체를 연결한다. 커널은 이 VM객체를 통해 상주/비상주 페이지를 추적하고 관리하게 된다. 임의의 메모리 영역은 지원저장소나 메모리 맵핑 파일에 맵핑될 수 있다. 각각의 VM객체는 메모리 영역에 대해 기본페이저나 vnode 페이저와 연결되는 맵핑 정보를 가지고 있다. 디폴트 페이저는 비상주 가상 메모리 페이지를 관리하는 시스템 매니저로, 지원 저장소에 쓰여있는 페이지를 관리하고, 필요시에 이들 페이지를 읽어들인다. vnode 페이저는 메모리 맵 파일의 액세스를 구현한다. 이 매커니즘은 기과거에 메모리에 로드된 적이 있는 데이터를 읽고 쓸 수 있게 해준다.

디폴트 페이저나 vnode 페이저에 대한 맵핑 정보 외에도, VM객체는 메모리 영역(여기서는 임의의 영역이라기보다는 페이지 묶음)을 다른 VM객체에도 맵핑한다. 커널은 이러한 자기 참조 테크닉을 copy-on-write을 구현하는데 사용한다. 여역을 쓰기시에 복사하는 것은 여러 프로세스 혹은 하나의 프로세스 내의 여러 코드에서 동일한 페이지를 참조하는 것을 가능하게 한다. 어떤 프로세스가 페이지에 대해 쓰기를 시도하면 프로세스가 쓰고 있는 영역의 논리적 주소에 해당 페이지의 사본이 만들어진다. 그리고 "쓰기를 수행한 프로세스"는 해당 페이지의 사본을 갖게되고 이를 유지해나간다. copy-on-write는 방대한 메모리 영역을 공유하고 직접적으로 안전하게 쓸 수 있도록 해준다. 이러한 타입의 메모리 영역이 가장 일반적으로 사용된다. 다음은 VM객체에 담긴 여러 필드들이다.

  • 상주페이지 – 현재 램에 올라가 있는 모든 페이지의 목록
  • 크기 – 영역의 크기 (바이트 수 = 전체 페이지의 수 / 4)
  • 페이저 – 이 영역의 페이지들은 지원저장소로 옮기고 관리하는 페이저
  • 섀도우 – copy-on-write 최적화에 사용됨
  • 사본 – 상동
  • 속성들 – 여러 구현 상세 규격에 대한 상태 플래그 값

만약 VM 객체가 copy-on-write와 연관되어 있다면 shadow, copy는 다른 VM을 가리키는 포인터가 된다. 그렇지 않은 경우 이 값들은 NULL이다.

와이어드 메모리

와이어드메모리는 상주메모리라고도 하며, 커널코드와 디스크로 페이징아웃되지 않는 데이터 구조를 저장하고 있다. 앱이나 프레임워크 혹은 다른 사용자 레벨 소프트웨어는 이 영역을 할당할 수 없다. 그러나 이들은 와어어드 메모리의 크기가 얼마가 될 것인가에 대해서는 영향을 줄 수 있다. 예를 들어 스레드나 포트를 생성하는 앱은 암시적으로 커널 리소스를 필요로하기 때문에 와어어드 메모리 영역에 할당이 발생하게 된다.

다음은 앱이 생성하는 와이어드 메모리에 대한 비용이다.

  • 프로세스 – 프로세스 당 16킬로바이트
  • 스레드 – 5 or 21 킬로바이트
  • Mach Port – 116바이트
  • 맵핑 – 32바이트
  • 라이브러리 – 2킬로바이트를 할당하며, 사용시마다 200바이트 추가 할당
  • 메모리 영역(Mem Region) – 160바이트

모든 프로세스, 스레드, 라이브러리는 시스템의 상주 메모리 크기에 영향을 준다. 앱이 쓰고자하는 것 외에도 커널은 다음에 대해 와이어드 메모리 영역을 소비한다.

  • VM객체
  • 가상메모리 버퍼 캐시
  • 입출력 버퍼 캐시
  • 드라이버

와이어드 데이터 구조는 실제 물리 메모리와 맵테이블(가상메모리를 디스크에 맵핑하기 위한)에 연관되어 있었으며, 이러한 엔트리들은 가뇽 램크기에 대해 함께 커진다. 결과적으로 시스템에 메모리를 증설하면, 다른 어떤 것이 변하지 않더라고 와이어드 영역은 함께 커진다. 시스템이 부팅되어 파인더가 로드되고, 다른 앱이 실행되지 않는다고 가정하며, 와이어드 메모리는 약 62메가 램 컴퓨터에서는 14메가 정도를, 128메가 램에서는 17메가 정도를 소모한다.

이 영역의 메모리는 사용하지 않더라도 즉시 해제되지 않으며, "가비지 컬렉트"에 의해 해제된다.

커널의 페이지 리스트

커널은 물리적 메모리 페이지에 대한 3종류의 시스템 범위에서의 리스트를 가지고 있다.

  • active list는 최근에 활성화되었으며, 메모리에 맵핑되어 있는 페이지들의 목록이다.
  • inactive list는 현재 메모리에 맵핑되어 있으나, 최근에 액세스 되지 않은 페이지들의 목록이다. 이 페이지들은 유효한 데이터를 가지고 있지만, 언제든지 제거될 수 있다.
  • free list는 VM객체에 연결되지 않은 메모리 주소의 물리 메모리 상 페이지를 포함한다. 이 페이지들은 필요시 언제든지 사용될 수 있다.

free list의 페이지의 수가 적정수준(이는 램의 물리적 크기에 관련된다) 이하로 떨어지면 페이저는 요청의 균형을 맞추려고 시도한다. 페이저는 비활성 리스트에서 페이징아웃을 한다. 이 리스트에서 최근에 액세스된 페이지는 활성리스트로 옮겨져 다시 활성화된다. OSX에서는 만약 비활성 리스트에 있는 페이지의 컨텐츠가 한 번도 페이징 아웃되어 디스크에 기록된 적 없다면, 디스크에 기록하고 이 페이지를 free list로 옮긴다. (iOS에서는 비활성 리스트에 그대로 남게 되지만, 이를 소유한 애플리케이션에서 이 영역을 직접 해제하도록 한다.) 만약 비활성 페이지 하나가 한 번도 변경된적 없고, 영구상주하는 것도 아니라면(wired)이는 해제되어 free list에 추가된다. free list의 크기가 적정 수치 이상이 되면 페이저의 작업은 종료된다.

또한 커널은 페이지가 액세스되지 않으면 이들을 비활성 목록으로 옮긴다. 반대로 '소프트폴트'-뒤에서 설명하겠다-가 일어나면 비활성 페이지를 활성페이지로 옮긴다. 만약 가상 페이지가 비워지면 연관된 물리적 페이지들은 모두 free list로 이동된다. 또한 프로세스가 명시적으로 메모리를 해제하면 커널은 여기에 영향을 받는 메모리 페이지를 free list로 이동시킨다.

페이징-아웃 프로세스

OSX에서, 여유목록의 페이지 개수가 적정 수치 이하로 떨어지면, 커널은 비활성 페이지들을 메모리에서 빼내어 여유 리스트의 크기를 확보하려 든다. 이를 위해 커널은 모든 활성, 비활성 페이지에 대해 반복적으로 다음과 같은 단계를 밟는다.

  1. 활성리스트의 페이지가 최근에 건드려지지 않았다면, 비활성 리스트로 옮긴다.
  2. 비활성 리스트이 페이지가 최근에 건드려지지 않았다면, 커널은 이 페이지의 VM객체를 찾는다.
  3. VM객체가 한번도 페이지되지 않았다면, 커널은 디폴트 페이저 객체를 생성하고 할당하는 초기화 루틴을 호출한다.
  4. VM객체의 디폴트 페이저는 페이지의 내용을 지원저장소에 쓰려고 시도한다.
  5. 페이저가 작업을 완료하며, 커널은 페이지가 점유하던 메모리 공간을 해제하고, 이 페이지를 비활성 리스트에서 여유공간 리스트로 옮긴다.

iOS에서는 커널은 페이지를 디스크에 기록하지 않는다. 여유 메모리의 총량이 부족해지면, 커널은 비활성이면서 수정된적이 없는 페이지들을 비워버리고, 페이지를 점유한 앱에게 메모리를 해제할 것을 요구한다.

페이징-인 프로세스

가상 메모리 관리의 마지막 단계는 페이지를 다시 램으로 올리는 일이다. 메모리 액세스 실패는 페이징인 절차를 시작하게 한다. 메모리 액세스 폴트는 프로그램의 코드가 현재 램에 맵핑되어 있지 않은 논리적 가상 메모리 주소를 액세스하려고 할 때 발생한다. 이러한 폴트에는 두 종류가 있다.

  • Soft Fault는 참조된 주소의 페이지가 램에 상주하고 있으나, 프로세스의 주소 공간에 맵핑되어 있지 않을 때 발생한다.
  • Hard Fault는 참조된 주소의 페이지가 물리적인 램에 없고, 스왑아웃되었을 때 발생한다. 이는 흔히 '페이지 폴트'라고도 한다.

종류를 막론하고 일단 폴트가 일어나면 커널은 맵엔트리와 해당 영역을 액세스하는 VM객체를 준비한다. 그런다음 상주중인 페이지의 VM객체를 훑으면서 찾으려는 페이지가 발견되면 커널은 소프트 폴트를 발생시킨다. 페이지가 상주 페이지 중에 없다면 하드 폴트가 유발된다.

소프트폴트가 일어나면 커널은 페이지가 포함된 램 주소를 가상 메모리 공간에 맵핑해고 이 영역을 활성으로 표시해둔다. 만약 폴트가 쓰기 동작과 관련되어 있다면, 페이지는 변경되었다는 플래그를 받게되고, 이는 나중에 해제하기 전에 디스크에 기록되도록 한다.

하드폴트의 경우, VM객체의 페이저가 지원저장소에서 페이지를 찾고, 맵 정보를 수정한 다음, 페이지를 다시 램으로 읽어들인다. 그런 다음 활성상태로 표시한다. 소프트폴트와 마찬가지로 하드폴트가 쓰기 동작에 관련돼 있다면 이 페이지는 '수정됨'으로 마킹된다.