Advanced C++

[C++] 메모리 관리 (1) Windows 메모리 관리

로파이 2021. 10. 3. 19:13

가상 메모리 관리

Windows에서 제공하는 가상 메모리를 관리하는 방법을 알아본다.

가상 메모리의 상태는 Reserve/Commit/Free를 가지는데, Free는 물리 메모리로 할당되지 않음, Commit은 물리 메모리로 할당된 상태 그리고 Reserve는 물리 메모리로 할당되지는 않았지만 접근을 금지하여 그 영역을 다시 Reserve 하거나 

Commit하는 것을 막을 수 있다.

페이징 시스템

프로세스에 할당되는 메모리 가상 공간을 고정 크기로 나누어 사용하는 것을 페이징 시스템이라고 하는데, 페이지 하나의 크기가 메모리 조각 크기를 결정하게 된다.

가상 메모리를 효율적으로 관리하기 위해 Commit/Reserve/Free 상태를 두는 페이지는 Reserve 예약이라는 상태를 가질 수 있다. 나중에 물리 메모리로 올릴 예정이라고 해당 페이지를 마킹하는 것인데, 물리 메모리로 한꺼번에 할당하고 싶지 않지만 나중에 메모리를 요청하여 연속된 메모리 공간을 얻고 싶을 때 사용한다. 

 

가상 메모리에 대한 상태를 변경할 때의 메모리 사이즈의 단위는 항상 페이지가 되며 새로운 예약된 블록을 얻거나 물리 메모리로 등록하는 과정에서 항상 페이지의 배수만큼 할당된다.

 

메모리를 Reserve할 때는 Allocation Granularity Boundary 크기의 배수에 해당하는 주소로 Commit할 때는 페이지 단위, 페이지 크기의 배수에 해당하는 주소로 시작하게 된다. 이 두 크기 파라미터는 운영체제 시스템에 정의되어 있다.

#include <iostream>
#include <cstdio>
#include <tchar.h>
#include <Windows.h>

int main()
{
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	SYSTEM_INFO info;
	DWORD allocGranularity;
	DWORD pageSize;

	GetSystemInfo(&info);
	pageSize = info.dwPageSize;
	allocGranularity = info.dwAllocationGranularity;

	_tprintf(_T("Page Size : %d KBytes\n"), pageSize / 1024); // 4KB
	_tprintf(_T("Allocation granularity : %d KBytes\n"), allocGranularity / 1024); // 64KB

	return 1;
}

VirtualAlloc과 VirtualFree 함수

- VirtualAlloc

LPVOID VirtualAlloc(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);
  • lpAddress : 메모리를 할당할 영역의 시작 주소를 가리킨다. 메모리가 예약된 상태라면 Allocation Granularity 크기의 배수 중 가장 가까운 주소로 반올림된다. 메모리가 예약되고 커밋된 상태라면 다음 페이지 경계로 반올림 된다. Allcoation Granularity와 페이지 크기를 알고 싶다면, GetSystemInfo 함수를 이용하라. null을 전달하면 시스템이 알아서 결정한다.
  • dwSize : 영역의 사이즈, lpAddress가 null이라면 다음 페이지 경계의 반올림 값으로 설정된다.
  • flAllocationType : 변경할 메모리 상태 타입을 말한다. MEM_RESERVE/MEM_COMMIT/MEM_RESET 등을 지정할 수 있다.
  • flProtect : 메모리 보호관련 하여 접근 권한을 설정한다. MEM_RESERVE로 예약을 설정하였다면 해당 접근을 막는 PAGE_NOACCESS, MEM_COMMIT으로 커밋을 설정하였다면 PAGE_READWRITE로 메모리 읽기 쓰기 권한을 부여한다.

- VirtualFree

BOOL VirtualFree(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  dwFreeType
);
  • lpAddress : 해제할 메모리 공간의 시작 주소를 지정한다. MEM_RELEASE를 지정할 경우 dwSize는 0이어야하고 시작 주소는 메모리를 예약할 때 지정한 시작 주소를 대입해야한다.
  • dwSize : 해제할 메모리의 사이즈
  • dwFreeType : MEM_RELEASE/MEM_DECOMMIT, MEM_DECOMMIT은 커밋에서 예약된 상태로 돌려놓는다. 물리 메모리가 할당되어 있다면 반납을 하게 된다.

- 예시

1) 예약된 가상 메모리를 얻는 방법

// MAX_PAGE 개수만큼 페이지 RESERVE
baseAddr = VirtualAlloc(NULL, MAX_PAGE * pageSize, MEM_RESERVE, PAGE_NOACCESS);

 2) 예약된 가상 메모리 중 커밋한 메모리를 얻는 방법 

LPVOID lpvResult = VirtualAlloc((LPVOID)nextPageAddr, pageSize, MEM_COMMIT, PAGE_READWRITE);

3) 가상 메모리를 모두 해제

BOOL isSuccess = VirtualFree(baseAddr, 0, MEM_RELEASE);

 

더보기
#include <iostream>
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>

#define MAX_PAGE 10

int* nextPageAddr;
DWORD pageCnt = 0;
DWORD pageSize;
int PageFaultExceptionFilter(DWORD);

int _tmain(int argc, TCHAR* argv[])
{
	LPVOID baseAddr;
	int* lpPtr;
	SYSTEM_INFO sSysInfo;

	GetSystemInfo(&sSysInfo);
	pageSize = sSysInfo.dwPageSize;

	// MAX_PAGE 개수만큼 페이지 RESERVE
	baseAddr = VirtualAlloc(NULL, MAX_PAGE * pageSize, MEM_RESERVE, PAGE_NOACCESS);

	if (baseAddr == NULL)
	{
		_tprintf(_T("VirtualAlloc Reserve Failed\n"));
		return -1;
	}

	lpPtr = (int*)baseAddr; // 예약된 힙 메모리의 시작 주소
	nextPageAddr = (int*)baseAddr;
	
	// page fault 발생시 예외
	for (int i = 0; i < (MAX_PAGE * pageSize) / sizeof(int); ++i)
	{
		__try {
			lpPtr[i] = i;
		}
		__except (PageFaultExceptionFilter(GetExceptionCode()))
		{
			ExitProcess(GetLastError()); // 예외처리 문제 발생 시 종료
		}
	}

	for (int i = 0; i < (MAX_PAGE * pageSize) / sizeof(int); ++i)
	{
		_tprintf(_T("%d "), lpPtr[i]);
	}

	BOOL isSuccess = VirtualFree(baseAddr, 0, MEM_RELEASE);

	if (isSuccess)
		_tprintf(_T("Release Success!\n"));
	else
		_tprintf(_T("Release Failed!\n"));
}

int PageFaultExceptionFilter(DWORD exceptCode)
{
	// 예외의 원인이 PAGE FAULT가 아니라면
	if (exceptCode != EXCEPTION_ACCESS_VIOLATION)
	{
		_tprintf(_T("Exception code = %d \n"), exceptCode);
		return EXCEPTION_EXECUTE_HANDLER;
	}
	_tprintf(_T("Exception is a page fault\n"));

	// 이미 최대 페이지 수 만큼 COMMIT한 경우
	if (pageCnt >= MAX_PAGE)
	{
		_tprintf(_T("Exception: out of pages\n"));
		return EXCEPTION_EXECUTE_HANDLER;
	}

	LPVOID lpvResult = VirtualAlloc((LPVOID)nextPageAddr, pageSize, MEM_COMMIT, PAGE_READWRITE);
	if (lpvResult == NULL)
	{
		_tprintf(_T("VirtualAlloc Failed\n"));
		return EXCEPTION_EXECUTE_HANDLER;
	}
	else
	{
		_tprintf(_T("Allocating Another Page...\n"));
	}

	pageCnt++;
	nextPageAddr += pageSize / sizeof(int);

	// page fault가 발생한 지점부터 실행을 계속
	return EXCEPTION_CONTINUE_EXECUTION;
}

 

힙 메모리 관리

힙 메모리를 가상 메모리를 관리하는 것과 유사하게 관리할 수 있다.

 

힙 메모리를 먼저 크게 예약한 후에 필요한 크기만큼 힙 메모리를 커밋하여 사용할 수 있는데 이렇게 사용하는 데는 다음과 같은 이점이 있다.

1. 메모리 단편화를 최소화한다.

메모리 단편화란 메모리를 조각으로 나누어 사용하면서 발생하는 문제로 조각 사이간 남는 메모리로 인한 내부 단편화와 새로운 메모리를 할당할 때 작은 메모리 공간을 사용할 수 없는 외부 단편화 문제가 있다. 뿐만 아니라 보통 힙 메모리를 한번에 예약하고자 하는 이유가 모듈내에서 자주 접근되는 메모리를 모아두기 위해서이다. 이는 자주 접근 되는 메모리가 지역성이 높아지기 때문에 캐시 히트율이 높아지고 페이지 부재와 같은 주 메모리 적재관련 비용도 적어진다. 왼쪽 그럼 처럼 힙 메모리를 모듈별로 나누어 크게 예약하지 않고 사용할 때마다 순서에 상관없이 메모리를 할당하게 된다면 오른쪽 그럼처럼 될 것이다. 이는 메모리 접근의 지역성을 떨어뜨리고 메모리 공간을 효율적 사용을 저해한다.

 

2. 동기화 문제

다중 스레드를 사용하는 프로그램에서 힙 영역을 공유하기 때문에 윈도우에서 힙 영역에 대한 메모리 할당과 해제에 대해 동기화를 기본적으로 제공한다. 그렇지 않다면 여러 스레드에서 동일한 주소에 메모리를 할당함으로써 메모리 오류(Corruption)이 발생할 수 있기 때문이다. 만약 모듈 별로 스레드를 할당하고 이를 위한 힙 메모리를 따로 예약해둔다면 예약할 때만 동기화하고 실제로 커밋으로 메모리를 가져올 때는 동기화를 할 필요가 없기 때문에 최적화의 여지가 있다.

 

HeapCreate / HeapDestroy / HeapAlloc / HeapFree

윈도우에서 제공하는 힙 메모리 관리를 할 수 있는 함수를 알아본다.

HeapCreate

HANDLE HeapCreate(
  DWORD  flOptions,
  SIZE_T dwInitialSize,
  SIZE_T dwMaximumSize
);
  • flOptions : 생성되는 힙의 특성에 대한 옵션으로 HEAP_NO_SERIALIZE의 옵션으로 동기화 기능을 사용하지 않거나 HEAP_GENERATE_EXCEPTIONS으로 오류 발생시 예외를 던질 수 있다.
  • dwMaximumSize : 생성되는 힙의 크기를 지정한다. 여기서 지정하는 만큼 힙이 예약되며 0을 지정하면 최대 한도내에서 증가하는(Growable) 힙 메모리가 생성된다. 
  • dwInitialSize : 지정한 메모리 중 커밋할 메모리 사이즈를 지정한다.
  • 예약된 힙에 대한 핸들을 반환한다. 

HeapAlloc

DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
  HANDLE hHeap,
  DWORD  dwFlags,
  SIZE_T dwBytes
);
  • hHeap : 메모리 할당이 이루어지는 힙 핸들을 지정한다.
  • dwFlags : HEAP_GENERATE_EXCEPTIONS / HEAP_NO_SERIALIZE / HEAP_ZERO_MEMORY를 지정할 수 있다.
  • dwBytes : 할당하고자 하는 메모리의 크기가 지정된다. 증가하는 힙이 아닐 경우 최대 크기는 0x7fff8로 제한된다.

HeapFree

BOOL HeapFree(
  HANDLE                 hHeap,
  DWORD                  dwFlags,
  _Frees_ptr_opt_ LPVOID lpMem
);
  • hHeap : 해제할 메모리를 담고 있는 힙을 지정
  • dwFlags : HEAP_NO_SERIALIZE 동기화 여부 지정
  • lpMem : 해제할 메모리의 시작주소

- 예시

int _tmain(int argc, TCHAR* argv[])
{
	SYSTEM_INFO sysInfo;
	GetSystemInfo(&sysInfo);
	UINT pageSize = sysInfo.dwPageSize;

	// 1. 힙의 생성
	HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, pageSize * 10, pageSize * 100);

	if (hHeap == NULL)
		return -1;

	// 2. 메모리 할당
	int* p = (int*)HeapAlloc(hHeap, 0, sizeof(int) * 10);

	// 3. 메모리 활용
	for (int i = 0; i < 10; ++i)
	{
		p[i] = i;
	}

	// 4. 메모리 해제
	HeapFree(hHeap, 0, p);

	// 5. 힙 소멸
	HeapDestroy(hHeap);

	HANDLE hDefaultHEap = GetProcessHeap();
	TCHAR* pDefault = (TCHAR*)HeapAlloc(hDefaultHEap, HEAP_NO_SERIALIZE, sizeof(TCHAR) * 30);
	_tcscpy_s(pDefault, 30, _T("Default Heap!"));
	_tprintf(_T("%s\n"), pDefault);
	HeapFree(hDefaultHEap, HEAP_NO_SERIALIZE, pDefault);

	return 0;
}

 

미리 예약한 힙을 사용한 연속 메모리를 가지는 링크드 리스트

더보기
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

template<typename T>
struct Node
{
	T Data;
	struct Node* Next = nullptr;
};

template<typename T>
class HeapLinkedList
{
private:
	Node<T>* Head;
	HANDLE	 hHeap;

public:
	HeapLinkedList()
	{
		SYSTEM_INFO sysInfo;
		GetSystemInfo(&sysInfo);

		UINT pageSize = sysInfo.dwPageSize;
		hHeap = HeapCreate(HEAP_NO_SERIALIZE, pageSize * 10, pageSize * 100);
		Head = nullptr;
	}

	~HeapLinkedList()
	{
		HeapDestroy(hHeap);
	}

	void Insert(const T& Data)
	{
		Node<T>* NewNode = (Node<T>*)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, sizeof(Node<T>));
		NewNode->Data = Data;
		NewNode->Next = nullptr;

		if (Head == nullptr)
		{
			Head = NewNode;
			return;
		}

		Node<T>* InsertNode = Head;

		while (InsertNode->Next)
		{
			InsertNode = InsertNode->Next;
		}

		InsertNode->Next = NewNode;
	}

	void Erase(const T& Data)
	{
		Node<T>* FindNode = Head;
		Node<T>* PrevNode = nullptr;

		while (FindNode)
		{
			if (FindNode->Data == Data)
				break;

			PrevNode = FindNode;
			FindNode = FindNode->Next;
		}

		if (FindNode != nullptr)
		{
			if (PrevNode == nullptr)
			{
				Head = nullptr;
			}
			else
			{
				PrevNode->Next = FindNode->Next;
			}

			HeapFree(hHeap, HEAP_NO_SERIALIZE, FindNode);
		}
	}

	void Print()
	{
		Node<T>* CurNode = Head;

		int iCount = 0;
		while (CurNode)
		{
			printf("%d Node / 0x%p : %d\n", iCount, CurNode, CurNode->Data);
			CurNode = CurNode->Next;
			++iCount;
		}
		printf("\n");
	}
};
#include <crtdbg.h>
#include "HeapLinkedList.h"

int main()
{
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	HeapLinkedList<int> list;
	
	for (int i = 0; i < 10; ++i)
	{
		list.Insert(i);
	}
	
	list.Erase(5);
	list.Erase(6);

	list.Insert(10);
	list.Insert(11);

	list.Print();

	return 0;
}

 

참고 : 뇌를 자극하는 윈도우즈 시스템 프로그래밍 (윤성우 저)