관리 메뉴

HAMA 블로그

메모리맵 파일과 자바 FileChannel 클래스 본문

운영체제

메모리맵 파일과 자바 FileChannel 클래스

[하마] 이승현 (wowlsh93@gmail.com) 2015. 9. 29. 11:37


메모리맵 파일이란?

거의 대부분의 어플리케이션들이 파일에 대한 i/o 작업을 수행함에도 불구하고,  파일에 대한 작업은 항상 개발자들을 괴롭혀온 요소 중 하나임이 틀림없다. 어떻게 파일을 열고, 읽고, 닫는 것이 좋은가 혹은 파일을 열고나서 그 내용을 읽고 쓸 때 얼마만큼의 내용을 버퍼링 하는것이 좋은가와 같은 의문들이 우리를 괴롭히는 좋은 예라 하겠다. 윈도우즈 OS 는 이 두가지 서로다른 형태의 질문에 대한 최상의 해결책을 메모리 맵 파일로 제시한다. 메모리 맵 파일 기능은 가상 메모리처럼 주소 공간을 예약하고, 예약된 영역에 물리적 저장소를 커밋하는 기능을 제공하고있다. 유일한 차이점이라면 시스템의 페이징 파일을 사용하는 대신 디스크 상에 존재하는 파일을 물리적 저장소로 사용한다는것이다. 이러한 파일이 일단 영역에 매핑되면 마치 메모리에 파일의 내용이 모두 로드된것처럼 사용할수 있다.


메모리맵 파일이 사용되는 3가지 이유 

- 시스템은 .exe 나 DLL 파일을 읽고 수행하기 위해 메모리 맵 파일을 사용한다. 메모리 맵 파일을 사용함으로써 시스템은 페이징 파일의 크기를 일정하게 유지할수 있으며, 어플리케이션의 시작 시간도 일정하게 유지할수있다.
- 디스크에 있는 데이터에 접근하기 위해 메모리 맵 파일을 사용할수 있다. 메모리 맵 파일을 사용하면 파일에 대한 i/o 작업이나 파일의 내용에 대한 버퍼링을 자동적으로 수행해준다.
- 동일한 머신에서 수행 중인 다수의 프로세스 간에 데이터를 공유하기 위해 메모리 맵 파일을 사용할수있다. 윈도우는 프로세스들 사이에 데이터를 전달하는 다양한 방법들을 제공하지만 내부적으로는 모두메모리 맵 파일을 사용하여 구현되었으며, 실제로 메모리 맵 파일을 사용하는것이 단일의 머신에서 프로세스 간 데이터를 전달하는 가장 효과적인 방법이다.


장점과 단점



장점

직접적인 파일 입출력을 수행할 필요가 없다. 메모리 맵 파일을 사용하면 파일 내용이 메모리 주소에 사상되므로 파일을 모두 불러온 것처럼 메모리 주소를 이용하여 연산 작업을 할 수 있어 코드가 깔끔해지고 유지보수가 편해진다.

버퍼나 파일 처리를 위한 추가적인 자료 구조가 필요 없다. 운영 체제에서 페이징 기법을 사용하여 파일의 내용을 관리하며, 페이지 크기(보통 4KiB)에 따라 적절히 파일의 내용을 읽고 쓸 수 있으며 파일 반영(flush) 작업도 수행해준다.

대용량의 자료를 처리할 때도 매우 효율적이다. 파일에 접근할 때 지연 적재를 이용하므로, 파일의 크기가 매우 크더라도 필요한 부분만 파일에서 불러와 작업할 수 있으며, 작업이 끝난 데이터는 자동으로 파일에 반영된다.

전통적인 파일 입출력 API보다 속도가 빠르다. API는 시스템 호출을 사용하기 때문에 작업을 수행하는 동안 유저 모드와 커널 모드를 전환하는 데 필요한 인터럽트가 오버헤드로 작용하게 된다. 메모리 맵 파일은 4KiB 단위로 자료를 미리 불러올 때 발생하는 페이지 부재외의 모든 작업이 실제 메모리상에서 이루어지므로 대부분 파일 API를 통한 파일 처리보다 빠르다.[1]

단점

POSIX와 윈도에서 파일의 크기(즉, EOF의 위치)를 변경할 수 없다. 파일 입출력 API에서는 간편한 방법으로 파일의 크기를 변경하는 방법을 제공한다.[2] 메모리 맵 파일은 파일의 크기를 바꿀 수는 없으며 메모리 맵 파일을 사용하기 이전, 또는 이후에만 파일의 크기를 바꿀 수 있다.[3]

메모리 맵 파일을 이용한 접근은 최적의 파일 접근 방식은 아니다. 파일 입출력 API를 사용한 방법이 API의 오버헤드인 것에 비해 메모리 맵 파일에서는 페이지 부재중의 데이터 전송 시간이 오버헤드로 작용한다.[4] 따라서 데이터베이스와 같이 파일을 읽고 쓰는데 오버헤드를 최소로 줄인 자료구조와 알고리즘과 메모리 맵 파일을 비교할 때 오히려 메모리 맵 파일이 느릴 수도 있다.

크기가 지나치게 큰 파일을 처리하는 데 어려움이 있다. IA-32 기반 시스템에서 하나의 프로세스에서 PAE 기술을 사용하지 않고 사용 가능한 최대 크기는 4GB로 제한된다.[5] 따라서 프로세스의 메모리 주소를 사용하는 메모리 맵 파일이 한번에 다룰 수 있는 크기는 이보다 작아지며, 이 크기를 넘어가는 파일을 메모리 맵 파일로 다루려 할 때 계산이 복잡해질 수 있다.


언어별 지원


- 자바에서는 FileChannel 클래스를 통해 메모리 맵 파일 기능을 제공한다.

- 닷넷 프레임워크에서는 MemoryMappedFile 클래스를 통해 메모리 맵 파일을 제공하고 있다.

- 파이썬에서는 mmap 클래스를 통해 메모리 맵 파일을 제공하고 있다. 다만, 윈도와 POSIX 기반     운영 체제에서 클래스 구조가 다르므로 주의가 필요하다.

- Boost에서도 메모리 맵 파일기능을 제공한다.



예제 1



메모리 맵 데이터 파일을 사용하는것이 얼마나 편리한지를 이해하기 위해 파일의 내용을 바이트 단위로 뒤집는 네가지 방법에 대해 살펴보도록하자.


방법1 : 한개의 파일, 한개의 버퍼

  => 파일을 열고, 버퍼에 담고, 뒤짚은후에 , 다시 파일에 쓴다.

방법2: 두개의 파일, 한개의 버퍼

  => 파일을 열고, 일정량을 읽고, 다른 파일 ( 아무것도 없는) 을 열어서, 그 파일에다 쓴다. 

방법3: 한개의 파일, 두개의 버퍼

  => 파일을 열고,  파일의 앞부분, 뒷부분을 각각 버퍼에 담은후에, 뒤짚은후에 파일에 쓴다.

방법4: 한개의 파일, 버퍼는 사용하지 않음 

  => 파일을  가상주소공간상에 매핑한후에 그대로 뒤짚는다. (_tcsrev C 함수 이용)


예제 2



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

굉장히 큰 파일을 메모리맵 파일로 처리하려면, 한방에 파일 전체를 매모리에 매핑하는것은 불가능하다.
대신 파일 데이터의 일부분만을 나타낼수 있는 뷰를 주소 공간에 매핑해야한다. 
이렇듯 일부분만 뷰로 접근한후에, 매핑을 해제하고 다시 파일의 다른 부분에 대한 뷰를 구성하는식으로 반복해야한다. 


// 1. 데이터 파일을 열고

// 2. 파일 매핑 오브젝트를 생성하고 

// 3. 반복하면서 뷰를 통해 파일 내의 어느 부분을 얼마만큼 매핑할지를 결정한다. 

// 3.1뷰 내의 블록으로부터 비지니스 로직을 실행한다.

// 3.2뷰를 해제한다. 

// 3.3파일 내의 다른 영역 계산 


3번을 

 파일 끝까지 반복한다.


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



자바의 FileChannel


nio

http://examples.javacodegeeks.com/core-java/nio/filechannel/java-nio-channels-filechannel-example/

http://javarevisited.blogspot.kr/2012/01/memorymapped-file-and-io-in-java.html

The theme of wrapping ByteBuffer objects around arbitrary memory spaces continues withMappedByteBuffer, a specialized form of ByteBuffer. On most operating systems, it's possible to memory map a file using the mmap() system call (or something similar) on an open file descriptor. Calling mmap()returns a pointer to a memory segment, which actually represents the content of the file. Fetches from memory locations within that memory area will return data from the file at the corresponding offset. Modifications made to the memory space are written to the file on disk.

memory mapping
Figure 7: User memory mapped to the filesystem.

There are two big advantages to memory mapped files. First, the "memory" does not usually consume normal virtual memory space. Or, more correctly, the virtual memory space of a file mapping is backed by the file data on disk. That means it's not necessary to allocate regular paging space for mapped files; their paging area is the file itself. If you were to open the file conventionally and read it into memory, that would consume a corresponding amount of paging space, because you're copying the data into regular memory. Second, multiple mappings of the same file share the same virtual address space. Theoretically, 100 mappings can be established by 100 different processes to the same 500MB file; each will appear to have the entire 500MB of data in memory, but the overall memory consumption of the system won't change a bit. Pieces of the file will be brought into memory as references are made, which will compete for RAM, but no paging space will be consumed.

In Figure 7, additional processes running in user space would map to that same physical memory space, through the same filesystem cache and thence to the same file data on disk. Each of those processes would see changes made by any other. This can be exploited as a form of persistent, shared memory. Operating systems vary in the way their virtual memory subsystems behave, so your mileage may also vary.

MappedByteBuffer instances are created by invoking the map() method on an open FileChannel object. The MappedByteBuffer class has a couple of additional methods for managing caching and flushing of updates to the underlying file.

Prior to NIO, it wasn't possible to memory map files without resorting to platform-specific, non-portable native code. It's now possible for any pure Java program to take advantage of memory mapping, easily and portably.



레퍼런스:

제프리리처의 Windows via C/C++

위키백과 

자바I/O & NIO  네트워크 프로그래밍 (김성박/송지훈) 



Comments