디스패치 루틴: 생성과 열기 동작
Dispatch Routines: Create and Open Algorithms
만약 우리가 커널 설계자라면 우리가 만든 프로그램이 컴파일이 끝난 뒤에 누구에 의해서 어떻게 생성될 지 모르는 모듈을 어떻게 사용 할 수 있고, 호출 할 수 있을까? 그들만의 함수 이름과 인자 값을 통해 개발을 할 텐데 우리는 어떻게 그 모듈을 우리 프로그램의 일부로 사용 할 수 있을까? 이 의문은 서드 파티(Third Party) 개발자에 의해서 개발되는 파일 시스템을 커널이 메모리상에 올려 놓고 그에 대한 서비스를 사용해야 하는 상황과 같다. 디스패치 루틴(Dispatch Routine)은 이러한 동적 로딩을 위해 커널 개발자와 드라이버 모듈 개발자간의 규약사항들을 반영하여 서로간의 커뮤니케이션을 원활하게 하는 작업을 수행한다.
정명수
|
필자는 지난 3년간 삼성전자에서 플래시 메모리와 관련된 연구와 임베디드 소프트웨어, 커널 드라이버 등을 개발 했었다. 현재는 조지아 공대(Georgia Institute of Technology) 컴퓨팅 칼리지에 재학 중이다. 글쓰기를 매우 좋아하며 학부시절에는 객체 지향 패러다임을 통하여 해석하는 프로그래밍 언어론에 관심이 있었으나 실무과정을 거치면서 컴퓨터 아키텍처로 관심사가 옮겨졌다. 최근에 관심 있는 분야는 운영체제, 파일시스템, 실시간 스케줄링 등이다.
오늘부터는 파일 시스템 실제 구현에 관련된 내용들을 직접적으로 다루게 될 것이다. 모든 커널 드라이버 개발자들은 자신의 개발 드라이버가 사용되어야 하는 환경에서 제공하는 프로토콜(Protocol)과 규약사항에 맞게 개발 되어야 하며, 파일 시스템도 일종의 커널 드라이버이기 때문에 파일 시스템의 서비스 기능들을 이 규약에 맞도록 개발 해야 한다. 파일 시스템 생성을 위해서는 기본적으로 생성, 열기, 닫기, 읽기, 쓰기 등의 기본작업을 가지며 이러한 작업들은 디스패치 루틴으로 불리는 윈도우 커널의 개발 프로토콜에 맞게 개발 되어야 한다.
본 컬럼은 파일 시스템 입장에서 개발되어야 하는 디스패치 루틴 중 생성과 열기 작업에 필요한 의사 코드들을 가지고 파일 시스템 구현을 설명 할 것이다. 독자가 초점을 맞추어야 하는 것은 실제 코드 작업이 아니라 파일 시스템 드라이버가 개발 되기 위해서 논리적 단계 또는 흐름이 어떤 식으로 구현 되어야 하는 지이며, 이를 이해 하고 나면 윈도우 DDK나 파일 시스템 개발 DDK의 샘플을 통해서 이를 응용 할 수 있다. 따라서 커널 드라이버 그 자체에 필요한 용어들은 독자들이 이미 숙지 하고 있는 것이 올바르며, 파일 시스템에서 벗어나는 커널 드라이버 개발 이슈들은 필요한 경우를 제외하고는 대부분 생략 될 것이다.
디스패치 루틴(Dispatch Routine)
모든 커널 모드 드라이버는 드라이버 엔트리(Driver Entry) 루틴을 필요로 한다. 이 루틴은 PASSIVE_LEVEL의IRQL 의 시스템 스레드 컨텍스트(System Thread Context) 상에 NT I/O 매니저에 의해서 호출 된다. 커널 드라이버를 처음 개발 해보는 사람이라면 드라이버 엔트리를 일반 프로그램의 main 함수쯤으로 생각하고 여러 가지 코드를 넣어두는 경우가 있다. 하지만 드라이버 엔트리(Entry)는 main과 엄연히 다르다. 다른 프로그램을 개발 하면서 존재 하지 않는 디스패치 루틴이 왜 필요한지를 생각 해보면 그 이유를 알 수 있다. 커널 입장에서 독자가 만들어 내는 파일 시스템 드라이버를 어떻게 알 수 있을까? 커널은 이미 컴파일 되어 이미지 상에 올라가 있는 상태이다. 다시 말해 여러분의 드라이버를 컴파일 할 때 같이 물려서 정적으로 만들어 질 수 없다. 따라서 커널 입장에서는 동적으로 드라이버의 특정 함수들을 호출 할 필요가 있는데 이를 위한 초기화 작업을 하는 것이 드라이버 엔트리이다. 커널과 약속된 형태의 디스패치 루틴을 만들어내고 커널이 드라이버를 호출 할 때는 함수의 심볼(Symbol)은 다르지만 기능적으로 이를 구분하여 호출 하는데, 이는 함수 포인터 형태를 호출 하는 것으로 처리한다. 따라서 커널에서는 규약된 형태의 함수 테이블을 제공하고 파일 시스템 드라이버는 이 함수 테이블에 자신이 만든 함수들의 포인터를 저장하여 쌍방간에 통신을 한다. 이런 의미에서 드라이버 엔트리가 사용되는 것이므로 이를 사용자 단의 main 함수와 비슷하게 생각 하면 안 된다.
기능적 측면의 공급성.
파일 시스템 드라이버는 일반적으로 드라이버 엔트리 루틴에서 아래와 같은 작업을 수행 하게 된다. 앞서 언급 되었듯이 이러한 드라이버 엔트리 루틴은 다른 커널 드라이버의 특성과 크게 차이가 없다.
- 전역 데이터를 위한 메모리를 할당하고 자료 구조들을 초기화 한다.
- 레지스터 정보를 읽어 온다.
사실 파일 시스템 드라이버 개발자 입장에서 사용자가 지정한 파라미터(Parameter)를 필요로 하는 경우가 드물기 때문에 레지스터를 읽어올 일이 잘 없지만, 만약 리디렉터(Redirector)나 서버의 경우는 여러가지 설정 내용을 레지스터를 통해서 읽어와 설정 해야 할 필요가 있다.
- 파일 시스템 드라이버 스스로가 필요로 하는 디바이스 오브젝트를 생성 한다.
- 여러 다른 형태의 IRP를 처리 할 수 있는 디스패치 루틴들의 함수 포인터를 초기화 한다.
- 모듈간 동기화를 위해 필요한 콜백(Callback) 함수를 와 빠른 입출력 경로(Fast I/O Path)를 위한 함수 포인터를 초기화 한다.
- 만약 개발자가 사용하는 파일 시스템 드라이버가 DPC(Differed Procedure Call) 오브젝트나 타이머 오브젝트들이 있다면 이를 초기화 한다. 타이머 오브젝트의 경우는 비 동기화 처리를 위해 종종 필요로 하므로 이러한 루틴이 들어간다면 그때 드라이버 엔트리에 넣어주면 된다.
- 만약 독자의 파일 시스템이 비동기화된 형태의 초기화를 수행하는 작업 스레드(Worker Thread)를 사용한다면 이는 드라이버 엔트리의 컨텍스트 스레드에서 생성할 수 있으므로 이에 해당한 초기화를 수행한다.
- 물리적 미디어 기반의 파일 시스템 드라이버는 현재 메모리에 올라와 있는 드라이버의 인스턴스를 등록하는 IoRegisterFileSystem() API를 호출 한다.
- UNC(Universal Naming Convention)을 지원하는 네트워크 파일 시스템 드라이버 구현에서는MUP 콤퍼넌트등을 등록 하기 위해서 FsRtlRegisterUncProvider()를 호출 해야 한다.
- 네트워크 리디렉터와 서버는 IoRegisterShudownNotification()을 통하여 셧다운(Shutdown)을 위한 루틴을 등록 해야 한다. 이러한 셧다운 루틴은 파일 시스템 드라이버에게 시스템의 전원이 사라지거나 다른 특정 프로세스를 위해서 필요한 경우 데이터를 플러시(Flush)할 수 있는 기회를 제공한다.
여기서 몇 가지 좀 더 정확히 하고 가도록 하자. 3번 절차에 사용되는 디바이스 오브젝트의 타입은 크게 4가지가 있다. 디스크 기반의 파일 시스템 예를 들면 NTFS, FAT과 같은 형태의 파일 시스템은 FILE_DEVICE_DISK_FILE_SYSTEM 타입으로 초기화 되어야 하며 랜 매니저 서버 같은 경우는 FILE_DEVICE_NETWORK으로 초기화 되어야 한다. 랜 매니저 리디렉터(Lan Manger Director)나 다른 형태의 NFS, DPS 구현에서는 FILE_DEVICE_NETWORK_FILE_SYSTEM으로 설정되어야 한다. 다른 나머지는 FILE_DEVICE_TAPE_FILE_SYSTEM인데 이 형태는 독자를 포함 필자에게 거의 사용 되지 않는 다고 봐도 무방하다. 그리고 8번에서 사용되는 물리 디스크 기반의 파일 시스템 드라이버는NT I/O 매니저에 의해 관리 되는데 여기에는 디스크, 가상 디시크, 시디롬, 테이프 형태의 디바이스만을 제공한다. I/O 매니저와 함께 등록되는 파일 시스템 드라이버는 반드시 부팅 시 처음 엑세스 되어 마운트(Mount)되는 시점에 수행 될 수 있도록 I/O 매니저에 의해 요청되는 파일 시스템 목록에 있어야 한다.
<그림 1, 드라이버 엔트리의 의사코드>
이 과정을 간단하게 의사코드로 작성 해봤다. 그림 1에서 작성된 루틴은 앞서 언급된 절차 중에 매우 범용적으로 사용 되는 것만 삽입한 것으로 이해의 편의를 위해 비동기적 리소스 초기화라던가 네트워크 관련 처리, 예외 처리 등은 제외 하였다. SFS접두사가 붙은 것은 드라이버 내부 함수인데, 그림 1의 SFS_InitFuncPtr은 드라이버 오브젝트 내에 있는 함수 테이블(MajorFunction 필드)에 만들어진 파일 시스템 드라이버의 함수 포인터를 배정하는 것이 주 업무이다.
DriverObject->MajorFunction[IRP_MJ_CREATE] = SFS_Create; DriverObject->MajorFunction[IRP_MJ_CLOSE] = SFS_Close; DriverObject->MajorFunction[IRP_MJ_READ] = SFS_Read; DriverObject->MajorFunction[IRP_MJ_WRITE] = SFS_Write;
DriverObject->MajorFunction[IRP_MJ_QUERY_INFORMATION] = SFS_FileInfo; DriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = SFS_FileInfo; DriverObject->MajorFunction[IRP_MJ_FLUSH_BUFFERS] = SFS_Flush; |
파일 시스템 드라이버가 구현한 함수들의 심볼을 이용하여 NT DDK에 정의된 숫자 위치(상위에서는 IRP_MJ로 정의 되어 있는 열거자)에 함수 포인터(주소)를 등록 하여 커널에게 관련된 기능을 알리는 과정. |
표 1. SFS_InitFuncPtr 코드 중 함수 테이블 초기화 부분. |
생성 함수(SFS_Create)
디스패치 루틴 중에서 생성 함수는 파일 시스템 디자이너 및 개발자에게 있어서 구현이나 설계에 있어 좀 까다로운 함수가 될 수 있다. 대부분에 경우에 있어 생성 함수의 경우, 온-디스크 자료구조에 관련된 대부분의 동작을 맡고 있으며 해당 오브젝트가 처음으로 생성하고 초기화 하는 작업을 수행 해야 하기 때문에 파일 시스템 드라이버에 베니어(veneer) 코드의 코어 중에 하나가 된다. 따라서 생성 함수의 루틴은 튼튼해야 해야 함은 물론, 해당 루틴이 전체 성능에 큰 영향을 미치므로 개발 당시 이를 잘 고려 해야 한다. 잘 못된 구현과 설계를 가진 생성 함수는 자주 발생하는 파일 시스템 조작 동작 때문에 시스템 전체에서 성능 병목현상의 주범이 될 수 있다.
생성
함수와
관련된
논리적
단계
I/O 스택 로케이션(Stack Location, 구조체 이름으로 사용되므로 발음을 그대로 이용) 자료 구조는 생성 함수에서 파일의 생성과 열기의 처리를 위한 관련 자료 구조를 표2에 정의해 두었다. 표2의 굵은 형태로 정의된 내부 자료 구조는 언급된 동작과 관련된 NtCreateFile을 위한 시스템 서비스 파라미터를 보관한다.
Typedef struct _IO_STACK_LOCATION { UCHAR MajorFunction; UCHAR MinorFunction; UCHAR Flags; UCHAR Control; union { struct { PIO_SECURITY_CONTEXT SecurityContext; ULONG Options; USHORT POINTER_ALIGNMENT FileAttributes; USHORT ShareAccess; ULONG POINTER_ALIGNMENT EaLength; } Create; struct { ULONG Length; ULONG POINTER_ALIGNMENT Key; LARGE_INTEGER ByteOffset; } Read; // … } Parameters; // … } IO_STACK_LOCATION, *PIO_STACK_LOCATION |
표2. 입출력 스택 로케이션의 자료구조 정의 |
개념적으로는 생성 루틴 작성은 그리 어려운 것은 아니지만 생성 요청을 처리하는 부분의 여러 가지 복잡한 처리는 꽤 까다롭다. 파일 시스템 개발자는 파일 열기 요청을 처리 하기 위하여 다음 단락부터 기술되는 절차를 따라야 한다.
<그림 2, 파일 시스템 드라이버의 생성 디스패치 루틴의 일반적인 절차>
호출자에 의해 제공되는 정보 인출 작업
우선 우리는 우리가 관심이 있는 오브젝트를 접근 할 수 있도록 호출자에 의해 제공되는 경로를 얻어야 한다. 대부분의 경우에 상업적으로 공급되는 운영 체제는 역 트리(Inverted Tree) 기반파일 시스템을 제공한다. 이러한 종류의 파일 시스템은 명명된 데이터 오브젝트에 접근 하기 위해서 디렉터리 자료구조 컨테이너 오브젝트(Container Object)가 존재 한다. 전형적으로 컨테이너 오브젝트는 파일 스트림에 추가적으로 다른 컨테이너 오브젝트를 가지고 있을 수 있고 이것은 역 트리 자료 구조 파일 시스템을 순환하는데 사용된다. UNIX와 같은 상업적 운영 체제에서는 파일 시스템 드라이버는 특정 이름으로 명명되어 있는 파일 스트림에 접근하는 전체 경로를 제공하지 않는다. 대신에 운영체제는 해당 파일 스트림을 위해 경로를 분석하는 작업을 수행하기 때문에 컨테이너 오브젝트와 컨테이너를 열기 위한 오브젝트 이름만 제공한다. 그림 2에서 볼 수 있듯이 만약 어떤 스레드가 \dir1\dir2\…\imaso 의 오브젝트를 열고자 한다면 운영 체제는 우선 파일 시스템 드라이버에게 루트(Root) 트리를 열도록 요청한 뒤 다시 파일 시스템 드라이버게 dir1을 열게 하고 그리고 나서 \dir1을 사용하여 dir2로 옮겨 간다. UNIX 시스템에서는 그렇게 하고 나서야 imaso 파일을 열 수 있다. 그러나 윈도우 NT플랫폼에서는 파일 시스템의 생성 루틴을 호출하는 I/O 매니저가 이와 같은 경로 및 이름 분석을 하지 않는다. 대신에 I/O 매니저는 파일 시스템의 생성 함수를 호출 할 때 앞서 언급 되었던 호출자에 의해 공급되는 전체 경로를 넘겨준다. 예를 들어 앞의 UNIX 예제에서 사용되었던 imaso 파일을 위해서 \dir1\dir2\…\imaso 를 통째로 파일 시스템 드라이버게 전달 한다고 보면 된다. 이것은 파일 시스템 드라이버 입장에서 양날의 검과 같다. 호출자에 의해 제공되는 전체 경로는 파일 시스템 드라이버로 하여금 온-디스크 자료구조의 이름과 온-디스크 오브젝트의 경로를 스스로 구성할 수 있도록 하게 해주는 반면에 파일 시스템 드라이버 개발자가 이 경로에 대한 분석으로 모두 해야 하는 책임을 전적으로 지게 된다.
< 그림 2, 역 트리 파일 시스템의 예제>
여기서 우리가 하나 확실히 하고 가야 하는 것은 NT I/O 매니저가 제공하는 파일 열기 동작 시 호출자에 의해 제공되는 경로 이름은 파일 시스템 트리의 루트에서 시작하는 경로 대신에 이미 앞에서 열린 컨테이너 오브젝트와 관련이 있을 것이라 가정한다는 것이다. 따라서 파일 시스템 드라이버 개발자는 반드시 파일 열기와 생성 동작, 둘 사이를 구분해야 하고 이에 따라서 경로를 적절히 조작 할 수 있어야 한다.
호출자에 의해 제공되는 경로를 얻고 나면 파일 시스템 드라이버 개발자는 호출자에 의해 제공되는 다른 전달 인자를 받을 수 있다. 이 전달 인자는 파일 시스템 드라이버로 하여금 파일에 대한 열기와 생성을 어떻게 처리 할 것인가를 결정하는데 사용되며 호출자에 의해 기술 된 아래와 같은 정보를 가지고 있다.
- 해당 파일이 생성되어야 하는 지의 여부.
만약 해당 파일이 이미 생성되어있다면 파일 시스템 개발자는 반드시 에러를 반환 해야 함.
- 해당 파일의 반드시 읽기 형태로 열려야 하는 여부
1의 조건과 반대로 만약 파일이 생성 되어 있지 않다면 파일 시스템 개발자는 에러를 반환.
- 조건적으로 생성 할 것인지의 의사
파일이 생성되어 있지 않다면 생성하고 그렇지 않은 경우에는 열기 동작을 수행.
호출자에 의해 넘겨지는 다른 전달 인자는 생성 중이거나 열기 동작을 수행중인 오브젝트의 타입에 대한 명세를 가지고 있다. 다른 말로 호출자가 컨테이너 오브젝트를 원하는지 파일 스트림 오브젝트를 원하는 지 기술 할 수 있다. 추가적으로 호출자는 생성중인 오브젝트와 연관된 정보들을 명세 할 수도 있다. 다시 말해서 호출자는 파일 시스템 드라이버로 하여금 마지막 오브젝트의 닫기 동작 시 이를 자동으로 삭제 하는 임시 오브젝트를 만들 수 있도록 해당 오브젝트에 대해서 닫기 시에 삭제와 같은 속성을 줄 수 있다. 다른 속성으로는 호출자가 오브젝트가 계속 열려 있게 만들고 싶을 경우 공유 모드를 줄 수도 있다.
경로상의 오브젝트 찾기
파일 시스템 드라이버가 호출자로부터 제공되는 모든 정보를 인출 하고 나면 사용자에 의해서 제공된 경로를 통해 해당 오브젝트를 찾아 내어야 하는 것이 다음 순서이다. 이 경우 파일 시스템 드라이버 개발자는 사용자에 의해 제공된 경로를 통해 해당 오브젝트를 찾을 수 없는 경우가 발생 할 수도 있다. 만약 호출자가 생성(FILE_CREATE)을 요구 하였는데 파일 시스템 드라이버가 오브젝트를 찾았다면 반드시 오브젝트가 존재 한다라는 에러 코드를 반환 해주어야 한다. 반면 호출자가 열기 동작을 원하는데 오브젝트를 찾지 못했거나 생성해야 한다면 오브젝트를 찾지 못했다는 에러를 반환 해야 하며 앞서 언급 했듯이 조건부 생성 동작에도 같은 에러처리를 해주어야 한다. 어찌됐던 간에 이 시점에서는 다음 동작으로 넘어가거나 아니면 에러를 반환 해주는 결정을 해주어야 한다.
호출자 요청의 적절성 검사.
에러를 반환 하지 않았다면 이제는 호출자가 자신의 식별자와 명세한 속성을 통하여 오브젝트를 생성하거나 열기 동작을 수행 하도록 할 것인지 아닌지를 검사해야 한다. 공유 모드 속성은 다른 스레드가 이미 해당 오브젝트를 이미 열어 둔 경우에 새로운 요청이 이전 열기 동작과 충돌이 일어 나지 않을 것이라는 가정에서 수행 된다. 만약 파일 시스템 드라이버가 접근성 검사 기능을 제공해야 한다면 파일 시스템 개발자는 우리는 호출자의 컨텍스트를 이용하여 호출자가 파일이나 디렉터리 오브젝트에 작업을 하기 위한 적절한 권한 레벨을 가지고 있는 지 시험 해 볼 수 있다. 이러한 호출자 컨텍스트에 접근은 이 이외에도 해당 오브젝트에 이르는 경로를 분석하는 동안에도 발생 할 수 있다.
인-메모리 자료 구조 관리.
파일 시스템 드라이버가 정상적으로 해당 오브젝트를 생성하거나 열었다면 나중에 오브젝트를 다시 접근 하는 경우를 생각 해서 인-메모리 자료구조로 만들어두는 것이 올바르다. 예를 들어 만약 현재 오브젝트를 기술하는FCB가 존재 하디 않는다면 파일 시스템 드라이버가 FCB를 생성하게 되는 것과 같이 파일 시스템 드라이버는 생성 요청에 대하여 CCB 구조체의 특정 컨텍스트를 메모리에 유지 해야 한다. 이 두 구조체뿐만 아니라 어떠한 파일 시스템 지원 자료구조체들도 파일 시스템 드라이버에 의해서 초기화 되고 경우에 따라 해당 컨텍스트를 관리 해야 한다.
마무리 작업과 제어권 반환
윈도우 NT I/O 매니저는 생성과 열기 동작이 정상적으로 끝나고 나면 파일 시스템 드라이버가 파일 오브젝트 구조체에 필요한 대부분의 정보는 모두 초기화 했을 것이라고 가정하기 때문에 파일 시스템 드라이버 개발자는 제어권을 반환 하기 전에 이를 모두 수행 해야 하며 정상적으로 완료가 되었다면 호출자에게 수행 과정에 생겼던 결과를 반환 해주어야 한다.
파일 시스템 개발자는 도식에 나와 있는 각각의 단계에 대해서 모든 책임을 가지고 있으므로 생성과 열기 동작에 대한 요청 시 이를 체계적으로 구성하고 생성 요청을 올바르게 처리 할 수 있어야 한다.
동기화 이슈
파일 시스템 개발자는 생성과 열기 동작과 관련된 디스패치 루틴 이외에도 심각하게 고려 해야 하는 몇 가지 사항들이 있다. 그 중에 하나는 파일 시스템 개발자가 반드시 자신이 만드는 파일 시스템의 생성 동작이 효율적이고 올바르게 동작하기 위한 동기화 과정을 충분히 잘 이해하고 있어야 한다는 것이다. 파일 시스템 드라이버는 다수의 열기 및 생성 동작이 동시에 같은 오브젝트에 수행 될 수 있다는 것을 충분이 인지 하고 있어야 하며 이러한 동시다발적 다수의 동작들은 일관성 있게 다루어져야 한다. 예를 들어서 \dir1\imaso 의 파일에 대한 두 개의 스레드가 동시에 생성을 했다고 가정해보자. 이러한 경우\dir1\imaso는 다른 두 개의 스레드에 대해서 존재 하지 않을 경우 생성되어야 하고 존재 한다면 단순이 열기 동작을 수행 해야 한다. 파일이 존재 하지 않은 경우, 파일 시스템 드라이버가 동시에 발생하는 두 요청에 대하여 충분히 고려 하지 않았다면 imaso에 대한 새로운 디렉터리를 할당하고 해당 위치에 같은 이름의 imaso를 만들어 낼 가능성이 있다. 이러한 비 일관성 문제는 대부분 직렬화(Serialize)를 통해서 해결 해야 한다.
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' 카테고리의 다른 글
| Dispatch Routines: Create and Open Algorithms (0) | 2010/08/15 |
|---|---|
| Characteristics of Data Structures for NT File System (0) | 2010/05/06 |
| Major Control Blocks and Design for NT File System Implementation (0) | 2010/04/19 |
| Basic Functionalities and Concepts for File System Implementation (0) | 2010/03/17 |
| Communication between Virtual Memory Manager and File System (0) | 2010/02/16 |
| The Virtual Address Translation with Considering MMU and TLB (0) | 2010/01/05 |