IRP를 처리하는 과정을 공부하다 보니... 몇가지 이해가 가지 않았던 부분들이 있었습니다. 예를 들면...

1. IoMarkIrpPending을 도대체 왜 해주는 건지...?

2. Irp->PendingReturnedFlag 와 IoStackLocation의 SL_PENDING Flag (IoMarkIrpPending)가 가 별도로 존재하는 이유

3. Dispatch 루틴이나 Completion Routine에서 IoMarkIrpPending을 호출해주어야 하는 경우? 호출해주지 않아도 되는 경우? 호출해선 안되는 경우?

... 모 이런 궁금증 들이었죠. 

이제... 이것 저것 찾아보다 보니 이제 확실하게 이해가 된것 같아서... 여기 정리해봅니다.


Q: 왜.. 어떨때.. IoMarkIrpPending 를 호출해야 하는가?

A: 완료루틴 코드를 보다 보면... 이렇게 생긴 코드가 무지 자주 보입니다.

IoMarkIrpPending 은 다음과 같이 생긴 매크로입니다.

말하자면... 위의 코드는 완료루틴에서 Irp->PendingReturned 의 플래그 세팅 여부를 Irp CurrentStack 위치의 SL_PENDING_RETURNED 플래그로 전달하는 역할을 합니다. 내 완료루틴이 return되면 완료루틴을 호출한 IoCompleteRequest 함수는 내가 Mark한 SL_PENDING_RETURNED 플래그를 다시 Irp->PendingReturned에 전달하고... 그 다음 상위 드라이버의 완료루틴은 그 Irp->PendingReturned 플래그를 다시 (IoMarkIrpPending 매크로를 호출함으로서) IoStackLocation의 SL_PENDING_RETURNED 로 전달하고... 하면서 최 상위 드라이버의 완료루틴이 호출될 때까지 전달전달...합니다.

왜 이런 작업을 해야 되는지 이유를 찾아본 결과... WDM의 Classic 에서 그 답을 찾을 수 있었습니다 (여기!)

여기서는 Walter Oney 책의 내용을 좀 더 쉽게 풀어서 써보고자 합니다.

I/O Manager는 시스템 성능을 높이기 위해 드라이버들이 IRP Completion을 나중에 따로 처리(Defer)하는 "비동기 I/O"를 지원합니다. 드라이버들은 Completion Routine을 나중에 처리하기 위해 "IoMarkIrpPending"을 호출하고 Dispatch루틴에서 STATUS_PENDING을 리턴하면 됩니다. 그런데 가끔 I/O Manager 는 자신이 만들어서 내려보낸 IRP의 Pending 처리되가 완료되는 시점까지 기다릴 필요가 있는데, 이를 위해 다음과 같은 처리를 합니다. (완료루틴에서 SetEvent할때까지 Dispatch 루틴에서 WaitFor~ 해주는 것과 비슷합니다.)

만약 저 Irp->UserEvent를 누군가가 Set해주지 않는다면 I/O Manager의 시스템스레드는 영원히 깨어나지 못하게 되겠죠? 저 UserEvent를 KeSetEvent해주는 것이 IoCompleteRequest 함수가 하는 일 중 하나입니다. IoCompleteRequest는 Special KernelMode Apc를 등록함으로서 APC에서 KeSetEvent가 호출되도록 합니다. (왜 직접 KeSetEvent하지 않는지는 아직 모르겠습니다. ^^;;) ==> 아래 댓글 부분 참고! ^^

그런데 문제는... APC란 작업이 꽤 시스템에 성능 부하를 주는 작업이기 때문에... 꼭 필요할 때만(I/O Manager가 WaitFor~ 를 하는 경우에만) 해 주는 것이 시스템 성능을 향상시킬 수 있다는 점입니다. 이를 위해 IoCompleteRequest는 현재 I/O Manager가 Event를 WaitFor 하고 있는지(즉, APC를 통해 SetEvent를 해주어야 하는 상황인지)를 판단할 근거가 필요한데, 이것이 바로 Irp->PendingReturned Flag가 되겠습니다.

즉, 아래의 세가지 표현은 동일한 의미가 됩니다.

DeviceStack에서 해당 IRP에 대해서 최종적으로 Pending을 리턴했는가?

I/O Manager가 Irp->UserEvent를 WaitFor... 하고 있는가?

현재 IoCompleteRequest가 실행중인 Thread Context가 최초에 I/O Manager가 IoCallDriver를 호출했던 Thread와 동일한 Context인가??


일단... IoCompleteRequest 함수가 어떤 일을 하는지를 살펴 봅시다.

아래의 코드는... ReactOs의 소스코드를 바탕으로 코드의 흐름만을 남기고 다 정리한 내용입니다.

간단하게 정리하면... IoCompleteRequest  함수는 최상단 드라이버의 완료루틴 호출까지가 마무리된 시점에서 "Irp->PendingReturned" 플래그가 세팅되지 않은 경우에만 Irp->UserEvent 를 Set 하게 된다는 것을 알수 있습니다. 

IoStackLocation의 SL_PENDING_RETURNED 및 Irp->PendingReturned Flag는 ioCompleteRequest 호출 시점에서 현재 스레드가 I/O Manager가 최초 호출한 IoCallDriver의 Context와 동일한 Context를 유지하고 있는지를 판단하는 기준이 된다.


그렇기 때문에 DeviceStack 상의 어떤 드라이버가 STATUS_PENDING을 리턴시키고 Irp를 Pending시켰는데, 해당 Irp의 Completion 처리 과정에서 "Irp->PendingReturned" Flag가 최 상위 드라이버까지 전달되지 않는다면, IoCompleteRequest가 KeSetEvent를 호출해주지 않기 때문에 I/O Manager의 시스템 스레드가 영원히 깨어나지 못하게 됩니다. 이것이 완료루틴에서

를 해주는 이유입니다.

반대로 모든 IRP에 대해 IoMarkIrpPending을 호출해준다면?? 그러면 불필요한 Apc가 남발되기 때문에 시스템 성능이 떨어지게 되지만 데드락 같은 문제는 생기지 않습니다~


Q: Irp->PendingReturnedFlag와 IoStackLocation의 SL_PENDING_RETURNED Flag가 별도로 존재하는 이유?

A: 정말 이해가 안가던 부분인데... 이제는 이해했습니다. ^^

위 질문(Irp->PendingReturnedFlag와 IoStackLocation의 SL_PENDING_RETURNED Flag가 별도로 존재하는 이유)의 답을 이해하기 위해서는, 디바이스 스택 상의 드라이버 중 하나의 Dispatch 루틴에서 STATUS_PENDING을 리턴했다고 해도 IRP의 최종적인 처리가 비동기식이 아닐수도 있다는 것을 이해하는 것이 중요합니다.

하위 드라이버의 Dispatch 루틴이 STATUS_PENDING을 리턴했다고 해도 상위 드라이버의 Dispatch 루틴에서 WaitFor~ 로 대기 처리를 하는 경우에는 결과적으로 최초에 IRP를 전송한 I/O Manager 입장에서는 IRP가 (비동기 처리가 아니라) 동기적으로 처리되었다고 볼수 있다는 거죠. 그러니까 I/O Manager는 IRP 완료시점까지 IoCallDriver가 return하지 않을 것이고, 그러니 KeWaitForSingleObject(Irp->UserEvent) 도 (당연히) 하지 않을것이고, IoCompleteRequest 에서도 SetEvent를 해줄 필요가 없습니다.  

따라서, Dispatch 루틴에서 PENDING을 리턴하지 않고 대기중인 드라이버의 완료루틴에서는 (하위 드라이버에서 Pending Flag를 mark했다고 하더라도) IoMarkIrpPending 을 해줄 필요가 없는 것입니다. 

예를 들어... 디바이스 스택 상에 다음과 같은 드라이버가 있다면, 하위 드라이버의 Dispatch루틴이 STATUS_PENDING을 리턴한다고 해도 상위 드라이버에게 STATUS_PENDING 리턴값이 전달되지 않게 되고, (상위 드라이버들에게) 최종적으로 IRP는 동기적인 처리가 이루어지게 됩니다.

이와 같이 IoCallDriver를 최초 호출해 IRP를 전송한 I/O Manager에게 STATUS_PENDING 이 return될지 여부가 디바이스 스택 상의 어느 한 드라이버의 행동만으로 결정될 부분이 아니기 때문에, IRP 본체가 아닌 IoStack 상에 Pending 비트를 설정하도록 해놓고 완료루틴 호출과정에서 각 드라이버들에게 물어보아 최종적인 IRP 처리 상태를 확인하는 것입니다. 

만약 IRP 구조체에 한개의 Flag만 세팅하도록 한다면... 디바이스 스택 상의 드라이버중에 STATUS_PENDING을 리턴한 드라이버가 하나라도 있었는지 확인은 될 수 있겠지만 최종적으로 IRP가 STATUS_PENDING으로 리턴되었는지 여부는 확인이 불가능할 것입니다.

결론적으로 Irp->PendingReturnedFlag와 IoStack상의 SL_PENDING_RETURNED 는 다음과 같은 차이가 있습니다.

Irp->PendingReturnedFlag : IRP 완료처리 과정에서 IoCompleteRequest 함수가 디바이스 스택상의 드라이버들을 Loop돌면서 각 드라이버들의 완료루틴을 호출해주는데, 이때 각 드라이버들에게 "니 바로 밑에 있는 드라이버가 Pending을 Return했다"라고 알려주는 플래그입니다. 즉, Irp->PendingReturnedFlag는 "I/O Manager가 드라이버에게 전달하는 정보" 인 셈이죠.

SL_PENDING_RETURNED (IoMarkIrpPending) : IoCompleteRequest에 의해서 각 드라이버들의 완료루틴이 호출되었을 때, 전달된 위에서 설명한 바와 같이 바로 아래 드라이버의 Pending 처리 여부가 Irp의 PendingReturnedFlag에 전달됩니다. 이것을 "이전 드라이버의 Pending 처리를 나도 위로 전달할지" 여부를 결정해서 IoCompleteRequest에게 전달하는 방법(Output Parameter)이 바로 IoMarkPending입니다. 즉 IoMarkIrpPending은 "드라이버가 I/O Manager에게 전달하는 정보"입니다. Irp의 PendingReturnedFlag와는 전달 방향이 반대죠. ^^

완료루틴에서 Pending처리 전달여부를 결정한다고 하니 좀 이상하게 들리는데요... 사실은 완료루틴 호출 시점에서는 이미 Dispatch 루틴에서 어떻게 처리했는지에 따라 결정이 나 있는 상황이구요... 이걸 대답해주기만 하면 됩니다. 나의 "Dispatch" 루틴에서 이미 STATUS_PENDING을 (명시적으로) 리턴해버린 상황이라면 IoMarkIrpPending을 전달해주어야 하구요... 아니면 (==> Dispatch 루틴이 아직 return하지 않고 대기중이라면) IoMarkIrpPending을 해 줄 필요가 없습니다. 왜냐하면... Dispatch 루틴이 완료루틴 호출시점까지 대기중(WaitFor~)인 상황이라면 

완료루틴 호출 시점에서 현재 Dispatch 루틴에서 WaitFor 중인 Thread의 Context == I/O Manager가 최초에 IoCallDriver를 호출한 Thread

라고 볼 수 있기 때문입니다.

말하자면, 

 위의 코드는 "바로 밑에 있는 드라이버가 Pending을 리턴했으면 나도 그렇게 하겠다."라고 하는 의사 결정이라기 보다는 "내 Dispatch루틴은 바로 아래 있는 드라이버가 STATUS_PENDING을 리턴한 경우 그 리턴값을 그대로 상위 드라이버에 전달하도록 구현되어 있다"고 확인해주는 작업입니다. Dispatch 루틴이 STATUS_PENDING을 리턴했다면 완료루틴에서도 IoMarkIrpPending을 해주고, 그렇지 않다면 해줄 필요가 없는 거죠.



Q: 결론적으로, 어떨때 IoMarkPending을 호출해야 하는가?

A: 완료루틴인지 Dispatch루틴인지에 따라 경우가 다릅니다.

일단 Dispatch루틴의 경우는 다음과 같이 정리할 수 있습니다.

Dispatch 루틴에서 (자신의 필요에 의해) 명시적으로 STATUS_PENDING을 리턴하는 경우에는 IoMarkIrpPending을 해주어야 한다.

말하자면 이런거죠.

그런데 왜 "명시적으로"라는 단서가 붙었을까요? 

이런 경우도 있다는 얘기입니다.

위와 같은 경우는 IoCallDriver의 리턴값이 STATUS_PENDING이라고 하더라도 이미 IoSkipCurrentIrpStackLocation해 버렸기 때문에 IoMarkPending을 할 필요가 없습니다. (내 스택이 없어졌으니까요~ Mark 찍을 곳이 없어져버렸네요~ ^^)

완료루틴의 경우는... 위에서 설명을 자세하게 했습니다만... 한마디로 요약하면 다음과 같습니다.
완료루틴에서 STATUS_MORE_PROCESSING_REQUIRED 가 아닌 다른 값을 리턴하는 경우라면, 다음의 코드를 실행해 주어야 합니다. 

    if (Irp->PendingReturned) {  
        IoMarkIrpPending( Irp );    // propargate!!  
    }  

STATUS_MORE_PROCESS_REQUIRED가 리턴될때는 왜 저 작업을 하지 않아도 될까요??
STATUS_MORE_PROCESS_REQUIRED가 리턴되는 경우는 둘중 한가지 경우로 생각할 수 있습니다.

1. Dispatch 루틴에서 완료루틴의 KeSetEvent를 기다리고 있는 경우 : 이 경우는 위에서 설명한 바와 같이 Dispatch루틴에서 STATUS_PENDING 리턴값이 상위 드라이버에 전달되지 않았고, 최초 IoCallDriver가 호출된 Thread Context가 유지되고 있는 상황이기 때문에 IoMarkPending을 해 줄 필요가 없습니다. 

2. Dispatch 루틴에서 IRP를 Queueing한 후 STATUS_PENDING을 이미 리턴해버린 경우 : 이 경우에는 Dispatch 루틴에서 STATUS_PENDING을 리턴하기 전에 이미 IoMarkPending을 리턴했을 것입니다. 따라서 완료루틴에서 다시 IoMarkPending을 할 필요가 없지요~ ^^

네. 두가지 경우 모두... 필요가 없겠군요. ^^


Posted by kuaaan

댓글을 달아 주세요

  1. 이태화 2012.04.25 08:02  댓글주소  수정/삭제  댓글쓰기

    APC를 사용해서 SetEvent를 해주는 이유는 해당 Thread의 Context로 이동하기 위함입니다. 다른 Process의 Context에서 Complete를 완료하게 되면 UserBuffer가 올바르지 않을 수 있기 때문입니다.



사랑합니다. 편안히 잠드소서