'Win32'에 해당되는 글 2건

  1. 2006.12.27 TryEnterCriticalSection
  2. 2006.12.22 WIN32 메모리 관리


TryEnterCriticalSection

Development/C / C++ 2006. 12. 27. 14:50
크리티컬 섹션에 관련된 함수 중의 TryEnterCriticalSection은 EnterCriticalSection함수 대신에
사용할 수 있는 함수인데 이 녀석은 Windows NT이상에만 사용할 수 있기 때문에 사용하기 위해서는 다음과 같은 일을 해주어야 한다.

1. #include <windows.h>   ; TryEnterCriticalSection은 winbase.h에 선언되어 있다.
2. _WIN32_WINNT Define
 두 가지 방법이 있다. 첫번째는 직접 소스코드에 define 해주거나 프로젝트 설정에서 Define해준다.
  1) _WIN32_WINNT 은 windows.h를 include하기 전에 define해준다.

#ifdef WIN32
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0500
#endif
#endif

#include <windows.h>

 2) Project Property - C/C++ - Preprocessor
사용자 삽입 이미지
   
이 때 "_WIN32_WINNT=0x400" 에서 공백문자가 없도록!!

 
:

WIN32 메모리 관리

Development 2006. 12. 22. 16:01
출처 : http://www.dasomnetwork.com/~leedw/mywiki/moin.cgi

WIN32 메모리 관리
  1. Introduction
  2. Virtual Memory
  3. 구조
  4. 메모리 할당 알고리즘
  5. 관련 함수
    1. 가상 메모리 확보reserve
    2. 확보된 영역에서 특정 페이지를 commit
    3. 가상 메모리 해제free
  6. Memory-Mapped File
  7. Memory-Mapped File과 주소 공간
  8. Memory-Mapped File의 이용
    1. File Kernel Object 생성
    2. File-Mapping Kernel Object 생성
    3. File Data를 프로세스 주소 공간에 mapping
    4. Memory-Mapped File의 해제
  9. Heap
  10. 프로세스의 기본 Heap
  11. 추가적인 heap의 용도
  12. 추가적인 heap의 사용
    1. Heap 생성
    2. Heap으로부터 메모리 블럭 할당
    3. 블럭 크기 변경
    4. Heap 메모리 블럭 해제 및 Heap 제거
  13. Heap을 이용한 new, delete 구현
  14. References

1 Introduction

win32에서는 크게 3가지의 메모리 관리 메커니즘을 제공하고 있다. 이 페이지에서는 각각의 메모리 관리 메커니즘의 특성과 관련 함수, 이용 방법등을 알아보도록 한다.
  1. 가상 메모리(Virtual memory) : 객체들의 큰 배열을 관리할 때 유용(x86 플랫폼이라면 4K이상)
  2. 메모리 맵 파일(Memory-mapped file) : 파일등의 연속된 데이타를 관리하고 프로세스 간의 통신을 할 때 유용
  3. 힙(Heap) : 많은 수의 작은 객체들을 관리할 때 유용

2 Virtual Memory

win32의 프로세스1는 시스템에 생성될 때 4GB 크기의 독립적인 가상 메모리를 가지게 된다. 32비트 메모리 포인터로 0x0에서 시작하여 0xFFFFFFFF까지 4,294,967,296개의 값중 어느 것이나 취할 수 있는데 이것이 한 프로세스가 가지는 4GB의 메모리 공간이 된다. win32이전의 윈도우 3.x나 DOS는 각 프로세스가 독립된 메모리 공간을 갖지 않고 윈도우OS나 DLL등의 윈도우 공유파일, 그리고 일반 윈도우 어플리케이션이 하나의 공통된 메모리 공간을 사용했다. 이런 메모리 구조는 상당히 취약해서 윈도우OS 내의 모든 프로그램들은 이미 메모리에 올려진 다른 프로세스나 나중에 올려지게될 프로세스까지 고려해야 하며 모든 프로세스가 다른 프로세스에 의해 간섭받거나 방해받을 수 있었다. 윈도우OS가 사용하는 코드 및 데이타 영역과 일반 어플리케이션이 사용하는 코드 및 데이타 영역이 구분되지 않아 시스템이 불안정할 수 밖에 없었다. 그러나 win32로 넘어오면서 윈도우OS는 시스템에 의해 일반 프로그램의 접근이 허용되지 않고 각 프로세스마다 독립된 주소공간을 부여받기 때문에 훨씬 안정적으로 동작하게 된다. 물론 이론적으로... :)
자, 그럼 윈도우의 가상 메모리 구조와 관리가 어떻게 이루어져 있는지 살펴보자.

2.1 구조

위에서 모든 프로세스는 자신이 실제 소유하고 있는 메모리와 무관하게 4GB의 독립된 주소 공간을 갖는다고 말했다. 따라서 프로세스 A의 메모리 포인터 0x12345678은 프로세스 B의 메모리 포인터 0x12345678가 가리키는 주소와 무관하게 동작한다. 따라서 프로세스 A와 프로세스 B는 서로의 영역에 접근이 허용되지 않는다. 시스템에 생성되는 각 프로세스는 4GB의 메모리 공간이 주어진다고 했는데 현실적으로 4GB 이상의 메모리를 가지는 시스템이 흔치 않기 때문에 가상 메모리라는 기법을 사용하게 된다. 가상 메모리는 은행의 원리와 동일하다. 은행의 경우 모든 예금주가 한꺼번에 돈을 찾아가지 않는다는 가정 하에 실제 은행 자신이 보유하고 있는 현금보다 더 많은 금액을 고객에게 빌려줄 수 있다. 이와 마찬가지로 win32 시스템은 실제 자신이 보유한 실 메모리의 양보다 더 많은 메모리를 프로세스에게 빌려준다.
win32의 메모리 구조는 win98과 NT계열인 win2000이 약간씩 다른데 전체적인 구조는 다음과 같다.
Windows의 가상 메모리 구조
memorylayout.JPG
각 partition별 기능은 다음과 같다.
Null-pointer Assignment 이 지역은 프로그램상의 메모리 접근 오류를 잡아내기 위하여 할당된 영역이다. 어떤 프로그램이든 이 영역에 읽고 쓰기가 금지되어 있는데 이 지역에 read/write가 행해지면 시스템은 access viloation을 발생시킨다. 시스템으로부터 메모리를 정상적으로 할당 받지 못할 경우 NULL(0x0)을 리턴받게 되는데 바로 이런 NULL pointer assignment를 방지하기 위한 공간이다.
MS-DOS 16-bit Compatibility(win98 only) win9x 에만 존재하는 4MB의 공간으로 MS-DOS와 win16과의 하위 호환성을 유지하기 위한 공간이다. 원래 이 공간 또한 read/write가 금지되어야 하는데 MS사에서 기술적인 몇 가지 이유로 이 공간을 보호하지 못했다고 한다. (이유는 모름.. |) ) win2000에서는 DOS나 win16 어플리케이션을 자신의 유저 주소 공간에서 실행하기 때문이 이 영역이 없다.
User area 프 로세스의 사용자 공간이다. 다른 프로세스에 의해 간섭받지 않는 고유의 공간으로서 이 공간에 exe와 dll 모듈등 자신들만의 데이타를 담게된다. 특기할 만한 것은 win9x에서는 처음 4MB를 제외한 0x400000부터 0x7FFFFFFF인 반면, win NT는 0x10000부터 0x7FFEFFFF로 역시 처음 2GB에서 64KB를 제외한 부분이다. 이는 어떤 프로그램이 실행될 때 메모리 상의 베이스 주소가 다를 수 있다는 것이다. 때문에 프로그램을 만들 때 win NT는 win 9x와 호환성에 주의해야 한다. win NT에 맞추어 프로그램의 베이스 메모리를 0x10000로 맞추었다가는 win9x에서는 접근이 금지된 지역이므로 실행이 불가능하게 된다. (물론 요즘엔 win 9x가 사라져가는 추세라서 그리 큰 영향은 없겠지만...)
64KB Off-Limits(win2000 only) 0x7FFF0000부터 0x7FFFFFFF까지는 win NT에서 처음 64KB의 영역과 마찬가지로 메모리 경계(off-limit)를 나타내는 지역이다. 커널 영역의 접근을 방지하기 위한 일종의 완충지역이라고 보면 된다.
Shared Memory Mapped File(win98 only) win9x 에만 존재하는 이 1GB의 영역은 모든 프로세스들이 공통적으로 접근하고 공유할 수 있는 시스템 dll들과 MMF(memory-mapped files)가 적재되는 곳이다. 3대 시스템 dll이라 불리는 kernel32.dll, user32.dll, gdi32.dll에서 advapi32.dll도 이 영역에 로드된다. win NT에서는 이런 시스템 dll조차 비공유 메모리 영역에 따로 적재하여 사용한다.
Kernel area 마 지막으로 win9x에서는 1GB, win NT에서는 2GB로 예약되어 있는 커널 공간이다. 스케쥴러, 메모리 관리자, 파일 시스템 코드, 네트워크 코드등의 OS 코드와 디바이스 드라이버 레벨의 모듈이 적재되는 곳이다. 원칙적으로는 이 영역에 대한 read/write가 금지되어 있지만, win98에서는 이 영역에 대해 접근이 가능하다. 유저 어플리케이션에서 이 영역을 손상시킬 수 있다는 점 때문에 win98이 자주 다운되고 불안하다는 비난을 받게 된 이유이다.
win32 메모리는 region이라는 단위로 나뉘어져 있다. 메모리 전체를 우리나라 지도라고 봤을 때 region은 한 개의 도로 비유할 수 있다. 윈도우즈 메모리는 region들의 집합으로 이루어져 있고 x86기반의 PC에서는 한 개의 region은 64KB의 크기를 갖는다. 프로그램에서 메모리를 할당받을 때 User area 내에서 할당을 받게 되어 있는데 새로운 메모리 할당시 반드시 64KB의 배수가 되는 시점에서 할당받도록 되어 있다. 이렇게 메모리를 할당받는 것을 allocation granularity라고 부르는데 메모리의 관리의 용이성때문이라고 보면 된다. allocation granularity는 CPU 플랫폼에 의존적이기도 한데 현재는 x86플랫폼뿐 아니라 32비트 알파칩, 64비트 알파, IA64등의 플랫폼에서도 allocation granularity가 64KB이다.
그리고 프로세스가 메모리를 사용하기 전에 예약reserve하게 되는데 이때 예약되는 메모리 region의 크기는 페이지 크기의 배수가 된다. 페이지page는 메모리 할당의 최소 단위로 allocation granularity와 마찬가지로 CPU에 따라 다르다. 대개는 4K나 8K를 쓰는데 인텔 x86 플랫폼은 allocation granularity와 마찬가지로 페이지 사이즈는 4KB이다.2 만약 프로그래에서 10KB의 메모리 공간을 할당받으려 한다면 x86 기반에서는 4K의 배수인 12KB를 할당받게 되는 것이다.
가상 메모리를 예약reserve할 때는 VirtualAlloc()을 사용하며 할당받은 메모리를 반환할 때는 VirtualFree()라는 API를 사용한다.
주의할 것은 메모리를 확보reserved 했다고 해서 그 영역을 바로 사용할 수 있는 것은 아니다. 확보된 영역을 사용하기 위해서는 실제로 물리적 메모리를 이 영역에 맵팽시키는 작업이 필요하다. 이를 commit이라고 하며 이때도 VirtualAlloc() 함수를 사용한다. commit을 할 때 반드시 확보된 영역 전체를 할 필요는 없으며 페이지 단위로도 할 수 있다. 만일 어떤 64KB 공간을 확보했을 때 첫번재와 두 번째 페이지(x86 CPU라면 8KB)만 commit할 수도 있다.

2.2 메모리 할당 알고리즘

pagetable.jpg
<물리적 메모리와 가상 메모리의 매핑 관계. Page table에는 페이지가 RAM에 존재하는지 paging file에 존재하는지에 대한 정보를 가지고 있다.>
옛날 DOS및 윈3.1 시절에는 시스템의 메모리는 그 시스템에 설치된 RAM이 전부였다. 만약 시스템에 1MB의 RAM이 설치되어 있다면 커밋할 수 있는 메모리는 1MB가 전부였던 것이다. 3그러나 오늘날의 현대적 운영체제들은 대부분 시스템에 설치된 RAM 이외에 디스크 파일을 메모리로 간주할 수 있는 메커니즘을 제공하고 있다. 이 때 이 메모리로 간주되는 파일을 페이징 파일(paging file)이라고 한다. 간단히 말해 자신의 시스템에 256MB의 메모리와 256MB의 페이징 파일을 가지고 있다면 물리적 메모리는 512MB가 되는 것이다.
가상 메모리 할당 알고리즘
위 그림은 가상 주소 공간이 물리적 주소로 변환되는 메커니즘을 보여주고 있다.
한 프로세스의 어떤 쓰레드thread4가 프로세스 주소 공간에 있는 데이타에 접근하려고 할 때 그 데이타가 RAM에 존재하고 있다면 CPU는 프로세스의 가상 주소를 해당 물리 주소에 맵핑하고 데이타에 접근하게 된다. 그러나 데이타가 RAM에 없다면 그 데이타가 페이징 파일에 있는지 확인하게 된다. 이 상황을 Page fault라고 한다. page fault가 발생하면 시스템은 페이징 파일내에 데이타가 존재하는지 확인하고 없다면 access violation을 발생시킨다. 접근하려는 데이타가 페이징 파일에 있다면 시스템은 현재 RAM에 비어 있는 페이지가 존재하는지 확인하고 존재하지 않는다면 RAM상의 어떤 페이지 중에 한 페이지를 선택해서 이를 해제free하게 된다.
만일 해제하려는 페이지의 데이터가 RAM에서 변경된 적이 있다면 (이를 데이터가 더럽혀졌다(dirty)라고 한다. :) ) 해당 페이지의 내용을 페이징 파일에 갱신한 후 해당 RAM 페이지를 해제한다. 새로 비워진 페이지에 원래 접근하려고 했던 데이터가 존재하는 페이지를 페이징 파일로부터 로딩하게 되고 CPU가 가상 주소를 물리 주소에 맵핑한 후에야 비로소 데이터에 접근하게 되는 것이다. 만일 시스템에 RAM이 부족하게 되면 page fault가 빈번하게 발생하게 되고 운영체제는 계속해서 RAM과 하드 디스크의 페이징 파일 사이에서 페이즈를 바꾸는 작업에 대부분의 시간을 소모하게 된다. 이런 현상을 thrasing이라고 부르는데 가끔 win2000을 사용하다보면 어느 순간에 이유없이 하드 디스크가 버버벅 거리며 돌아가면서 성능이 저하되는 모습을 본적이 있을 것이다. 이때가 바로 thrashing이 발생한 경우이다.
여기서 한 시스템에 수많은 프로그램들이 동시에 실행된다면 그 많은 프로그램들의 실행 코드나 데이타들을 페이징 파일에서 할당해야 할 테니 페이징 파일이 대책없이 커지지 않을까 하는 의문을 가질 수 있다. 이것을 방지하기 위해 시스템은 프로그램이 실행될 때 실행 파일을 열고 코드와 데이타의 크기를 먼저 확인한 뒤에 실행에 필요한 만큼의 영역을 확보한 후 실행 파일의 이미지 자체를 영역으로 할당한다. 프로그램의 .exe 실행 파일이나 DLL등의 파일 이미지가 주소 공간의 확보된 영역으로 사용될 때 이를 memory mapped file이라고 한다.5

2.3 관련 함수

위에서 설명했듯 가상 메모리 공간을 확보하거나 커밋하기 위해 VirtualAlloc()이라는 API를 쓴다. 이 함수의 프로토 타입은 다음과 같다.
LPVOID VirtualAlloc( 
  LPVOID lpAddress, // address of region to reserve or commit 
  DWORD dwSize,     // size of region 
  DWORD flAllocationType, 
                    // type of allocation 
  DWORD flProtect   // type of access protection 
); 

2.3.1 가상 메모리 확보reserve

lpAddress
확보하고자 하는 가상 주소의 시작 주소를 나타낸다. 시스템이 자동으로 빈 공간을 정하도록 할 때는 NULL을 주는데 만약 특정 주소를 넘겨줄 땐 반드시 User area 범위 내의 주소를 줘야 한다. 그렇지 않을 경우 VirtualAlloc은 NULL을 반환하게 된다. 또한 앞에서도 언급했지만 이 주소 값은 할당 세밀도(allocation granularity)의 단위에 맞춰져야 한다. 예를 들어 6,554,624(64KB*100+1024B)의 주소에서부터 영역을 할당하게 되면 시스템은 이를 내림하여 65,543,600(64KB*100)의 주소부터 영역을 할당하게 될 것이다.
dwSize
이 파라미터는 확보하고 싶은 주소의 크기를 바이트 단위로 나타낸다. 역시 위에서 언급했듯 확보하고 싶은 영역의 크기는 반드시 시스템의 페이지 크기의 배수여야 한다. 예를 들어 x86 플랫폼에서 126KB의 영역을 확보하려 한다면 시스템은 128KB 크기의 영역을 확보하게 된다.
flAllocationType
VirtualAlloc 함수를 이용해 메모리를 확보할 것인지 커밋할 것인지를 나타내며 각각 MEM_RESERVE, MEM_COMMIT의 값을 가진다. 시스템은 비어 있는 주소 중 어디에나 확보할 수 있기 때문에 차곡차곡 순서대로 영역이 확보될 것이라는 보장은 없다. 2000 계열에서는 MEM_TOP_DOWN이라는 플래그를 사용하면 가장 높은 주소부터 영역을 확보하게 된다.
flProtect
이 파라미터는 해당 영역에 할당할 보호 특성(protection attribute)을 지정한다. 어떤 페이지를 읽고 쓰기로 커밋하고 싶다면 사전에 PAGE_READWRITE 플래그를 주어 메모리를 확보해야 한다. 그외 페이지의 읽고 쓰기에 관한 권한이나 실행(execute)에 관한 여러 플래그가 있으니 MSDN을 참조한다. 98 계열에서는 이중 PAGE_NOACCESS, PAGE_READONLY, PAGE_READWRITE만 지원하고 있다. 주의할 것은 이 protection attribute가 확보된 영역에 해당되는 것이며 커밋된 물리적 메모리와는 무관하다는 것이다. 일단 메모리가 확보만 되고 커밋되지 않았다면 해당 주소에 대한 접근은 무조건 access violation을 일으키게 된다.

2.3.2 확보된 영역에서 특정 페이지를 commit

VirtualAlloc으로 커밋할 때는 flAllocationType 인자를 MEM_COMMIT으로 주면 된다. 물론 lpAddress 인자에 커밋을 원하는 메모리 시작 주소를, dwSize에 커밋할 메모리 크기를 주게 된다. 이 때 반드시 한번에 모든 영역을 커밋할 필요는 없다.
다음의 코드는 어떤 512KB 영역이 주소 6,553,600에서부터 확보되었을 때, 이로부터 2KB 후에서 6KB의 크기만큼 커밋하는 코드이다(x86 CPU 기준)
VirtualAlloc((PVOID)(6553600+2*1024), 6*1024, MEM_COMMIT, PAGE_READWRITE); 
이 경우 시스템은 페이지 크기의 배수로 커밋을 해야 하기 때문에 8KB의 메모리를 커밋하게 되며 커밋된 주소 공간은 6,553,600부터 6,561,791(6553600+8 KB-1)까지가 된다.

2.3.3 가상 메모리 해제free

프로세스가 더 이상 가상 메모리를 사용하지 않을 때는 이를 해제시켜 줘야 한다. 이때 사용하는 함수는 VirtualFree() 함수를 이용한다.
BOOL VirtualFree( 
  LPVOID lpAddress,  // address of region of committed pages 
  DWORD dwSize,      // size of region 
  DWORD dwFreeType   // type of free operation 
); 
커밋된 페이지를 해제할 때는 flFreeType 파라미터에 MEM_DECOMMIT 값을, 확보된 영역을 해제할 때는 MEM_RELEASE 값을 주게 된다. dwSize에 0을 주어 확보된 영역 전체를 모두 decommit하고 free한다.
VirtualFree(6553600, 0, MEM_DECOMMIT | MEM_RELEASE); 
만일 영역 전체가 커밋된 것이 아니라면 반드시 커밋된 페이지를 먼저 decommit 시키고 해제해야 한다.

3 Memory-Mapped File

가상 메모리와 유사하게 memory mapped file도 프로세스의 주소 공간에 특정 영역을 확보하고 이를 물리적 메모리에 맵핑할 수 있게 해준다. 다른 점은 가상 메모리에서는 물리적 메모리가 페이징 파일로 한정되는 반면, memory mapped file은 유저가 지정하는 임의의 파일 자체라는 것이다. 이렇게 파일이 매핑이 되고 나면 파일을 마치 메모리인 것처럼 액세스할 수 있게 되는 것이다.
memmap.jpg
<Memory-Mapped File과 가상 주소와의 관계>
memory mapped file은 보통 다음의 세 가지 경우에 사용된다.
  • EXE파일이나 DLL들을 로딩할 때 : 페이징 파일 크기를 줄이고 어플리케이션의 실행에 필요한 시간을 줄일 수 있다. 운영체제가 실행 파일을 읽어 오고 실행하는 내부적인 방법도 바로 memory-mapped file이다.
  • 디스크의 데이타 파일에 접근할 때 : 디스크의 I/O를 줄이고 파일을 메모리에 buffering할 수 있다. 이는 파일을 마치 메모리처럼 사용할 수 있으므로 간편한 파일 조작을 가능하게 한다.
  • 프로세스간 통신(IPC) : memory-mapped file은 win32에서 프로세스간 메모리를 공유하는 유일한 방법이기 때문에 시스템 상에서 수행중인 서로 다른 프로세스끼리 데이타를 주고 받을 때 사용한다. 6.
특히 IPC의 경우 윈도우즈에서는 RPC, COM, OLE, DDE, 윈도우 메시지(WM_COPYDATA), clipboard, mailslot, 파이프, 소켓과 같은 다양한 IPC 메커니즘을 제공하지만 실제로 하위 레벨에 가서는 이들 모두가 memory-mapped file로 구현되어 있다. 왜냐하면 memory-mapped file로 IPC를 구현하는 것이 성능대비 overhead가 적기 때문이다.

3.1 Memory-Mapped File과 주소 공간

CreateProcess()를 호출해서 EXE파일을 실행할 때 windows 운영체제가 어떤 일들을 하는지 step별로 살펴보자.
  1. CreateProcess()의 인자로 넘겨진 .exe 파일을 디스크에서 찾는다. 만약 찾지 못하면 FALSE를 리턴한다.
  2. windows는 새로운 Kernel Object를 생성한다.
  3. 새로운 프로세스를 위한 고유의 주소 공간(address space)를 생성한다.
  4. 새로운 프로세스를 로딩하기에 충분한 주소 공간 영역을 확보reserve 한다. windows에서는 .exe파일을 로딩하기 위한 base address의 기본값 으로 0x00400000 번지를 할당한다.7 만일 다른 주소를 base address로 지정하고자 한다면 linker에서 제공하는 옵션 /BASE로 지정할 수 있다.
  5. 시스템은 reserve된 물리적 메모리가 시스템의 paging file 대신 .exe파일에 있다는 것을 명시한다. 이 부분이 바로 Memory-Mapped File 형태로 동작하는 부분이다.
.exe파일이 프로세스 주소 공간에 매핑이 되면 시스템은 .exe에서 사용하는 dll을 로딩하기 위해 dll 파일들을 명시한 section을 읽어서 이 dll들을 LoadLibrary() 함수를 이용해 각각 메모리에 로딩하게 된다. LoadLibrary()가 호출하여 dll을 로딩할때의 동작은 4, 5번과 유사하다. 단지, 메모리에 dll을 로딩할 때의 base 주소값이 0x10000000 번지라는 것만 다르다. 이 역시 기본값으로 linker에서 /BASE 옵션으로 프로그래머가 재설정할 수 있다.
이렇게 실행 파일이 프로세스 주소 공간에 매핑 되고 dll까지 로딩이 끝났으면 운영체제는 .code section에 기록된 startup 코드를 실행하게 된다. 이후 시스템은 코드를 실행하면서 paging, buffering, caching을 모두 관리하는데 예를 들어 코드에서 아직 메모리에 로딩되지 않는 주소의 인스트럭션으로 점프하게 되면 page fault가 발생하고 시스템은 파일 이미지에서 해당 주소의 페이지를 RAM으로 가져와서 프로세스 주소 공간에 매핑한다. 그리고 마치 아무 일 없었다는 듯이 코드를 계속 실행시키게 된다. 이 과정은 RAM에 로딩되지 않는 코드 영역이나 데이타 영역을 액세스할 때마다 반복되는 것이다.
그럼 여기서 새로운 어플리케이션이 아닌, 이미 실행되고 있는 어플리케이션을 하나 더 실행시키면 어떻게 될까.
이때 는 실행 파일 이미지를 구분할 수 있는 memoy-mapped view를 하나 더 생성하고 새로운 process object를 만들게 된다. 여기에 새 process id와 thread id를 부여하고 memory-mapped file를 이용해서 가상 메모리에 로딩되어 있는 코드와 데이타를 공유하게 된다.
mmf1.jpg
<어플리케이션이 1개만 실행되고 있을 때 프로세스 주소 공간과 가상 메모리가 매핑되어 있는 모습>
mmf2.jpg
<같은 어플리케이션을 하나 더 실행했을 때. 시스템은 같은 파일을 메모리에 로딩하지 않고 이미 가상 메모리에 올라가 있는 코드와 데이터 영역 페이지를 매핑한다.>
만약 여기서 실행되고 있는 두개의 어플리케이션 인스턴스중 하나가 데이타 페이지에 있는 내용을 변경시키면 다른 인스턴스에까지 영향을 미치게 되는 문제가 발생하게 된다. 이를 예방하기 위해 시스템은 copy-on-write라 는 기법을 사용하고 있는데 이는 공유하고 있는 페이지중 하나를 어떤 인스턴스에서 변경하려고 하면 가상 메모리에 원래 페이지를 복사해서 같은 페이지를 생성한 후 새 페이지에 변경 내용을 반영하는 기법을 말한다. 아래 그림은 이렇게 변경된 주소 공간의 모습을 보여주고 있다.
mmf3.jpg
<First Instance에서 Data page 2이 내용을 변경했을 때 운영체제는 가상 메모리에 새로운 페이지를 생성해서 First Instance의 주소 공간에 매핑하게 된다. 그리고 New page의 내용에 변경된 값을 쓰게 된다.>

3.2 Memory-Mapped File의 이용

memory-mapped file을 이용하기 위해선 다음의 세 단계를 거쳐야 한다.
  1. 디스크에서 Memory-mapped file로 사용할 파일를 식별하기 위하여 파일 커널 객체(file kernel object)를 생성한다.
  2. 시스템에게 파일의 크기와 어떻게 파일에 접근할 것인지를 알리기 위해 파일 매핑 커널 객체(file-mapping kernel object)를 생성한다.
  3. 시스템에게 파일 매핑 커널 객체의 일부, 혹은 전부를 자신의 프로세스 주소 공간에 매핑시키도록 한다.
이렇게 memory-mapped file을 생성하여 사용을 끝마쳤을 때는 다음의 세 단계를 거쳐서 memory-mapped file을 해제시켜야 한다.
  1. 프로세스 주소 공간에 매핑되어 있는 파일 매핑 커널 객체(file-mapping kernel object)를 unmap한다.
  2. 파일 매핑 커널 객체를 close한다.
  3. 파일 커널 객체(file kernel object)를 close 한다.

3.2.1 File Kernel Object 생성

memory-mapped file을 위해 이미 존재하는 파일을 열거나 혹은 새로 생성하고자 한다면 CreateFile() 함수를 이용해서 새로운 파일 커널 객체를 생성하게 된다.
HANDLE CreateFile( 
  LPCTSTR lpFileName,          // pointer to name of the file 
  DWORD dwDesiredAccess,       // access (read-write) mode 
  DWORD dwShareMode,           // share mode 
  LPSECURITY_ATTRIBUTES lpSecurityAttributes, 
                               // pointer to security attributes 
  DWORD dwCreationDisposition,  // how to create 
  DWORD dwFlagsAndAttributes,  // file attributes 
  HANDLE hTemplateFile         // handle to file with attributes to  
                               // copy 
); 
각 인자에 대한 자세한 설명은 MSDN 라이브러리를 참고하고 여기서는 이 함수를 쓰는 간단한 예를 들어 보겠다.
예)
HANDLE hFile = CreateFile( pszPathName, 
                           GENERIC_WRITE | GENERIC_READ, 
                           FILE_SHARE_READ | FILE_SHARE_WRITE, 
                           NULL, 
                           CREATE_ALWAYS, 
                           FILE_ATTRIBUTE_NORMAL, 
                           NULL); 
pszPathName이라는 이름으로 파일을 읽고 쓰기 권한으로 열고 오픈한 파일의 공유 모드 역시 읽고 쓰기 모드로 공유하게 했다. 그리고 같은 이름의 파일의 존재 여부와 상관없이 새로운 파일을 생성시키는데 만약 기존에 같은 이름의 파일이 존재하면 덮어써버리게 된다.
함수가 성공적으로 수행이 되면 파일에 대한 핸들이 리턴되며 함수 실행이 제대로 수행되지 않았다면 INVALID_HANDLE_VALUE(-1)가 반환된다.
만약 프로세스간 공유를 목적으로 운영체제에서 사용하는 페이징 파일을 이용하고자 한다면 굳이 새로운 파일을 생성시킬 필요는 없다. 이때는 CreateFile함수를 이용해서 파일 커널 객체를 생성하는 이 과정은 생략이 된다.

3.2.2 File-Mapping Kernel Object 생성

시스템에게 CreateFile함수로 파일 매핑에 사용될 파일의 위치를 지정해 주었다면, 이번엔 파일 매핑 객체를 위해 얼마만큼의 공간을 잡을 것인지를 알려줘야 한다. 이때 사용하는 함수가 CreateFileMapping() 함수이다.
HANDLE CreateFileMapping( 
  HANDLE hFile,              // handle to file to map 
  LPSECURITY_ATTRIBUTES lpFileMappingAttributes, 
                             // optional security attributes 
  DWORD flProtect,           // protection for mapping object 
  DWORD dwMaximumSizeHigh,   // high-order 32 bits of object size 
  DWORD dwMaximumSizeLow,    // low-order 32 bits of object size 
  LPCTSTR lpName             // name of file-mapping object 
); 
역시 각 인자에 대한 설명은 MSDN 도움말을 참고한다.
예)
HANDLE hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE, 
                                        NULL, 
                                        PAGE_READWRITE, 
                                        0, 
                                        0x1000, 
                                        “SharedMemoryFile” 
CreateFileMapping()의 첫번재 인자는 memory-mapped file로 이용할 파일 커널 객체의 핸들을 넘겨주게 되어 있다. 따라서 위의 CreateFile()을 호출해서 리턴되는 핸들을 이 인자로 전달하면 된다. 여기서의 예는 이 값을 INVALID_HANLDE_VALUE로 주어 특정 파일 대신 시스템의 페이징 파일을 이용할 수도 있다는 것을 보여준다.
그 리고 다음에는 파일 매핑 객체에 보호 특성(protection attribute)를 주도록 되어 있다. 위에서 설명했지만, memory-mapped file을 생성한다는 것도 결국 가상 메모리를 할당받는 것과 마찬가지로 프로세스의 주소 공간의 일부 영역을 확보reserve해서 그 영역을 실제 물리적 저장소에 커밋commit하는 것과 같다. 단지 memory-mapped file은 그 물리적 저장소를 시스템의 페이징 파일 대신에 디스크의 특정 파일로 지정해줄 수 있다는 차이뿐인 것이다. 여기서 파일 매핑 객체를 생성한다는 것이 막바로 시스템으로 하여금 주소 공간을 확보reserve해서 그 파일 영역을 그 주소로 매핑하도록 하는 것은 아니다. 그러나 파일 영역을 프로세스의 주소 공간에 매핑할 때 시스템은 물리적 저장소에 어떤 보호 특성을 지정해줘야 할지 알아야 하기 때문에 CreateFileMapping() 함수에서 보호 특성을 지정해 줄 수 있게끔 되어 있다.
이 예에서는 파일 매핑 공간에 대해 읽고 쓰기 특성을 부여하도록 했다.
보호 특성 다음의 두 인자 dwMaximumSizeHighdwMaximumSizeLow는 memory-mapped file로 사용할 파일의 크기를 전달하는 인자인데 각 32비트씩 상위 하위 바이트로 총 64비트, 즉 16EB 크기의 파일까지 지정해줄 수 있게 되어 있다. 그러나 대부분 32비트 윈도우 플랫폼이 사용되는 현재는 하나의 파일 크기가 4GB를 넘는 경우가 거의 드물기 때문에 dwMaximumSizeHigh 인자에는 0을 할당하고 dwMaximumSizeLow 인자에 memory-mapped file의 크기를 지정해 주면 된다.8 기존에 존재하는 파일일 경우는 GetFileSize()함 수로 파일의 크기를 가져와서 그 값을 전달해도 되고 이 예에서는 고정된 값 0x1000, 즉 4KB 만큼 주었다. 만약 파일 매핑 오브젝트의 크기를 오픈한 파일 사이즈 값을 그대로 반영시키고 오직 읽기 위해서 액세스를 한다면 두 인자 모두 0을 주면 된다. 그리고 memory-mapped file에 어떤 내용을 덧붙여서 파일 크기가 증가할 것 같으면 실제 파일 크기보다 더 여유있는 크기로 파일 매핑 오브젝트를 생성하도록 한다.
만약 memory-mapped file로 사용할 파일을 새로 생성해서 이 함수를 호출한 것이라면 이 함수가 불려진 후에는 실제로 디스크에 이 함수의 인자로 넘겨준 크기만큼의 파일이 생성이 될 것이다. 그러나 이 시점에서는 memory-mapped file로 사용할 파일을 생성시킨 것 뿐 실제로 프로세스의 메모리에 매핑된 것은 아니다.
마지막 인자 pszName 자리에 들어간 "SharedMemoryFile"은 파일 매핑 커널 객체(file-mapping kernel object)의 이름을 부여하는 것으로 주로 다른 프로세스에서 이 메모리 맵 파일을 공유할 때 사용된다. 만약 공유 목적이 아니라면 이 자리에 NULL을 주어도 상관없다.
이렇게 파일 매핑 커널 객체를 생성했으면 이제 이 객체object를 프로세스의 주소 공간에 매핑하는 일이 남았다.

3.2.3 File Data를 프로세스 주소 공간에 mapping

프로세스의 주소 공간의 영역을 확보reserve하고 그 영역에 매핑되는 물리적 주소를 파일 데이타로 커밋commit하는 함수가 MapViewOfFile() 함수이다.
LPVOID MapViewOfFile( 
  HANDLE hFileMappingObject,  // file-mapping object to map into  
                              // address space 
  DWORD dwDesiredAccess,      // access mode 
  DWORD dwFileOffsetHigh,     // high-order 32 bits of file offset 
  DWORD dwFileOffsetLow,      // low-order 32 bits of file offset 
  DWORD dwNumberOfBytesToMap  // number of bytes to map 
); 
여기까지 제대로 따라왔다면 여기에 넘겨지는 인자들의 내용은 그리 어렵지 않게 이해할 수 있을 것이다.
예)
lpMapVIew = MapViewOfFile(hFileMap, 
                          FILE_MAP_READ | FILE_MAP_WRITE, 
                          0, 0, 
                          0); 
첫번째 인자는 CreateFileMapping()함 수에서 리턴되는 파일 매핑 객체(file-mapping kernel object)의 핸들이다. 두 번째는 파일에서 매핑된 메모리의 접근 권한을 명시하며 세 번째와 네 번째는 메모리 맵 파일의 offset 값이다. 파일 매핑 객체를 생성했다고 해서 반드시 파일 전체 크기를 메모리로 매핑시킬 필요는 없고 파일의 일부를 메모리로 매핑시킬 수 있는데 이렇게 메모리로 매핑된 파일의 일부를 view라고 부르는 것이다. 윈도우즈에서는 memory-mapped file의 크기를 16EB까지 지원하므로 32비트 인자 2개를 써서 64비트의 offset 값을 사용한다. win32에서 프로그래밍하고 있다면 dwFileOffsetHigh는 0을 넣고 dwFileOffsetLow에 실제 파일의 offset 값을 넘겨주면 될 것이다. 이때 주의할 것은 파일의 offset 값 또한 위의 가상 메모리에서 설명한 시스템의 할당 세밀도(allocation granularity)의 배수로 지정해줘야 한다는 것이다.
다섯 번째 인자는 파일의 offset으로부터 얼마만큼의 크기를 메모리로 매핑시킬 것인지를 알려준다. 0을 넘겨주면 offset에서부터 파일의 전체를 메모리로 매핑시키겠다는 의미이다.9
만약 파일의 view를 프로세스 주소 공간의 특정 주소로 매핑시키고 싶다면 다음의 함수를 이용한다.
LPVOID MapViewOfFileEx( 
  HANDLE hFileMappingObject,  // file-mapping object to map into  
                              // address space 
  DWORD dwDesiredAccess,      // access mode 
  DWORD dwFileOffsetHigh,     // high-order 32 bits of file offset 
  DWORD dwFileOffsetLow,      // low-order 32 bits of file offset 
  DWORD dwNumberOfBytesToMap, // number of bytes to map 
  LPVOID lpBaseAddress        // suggested starting address for mapped  
                              // view 
); 
대부분의 인자는 MapViewOfFile()함수와 같고 마지막 인자 lpBaseAddress에서 매핑할 주소를 지정하도록 되어 있다. 이때 지정할 주소 역시 allocation graularity(64KB)의 경계에 맞추어 주어야 한다. 그렇지 않으면 에러가 발생하여 NULL을 리턴하게 된다.10 그외 매핑하려는 주소에서 파일 사이즈에 비해 빈 공간이 적어서 이미 확보된 다른 영역까지 overlap될 위험이 있을 경우 역시 NULL이 반환된다. 이 MapViewOfFileEx() 함수는 때때로 유용하게 쓰이는데 예를 들어 두 개 이상의 프로세스가 memory-mapped file을 공유하고 있고 공유하는 데이타 내용이 linked list인 경우 모든 프로세스가 linked list의 노드를 정상적으로 액세스하려면 동일한 주소로 매핑시켜야 할 것이다. 그렇지 않으면 노드 안에 있는 next node의 주소 값이 한 프로세스에게만 의미가 있고 나머지 프로세스들은 엉뚱한 주소로 액세스할테니 말이다.
MapViewOfFIle()에서 정상적으로 메모리가 commit이 되었다면 commit된 메모리의 base address가 void pointer 형태로 반환된다. 이후에는 이 값으로 메모리 포인터를 사용하듯 이용하면 되는 것이다.
만약 다른 프로세스에서 이 메모리 맵 파일을 공유하여 사용하려면 OpenFileMapping() 함수와 파일 매핑 커널 객체의 이름을 이용하면 된다.11
예)
hFileMapping = OpenFileMapping(FILE_MAP_WRITE, FALSE, "SharedMemMapFile"); 

3.2.4 Memory-Mapped File의 해제

파일 데이타를 더 이상 메모리로 매핑할 필요가 없어졌을 땐 UnmapViewOfFile() 함수로 메모리 영역을 release시켜줘야 한다.
BOOL UnmapViewOfFile( 
  LPCVOID lpBaseAddress   // address where mapped view begins 
); 
첫번째 인자 lpBaseAddressMapViewOfFile()에서 리턴된 메모리의 시작 번지이다.
memory-mapped file도 어쨌든 시스템으로부터 할당 받은 메모리로 간주되므로 이것을 release시켜주지 않으면 프로세스가 끝날 때까지 리소스를 쥐고 있게 된다. MapViewOfFile()이 호출될 때 마다 시스템은 매번 새로운 메모리 영역을 확보해서 리턴할 뿐 이전의 호출로 확보된 영역을 release해주지 않으므로 반드시 MapViewOfFile() 함수마다 UnmapViewOfFile()이 쌍을 이루어 호출되어야 한다. 이 함수가 불려지면 운영체제는 메모리로 매핑된 파일중 변경된 내용을 파일 이미지에 반영시키고 프로세스 주소 공간중 memory-mapped file에서 매핑된 영역을 release한다.12
파일 매핑 객체(file-mapping object)로 또다른 map view를 생성할게 아니라면, 지금까지 오픈했던 커널 객체들, 즉 파일 매핑 객체와 파일 객체(file object)를 시스템에 반환해줘야 한다. 이것은 CloseHandle() 함수를 사용해서 각각의 객체 핸들을 인자로 넘겨주면 된다. 이때 각 객체를 반환하는 순서는 상관이 없다.

see also

4 Heap

자, 이제 마지막으로 win32에서 제공하는 메모리 관리의 3번째 메커니즘은 힙heap이다.
heap은 내부적으로 프로세스 주소 공간에 확보served된 메모리 영역region을 의미한다. 즉, heap 영역은 reserve되어 있고 "heap 메모리를 할당받는다."는 것은 그 reseve된 공간을 실제 물리적 메모리 공간으로 커밋commit한다라는 의미인 것이다. 이렇게 commit되는 물리적 메모리 공간은 항상 시스템의 페이징 파일에서 할당 받게 되며 할당받은 메모리 블럭을 해제free하면 heap 관리자는 물리적 메모리에서 해당 블럭을 decomiit하게 된다.13
heap은 작은 크기의 블럭을 여러개 할당해야할 응용에 가장 적합한데 그 이유는 가상 메모리는 페이지 단위로 할당되기 때문에, 즉 몇 바이트를 쓰든 상관없이 항상 할당 세밀도(allocation granularity)로 할당 받게되며 페이지 경계(page bounary)도 신경을 써야하는 반면, heap은 어플리케이션에서 요구하는 만큼만 할당되므로 훨씬 경제적으로 이용할 수 있다는 장점이 있는 것이다. 예를 들어 링크드 리스트(linked list), 트리(binary tree)는 가상 메모리나 메모리 맵 파일로 관리하는 것보다는 힙으로 관리하는 것이 가장 적당할 것이다. 그리고 C++에서 new연산자등을 통해 class object를 동적으로 할당 받는 것에도 heap이 이용된다. 면 heap은 가상 메모리를 이용해서 구현된 높은 수준의 메모리 관리 메커니즘이므로 메모리를 할당받거나 해제할 때 다른 메모리 관리 메커니즘보다는 속도 측면에서 떨어질 수 밖에 없고 물리적 메모리를 commit하거나 decommit하는 등의 직접 제어가 불가능하다는 단점이 있다.

4.1 프로세스의 기본 Heap

운영체제에서 프로세스를 생성할 때 기본적으로 할당되는 힙 영역이 있는데 이를 프로세스의 기본 힙(Process`s Default Heap)이라고 한다. 프로그래머가 링커 옵션인 /HEAP으로 사이즈를 지정해주지 않는다면 프로세스 주소 공간에 기본적으로 1MB의 heap영역이 만들어 진다.14 물론 이 기본 heap은 run-time시에 크기를 키워나갈 수 있다.
프 로세스의 기본 heap은 윈도우즈 API에서 주로 사용하는데 ANSI 문자열을 UNICODE 문자열로 컨버전할 때, 혹은 API내에서 임시 메모리 블럭을 사용할 때 쓰이는 공간으로 유저가 맘대로 해제할 순 없다. 따라서 프로세스는 이 기본 heap 이외에 추가적인 heap을 할당해서 사용한다.
defaultheap.jpg

4.2 추가적인 heap의 용도

프로세스에서 추가적인 heap을 생성해서 사용하는 용도는 크게 5가지로 구분할 수 있다.
컴포넌트 보호(Component Protection)
예를 들어, 어떤 어플에서 링크드 리스트(linked list)와 트리(tree)와 같은 상이한 타입의 데이터 구조들을 운용한다고 생각해 보자. 그런데 이들 데이타 구조를 하나의 단일 heap에서 관리하고 있을 때 코드내에 숨어 있는 버그로 인해 heap의 consistency를 깨뜨릴 위험이 있다. 단일 heap에서 다음과 같이 linked list 노드와 tree의 branch 노드가 같이 할당되어 있다고 했을때,
heap1.jpg
linked list의 노드를 연산하는 함수에서 linked list의 2번 NODE를 연산하다가 실수로 노드의 크기를 over해서 연산해 버리면 BRANCH 2번에 덮어써 버리게 된다. 이런 위험을 방지하기 위해 linked list의 NODE와 tree의 BRANCH를 다른 heap으로 각각 전담시키게 하면 heap을 깨뜨릴 위험이 많이 줄어들 것이다.
효율적인 메모리 관리(More Efficient Memory Management)
heap은 같은 크기의 블럭 사이즈로 할당할 때 가장 효율적으로 관리가 된다. 위의 component 보호에서 예를 든 것처럼, 리스트와 트리를 단일 heap에서 운용할 때, NODE와 BRANCH를 다른 사이즈로 각각 24바이트, 32바이트씩 할당한다고 가정하자. NODE와 BRANCH 인스턴스를 heap에서 계속 할당하다가 전체 heap을 모두 할당했다고 했을 때, NODE 하나를 해제하면 24바이트의 메모리 공간이 생긴다. 그리고 다른 공간에 또다른 NODE를 해제해서 총 48바이트의 빈공간이 있어도 정작 32바이트의 BRANCH를 할당할 수 있는 공간이 없게 된다. 이런 상황을 fragmentation이라고 부르는데 만약 NODE와 BRANCH를 동일한 사이즈의 블럭으로 할당하면 이런 fragmentation을 방지하고 효율적으로 메모리를 관리할 수 있게 된다.
지역적 접근(Local Access)
시스템에서 page fault가 발생해서 RAM과 디스크의 페이징 파일(paging file) 사이에 스왑swap이 발생할 때마다 시스템 성능에 막대한 영향을 끼친다. 만약 어플에서 접근하는 메모리 영역을 일정 주소 영역으로 한정해서 지역화 시키면 이런 디스크와의 스와핑을 줄여서 시스템 성능을 높일 수가 있게 된다. 만약 다른 타입의 데이타 구조들을 단일 heap에 혼재해서 쓰다보면 구조체의 인스턴스들의 근접성이 떨어져서 page fault가 발생할 확률을 높이게 된다. 위에서 예로든 리스트와 트리의 응용에서 최악의 경우, 한 페이지에 NODE가 하나만 할당되고 나머진 BRANCH 인스턴스가 자리잡는다면, list의 NODE를 하나씩 traverse할 때마다 page fault가 발생하여 프로세스의 속도는 상당히 저하될 것이다. 이때 각각의 데이터 구조를 별개의 heap으로 관리해서 각 인스턴스를 인접시키면 성능을 향상시킬 수 있게 된다.
쓰레드 동기화 부담 해소(Avoiding Thread Synchronization Overhead)
기본적으로 heap은 순서화(serialization)를 보장한다. 이말은 프로세스에서 다중 쓰레드를 운용할 때 각 쓰레드가 동일한 heap에서 블럭을 할당하거나 해제하려고 한다면, 한번에 오직 하나의 쓰레드만 할당하거나 해제하는 기능을 수행할 수 있으며 나머지 쓰레드는 이전 쓰레드에서 할당, 해제 작업이 끝난 다음에서야 heap 블럭에 대한 할당, 해제에 들어갈 수 있다라는 의미이다. 그러나 이를 위해 당연히 heap의 메모리 할당, 해제 함수에서 serialization을 위한 추가적인 코드가 실행됨으로써 속도에 영향을 미치게 된다. 이때 추가적인 heap을 생성해서 각각의 heap에 하나의 프로세스만 접근하게 하고 serialization을 적용하지 않는다면 성능상의 이익을 얻을 수 있게 된다.
빠른 해제(Quick Free)
마지막으로 데이터 구조체에 전담으로 할당한 heap이 있다면, 해당 구조체를 더 이상 사용하지 않을 때 heap의 메모리 블럭을 해제하는 것보다 heap 자체를 해제해 버리는 것이 더 편리하고 성능 또한 높이게 된다.

4.3 추가적인 heap의 사용

4.3.1 Heap 생성

프로세스에서 heap을 추가하기 위해선 다음의 함수를 호출해서 사용하면 된다.
HANDLE HeapCreate( 
  DWORD flOptions,      // heap allocation flag 
  DWORD dwInitialSize,  // initial heap size 
  DWORD dwMaximumSize   // maximum heap size 
); 
이 함수는 가상 메모리에 유저가 지정해주는 크기의 heap object를 생성시키는 함수이다.
flOptions은 생성하는 heap의 특성을 지정해주는 플래그이다. 0, HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE 셋 중의 하나의 값, 혹은 두 개 이상의 or 연산값을 넘겨줄 수 있다. 여기서 중요한 것은 HEAP_NO_SERIALIZE 플래그인데 heap은 기본적으로 동적 메모리를 할당하거나 해제할 때 순서화된 접근을 허용한다고 위에서 설명하였다. 이 옵션을 주면 새로 생성하는 heap에서는 순서화serialization 특성을 적용하지 않겠다라는 의미이다. 따라서 멀티 프로세스 및 멀티 쓰레드 환경에서 heap을 사용하는 응용이라면 이 옵션을 지정해주지 않는 것이 좋다. 이 옵션은 메모리 할당 및 해제시에 속도를 높여주긴 하므로 이 옵션은 단일 쓰레드나 단일 프로세스일 때, 다중 쓰레드이긴 하지만 오직 하나의 쓰레드만 heap에 접근할 때나 크리티컬 섹션(criticial section), 뮤텍스(mutax), 세마포(semaphor)등의 동기화 메커니즘을 사용해서 heap의 접근을 제어할 때만 사용하도록 한다.
HEAP_GENERATE_EXCEPTIONS 은 heap의 생성이 실패할 때 시스템에서 예외 시그널(exception)을 발생시키는 플래그로 어떤 이유로 함수의 리턴값으로 체크하는 것보다 exception을 체크하는 것이 더 나은 응용일 때 사용한다.
dwInitialSize은 heap의 초기 사이즈를 지정해주는 인자로 내부에서 페이지 크기로 올림하여 처리된다. dwMaximumSize는 heap에서 허용하는 최대 크기를 지정해주는 인자로 메모리를 할당 받을 때 총 할당량이 이 값 이상이 되어버리면 할당이 안된다. 0으로 지정해주면 시스템에서 남는 메모리가 있을 때까지 무제한으로 heap을 할당할 수 있게 된다.
heap이 정상적으로 생성이 되면 heap object에 대한 핸들이 리턴된다. 이후 이 heap에 대한 제어 함수에 이 핸들을 인자로 넘겨주게 된다.

4.3.2 Heap으로부터 메모리 블럭 할당

heap으로부터 메모리 블럭을 할당받을 때는 시스템 내에서 다음의 과정을 거치게된다.
  1. 메모리 관리자 내부에는 할당된 메모리 블럭과 free 메모리 블럭들이 링크드 리스트(linked list)로 관리되는데 이 링크드 리스트에 대해 순회traverse를 시작한다.
  2. free block의 node를 검색한다.
  3. 검색된 free block에 할당되었다는 마킹을 해서 새로운 메모리 블럭을 할당한다.
  4. 메모리 블럭에 새로운 entry를 추가한다.
이러한 과정을 거쳐서 새로운 메모리 블럭을 할당받게 하는 함수가 HeapAlloc()이다.
LPVOID HeapAlloc( 
  HANDLE hHeap,  // handle to the private heap block 
  DWORD dwFlags, // heap allocation control flags 
  DWORD dwBytes  // number of bytes to allocate 
); 
hHeap은 heap의 핸들을 받는 인자이고 dwFlags은 할당받는 메모리 블럭의 특성을 지정해주는 인자이다. 이 인자에는 현재 3개의 플래그만 지정해줄 수 있는데 HEAP_GENERATE_EXCEPTIONS, HEAP_ZERO_MEMORY, HEAP_NO_SERIALIZE 가 있다. HEAP_GENERATE_EXCEPTIONS는 위에서 설명했듯 함수가 실패할 때 exception을 발생시키는 플래그이고 HEAP_NO_SERIALIZE 또한 앞서와 마찬가지로 메모리 블럭을 할당할 때 serialization을 위한 코드를 실행하지 않도록 하는 플래그이다. 만약 HeapCreate()에서 이 플래그를 넘겨주었다면 이후 이 heap에서 할당받거나 해제할 때는 자동으로 serialization을 적용하지 않기 때문에 굳이 여기서 넘겨주지 않아도 된다. 그러나 HeapCreate()에서 이 플래그를 넣지 않았다면 이 플래그를 사용하는 개별 함수에서만 serialization을 적용하지 않게 된다. 다시한번 강조하지만, 멀티 쓰레드 환경이라면 이 플래그는 상당히 조심해서 적용해야 한다.
HEAP_ZERO_MEMORY는 메모리 블럭을 할당받을 때 0으로 초기화하도록 지정해주는 플래그이다.
heap에서 메모리가 정상적으로 할당되었다면 할당된 메모리 블럭이 주소가 리턴되고 함수가 실패하면 NULL이 리턴된다.
주 의할 것은 win98 계열에서 이 함수를 사용해서 256MB 이상의 메모리를 할당하고자 한다면 메모리 할당이 실패하고 NULL이 리턴된다. 할당받고자 하는 메모리가 1MB가 넘을 때는 heap보다는 가상 메모리를 이용해서 할당받는 것이 바람직하다.

4.3.3 블럭 크기 변경

HeapAlloc() 를 통해 할당 받은 메모리 블럭을 사용하다 보면 이 블럭의 크기를 더 키워야 한다거나 줄여야 하는등 블럭 크기를 재조정해야할 때가 있다. 이때는 다음의 함수를 사용해서 할당 받은 메모리 블럭의 사이즈를 재조정해서 할당받는다.
LPVOID HeapReAlloc( 
  HANDLE hHeap,  // handle to a heap block 
  DWORD dwFlags, // heap reallocation flags 
  LPVOID lpMem,  // pointer to the memory to reallocate 
  DWORD dwBytes  // number of bytes to reallocate 
); 
dwFlags에는 재할당 받는 메모리 블럭의 특성을 지정해주는데 4개의 플래그가 들어갈 수 있다. 이중 HEAP_ZERO_MEMORY는 재조정하는 메모리 블럭이 기존에 할당받은 메모리 블럭보다 클 경우, 늘어난 크기의 메모리 블럭을 0으로 초기화하라는 플래그이다. HEAP_REALLOC_IN_PLACE_ONLY 는 현재 메모리 위치에서 사이즈를 조정해서 리턴하도록 지시하는 옵션이다. 일반적으로 HeapReAlloc()는 유저가 지정하는 사이즈에 맞는 메모리 위치를 찾아서 이전 블럭의 내용을 옮기고 그 주소를 리턴하게 된다. 그런데 이 옵션을 넘겨주면 메모리 블럭 위치를 변경시키지 말고 현재 메모리 블럭의 사이즈를 조정하라는 옵션인 것이다. 주의할 것은 블럭 크기가 줄어드는 경우에는 상관없지만, 그 크기를 키우는 것이라면 인접한 메모리 공간이 늘어나는 크기만큼 비어있어야 한다. 그렇지 않고 인접한 메모리 공간이 점유되어 있으면 이 함수는 수행이 안되고 NULL이 리턴된다. 이 옵션을 사용하는 경우는, 예를 들어 링크드 리스트나 트리의 한 노드의 크기를 키워야 하는 경우에 사용된다. 만약 블럭 크기를 조정하는 노드의 주소가 바뀌면 그 노드를 포인트하는 인접 노드의 내용도 함께 바뀌어야 할 것이다.
lpMem은 재할당 받고자 하는 블럭의 주소를 넘겨주는 인자이고 dwBytes에는 재조정하고자 하는 크기를 지정해준다.
현재 할당된 메모리 블럭의 크기를 알고자 한다면 다음의 함수를 사용한다.
DWORD HeapSize( 
  HANDLE hHeap,  // handle to the heap 
  DWORD dwFlags, // heap size control flags 
  LPCVOID lpMem  // pointer to memory to return size for 
); 
lpMem에 크기를 알고자 하는 메모리 블럭의 주소를 넘겨준다.

4.3.4 Heap 메모리 블럭 해제 및 Heap 제거

할당받은 heap 메모리 블럭을 해제할 때는 HeapFree() 함수를 사용한다.
BOOL HeapFree( 
  HANDLE hHeap,  // handle to the heap 
  DWORD dwFlags, // heap freeing flags 
  LPVOID lpMem   // pointer to the memory to free 
); 
lpMem에 해제하고자 하는 메모리 블럭의 주소를 넘겨준다.
이렇게 메모리 블럭을 할당하다가 더 이상 heap을 사용하지 않는다면 heap object를 시스템에서 제거할 수 있다.
BOOL HeapDestroy( 
  HANDLE hHeap   // handle to the heap 
); 

4.4 Heap을 이용한 new, delete 구현

heap 메모리를 이용하는 응용중 대표적인 케이스는 C++에서 다음과 같이 new 연산자를 이용해서 클래스 객체를 할당받는 예일 것이다.
CSomeClass *pSomClass = new CSomClass; 
C++ 컴파일러는 이 문장을 만나게 되면 클래스에서 new 연산자가 정의되어 있는지 체크해서 만약 정의되어 있다면 클래스에서 정의한 new 연산자 함수가 실행되고 클래스내에서 정의되어 있지 않다면 표준 C++ 라이브러리에서 구현된 new 연산자 함수를 실행하게 된다.
이렇게 할당받은 클래스 객체의 사용이 끝나면 delete 연산자로 객체를 메모리에서 해제하게 된다.
delete pSomeClass; 
자, 그럼 new연산자와 delete 연산자를 각각 어떻게 구현할 수 있는지 살펴보도록 하겠다. 다음은 클래스를 정의한 헤더의 모습이다.
class CSomeClass { 
private: 
    static HANDLE s_hHeap; 
    static UINT s_uNumAllocsInHeap; 
               .......... 
public: 
    void *operator new(size_t size); 
    void operator delete(void *p); 
              .......... 
}; 
클래스의 멤버 변수인 s_hHeaps_uNumAllocsInHeap를 static으로 정의하였다. 이는 CSomeClass로 생성되는 모든 인스턴스는 같은 변수를 공유하도록 하기 위함이다. C++ 컴파일러는 CSomeClass 객체가 생성될 때마다 s_hHeaps_uNumAllocsInHeap 멤버 변수를 따로 할당하지 않게 된다.
다음은 각각 new와 delete의 구현 코드이다. 그리 어렵지 않으므로 코드내의 코멘트로 설명을 대신하도록 하겠다.
HANDLE CSomeClass::s_hHeap = NULL; 
UINT CSomeClass::s_uNumAllocsInHeap = 0; 
 
void *CSomeClass::operator new(size_t size) { 
    if (s_hHeap == NULL) {    
         // heap object가 존재하지 않으면 heap 생성 
        s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0); 
 
        if (s_hHeap == NULL) 
            return NULL; 
     } 
 
    // class 객체를 위해 heap 메모리 블럭 할당 
    void *p = HeapAlloc(s_hHeap, 0, size); 
 
    if (p != NULL) { 
         s_uNumAllocsInHeap++; 
    } 
 
    // 할당된 메모리 블럭의 주소를 리턴 
    return p; 
} 
void CSomeClass::operator delete(void *p) 
{ 
    if (HeapFree(s_hHeap, 0, p)) { 
         // 메모리 블럭이 성공적으로 해제되었다. 
         s_uNumAllocsInHeap--; 
    } 
 
    if (s_uNumAllocsInHeap == 0) { 
         // heap에 더 이상 할당된 메모리 블럭이 없으면 heap을 제거한다. 
         if (HeapDestroy(s_hHeap)) { 
             // heap을 제거하면 힙 핸들에 NULL을 할당한다. 
             s_hHeap == NULL; 
         } 
    } 
} 

5 References

  • Programming Applications for Microsoft Windows 4ed, Jeffrey Richter
  • MSDN Library, Microsof Visual Studio 6.0
  • 마이크로소프트 2003.11월호, "윈도우 이해하기3", 고동일
주석
____
  1 프 로세스란 일반적으로 실행되고 있는 프로그램의 인스턴스로 정의한다. win32에서 각 프로세스는 4GB의 가상 메모리 영역으로 구성된 프로세스의 주소영역(address space)를 갖는다. win32에서 프로세스는 코드를 실행하지 않는다. 다만 프로세스는 단순히 코드와 데이타를 포함하는 4GB의 주소 영역을 소유하고 있을 뿐이다. 주소영역과 함께 프로세스는 운영체제가 할당하는 파일, 파이프, 통신 포트 등의 시스템 자원을 저장한다.이들 시스템 자원은 프로세스가 종료될 때 소멸된다. 이와 함께 모든 프로세스는 적어도 하나의 쓰레드를 갖고 있어야 한다. 프로세스가 소유할 수 있는 쓰레드의 수는 메모리가 허용하는 한 제한이 없다.
  2 Alpha칩은 32/64비트 공히 페이지 크기를 8KB로 쓴다. IA64도 8KB 페이즈 크기를 가지는 것으로 알고 있다.
  3 그래서 옛날 DOS를 이용했던 유저들은 시스템에 물리적 메모리를 어떻게든 쥐어 짜내보려고 EMM 386등을 이용해 메모리를 확장(Extened)시키는 등의 눈물겨운(?) 노력을 했던 기억이 있을 것이다.
  4 thread 는 process 안에서 실행되는 코드의 실행 흐름이라고 할 수 있다. 쓰레드는 프로세스가 할당한 메모리 영역에서 실행되며 프로세스에 할당된 시스템 자원을 사용하게 된다. 프로세스가 생성되고 초기화될 때마다 운영체제는 기본쓰레드(primary thread)를 생성한다. 각 쓰레드는 명령 코드와 함께 CPU 레지스터 상태를 저장하는 context와 2개의 스택 영역이 포함된다. 2개의 스택 영역은 특권 프로세스 모드와 사용자 모드를 위한 스택이다.
  5 윈 도우즈에서 실행중인 파일이나 dll을 삭제하려고 할 때 실행중이므로 삭제할 수 없다라는 메시지까 뜨면서 삭제가 안되는 경우를 많이 보았을 것이다. 이는 실행한 파일이나 dll을 전부 메모리에 올리지 않고 필요한 부분만 메모리에 올려지고 나머지는 memory mapped file로 묶여져 있기 때문인 것이다.
  6 windows에서는 IPC를 위해 여러 메커니즘을 지원하는데 이들 모두는 하위 레벨에 가서는 결국 memory-mapped file로 구현되어 있다.
  7 base address 0x00400000번지는 win9x의 경우이다. win NT 및 win2000은 프로세스의 base address로 0x00100000 번지가 기본값이다. 실행파일이 메모리에 로딩되는 base address 값은 .exe파일의 header에 들어있다.
  8 32 비트 주소 공간에서 4GB 크기를 넘는 파일을 memory-mapped file로 사용하는 방법이 있긴 있다. 그것은 파일 전체를 한번에 매핑시키지 않고 액세스 가능한 크기로 나누어서 매핑시키고 다시 unmap하여 다른 부분을 매핑시키는 기법을 쓰는데 자세한 것은 Reference에 있는 Jeffrey의 책에 자세히 설명되어 있다.
  9 여 기서 win98과 win2000이 다른 점이 있는데 win98에서는 만약 MapVIewOfFile에서 전체 파일 크기만큼의 메모리 영역을 확보하지 못하면 dwNumberOfBytesToMap 값에 관계없이 NULL을 반환한다. 반면, win2000에서는 파일 전체 크기와 상관없이 dwNumberOfBytesToMap 인자로 요구한 크기 만큼의 메모리가 확보되면 정상적으로 commit이 된다.
 10 win2000 의 경우에는 에러 처리하여 NULL을 반환하는 반면, win98에서는 에러로 처리를 안하고 할당 세밀도의 경계에 맞게 반내림하여 처리한다. 그리고 주소값에 넣어줄 범위도 플랫폼에 따라 다른데 win98에서는 lpBaseAddress 값을 공유 영역인 0x80000000부터 0xBFFFFFFF 사이의 값을 넣어주어야 하고 win2000에서는 user-mode partition 내의 범위라면 어떤 값을 넣어줘도 상관없다.
 11 이 미 전술한 바 있지만 win98에서는 memory-mapped file의 위치가 공유 영역에 있다. 따라서 win98에서는 한 프로세스가 MapViewOfFile()을 호출해서 공유 영역을 확보했으면 그 리턴값을 다른 프로세스에게 전달만 해주면 다른 프로세스에서도 그 값을 이용해서 memory-mapped file을 맘껏 사용할 수 있는 것이다. 그러나 이렇게 프로그램을 짜버리면 win2000과 호환되지 않으며 첫번째 프로세스에서 UnpaViewOfFile()을 호출해서 그 영역을 release해 버리면 다른 프로세스에서는 그 영역을 접근할 때 access viloation이 발생하게 될 것이다.
 12 유 념해야할 점은 memory-mapped file이라고 해서 view에 작업한 내용들이 그 즉시 파일에 반영되는 것은 아니라는 것이다. 어플리케이션이 파일의 view에 작업하는 동안 운영체제는 퍼포먼스 문제 때문에 파일 데이타 페이지를 버퍼링한다. 따라서 memory-mapped file에 작업하는 동안 시스템이 불시에 멈춘다거나 재부팅되면 그 직전에 작업중이던 내용이 모두 날아갈 수 있다. 이것을 방지하려면 중간에 FlushViewOfFile()이라는 함수를 사용해서 작업중이던 내용을 파일 이미지에 쓰게할 수 있다.
 13 heap 메모리 블럭을 해제하는 룰은 win98과 win2000이 약간 다른데 win98은 메모리 사용률에 중점을 둬서 동적 메모리를 해제할 때마다 바로 물리적 메모리를 decommit하려는 반면, win2000은 메모리 함수의 속도를 중시해서 동적 메모리를 해제한다고 해서 바로 물리적 메모리를 decommit하지 않고 일정 기간 메모리 공간을 쥐고 있다가 해제하는 경향을 보인다. 그런데 이런 룰은 새로운 버전의 운영체제나 하드웨어가 등장하면 언제든 바뀔 수 있는 정책이라고 한다.
 14 dll은 heap을 가질 수 없기 때문에 dll을 만들때는 /HEAP 옵션을 사용할 수 없다.
: