반응형


http://blog.naver.com/ultimidou/130095833416

http://blog.naver.com/PostView.nhn?blogId=kimgudtjr&logNo=140116


메모리 - 가상 메모리 할당 함수, VirtualAlloc, 예약, 확정, 보호속성, 대용량 메모리 할당

 
[ C 런타임 함수 ]

 

응용 프로그램에서 메모리가 필요할 경우 운영체제에게 메모리 할당을 요청한다. 운영체제는 원칙적으로

응용 프로그램의 메모리 할당 요청을 거절하지 않으며 한 번 할당한 메모리는 해제하기 전에는

다른 할당 요청에 사용되지 않는다. Win32에서 메모를 할당하는 바업에는 여러 가지가 있는데 가장

간단한 방법이 C 런타임 함수를 사용하는 것이다. 4G 평면 메모리 모델의 간단한 구조 덕분에

malloc, free 두 함수만 사용해도 기본적인 메모리 할당은 할 수 있다 원형은 다음과 같다.

 void *malloc(size_t size);
 void free(void *membolck);

malloc의 인수로 할당하고자 하는 메모리의 크기를 바이트 단위로 밝히기만 하면 된다. 운영체제는

물리적인 메모리를 할당하여 가상 주소 공간에 맵한 후 그 번지를 리턴한다. 이 때 리턴되는 값은

void 포인터 형이므로 반드시 원하는 타입으로 캐스팅해야 한다. 만약 메모리 부족으로 할당이 되지

않을 경우는 에러에 해당하는 NULL을 리턴한다. win16에서는 malloc의 리턴 값을 반드시 점검해 보아야

했으니나 Win32에서는 메모리 할당이 실패하는 경우가 극히 드물기 때문에 종종 리턴값 점검을 생략한다.

(그렇다고 해서 생략하는 것이 반드시 좋다는 뜻은 아니다.) malloc 으로 할당한 메모리를 다시 사용 후

free로 해제한다.

 

------------------------------------------------------------------------
메모리 할당 해제 예제 생략... (너무 쉬우므로..)
------------------------------------------------------------------------

 

참고로 잘 사용되지는 않지만 malloc와 유사한 다음과 같은 할당 함수들이 있따.

void *calloc(size_t num, size_t size);
void *realloc(void *memblock, size_t size);

calloc은 size크기의 변수값 num개분에 해당하는 메모리를 할당한다. malloc(size*num) 과 동일하되

다만 필요한 메모리 양을 좀더 논리적으로 나타낸다는 점만 다르다.  realloc은 이미 할당된 메모리의

크기를 변경하여 재할당하는 함수이다. 이미 할당한 메모리르 더 크게 할당하거나 더 작게 축소하고자 할 때

realloc 함수를 사용한다. 확장시는 연속된 공간에 재할당하기 위해 메모리의 위치가 변경될 수도 있다.

malloc, free 함수는 Win16 에서는 물론이고 그 이전의 도스 프로그래밍에서도 사용하던 아주 오래된 함수이다.

malloc은 절대 번지를 할당하기 때문에 (Win16 환경에서는 절대번지 할당) 운영체제가 메모리르 이동하거나

스왑할 수 없는 몇 가지 잠재적인 문제가 있어 Win16 환경에서는 잘 사용되지 않았다. 그러나

가상 주소 공간을 지원하는 Win32 환경에서는 다시 이 함수들이 사용되는데 Win32의 메모리는 원칙적으로

이동 가능(Moveable)하며 언제든 스왑(Descardable) 할 수 있기 때문이다.

운영체제가 필요에 의해 가상 메모리상의 위치를 옮기더라도 페이지 테이블을 같이 수정하면 응용 프로그램은

자신이 알고 있는 포인터로 계속 그 메모리를 사용할 수 있다. 가상 쉽고 또 무난하므로 특별한 기능 제한

(메모리 보호, 엑세스 지정 등) 없이 메모리 할당 자체가 목적이라면 이 함수만 사용해도 무방하다.

C++의 객체를 동적으로 할당할 때는 new연산자를 사용한다. new 연산자를 피연산자로 주어진  클래스 형

객체만큼의 메모리를 할당할 뿐만 아니라 해당 클래스의 생성자를 호출하여 객체를 초기화하기까지 한다.

그리고 해당 객체의 포인터를 리턴한다. 이렇게 동적으로 생성된 객체는 delete 연산자로 파괴하는데

다음이 간단한 사용 예이다.


 MyClass *pObj;
 pObj = new MyClass(1,2);
 // pObj를 사용한다.
 delete pObj;

하나의 객체뿐만 아니라 객체 배열을 할당할 수도 있다. 객체 위주로 프로그램을 작성할 때는 주로

이 연산자를 사용하여 메모리르 할당한다. C++ 언어가 제공하는 연산자이므로 자세한 문법에 대해서는

C++ 문법서를 참고하기 바란다.


================================================================================
[ 가상 메모리 할당함수 ]

 

응용 프로그램이 필요로 하는 메모리르 할당할 때는 앞에서  살펴본 malloc 함수 , new 연산자만 써도

충분하다. Win32에서 추가된 가상 메모리 할당 함수들은 전통적인 이런 함수들에 비해 몇 가지 추가적인

이점들을 제공하며 메모리에 대한 좀 더 섬세한 통제를 할 수 있다. 단순히 할당해서 사용할 목적이라면

malloc 함수를 쓰고 가상 메모리 구조의 이점을 활용하고 싶다면 가상 메모리 함수를 사용하면 된다.

가상 메모리 함수가 malloc 함수에 비해 가지는 이점은 다음 두 가지이다.


------------------------------------------------------------------------------------
○ 메모리를 예약 상태로 할당할 수 있다. 예약이란 물리적인 메모리를 소비하지 않으면서 주소 공간만을

미리 할당해 놓는 방법이다. 이렇게 예약된 페이지는 필요할 때 언제든지 필요한 부분만 확정해서

사용할 수 있으므로 realloc 회수를 줄일 수 있다.

○ 할당한 메모리의 엑세스 권한을 지정할 수 있다. malloc으로 할당한 메모리는 언제나 읽기/쓰기가

가능하지만 가상 메모리 함수로 할당한 메모리는 읽기 전욕, 액세스 금지 속성을 가질 수 있어

실수로 인한 데이터 파괴를 막을 수 있다.
------------------------------------------------------------------------------------

 

이 외에도 여러 가지 차이가 있지만 대부분이 이 두 가지 차이점으로 인해 파생되는 차이점이다.

가상 메모리 할당 함수는 일단 다음 두 가지가 있다. 할당 할때는 VirutalAlloc 함수를 사용하고

해제할 때는 VirtualFree 함수를 사용한다.


LPVOID VirtualAlloc(LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect);

BOOL VirtualFree(LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType);

첫 번째 인수 lpAddress는 할당하고자 하는 메모리의 절대 번지를 지정하되 NULL이면 시스템이 알아서

할당 번지를 지정한다. 동적으로 메모리를 할당할 때 할당 위치는 별 의미가 없으므로 보통 NULL을 주되

예약된 페이지를 확장할 때는 예약되어 있는 번지를 지정해야 한다. 두 번째 인수 dwSize는 할당하고자

하는 메모리의 양을 바이트 단위로 지정한다. 세 번째 인수 flAllocationType은

할당 방법을 지정한다.

 

------------------------------------------------------------------------------------
할당 방법  설명
------------------------------------------------------------------------------------
MEM_RESERVE 물리적인 메모리의 할당없이 주소 공간만을 예약한다.

MEM_COMMIT 물리적인 메모리를 확정한다.

MEM_TOPDOWN 가급적 높은 번지에 메모리를 할당한다. NT이상에서만 쓸 수 있다.
------------------------------------------------------------------------------------

 

네 번째 인수 flProtect는 할당한 페이지의 액세스 타입을 지정하며 보통 PAGE_READWRITE로 지정한다.

이에 대해서는 잠시 후에 자세하게 알아볼 것이다. 메모리 할당에 성공하면 할당된 메모리의 번지를

리턴하며 실패했을 경우 NULL을 리턴한다.


VirtualFree 함수는 할당된 페이지를 해제한다. 첫 번째 인수 lpAddress는 해제하고자 하는 메모리의

선두 번지를 지정하며 두 번째 인수 dwSize는 해제하고자 하는 메모리의 크기를 지정한다.

세 번째인수 dwFreeType은 다음 두 값중 하나이되 둘을 같이 쓸 수는 없으며 반드시 따로 사용해야 한다.

만약 확정된 메모리르 해제하려면 확장 해제 후 예약 해제해야 한다.


------------------------------------------------------------------------------------
값  설명
------------------------------------------------------------------------------------
MEM_DECOMMIT 확정된 페이지를 확정 해제한다.

MEM_RELEASE 예약된 페이지를 예약 해제한다.
------------------------------------------------------------------------------------

다음은 가상 메모리 할당 함수를 사용하는 간단한 예이다.

------------------------------------------------------------------------------------
ptr = (int*)VirtualAlloc(NULL, sizeof(int)*10, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);

VirtualFree(ptr,sizeof(int)*10,MEM_DECOMMIT);
VirtualFree(ptr,0,MEM_RELEASE);

------------------------------------------------------------------------------------


================================================================================

[ 예약과 확정 ]

 

Win32 프로세스가 가지는 4G의 가상 주소공간은 "페이지"라는 단위로 구성된다.  한 페이지의 크기는

시스템 마다 다른데 가장 많이 사용되는 인텔 계열의 CPU에서는 4K 바이트의 크기를 가진다.

윈도우즈는 페이지 단위로 주소 공간을 관리한다.  즉 할당하거나 해제하는 단위가 페이지 단위라는

뜻이다. 주소 공간을 구성하는 각 페이지는 다음 세 가지 상태 중 하나의 상태로 존재한다.


------------------------------------------------------------------------------------
상태  설명
------------------------------------------------------------------------------------
자유 영역(Free)  - 사용되지 않는 자유 영역이다. 언제든지 예약하거나 확정할 수 있다.

예약(Reserved)  - 장래 사용을 위해 예약만 되어 있는 페이지이며 물리적인 메모리가 할당되어 있지 않다.
  주소 공간만 할당되어 있는 상태이다.

확정(Committed) - 가상 메모리와 연결되어 있는 상태이며 바로 사용할 수 있다. 물리적 메모리를 소모한다.
------------------------------------------------------------------------------------

 

프로세스가 처음 실행되었을 때 부부분의 주소 공간은 자유 영역일 것이며 실행 파일의 이미지와 공유 DLL등이

확정되어 사용될 것이다. 자유 영역으로 남아있는 주소 공간은 언제든지 할당해 사용할 수 있는데

할당의 수준이 예약과 확정 두 종유가 있다. 예약이란 말 그대로 주소 공간만 할당하여 이 번지가

다른 목적으로 사용되지 않도록 하는 것이며 확정은 물리적인 메모리가 실제로 필요할 때에

RAM 또는 페이징 파일을 주소 공간에 연결(MAP)하는  것이다.

 Win16에는 없던 이런 예약과 확정이라는 것이 왜 필요하게 되었는가 하면 물리적인 메모리와

논리적인 주소공간이 분리되었기 때문이다. 논리적인 주소 공간을 할당하는 것이 예약이고 에약된 주소

공간에 물리적인 메모리르 연결하는 것이 확정이다. 주소 공간만을 할당하는 예약은 물리적인 메모리를

전혀 소모하지 않는다. 그래서 충분한 주소 공간을 미리 예약해 두어도 전혀 손해를 볼 것이 없다.

일단 예약된 주소 공간은 다른 할당 요청에 의해 다시 할당되지 않으므로 필요할 때마다 물리적인

메모리를 확정해서 사용하면 된다.

예를 들어 어떤 프로그램에서 10M바이트의 연속적인 메모리가 필요하다고 하자 그런데 당장은 이 메모리가

한꺼번에 사용되지 않지만 반드시 연속적인 메모리여야 한다면 일단 10M의 주소 공간을 예약해 둔다.

예약만 했으므로주소 공간만 할당되었을 뿐 물리적인 메모리는 전혀 소모하지 않았다. 그리고 필요할 때마다

원하는 위치의 주소 공간을 확정하여 물리적 메모리와 연결하여 사용하면 된다. 주소 공간이 연속되어

있으므로 예약된 주소와 연결되는 물리의 번지가 반드시 연속되지 않아도 아무 문제가 없다.

메모리를 예약할 것인가 확정할 것인가는 VirtualAlloc 함수의 세 번째 인수 flAllocationType으로

지정하는데 예약만 할 때는 MEM_RESERVE를 주고 예약된 메모리를 확정할 때는 MEM_COMMIT를 준다.

예약과 동시에 확정하려면 두 플래그를 OR로 묶어서 같이 지정한다. 예약과 확정을 따로 하고 싶다면

다음과 같이 두 번 호출한다.

ptr = (int*)VirtualAlloc(NULL,sizeof(int)*10,MEM_RESERVE,PAGE_READWRITE);
ptr = (int*)VirutalAlloc(NULL,sizeof(int)*10,MEM_COMMIT,PAGE_READWRITE);

예약에 의해 주소 공간이 임의의 번지에 먼저 할당되고 확정에 의해 이 주소 공간이 가상 메모리에

맵된다. 예약만 하고 확정은 하지 않은 상태는 주소 공간만 할당되어 있고 물리적인 메모리와 맵되어

있지 않은 상태이기 때문에 실제 메모리 구실을 할수 없다. 따라서 다음과 같은 코드는 AccessViolation

예외를 발생시킨다.

ptr = (int*)VirtualAlloc(NULL,sizeof(int)*10,MEM_RESERVE,PAGE_READWRITE);

ptr[0] = 'S';

메모리를 예약만 했는데 이 때 예약된 번지수가 리턴되기는 하지만 이 번지는 가상 주소 공간의

한 지점일 뿐 실제 물리적인 메모리와 연결되지 않았다. 그러므로이 번지에 무엇인가 값을 대입하는 것은

불법이며 읽기만 해도 불법이다.


================================================================================

[ 할당 단위와 페이지 ]

 

VirtualAlloc 함수가 메모리를 할당할 때는 바이트 단위를 사용하지 않는다. 4G나 되는 주소공간을

바이트 단위로 사용하는 것은 너무 비효율적이기 때문에 일정한 단위로 주소 공간을 분할한다. 마치

하드딧크가 바이트 단위로 파일을 기록하지 않고 섹터, 클러스터 단위를 사용하는 것처럼 말이다.

클러스터 단위로 파일을 기록하면 낭비되는 디스크 공간이 생기지만 속도는 훨씬 더 빨라진다.

가상 주소 공간의 단위는 두 가지가 있다. 우선 할당의 시작점을 지정하는 할당 단위(Allocation Granualrity)가

있고 할당의 크기를 지정하는 페이지(Page)가 있다. 이런 단위를 사용하여 메모리를 관리하는 이유는

메모리가 지나치게 조각나는 것을 방지하고 좀 더 신속하게 메모리를 관리하기 위해서이다. 물론 바이트

단위로 메모리를 관리하는 것이 효율 면에서는 이상적이지만 이렇게 하면 페이지 테이블이 지나치게

커질 것이며 운영체제는 너무 너무 바빠질 것이다.

VirtualAlloc으로 메모리를 할당(예약하거나 확정)할 때 그 시작점은 반드시 할당 단위의 경계선에

정렬된다. 즉 할당 단위의 배수 위치에서 할당이 시작된다. 대부분의 플랫폼에서 할당 단위는 64K이므로

가상 메모리 공간은 64K 단위로 할당된다고 할 수 있다.  예를 들어 다음과 같이 예약 명령을

내렸다고 하자.

ptr = (int*)VirtualAlloc(0x71234, sizeof(int)*10, MEM_RESERVE, PAGE_READWRITE);

예약의 시작점을 0x71234번지로 강제로 지정하였다. 그러나 운영체제는 정확하게 이 번지에서 할당을

시작하지 않고 할당 단위의 배수가 되도록 번지를 내림하여 할당한다. 이 경우 실제 예약되는 번지는

0x70000번지가 된다. 하위 2바이트를 0으로 만든 번지에서 할당된다고 생각하면 된다.

또 할당된 영역의 크기는 반드시 페이지 단위의 배수가 된다. 페이지의 크기는 플랫폼에 따라서 다른데

인텔을 비록한 대부분의 시스템에서 페이지 크기는 4K바이트이다. 예를 들어 10K의 크기만큼 할당을

요청했다면 실제로 할당되는 영역의 크기는 12K가 될 것이다. 현재 플랫폼에서 할당 단위와 페이지 크기를

조사하고 싶으면 GetSystemInfo 함수를 사용하면 된다. 다음은 이 함수를 사용하여 할당 단위와 페이지

크기를 조사하는 간단한 예제이다.

<GetSystemInfo 함수사용 예> (독자는 콘솔어플리케이션으로 했음..)


---------------------------------------------------------

#include <stdio.h>
#include <windows.h>

int main()
{

 SYSTEM_INFO si = {0};
 
 GetSystemInfo(&si);

 printf("Page Size : %d\n",si.dwPageSize);
 printf("Min Addr : %d\n",(int)si.lpMinimumApplicationAddress);
 printf("Max Addr : %d\n",(int)si.lpMaximumApplicationAddress);
 printf("Alloc Gra : %d\n",si.dwAllocationGranularity);
 

 return 0;
}
---------------------------------------------------------


할당 단위는 Wion32가 실행되는 현재까지의 모든 플랫폼에서 64K이며 페이지 크기는 Alpha 시스템만

8K이고 그 외의 CPU(Intel, MIPs, PowerPC)에서는 모두 4K이다.  모든 플랫폼에서 이상없이 돌아가는

프로그램을 만들려면 GetSystemInfo 함수로 이 값들을 잘 조사해 보아야 하나 내부적인 메모리 관리에만

사용되므로 응용 프로그램 수준에서 실제로 이 정보가 유용한 경우는 별로 없다.

 

================================================================================

[ 보호 속성 ]

 

VirtualAlloc의 네 번째 인수 flProtect는 할당하고자 하는 메모리의 액세스 타입(Access Protection)을

지정한다. Win32 가상 메모리의 특징 중 하나가 메모리 페이지마다 액세스 타입을 설정하여 미연의

실수를 방지할 수 있는 점이다. 마치 파일에 읽기 전용 속성을 주어 실수로 지워지지 않도록 하는 것과

개념적으로 동일하다 지정 가능한 플래그는 다음과 같다.   이 중 PAGE_GUARD, PAGE_NOCACHE 플래그는

PAGE_NOACCESS 플래그를 제와한 다른 플래그와 함께 사용할 수도 있다.


< AirtualAlloc의 네 번째 인자에 줄 수 있는 옵션 값 >
---------------------------------------------------------------------
액세스 권한  설명
---------------------------------------------------------------------
PAGE_READONLY  읽기만 가능하다. 이 메모리는 쓰기를 할 수 없다.
PAGE_READWRITE  읽기 쓰기 가능하다.
PAGE_EXECUTE  실행만 가능하다. 읽기, 쓰기 모두 할 수 없다.
PAGE_EXECUTE_READ 실행, 읽기, 쓰기를 가능하다.
PAGE_GUARD  보호 페이지로 지정한다. 이 페이지에 읽기 , 쓰기를 시도하면
   STATUS_GUARD_PAGE 예외가 발생하며 보호 페이지 상태가 해제
   된다. 보호 페이지는 메모리의 끝을 표시하는 용도로 주로 사용된다.
   NT 이상만 지원한다.
PAGE_NOACCESS  어떤 액세스도 하지 못하게 한다. 읽기, 쓰기 , 실행 어떤 것도 하지 못하게 한다.
PAGE_NOCACHE  캐시를 금지시킨다. 일반적으로 응용 프로그램은 이 플래그를 사용하지 않는 
   것이 좋다. 디바이스 드라이버 등의 시스템 소프으웨어에서 이 플래그를
   사용한다.
PAGE_EXECUTE_WRITECOPY 공유된 영역에 쓰기를 할 때 사본을 작성한다. NT이상만 지원한다. 
---------------------------------------------------------------------


일반적인 용도로 읽기도 하고 쓰기도 할 메모리라면 PAGE_READWRITE 로 지정하여 마음대로 읽고 쓰도록

하면 된다. 그러나 특정 목적으로 읽기만 하고 쓰지는 못하게 하려면 PAGE_READONLY 플래그를 지정한다.

이렇게 할당된 메모리는 읽을 수는 있어도 쓸 수는 없다. 만약 읽기 전용의 메모리에 쓰기를 시도하면

Access Violation 예외가 발생한다


ex) 
ptr = (int*) VirtualAlloc(NULL,sizeof(int)*10, MEM_RESERVE | MEM_COMMIT, PAGE_READONLY);

int i = ptr[0];  // 읽기 가능
ptr[0] = 'x'; //쓰기 불가능

읽기 전용, 쓰기 전용의 말은 다 이해하겠는데 실행 전용 이라는 말은 선뜻 이해가 되지 않을 것이다.

읽기는 해당 메모리의 데이터를 읽을 수 있다는 말이며 실행은 해당 메모리의 코드를 CPU가

실행할 수 있다는 뜻이다. 현재까지의 모든 시스템(인텔,알파)은 읽을 수 있으면 실행도 가능하므로

사실 아직까지 읽기 전용은 같은 뜻이다. Win32가 플랫폼에 독립적인 API 이다 보니 미래에 읽기와

실행을 구분하는 CPU를 위해 이런 액세스 타입을 미리 준비해두고 있을 분이다. 메모리의

엑세스 타입은 할당할 때 지정하지만 실행중에도 다음 함수를 사용하여 변경할 수 있다.

하지만 응용 프로그램 수준에서 메모리의 액세스 타입을 그거솓 실행중에 변경하는 경우는 지극히 드물며

예를 들기도 무척 곤란하므로 이런 함수도 있다는 것만 알아두자. 꼭 예를 든다면 아주 중요하나 데이터를

특정 함수만 변경하고 싶을 때 읽기 전요응로 만들어 두고 이 함수에서만 액세스 타입을 잠시  바꾸는

정도가 있다. 시스템의 함수를 훅킹하는 고난이도의 기법을 구사할 때 이 함수를 슨다.

가상 메모리의 보호 속성은 프로그램 로더가 사용한다. 응용 프로그램을 메모리로 읽어올 때 실행파일(PE)에

기록되어 있는 섹션 속성에 따라 쓰기가 가능한 메모리 영역과 읽을 수만 있는 메로리 영역이 구분되어

가상 메모리에 배치된다. 일반적으로 코드영역은 읽기 전용이며 번역변수 영역은 읽고 쓸 수 있는

영역이다. 하지만 문자열 상수는 읽기 전용 영역에 배치되므로 이 값은 실행중에 변경할 수 없다.


다음 예제를 보자

------------------------------------------------------------------
#include <stdio.h>


char *str = "string";
int i = 0;

int main()
{
 
 str[0] = 'A'; // 메모리 액세스 에러

 i = 10;  // 메모리 액세스 성공

 

 return 0;
}
------------------------------------------------------------------

 

위에서 str[0] = 'A'는 에러가 날것이고 i = 10은 에러가 나지 않을 것이다.

이것이 문자열 상수 영역과 정 전역변수 영역의 메모리 접근 권한의차이 이다.


16비트의 메모리는 이런 구분이 없기 때문에 우발적인 사고로 코드 영역을 건드릴 수도 있었는데

32비트의 가상 메모리는 메모리에 속성을 부여함으로써 변경되지 말아야 할 영역을 보호 할 수 있다.


PAGE_GUARD 속성은 가드 페이지를 설정함으로써 효율적인 스택 확장에 사용된다.

스택은 기본적으로 1M 예약되며 그 중 한 페이지 분량이 4K 정도만 확정된 채로 생성된다.

예약된 1M는 스택의 최대 크기이며  확정된 페이지는 당장 사용할 수 있는 영역인데 스택을 많이

사용하면 확정을 점점 늘린다. 이때 스택을 언제 확장할지를 결정하기 위해 운영체제가 사용하는

속성이 PAGE_GUARD이다. 스택이 확장되는 과정은 아래와 같다.


 ---------------------------------
 예약| 예약 | 예약 | 가드 | 확정   ( 확장 전)
 ---------------------------------
 낮은번지   높은번지

 ---------------------------------
 예약| 예약 | 가드| 확정 | 확정   ( 확장 후)
 ---------------------------------
 낮은번지   높은번지

 격자 하나가 1페이지 이다.


최초 제일 높은 번지의 한 페이지만 확정되어 있으며 이 안에서 함수를 호출하고 지역 변수를 생성한다.

그러다가 스택 사용량이 늘어 가드 페이지에 닿으면 예외가 발생하는데 운영체제는 이 예외가 발생했을 때

가드 페이지를 추가로 확정하고 바로 다음의 예약 페이지를 다시 가드 페이지로 설정한다.

만약 가드 페이지가 없다면 스택을 액세스할 때마다 확정 여부를 점검해야 하므로 무척 비효율적일 것이다.


이런 식으로 스택은 점차적으로 확장되는데 단, 마지막 페이지를 확정하지 않으므로써 예약된 1M 영역을

절대로 넘지는 않는다. 만약 스택 크기다 !M를 넘어서 오버플로우가 발생했다면 이는 대개의 경우

프로그램의 논리가 잘못된 것이다. 스택은 함수를 호출함에 따라 오르락 내리락거리는 것인데

이 정도로 커졌다면 재귀 호출이 잘못된 경우이므로 예외를 일으키고 죽도록 내버려 두는 편이 더 낫다.

참고로 95/98 에서는 PAGE_GUARD 속성이 지원되지 않기 대문에 PAGE_NOACCESS 속성이 대신 사용된다.


================================================================================

[ 메모리 잠금 ]

 

가상 주소 공간은 논리적으로 존재할 뿐이며 데이터나 코드가 저장되는 곳은 가상 메모리 이다.

여기서 가상 메모리라고 칭하는 것은 RAM과 하드 디스크의 페이징 파일을 합쳐 부르는 말이다.

페이징 파일도 RAM보다 좀 느릴 뿐이지 분명히 메모리이다. 운영체제는 RAM과 페이징 파일 사이를

끊임없이 교체하면서 돌아간다. 당장 필요한 부분은 RAM으로 읽혀지며 당분간 사용되지 않을 부분은

페이징 파일로 이동된다.

운영체제의 이런 가상 메모리 관리는 응용 프록램 입장에서 볼 때 완전히 투명하다. 즉 원하는 데이터가

RAM에만 있도록 할 수 있다. 즉 페이징 파일로 복사하지 못하게 금지할 수 있는데 이렇게 하면 원하는 데이터를

물리 RAM에서 바로 찾을 수 있으므로 속도가 더 발라진다. 이 때는 다음 두 함수를 사용한다.


BOOL VirtualLock(LPVOID lpAddress, DWORD dwSize);
BOOL VirtualUnlock(LPVOID lpAddress, DWORD dwSize);

VirtualLock 함수는 lpAddress가 지정한 번지로부터 dwSize 길이 만큼의 메모리 페이지를 잠근다.

이렇게 되면 운영체제는 이 번지의 데이터는 페이징 파일로 보내지 않고 항상 RAM에 남아 있도록 하여

최대한 신속하게 사용할 수 있도록 보장한다. 이때 lpAddress 번지는 반드시 가상 메모리가 맵되어 있는

확정 상태여야만 한다. 그렇지 않으면 잠금 동작 자체가 무의미하다. 장금을 풀 때는 VirtualUnlock 함수를

사용한다.

잠겨진 메모리에 대해서는 페이징 파일로 스왑하지 않아 원하는 데이터가 RAM에 없는 상태(Page Fault) 가

발생하지 않는다. 단 예외적으로 해당 프로세스가 액티브 상태가 아니면 이 때는 잠간 페이지라도

페이징 파일로 이동시켜버릴 수 있다. 메모리를 잠그면 응용 프로그램은 자신이 필요로 하느 데이터를 언제나

RAM에서 읽으므로 더 빨리 실행될 수 있어서 좋겠지만 이 기능은 반드시 꼭 필요한 부분에만 신중하게

사용해야 한다. 그렇지 않고 남발하게 되면 운영체제의 시스템 관리 능력을 저해하여 자칫하면 전반적으로

속도가 느려질 수 있다.

Win32의 메모리 관리 알고리즘은 상당히 정교하며 효율적으로 작서오디어 있으므로 굳이 메모리를

잠그지 않아도 큰 불편 없이 쓸 수 있다. 메모리 잠금을 반드시 사용해야 하는 프로그램은 디바이스 드라이버

정도이다. 응용 프로그램 수준에서는 거의 쓸 일이 없으며 함부로 메모리 관리에 개입해서는 안된다.

멀티 태스킹은 운영체제의 지휘 아래응용 프로그램들의 자발적인 협조에 의해 부드럽게 돌아가는 것인데

특별히 이유없이 혼자서 자원과 시간을 독점하는 것은 금물이다.


================================================================================

[ 대용량 메모리 ]


가상 메모리를 제대로 사용하는 예제를 만들어 보자. 만약 어떤 프로그램이 실행중에 데이터를 읽어들이는데

다 읽기 전에는 정확한 용량을 알 수 없으며 최대 100M까지의 용량이 필요하다고 해 보자.

이 경우 필요한 최대값인 100M을 몽땅 할당해 놓고 작업을 시작한다면 시스템의 메모리 실장량에 따라

메모리 할당이 실패할 수도 있다. 실제 필요한 용량은 그보다 훨씬 작은데 그렇다고 해서

최대값을 무시할 수도없고, 이럴 때 바로 가상 메모리의 예약과 확정 기능을 사용한다. 다음 예제는

그 예를 보여 부는데 작업 관리자를 열어 놓고 메모리의 변화를 잘 관찰해 보아라.

 

<가상 메모리 할당함수 사용>
------------------------------------------------------------------------------------

#include <windows.h>
#include <windows.h>

#define MEGA 1048576
PBYTE stptr = NULL;
TCHAR Status[256] = TEXT("할당되지 않았습니다.");


LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
HWND hWndMain;
LPCTSTR lpszClass = TEXT("First");  //윈도우 이름 및 타이틀바에 등록할 문자열

void FreeRecords();
void ReadRecords();

void Error(const char *mes);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow)
{
 HWND hWnd;
 MSG Message;
 WNDCLASS WndClass;
 
 g_hInst = hInstance;

 //------------ 아래 부분은 윈도우 클래스를 설정해주는 것이다. --------------------

 WndClass.cbClsExtra = 0;
 WndClass.cbWndExtra = 0;
 WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
 WndClass.hCursor = LoadCursor(NULL,IDC_ARROW);
 WndClass.hIcon = LoadIcon(NULL,IDI_APPLICATION);
 WndClass.hInstance = hInstance;
 WndClass.lpfnWndProc = WndProc;
 WndClass.lpszClassName = lpszClass;
 WndClass.lpszMenuName = NULL;
 WndClass.style = CS_HREDRAW | CS_VREDRAW;

 //------------ 위 부분은 윈도우 클래스를 설정해주는 것이다. --------------------

 RegisterClass(&WndClass);   //  <-- 여기서는 위에서 설정한 클래스를 등록한다.

 hWnd = CreateWindow(lpszClass,lpszClass,WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
      CW_USEDEFAULT,NULL,(HMENU)NULL,hInstance,NULL);   // 설정하고 등록한 윈도우를 생성한다.

 ShowWindow(hWnd,nCmdShow);   //생성한 윈도우를 출력..(이 함수를 호출하지않으면 윈도우가 보이지 않는다.)

 while(GetMessage(&Message,NULL,0,0))   //사용자가 종료하기 전까지 반복해서 메세지 처리를 호출한다.
 {
  TranslateMessage(&Message);
  DispatchMessage(&Message);
 }

  return (int)Message.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) //여기서 실제로 메시지를 처리한다.
{
 HDC hdc;
 PAINTSTRUCT ps;
 TCHAR *Mes = TEXT("왼쪽 마우스 버튼 : 메모리 할당, 오른쪽 마우스 버튼:메모리 해제");

 switch(iMessage)
 { 
 case WM_CREATE:
  hWndMain = hWnd;
 srand(GetTickCount());
  return 0;

 case WM_LBUTTONDOWN:
  SetCursor(LoadCursor(NULL,IDC_WAIT));
 ReadRecords();
 SetCursor(LoadCursor(NULL,IDC_ARROW));
  return 0;

 case WM_RBUTTONDOWN:
  FreeRecords();
  return 0;

 case WM_PAINT:
  hdc = BeginPaint(hWnd,&ps);
  TextOut(hdc,50,50,Mes,lstrlen(Mes));
  TextOut(hdc,50,80,Status,lstrlen(Status));
  EndPaint(hWnd,&ps);
  return 0;

 case WM_DESTROY:
  PostQuitMessage(0);
  return 0;
 }

 return DefWindowProc(hWnd,iMessage,wParam,lParam);  //프로그래머가 처리하지 않은 나머지 동작을 기본처리로 넘긴다.
}


void FreeRecords()
{
 if(stptr != NULL)
 {
  if(VirtualFree(stptr,(100 * MEGA),MEM_DECOMMIT) == 0)
    Error("메모리 확정 해제 실패");

  if(VirtualFree(stptr,0,MEM_RELEASE) == 0)
    Error("메모리 예약 해제 실패");

  stptr = NULL;
 }

 wsprintf(Status,TEXT("할당되지 않았습니다."));
 InvalidateRect(hWndMain,NULL,TRUE);
}

void ReadRecords()
{
 int c = 0;
 int RecSize = 0;

 PBYTE nowptr, endptr;

 FreeRecords();

 stptr = (PBYTE)VirtualAlloc(NULL,(100 * MEGA),MEM_RESERVE,PAGE_READWRITE);

 if(stptr == NULL) Error("메모리 예약 실패");

 nowptr = stptr;
 endptr = stptr;

 c = rand() % 90 + 10;

 for(int i=0; i<c; i++)
 {
  if( (endptr - nowptr) < MEGA )
  {
   VirtualAlloc(endptr,MEGA,MEM_COMMIT,PAGE_READWRITE);
   endptr += MEGA;
  }

  RecSize = ((rand() % 100)+1)*10240;
  memset(nowptr,i,RecSize);
  nowptr += RecSize;
  
 }

 wsprintf(Status,TEXT("예약:100 메가, 확정:%d 메가, 사용%d 메가"),(endptr-stptr)/MEGA,(nowptr-stptr)/MEGA);
 InvalidateRect(hWndMain,NULL,TRUE);
}

void Error(const char *mes)
{
 MessageBoxA(0,mes,"error",0);
 exit(0);
}
------------------------------------------------------------------------------------


ReadRecords 함수에서 네트워크 또는 DB에서 레코드를 메모리로 읽어들이는데 레코드의 개수는 정해져

있지 않지만 최대 100까지 읽을 수도 있다. 또한 레코드 크기도 실제로 읽어봐야 알 수 있는데

작게는 10K 정도 되고 큰 레코드는 1M 정도 된다. 이 경우 필요한 메모리는 레코드 개수와 크기에 따라

100K~100M에 달하는데 문제는 실시간으로 읽어들이는 레코드이기 때문에 필요한 메모리 용량을

정확하게 알 수 없다는 것이다. 미래의 일을 예측할 수 없는 이런 상황이 항상 문제가 되는데

생각보다 자주 발생한다.

그렇다고 해서 최대 용량인 100M를 다 할당하는 것은 무리고 대충 충분한 용량인 50M 정도를 할당하는 것도

꺼림직하다. 이럴 경우 100M의 주소 공간을 예약만 해 놓고 필요할 때 그때 그때 메모리를 확장해서

사용하면 된다. ReadRecords 함수에서 stptr에 100M의 주소 공간을 예약하는데 예약만 했기 때문에

실제 메모리를 소모하는 것은 아니다. 그리고 루프를 돌면서 필요한 레코드들이 이 메모리로 읽어들이되

남은 메모리가 1M 미만이면 추가로 1M을 확장해서 사용한다. 이때 추가 확정되는 메모리가 반드시

기존의 영역과 연속적일 필요는 없는데 어차피 주소 공간에서는 연속적인 메모리로 인식되기 때문이다.

예제의 enptr은 할당된 메모리의 끝이며 nowptr은 읽어들인 레코드의 끝이다.  endptr에서 nowptr을

뺀 용량, 즉 현재 남은 용량이 다음 읽을 레코드의 최대 크기인 1M보다 작을 때 1M을 더 확정하여

endptr을 1M 더 뒤쪽으로 이동시킨다. endptr과 nowptr의 간격이 항상 1M 이상이 되도록 유지하는 것이다.

이런 식으로 메모리를 예약, 확정하면 실시간으로 필요한 연속적 메모리르 시스템에 무리를 주지 않고

할당해 사용할 수 있다. FeeRecords에서는 예약 및 확정된 메모리를 해제한다.

실행주으이 모습은 다음과 같다. (생략)

예약된 메모리 용량과 확정된 메모리 용량을 보여준다. 필요한 메모리 용량으 정확하게 계산하기

힘들 때는 이 방법대로 메모리를 할당해서 사용하면 된다. 다음은 똑같은 문제를 malloc, realloc 함수로

다시 작성해 본것이다. 동일한 동작을 하며 필요할 때마다 메모리를 재할당한다.

<C 메모리할당 함수 사용>


------------------------------------------------------------------------------------
#define MEGA 1048576
PBYTE stptr = NULL;

void FreeRecords()
{
 if(stptr != NULL)
 {
  free(stptr);
  stptr = NULL;
 }

 wsprintf(Status,"할당되지 않았습니다.");
 InvalidateRect(hWndMain,NULL,TRUE);
}

void ReadRecords()
{
 int i, c;
 int RecSize;
 int AllocSize;
 int RemainSize;
 
 FreeRecords();
 
 AllocSize = RemainSize = 2*MEGA;
 stptr = (PBYTE)malloc(AllocSize);

 if(stptr == NULL) Error("메모리 할당 실패");


 c = rand() % 91 + 10;

 for(i=0; i<c; i++)
 {
  if(RemainSize < MEGA)
  {
   AllocSize += MEGA;
   RemainSize += MEGA;
   stptr = (PBYTE)remalloc(stptr,AllocSize);
  }

  RecSize = ((rand() % 1000)+1)*10240;
  memset(arpre+AllocSize-RemainSize,i,RecSize);
  RemainSize -= RecSize;
 }

 wsprintf(Status,"총 할당량:%d 멕, 사용: %d 메가",
  AllocSize/MEGA,(AllocSize-RemainSize)/MEGA);

 InvalidateRect(hWndMain,NULL,TRUE);
}

------------------------------------------------------------------------------------


똑같은 동작을 하기는 하지만 realloc 함수가 굉장히 느리기 때문에 전체적으로 속도가 느리다는 큰

차이점이 있고 재할당할 때마다 번지가 바뀔 수 있다는 단점이 있다. 속도를 좀 높이려면 재할당할때

여유분을 충분히 주는 방법을 사용할 수는 있지만 가상 메모리를 쓰는 것보다는 확실히 성능이 떨어진다.

대용량의 가변적인 메모리를 다룰 때는 가상 메모리의 이점을 활용하는 것이 더 좋다.

가상 메모리는 예약 후 필요한만큼만 점진적으로 확정해 가며 쓸 수 있으므로 물리적인 메모리를

낭비하지 않으면서도 한번 예약한 번지가 바뀌지도 않아 쓰기 편리하고 속도도 빠르다.

단. 가상 메모리는 할당단위가 크므로 필요 메모리량이 클 때만 사용하는 것이 좋다. 연결 리스트의 노드처럼

작은메모리를 할당할 때는 realloc을 쓰는 방법이 더 바람직하다.

 

475366

반응형

+ Recent posts