Обработка IRP_MN_REMOVE_DEVICE в DDK примере "toaster"

Тема в разделе "WASM.NT.KERNEL", создана пользователем Sheph, 27 июл 2008.

  1. Sheph

    Sheph New Member

    Публикаций:
    0
    Регистрация:
    24 янв 2008
    Сообщения:
    89
    В примере из DDK "toaster" (если конкретно, то toaster/bus) используется следующая методика для слежения за
    количеством активных IRP:
    Есть функции Bus_IncIoCount и Bus_DecIoCount, которые вызывают InterlockedIncrement и InterlockedDecrement
    на FdoData->OutstandingIO, где FdoData - DEVICE_EXTENSION FDO шины. Также, Bus_DecIoCount делает следующее:

    LONG result;

    result = InterlockedDecrement(&FdoData->OutstandingIO);

    ASSERT(result >= 0);

    if (result == 1) {
    //
    // Set the stop event. Note that when this happens
    // (i.e. a transition from 2 to 1), the type of requests we
    // want to be processed are already held instead of being
    // passed away, so that we can't "miss" a request that
    // will appear between the decrement and the moment when
    // the value is actually used.
    //

    KeSetEvent (&FdoData->StopEvent, IO_NO_INCREMENT, FALSE);

    }

    if (result == 0) {

    //
    // The count is 1-biased, so it can be zero only if an
    // extra decrement is done when a remove Irp is received
    //

    ASSERT(FdoData->DevicePnPState == Deleted);

    //
    // Set the remove event, so the device object can be deleted
    //

    KeSetEvent (&FdoData->RemoveEvent, IO_NO_INCREMENT, FALSE);

    }

    Т.е когда к-во висячих IRP становится равным 0 и Bus_DecIoCount был вызван ещё раз из обработчика IRP_MN_REMOVE_DEVICE, дабы указать что мы ждём того момента, когда устройство можно удалять, мы выставляем
    событие RemoveEvent. Обработчик IRP_MN_REMOVE_DEVICE ждёт этого события и дождавшись делает IoDeleteDevice.

    Bus_IncIoCount и Bus_DecIoCount используются во всех Dispatch процедурах, например в обработчике IRP_MJ_READ сначала надо вызвать Bus_IncIoCount, чтобы пометить IRP как висячий, а после его обработки вызвать Bus_DecIoCount, чтобы указать тот факт что запрос отработан. Так вот, рассмотрим следующую ситуацию:

    1.Мы поподаем в обработчик IRP_MJ_READ, и контекст переключается прямо перед Bus_IncIoCount, а если быть конкретнее, перед строкой:

    result = InterlockedIncrement(&FdoData->OutstandingIO);

    2.PnP Manager посылает нам IRP_MN_REMOVE_DEVICE, его код исополняется, доходит до ожидания события RemoveEvent и успешно проходит дальше, ведь в другом потоке InterlockedIncrement ещё не был выполнен, а больше запросов нет, проходим дальше и выполняем IoDeleteDevice, которое удаляет устройство и его DEVICE_EXTENSION в частности.

    3.Восстанавливаемся в обработчике IRP_MJ_READ выполняем

    result = InterlockedDecrement(&FdoData->OutstandingIO);

    и получаем BSOD, т.к никакого FdoData уже нет ???

    Мне непонятен этот момент! Подозреваю что всё не так и что PnP manager не пошлёт IRP_MN_REMOVE_DEVICE пока все IRP не завершаться, тогда зачем придумывать какие-то свои механизмы типа Bus_IncIoCount и Bus_DecIoCount ? Какой смысл вообще они несут ?
     
  2. Four-F

    Four-F New Member

    Публикаций:
    0
    Регистрация:
    31 авг 2002
    Сообщения:
    1.237
    Тут используется стандартный механизм Remove Lock (Using Remove Locks, My device is gone. Why am I still getting IRPs?). Обычно проще юзать системные апи IoAcquireRemoveLock/IoReleaseRemoveLock, но в тостере это сделано вручную.

    Там завязка на двух событиях StopEvent и RemoveEvent, а также на том, что счетчик OutstandingIO инициализируется единицей, и в IRP_MN_REMOVE_DEVICE он декрементится два раза. + к тому, все PnP запросы сериализируются, т.е., например, не получив ответ на IRP_MN_QUERY_REMOVE_DEVICE/IRP_MN_SURPRISE_REMOVAL PnP менеджер не пошлет IRP_MN_REMOVE_DEVICE и т.п. Вся эта сложная конструкция как раз и препятствует осуществлению описанного тобой сценария.

    Извиняюсь, что подробно не объясняю - слишком долго клаву топтать. У Walter Oney в гниге Programming The Windows Driver Model в шестой главе есть раздел "Why Do I Need This @#$! Remove Lock, Anyway?". Там очень продробно описано как это работает и разобран именно описываемый тобой сценарий. А также объяснено почему Remove Lock на самом деле делает не то, что все думают.
     
  3. Sheph

    Sheph New Member

    Публикаций:
    0
    Регистрация:
    24 янв 2008
    Сообщения:
    89
    Four-F, прочитал, теперь понятно.
    P.S: Как же всё запутано, вместо того чтобы релизовывать полезный функционал, приходится писать кучу boilerplate'а, который ещё хрен знает будет ли правильно работать. Кстати, в той книге была описана структура DEVQUEUE, на первый взгляд весьма полезная, стоит ли ей пользоваться ? Насколько она надёжна ?
     
  4. Four-F

    Four-F New Member

    Публикаций:
    0
    Регистрация:
    31 авг 2002
    Сообщения:
    1.237
    Ну тогда добро пожаловать в Windows Driver Foundation. Многие примеры в WDK - это DDK-порты WDM -> WDF. Для тостера тоже есть порт.

    Надежна, но лучше юзай Cancel-Safe IRP Queue.
     
  5. Sheph

    Sheph New Member

    Публикаций:
    0
    Регистрация:
    24 янв 2008
    Сообщения:
    89
    Four-F, просто для Cancel-Safe Queue обязательно нужен поток, а меня это не устраивает, у меня 4 независимых очереди, а заводить 4 потока не хочется. Более того, девайсов может быть хоть 10, в этом случае получается 40(!) потоков.
    P.S: Кстати, ещё один вопрос, не хочется заводить отдельную тему, вообщем вот:
    В примере из DDK "serial" работа с DPC производится следующим образом:
    1. Есть счётчик DPC - DpcCount и событие PendingDpcEvent. Изначально DpcCount = 1, событие сброшено
    2. Во время работы драйвер назначает кучу DPC, но перед тем как назначить делает InterlockedIncrement на DpcCount
    3. Когда DPC завершается, она делает InterlockedDecrement на DpcCount и если DpcCount == 0 то выставляет PendingDpcEvent
    4. SerialClose делает InterlockedDecrement на DpcCount и ждёт PendingDpcEvent

    Так вот, рассмотрим такой сценарий: какой-нибудь DPC вызовет InterlockedDecrement, но не вызовет return (не завершится), затем контекст переключится на SerialClose, тот пройдёт сквозь KeWaitForSingleObject, закроет файл, потом вызовется IRP_MN_REMOVE_DEVICE, потом вызовется DriverUnload и вот нам BSOD, ведь тот DPC ещё не завершился.

    Если я правильно понимаю, такой сценарий невозможет, т.к DPC исполняется на уровне DISPATCH_LEVEL, а обработчик SerialClose на PASSIVE_LEVEL, следовательно мы не сможем переключить контекст на SerialClose, а сможем только на поток с IRQL > DISPATCH_LEVEL, но это не страшно. Т.е наш DPC завершится, вызовет InterlockedDecrement и выйдет и только после этого мы попадём в KeWaitXXX в SerialClose, следовательно, такая схема учёта к-ва исполняющихся DPC надёжна. Я прав ?
     
  6. Four-F

    Four-F New Member

    Публикаций:
    0
    Регистрация:
    31 авг 2002
    Сообщения:
    1.237
    Может я чего не понял, но не нужно там никаких потоков. Внутри себя CSQ потоков тоже не создает.

    Этот пример я подробно не разбирал, так что залез в код только что и нашёл там опорожнение всех очередей dpc: IRP_MN_REMOVE_DEVICE -> SerialRemoveDevObj -> SerialDisableInterfacesResources -> SerialReleaseResources -> SerialRemoveQueueDpc. Так что, когда IRP_MN_REMOVE_DEVICE завершится никаких dpc уже не будет.
     
  7. Sheph

    Sheph New Member

    Публикаций:
    0
    Регистрация:
    24 янв 2008
    Сообщения:
    89
    Four-F, вот я тоже не понимаю, где мне IoCsqRemoveIrp вызывать как не в потоке. Я просто читал пример из DDK, "cancel" называется, так вот, там для вычитывания делается поток. В книге Walter Oney тоже говорится что нужен поток, честно говоря, я не совсем понимаю как здесь можно без потока обойтись...

    P.S:
    Ещё хочется узнать прав ли я в этом месте:
    т.к DPC исполняется на уровне DISPATCH_LEVEL, а обработчик IRP_MJ_CLOSE на PASSIVE_LEVEL, следовательно мы не сможем переключить контекст на обработчик IRP_MJ_CLOSE во время выполнения DPC, а сможем только на поток с IRQL > DISPATCH_LEVEL. Т.е наш DPC завершится и только после этого мы можем продолжить выполнение в обработчике IRP_MJ_CLOSE...
     
  8. Four-F

    Four-F New Member

    Публикаций:
    0
    Регистрация:
    31 авг 2002
    Сообщения:
    1.237
    Если мы говорим про драйвер, то ему никакой поток не нужен, ни для использования CSQ, ни для других, описанных в ддк и у Уолтера Они (ммм.. про DEVQUEUE не помню, но вряд ли...) типов очередей. Задача драйвера - обрабатывать IRP. Если IRP не может быть обработан в момент получения, но может быть обработан позже, его надо где-то хранить, тем более, что IRP может быть больше одного. Только для этого и нужна очередь. Всю работу с IRP и очередью драйвер выполняет в контексте текущего (чужого) потока. В DDKашном примере cancel, поток просто имитирует некий внешний фактор или драйвер работает с девайсом требующим периодического опроса (частный случай). В общем случае для работы с очередью собственный отдельный поток не нужен. Например, класс-дарайвер клавы kbdclass постоянно получает IRP от csrss, засовывает их в очередь (в потоке принадлежащем csrss) и курит. Когда юзер топтанет клаву, сгенерится аппаратное прерывание и его обработчик дернет драйвер, который достанет IRP и завершит (в случайном текущем потоке). Т.е. kbdclass по-барабану какой там поток сейчас выполняется. Драйверы вообще редко используют собственные потоки.

    1. Контекст переключает планировщик потоков.
    2. Планировщик не работает на DISPATCH_LEVEL, т.е. до тех пор пока поток сам не понизит IRQL, он (поток) будет выполняться вечно.
    3. Аппаратные прерывания (IRQL > DISPATCH_LEVEL) обрабатываются в контексте текущего потока.
    4. На мультипроцессорной машине потоки выполняются параллельно независимо от их IRQL, т.е. в данном случае обработчик IRP_MJ_CLOSE на PASSIVE_LEVEL и обработчик DPC на DISPATCH_LEVEL будут работать параллельно (разумеется, если между ними не предусмотрена синхронизация).
     
  9. Sheph

    Sheph New Member

    Публикаций:
    0
    Регистрация:
    24 янв 2008
    Сообщения:
    89
    Four-F, спасибо за объяснение, насчёт Csq понятно.
    А вот насчёт DPC не совсем, т.е получается что нет надёжного способа дождаться завершения всех DPC перед тем как делать DriverUnload ? Счётчик тут как я понял не поможет, если процов больше чем один. KeRemoveQueueDpc тоже не особо поможет, он просто вернёт FALSE если DPC уже выполняется, а завершился он или нет он не скажет.
    Но это ведь не корректно, завершать DriverUnload с ещё не завершёнными DPC, BSOD же будет или я чего-то не понимаю...
     
  10. Four-F

    Four-F New Member

    Публикаций:
    0
    Регистрация:
    31 авг 2002
    Сообщения:
    1.237
    Не морочь себе голову. Синхронизация происходит ЗАДОЛГО до того, как DriverUnload может быть вызвана, а именно в SerialClose, т.е. при закрытии юзерного хендла.

    Код (Text):
    1. SerialClose(
    2.     )
    3. {
    4. ...
    5.     pendingDPCs = InterlockedDecrement(&extension->DpcCount);
    6.     if (pendingDPCs) {
    7.        KeWaitForSingleObject(&extension->PendingDpcEvent, ...);
    8.     }
    9. ...
    10.     InterlockedIncrement(&extension->DpcCount);
    11. }
    Если DPC уже выполняется, то SerialClose будет ждать, пока dpc-процедура не доберется до SerialDpcEpilogue. Пока не закрыт хендл PnP-менеджер не пошлет ни одного PnP-IRP. Так что до DriverUnload ещё очень и очень далеко.

    Рассматривать же теоретическую возможность того, что...

    1. dpc-процедура уже вызвала SerialDpcEpilogue и та, в свою очередь уже внутри KeSetEvent(&extension->PendingDpcEvent, ...) и событие уже просигналено, и остается всего несколько инструкций до выхода из KeSetEvent/SerialDpcEpilogue/dpc-процедура. И всё это на DISPATCH_LEVEL.

    2. Но при этом SerialClose только начинает просыпаться внутри KeWaitForSingleObject, потом что-то делает, потом бодяга с закрытием хендла, потом длинная бодяга с обработкой нескольких PnP-IRP, потом бодяга с DriverUnload... И всё это на PASSIVE_LEVEL.

    ...2. произойдет быстрее чем 1. мне лень :)

    Возможно чисто теоретически действительно есть отличная от нуля вероятность этого... хз.
     
  11. Sheph

    Sheph New Member

    Публикаций:
    0
    Регистрация:
    24 янв 2008
    Сообщения:
    89
    Во, вот про это я и говорил, мне просто было интересно возможно ли это теоретически. На практике конечно такое
    вряд ли произойдёт, да и вообще вся эта синхронизация срабатывает крайне редко, мне с трудом удалось заставить попасть код в KeWaitXXX, это случилось только при агрессивной бомбордировке IRP_MJ_WRITE, а затем TerminateProcess...