Communication between Virtual Memory Manager and File System
from Drafts/Kernel mode 2010/02/17 11:03NT 가상 메모리 매니저와 파일 시스템
Communication between Virtual Memory Manager and File System
가상 메모리 매니저를 다루는 이유는 결국 파일 시스템 드라이버 입장에서 가상 메모리 매니저의 동작을 이해하고 이를 적절히 운용하여 시스템의 성능을 올리기 위해서이다. 대부분의 운영체제에서 가상 메모리 매니저와 파일 시스템간의 정보를 정의 하고 있으나 실제 NT에서는 가상 메모리 매니저의 코드를 볼 수 없기 때문에 어떠한 인터페이스를 통해서 기존 운영체제의 컨셉을 이용하고, 시스템을 조율 할 지가 난해 할 수 있다. 파일 시스템 드라이버 개발자는 가상 메모리 매니저에 의해서 운용되는 사상된 메모리 이미지(Mapped Memory Image)나 가상 메모리 매니저만이 오직 조작 할 수 있는 물리 메모리 주소 영역을 이해하고 파일 시스템 드라이버가 동작하는 시스템 가상 주소 공간을 운용하기 위한 동작을 이해 해야 할 뿐만 아니라, 두 모듈간의 의존성을 파악하여 시스템을 설계 해야 한다.
정명수
|
필자는 지난 3년간 삼성전자에서 플래시 메모리와 관련된 연구와 임베디드 소프트웨어, 커널 드라이버 등을 개발 했었다. 현재는 조지아 공대(Georgia Institute of Technology) 컴퓨팅 칼리지에 재학 중이다. 글쓰기를 매우 좋아하며 학부시절에는 객체 지향 패러다임을 통하여 해석하는 프로그래밍 언어론에 관심이 있었으나 실무과정을 거치면서 컴퓨터 아키텍처로 관심사가 옮겨졌다. 최근에 관심 있는 분야는 운영체제, 파일시스템, 실시간 스케줄링 등이다.
NT의 가상 메모리 매니저의 마지막 칼럼으로서 메모리 매니저가 파일 시스템 드라이버나 커널 드라이버에게 노출 시켜 놓은 함수나 루틴들을 알아보고 그 안의 동작을 이해하도록 한다. 이러한 가상 메모리 매니저의 API들은 대부분 파일 시스템 드라이버 개발자에게 노출 되어있고 매우 중요한 역할을 하고 있으나 실제로 잘 문서화가 되어 있지 않기 때문에 파일 시스템 드라이버 개발자가 간과 하기 쉽다.
하지만 현재 파일 시스템 드라이버가 운용되는 시스템 가상 주소 공간과 메모리 할당, 그리고 사용자 모드의 어플리케이션과 통신을 위해서 어디까지가 파일 시스템의 영역이고 어디까지가 가상 메모리 영역까지 인지를 판단하여 시스템의 성능을 향상하도록 설계하기 위해서는 가상 메모리 매니저의 내부와 이를 이해하여 파일 시스템 정책을 적절히 추가 해야 한다. 이번 칼럼에서는 메모리 페이징을 위한 서로 간의 영역과 역할 차이, 그리고 운영체제의 핵심이 되는 페이지 폴트 핸들링, 마지막으로 파일 시스템과의 교류를 위한 여러 가지 가상 메모리 매너저의 API들을 소개한다.
MPW, 수정되거나 사상된 페이지 쓰기 스레드
이전에 컬럼에 언급 되었듯이, NT 가상 메모리 매니저 (VMM, Virtual Memory Manager)는 물리적 메모리 제약과 관계 없이 각 프로세스마다 자신의 독립된 가상의 메모리 공간을 제공하기 위한 태스크(Task)를 가지고 있다. 이 태스크를 위해서 NT VMM은 메모리에 존재하는 페이지 데이터와 정보들을 블록 디바이스(Block Device)로 내보 내거나(스왑 아웃, Swap out이라고도 불린다.) 블록 디바이스로 내보냈던 데이터를 다시 메모리로 불러 들어오는 동작(스왑 인, Swap in)을 지원 해야 한다. 페이징(Paging)이라고 부르는 시스템에서 수행 되는 프로세스에 대해서 모두 균등하게 적용 된다. 적은 물리 메모리를 가지고서 상대적으로 큰 메모리를 사용자에게 보여주기 위해서는 가상 메모리에 할당된 물리 메모리의 양에 한계가 발생할 때 앞서 말한 스왑 인,아웃 작업을 통해 새로운 페이지 프레임(Page Frame)을 할당 받아야만 한다. 따라서 이런 작업을 위해 NT VMM은 자동적으로 데이터가 쓰여진 페이지(더티 페이지, Dirty Page)나 수정된 데이터를 블록 디바이스로 플러시(Flush)하게 된다. 이렇게 페이지 프레임 안에 수정된 데이터는 크게 두 가지 방법으로 블록 디바이스로 스왑 아웃 된다. 첫 번째는 하나에서 열 여섯 개의 페이지 파일을 통해서 수정된 데이터를 쓰는 작업을 통해 스왑 아웃 하는 것이다. 만약 페이지 파일이 사상된 섹션 오브젝트(Mapped Section Object)로 할당 되어 있다면 수정된 데이터는 디스크상에 이름이 있는 파일 형태로 저장된다. 만약 페이지 프레임이 디스크로 플러시 되지 않는다면 NT VMM은 페이지 프레임을 재 사용 할 수 없고 이는 데이터 손실로 이어진다.
메모리 사용이 요청에 대해서 언제든지 서비스 할 수 있도록 NT VMM은 항상 사용 가능한 충분한 램(Ram)을 가지고 있어야 한다. 이를 위해서 NT VMM은 일정량의 사용 가능한 페이지 프레임을 고정적으로 보유하고 있다. 따라서 이 페이지 프레임은 NT VMM이 페이지 할당 시에 이를 사용 할 수 있도록 어떤 수정된 데이터도 가지고 있지 않아야 한다. 먄약 VMM이 이러한 가용 가능한 페이지 프레임들의 영역을 소유하고 있지 않다면 페이지 프레임을 필요하는 대부분의 프로세스가 블록 되게 된다. 그 프로세스들의 수정된 데이터를 위한 페이지 프레임을 확보 하기 위해 미리 확보된 가용 공간을 페이지 프레임을 사용 하는 것이 아니라 필요 즉시에 스왑 아웃을 하여 이를 확보 하기 때문에 스왑 아웃 시에 블록 디바이스로 접근이 발생 함으로서 이에 대한 지연시간이 모두 프로세스에게 보이게 되기 때문이다. 대부분 이해하고 있듯이 현재까지 컴퓨터 아키텍처에서 속도상 가장 병목을 보이는 것이 I/O이기 때문에 프로세스의 스왑 아웃 작업이 그대로 사용자에게 보여지게 된다면 큰 문제가 될 수 있다.
따라서 NT VMM은 수정되거나 사상된 페이지 쓰기(Modified and Mapped Thread, 이후 MPW로 호칭)에 대한 스레드를 적어도 두 개 이상 소유하고 있다. 이 중에 하나는 비동기적(Asynchronous)으로 사상된 페이지를 페이지 파일에 쓰기 위해서 생성된다. 수정된 페이지 프레임을 쓰든지 아니면 사상된 페이지를 쓰든지 그 기본적인 기능은 동일 하기 때문에 보통은 이 둘을 하나의 통일된 용어로 쓰는 것이 일반적이다. 이 스레드는 앞서 언급 된 병목 현상과 디스크에 대한 지연시간을 감추기 위한 것이기 때문에 항상 NT VMM이 요청에 따라 즉각적으로 할당할 수 있는 페이지 프레임을 보유하도록 해주며 이와 중첩적으로(Overlapped) 블록 디바이스로 데이터를 쓴다. MPW 스레드 각각은 이러한 방법을 통하여 높은 시스템 포퍼먼스를 확보하기 위해 실시간 스레드로 정의 되는데 NT에서는 이를 LOW_REAL_TIME_PRIORITY 보다 한 단계를 더 높은 우선순위를 가진다.
NT VMM은 데이터를 블록 디바이스로 플러시 하기 위해서 앞서 언급된 비동기 쓰기를 사용하는데 이때 I/O 매니저의 IoAsynchronousPageWrite()를 참조한다. 이API 호출은 I/O 매니저에 의해서 해당 사상된 페이지 파일이나 이름이 붙여진 형태의 파일을 가진 디스크 볼륨을 관리하는 파일 시스템에게로 바로 전달 된다. 파일 시스템 개발자는 이런 면 때문에 개발 당시 I/O를 좀 더 세분화 하여 관리 할 필요가 있다. NT VMM에 의해서 수정되거나 사상된 페이지를 플러시할 때 생성된 입출력은 다른 일반적인 입출력과 구분 할 수 있도록 IRP_PAGING_I/O의 파라미터를 가는 요청으로 수행된다. 이 입출력 요청 패킷(IRP, I/O Request Packets)에는 페이징에 대한 정보 말고도 IRP_NOCACHE 플래그도 설정되어 있다. 파일 시스템 개발자가 관심을 가져야 하는 것은 페이징 I/O 쓰기 요청을 처리하면서 절대 다른 페이지 폴트를 발생시켜서는 안된 다는 것이다. I/O 매니저는 다른 케이스의 입출력과 페이징에 대해서 다른 형태로 페이지 쓰기 형태를 관리 하기 때문이다. I/O 매니저는 페이징 I/O의 비동기 쓰기의 커널 APC를 정상적으로 마무리하기 위하여 MiWriteComplete() 루틴을 사용한다.
페이지 폴트 핸들링(Page Fault Handling)
NT VMM 물리 메모리 주소공간에 표현 되지 않는 가상 메모리의 컨텐츠를 참조하는 모든 케이스를 처리 해야 하는 책임이 있다. MMU (Memory Mapping Unit)와 같은 하드웨어가 일반적으로 가상 주소와 물리 주소를 번역하긴 하지만 NT 커널에서는 MMU가 이전 컬럼에 언급된 PTE가 메모리에 없는 경우 이 문제를 해결 하기 위에 직접적으로 매핑 정보에 접근 하는 것이 아니라 VMM에게 해당 문제를 전달 하는 것으로 종료 된다. VMM은 페이지 폴트가 발생 했을 때 커널 모드든 유저 모드든 MmAccessFault()라는 함수로 이 제어권을 넘겨 받는다. 다른 NT함수들도 마찬가지 이겠지만 MmAccessFault()는 VMM이 하는 가장 기본이 되는 기능 중에 하나임에도 불구 하고 그 내부 정보를 알기가 매우 힘들 정도로 기술 문서가 부족하다. 따라서 MmAccessFault()의 함수에 대해서 좀 더 이해하는 시간을 가져 보도록 하자.
MmAccessFault()함수는 크게 3가지 종류의 정보를 받아 들일 수 있도록 파리미터를 정의하고 있다. 하나는 페이지 폴트가 일어난 가상 주소 공간에 대한 정보 이고 다른 하나는 현재 페이지 폴트가 일어난 동작(Operation)에 대한 속성을 나타내는 플래그이다. 다시 말해 폴트가 발생한 메모리 페이지가 쓰기를 하다가 일어난 것인지 아니면 읽기를 하다가 일어 난 것인지를 나타내며 일반적으로 FALSE형태가 읽기상태에서 발생 한 것으로 알려져 있다. 마지막으로는 해당 MmAccessFault() 함수가 사용자, 커널 모드 둘 다를 관리 하므로 이를 구분하기 위한 추가 정보가 하나 더 존재 한다.
VMM이 페이지 폴트를 핸들링하기 위해 제어권을 받으면 우선적으로 하는 것이 현재IRQL이문제가 없는 것인지를 확인 하는 것이다. MInAccessFault()함수는 현재 IRQL레벨을 체크하는데 만약 APC_LEVEL보다 높고 페이지 테이블 디렉토리(Directory)와 페이지 테이블 엔트리(Entry)가 유효하지 않은 것이라면 VMM에게 현재 IRQL 레벨을 디버거 정보로 출력하게 한다. 그 다음이 페이지 폴트 문제를 해결 하기 위해 호출 되는 함수는 MiDispatchFault()함수이다. 이 함수는 처음 이야기 하였듯이 VMM이 페이지 폴트의 제어권을 할당 받은 MmAccessFault() 함수가 페이지 프레임을 유효하게 하기 위해 실행 되는 함수이다. 이 루틴은 가상 주소 공간의 상위 2GB인 시스템 주소 공간가 사용자 주소 공간을 접근 하기 위한 전처리 작업들을 수행한다. 페이지 폴트에 관련된 문제들을 해결 하기 위해 좀 더 세부적인 루틴들이 연이어서 호출 되는데 이런 서브루틴들은 폴트가 발생한 주소에 따라 다르다.
만약 폴트가 일어난 주소가 페이지 파일을 기반으로 하고 있다면 MiResolvePageFileFault()함수가 호출 되는데 이 함수는 우선적으로 MiEnsureAvailablePageOrWait() 함수를 이용하여 해당 페이지 파일로부터 데이터를 읽기 위해서 충분한 페이지 프레임을 메모리로 할당한다. 메모리 할당을 마치고 나면 이 MiResolvePageFileFault() 함수는 PTE로부터 읽기 동작이 지정된 해당 페이지 파일을 파악하고 가용 가능한 물리 페이지의 리스트를 가지고 있는 메모리 디스크립터 리스트(MDL, Memory Descriptor List, 메모리 기술자 리스트라고도 불림) 생성한다. MDL이 생성되고 나면 해당 패이지에 대한 메모리 처리가 진행중인 것을 PTE에 기록 하고서 이를 호출한 MiDispatchFault()에게 0xC0033333으로 사전 예약된 상태를 리턴한다. 0xC0033333로 정의 된 상태 값을 리턴 받은 MiDispatchFault()는 I/O 매니저의 API인 IoPageRead()를 사용하여 페이징에 대한 읽기 동작을 수행한다. 앞선 섹션 MPW 스레드에서 언급 했듯이, 파일 시스템 드라이버는 IRP_PAGING_IO와 IRP_NOCACHE 플래그로 먼저 요청사항을 수행 했기 때문에 이번 IoPageRead를 페이징에 관한 읽기 동적으로 수행 할 수 있다. VMM은 페이지 폴트가 일어난 읽기 요청이 모두 끝날 때 까지 기다리고서 요청이 성공적으로 끝나면 해당 프로세스에 대한 워킹 셋(Working Set Model)에 페이지를 추가 한다. 워킹 셋은 NT 커널에서만 적용 되는 것이 아니라 여러가지 운영체제에서 빈번하게 일어나는 메모리 폴트에 의해 운영체제 자체가 제 기능을 못하게 되는 것을 막기 위해서 사용된다.
워킹 셋에 대한 문제는 NT 가상 메모리 매니저가 언급된 2회전 칼럼에서부터 계속 언급 되어 왔다. 독자의 이해를 돕기 위 워킹 셋에 대한 학술적 정의와 함께 이를 사용하는 모델이 어떤 식으로 설계 되는 지를 첨부 하였다. |
워킹 셋(Working Set) 프로세스의 워킹 셋은 메모리 사용의 다이나믹(Dynamic) 로컬리티를 가진 모델에 쓰이곤 하는데 Peter Denning 에 의해 1980년에 프로세스가 필요로 하는 페이지의 집합(Set)으로 정의 되었다. 좀더 정확한 정의를 살펴 보자면 워킹셋 집합은 WS(t,w) = { (t,t-w) 의 시간 사이에 참조된 페이지들}으로 정의 될 수 있다. 다시 말하면 인터벌 안에 정해진 페이지 참조 수를 채우는 동안의 참조된 페이지의 집합이며 여기에 사용된 t는 일반적인 시간, w는 워킹셋 윈도우 사이즈(working set window size)를 의미한다. 즉 워킹 셋 윈도우(working-set window) 사이즈, w라는 것은 고정된 페이지 참조 수를 이야기 하는 것을 판단 할 수 있다.
워킹 셋을 어떻게 구성하는가에 따라 시스템의 성능이 민감하게 반응하는데 만약에 윈도우 사이즈(참조 수)를 너무 작게 잡으면 로컬리티(Locality)를 모두 내포(Encompass) 할 수 없고 윈도우 사이즈를 크게 잡으면 몇몇 로컬리티(Locality)들을 커버 할 수 있고 만약 무한대로 잡는다면 프로그램 전체를 커버 할 수 있다. 이렇게 워킹 셋은 참조된 페이지들을 의미하는 반면 워킹 셋 사이즈(Working Set Size, WSS)는 워킹 셋 안에 페이지 수를 의미한다. 다시 말해 interval(t, t-w)안에 참조된 된 페이지의 수에 의미를 두는 것으로 로컬리티가 떨어지는 경우에는 더 많은 페이지들이 프로그램의 로컬리티에 따라 WSS는 변화 한다. 직관적으로 봤을 때, 워킹 셋은 반드시 스레싱(Threshing)이라고 불리는 메모리의 자주 일어나는 메모리 폴트 (heavy fault)를 보호 할 수 있어야 한다.
워킹 셋 모델(Working Set Model) 워킹 셋 모델은 워킹셋을 어떻게 사용 할 것인지를 다루는 것으로 특정 페이지가 WS(t, w) 안에 속하였으면 메모리에 남겨두고, 아니면 스왑 아웃 시킨다. 워킹 셋 모델은 이 원칙에 따라 페이지 프레임의 교체 할당, 플러시등의 동작들을 결정한다. 워킹 셋 페이지의 교체를 위해 마지막 k 번 참조된 페이지들의 집합을 유지하는 것은 매 참조 마다 각 페이지들의 최근 참조 수를 워킹 셋 윈도우 사이즈와 비교 해야 하기 때문에 매우 비용이 싸다. 따라서 과거 인터벌 내에 사용된 페이지의 집합으로서 대략적인 워킹 셋을 사용하고 이를 위해서는 아래 도표와 같이 현재 가상 시간(virtual time)을 사용한다.
현재 가상시간(current virtual time)이라는 것은 프로세스가 실제로 사용한 CPU 시간의 총량으로 나타내어지기 때문에 페이지 테이블로부터 워킹 셋 안에 없는 페이지를 찾아서 그것을 교체 대상으로 선정한다. 좀더 상세하게 보자면 각 이벤트 사이의 인터벌(Interval) 시간은 각 PTE의 필드 중 Tlast라고 불리는 마지막 사용 시간(time of last use)값을 이용하고 주기적 클럭 인터럽트(Clock interrupt)에 의해 R 비트는 제거한다. 모든 페이지 폴트 때 교체하기 에 알맞은 페이지를 찾기 위해서 테이블을 스캔 하는데 R비트가 1일때 현재 가상 시간을 타임스탬프 해둔다(Tlast := Tcurrent). 만약 R비트가 0이고 Tcurrent - Tlast가 정해진 시간 동안 레퍼런스가 없어서 특정 수치보다 커질 때 페이지를 교체 한다. 교체로 선정된 대상이 아닐 때는 페이지의 큰 나이값(Age)를 기록 해두었다가 추후 교체처리 한다. |
워킹 셋과 페이지 테이블을 이용한 워킹 셋 모델 운용의 예 |
만약 폴트가 발생한 주소의 PTE가 처리중임을 나타낸다면 MiResolveTransitionFault()를 호출 한다. 처리 중에 있는 페이지는 앞서 언급된 PTE에 기록된 정보를 통해서 알아내는데 이것이 처리중인 경우를 좀 더 상세히 살펴 보면 아래와 같은 세가지 이유가 주를 이룬다.
- 페이지 프레임이 유효한 정보를 가지고 있음에도 불구하고 해당 페이지가 프리(Free) 페이지 영역에 존재 할 경우.
- 마찬가지 이유로 페이지 프레임이 유효한 정보를 가지고 있음에도 불구하고 프로세스의 워킹 셋에 의해 수정된 페이지 리스트에 존재 하는 경우.
- 페이지가 블록 디바이스로부터 읽어오는 중인 경우
MiResolveTransitionFault()가 처리 중에 있는 페이지 정보를 PTE로부터 인출 하고 나면 이 함수는 블록 되고 나서 입출력이 모두 끝나기를 기다리게 된다. 만약 에러가 일어난다면 PTE에 무효(Invalid) 상태로 바꾸고 나서 함수를 정상 리턴 시킨다. 이것은 강제적으로 다른 페이지 폴트에 의해서 해당 PTE가 더 이상 진행 중이라는 상태를 번복해서 저장 하지 않기 위함이다. 이러한 경우가 아니라면 PTE를 유효 상태로 두고 현재 프로세스에 워킹 셋에 이를 추가한다. MInAccessFault() 함수는 메모리 사상 파일이나 공유 메모리 범위에 존재하는 가상 주소 공간이 폴트를 냈을 경우 PPTE(Prototype PTE)와 함께 MiDispatchFault()를 호출 하게 된다. 이 경우 MiDispatchFault()는 MiResolveProtoPteFault() 함수를 호출하여 문제를 처리한다. 만약 사상 파일에 소유된 PPTE메모리로 폴트가 발생한 페이지의 집합을 결정 해야 하는 경우, VMM은 여퍼 페이질을 한번에 처리 하여 시스템 성능을 올리기 위해 MiResolveProtoPteFault() 루틴을 이용하여 MDL을 할당하고 0xC00333333을 리턴 한다. 만약 PPTE가 페이지 파일내에 공유된 메모리를 백업하기 위해 생성 된 것이라면 MiResolvePageFileFault() 루틴이 호출 되는데 이 루틴은 페이징에 대한 읽기 동작을 수행하기 위한 페이지 파일 넘버(PFN)을 결정하고 이를 통하여 읽기에 사용될 MDL 구조체를 생성한 뒤 앞선 상황과 마찬가지로 0xC00333333을 리턴 한다. 만약 PPTE가 사용 중이라면 이 루틴은 MiResolveTransitionFault() 서브 루틴을 자체적으로 호출하여 이를 처리 할 것이며 만약 '0'으로 채워진 제로페이지를 요구하는 경우라면 MiREsolveDemandZeroFault()를 호출 할 것이다.
이제까지 언급된 서브 루틴들이 적절히 호출 되고 성공적으로 완료를 하였다면 MiResolveProtoPteFault() 함수는 PPTE의 데이터들을 반영하는 PTE를 만들게 된다. 한번 프로세스를 위한 PTE를 생성하고 나면 페이징에 대한 읽기가 가능한 해당 PTE는 PEN 데이터 베이스 엔트리에 추가 되게 된다. 간혹 NT VMM은 페이지 폴트에 대한 응답으로 '0'으로 채워진 제로 페이지 프레임이 필요한 경우가 있는데 이런 대부분 디스크 상에 파일을 확장하거나 또는 새롭게 할당된 영역에 특정 스레드가 접근 하는 경우다. 이런 경우는 MiDispatchFault() 루틴이 가용가능한 페이지 프레임의 리스트로부터 제로 페이지 프레임을 할당 해주는 MiResolveDemandZeroFault() 서브 루틴을 호출 하는 것으로 해결 될 수 있다. 만약 제로 페이지 프레임을 할당 하기 위한 가용 가능한 페이지가 없다면 MiResolveDemandZeroFault() 함수는 0xC7903004를 리턴하여 가용 가능한 페이지 프레임을 생성 하도록 하는 페이지 폴트를 발생시켜 이를 해결 한다.
지금까지 기술 되었던 것처럼 NT VMM은 시스템 메모리에 기술 되지 않는 페이지를 폴트를 발생 시킴으로써 MMU가 가상 주소와 물리 주소를 번역 할 수 있도록 하고 있다. 만약 우리가 파일 시스템을 드라이버에서 페이지 폴트와 관계된 작업을 수행할 때 IRQ 레벨을 DISPATCH_LEVEL이나 그 이상의 우선순위를 가지는 일련의 동작을 만들었다면 각별한 주의를 해야 한다. VMM은 언급된 IRQL 레벨에서 페이지 폴트를 만족 시켜주지 못하기 때문이다. 우리에게 비 페이징 영역과 페이징 영역을 구분하여 할당 해야 한다면 어떤 기준으로 할 것인가? 라는 질문을 누군가 던진다면 단순히 그 페이지가 빈번하게 동작하거나 중요한 메타(meta)데이터를 가지고 있기 때문이라고 답할 수 없다. 이번 컬럼에서 알 수 있듯이 IRQL 레벨이 DISPATCH_LEVEL에서 수행되거나 또는 그 이상에서 수행 되는 경우 반드시 시스템 메모리를 비 페이징 영역으로 고정 시키고 해당 페이지에 접근 해야 한다.
파일 시스템 드라이버와 상호작용
NT VMM과 파일 시스템 드라이버는 서로에게 여러 가지 기능을 의존 하고 있다. 이러한 의존성은 소프트웨어의 엔트로피(Entropy)를 높이는 역할을 하기 때문에 일반적으로 소프트웨어 공학에서 이를 회피 하려고 하지만 가상머신을 통한 운영체제가 아닌 이상 가상 메모리 관리와 폴트 시 파일 시스템의 서로간의 협업은 반드시 필요하기 때문에 두 모듈간의 의존성은 불가 피하다. 특히 NT 커널상에서는 파일 시스템이 페이지 파일 I/O와 메모리 사상에 의한 오브젝트를 처리 해주기 때문에 VMM이 파일 시스템에 의존 할 수 밖에 없고 파일 시스템 입장에서는 NT VMM이 시스템 드라이버에서 발생한 페이지 폴트를 처리뿐만 아니라 파일 시스템이 사용하는 캐시처리 메모리 할당등의 메모리 처리 문제 때문에 VMM에 의존 적일 수 밖에 없다. 이것은 기능상의 모듈을 위반하는 것이 아니라 오히려 부분적 순서(Partial Order) 상의 레이어드(Layerd) 아키텍처의 최대한 지키면서 각자 해야 할 일을 명확히 정의 한 것으로 볼 수 있다.
이런 두 모듈간에 협조 속에서도 서로 침범 하지 않아야 하는 것들이 있는데 파일 시스템 드라이버 입장에서는 일부 데이터에 대하여 메모리를 명시적으로 조절 하는 것이나 모듈 자체가 VMM의 기능에 의해서 더럽혀지는 것을 막아야 하는 부분이다. NT 플랫폼(Platform)에서 파일 시스템은 VMM이 제공하는 메모리 공간의 시스템에 동적으로 올라가는 실행 이미지이기 때문에 기본적으로 파일 시스템과 다른 기타 커널 드라이버들은 페이징 되지 않는다. 다시 말해 이러한 모듈들이 올라가 있는 동안 이 드라이버들은 램상에 상주하게 된다. 물론 커널 드라이버와 관련된 전역 메모리 부분들도 기본적으로 스왑 아웃 되지 않는 것을 기본으로 한다. 만약 우리가 이런 기본 사항을 변경 하기 위해서는 컴파일러에게 시스템 성능 향상을 위해 스왑 아웃을 명시적으로 알려 줄 수 있다. #progma alloc_text() 라는 전처리로 컴파일러에게 전달 할 수 있으며 첫 번째 매크로 인자로서는 페이징 가능한 코드를 알려주는 4자의 유일한 스트링을 정의 해주어야 한다. 두 번 째 인자는 루틴의 개수를 알려 주는 것이다. 이렇게 전처리를 통해 컴파일러에게 명시적으로 자신의 코드의 특정 부분이 페이징이 가능하다고 알려 줄 수도 있지만 mmLockPageableDataSection()이나 LmLockPageableCodeSection() 같은 함수를 사용하여 동적으로 코드나 데이터를 페이징으로 막기 위해서 락(Lock) 시킬 수도 있다.
사용자 스레드들과 유사하게 파일 시스템과 커널 드라이버도 VMM의 상호 참조 구조 안에서도 동적 메모리를 할 당 할 수 있다. 전형적으로 개발자가 명시적으로 페이징이 가능한 메모리, 불가능한 메모리 그리고 캐시 메모리에서 데이터 정렬이 되어 있는 메모리를 ExAllocatePoolWithTag()를 사용하여 할당한다. 이러한 조건과 함께 메모리를 할당하는 경우 메모리 할당이 실패할 경우 시스템 전체에 문제가 생길 수 있다는 것을 숙지하고 예외 처리를 잘 해야 한다. 또한 NT의 실행부에서 이러한 메모리 할당을 지원 하긴 하지만, 절대적으로 이 해당 주소의 물리 메모는 VMM만이 조작 가능 하다는 것도 알고 있어야 한다. ExAllocatePool() 관련된 함수들 중 하나를 사용하여 할당하는 가상 주소 공간의 어떤 포인터들도 커널 가상 주소 공간을 벗어 날 수 없다. 엄격히 말해서 사용자 프로세스와 달리 커널 프로세스는 실제로 하나의 가상 주소 공간을 사용하고 있지만, ZwAllocateVirtualMemory()를 사용하여 할당 당시에 컨텍스트에서만 접근이 가능한 메모리를 할당 받을 수 있다. 커널 가상 주소 공간은 현재 실행 되고 있는 커널 모드 드라이버가 어떤 프로세스의 컨텍스트에 동작 하던지 간에 엑세스가 가능 해야 하기 때문에 (NT 커널은 사용자와 커널 드라이버간에 1:1 매핑 구조를 사용 하고 있다. 사용자 모드와 커널 모드간의 프로세스 매핑은 전형적인 운영체제 이슈로 이 부분을 모르는 독자들은 swblog.net의 펀더멘털 노트를 참조하기 바란다) NT VMM은 가상 공간을 하위 2GB로 사용하는 모든 프로세스가 시스템 가상 주소 공간으로 예약된 주소를 사용하며 시스템에서 실행되는 모든 프로세스 컨텍스트 안에서 같은 주소 간간으로 사상된다.
파일 시스템 개발자는 사용자 모드의 코드에서 전달된 파라미터안의 사용자 공간의 버퍼를 조작 해야 하는 경우가 있는데 이는 대부분 사용자 모드에서 정의된 주소 공간에 데이터 넣거나 빼는 작업이다. 따라서 메모리 조작에 특히 조심해야 하는 것들이 있다. 이렇게 전달 된 주소 공간은 실제로는 커널 공간의 가상 주소가 아니기 때문에 사용자 모드의 특정 프로세스 컨텍스트 안에서만 유효한 값을 가지게 되는 경우가 존재 한다. 물론 앞서 언급한 VMM간의 특징 때문에 사용자 모드에서 APC_LEVEL보다 높은 우선 순위로 전달된 메모리를 접근 하기 위해서는 반드시 페이지 락을 통해서 이 버퍼를 사용하다가 페이지 폴트가 발생 하지 않도록 하기 위해 물리 페이지를 고정해야 한다. 다시 말해 우연하게 할당된 메모리의 프로세스 컨텍스트와 동일한 환경에서 실행 된다고 하더라도 페이지 아웃이 되어 있다면 완전 쓰레기 값을 참조하거나 기대치 못한 폴트를 발생 시켜 전체 시스템이 멈출 수 있다는 것을 상기 해야 한다. VMM은 언급된 개발자의 고려사항을 모두 지원한다. 우선 VMM의 MmProveAndLockPages() 이나 MmBuildMdl()과 같은 보조 함수들을 이용하여 어떤 버퍼들이든 간에 관련된 물리 메모리를 고정 시킬 수 있다. VMM의 MmProveAndLockPages() 이나 MmBuildMdl()의 함수는 내부적으로 해당 가상 주소 공간 뒤에 존재하는 물리 페이지 프레임들을 기술하여 리스트로 가지고 있는 MDL을 생성하며 부가적으로 기술된 페이지들이 메모리상에서 쫓겨 나가지 않도록 관리하여 버퍼를 편하게 쓸 수 있는 방법을 제시하고 있다. 두 번째로 사용자 모드에서 전달된 메모리 공간을 시스템 가상 주소 공간으로 사상 시켜 해당 주소가 존재하는 컨텍스트와 관계 없이 버퍼를 쉽게 접근하게 하는 것을 툴을 제공하는데 이 것이 MmGetSystemAddressForMdl()이다.
이러한 것 이외에도 시스템적으로 메모리를 관리 해야 하는 여러 가지 일련의 작업들을 처리한다. 예를 들어 NT VMM은 시스템에서 사용되는 모든 스레드에게 4KB 페이지 프레임 기준으로 3개의 페이지를 고정적으로 할당 된 스택 프레임들도 관리한다. 앞서 언급 되었듯이 물리 페이지 프레임을 조작 할 수 있는 것은 오직 VMM 만 가능하기 때문에 VMM이 파일 시스템과 NT 캐시 매니저의 캐싱 파일 데이터들의 관리를 보조하고 페이지 폴트가 생겼을 때 시스템 성능을 증가 시키기 위해서 페이지 클러스터(Cluster)를 지원 한다. 여기서 클러스터라는 것은 16 페이지를 하나의 클러스트로 묶어서 64KB 사이즈 또는 128KB 단위로 접근하게 하는 것을 의미한다. 이런 클러스터의 개념은 아주 오래 전에 Marshall K. McKusick에 의해 제시된 "A Fast File System for Unix"나 "The Design and Implementation of a Log-Structured File System" 에 의해서 그 성능이 검증 된 바 있다. 이러한 성능상에 문제 말고도 VMM은 커널 필터 드라이버나 사용자 모드의 어플리케이션간에 커뮤니케이션을 위해서 공유 메모리 오브젝트나 메모리 사상 파일들도 대신 해서 생성 해준다.
typedef struct _MDL { struct _MDL *Next; CSHORT Size; CSHORT MdlFlags; struct _EPROCESS *Process; PVOID MappedSystemVa; PVOID StartVa; ULONG ByteCount; ULONG ByteOffset; } MDL, *PMDL; |
MDL 자료구조 |
메모리 디스크립터 리스트(Memory Descriptor List, MDL)은 물리 주소 공간을 사용하여 버퍼로서의 가상 주소 공간일 기술 하는 시스템 정의 자료 구조이다. MDL은 배열을 포함하고 있는데 해당 배열의 각 원소들은 가상 주소 공간에 사상되어 있는 물리 주소 페이지 프레임의 인덱스를 나타낸다. 메모리 레이아웃으로 봤을 때 이 물리 페이지 프레임을 나타내는 주소에 대한 배열은 MDL 바로 뒤에 존재한다. 다시 말해서 시스템에서 할당한 비 페이징 영역의 메모리에 해당 배열과 MDL구조가 연속적으로 할당 된다는 것이다. 계속 언급 되어 왔듯이 파일 시스템 드라이버 개발자들은 사용자 주소 공간에서 할당된 버퍼에 접근 하기 위해서 NT VMM에게 이와 같은 MDL을 계속 할당 받아 사용자 주소 공간을 시스템 가상 주소 공간에 사상시킨다.
이러한 작업은 개발자가 해당 버퍼를 건드리는 동안 페이지를 스왑 아웃 시키지 않도록 해주고 MDL을 통해 물리 주소 공간의 사상 내용을 메모리에 가지고 있음으로써 시스템의 가상 주소 공간으로 접근하여 어떤 프로세스 컨텍스트에서도 해당 버퍼에 접근 하는 것을 허락해준다. 대부분의 커널 자료구조가 그렇듯이 MDL을 통하지 않고 물리 페이지 주소가 있는 위치로 바로 접근 하면 안 된다. 이는 MDL과 해당 기술 배열이 연속적인 비 페이징 영역에 존재 하더라도 이 두 개의 메모리 레이아웃이 언제든지 NT 커널 설계에 의해서 변경 될 수 있다는 것을 의미한다. |
파일 시스템 드라이버에게 유용한 VMM의 지원 함수가 있는데 그것은 MmQuerySystemSize() 이다. NT VMM은 내부적으로 MmSystemSize라는 전역 변수를 초기화하여 시스템의 메모리량을 대략적으로 알려주는데 MmQuerySystemSize()는 이 변수의 정보를 얻는 함수이다. 이 함수에 의해서 반환 되는 시스템 메모리양은 크게 3가지인데 하나는 MmSmallSystem, MmMediumSystem 그리고 MmLargeSystem이다. 이 시스템 정의 값은 이름에서 알 수 있듯이 메모리 양이 적은 시스템, 중간형태, 큰 시스템을 대략적으로 구분 해준다.
NTOSAPI |
MmQuerySystemSize의 프로토타입 정의 |
typedef enum _MM_SYSTEM_SIZE { MmSmallSystem, MmMediumSystem, MmLargeSystem |
MM_SYSTEM_SIZE의 타입정의 |
이 리턴 값을 가지고 정확한 물리 메모리의 양을 파악 할 수는 없지만 파일 시스템 개발자는 현재 시스템에 대해서 동적으로 시스템의 성능을 고려한 정책을 사용 할 수 있다. 예를 들어 MmQuerySystemSize() 함수가 MmSmallSystem을 리턴했다면 파일 시스템은 큰 메모리 시스템이나 중간형태의 메모리 시스템 보다 적은 형태로 메모리를 할당 받거나, 미리 존(Zone)형태로 메모리를 할당 받아 시스템을 운영 할 수 있고 또한 다른 큰 시스템이나 중간 크기 메모리 사이즈를 가진 시스템보다 적은 워커(Worker) 스레드를 할 당 할 수 있다. 파일 시스템 개발자는 AcquireFileForNtCreateSection()과 ReleaseFileForNtCreateSection() 두 함수를 반드시 제공해야 하는데 이유는 공유 메모리와 메모리 사상 파일을 지원 하기 위해 VMM이 하이라키(Hierarchy) 상에서 파일 시스템 아래에 존재 하기 때문에 파일 시스템의 적절한 콜백 시스템을 요구하기 때문이다.
파일 시스템 구현을 위한 VMM의 지원 루틴.
NT VMM은 MmFlushImageSection과 MmCanFileBeTruncated()라는 두 개의 함수를 제공한다. 이 두 함수는 파일 시스템 설계자나 개발자들에게 매우 중요한 역할을 하는 함수 인데 다른 VMM의 지원 루틴이나 API들처럼 잘 문서화 되어 있지 않다. 이번 칼럼에서는 이 두 개의 함수를 좀 더 구체적으로 알아보는 것으로 글을 마감하고자 한다.
/* Used in MmFlushImageSection */ typedef enum _MMFLUSH_TYPE { MmFlushForDelete, MmFlushForWrite } MMFLUSH_TYPE; |
MMFLUSH_TYPE 타입 정의 |
NTKERNELAPI BOOLEAN NTAPI MmFlushImageSection ( IN PSECTION_OBJECT_POINTERS SectionObjectPointer, IN MMFLUSH_TYPE FlushType ); |
MmFlushImageSection 함수 프로토타입. |
NTKERNELAPI BOOLEAN NTAPI MmCanFileBeTruncated ( IN PSECTION_OBJECT_POINTERS SectionObjectPointer, IN PLARGE_INTEGER NewFileSize ); |
MmCanFileBeTruncated 프로토타입 |
Rejeev Nagar, "Windows NT File System Internals": A Developer Guide, O'Reilly 1998
P. B. Kruchten."The 4+1 View Model of architecture."
David Garlan and Mary Shaw January 1994 "An Introduction to Software Architecture"
Kernel Source http://reactos-mirror.googlecode.com/svn
Kernel Source http://nuwen.net
'Drafts > Kernel mode' 카테고리의 다른 글
| Major Control Blocks and Design for NT File System Implementation (0) | 2010/04/20 |
|---|---|
| Basic Functionalities and Concepts for File System Implementation (0) | 2010/03/17 |
| Communication between Virtual Memory Manager and File System (0) | 2010/02/17 |
| The Virtual Address Translation with Considering MMU and TLB (0) | 2010/01/05 |
| NT Virtual Memory Manager Overview (0) | 2009/12/13 |
| Common Data Structures of NT I/O Manager (0) | 2009/09/30 |