KoreanFoodie's Study

[C++ 게임 서버] 2-4. StompAllocator 본문

Game Dev/Game Server

[C++ 게임 서버] 2-4. StompAllocator

GoldGiver 2023. 8. 24. 22:22

Rookiss 님의 '[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버' 를 들으며 배운 내용을 정리하고 있습니다. 관심이 있으신 분은 꼭 한 번 들어보시기를 추천합니다!

[C++ 게임 서버] 2-4. StompAllocator

핵심 :

1. 프로그램은 가상 메모리를 사용하므로, 메모리 오염이 즉각적으로 발견되지 않을 수도 있다.

2. StompAllocator 를 사용하면, 할당 및 해제 시 '실제로' 메모리의 할당과 해제가 일어나므로, 메모리 오염으로 인한 문제를 즉각적으로 발견할 수 있다(alloc 은 '예약' 상태밖에 사용안함).

3. PageSize 와 Granularity 등의 개념을 잘 알아두자. 일반적으로 기본 PAGE_SIZE 는 4KB, Granularity 는 64KB 이다.

이전에 언리얼 엔진과 관련된 테스트를 진행할 때, UnrealVS 를 이용해 '-stompmalloc' 테스트를 하는 내용을 포스팅한 적이 있었다.

StompAllocator 를 이용하면 메모리 오염을 쉽게 잡을 수 있다. 예를 들어 다음과 같은 상황을 보자.

위에서, k1 은 delete 가 되었지만, 여전히 k1->hp 에 접근이 가능하고, 위를 보면 심지어 값이 바뀐 것을(!) 알 수 있다.

이런 일이 일어나는 이유는, 각 프로그램별로 '가상 메모리'를 사용하기 때문이다. 😅

즉, 각각의 프로그램들은 메모리를 할당하고 해제할때 '진짜 물리적인' 메모리에 바로 접근하는 것이 아니라, 프로그램별로 맵핑된 독립된 가상 메모리를 사용한다! 이는 프로그램 끼리 메모리를 서로 침범하지 않는 효과를 갖기도 한다.

 

어쨋든, 위와 같은 이유로 메모리가 오염되었을 경우, '운이 나쁘면' 라이브 서비스에서 크래시가 발생하게 되는데...

이번에는 메모리 오염을 제대로 잡아주는 StompMalloc Allocator 를 간단하게 직접 구현해 볼 것이다 😁

사실, 저번에 설명한 Allocator 와 살짝 다를 뿐이다. 코드를 보자.

/*-------------------
	StompAllocator
-------------------*/

/** 선언 */
class StompAllocator
{
	enum { PAGE_SIZE = 0x1000 };

public:
	static void*	Alloc(int32 size);
	static void	Release(void* ptr);
};

/** 구현 */
void* StompAllocator::Alloc(int32 size)
{
	// pageCount 를 딱 떨어지도록 하기 위해, PAGE_SIZE - 1 더하고 PAGE_SIZE 로 나눔
	const int64 pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE;

	// dataOffset 은, 페이지 내에서 실제 데이터가 위치할 메모리의 시작 지점이다.
	// 메모리 할당 시, 객체의 정보를 페이지 영역의 마지막에 넣어준다.
	// 아래 그림처럼 나올 것임 (큰 [ ] 은 페이지, 작은 [ ] 는 실제 객체의 영역)
	// [                    [   ]]
	const int64 dataOffset = pageCount * PAGE_SIZE - size;

	// VirtualAlloc 은 Windows.h 에 있는, 윈도우에서 예약/확정 상태를 조절할 수 있다.
	// 예약만 한 경우, 물리적 메모리는 소모되지 않는다.
    // 예약 및 확정을 할 경우, MEM_RESERVE | MEM_COMMIT 로 사용한다.
	void* baseAddress = ::VirtualAlloc(NULL, pageCount * PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
	return static_cast<void*>(static_cast<int8*>(baseAddress) + dataOffset);
}

void StompAllocator::Release(void* ptr)
{
	// 실제 객체가 할당된 곳의 포인터를 받는다.
	const int64 address = reinterpret_cast<int64>(ptr);
	
	// offset 을 빼서, 페이지의 시작 지점을 얻는다.
	const int64 baseAddress = address - (address % PAGE_SIZE);

	// 페이지 시작 지점으로부터, 메모리를 해제한다.
	::VirtualFree(reinterpret_cast<void*>(baseAddress), 0, MEM_RELEASE);
}

일단, PAGE_SIZE 는 4096 으로 잡았다. 이건 4kb 인데... 메모리 할당할때, 페이지 크기가 기본적으로 4kb 이다.

실제로 위를 보면, dwPageSize 는 4096 이고, dwAllocationGranularity 라는 녀석이, 65536 (64kb) 로 되어 있는 것을 알 수 있다.  granularity -> 이 녀석은, 메모리가 할당되는 최소 단위를 의미한다(Alignment 에도 사용) 😉

 

사실 웬만한 건 위의 설명으로도 충분할 테지만... VirtualAlloc 에 대해서만 조금 더 기술하고, 글을 마무리하고자 한다 😊

 

VirtualAlloc 의 정의는 다음과 같다 : (공식 문서)

VirtualAlloc(
    _In_opt_ LPVOID lpAddress,
    _In_     SIZE_T dwSize,
    _In_     DWORD flAllocationType,
    _In_     DWORD flProtect
    )
{
    return VirtualAllocFromApp (lpAddress, dwSize, flAllocationType, flProtect);
}

첫번째는.. 음 그냥 NULL 로 넣어주면 된다.

두번째는 페이지의 사이즈를, 세번째는 예약/확정 여부를 넣어준다. 예약과 동시에 물리 메모리를 실제로 사용하고 싶으면,  MEM_RESERVE | MEM_COMMIT 를 사용한다.

마지막은 Read/Write 접근 권한에 대한 부분이다.

Comments