시스템 프로그래밍 20장 - 메모리 관리(Virtual Memory,Heap, MMF)

김주현·2021년 10월 24일
0

시스템 프로그래밍

목록 보기
20/21

01 . 가상 메모리 컨트롤

  • Reserve,Commit 그리고 Free

리저브는 예약, 커밋은 할당, free는 할당되지 않았음을 의미한다. Windows 시스템에서 부여할 수 있도록 정의한 페이지의 상태를 의미하는것이다. 페이지의 총 개수는 다음과 같은 공식으로 계산할 수 있다.

가상메모리의 크기 / 페이지 하나당 크기 = 페이지의 개수

즉 페이지 개수는 가상 메모리의 크기에 비례하며,모든 페이지는 Reserved,Commit 그리고 Free 이 세 가지 중 하나의 상태를 지닌다.

일단 Commit으로 표시된 부분은 물리 메모리에 할당이 이뤄진 부분들이다. 여기서 말하는 물리 메모리란 램과 하드디스크를 모두 포함한 것이다. 즉 해당 페이지가 물리 메모리에 할당된 상태를 가리켜 COMMIT 상태라 표현한다. 여러분이 malloc 함수 호출을 통해서 일부 메모리를 할당 받으면 해당 메모리의 페이지는 COMMIT 상태가 된다. 할당이 이뤄지지 않은 페이지는 Free상태이다.

프리 상태에 있던 페이지 중에서 총 다섯 페이지를 RESERVE 상태로 변경 시켰다. RESERVE 상태는 FREE와 COMMIT의 중간 상태이다. 일부 페이지를 예약 상태로 둠으로써 다른 메모리 할당 함수에 의해 해당 번지가 할당 되지 못하도록 선언할 수있다. 그러나 예약을 했을뿐 할당이 완료된 상태가 아니므로 물리 메모리에 할당되지는 않는다.

예약 상태에는 메모리중에서 일부만 커밋상태로 변경하는것도 가능하다.

  • 메모리 할당의 시작점과 단위 확인하기

메모리의 할당하기 전에 "메모리 할당의 시작 주소"와 "할당할 메모리의 크기"를 고려해야 한다.

가상 메모리 시스템은 페이지 단위로 관리되므로 페이지의 중간 위치에서부터 할당을 시작할 수 없고, 페이지 크기의 배수 단위로 할당을 해야만 한다.

페이지 크기가 4K 바이트라면 4K의 배수 값이 할당의 시작 주소가 될 수 있음. 그러나 Windows 시스템에서는 메모리가 지나치게 조각나는 것을 막기 위해서,그리고 관리의 효율성을 이유로 조금 더 넓은 범위의 값을 할당의 경계로 정의함.

메모리 할당의 시작 주소가 될 수 있는 기본 단위를 가리켜 "Allocation Granularity Boundary"라고 함. 페이지 하나의 크기가 할당의 기본 단위 이고, 배수 단위로 할당이 가능함

GetSystemInfo 함수를 통해 "Allocation Granularity Boundary"와 페이지의 크기를 알수 있음

  • VirtualAlloc & VritualFree 함수

VirtualAlloc 함수는 페이지 상태를 RESERVE와 Commit 상태로 만드는 역할을 한다.

LPVOID VirtualAlloc (
LPVOID lpAddress // 예약 및 할당하고자 하는 메모리의 시작 주소를 지정한다. 일반적으로 NULL을 전달 하게 되는데, NULL이 전달되면 할당하고자 하는 크기에 맞춰서 메모리의 위치를 임의로 결정한다. 그러나 RESERVE 상태에 있는 페이지를 COMMIT 상태로 변경할 때에는 해당 페이지의 시작 주소를 지정해야한다 예약을 할때에는 "Allocation Granularity Boundary"를 기준으로 값이 조절되고,할당을 할 때에는 페이지 크기 단위로 값이 조절된다.

SIZE_T dwSize // 할당하고자 하는 메모리의 크기를 바이트 단위로 지정한다. 메로리의 할당은 페이지 크기 단위로 결정된다.

DWORD flAllcationType // 메모리 할당의 타입을 결정한다. 페이지를 RESERVE 상태로 두고자 하는 경우에는 MEM_RESERVE를 인자로 전달한다. 페이지를 COMMIT 상태로 변경하고자 하는 경우에는 MEM_COMMIT을 인자로 전달한다.

DWORD flProtect //페이지별 접근방식에 제한을 두는 용도로 사용한다. 지금까지 설명한 적은 없지만 Windows 시스템 함수를 사용했을 때 얻게 되는 장점이다. 기본적으로 RESERVE 상태에 둘 때에는 접근을 허용하지 않는 PAGE_NOACCESS를 COMMIT 상태로 변경할 때에는 읽기 쓰기를 모두 허용하는 PAFE_READWRITE를 인자로 전달한다. 

함수호출이 성공하면 할당이 이뤄진 메모리의 시작 번지를 반환한다. 

);

VirtualFree 함수는 VirtualAlloc 함수가 정해 놓은 상태를 되돌리는 역할을 한다.

BOOL VirtualFree (
LPVOID lpAddress, // 해제할 메모리 공간의 시작 주소를 지정한다.
SIZE_T dwSize // 해제할 메모리 크기를 바이트 다누이로 지정한다.
DWORD dwFreeType // MEM_DECOMMIT과 MEM_RELEASE 중 하나를 지정할수 있다
MEM_RELEASE를 지정할 경우 해당 페이지는 FREE 상태가된다. 물리적 메모리가 할당되어 이싸면 해당 메모리는 반환된다. 주의할 사항은 MEM_RELEASE 전달 시 두 번째 전달인자 dwSize는 반드시 0이어야 하고, lpAddress의 값은 VirtualAlloc 함수 호출을 통해 예약된 메모리의 시작 번지가 되어야 한다는 것이다.

malloc 함수가 반환한 주소값을 통해서 free 함수를 호출하는 것과 동일한 이치이다. 그리고 예약이 이뤄진 메모리의 일부만 반환하는 것은 불가능하다.

MEM_DECOMMIT을 인자로 전달할 경우에는 해당 페이지의 상태를 RESERVE 상태로 되돌리게 된다. 이 경우에도 물리적 메모리가 할당되어 있다면 해당 메모리는 반환하게 된다.
  • 동적 배열 디자인

일반적인 배열 처럼 한순간에 배열 크기만큼 물리 메모리가 할당되는 것이 아니라 사용량의 증가에 따라 물리 메모리에 할당되는, 배열의 크기가 점진적으로 증가하는 배열을 디자인한다.

1단계
시스템의 페이지 사이즈와 "Allocation Granularity Boundary"값을 얻어 온다. 할당하고자 하는 메모리의 위치에 직접적으로 관여하지 않겠다면,페이지 사이즈만 얻어와도 된다.

2단계
메모리를 예약한다. 예약을 할 때에는 필요하다고 예상되는 최대의 크기로 예약을 한다.

3단계
필요한 만큼의 메모리를 무리 메모리에 할당한다. 필요에 따라 점진적으로 할당의 크기를 증가시킨다.

4단계
할당했던 메모리를 반환한다.

02 . 힙 컨트롤

-디폴트 힙 & 윈도우즈 시스템에서의 힙

C 언어를 사용할 경우 malloc 함수와 free함수를, C++를 사용할 경우 new와 delete 연산자를 사용해서 힙 영역에 메모리를 할당한다. 이러한 경우 프로세스를 생성할 때 더불어 생성되는 힙,정확히 말하면 1M바이트 크기의 디폴트 힙 영역에 메모리를 할당하게 된다. 이 디폴트 힙은 프로세스에 기본적으로 할당되는 힙이라 하여 프로세스 힙이라고도 부른다.
Windows 시스템 함수를 활용하면 다음과 같은 구조로 메모리 구성을 변경할 수 있다.

위 그림은 추가적인 힙 생성이 가능함을 보여준다.

티폴트 힙을 구성하는 페이지들의 상태는 RESERVE이다. 일부 페이지 상태는 이미 COMMIT 상태에 놓여 있을 수도 있다. 맞추셨다면 가상 메모리에 대한 개념이 완전히 들어선 상태라고 볼 수 있다. RESERVE 상태에 있는 힙의 페이지들이 malloc과 free 함수의 호출을 통해서 페이지 크기의 정수배로 COMMIT과 RESERVE 상태를 오가는 것이다.

  • 디폴트 힙 컨트롤
    앞서 말한 디폴트 힙의 기본 크기는 1M 바이트이다. 그러나 링커 옵션을 통해서 변경이 가능하다. 다음과 같은 형식으로 링커 옵션을 통해 RESERVE 상태에 놓이게 되는 힙의 전체 크기와 이중에서 초기에 COMMIT 상태로 둘 메모리 크기를 지정할 수 있다.
    /HEAP : reserve, commit
    예를 들어서 다음과 같이 링커 옵션을 지정한경우
    /HEAP : 0x200000, 0x10000

그렇다면 디폴트 힙의 크기는 2M바이트가 되며, 이중 64K바이트가 COMMIT 상태에 있게 된다.

디폴트 힙의 기본 크기 1M 바이트는 힙이 생성된 직후의 초기 크기를 말하는 것이다. 필요에 따라서 그 크기는 자동으로 늘어난다. 자동으로 늘어난다고 표현하는 것보다는 Windows 시스템이 알아서 늘려준다고 하는 것이 정확한 표현이다.
결국 번거롭게 디폴트 힙 크기를 개발자가 정해줄 필요가 없다는 결론이 나온다. 그러나 디폴트 힙 크기를 정해줌으로 얻게되는 장점 한 가지가 있다. 프로세스가 실행 중인 상태에서 새로운 메모리 영역을 할당하는 것은, 시간이 제법 걸리는 작업이다. 따라서 필요한 크기만큼 여유 있는 디폴트 힙을 요청해 둔다면 그만큼의 시간을 아낄수 잇다.

  • 힙 생성이 가져다 주는 또 다른 이점
    디폴트 힙 이외에 Windows 시스템 함수 호출을 통해서 생성되는 힙을 가리켜 동적 힙이라 한다.

장점 1.메모리 단편화의 최소화에 따른 성능 향상

A라는 기능을 위해서 힙 A를 생성하고,B라는 기능을 위해서 힙 B를 생성하고, C라는 기능을 위해서 힙C를 생성한다면 아래 그림과 같은 구조로 메모리가 할당된다.

반면에 이것을 하나의 디폴트 힙에서 처리한다면 오른쪽과 같은 구조가 된다.
훨씬 복잡하게 단편화가 발생할 소지가 높다.

일단 A,B,C의 힙을 미리 선언하면 할당된 페이지가 RESERVE 상태에 놓이기 때문에 메모리 단편화가 발생하지 않음. 빈면에 디폴트 힙을 활용할 경우 프로그램 실행과정에서 무작위 메모리 할당 및 그에 따른 힙 크기의 증가에 의해 메모리 단편화가 심하게 발생함. 단편화가 심하다는것은 프로그램의 로컬리티 특성이 낮아진다는 것을 의미하며 이는 성능에 많은 영향을 미치게 됨 . 즉 필요에 맞게 추가적인 힙을 생성해서 활용한다면, 성능 향상도 기대 할 수 있다.

장점 2. 동기화 문제에서 자유로워짐으로 인한 성능 향상

일반적으로 힙은 쓰레드가 공유하는 메모리 영역임. 때문에 둘 이상의 스레드가 동시접근 할 때 문제가 발생할 소지가 있어서 Windows 내부적으로 동기화 처리를 해주고 있다. 여기서 말하는 동시접근이란 메모리의 할당과 해제이다.

같은 주소 번지에 둘 이상의 쓰레드가 동시에 메모리를 할당 및 해제하는 사오항이 발생할 경우 메모리 오류가 발생함. 때문에 디폴트 프로세스 힙은 쓰레드가 메모리를 할당하려고 하는 경우 내부적으로 동기화 처리를 하고있음. 그런데 하나의 쓰레드당 독립된 하나의 힙을 할당할 경우 동기화 처리를 할 필요가 없고 이것으로 인한 성능 향상을 기대할 수 있게 된다.

  • 힙의 생성과 소멸 그리고 할당

힙을 생성하는 데 사용되는 함수

HANDLE HeapCreate (
DWORD flOptions // 생성되는 힙의 특성을 부여하는 데 사용된다. 0을 전달할 경우 가장 일반적인 힙이 생성된다. HEAP_GENERATE_EXCEPTIONS를 전달할 경우 오류 발생 시 NULL을 반환하는 것이 아니라 예외를 발생 시킨다.
HEAP_NO_SERIALIZE를 지정할 경우 생성된 힙에 메모리를 할당 및 해제할 때 동기화 처리를 하지 않게 된다("장점 2. 동기화 문제에서 자유로워짐으로 인한 성능향상")
쓰레드별 독립된 힙을 생성할 경우에는 HEAP_NO_SERIALIZE를 인자로 지정해서 성능을 향상시키는것이 좋다. 둘 이상의 속성을 비트 단위 OR 연산자로 동시 지정 가능하다.

SIZE_T dwInitialSize
SIZE_T dwMaximumSize

dwMaximumSize : 생성되는 힙의 크기를 결정한다. 여기서 지정하는 크기에 해당하는 페이지의 수 만큼 RESERVE 상태가 된다. 0이 값으로 전달되면, 힙은 증가 가능한 메모리가 된다. 따라서 0이 전달될 경우 메모리가 허락하는 한도 내에서 힙의 크기는 증가한다.

dwInitalSize : dwMaximumSize에서 지정한 메모리 중에서 초기에 할당할 물리 메모리 크기를 지정한다. 여기서 지정한 크기에 해당하는 페이지의 수만큼 힙이 생성되지마자 COMMIT 상태가 된다.

위 함수는 오류가 발생하지 않는다면, 생성된 힙을 컨트롤하는 데 사용되는 힙의 핸들을 반환한다.

다음은 힙을 소멸하는 데 사용하는 함수이다. RESERVE 상태에 놓여있던 페이지를(COMMIT 상태에 있던 페이지 포함해서)FREE 상태로 되돌리는 함수로 이해해도 좋다.

BOOL HeapDestroy (
HANDLE hHeap // 반환하고자 하는 힙의 핸들을 인자로 전달한다.
);

힙을 생성하였다면 힙에 메모리를 할당하고 해제할 차례다. 힙에 메모리를 할당할 때에는 다음 함수를 사용한다. 요청 크기에 해당하는 페이지 수만큼 COMMIT 상태로 변경시킨다는 점을 더불어 이해하자

LPVOID HeapAlloc (
HANDLE hHeap : 메모리 할당이 이뤄질 힙의 핸들을 지정한다
DWORD dwFalgs : HEAP_GENERATE_EXCEPTIONS가 인자로 올 경우, 오류 발생 시 NULL을 반환하지 않고 예외를 발생시킨다. HEAP_NO_SERIALIZE를 인자로 전달할 경우 함수 호출은 동기화 처리되지 않는다. HeapCreate 함수 호출 과정에서 이미 HEAP_NO_SERIALIZE를 전달하였다면 이 전달인자를 통해서 중복 지정할 필요가 없다. 그리고 HEAP_ZERO_MEMORY가 전달되면 할당된 메모리는 0으로 초기화된다. 둘 이상의 속성을 비트 단위 OR 연산자로 동시 지정 가능하다.
SIZE_T dwBytes : 할당하고자 하는 메모리의 크기를 지정한다. 참고로 힙이 증가 가능한 힙이 아니라면, 다시 말해서 힙 생성 시 HeapCreate 함수 호출의 마지막 전달인자가 0이 아니었다면 할당의 최대 크기는 0x7FFF8로 제한된다.

다음은 힙헤 할당된 메모리를 해제하는 데 사용하는 함수이다. 메모리가 해제되는 과정에서 페이지는 다시 RESERVE 상태가 될 수 있다.
BOOL HeapFree(

HANDLE hHeap : 해제할 메모리를 담고 있는 힙을 지정한다.
DWORD dwFlags : HEAP_NOSERIALIZE가 인자로 올 수 있다. HeapCreate 함수 호출 과정에서 이미 HEAP_NO_SERIALIZE를 전달하였다면 이 전달인자를 통해서 중복 지정할 필요는 없다.
LPVOID lpMEM : 해제할 메모리의 시작 주소를 지정한다.

03 . MMF

-MMF의 이해

MMF는 Memory Mapped File의 약자로서 File을 Memory에 Mapping 시킨다는 의미를 지니고 있다. 다음 그림은 프로세스의 가상 메모리 일부가 파일의 일부 영역에 연결되어 있는 상황을 보여준다. 이렇게 파일의 일부 영역을 가상 메모리 일부에 연결시키는 메커니즘을 가리켜 MMF라 한다.

MMF를 사용하면 다음과 같은 특성을 얻게 된다.이는 MMF에서 말하는 '연결'이라는 단어가 의미하는 바이기도 하다.

가상 메모리 중 파일에 연결되어 있는 영역에 데이터를 저장한다. 이렇게 메모리에 저장된 데이터는 실제 파일에도 효과를 미친다. 즉 메모리에만 데이터가 저장되는 것이 아니라 메모리에 연결된 파일에 실제 데이터가 저장되는 것이다.

[장점 1 : 프로그래밍하기 편하다]
개발자 대부분이 파일에 저장된 데이터를 조작(알고리즘이나 기타 여러 가지 이유로 인해 데이터를 변경하는 행위)하는 것보다, 메모리상에 저장된 데이터 조작으 훨씬 좋아한다. 이는 너무나도 당연하다. 파일 안에 저장되어 있는 데이터를 조작하려면 일단 메모리로 읽어 들여야 한다. 그리고 조작 후에 다시 파일에 저장하는 과정을 거쳐야 한다. 이 과정이 너무나 번거롭다. 파일에 저장된 데이터를 조작하는 일은 메모리상에 저장된 데이터 조작에 비해서 많이 불편하다. 그런데 MMF를 사용하면 메모리상에 저장된 데이터를 조작하는 방식으로 파일 내 데이터를 조작할 수 있다. 엄청난 편리성이 제공된다.

[장점 2 : 성능이 향상된다]

메모리는 파일데이터의 캐쉬 역할을 한다.

-MMF의 구현과정

[MMF의 구현과정 1단계 : 파일 개방] :파일의 핸들을 얻어야만 한다.

[MMF의 구현과정 2단계 : 파일 연결 오브젝트 생성] : 파일 핸들을 가지고 메모리에 연결할 파일 정보를 담고 있는 커널 오브젝트를 생성

[MMF의 구현과정 3단계 : 가상 메모리에 파일 연결] : MapViewOfFile 함수를 호출해 연결 이때 반환되는 포인터를 가지고 메모리에 접근

-MMF의 구현 함수

2단계에 해당하는 파일 연결 오브젝트 생성을 위한 함수

위 함수 호출 성공 시 파일 연결 오브젝트의 핸들을 반환한다. 이 핸들을 이용해서 다음 함수를 호출하면 실제 메모리로의 연결이 완성된다 MMF 3단계에 해당한다.

위 함수 호출 성공 시 void형 포인터가 반환된다. 이 포인터를 용도에 맞게 형 변환하여 사용하면된다. malloc 함수 호출 시 얻게 되는 포인터를 사용하는 방법과 동일하다.

이렇게 파일과 연결된 메모리는 작업이 끝나고 나면, 연결 해제 과정을 거쳐야 한다.

  • Copy-On-Write

데이터를 쓸 때 복사를 하라. 최적화 기술임

모든 쓰레드들이 하나의 기본 테이블을 공유하도록 하고. 다만 테이블의 데이터를 변경하고자 하는 쓰레드가 등장하면, 기본 테이블을 복사해서 해당 쓰레드에게 할당한 다음, 복사본을 변경하게 한다. 물론 그 이후부터는 복사본에 해당하는 테이블을 참조한다.

이것만은 알고 갑시다.

  1. 가상 메모리의 페이지 상태
    가상 메모리 페이지의 상태 RESERVE,COMMIT가 있다.

  2. 동적 힙 생성

  3. MMF는 파일의 일부 영역을 가상 메모리 일부에 연결시키는 메커니즘을 가리켜 MMF라 한다.

0개의 댓글