Media Log

CreateFile은 Wndows Api 들 중 가장 기본적이면서도 중요한 함수이다.
이 함수는 단지 파일을 생성하는 것 뿐만이 아니라 파일을 오픈할 수도 있고 디렉터리를 오픈할 수도 있으며 또한 여러 디바이스들까지 오픈 할 수 있다. 사실 CreateFile에서 File은 꼭 파일만이 아닌 여러 디바이스들을 추상화한 Virtual File을 뜻하는 셈이다.
이 함수를 통해 파일을 여는 순간에 동기 I/O를 할지 비동기 I/O를 할지 결정하게 되며, 내가 어떤 작업을 하려는지 내가 파일을 열고 있는 동안 다른 클라이언트들에게는 어떤 작업을 허용할지도 결정하게 된다.
생성하려는 파일의 읽기 전용, 숨김 파일 등의 속성도 정할 수 있으며, 캐시를 이용 할지 말지, 쓰기를 하는 족족 플러시 하게 할지 또 I/O를 순차적으로 할지 랜덤하게 할지 등의 힌트도 파일 시스템으로 전달해줄 수 있다.

이렇게 중요한 함수이니만큼 MSDN에는 CreateFile에 대한 문서가 아주 잘 나와있는데, 페이지 내의 링크들까지 하나하나  따라가면서 차근 차근 읽다보면 시스템 프로그래밍에 대해 배울 수 있는 것들도 많고 무엇보다도 아주 재밌다.

하지만 아무리 열심히 읽어도 글만 읽고서 지식을 자기 것으로 만들 수는 없는 법이다.
언제나 마지막은 실습으로 끝나야 한다. 글을 다 이해한 것 같아도 막상 진짜로 해보려고 하면 거기서 또 어려운 문제가 닥치기 마련이며, 이 것까지 해결하고 나서야 비로소 완전히 자기 지식으로 만들었다고 할 수 있다.

CreateFile의 파라메터는 몇 개 안되는 것 같지만 엄청난 플래그들의 조합이 가능하기 때문에 사실은 적은 갯수가 아니다.
실습을 해보기 위해서 항상 무거운 비주얼 스튜디오를 켜고 그 지겨운 파라메터들을 매번 입력하는 것은 손가락도 아프고 시간도 많이 들어가는 비효율적인 방식이다.

이런 실습을 위해 누군가가 이미 아주 훌륭한 도구를 만들어서 osronline.com에 올려놓았다.
이는 프로세스 모니터와 함께 내가 가장 즐겨쓰는 도구들 중 하나인데, 병들어가는 내 손가락을 조금이나마 쉴 수 있게 해주는 아주 고마운 친구이다.


위 그림에서 보이는 것 처럼 CreateFile 함수 형태 그대로 UI를 제공하고, 실험해보고 싶은 모든 플래그 조합을 넣어볼 수 있다.
생성뿐만이 아니라 읽기 쓰기도 해볼 수 있으며 조금 더 저수준 함수인 NtCreateFile까지도 다루어볼 수 있다.

우측의 버튼들을 클릭하면 아래처럼 또 다른 대화상자가 나와서 CreateFile의 많은 옵션들을 손쉽게 넣어서 테스트 할 수 있다.



파일 시스템과 관련이 있는 일은 하는 사람들은 두말 할 것도 없고, Wndows 플랫폼에서 개발하는 모든 개발자들이 알아두면 좋을 훌륭한 도구이다.
저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 재호님 팬 at 2010.12.29 12:14 신고 [edit/del]

    재호님~ 파일시스템에 관심이 많으신거 같네요!
    저도 파일시스템에 관심있어요 ^^
    이병오님의 "윈도우 파일시스템" 책과 정명수님의 커널관련글 읽으면서 공부하고 있는데 좋은거 같습니다. 재호님도 화이팅! ^^

    Reply
    • Favicon of http://www.benjaminlog.com BlogIcon 김재호 at 2010.12.29 12:37 신고 [edit/del]

      윈도 파일시스템 책은 저도 가지고 있는데 정명수님 커널 관련글은 잘 모르겠어요. 혹시 글들 정리되어 있는 URL이 있으면 좀 가르쳐주세요.^^

  2. 재호님 팬 at 2010.12.29 20:19 신고 [edit/del]

    이런 제가 센스가 없어서 ㅋㅋ
    www.swblog.net 입니다. 저도 마이크로소프트 잡지를 통해서 알게 되었구요~ 이분 글로 공부하고 있어요 ^^

    Reply

submit
유저모드에서는 CancelIo 함수를 통해서 해당 장치에 들어간 모든 I/O를 취소할 수 있고 CancelIoEx 함수를 통해서 특정 비동기 I/O만을 취소할 수도 있다.
비스타 이후부터는 CancelSynchronousIo 함수를 통해 CreateFile 같은 동기 함수도 취소할 수가 있다. 비동기가 지원이 되지 않는 함수들은 CreateFile 처럼 금방 수행되는 함수들인데, 이런 함수들을 과연 취소할 필요가 있는가 생각이 들수도 있지만, Network-redirector(SMB 같은)를 이용하여 원격지에 있는 파일에 접근할 경우 네트워크가 지연될 때 응용 프로그램에 꽤 오랜 블록킹이 발생할 수가 있다. 이런 함수를 잘 알고 이용하면 조금 더 응답성이 좋은 애플리케이션을 만들 수 있다.

사실 유저모드에서야 CancelIo를 부를 필요도 없이 애플리케이션을 꺼버리거나 I/O를 하는 장치의 핸들을 닫아버리면 알아서 취소가 되기 때문에 I/O의 취소에 대해서 그다지 고민할 일이 없지만 -대부분의 응용프로그래머들은 CancelIo같은 함수에 크게 관심을 갖지 않는다- 디바이스 드라이버에서는 조금 다르다.

디바이스 드라이버가 I/O의 취소를 제대로 구현해주지 않으면, 애플리케이션이 종료될 때에 Irp가 취소되지 못하고 드라이버에게 계속 잡혀있어서 애플리케이션이 제대로 종료되지 않은 채 계속 좀비로 남아있다거나, 운영체제가 셧다운되지도 않는 몹시 나쁜 상황을 맞이할 수 있기 때문에 I/O 취소의 올바른 구현은 필수적이다. -이런 경우를 조금이라도 방지하기 위해서 윈도우즈는 5분이 지나면 해당 Irp의 데이터구조는 삭제하지 않은채 취소를 시켜준다. 이것은 엄밀히 말하면 I/O의 취소라고는 할 수 없다.

그럼 디바이스 드라이버에서는 I/O를 어떻게 취소하는가.
디바이스 드라이버에게 I/O의 취소라는 것은 단순히 STATUS_CANCELLED 상태로 Irp를 완료시키는 것이다.
Irp에는 취소 루틴의 포인터가 담겨있는데, 우리가 이곳에 취소 로직을 적절히 구현하여 넣어주면 애플리케이션이 I/O의 취소를 요청할 때 I/O 매니저가 이 취소루틴을 호출 해주고 Irp는 취소로 완료될 수 있다.
하지만 드라이버는 취소루틴에서 STATUS_SUCCESS로 완료시켜버릴 수도 있고, STATUS_CANCELLED로 리턴하더라도 그 바로 직전 I/O가 정말로 완료되었을 수도 있기 때문에 유저모드에서 CancelIo 등을 사용해서 I/O가 완전히 끝났는지 잘 취소되었는지를 검사하는 것은 사실 별로 의미가 없다. 그냥 취소 했다는 것에만 의미를 두면 된다.

취소 루틴에 대해 간단하게 이야기 했지만,  취소루틴을 구현한다는 것은 생전 만나보지 못한 어려운 경쟁 상태를 해결해야 하기 때문에 많은 어려움이 따른다.
이런 어려움을 해소해주기 위해 마이크로소프트에서는 언제부턴가 Cancel-Safe Queue 라이브러리를 제공해주고 있다.

나는 다행히도 꽤 편한 세상에서 태어났고 디바이스 드라이버의 세상에 입문한지 얼마 되지 않았기 때문에, Cancel-Safe Queue를 사용하지 않고 직접 취소를 구현하는 드라이버는 구현해보지 않았다. -진심으로 다행이라 생각한다.

Cancel-Safe Queue를 이용하면 이런 골치 아픈 동기화 처리를 직접하지 않아도 된다.
우리는 라이브러리 루틴 내에서 제공하는 몇 가지 콜백함수들만 적절히 구현해주면 되는데, Csq 라이브러리가 자신들의 취소루틴을 붙였다 떼었다 하면서 우리의 콜백 루틴들을 동기화까지 포함해서 적절히 호출해주기 때문에 우리는 취소루틴을 제공할 필요도 없다. 대단하지 않은가. 이런 방식의 라이브러리를 제공한다는 것은 보통 일이 아니다.

앞으로 점점 많이 사용될 WDF에서는 우리가 취소에 관해 알아야 할 것들이 더욱 줄어들기 때문에 이런 처리가 더욱 쉬워지는데, 아직 WDF를 공부해보지 않아서 어떻게 동작하는 것인지는 모르겠다.

Cancel-Safe Queue의 예제 코드는 WDK 샘플 코드의 /src/general/cancel 위치에 있다.

Csq를 사용하기 위해 우리가 구현해줘야 할 콜백함수들은 다음과 같다.

  • XxxCsqInsertIrp
  • XxxCsqRemoveIrp
  • XxxCsqPeekNextIrp
  • XxxCsqAcquireLock
  • XxxCsqReleaseLock
  • XxxCsqCompleteCanceledIrp

이름에서 볼 수 있듯이 자료구조에 Irp를 넣고 찾고 빼고 잠그는 루틴들을 우리가 구현해주면 되는 것이다.
즉, 자료 구조와 동기화 방식을 우리가 결정할 수 있다. 자료구조는 거의 링크드 리스트를 이용하며, 어떤 커널모드 동기화 방식을 써서 구현해도 상관없지만 성능을 위해 보통 스핀락을 사용한다.
Csq를 쓰면 전역 캔슬 락을 사용해서 구현한 기존의 드라이버들 보다 성능에도 이점이 있다.

각 콜백 함수 구현에 대한 코드는 샘플 코드에서도 찾아볼 수 있지만, 이 문서에는 설명까지 덧붙여 잘 나와있으므로 한 번쯤 읽어보는 것이 좋겠다. 보통의 경우에는 샘플 코드를 복사해서 쓰는 것으로 충분할 것이므로 여기에 따로 코드를 적지는 않는다.

이 루틴들을 다 구현했으면 IoCsqInitialize 함수로 적절한 곳에서 초기화를 하며 콜백 함수들을 등록시켜준 뒤에, Irp가 들어올 때 IoCsqInsertIrp함수를 통해 큐에 집어넣고 나서 I/O 작업을 한다. 작업이 끝나면 IoCsqRemoveIrp함수를 통해 큐에서 빼고 잘 제거된지 확인 한 후에 여느 때처럼 IoCompleteRequest 함수로 Irp를 완료시켜주면 된다. 도중에 애플리케이션들로부터 취소가 요청되면 Csq가 적절히 우리가 작성한 취소 로직들을 이용하여 취소를 수행 해줄 것이다.

위의 설명에서 몇 가지 추가 설명을 해야 할 것들이 있는데, 콜백함수는 우리가 직접 부르는 것이 아니다. 우리 코드에서는 IoCsqInsertIrp처럼 IoCsqXxx 루틴들을 사용 한다. 이 루틴들이 내부에서 우리의 콜백함수를 적절한 곳에서 호출해줄 것이다.

초기화 할 때 IoCsqInitialize가 아니라 IoCsqInitializeEx함수를 이용하면 IoCsqInsertIrpEx 함수를 통해 추가적인 컨텍스트를 담을 수 있다. 이런 추가적인 컨텍스트는 우리가 Queue를 사용하는데 있어서 더 많은 유연함을 가능하게 한다. 

Csq를 사용할 때는 Csq에 관련된 데이터구조가 Irp->Tail.Overlay.DriverContext[3] 에 보관된다.
그러므로 Csq를 사용할 때는 이름이 DriverContext라고 해서 이 곳에 함부로 아무 데이터나 담아서는 안되겠다. 참고로 유저모드 파일시스템 드라이버 프레임워크인 Dokan에서는 Csq를 사용하지 않고 직접 취소루틴을 구현했는데, DriverContext[2]와 [3]에 추가적인 데이터를 담아 사용하고 있다.

위에서 작업이 끝나면 IoCsqRemoveIrp 함수를 통해 큐에서 빼고 잘 제거되었는지 확인하라고 했는데, 이는 그 사이에 취소가 들어왔을 경우 큐에서 이미 빠져버렸을 수 있기 때문이다. 만일 NULL이 리턴되었다면 도중에 취소 요청이 들어와 큐에서 이미 빠진 것이다. 그 Irp는 곧 XxxCsqCompleteCanceledIrp 루틴에 의해 완료되게 될 것이고 이 Irp를 우리가 또 완료시켜서는 안된다.

마지막으로 IRP_MJ_CLEANUP 디스패치 루틴에서는 내 디바이스의 핸들이 닫히는 경우에 큐에 Pending되어 있는 Irp들을 모두 완료 시켜주어야 한다.

저작자 표시 비영리 동일 조건 변경 허락
신고
  1. 디바이스 드라이버 at 2010.10.25 12:44 신고 [edit/del]

    우연히 찾게 되었는데 무척 좋은 글 잘 읽고 갑니다.^^

    Reply

submit