WHAT IS FILE MAPPING?

FILE MAPPING 이란, 가상 메모리처럼 프로세스 주소 공간을 예약하고, 예약한 영역에 물리 저장소를 커밋하는 기능을 제공하는 것을 말한다.

가상 메모리와 유일한 차이점이라면, 시스템의 페이징(Paging) 파일을 사용하는 대신 디스크 상에 존재하는 어떤 파일이라도 물리 저장소로 사용 가능하다는 점이다. (메모리 맵 파일도 페이징 파일 사용이 가능하나, 이 부분은 아래 챕터에서 자세히 설명하겠다.)


FILE MAPPING 은 아래 세 가지 목적으로 사용될 수 있다.

  1. 실행 파일(.exe)과 DLL 파일을 읽고 수행
  2. 디스크에 있는 데이터 접근
  3. 동일 머신에서 수행중인 다수의 프로세스 간 데이터 공유
디스크 입출력은 메모리 입출력보다 훨씬 느리다. CPU에서 디스크가 램보다 더 멀고, 본래 RAM 이 디스크 보다 빠르기 때문이다. 따라서 파일을 메모리에 올려 메모리에 입출력하고, 그 결과를 캐싱한 후 메모리에 올린 파일을 더이상 사용 할 필요가 없을때 디스크에 변경된 내용을 덮어 씌운다면 고속으로 I/O를 진행 할 수 있을것이다. 

디스크에 존재하는 파일을 메모리에 사상(연결, Mapping) 하여 얻은 주소에 입출력 하면, 디스크에 입출력이 된다.

이것이 FILE MAPPING 의 개념이다. 그림을 보자. MSDN에서 제공하는 그림이다.



FILE MAPPING

메모리 맵 파일을 사용하려면 다음의 세 가지 단계를 수행해야 한다.
  1. 메모리 맵 파일로 사용할 디스크 상의 파일을 나타내는 파일 커널 오브젝트를 생성하거나 연다 (CreateFile)
  2. 파일의 크기와 접근 방식을 고려하여, 파일 매핑 커널 오브젝트를 생성한다 (CreateFileMapping)
  3. 프로세스의 주소 공간 상에 파일 매핑 오브젝트의 전체나 일부를 매핑시킨다 (MapViewOfFile)
메모리 맵 파일을 더 이상 사용할 필요가 없다면, 다음이 세 가지 단계를 수행해야 한다.
  1. 프로세스의 주소 공간으로부터 파일 매핑 오브젝트의 매핑을 해제한다 (UnmapViewOfFile)
  2. 파일 커널 매핑 오브젝트를 닫는다 (CloseHandle)
  3. 파일 커널 오브젝트를 닫는다 (CloseHandle)

CreateFIle

우선 CreateFile 의 함수 원형은 아래와 같다.

1
2
3
4
5
6
7
8
HANDLE CreateFile(
        PCSTR pszFileName,
        HANDLE hTemplateFile);
        DWORD dwShareMode,
        PSECURITY_ATTRIBUTES psa,
        DWORD dwCreationDisposition,
        DWORD dwFlagsAndAttributes,
        HANDLE hTemplateFile);
cs
위 파라미터 중 메모리 맵 파일 관련하여, 신경써야 할 파라미터는 아래와 같다.
  • dwDesiredAccess : GENERIC_READ, GENERIC_WRITE, GENERIC_READ | GENERIC_WRITE 중 하나만 가능하다.
  • dwSharedMode : 0 으로 설정하는 것을 추천한다.
dwSharedMode를 가급적 0으로 셋팅하라고 했다.

세 개의 프로세스가 있다.
A와 B프로세스는 X 파일에 대해 메모리 매핑 파일로 이 내용을 공유 중이고,
C 프로세스는 X 파일에 대해 직접 파일 핸들링을 하려고 한다.

이 경우, 공유가 가능하도록 dwSharedMode가 설정되어 있다면, 메모리 매핑 파일의 일관성이 깨지게 된다.
메모리 매핑 일관성 역시 이후 내용에서 자세하게 설명하겠다.

그리고, 매번 강조하지만 잊지 말자.
CreateFile은 실패시 NULL이 아닌, INVALID_HANDLE_VALUE를 리턴한다.

CreateFileMapping

앞서 CreateFile 을 호출한 것은 운영체제에게 파일 매핑을 수행할 파일의 물리 저장소를 알려주기 위함이다.


이제, 파일 매핑 오브젝트를 생성시켜 보자.

1
2
3
4
5
6
7
HANDLE CreateFileMapping(
    HANDLE hFile,    // 물리 저장소로 사용할 파일의 핸들
    PSECURITY_ATTRIBUTES psa,
    DWORD fdwProtect,
    DWORD dwMaximumSizeHigh,
    DWORD dwMaximumSizeLow,
    PCTSTR pszName);
cs
1) 첫번째 인자는 바로 앞서 생성해둔 파일의 핸들이다.

2) 두번째 인자는 커널 오브젝트라면 무조건 가지는 보안 관련 파라미터이므로 패스하고,

3) 세번째 인자인 보호 속성은 다음 세가지 페이지 보호 속성을 기본으로 가질 수 있고,
  • PAGE_READONLY : CreateFile 호출시 GENERIC_READ 보호 속성으로 설정한 경우
  • PAGE_READWRITE : CreateFile 호출시 GENERIC_READ | GENERIC_WRITE로 설정한 경우
  • PAGE_WRITECOPY : Copy-on-write 메커니즘을 사용. 해당 페이지에 데이터를 쓰게 되면, 새로운 페이지를 복사하여 쓴다.
위 페이지 보호 속성 외 아래 다섯 가지 메모리 매핑 파일만의 속성을 추가로 지정할 수 있다.
  • SEC_NOCACHE : 메모리 매핑 파일에 대한 캐싱을 수행하지 못하게 한다. 일반 어플리케이션에서는 거의 사용되지 않는다.
  • SEC_IMAGE : 매핑한 파일이 PE 파일 이미지임을 알려준다. 즉, 실행파일/DLL 실행시 사용된다.
  • SEC_RESERVE / SEC_COMMIT : 이 두 개는 배타적으로 사용되어야 한다. 스파스 메모리 맵 파일과 관련있다.
  • SEC_LARGE_PAGES : 큰 페이지 할당 기능과 관련있다.
추가 속성의 prefix는 SEC_ 로 되어 있는데, 이는 메모리 매핑을 섹션이라고도 부르는 것에 기인한다.

4) 네번째/다섯번째 인자는 매핑할 파일의 최대 크기를 바이트 단위로 설정한다.

이것이 High/Low 2개의 DWORD 타입으로 나뉘어진 것은 윈도우가 파일의 크기를 64비트 단위로 표현하기 때문이다.
(즉, 4GB 크기가 넘는 파일을 핸들링 하기 위함)
따라서, 파일의 크기가 4GB를 넘지 않는다면, dwMaximumSizeHigh는 항상 0 이 될 것이다.

하지만, 32비트 프로세스에서 사실 메모리 맵 파일을 이용해 사용할 수 있는 파일의 크기는 최대 2GB 이다.
이는 메모리 맵 파일 역시 유저 모드 파티션에서만 공간 예약/커밋이 가능하기 때문이다.

만일, 매핑 파일로 지정한 파일의 크기를 기준으로 파일 매핑 오브젝를 만드는 경우엔, 이 두 개의 파라미터에 0 을 전달할 수 있다.
아래 예제 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int WINAPI _tWinMain(HINSTANCE, HINSTANCE, PTSTR, int)
{        
    // 아래 문장을 수행하기 전까지는 C:\에 MMFTest.dat 파일이 생기지 않을 것이다.
    HANDLE hFile = CreateFile(TEXT("C:\\MMFTest.dat"),
                              GENERIC_READ | GENERIC_WRITE,
                              0, NULL, CREATE_ALWAYS,
                              FILE_ATTRIBUTE_NORMAL, NULL);
 
      // 아래 문장을 수행하기 전까지는 MMFTest.dat 파일은 크기가 0 이다.
       HANDLE hFileMapping = CreateFileMapping(hFile, NULL,
                                            PAGE_READWRITE,    // 파일속성과 맞춤
                                            0,      // dwMaximumSizeHigh
                                            100,    // dwMaximumSizeLow
                                            NULL);        
 
// 이제 MMFTest.dat 파일은 크기가 100 바이트      
 
// 정리        
    CloseHandle(hFileMapping);    
    CloseHandle(hFile);    
    // 프로세스가 종료되어도, MMFTest.dat 파일은 크기가 100으로 유지된다.    
    return 0;
}
cs
CreateFileMapping에 전달된 파일의 크기가 파라미터로 전달된 크기보다 작을 경우
파일의 크기를 파라미터로 전달된 파일의 크기에 맞도록 증가시킨다.

이렇게 파일의 크기를 증가시켜야, 나중에 메모리에 매핑을 수행할 때 물리 저장소가 충분히 확보된 상태가 된다.

5) 여섯번째 인자는 네임드 커널 오브젝트를 위한 것이며, 이는 프로세스간 공유에 활용된다.

지금까지 CreateFileMapping 함수에 대해 자세히 알아보았다.

파일 매핑 오브젝트를 생성한다 하더라도, 시스템은 곧바로 프로세스의 주소 공간 상에 영역을 예약하지 않는다.
그리고 물리 저장소를 해당 영역에 매핑하지도 않는다.

이 함수의 주 목적은,
 
지정 파일을 파일 매핑 오브젝트와 연결시키며,

파일 매핑 오브젝트를 위한 충분한 물리 저장소가 존재한다는 것을 확인시키는 것이다.
(위의 예제처럼 공간이 작았으면 필요 크기에 맞게 늘린다)

아참, 그리고 CreateFileMapping 함수는 실패시 NULL을 리턴한다.
CreateFile은 실패시 INVALID_HANDLE_VALUE를 리턴한다.


데이터를 찾게 되는 순서도.


MapViewOfFile

파일 매핑 오브젝트를 생성했다 하더라도, 
파일의 데이터에 접근하기 위한 영역을 프로세스 주소 공간 내에 확보해야 하며,
이 영역에 임의의 파일을 물리 저장소로 사용하기 위한 커밋 단계를 거쳐야 한다.

이 작업을 수행해 주는 것이 MapViewOfFile 함수이다.
1
2
3
4
5
6
PVOID MapViewOfFile(
    HANDLE hFileMappingObject,
    DWORD dwDesiredAccess,
    DWORD dwFileOffsetHigh,
    DWORD dwFileOffsetLow,
    DWORD dwNumberOfBytesToMap);
cs


첫번째 인자로는 CreateFileMapping으로 얻은 핸들을 넘겨주면 된다.

두번째 인자인 dwDesiredAccess로는 아래 것들을 넘길 수 있다.
  • FILE_MAP_READ : CreateFileMapping에서 PAGE_READ_ONLY로 설정한 경우
  • FILE_MAP_WRITE : CreateFileMapping에서 PAGE_READWRITE로 설정한 경우
  • FILE_MAP_ALL_ACCESS : 이것은 FILE_MAP_READ | FILE_MAP_WRITE | FILE_MAP_COPY 와 같다.
  • FILE_MAP_COPY : 데이터를 쓰게되면 새로운 페이지가 생성된다. CreateFileMapping에서 PAGE_WRITECOPY로 설정.
  • FILE_MAP_EXECUTE : 데이터를 코드로 수행할 수 있다. 

파일을 주소 공간에 매핑할 때 파일 전체를 한꺼번에 매핑할 수도 있고, 
파일의 일부분만 분리해서 주소 공간에 매핑할 수도 있다.

이렇듯 주소 공간에 매핑된 영역을 뷰(view)라고 한다.
(MapViewOfFile 함수의 이름은 이로부터 유래한 것이다)


프로세스의 주소 공간에 파일을 매핑하기 위해서는 두 가지 추가적인 정보가 필요하다.
  • 파일의 어디부터 매핑할 것인가?
  • 파일의 얼마만큼을 매핑할 것인가?
어디부터에 해당하는 것이 dwFileOffsetHigh와 dwFileOffsetLow가 되는 것이고,
얼마만큼에 해당하는 것이 dwNumberOfBytesToMap이 되는 것이다.

파일의 오프셋 값은 반드시 시스템의 할당 단위(allocation granularity)의 배수여야 한다.
(모든 윈도우 시스템은 64KB 할당 단위를 사용한다)

그리고, 얼마만큼에 해당하는 dwNumberOfBytesToMap 값을 0 으로 설정하면,
offset으로부터 파일의 끝까지를 view로 구성하려 시도한다.

MapViewOfFile은 파일 매핑 오브젝트의 전체 크기는 고려하지 않으며,
단지 view에 필요한 크기만을 고려하여 영역이 충분한지를 확인한다.

참고로, MapViewOfFile 파일 오브젝트 파일 매핑 오브젝트 usage count를 증가시킨다.
즉, MapViewOfFile을 하게 되면, 반드시 UnmapViewOfFile을 해 주어야 usage count가 감소하게 되고,
CloseHandle(hFileMapping)을 통해 완전히 파일 매핑 오브젝트를 닫을 수 있게 된다.


UmmapViewOfFile

프로세스의 주소 공간 내의 특정 영역에 매핑된 데이터 파일을 더 이상 유지할 필요가 없다면,
UnmapViewOfFile 함수를 호출하여 영역을 해제해 주어야 한다.

BOOL UnmapViewOfFile(PVOID pvBaseAddress);

유일한 매개변수인 pvBaseAddress는 해제할 영역의 주소이며,
MapViewOfFile 함수의 반환값과 반드시 동일한 값을 사용해야 한다.

MapViewOfFile은 앞서 예약되었던 영역을 삭제하지 않고, 매번 새로운 주소 공간에 영역을 확보하기에,
다 쓴 영역은 UnmapViewOfFile을 호출하여 해제해 주는 것이 바람직하다.


CloseHandle

매핑할 파일 생성하거나 여는 작업과 해당 파일을 이용해 파일 매핑 오브젝트를 만드는 것 모두
파일 오브젝트와 파일 매핑 오브젝트를 생성하고 이에 대한 핸들을 반환한다.

따라서, 반드시 CloseHandle을 통해 올바르게 반환이 이루어질 수 있도록 하는 것이 바람직하다.


Internded Use Of Memory Mapped Files

메모리 맵 파일의 특징과 사용법에 대해 알아 보았다.

지금부터 메모리 맵 파일의 사용 목적에 따른 특징에 대해 알아보자.


실행 파일과 DLL 파일을 읽고 수행

CreateProcess를 수행하면 시스템은 다음과 같은 절차를 순차적으로 수행한다.
  1. 매개변수로 전달된 .exe 파일을 찾는다.
  2. 새로운 프로세스 커널 오브젝트를 생성한다.
  3. 새로운 프로세스를 위한 전용 주소 공간을 생성한다.
  4. .exe 파일을 수용할 수 있을 만큼의 충분한 영역을 주소 공간 내에 예약한다.
  5. 예약된 영역에 사용할 물리 저장소로 시스템의 페이징 파일 대신 .exe 파일 자체를 지정한다.
  6. 프로세스의 주 쓰레드를 생성한다.
  7. 실행 가능한 첫 번째 바이트를 가리키는 주소 값을 이 쓰레드의 IP (인스트럭션 포인터)로 설정하여,
    해당 코드를 수행하게 한다.
위 과정 중 4, 5번 과정에서 메모리 맵 파일이 사용된다.

4, 5번 과정을 조금 더 자세하게 풀면 아래와 같다.
  1. 시스템은 디스크에 있는 .exe 파일에 대해 CreateFile을 호출하여 열기 작업을 수행한다.
  2. CreateFileMapping 함수를 호출하여 파일 매핑 오브젝트를 생성한다.
  3. MapViewOfFileEx(SEC_IMAGE 플래그를 인자로) 함수를 호출하여, 
    새롭게 생성된 프로세스를 대신하여 .exe 파일을 프로세스의 주소 공간에 매핑해 준다.
위에서 MapViewOfFile이 아닌 MapViewOfFileEx를 사용한다 하였다.
이는 프로세스가 시작될 때 .exe 파일의 시작 주소를 지정해 주어야 하기 때문이며, 이는 DLL의 경우도 마찬가지이다.
(이는 VirtualAlloc시 예약할 주소 공간의 시작 주소를 정하는 것과 유사하다)

MapViewOfFileEx 함수는 MapViewOfFile 함수에 비해 하나의 파라미터를 더 가진다.
바로 시작 주소를 뜻하는 PVOID pvBaseAddress 인데, 이를 NULL로 넘기면 MapViewOfFile과 완전히 동일하게 동작한다.

이렇게 실행 파일이나 DLL에 대해 메모리 맵 파일을 이용하게 함으로써 (즉, 페이징 파일을 사용하지 않음으로써)
시스템은 여러 실행 파일을 실행시켜도 페이징 파일 크기를 일정하게 유지할 수 있게 된다.
또한, 애플리케이션의 시작 시간도 어느 정도는 일정하게 유지할 수 있게 된다.

.exe / DLL 파일이 프로세스의 주소 공간에 매핑된 이후엔, 
시스템이 페이징, 버퍼링, 캐싱과 관련된 모든 작업들을 시스템이 직접 관리해준다.

예를 들어, .exe 파일 내의 코드를 수행하는 중에 아직까지 프로세스 주소 공간으로 
로드되지 않은 주소로 점프를 수행하게 되면, 폴트가 발생하게 되는데
이 때 시스템은 이러한 폴트를 인지하고 자동으로 파일 이미지를 램의 페이지로 로드해 준다.

이 후 이미지를 로드한 램 페이지를 적잘한 프로세스 주소 공간으로 매핑시킨다.
이러한 작업이 완료되면 마치 처음부터 수행할 코드가 주소 공간 상에 로드되어 있었던 것처럼 쓰레드는 수행을 재개하게 된다.

프로세스가 수행되는 동안 램에 로드되지 않은 코드나 데이터에 대한 접근이 일어날 때마다 
위와 같은 작업들이 반복적으로 일어나게 된다.

DLL의 경우 메모리 맵 파일이 아닌 페이징 파일로 매핑될 때가 있다.

각각의 DLL 파일에 대해 LoadLibrary가 호출되면 .exe 실행 과정의 4, 5번 과정과 유사한 작업이 수행된다.

1) 시스템은 DLL 파일을 수용할 수 있는 충분한 영역을 주소 공간 내에 예악한다.
DLL 파일이 선호하는 시작 주소는 파일 내에 기록되어 있는데,
x86 DLL 파일에 대해서는 기본 시작 주소를 0x10000000으로,
x64 DLL 파일에 대해서는 기본 시작 주소를 0x00400000으로 설정하고 있다.
(이 값들은 /BASE 링커 옵션을 통해 변경할 수 있다)

참고로, 윈도우와 함께 제공되는 시스템 DLL들을 서로 다른 시작 주소를 가지고 있기 때문에,
단일의 프로세스 주소 공간에 로드되더라도, 서로 겹치지 않는다.

2) DLL 파일이 선호하는 시작 주소에 로드되지 못하면, (.exe가 직접 그 주소로 또는 이미 다른 DLL이 해당 주소에 로드된 경우)
시스템은 프로세스의 주소 공간으로부터 DLL 파일을 로드할 수 있는 다른 영역을 찾게 된다.

DLL 파일이 자신이 선호하는 주소에 로드되지 못하는 경우 두 가지 중 하나의 문제가 발생한다.

a. DLL 파일에 재배치 관련 정보가 포함되어 있지 않은 경우, 로드는 실패하게 된다.

b. DLL 파일에 재배치 관련 정보가 포함된 경우,
시스템은 DLL 파일에 대해 재배치 작업을 수행해야 하는데,
이 경우 DLL 파일을 위해 예약된 영역은 시스템 페이징 파일로 매핑된다.

즉, 시스템 페이징 파일에 추가적인 저장소를 필요로 하게 되며, 이는 DLL 파일을 로드하는데 더 많은 시간이 소비됨을 의미한다.

따라서, 자신이 만든 프로그램에서 여러 DLL을 로드해야 될 때,
해당 DLL들의 시작 주소를 적절히 분배하는 것이 성능상 유리하다.


디스크에 있는 데이터 접근

파일의 메모리 매핑은 프로세스의 가상 메모리 일부분을 디스크에 있는 파일의 블록에 매핑함으로써 이루어진다.

첫 접근은 일반적인 페이징 과정에 따라 페이지 부재를 발생시킨다.
그때 그 파일의 내용 중 페이지 크기 만큼의 해당 부분이 파일 시스템으로부터 가상 메모리 페이지로 읽혀 들어오게 된다.
그 이후 파일의 I/O는 다른 메모리 엑세스나 마찬가지로 취급하여 파일 접근과 사용을 단순하게 만들어 준다.
또한, read/write 시스템을 호출할 때마다 발생했던 오버헤드 비용을 줄일 수 있다.


메모리 맵 파일을 사용하면, 파일에 대한 I/O 작업이나 파일의 내용에 대한 버퍼링을 자동으로 수행해 준다.

메모리 맵 파일을 사용하는 것이 얼마나 편리한지를 이해하기 위해 파일의 내용을 바이트 단위로 뒤집는 방법에 대해 생각해 보자.

이를 일반적으로 해결하려면 아래 방법들을 사용해야 할 것이다.

1) 한 개의 파일, 한 개의 버퍼

파일을 열고, 하나의 버퍼를 이용해 그 내용을 모두 읽어온 뒤 파일을 닫는다.
이제, 버퍼의 내용을 앞/뒤를 모두 바꾸는 작업을 수행하면 된다.

이는 두 가지 단점이 존재한다.

a. 32비트 시스템에서 파일의 크기가 2GB 를 넘어서면, 모든 내용을 버퍼로 로드할 수 없다.
프로세스 주소 공간 내 유저 영역이 2GB 밖에 되지 않기 때문이다.

b. 뒤집은 내용을 파일에 다시 쓰다가, 중간에 실패가 뜨면, 파일의 내용이 손상된다.

2) 두 개의 파일, 한 개의 버퍼

1)번 방법의 단점들을 커버하기 위한 방법이다.

특정 크기의 버퍼 단위로 파일의 내용을 끊어 로드한다.
이후 버퍼 단위로 뒤집기를 수행하고, 원본 파일에 데이터를 갱신하는 도중 실패하는 것을 방지하기 위해
A 파일의 내용을 일부 뒤집고, 이를 B 파일에 쓰도록 하는 것이다.

이 역시 두 가지 단점이 있는데...

a. 파일의 내용을 조금씩 조금씩 읽어 나가기에, 파일 포인터를 그만큼 움직여야 한다.
즉, 첫번째 방법에 비해 수행 속도가 느리다.

b. 하드 디스크가 두 배로 소비된다. 그리고 작업이 끝난 뒤 원본 파일을 별도로 지워줘야 한다.

이처럼, 파일의 내용 하나 뒤집는데도 추가적인 비용이 많이 발생하게 되는데,
메모리 맵 파일을 사용함으로써 이 같은 번거로움이나 추가 비용을 회피할 수 있게 된다.

메모리 맵 파일을 이용하여 파일의 내용을 뒤집기 위해서는 파일을 열고 가상 주소 공간 상에 영역을 예약한 뒤,
파일의 첫 번째 바이트와 예약한 영역의 첫 번째 위치를 매핑시킨다.

이후 가상 메모리 주소에 접근하게 되면, 이는 마치 파일의 내용에 직접적으로 접근하는 것과 같은 효과를 가져온다.

즉, 별도의 버퍼도 필요하지 않고, 파일의 내용을 다시 써나가야 하는 불편함도 사라진다.
시스템이 파일에 대한 캐싱 작업을 직접 수행해 주기 때문이다.


동일 머신에서 수행 중인 다수의 프로세스 간 데이터 공유

윈도우는 프로세스간 데이터는 전달하는 다양한 방법들을 제공하고 있지만, 
내부적으로는 모두 메모리 맵 파일을 사용하여 구현되었으며, 
실제로 메모리 맵 파일을 사용하는 것이 단일 머신에서는 가장 효과적인 방법이다.

둘 이상의 프로세스 사이에 데이터를 공유하려면, 동일 파일 매핑 오브젝트에 대해 각 프로세스별로 뷰(view)를 매핑하면 된다.
이렇게 하면 각 프로세스들은 동일 물리 저장소(파일이든 페이징 파일이든)를 공유하게 된다.



특정 프로세스에서 이처럼 공유되는 파일 매핑 오브젝트의 뷰 내의 데이터를 변경하게 되면,
다른 프로세스는 자신의 뷰를 통해 이러한 변경 사항이 즉각 반영되는 것을 확인할 수 있다.

이를 메모리 맵 파일의 일관성이라고 한다.

단일의 매핑 파일 오브젝트를 사용한다면, 
매핑된 뷰가 여러 개 일지라도, 각 뷰의 오프셋과 크기가 다를 지라도,
서로 다른 프로세스라 할 지라도, 파일 내의 데이터에 대한 일관성이 유지된다.

하지만, 동일 파일에 대해 다중 매핑 파일 오브젝트를 사용하는 경우엔 일관성이 유지되지 않는다.
(이것이 위에서도 얘기했듯이, CreateFile시 dwShareMode를 0 으로 설정하라는 이유이다)

예를 들어 살펴보자.

애플리케이션을 수행하면, 위 3-1에서 설명했듯이
.exe/DLL 파일을 실행시키기 위해 메모리 맵 파일을 사용한다.

만일 사용자가 동일 애플리케이션의 두 번째 인스턴스를 수행하였다면,
시스템은 새로운 파일 오브젝트나 파일 매핑 오브젝트를 생성하지 않고,
첫 번째 인스턴스를 수행할 당시 사용하였던 파일 매핑 오브젝트를 이용하여 뷰를 매핑한다.

물론, 각 프로세스 별로 매핑되는 가상 주소는 상이할 수 있다.

이렇게 함으로써 시스템은 동일한 파일을 두 개의 서로 다른 주소 공간에 동시에 매핑하게 된다.
이러한 동작 방식은 실행할 코드를 포함하고 있는 물리 저장소의 동일 페이지를
프로세스 상호 간에 공유하게 되므로 메모리 사용 효율 면에서는 상당히 효과적인 방법이라 할 수 있다.


하지만, 모든 변경 사항이 즉각적으로 다른 프로세스에 반영되는 것은 아니다.

하나의 프로그램에 대해 멀티 인스턴스를 수행하는 경우를 다시 예로 삼아보자.

우선 애플리케이션을 구성하는 코드와 데이터가 실제로 어떻게 가상 메모리에 로드되고,
애플리케이션의 주소 공간에 매핑되는지를 살펴보자.


이제 애플리케이션의 두 번째 인스턴스가 수행되면, 

시스템은 가상 메모리에 로드되어 있는 코드와 데이터를 두 번째 인스턴스 주소 공간에 매핑한다.

이 때 특정 인스턴스가 데이터 영역에 포함되어 있는 전역 변수의 값을 변경한다고 가정해보자.

이러면, 동일 프로그램의 모든 인스턴스의 메모리 내용이 모두 변경되게 되는데,
이는 재앙에 가까운 문제를 유발하게 될 것이다.

시스템은 프로세스를 실행할 때, 어느 영역이 copy-on-write에 의해 보호되어야 하는지를 마킹해 둔다.
(페이지 보호 속성을 PAGE_COPYWRITE 로 설정한다)

그리고, 해당 페이지를 변경하려 하면, 복사본을 만들고 그 복사본에 값을 쓴다.
이렇게 함으로서, 다른 인스턴스의 메모리를 변경하지 않게 되는 것이다.

이 과정은 프로세스가 차일드 프로세스를 생성하고 둘 중 하나가 

데이터 영역의 데이터를 변경하려 할 때, 발동되는 copy-on-write 기능과 동일하다.


페이징 파일을 이용하는 메모리 맵 파일

지금껏 디스크 드라이브 상에 존재하는 파일에 대한 뷰를 매핑하는 기법에 대해 알아보았다.

많은 애플리케이션들이 수행 중에 데이터를 생성하고, 
이러한 데이터들을 다른 프로세스에 전달하거나 공유해야 할 필요가 있다.

하지만, 이를 위해 일일히 디스크 상에 파일을 만들어야 하고, 
또 그 안에 데이터를 저장해야 한다면 이를 공유하는 과정이 사실 편한 것만은 아니다.

이러한 불편함을 해소하기 위해 윈도우는 시스템의 페이징 파일을 이용하여 메모리 맵 파일을 생성하는 방법을 제공하고 있다.

메모리에 매핑할 파일을 열거나 생성할 필요가 없기에, CreateFile을 호출할 필요도 없다.

대신 CreateFileMapping 함수를 호출할 때, hFile 파라미터로 INVALID_HANDLE_VALUE를 전달해주면 된다.
INVALID_HANDLE_VALUE가 인자로 넘어가게 되면, 시스템은 페이징 파일을 물리 저장소로 사용하는 것으로 인지한다.

이 부분에서 대단히 조심해야 할 것이 하나 있다.

특정 파일을 이용하여 파일 매핑 오브젝트를 사용하려 하는 다음의 예제를 살펴보자.
1
2
3
4
5
6
7
8
HANDLE hFile = CreateFile(...); 
 
// hFile에 대한 유효성 체크 없이 CreateFileMapping을 바로 호출하였다.
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
if (nullptr == hFileMapping)
{
    return GetLastError();
}
cs


위 예제에서 CreateFile이 실패했다면 어떻게 될 것인가?

프로그래머가 의도한 바와 다르게 디스크 상에 존재하는 특정 파일을 위한 파일 매핑 오브젝트를 생성하는 것이 아니라,
시스템 페이징 파일을 저장소로 하는 파일 매핑 오브젝트를 생성하게 된다.

이후 모든 과정이 정상처럼 보이겠지만,
파일 매핑 오브젝트가 파괴되는 순간, 그 동안 변경되었던 내용은 모두 사라지게 될 것이다.
(의도했던 특정 파일에 내용이 반영되지 않게 되는 것이다)


메모리 맵 파일을 이용하여 큰 파일 처리하기

(CreateFileMapping) 설명에서 잠시 비추었지만,
32비트 프로세스에서 2GB 이상의 메모리 맵 파일을 핸들링하는 것은 사실 불가능하다.

2GB 이상의 파일을 메모리 맵 파일을 통해 핸들링 하는 방법은

파일의 일부분만을 특정 뷰를 통해 접근한 이후에 매핑을 해제하고,
다시 파일의 다른 부분에 대한 뷰를 구성하여, 프로세스 주소 공간에 매핑하는 과정을 반복해야 한다.

사실 불편하긴 하다만, 2GB 넘는 파일을 핸들링할 일이 그닥 잦지 않다는 점이 다행이라면 다행이다.

8GB의 파일을 1024 KB(1MB) 단위로 쪼개어, 핸들링하는 예제를 살펴보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
void HandleBigFile()
{
    // 뷰는 항상 할당 단위의 배수로 시작해야 한다.
    SYSTEM_INFO si;
    GetSystemInfo(&si);
 
    // 읽기 전용으로 파일을 연다.
    HANDLE hFile = CreateFile(TEXT("C:\\BigFile.dat"),
                              GENERIC_READ, 0, NULL, OPEN_EXISTING,
                              FILE_FLAG_SEQUENTIAL_SCAN, NULL);
 
    // 파일의 크기 만큼 파일 매핑 오브젝트를 연다.
    HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READ_ONLY, 000);
    
    // 파일의 크기를 구한다.
    DWORD dwFileSizeHigh;
    __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
    qwFileSize += ( ((__int64)dwFileSize) << 32 );
 
     __int64 qwFileOffset = 0;
 
     while (qwFileSize > 0)
     {
        // 만약 남은 파일 크기가 1MB보다 적다면, 남은 크기만큼만 뷰로 맵핑한다.
        DWORD dwBytesInBlock = sinf.dwAllocationGranularity * 16;
        if (qwFileSize < sinf.dwAllocationGranularity * 16)
        {
            dwBytesInBlock = qwFileSize;
        }         
        PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_READ,
                                            (DWORD)(qwFileOffset >> 32),  // 상위 오프셋
                                            (DWORD)(qwFileOffset & 0xFFFFFFFF), // 하위 오프셋
                                            dwBytesInBlock);         
 
        // 뷰 내의 메모리에 대해 처리를 한다
        // 뷰를 다 썼으므로, 뷰를 해제한다.
        UnmapViewOfFile(hFileMapping);
 
        // 오프셋 및 남은 파일 크기 갱신
        qwFileOffset += dwBytesInBlock;
        qwFileSize -= dwBytesInBlock;
    }
 
    // 처리가 완전히 끝났으므로 파일 매핑 오브젝트와 파일 오브젝트를 닫아준다.
    CloseHandle(hFileMapping);
    CloseHandle(hFile);
}
cs


출처

http://egloos.zum.com/sweeper/v/2990023

http://egloos.zum.com/anster/v/2156072


  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기