Интервалы ожидания для отдельного потока
Достаточно часто встречаются ситуации, когда отдельно взятый поток вынужден откладывать продолжение своей работы на более поздний срок. Например, поток, занимающийся периодическим опросом устройства (если с этим устройством невозможно работать через механизм прерываний), должен задерживать свою работу каким-нибудь способом, более приемлемым, нежели цикл for с настраиваемым числом проходов.
Поток, который желает приостановить свою работу на время до 50 мкс, может использовать вызов KeStallExecutionProcessor.
Таблица 10.7. Прототип вызова KeStallExecutionProcessor
VOID KeStallExecutionProcessor | IRQL == любой |
Параметры | Останавливает работу на указанный интервал, независимо от производительности процессора |
IN ULONG IntervalCount | Время задержки в 1 мкс интервалах |
Возвращаемое значение | void |
В случае, если устройство должно опрашиваться быстро, но все-таки с интервалом более чем 50 мкс, драйвер должен использовать несколько программных потоков, о чем речь пойдёт далее.
Более сложным является вызов KeDelayExecutionThread (таблица 10.8). Он удаляет программный поток из очереди "ready to run", следовательно, не мешает выполнению других потоков, готовых к работе. Минимальный временной интервал определяемой им задержки составляет 100 нс.
Рекомендуемые для драйверов значения WaitMode=KernelMode
и Alertable=FALSE ограничивают применимость вызова KeDelayExecutionThread кодом системных потоков, созданных самим драйвером, и кодом процедур инициализации и завершения работы драйвера (то есть работающего заведомо вне пользовательского контекста).
Таблица 10.8. Прототип вызова KeDelayExecutionThread
NTSTATUS KeDelayExecutionThread | IRQL == PASSIVE_LEVEL | |
Параметры | Останавливает работу на указанный интервал, независимо от производительности процессора | |
IN KPROCESSOR_MODE WaitMode | Для драйверов: KernelMode | |
IN BOOLEAN Alertable | Для драйверов: FALSE | |
IN PLARGE_INTEGER TimeInterval | Время задержки в 100нс интервалах | |
Возвращаемое значение | STATUS_SUCCESS — ожидание завершено |
Значение TimeInterval может описывать как относительные, так и абсолютные временные интервалы. Для задания абсолютных интервалов следует использовать вызов KeQuerySystemTime
(см. таблицу 7.45), при помощи которого можно получить текущее системное время — как время начала ожидания. Функции, которые можно использовать для операции над типом данных LARGE_INTEGER, перечислены в таблице 7.44.
Переходным методом (между методами, описанными выше, и методами с использованием синхронизационных примитивов, рассматриваемых далее) является использование callback-функции IoTimerRoutine, которую драйвер регистрирует при помощи вызова IoInitializeTimer (имя данной функции, IoTimerRoutine, здесь присвоено для определенности, разработчик имеет право называть ее любым приглянувшимся ему именем). "Переходным" этот метод можно считать потому, что объект таймера уже используется в полной мере, но этот объект пока еще скрыт от разработчика драйвера за интерфейсом функций, предоставляемых Диспетчером ввода/вывода.
Таблица 10.9. Прототип вызова IoInitializeTimer
NTSTATUS IoInitializeTimer | IRQL == PASSIVE_LEVEL |
Параметры | Выполняет регистрацию callback-функции IoTimerRoutine, предоставляемой драйвером |
IN PDEVICE_OBJECT pDevObject | Объект устройства инициатора вызова, за которым будет "закреплен" создаваемый данным вызовом объект таймера |
IN PIO_TIMER_ROUTINE pIoTimerRoutine | Указатель на регистрируемую callback-функцию IoTimerRoutine |
IN PVOID pContext | Аргумент, передаваемый впоследствии в callback-функцию IoTimerRoutine |
Возвращаемое значение | STATUS_SUCCESS при успешном завершении |
Поскольку функция IoTimerRoutine при вызове будет получать указатель на объект устройства (из которого можно легко определить местоположение структуры расширения объекта устройства), то необходимые контекстные параметры можно разместить и в расширении устройства, в частности счетчик вызовов. Подробнее вопросы подсчета односекундных интервалов будут обсуждены ниже.
Таблица 10.10. Прототип функции обратного вызова IoTimerRoutine
VOID IoTimerRoutine | IRQL == DISPATCH_LEVEL |
Параметры | Callback-функция, вызываемая через 1 сек. интервал |
IN PDEVICE_OBJECT pDeviceObject | Указатель на объект устройства, с которым соотнесена данная функция |
IN PVOID pContext | Контекстный аргумент |
Возвращаемое значение | void |
Если внимательно присмотреться к структуре DEVICE_OBJECT, то несложно заметить, что поле "PIO_TIMER Timer" в этой структуре единственное. Это недвусмысленно подразумевает, что более одной функций IoTimerRoutine для данного устройства использовать просто невозможно, хотя ничто не запрещает использовать одну callback-функцию IoTimerRoutine c несколькими объектами устройств.
Для запуска таймера, ассоциированного с callback-функцией IoTimerRoutine, используется вызов IoStartTimer. Останавливается таймер вызовом IoStopTimer.
Таблица 10.11. Прототип функции обратного вызова IoStartTimer
VOID IoStartTimer | IRQL<=DISPATCH_LEVEL |
Параметры | Запуск таймера, в результате чего callback-функция IoTimerRoutine, соотнесенная с данным объектом устройства будет вызываться каждую секунду |
IN PDEVICE_OBJECT pDeviceObject | Указатель на объект устройства, с которым соотнесен таймер, который следует запустить |
Возвращаемое значение | void |
VOID IoStopTimer | IRQL<=DISPATCH_LEVEL |
Параметры | Остановка таймера |
IN PDEVICE_OBJECT pDeviceObject | Указатель на объект устройства, с которым соотнесен таймер, который следует остановить |
Возвращаемое значение | void |
Выполнение вызова IoStopTimer из функции IoTimerRoutine не допускается.
Для организации ожидания, более длительного, чем 1 секунда, можно организовать счетчик (например, внутри структуры расширения устройства), значение которого можно уменьшать (увеличивать) на единицу в самой функции IoTimerRoutine.
Работа с использованием callback-функции IoTimerRoutine может протекать следующим образом.
и связываются таймерная функция IoTimerRoutine и конкретный объект устройства.
Разумеется, практически использовать собственно код callback-функции IoTimerRoutine можно весьма ограниченно, поскольку она стоит в стороне от "главных дорог" драйверных потоков. Как правило, при работе с этой функцией привлекаются еще DPC процедуры и/или другие синхронизационные примитивы (например, объекты события).
Рассмотрим несложный частный случай.
Предположим, существует устройство, которое генерирует прерывание всякий раз, когда оно готово принять очередную порцию данных.
Дополняем структуру расширения объекта устройства счетчиком времени, оставшегося до наступления таймаута (превышения времени ожидания) с момента последнего прерывания, поступившего от обслуживаемого устройства:
typedef struct {
. . . LONG Remaining; // сколько еще осталось секунд . . . } MYDEVICE_EXTENSION, *PMYDEVICE_EXTENSION;
В процедуре AddDevice инициализируем таймер, связанный с данным объектом устройства. Значение счетчика не устанавливаем до момента реального запуска таймера.
NTSTATUS AddDevice ( IN PDRIVER_OBJECT pDriverObject, IN PDEVICE_OBJECT pDeviceObject ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDeviceObject ->DeviceExtension; . . . IoInitializeTimer(pDeviceObject, MyIoTimerRoutine, pDevExt); . . . }
Рабочая процедура драйвера CreateRequestHandler вызывается, когда в пользовательском приложении была попытка доступа к устройству через Win API вызов CreateFile. В этот момент вполне можно запустить таймер. Он продолжает отсчеты до тех пор, пока дескриптор доступа к устройству из пользовательского приложения остается открытым. Поскольку таймер работает, а его отсчеты нам еще не нужны, то необходимо инициализировать таймер таким значением, которое показывало бы, что его можно игнорировать (это будет -1).
NTSTATUS CreateRequestHandler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension; . . . // ближе к концу инициализируем и запускаем таймер pDevExt->Remaining = -1; IoStartTimer(pDevObj); . . . }
Всякий раз, когда запускается обработка нового IRP пакета, а драйвер ожидает разрешающие сигналы прерывания от физического устройства, необходимо производить подсчет "тиков" таймера для того, чтобы можно было определенно сказать, не превышено ли время ожидания очередного сигнала. В счетчике необходимо выставить число секунд, которое драйвер может считать, что допустимая длительность ожидания не превышена. Доступ к счетчику из разных ветвей кода драйвера должен быть синхронизирован во избежание порчи его значения, поскольку и процедура обработки прерываний и callback-функция, вызываемая по каждому отсчету таймера, работающие как части обработки прерываний, будут обращаться к этому счетчику.
Использование системного вызова InterlockedExchange обеспечивает безопасное обновление и считывание 32-разрядного счетчика срабатываний таймера, который был ранее размещен в полностью определяемой разработчиком структуре расширения объекта устройства.
#define MY_INTERRUPT_TIMEOUT (10)
VOID StartIo( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension; . . . InterlockedExchange(&pDevExt->Remaining, MY_INTERRUPT_TIMEOUT); // Старт устройства: MyTransmitDataRoutine(pDevObj, pIrp); . . . }
Перед физическим стартом устройства необходимо инициализировать счетчик срабатываний таймера. Значение MY_INTERRUPT_TIMEOUT следует выбирать из тех соображений, что устройство может использоваться впервые в данном сеансе либо имело длительный простой перед данным вызовом. При выборе этого значения необходимо учесть все виды внутренних задержек в устройстве (прогрев, самодиагностику и т.п.).
Процедура ISR по прибытии ожидаемого прерывания устанавливает соответствующее значение счетчика секунд ожидания. В случае, если нет работы (все операции ввода/вывода завершены), логично установить значение счетчика -1.
BOOLEAN OnInterrupt ( IN PKINTERRUPT pInterruptObject, IN PVOID pContext ) { PDEVICE_EXTENSION pDeviceExt = (PDEVICE_EXTENSION)pContext; . . . // В случае, если остались еще данные для передачи, то // обновить счетчик if( IHaveTransmitBytes( pDeviceExt ) ) InterlockedExchange( &pDeviceExt->Remaining, MY_INTERRUPT_TIMEOUT ); else // иначе - очистить счетчик InterlockedExchange ( &pDevExt->Remaining, -1 ); . . . }
Наконец, callback-функция MyIoTimerRoutine, которая вызывается всякий раз по срабатыванию таймера (каждую секунду), как только он запущен. В том случае, если данная процедура установила, что время ожидания активности устройства истекло, то она посредством процедуры DpcForIsr завершает работу над текущим IRP пакетом, объявляя его невыполненным.
VOID MyIoTimerRoutine( IN PDEVICE_OBJECT pDeviceObj, IN PVOID pContext ) { PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pContext; // Проверить время ожидания if( (pDevExt->Remaining,-1) < 0 ) return; // значение счетчика не важно (поскольку -1) if( InterlockedDecrement(&pDevExt->Remaining) == 0 ) { // Время ожидания истекло InterlockedExchange( &pDevExt->Remaining, (-1) ); PIRP pCurrentIrp = pDeviceObj->CurrentIrp; pCurrentIrp->IoStatus.Status = STATUS_IO_TIMEOUT; pCurrentIrp->IoStatus.Information = 0; IoRequestDpc( pDeviceObj, pCurrentIrp, NULL ) // Некоторые делают совсем "просто": // MyDpcForIsr(NULL, pDeviceObj, pCurrentIrp, pDevExt); } return; }
Существует маленький временной зазор между тем, как функция MyIoTimerRoutine убедилась, что счетчик активен, и моментом, когда произошло его уменьшение на единицу. Если предположить, что в этот момент "вклинилась" процедура OnInterrupt и установила значение счетчика в -1, то функция MyIoTimerRoutine, получив управление, сделает значение счетчика равным -2. Код, приведенный выше, учитывает эту возможность, сравнивая Remaining c нулем.
Зарегистрированная соответствующим образом процедура DpcForIsr может выглядеть следующим образом:
VOID MyDpcForIsr( IN PKDPC pDpcObj, IN PDEVICE_OBJECT pDeviceObj, IN PIRP pIrp, IN PVOID pContext ) { . . . // Инициируем поступление IRP из внутренней очереди в // процедуру StartIO (): TodoStartNextPacket(&pDevExtension->dqReadWrite, pDevObject);
// Даем возможность отработать процедурам завершения всех // вышестоящих драйверов, если они есть: IoCompleteRequest(pIrp, IO_NO_INCREMENT); . . . }