메모리맵 파일

반응형
http://blog.naver.com/kimgudtjr?Redirect=Log&logNo=140117254752

[ 메모리 맵 파일 ]

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


[ 정의 ]

 

윈도우즈는 물리적인 메모리(RAM)가 부족할 경우 하드 디스크의 페이징 파일(Paging File)을 메모리 대신

사용한다. 마치 페이징 파일이 물리적인 메모리의 일부인 것처럼 프로세스의 주소 공간에 맵하여 사용하며

필요할 경우 RAM으로 읽어오므로 응용 프로그램의 입장에서 볼 때 페이징 파일은 속도가 좀 느릴 뿐

RAM과 전혀 다를 것이 없다.

 운영체제가 하드 디스크의 페이징 파일을 RAM 대용으로 사용하는 것이 가능하다면 일반 파일도  RAM

대요으로 사용하여 주소 공간에 맵할 수 있을 것이다. 일반 파일도 정보를 저장하고 읽고 쓸 수 있으므로

이론적으로 전혀 문제가 없다. 메모리 맵 파일(Memory Mapped File)은 이런 이론에 기반하여 하드 디스크에

존재하는 파일의 내용을 프로세스의 주소 공간에 연결(Map)하는 기법이다.  요약하자면

파일을 마치 메모리인 것처럼 사용하는 기법이라 하겠다.

이처럼 가상 주소 공간에 파일을 맵한 후 그 포인터를 사용하면 파일의 내용을 마치 메모리 다루듯이

똑같이 사용할 수 있다. 파일을 열고 닫고 파일 포인터를 옮기고 버퍼를 유지하는 복잡한 처리를

할 필요없이 마치 메모리에 있는 데이터를 읽고 쓰듯 *ptr = data; 등과 같이 간편하게 파일 조작을 할 수 있는

것이다. 파일을 메모리처럼 사용해도 그 뒷처리는 운영체제가 철저하게 책임진다. 포인터로 파일을

액세스하면 RAM으로 스왑할 것이고 오랫동안 사용하지 않으면 다시 파일에 기록하며 파일 맵핑을 닫을 때

대기중인 모든 출력이 파일에 완전히 기록된다.

메모리 맵 파일은 편리함뿐만 아니라 아주 여러 가지 용도로 가지고 있다. 운영체제가 실행 파일을 읽어오고

실행하는 내부적인 방법도 바로 메모리 맵 파일이다. 실행 파일을 메모리로 읽어올 필요없이 디스크의

이미지를 곧바로 프로세스의 주소 공간에 맵한 후 바로 실행할 수 있다.  물론 그 배경에서 시스템은

실행 파일 이미지에서 당장 필요한 부분을 물리적인 RAM으로 읽어보고 더 이상 필요하지 않은 부분을

RAM에서 제거하는 복잡한 처리를 할 것이다.

운영체제가 이런 식으로 파일 맵핑을 통해 실행 파일을 로드하기 때문에 로딩 속도가 대단히 빠르며

가상 메모리를 절약할 수 있다. 실행 파일 이미지의 대부분은 읽기 전용이므로 파일 자체를 주소 공간에

맵해서 사용해도 별 상관이 없는 것이다.  100M 넘는 큰 파일을 실행해도 순식간에 실행되는 이유가

바로 파일을 곧바로 맵핑하기 때문이다. 단, 플로피나 CD-ROM, 이동식 디스크 같은 착탈식 미디어는

언제 제거될지 알 수 없으므로 가상 메모리로 전부 읽어들인 후에 실행한다.

또한 메모리 맵 파일은 Win32에서 프로세스간 메모리를 공유하는 유일하고 합법적인 방법이다.

두 개의 프로세스가 하나의 메모리 맵 파일을 동시에 액세스할 수 있기 때문에 메모리 맵 파일을 통해

데이터를 주고받을 수 있고 동시에 한 메모리 영역을 액세스할 수도 있다. 프로세스간 통신에 사용되는

여러 가지 기술들도 내부적으로 모두 메모리 맵 파일을 활용한다.

 

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

 

[ 파일 액세스 ]

 

메모리 맵 파일을 만드는 절차는 그리 간단하지 않다. 더구나 관련 함수들의 옵션이 지겨울 정도로

만힉 대문에 그 옵션들의 의미까지 전부 한꺼번에다 알기란 무척 어렵다. 그래서 일단 아주 간단한

예제를 통해 파일 맵핑 오브젝트를 만들고 사용하는 방법만 먼저 알아본다. 다음 예제는 파일 맵핑

오브젝트를 통해 디스크 상의 파일을 읽는 시범을 보이는 MMFile 예제이다. 같은 디렉토리에

Naru.txt라는 데이터 파일을 만들어 두고 이 파일을 가상 주소 공간에 맵한 후 메모리 읽듯이 파일을

읽어서 출력할 것이다.


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

#include <windows.h>

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

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;
 HANDLE hFile, hMap;
 TCHAR *PtrInFile;
 RECT rt;

 switch(iMessage)
 {
 case WM_LBUTTONDOWN:
  hdc = GetDC(hWnd);
 hFile = CreateFile(TEXT("NaRu.txt"),GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

 if(hFile == INVALID_HANDLE_VALUE)
 {
  MessageBoxA(0,"CreateFile Errr","",0);
  exit(0);
 }
 
 hMap = CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL);

 if(hMap == NULL)
 {
  MessageBoxA(0,"CreateFileMapping Error","",0);
  exit(0);
 }

 PtrInFile = (TCHAR*)MapViewOfFile(hMap,FILE_MAP_READ,0,0,0);

 if(PtrInFile == NULL)
 {
  MessageBoxA(0,"MapViewOfFile Error","",0);
  exit(0);
 }

 SetRect(&rt,10,10,640,400);

 DrawText(hdc,PtrInFile,GetFileSize(hFile,NULL),&rt,DT_EXPANDTABS);
 
 

 UnmapViewOfFile(PtrInFile);
 CloseHandle(hMap);
 CloseHandle(hFile);
 ReleaseDC(hWnd,hdc);
  return 0;

 case WM_RBUTTONDOWN:
 
  hFile = CreateFile(TEXT("NaRu.txt"),GENERIC_WRITE | GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

 if(hFile == INVALID_HANDLE_VALUE)
 {
  MessageBoxA(0,"CreateFile Errr","",0);
  exit(0);
 }


 hMap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,0,NULL);

 if(hMap == NULL)
 {
  MessageBoxA(0,"CreateFileMapping Error","",0);
  exit(0);
 }
 
 PtrInFile = (TCHAR*)MapViewOfFile(hMap,FILE_MAP_WRITE,0,0,0);

 if(PtrInFile == NULL)
 {
  MessageBoxA(0,"MapViewOfFile Error","",0);
  exit(0);
 }

 PtrInFile[0] = 'A';
 PtrInFile[1] = 'B';
 PtrInFile[2] = 'C';
 
 UnmapViewOfFile(PtrInFile);
 CloseHandle(hMap);
 CloseHandle(hFile);

  return 0;

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

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

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


마우스 왼쪽 버튼을 누르면 파일의 내용을 읽어서 화면에 출력된다.

코드는 WM_LBUTTONDOWN 에 있으며 파일 입출력 함수를 사용하여 파일을 읽는 것과

질적으로 다른 방법을 사용하고 있다. 포인터로 바로 읽어들이는 것이 다소 신기해 보인다.

먼저 CreateFile 함수를 사용하여 Naru.txt 파일을 읽기 전요응로 열었다. 그리고 그 핸들을

사용하여 파일 맵핑 오브젝트를 만든다. 파일 맵핑 오브젝트(File Mapping Object)는

디스크 상의 파일을 가상 주소 공간에 맵하는데 필요한 정보를 가진다.


HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName);

첫 번째 인수 hFile은 대상 파일의 핸들이다. 이 핸들은 CreateFile로 오픈한 핸들이거나 아니면

0xFFFFFFFF(=INVALID_HANDLE_VALUE)로 주어 페이징 파일의 일부 영역을 대신 사용할 수도 있다.

페이징 파일은 가상 메모리의 일부분이므로 하드 디스크의 파일이 아닌 가상 메모리에 파일 매핑이

생성되며 이 방법은 두 프로세스의 메모리 공유에 사용된다. 두 번째 인수는 보안 정보를 가지는

구조체이되 대부분 NULL로 준다. fProtect는 일종의 액세스 타입을 지정하는 데 다음 중 한 값을 지정한다.

------------------------------------------------------------------
값   설명
------------------------------------------------------------------
PAGE_READONLY  읽기 전용의 파일 맵핑 오브젝트를 만든다. 이렇게 만들어진
   메모리 맵 파일에 쓰기를 해서는 안 된다. 이 엑세스 지정을 사용할 때
   hFile은 반드시 GENERIC_READ로 열려 있어야 한다.

PAGE_READWRITE  읽고 쓸 수 있는 파일 맵핑 오브젝트를 만든다. hFile은 GENERIC_READ
   | GENERIC_WRITE로 열려 있어야 한다.

PAGE_WRITECOPY  읽고 쓸수 있는 파일 맵핑 오브젝트를 만들되 쓰기 즉시 복사
   (Write ON Compy)기법을 사용한다. 즉 쓰기를 수행하는 시점에서
   별도의 복사본이 생성된다. 95/98은 이 플래그를 지원하지 않는다.
------------------------------------------------------------------


이 외에 flProtect에 지정할 수 있는 SEC_COMMIT, SEC_IMAGE, SEC_NOCACHE, SEC_RESERVE 등의

플래그들이 더 있는데 자세한 사항은 래퍼런스를 참고하기 바란다. 이 예제에서는 파일을 읽기만 할

것이므로 PAGE_READONLY 플래그를 주었다. hFile이 디스크상의 파일일 경우 액세스 타입은 hFile의

액세스 타입과 호환되야 한다. 파일은 읽기 모드로만 열고 파일 맵핑은 읽기 쓰기 모두 가능하도록

열 수는 없다.

네 번째 인수와 다섯 번째 인수는 생성될 파일 맵핑 오브젝트의 최대 크기를 지정하는 64비트의

정수이다. Win32가 지원하는 메모리 맵 파일의 최대 크기는 64비트라는 엄청난 그야말로 무모할 정도로

큰 크기이다. 이 함수가 처음 만들어질 때만 해도 64비트 정수형이 없었기 때문에(지금은 있다) 32비트

정수 두 개를 사용하여 상위, 하위 32비트씩을 전달한다. 이 인수들이 모두 0이면 즉 파일 맵핑 오브젝트의

크기를 지정하지 않으면 hFile에서 지정한 파일의 크기가 그대로 사용된다. 파일 맵핑의 크기는 hFile이

-1일 경우, 즉 하드 디스크의 파일이 아닌 페이징 파일에 메모리 맵 파일을 생성할 때나 또는

대단히 큰 파일의 일부부만을 열고자 할 때 사용된다.

64비트라면 곧 2의 64승 크기를 의미한다. 18뒤에 0을 18개를 적은 수이며 우리나라의 단위로 하면

180만조=1800경=0.18해가 된다. 컴퓨터 용어로는 16GG(기가 기가)바이트가 되며 한 단위로는

16E(엑사)바이트라고 읽는다. 왜 Win32 설계자들은 이렇게 무지막지하게 큰 범위까지 메모리 맵 파일을

지원하도록 했는지 의아할 정도다. 32비트를 넘어섰기 때문에, 또한 그보다 더 큰 미디어가 이미 개발 완료된

상태이기 대문에 32비트로는 부족할지도 모른다고 생각했던 것 같다. 과거에 용량의 상한을 잘못 정한

댓가로 인한 엄청난 혼란을 다시 재현하지 않기 위한 노력이라고 생각하면 된다.

 마지막 인수 lpName은 파일 맵핑 오브젝트의 이름이며 문자열 형태로 주므로 역슬래시 문자를 재외한

어떤 문자든지 사용할 수 있다. 필요하지 않을 경우 NULL로 주면 된다. 이 인수는 복수 개의 프로세스가

하나의 파일 맵핑 오브젝트를 공동으로 사용하고자 할 때 사용되는데 이런 예는 앞에서도 사용자

저으이 메시지나 클립보드 포맷을 등록할 때 본 적이 있다. 동기화 객체의 경우도 프로세스간에 공유할

필요가 있는 것들은 구분 가능한 이름을 지정해야 한다. 이 에제는 파일 맵핑 오브젝트를 혼자서

사용하므로 이름을 줄 필요가 없다.

 CreateFileMapping 함수는 인수로 주어진 정보를 참고하여 파일 맵핑 오브젝트를 만들며 그 핸들을

리턴한다. 만약 에러가 발생하면 리턴값은 NULL이 된다. 파일 맵핑 오브젝트를 만든 후에는 이 오브젝트를

프로세스의 주소 공간에 맵해야 한다. 응용 프로그램에서는 파일 맵핑 오브젝트를 곧바로 사용하는 것이

아니라 주소 공간에 맵한 후 그 주소 공간을 사용한다. 이때 주소 공간에 맵된 파일의 일부분을 파일 뷰(View)라고

하는데 이 이름은 메모리의 특정 번지를 통해 파일의 내용을 엿볼 수 있다는 의미로 붙여졌다.

파일 맵핑 오브젝트를 주소 공간에 맵할 때는 다음 함수를 사용한다.

 LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesireAccess, DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap);


함수 이름 그대로 파일의(OfFile) 뷰를 (View)를 주소 공간에 맵(Map)한다. 첫 번째 인수는 주소 공간에

맵하려는 파일 맵핑 오브젝트의 핸들이며 이는 CreateFileMapping 함수에 의해 생성된 핸들이거나

아니면 OpenFileMapping 함수에 의해 오픈한 핸들이다. 두 번째 인수는 액세스 지정이다. 파일 맵핑

오브젝트를 만들 때도 지정했지만 뷰를 만들 때 각 뷰에 대해서도 액세스 지정을 다르게 할 수 있다.

단 이 함수의 dwDesiredAccess 인수에서 지정한 액세스 지정은 CreateFileMapping 함수의

flPretect 인수와 적당히 호환되어야 한다. 예를 들어 여기서 FILE_MAP_WRITE 값을 주려면 맵핑 오브젝트를

만들 때도 쓰기가 가능하도록 했어야 한다는 상식적인 얘기다. dwDesireAccess 는 다음 중 한 값을 가진다.


-----------------------------------------------------------------------------
플래그   설명
-----------------------------------------------------------------------------
FILE_MAP_WRITE  읽고 쓸 수 있다.
FILE_MAP_READ  읽을 수 있다.
FILE_MAP_ALL_ACCESS 읽을 수도 있고 쓸 수도 있다.
FILE_MAP_COPY  읽고 쓸 수 있다. 쓰기 시도가 발생하면 데이터의 복사본을 만든 후 쓴다.
-----------------------------------------------------------------------------

세 번째 인수와 네 번째 인수는 맵핑을 시작할 오프셋 위치를 나타내는 64비트 정수를 지정한다.

이 값이 0이면 당연히 파일의 선두부터 맵핑되겠지만 0이 아니면 파일의 중간부터 맵핑할 수도 있다.

단 이 오프셋은 반드시 시스템의 할당 단위(보통64K)의 배수여야만 한다. 마지막 인수는 맵핑할 뷰의 크기를

지정한다. 이 값이 0이면 파일 전체가 맵되지만 일부만 맵하고자 하면 그 크기를 지정하면 된다.

 MapViewOfFile 함수는 인수가 지정하는대로 맵핑 오브젝트의 일부 또는 전체를 프로세스의 주소 공간에

맵한후 그 시작 번지를 리턴한다. 이 포인터 값을 받아 마치 메모리를 읽고 쓰듯이 파일에 읽고 쓰기를

한다. 예제의 코드를 다시 보자.

-----------------------------------------------------------------------------
hFile = CreateFile("NaRu.txt",GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

hFMap = CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL);

PtrInFile = (TCHAR*)MapViewOfFile(hFMap, FILE_MAP_READ,0,0,0);
-----------------------------------------------------------------------------

하드 디스크의 Naru.txt 파일을 머넞 열어그 핸들을 구하되 읽기만 할 것이므로 GENERIC_READ 액세스

타입으로 열었다. 파일 맵핑에 사용할 파일은 다른 프로세스와 함께 공유할 수 없으므로 공유 플래그는

반드시 0으로 지정해야 한다. 메모리에 연결되어 있는 파일을 다른 프로세스가 변경하거나

지우도록 놔 둘 수는 없기 때문이다. 또한 열러진 파일의 핸들은 파일 맵핑 오브젝트가 파괴되기 전까지

계속 열려 있어야 한다.

hFile 파일로부터 파일 맵핑 오브젝트를 만들어  hFMap 핸들에 대입하되 파일과 같은 댁세스 타입으로

열었다. 파일 맵핑의 크기는 0으로 지정하여 hFile과 같은 크기를 가진다. 그리고 이 핸들을 사용하여

파일 전체를 주소 공간에 맵한 후 그 포인터를 PtrInFile변수에 대압하였다. 이후의 코드는

PtrInFile을 사용하여 파일의 내용을 메모리 읽듯이 읽어내는 것이다. 예제에서는 DrawText 함수를

사용하여 PtrInFile 메모리 주소에 있는 문자열을 화면에 출력하였다.

DrawText 함수는 메모리 상의 문자열을 읽어 화면으로 출력하는 함수이지 파일을 직접 읽어 출력하는

기능을 가지고 있지 않음에도 불구하고 이 경우는 아주 훌륭히 파일의 문자열을 출력하고 있다.

왜냐하면 메모리 맵 파일에 의해 디스크 상의 파일 이미지가 이 프로그램의 가상 주소 공간에

연결되어 있고 가상 주소 공간상의 문자열 포인터를 받았기 때문이다. 이렇게 연결된 파일을 다 사용하고

나 후에는 다음 함수로 뷰를 닫는다. 뷰의 시작 번지를 인수로 넘기기만 하면 된다.

-----------------------------------------------------------------------------
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
-----------------------------------------------------------------------------

그리고 파일 맵핑 오브젝트(hFMap)와 파일 (hFile) 자체는 CloseHandle 함수로 제거한다.

이상이 메모리 맵 파일을 이용한 파일 읽기 시범이다. 이번에는 코드 조금 바꾸어서 오른쪽 마우스

버튼을 누를 때 파일 쓰기를 해 보자. Alpha.txt 라는 텍스트 파일을 만들고 이파일에 a~z까지의

알파벳을 타이프해 놓은 후 마우스 오른쪽 버튼을 누르면 이 파일의 선두를 다른 문자열로 바꾼다.

절차나 사용하는 함수는 동일하되 액세스 지정이 읽기 전용에서 쓰기도 가능하도록 바뀌었다는 점이

다르다. 메모리에 Alpha.txt파일이 직접 맵 되었으므로 이 메모리의 내용을 바꾸는 것은 곧 파일의

내용을 바꾸는 것과 동일하다. strncpy등의 함수를 사용할 수도 있고 아니면 아예

pTrInFile포인터와 첨자 연산자로 직접 변경할 수도 있다. 이 프로그램을 실행하고 마우스 오른쪽 버튼을

눌러보자. 그리고 Alpha.txt 파일을 확인해보면 앞부분의 내용이 바뀌어 있을 것이다.

시스템은 파일 뷰의 내용이 바뀔 때마다 즉시 디스크의 파이롤 출력하지는 않고 변하된 부분에대해

캐싱을 하다가 시스템이 한가해지면 파일로 쓰기를 한다. 캐시를 하지 않으면 효율이 형편없이 떨어지고

파일 맵핑을 하용하는 의미가 없어질 것이다. 만약 즉 캐시를 비우려면 FlushViewOfFile 함수를 호출한다.

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

'Study > C++' 카테고리의 다른 글

Hash 함수 모음  (0) 2011.04.22
boost::has_trivial_assign  (0) 2011.04.10
LIB / DLL 차이점  (0) 2011.03.17
How to convert std::string to TCHAR*  (0) 2011.01.06
문자열 비교할때 대소문자 구분없이 하기.  (0) 2010.12.03
TAGS.

Comments