В примере из 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 ? Какой смысл вообще они несут ?
Тут используется стандартный механизм 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 на самом деле делает не то, что все думают.
Four-F, прочитал, теперь понятно. P.S: Как же всё запутано, вместо того чтобы релизовывать полезный функционал, приходится писать кучу boilerplate'а, который ещё хрен знает будет ли правильно работать. Кстати, в той книге была описана структура DEVQUEUE, на первый взгляд весьма полезная, стоит ли ей пользоваться ? Насколько она надёжна ?
Ну тогда добро пожаловать в Windows Driver Foundation. Многие примеры в WDK - это DDK-порты WDM -> WDF. Для тостера тоже есть порт. Надежна, но лучше юзай Cancel-Safe IRP Queue.
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 надёжна. Я прав ?
Может я чего не понял, но не нужно там никаких потоков. Внутри себя CSQ потоков тоже не создает. Этот пример я подробно не разбирал, так что залез в код только что и нашёл там опорожнение всех очередей dpc: IRP_MN_REMOVE_DEVICE -> SerialRemoveDevObj -> SerialDisableInterfacesResources -> SerialReleaseResources -> SerialRemoveQueueDpc. Так что, когда IRP_MN_REMOVE_DEVICE завершится никаких dpc уже не будет.
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...
Если мы говорим про драйвер, то ему никакой поток не нужен, ни для использования 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 будут работать параллельно (разумеется, если между ними не предусмотрена синхронизация).
Four-F, спасибо за объяснение, насчёт Csq понятно. А вот насчёт DPC не совсем, т.е получается что нет надёжного способа дождаться завершения всех DPC перед тем как делать DriverUnload ? Счётчик тут как я понял не поможет, если процов больше чем один. KeRemoveQueueDpc тоже не особо поможет, он просто вернёт FALSE если DPC уже выполняется, а завершился он или нет он не скажет. Но это ведь не корректно, завершать DriverUnload с ещё не завершёнными DPC, BSOD же будет или я чего-то не понимаю...
Не морочь себе голову. Синхронизация происходит ЗАДОЛГО до того, как DriverUnload может быть вызвана, а именно в SerialClose, т.е. при закрытии юзерного хендла. Код (Text): SerialClose( ) { ... pendingDPCs = InterlockedDecrement(&extension->DpcCount); if (pendingDPCs) { KeWaitForSingleObject(&extension->PendingDpcEvent, ...); } ... InterlockedIncrement(&extension->DpcCount); } Если 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. мне лень Возможно чисто теоретически действительно есть отличная от нуля вероятность этого... хз.
Во, вот про это я и говорил, мне просто было интересно возможно ли это теоретически. На практике конечно такое вряд ли произойдёт, да и вообще вся эта синхронизация срабатывает крайне редко, мне с трудом удалось заставить попасть код в KeWaitXXX, это случилось только при агрессивной бомбордировке IRP_MJ_WRITE, а затем TerminateProcess...