본문 바로가기
C/[서적] 뇌를 자극하는 윈도우즈 시프

Chapter 20. 메모리 관리(Virtual Memory, Heap, MMF)

by GameStudy 2022. 2. 14.

20.1 가상 메모리 컨트롤(Virtual Memory Control)

Prologue) 코드 상으로 구현하는 능력보다는, 세 가지(Virtual Memory, Heap, MMF)가

  갖는 의미, 왜 하는가, 장점이 뭔가를 이해 해야함. 그래서 적재적소에 쓸 수 있어야 함.

  16장의 내용이 기본적으로 필요함.

 

20.1-1 가상 메모리의 Commit, Free와 물리 메모리의 관계

  Note) CPU나 프로그래머나 메모리를 바라보는 관점은 4G가 모두 있다고 믿음.

    즉, "가상 메모리"를 바라보고 있는것. 실제 메모리는 이보다 적게 별도로 존재함.

    이 가상 메모리를 컨트롤 한다는게 어떤 의미를 지니는지 살펴보자

 

  Note) 가상 메모리의 세 가지 페이지 상태

    - 가상 메모리는 페이지 단위로 나뉘어짐.

      페이지의 갯수는 어떻게 구할까? 가상 메모리의 크기 / 페이지의 크기 = 페이지 갯수

    - 이 페이지를 관리하는건 결국 Windows.

      윈도우즈는 가상 메모리의 페이지를 관리할 때 특성 정보를 부여함.

      1. Commit 상태 3. Reserve 상태(Windows만의 특징) 2. Free 상태 

    - 아래 그림은 Commit 상태와 Free 상태의 차이를 알려주는 그림.

      일단 내가 할당받지 않은 가상 메모리 페이지는 Free 상태임.

      즉, 물리 메모리와의 연결이 이뤄지지 않은 가상 메모리 페이지. 비어있는 메모리.

    - 반대로 물리 메모리와의 연결이 이뤄진 가상 메모리 페이지는 Commit 상태

      즉 Commit 상태의 페이지는 메모리 할당이 이뤄진 것.

      ex. 대표적인 예로, malloc() 함수

        malloc() 함수로 Free 상태의 가상 메모리 페이지를 물리 메모리에 매핑 시킬 수 있음.

        free() 함수는 물리 메모리와의 매핑이 끊어지면서 해당 가상 메모리 페이지는 Free 상태가 됨.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 근데 COMMIT과 FREE 상태만 두고서 가상 메모리를 관리 했더니, 영 비효율적임.

 

20.1-2 RESERVE 상태의 필요성

  Note) 프로그램을 작성하는데, 배열의 크기를 1로 잡을지 1만으로 잡을지 고민 중이라 해보자.

    이 프로그램은 한달내내 동작하는데, 일반적인 때는 메모리 공간이 별로 필요 없음.

    아주 특별한 경우에 엄청 크게 필요함. 이 한번의 경우때문에 1만의 배열을 잡긴 너무 비효율적.

    이를 위해서 가상 메모리 페이지 상태에는 RESERVE가 있음.    

 

  Note) 아래 그림과 같이 5개의 가상 메모리 페이지를 COMMIT 했다고 해보자.

    근데 일반적인 때엔 2개 정도라면? 3개는 향후에 특수한 경우를 위한 거라고 해보자.

    그럼 3개는 현재 안쓰고 있지만, 물리 메모리와 연결이 되어 있기 때문에 남들은 할당 못받음.

    물리 메모리 연결까지 되어 있어서, 더 문제이기도 함.

    혹자는 일단 2개만 쓰고, 나중에 3개 더 COMMIT 하면 되지않냐고 반문할 수 있음.

    그러나, 배열 같은 정적인 자료구조라면 처음부터 연속적인 메모리라면 불가능함.

    처음에 2개짜리 배열 만들고 나중에 3개 짜리 배열을 만든다면,

    두 배열은 연속적인 하나의 배열이 아님. 즉, 둘 사이의 가상 메모리에 순차적 접근이 불가능함.

    정리하자면, 한번에 크게 할당받으면 낭비가 심하고, 근데 한 번에 연결된 메모리 공간이 필요하다.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 위와 같은 문제를 해결하기 위해서 RESERVE 상태가 나옴.

    즉, 지금 쓰고 있진 않지만 예약을 해놓겠다는 의미

    아래 그림처럼, 총 10개의 가상 메모리 페이지가 free였다가

    총 5개 페이지가 RESERVE 상태로 변함. 

    다른 누군가가 메모리 할당을 하려할때 이 공간은 할당을 허용하지 않음.

    다만 물리 메모리와의 매핑은 안되어있는 공간. 나중에 쓸때 비로소 매핑하면 되는 거.

    이를 통해서 메모리의 순차적 접근 + 메모리 낭비도 막을 수 있음.

    결국 두 개의 가상 메모리 페이지만 COMMIT 상태가 됨.    

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

20.1-3 메모리 할당의 시작점과 단위 확인

  Note) 가상 메모리 상에서 할당을 시작 해야 함.

    초기에는 일단 모든 가상 메모리 페이지가 FREE 상태일 거임.

    우리가 COMMIT / RESERVE 상태로 바꿀 수 있음.

    그런데 아무 주소나 시작으로 페이지 상태를 바꿀 수 있는건 아님.

 

  Note) 메모리 할당의 시작점

    - Allocation Granularity Boundary 기준.

      메모리 할당의 시작점이라고 생각하자.

    - 페이지 크기의 몇 배수: 지나친 단편화를 막기 위해.

      ex. 홀수 번지마다 할당 가능하다 -> A B A B 이런식이 불가능함.

        -> A A B B 는 가능함. 즉 단편화가 해결됨. 메모리 관리측면에서 편해짐.

 

  Note) 할당할 메모리의 크기

    - 최소 1페이지 이상

      그럼 1페이지의 크기는 뭐임? 그래야 그거 기준으로 메모리 할당을 요청하지.

  

  Note) GetSystemInfo(&si) // SYSTEM_INFO

    - pageSize = si.dwPageSize

    - allocGranularity = si.dwAllocationGranularity

      페이지 사이즈가 4KB이면 Allocation Granularity Boundary는 64KB.

      그럼 우리는 64KB 번지를 시작으로 4KB 만큼 메모리 할당 요청 가능.

      그 다음은 128KB 번지를 시작으로 4KB만큼 메모리 할당 요청 가능.

 

 

20.1-4 VirtualAlloc() & VirtualFree()

  Note)

// 마치 malloc()과 같음. malloc()은 힙 메모리, 아래 함수는 가상 메모리(근데 이는 사실 힙메모리)
// malloc()은 COMMIT/FREE 상태로만 가능하지만 아래 함수는 RESERVE 상태도 가능함.
LPVOID VirtualAlloc(
    LPVOID lpAddress,       // 할당의 시작 주소.
    SIZE_T dwSize,          // 할당의 크기
    DWORD flAllocationType, // MEM_RESERVE(거의 쓸일 없음.) or MEM_COMMIT
    DWORD flProtect         // PAGE_NOACCESS or PAGE_READWRITE
);

// 반환: 할당이 이뤄진 메모리의 시작번지

 

  Note)

// VirtualFree()는 COMMIT 상태를 FREE 상태로 끌어내리기도 하지만,
// RESERVE 상태를 FREE 상태로 끌어내리기도 함.(잘 안씀)
// 또한 COMMIT 상태를 RESERVE상태로 끌어내리기도 함.
// 결국 아래 함수는 할당 받은 가상 메모리 공간을 반환하는데 의의가 큼.
BOOL VirtualFree(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD dwFreeType // MEM_DECOMMIT(COMMIT->RESERVE) or MEM_RELEASE
);

 

  Note) 이제 예제에서 VA 함수를 이용해서 Resolve / Commit / Free 해보기.

 

20.2 힙 컨트롤(Heap Control)

Prologue) 이전 파트에서 VM를 C/R/F 상태로 둘 수 잇었음.

  중간에 VM를 힙으로 봐도 된다고 언급 했었음. 

 

20.2-1 디폴트 힙(Default Heap) & 동적 힙(Dynamic Heap)

  Note) 아래와 같이 비디오 대여점 프로그램을 작성한다고 해보자.

    이렇게 규칙적이지도 않고, 그 크기도 결정되어 있지 않은 경우에는 List 자료구조로 구현함.

    다만 List 자료구조는 삭제시에, 줄줄이 다 들어가서 삭제 해줘야한다는 단점이 있음.

    그래서 에러의 발생 확률이 높음. 프로그램을 좀 더 방어적으로 해야해서 귀찮아짐.

    왜 그러냐면 홍길동을 위한 데이터와 최대수를 위한 데이터가 같이 존재하기때문.

    최대수가 탈퇴한다면 최대수의 관련 데이터만 선별적으로 줄줄이 지워줘야해서 귀찮고 에러발생가능.

    이는 프로세스를 생성했을때 제공해주는 Windows 디폴트 힙과 유사함.

    디폴트는 왜붙었을까? 기본적으로 제공해주는 초기 크기가 있음. 이후에 더 늘어날 수도 있긴 함.

    또한 윈도우즈에서는 디폴트 힙 외에도 힙을 더 만들 수 있게 해줌. 이게 힙 컨트롤.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 위에서의 문제점을 해결하기 위해서 각자의 힙을 만듦.

    그럼 최대수가 탈퇴시 최대수의 힙 B만 한방에 삭제하면 됨.

    즉, 한 번의 함수 호출로 날려버릴 수 있음. 이걸 Dynamic Heap 기법이 제공해줌.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) VM Control의 중점 키워드는 "예약"이었음. 이를통한 메모리의 효율성 증대.

    DH는 메모리의 효율성 증대 측면도 있지만, 그보다는 데이터의 구분에 초점을 맞춤.

    즉 중점 키워드는 "관리"임.

 

  Note) 근데 VM를 왜 힙이라고 불러도 될까?

    4GB의 메인 메모리 공간이 있다고 해보자. 이는 stack/data/heap/기타로 나뉨.

    이때 heap 영역은 디폴트 힙 영역이됨. 기타 영역에 DH을 만들 수 있음.

    또 이 기타 영역에 VM가 할당됨. 할당된 주소를 받아가지고 해당 주소의 메모리 공간을

    C/R/F 상태로 전환시키면서 써감. 여긴 디폴트 힙도 아니고, 동적 힙도 아님.

    이걸 힙으로 볼지, 그냥 VM이라 부를지는 자기 마음.

    다만, 힙이라는 개념의 메모리 공간은 Runtime시에 결정되는 것이 특징.

    근데 VM도 Runtime시에 할당 받게됨. 그래서 힙이라고 불러도 됨.

 

20.2-2 Dynamic Heap의 장점

  Note) 메모리 단편화 해소

    - 프로그램의 로컬리티가 낮아지는 문제점 발생할 수 있으나

      동적 힙을 사용하면, 관련된 데이터들만 빈틈없이 연속적으로 배치할 수 있음.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

  Note) 동기화 문제에서 자유

    - 쓰레드 별로 힙을 생성할 수 있기 때문에 다른 쓰레드가 접근 안함.

      동적 힙을 사용 못한다면, 쓰레드 별로

      같은 가상 메모리 공간에 힙을 할당 요청할 수도 있음.

 

 

20.2-3 Dynamic Heap 생성 및 할당

  Note) 힙의 생성 및 소멸

    - HeapCreate(): 힙의 생성

    - HeapDestroy(): 힙의 소멸. 최대수 관련 모든 메모리가 한꺼번에 날아가는거 처럼.

 

  Note) 생성된 힙 내의 메모리 할당 및 해제

    - HeapAlloc(): 힙 내에 메모리 할당. HeapCreate()시에 반환되는 힙 핸들을 이용.

    - HeapFree(): 힙 내에 메모리 반환

    - 참고로 malloc()/free()를 쓰면 디폴트 힙에 할당하고 반환하게되는것.

 

 

20.3 MMF(Memory Mapped File)

 

20.3-1 MMF 개념

  Note) Memory를 File에 Mapping 시키겠다는 개념.

    아래 그림과 같이 파일의 일부 메모리 공간을 프로세스의 가상 메모리에 연결 시킴.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 위와 같이 매핑 시키면 프로세스의 가상 메모리 매핑 부분에 데이터 작성시

    그 위치만큼 오프셋되어 파일의 해당위치에 데이터가 작성됨.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 이런게 왜 필요할까?

    만약에 파일에 있는 데이터들을 정렬해야 한다면, 

    다 메인메모리로 올려서 정렬하고나서 다시 파일에 저장해야 함.

    위와 같이 MMF를 활용하면, 그냥 메인 메모리에서 곧바로 정렬만 하면됨.

    데이터 읽고 쓰기 과정이 필요없어짐.

 

20.3-2 MMF 장점

  Note) 위와 같은 장점도 있지만, read할때도 장점이 있음.

    데이터 쓰기를 파일에다가 하지 않고 메모리에 하기 때문에

    최신 데이터도 메모리에 적혀져 있음. 따라서 read도 메모리에서 읽으면 됨.

    파일에 데이터를 쓰는것은 주기적으로, 혹은 어떤 이벤트마다만 쓰면됨.

    즉 아래 그림과 같이 빈번한 IO가 필요할때는 가장 최신의 데이터를 유지하고

    정확한 데이터가 작성된다는 보장만 받으면 되는 곳은 주기적/상황적 IO 진행.

    즉 메인 메모리가 파일에게는 캐시의 역할을 해주는 것.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

 

20.3-3 MMF 구현과정

  Note) 1단계: 파일의 생성

    - HANDLE hFile = CreateFile(...);

 

  Note) 2단계: 파일 연결 오브젝트 생성

    - HANDLE hMapFIle = CreateFileMapping(hFile, ...);

      파일 만들고 바로 메모리와 매핑시키는게 아님.

      매핑을 시킬 수 있도록 파일 정보를 담은 오브젝트를 만듦.

      파일 매핑을 위한 연결 오브젝트라고도 함.

      이 오브젝트를 통해서 OS에게 "이 파일의 이부분을 매핑 시켜주세요."라고 요청.

 

  Note) 3단계: 가상 메모리에 파일 연결

    - TCHAR* pWrite = (TCHAR*)MapViewOfFile(hMapFile, ...);

      실질적으로 파일과 메모리의 어디서부터 어디까지를 매핑 진행 요청.

      이제 pWrite 부터 원하는 위치까지 포인터 연산을 통해서 접근하면 됨.

      ptr to char이기 때문에 1byte 마다 접근 가능해짐.

      ptr ro int로 바뀌면 4byte 단위 마다 접근 가능.

 

 

 

댓글