Мегаобучалка Главная | О нас | Обратная связь


Порт завершения ввода/вывода



2019-07-03 217 Обсуждений (0)
Порт завершения ввода/вывода 0.00 из 5.00 0 оценок




Это, безусловно, один из самых мощных и сложных объектов исполнительной системы. Он специально предназначен для оптимизации обработки клиентских запросов в серверных приложениях. Он не только организует очередь запросов, но и эффективно управляет их обработкой.

Основная идея порта завершения ввода/вывода (в дальнейшем просто порта) состоит в том, чтобы эффективно расходовать процессорное время при обработке клиентских запросов. Обработка должна вестись параллельно несколькими потоками, но строго до определенного момента, когда число потоков будет равняться максимальному значению. После этого новые потоки перестают создаваться, а запросы ставятся в очередь к существующим потокам. Давайте разберемся в этом поподробнее.

Как работает порт

При создании порта указывается максимальное количество активных потоков, способных обрабатывать клиентские запросы параллельно. Так как количество реально работающих параллельно потоков на компьютере равно количеству процессоров, то указание большего максимального количества активных потоков не выгодно. Почему? Дело в том, что для исполнения нескольких потоков на одном процессоре, системе приходится постоянно переключать процессор между потоками, эмулируя, таким образом, параллельность, однако это переключение, называемое переключением контекстов – довольно дорогая операция. Избежать ее можно только одним способом – не создавать параллельно работающие потоки в количестве большем, чем число процессоров. Таким образом, при создании порта, казалось бы, нужно указывать в качестве максимального количества активных потоков число процессоров в системе, но здесь есть одна тонкость. Допустим, у нас однопроцессорный компьютер и, соответственно, клиентские запросы мы обрабатываем в одном потоке. Что будет, если клиентский запрос придет в момент выполнения синхронной операции с диском или в момент ожидания какого-либо объекта этим потоком? Он будет ждать, пока поток не закончит свою работу, но ведь процессор в это время бездействует, потому что поток заблокирован на синхронной операции или на каком-либо объекте. Когда процессор бездействует, а клиентский запрос не обрабатывается – это плохо. Мы приходим к выводу о том, что всегда должен существовать резервный поток, который подхватывал бы запросы в момент, когда «основной» поток выполняет блокирующие операции, и процессор бездействует.

Работа с файлами (в самом широком смысле слова) очень тесно связана с многопоточностью и обработкой запросов на сервере. Сокет или pipe – это тоже файлы. Чтобы обрабатывать запросы через эти каналы параллельно, нужен порт. Давайте рассмотрим функцию создания порта и связи его с файлом (зачем-то разработчики из Microsoft объединили две эти функции в одну; в исполнительной системе эти две функции выполняют сервисы NtCreateIoCompletion и NtSetInformationFile, соответственно).

HANDLE CreateIoCompletionPort ( HANDLE FileHandle,         // хендл файла HANDLE ExistingCompletionPort, // хендл порта завершения ввода/вывода ULONG_PTR CompletionKey,   // ключ завершения DWORD NumberOfConcurrentThreads // максимальное число параллельных потоков );

Для простого создания порта нужно в качестве первого параметра передать INVALID_HANDLE_VALUE, а в качестве второго и третьего – 0. Для связывания файла с портом нужно указать первые три параметра и проигнорировать четвертый.

После того, как файл (под файлом здесь подразумевается объект подсистемы Win32, который реализуется с помощью объекта "файл исполнительной системы", к таковым относятся файлы, сокеты, почтовые ящики, именованные каналы и проч.) связан с портом, окончания всех асинхронных запросов ввода/вывода попадают в очередь порта и могут быть обработаны пулом потоков. Следующие функции могут быть использованы с портом завершения для обработки асинхронных операций ввода/вывода:

ConnectNamedPipe – ожидает подключения клиента к именованному каналу.

DeviceIoControl – низкоуровневый ввод/вывод.

LockFileEx – блокировка региона файла.

ReadDirectoryChangesW – ожидание изменений в директории.

ReadFile – чтение файла.

TransactNamedPipe – Комбинированное чтение и запись по именованному каналу, осуществляемые за одну сетевую операцию.

WaitCommEvent – ожидание события последовательного интерфейса (СОМ-порт).

WriteFile – запись в файл.

Если вы не хотите, чтобы окончание асинхронного ввода/вывода обрабатывалось портом (например, когда вам не важен результат операции), нужно использовать следующий трюк [1]. Нужно установить поле hEvent структуры OVERLAPPED равным описателю события с установленным первым битом. Делается это примерно так:

OVERLAPPED ov = {0}; ov.hEvent = CreateEvent(...); ov.hEvent = (HANDLE)((DWORD_PTR)(ov.hEvent) | 1);

И не забывайте сбрасывать младший бит при закрытии хендла события.

Добавлять поток к пулу (подключать его к обработке запросов) можно с помощью следующей функции:

BOOL GetQueuedCompletionStatus( // хендл порта завершения ввода/вывода HANDLE CompletionPort, // количество переданных байт LPDWORD lpNumberOfBytes, // ключ завершения PULONG_PTR lpCompletionKey, // структура OVERLAPPED LPOVERLAPPED *lpOverlapped, // значение таймаута DWORD dwMilliseconds );

Эта функция блокирует поток до тех пор, пока порт не передаст потоку пакет запроса или не истечет таймаут.

Поместить пакет запроса в порт можно с помощью функции PostQueuedCompletionStatus.

BOOL PostQueuedCompletionStatus( HANDLE CompletionPort,       // хендл порта завершения ввода/вывода DWORD dwNumberOfBytesTransferred, // количество переданных байт ULONG_PTR dwCompletionKey,   // ключ завершения LPOVERLAPPED lpOverlapped    // структура OVERLAPPED );

Пакет запроса не обязательно должен быть структурой OVERLAPPED или производной от нее [2].

Давайте соберем всю информацию воедино. Порт завершения – объект, организующий несколько очередей из клиентских запросов и потоков, их обрабатывающих. Поток добавляется в очередь ожидающих запрос потоков порта при вызове функции GetQueuedCompletionStatus. При поступлении запроса порт разблокирует первый поток в очереди ждущих потоков и передает ему этот запрос (в виде структуры OVERLAPPED и ключа завершения). Поток при этом перемещается в очередь активных потоков (число активных потоков увеличивается на 1). Предположим, у нас максимальное число активных потоков равно 1, тогда при поступлении следующего запроса другой поток из очереди ожидающих активирован не будет. После обработки клиентского запроса поток вновь вызывает GetQueuedCompletionStatus и ставится в начало списка ожидающих потоков. Почему поток ставится именно в начало списка? Дело в том, что потоки берутся из начала списка, и при низкой активности могут использоваться не все потоки. При этом стеки и контексты не используемых потоков могут быть выгружены на диск за ненадобностью.

Если в процессе обработки запроса поток обратился к блокирующей функции, число активных потоков уменьшается на 1, как если бы поток перешел снова в очередь ожидающих потоков. Это дает возможность при приходе следующего клиентского запроса задействовать следующий поток из очереди ожидающих. Когда первый поток закончит блокирующую операцию, число активных потоков превысит максимальное, и при следующем вызове функции GetQueuedCompletionStatus один из этих потоков заблокируется, а второй получит пакет запроса (если он имеется).

Очередь Запись добавляется при: Запись удаляется при:
Список устройств, ассоциированных с портом вызове CreateIoCompletionPort закрытии хенда файла
Очередь клиентских запросов (FIFO) завершении асинхронной операции файла, ассоциированного с портом, или вызове функции PostQueuedCompletionStatus передаче портом запроса потоку на обработку
Очередь ожидающих потоков вызове функции GetQueuedCompletionStatus начале обработки клиентского запроса потоком
Список работающих потоков начале обработки клиентского запроса потоком вызове потоком GetQueuedCompletionStatus или какую-либо блокирующей функции
Список приостановленных потоков вызове потоком какой-либо блокирующей функции выходе потока из какой-либо блокирующей функции

Таблица 1. Список очередей порта завершения ввода/вывода [1].

Недокументированные возможности порта и его низкоуровневое устройство

Как всегда это бывает у Microsoft, порт завершения обладает многими недокументированными возможностями:

У порта завершения ввода/вывода может быть имя, и соответственно, он доступен для других процессов. Совершенно непонятно, почему разработчики решили скрыть эту, на мой взгляд, нужную особенность порта. Имя можно задать в параметре ObjectAttributes функции NtCreateIoCompletion.

Вторая особенность вытекает из первой: с портом может быть связан дескриптор безопасности, который также задается в параметре ObjectAttributes функции NtCreateIoCompletion.

Открывается порт с помощью функции NtOpenIoCompletion. При вызове функции нужно указать имя порта и уровень доступа. В качестве уровня доступа можно указывать все стандартные и следующие специальные права [2] (таблица 2).

Символическое обозначение Константа Описание
IO_COMPLETION_QUERY_STATE 1 Необходим для запроса состояния объекта "порт"
IO_COMPLETION_MODIFY_STATE 2 Необходим для изменения состояния объекта "порт"

Таблица 2.

У порта можно запрашивать количество необработанных запросов с помощью функции NtQueryIoCompletion. Хотя в [3] утверждается, что эта функция определяет, находится ли порт в сигнальном состоянии, на самом деле она возвращает количество клиентских запросов в очереди. Это довольно важная информация, которую почему-то опять решили от нас скрыть.

Давайте более детально рассмотрим, как создается и функционирует порт завершения ввода/вывода [4].

При создании порта функцией CreateIoCompletionPort вызывается внутренний сервис NtCreateIoCompletion. Объект "порт" представлен следующей структурой [5]:

typedef stuct _IO_COMPLETION { KQUEUE Queue; } IO_COMPLETION;

То есть, по существу, объект "порт завершения" является объектом "очередь исполнительной системы" (KQUEUE). Вот как представлена очередь:

typedef stuct _KQUEUE { DISPATCHER_HEADER Header; LIST_ENTRY EnrtyListHead; //очередь пакетов DWORD CurrentCount; DWORD MaximumCount; LIST_ENTRY ThreadListHead; //очередь ожидающих потоков } KQUEUE;

Итак, для порта выделяется память, и затем происходит его инициализация с помощью функции KeInitializeQueue. (все, что касается такого супернизкого устройства порта, взято из [4], остальное – из DDK и [3]).

Когда происходит связывание порта с объектом "файл", Win32-функция CreateIoCompletionPort вызывает NtSetInformationFile. Класс информации для этой функции устанавливается как FileCompletionInformation, а в качестве параметра FileInformation передается указатель на структуру IO_COMPLETION_CONTEXT [5] или FILE_COMPLETION_INFORMATION [3].

typedef struct _IO_COMPLETION_CONTEXT { PVOID Port; PVOID Key; } IO_COMPLETION_CONTEXT;   typedef struct _FILE_COMPLETION_INFORMATION { HANDLE IoCompletionHandle; ULONG CompletionKey; } FILE_COMPLETION_INFORMATION, *PFILE_COMPLETION_INFORMATION;

Указатель на эту структуру заносится в поле CompletionConext структуры FILE_OBJECT (смещение 0x6C).

После завершения асинхронной операции ввода/вывода для ассоциированного файла диспетчер ввода/вывода проверяет поле CompletionConext и, если оно не равно 0, создает пакет запроса (из структуры OVERLAPPED и ключа завершения) и помещает его в очередь с помощью вызова KeInsertQueue. Когда поток вызывает функцию GetQueuedCompletionStatus, на самом деле вызывается функция NtRemoveIoCompletion. NtRemoveIoCompletion проверяет параметры и вызывает функцию KeRemoveQueue, которая блокирует поток, если в очереди отсутствуют запросы, или поле CurrentCount структуры KQUEUE больше или равно MaximumCount. Если запросы есть, и число активных потоков меньше максимального, KeRemoveQueue удаляет вызвавший ее поток из очереди ожидающих потоков и увеличивает число активных потоков на 1. При занесении потока в очередь ожидающих потоков поле Queue структуры KTHREAD (смещение 0xE0) устанавливается равным адресу очереди (порта завершения). Зачем это нужно? Когда вызываются функции блокировки потока (WaitForSingleObject и др.), планировщик проверяет это поле, и если оно не равно 0, вызывает функцию KeActivateWaiterQueue, которая уменьшает число активных потоков порта на 1. Когда поток пробуждается после вызова блокирующих функций, планировщик выполняет те же действия, только вызывает при этом функцию KeUnwaitThread, которая увеличивает счетчик активных потоков на 1.

Когда вы помещаете запрос в порт завершения функцией PostQueuedCompletionStatus, на самом деле вызывается функция NtSetIoCompletion, которая после проверки параметров и преобразования хендла порта в указатель, вызывает KeInsertQueue.

Организуем пул

Итак, мы знаем, как работает порт завершения ввода/вывода, когда потоки добавляются в пул и когда удаляются. Но сколько потоков должно быть в пуле? В два раза больше, чем число процессоров. Это очень общая рекомендация, и для некоторых задач она не подходит. По большому счету имеется только два критерия, по которым можно определять, нужно создавать новый поток или нет. Эти критерии – загруженность процессора и число пакетов запросов. Если число пакетов превышает определенное количество, и загруженность процессора невысока, есть смысл создать новый поток. Если пакетов мало, или процессор занят более чем на 90 процентов, дополнительный поток создавать не следует. Удалять поток из пула нужно, если он давно не обрабатывал клиентские запросы (просто подсчитать, сколько раз GetQueuedCompletionStatus вернула управление по таймауту). При удалении потока нужно следить, чтобы закончились все асинхронные операции ввода/вывода, начатые этим потоком.

Надо сказать, что определение загруженности процессора, количества пакетов в очереди порта и наличия у потока незавершенных операций ввода/вывода – задачи не самые простые. Например, вы можете использовать WMI для определения загруженности процессора, но при этом не сможете определить, есть ли у потока незавершенные операции ввода/вывода. Ниже я приведу функции получения вышеперечисленных показателей только недокументированными способами (здесь используется заголовочный файл ntdll.h из [3]):

// Функция получения загруженности процессора double GetCPUUsage() { #define Li2Double(x) ((double)((x).HighPart) * 4.294967296E9 \ + (double)((x).LowPart))   typedef NTSTATUS (NTAPI ZwQuerySystemInformation_t)( IN NT::SYSTEM_INFORMATION_CLASS SystemInformationClass, OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength OPTIONAL );   static ZwQuerySystemInformation_t* ZwQuerySystemInformation = 0; if(!ZwQuerySystemInformation) { ZwQuerySystemInformation = (ZwQuerySystemInformation_t*)GetProcAddress(   GetModuleHandle(_T("ntdll.dll")), _T("NtQuerySystemInformation")); }   double dbIdleTime = 0;   static NT::LARGE_INTEGER liOldIdleTime = {0, 0}; static NT::LARGE_INTEGER liOldSystemTime = {0, 0};   // Получаем число процессоров NT::SYSTEM_BASIC_INFORMATION sysinfo = {0}; NT::NTSTATUS status = ZwQuerySystemInformation(NT::SystemBasicInformation, &sysinfo, sizeof sysinfo, 0);     if(status != NO_ERROR)    return -1;     // Получаем системное время NT::SYSTEM_TIME_OF_DAY_INFORMATION timeinfo = {0}; status = ZwQuerySystemInformation(NT::SystemTimeOfDayInformation, &timeinfo, sizeof timeinfo, 0);     if(status!=NO_ERROR)    return -1;   // Получаем время простоя NT::SYSTEM_PERFORMANCE_INFORMATION perfinfo = {0}; status = ZwQuerySystemInformation(NT::SystemPerformanceInformation, &perfinfo, sizeof perfinfo, 0);     if(status != NO_ERROR)    return -1;   // если это первый вызов, значение вычислить нельзя if(liOldIdleTime.QuadPart != 0) { // Время простоя dbIdleTime = Li2Double(perfinfo.IdleTime) - Li2Double(liOldIdleTime);   // Системное время const double dbSystemTime = Li2Double(timeinfo.CurrentTime) - Li2Double(liOldSystemTime);   dbIdleTime = dbIdleTime / dbSystemTime;   dbIdleTime = 100.0 - dbIdleTime * 100.0 / (double)sysinfo.NumberProcessors + 0.5; }   // сохраняем полученные значения liOldIdleTime = perfinfo.IdleTime; liOldSystemTime = timeinfo.CurrentTime;   // Если это первый вызов, получаем загруженность CPU за последние // 200 милисекунд if(dbIdleTime == 0) { Sleep(200); dbIdleTime = GetCPUUsage(); }   return dbIdleTime; }   // Возвращает true, если поток имеет незавершенные операции ввода/вывода bool HasThreadIoPending(HANDLE hThread = GetCurrentThread()) { typedef NTSTATUS (NTAPI ZwQueryInformationThread_t)( IN HANDLE ThreadHandle, IN NT::THREADINFOCLASS ThreadInformationClass, OUT PVOID ThreadInformation, IN ULONG ThreadInformationLength, OUT PULONG ReturnLength OPTIONAL );   static ZwQueryInformationThread_t* ZwQueryInformationThread = 0; if(!ZwQueryInformationThread) { ZwQueryInformationThread = (ZwQueryInformationThread_t*)GetProcAddress( GetModuleHandle(_T("ntdll.dll")), _T("NtQueryInformationThread")); }   ULONG io = 0;   ZwQueryInformationThread(hThread, NT::ThreadIsIoPending, &io, 4, 0);   return io > 0; }   // Возвращает количество необработанных запросов в очереди порта DWORD GetIoCompletionLen(HANDLE hIoPort) { typedef NTSTATUS (NTAPI ZwQueryIoCompletion_t)( IN HANDLE IoCompletionHandle, IN NT::IO_COMPLETION_INFORMATION_CLASS IoCompletionInformationClass, OUT PVOID IoCompletionInformation, IN ULONG IoCompletionInformationLength, OUT PULONG ResultLength OPTIONAL );   static ZwQueryIoCompletion_t* ZwQueryIoCompletion = 0; if(!ZwQueryIoCompletion) { ZwQueryIoCompletion = (ZwQueryIoCompletion_t*)GetProcAddress( GetModuleHandle(_T("ntdll.dll")), _T("NtQueryIoCompletion")); }   NT::IO_COMPLETION_BASIC_INFORMATION ioinfo = {0}; DWORD dwRetLen = 0; ZwQueryIoCompletion(hIoPort, NT::IoCompletionBasicInformation, &ioinfo, sizeof ioinfo, &dwRetLen);   return ioinfo.SignalState; }

Как видите, не простое это дело – создавать эффективный пул потоков, однако кое-что ребята из Microsoft могут нам предложить. В Windows 2000 появились новые функции, которые полностью берут на себя всю черновую работу по созданию и удалению потоков в пуле. О них – следующий раздел.

Встроенная поддержка пула потоков

В Windows 2000 появились новые функции, которые условно можно разделить на четыре группы:

помещение запроса в очередь;

вызов функции при окончании асинхронной операции ввода/вывода;

периодический вызов функции;

вызов функции при переходе объекта в сигнальное состояние.

Рассмотрим их по порядку.

Помещение запроса в очередь

Передать на выполнение потоку из пула какую-либо функцию можно с помощью сервиса QueueUserWorkItem. Эта с виду простая функция делает очень много: она создает порт завершения ввода/вывода, создает и уничтожает потоки в пуле и многое другое. Вот ее описание:

BOOL QueueUserWorkItem( LPTHREAD_START_ROUTINE Function, // адрес функции PVOID Context,               // произвольный параметр ULONG Flags                  // флаги выполнения );

QueueUserWorkItem помещает пакет запроса в виде адреса функции и произвольного параметра в очередь запросов порта завершения и сразу же возвращает управление. Вот как выглядит функция, которая будет вызвана одним из потоков в пуле:

DWORD WINAPI ThreadProc( LPVOID lpParameter // произвольный параметр );

Ее прототип ничем не отличается от стартовой процедуры потока, так что здесь вам все должно быть ясно. Гораздо интереснее знать, что скрывается внутри функции QueueUserWorkItem. Давайте разбираться.

При первом помещении запроса количество потоков в пуле равно нулю, так что QueueUserWorkItem приходится создавать поток и порт завершения. Затем в порт помещается пакет запроса, а поток вызывает функцию GetQueuedCompletionStatus. После обработки запроса поток не разрушается, а остается еще некоторое время в пуле, так что следующий запрос обработается намного быстрее. Если вы отправляете запросы слишком часто, и количество необработанных пакетов увеличивается, QueueUserWorkItem создаст для вызова функции новый поток. Максимальное количество потоков в пуле равно количеству процессоров, что не очень хорошо, но есть способ заставить функцию всегда создавать новый поток.

ПРИМЕЧАНИЕ Те из вас, кто читал статью Дж. Рихтера «New Windows 2000 Pooling Functions Greatly Simplify Thread Management» из апрельского MSJ за 1999 год, могут поспорить со мной насчет размера пула. В статье указывается, что количество потоков в нем равно удвоенному количеству процессоров в системе, однако это не так. Вы можете собственноручно в этом убедиться, поставив breakpoint на функцию _RtlpInitializeWorkerThreadPool (адрес 0x77FA95CD на Windows 2000 Professional SP3) и вызвав функцию QueueUserWorkItem.

Рассмотрим флаги функции QueueUserWorkItem.

Константа Значение Описание
WT_EXECUTEDEFAULT 0 Запрос помещается в простой рабочий поток
WT_EXECUTEINIOTHREAD 1 Запрос помещается в поток ввода/вывода
WT_EXECUTEINPERSISTENTTHREAD 0x80 Запрос помещается в поток, который не завершается после обработки запроса, поэтому он может сохранять свое состояние, например в TLS.
WT_EXECUTELONGFUNCTION 0x10 Запрос с данным флагом всегда помещается в новый поток

Таблица 3. Флаги функции QueueUserWorkItem.

Если вы не выполняете асинхронных запросов ввода/вывода в функции ThreadProc, не используете TLS (Thread Local Storage) или функций, которые его используют, а продолжительность выполнения операции невелика – указывайте флаг WT_EXECUTEDEFAULT.

Предположим, вы начали асинхронную операцию ввода/вывода в своей функции ThreadProc. Для того чтобы она завершилась, поток в котором она началась, не должен быть разрушен. Однако флаг WT_EXECUTEDEFAULT этого не гарантирует. С этим флагом поток может быть удален, даже если у него имеются незавершенные асинхронные операции. Для того чтобы поток завершался только после окончания всех начатых асинхронных операций, нужно указать флаг WT_EXECUTEINIOTHREAD.

При указании флага WT_EXECUTEINPERSISTENTTHREAD пакет запроса помещается в поток, который никогда не удаляется, так что вы спокойно можете использовать TLS. Так как поток всего один, не рекомендуется выполнять в нем продолжительных операций.

И наоборот, если вам нужно каждый раз выполнять длительную операцию, укажите флаг WT_EXECUTELONGFUNCTION. Для каждой такой операции создается новый поток, который после ее обработки удаляется.

Характеристика Значение
Начальное коли-чество потоков в пуле 0
Когда поток удаляется Поток не имеет незавершенных операций ввода/вывода и простаивает некоторое время
Способ ожидания, используемый потоком Тревожное (alertable) ожидание
Поток просыпается при Приходе APC-запроса

Таблица 4. Описание работы функции QueueUserWorkItem

Вызов функции при окончании асинхронной операции ввода/вывода

Если в программе выполняется большое количество операций ввода/вывода, лучше, чтобы они выполнялись асинхронно. Это намного повышает производительность приложения и уменьшает время отклика на клиентские запросы. Однако самое сложное в асинхронных операциях – правильно продолжить или завершить их. Можно связать все устройства (файлы) с портом завершения и обрабатывать окончания операций в отдельном потоке. Когда операций много, вновь встает вопрос об организации пула потоков для обработки асинхронных операций ввода/вывода. К счастью, у нас имеется хороший помощник:

BOOL BindIoCompletionCallback( // хендл файла HANDLE FileHandle,                       // функция обработки завершения запроса LPOVERLAPPED_COMPLETION_ROUTINE Function, // зарезервировано ULONG Flags                              );

Эта функция создает порт завершения и связывает его с файлом. Затем функция создает поток, который сразу же начинает ждать (GetQueuedCompletionStatus) окончания асинхронной операции, после чего вызывает определяемую вами функцию для обработки запроса. Вот ее прототип:

VOID CALLBACK FileIOCompletionRoutine( DWORD dwErrorCode,           // код завершения DWORD dwNumberOfBytesTransfered, // количество переданных байтов LPOVERLAPPED lpOverlapped    // структура OVERLAPPED );

Хотя прототип этой функции идентичен функции, вызываемой при окончании операций, начатых ReadFileEx и WriteFileEx, не стоит их путать. При использовании BindIoCompletionCallback эта функция вызывается с помощью порта завершения ввода/вывода, тогда как при использовании ReadFileEx и WriteFileEx функция вызывается с помощью APC.

Совершенно непонятно, почему в Microsoft решили не использовать флаги для этой функции, но факт остается фактом. И хотя Рихтер в своей статье, которая упоминалась выше, утверждает, что можно указать флаг WT_EXECUTEINIOTHREAD, это неправда. Вы можете сами посмотреть дизассемблером в ntdll.dll, например, функцию RtlSetIoCompletionCallback и убедиться, что третий параметр в ней просто не используется.

Как видно из прототипа, вы не можете передавать какого-либо дополнительного параметра функции FileIOCompletionRoutine, что может вызвать определенные проблемы. Самым распространенным решением является передача в функцию начала асинхронной операции структуры, производной от OVERLAPPED. Тогда в дополнительных членах структуры можно передавать какую угодно информацию – сама структура OVERLAPPED не копируется, везде передается только указатель на нее.

Характеристика Значение
Начальное коли-чество потоков в пуле 0
Когда поток удаляется Поток простаи-вает некоторое время
Способ ожидания, используемый потоком GetQueuedCompletionStatus
Поток просыпается при Постановке пакета запроса в очередь порта

Таблица 5. Описание характеристик работы функции BindIoCompletionCallback

Периодический вызов функции

В самом начале статьи я обещал рассказать о новых «таймерных» функциях. До выхода Windows 2000 имелось три механизма периодического вызова пользовательских функций: «оконный» таймер, Multimedia-таймер и ожидающий таймер. У каждого из них были серьезные недостатки, к тому же они не поддерживали обработку запросов в пуле. Новые функции по созданию очереди таймеров более универсальны.

В Windows 2000 появился новый объект – очередь таймеров. Он основан на объекте исполнительной системы "ожидающий таймер", так что в качестве механизма обратного вызова используется APC. Создать объект можно с помощью следующей функции:

HANDLE CreateTimerQueue(VOID);

Она возвращает хендл объекта "очередь таймеров" (queues of timers). После создания очереди в нее можно добавлять новые таймеры. Для этого используется функция CreateTimerQueueTimer:

BOOL CreateTimerQueueTimer( // указатель на хендл таймера PHANDLE phNewTimer,         // хендл очереди таймеров HANDLE TimerQueue,          // функция обратного вызова WAITORTIMERCALLBACK Callback, // параметр для функции обратного вызова PVOID Parameter,            // время задержки перед первым вызовом в милисекундах DWORD DueTime,              // период в милисекундах DWORD Period,               // флаги ULONG Flags                 );

Рассмотрим параметры этой функции. Первый параметр возвращает хендл таймера, который можно использовать для вызова функций изменения таймера или его удаления (о них позже). Второй параметр – хендл очереди, созданной функцией CreateTimerQueue. В качестве него можно указать нулевое значение. В этом случае по умолчанию таймер будет добавлен к объекту "очередь таймеров". Третий параметр – адрес функции, которая будет вызвана при переходе таймера в сигнальное состояние. Вот ее прототип:

VOID CALLBACK WaitOrTimerCallback( PVOID lpParameter,   // произвольный параметр BOOLEAN TimerOrWaitFired // причина вызова );

Произвольный параметр для нее указывается в функции CreateTimerQueueTimer четвертым параметром. Параметр TimerOrWaitFired для таймеров всегда равен TRUE.

Пятый параметр определяет, сколько времени в миллисекундах пройдет до первого вызова функции WaitOrTimerCallback. Если указать 0, то эта функция будет вызвана практически сразу (примерно так же быстро, как и в случае QueueUserWorkItem).

Шестой параметр задает период вызова пользовательской функции. В качестве этого параметра можно указать нулевое значение, тогда функция WaitOrTimerCallback будет вызвана только один раз.

В качестве флагов функции CreateTimerQueueTimer можно указывать все флаги из таблицы 2 и два новых:

Константа Значение Описание
WT_EXECUTEINTIMERTHREAD 0x20 Пользовательская функция вызывается в потоке таймера
WT_EXECUTEONLYONCE 8 Пользовательская функция вызывается только один раз

Таблица 6. Флаги функции CreateTimerQueueTimer.

Если ваша функция WaitOrTimerCallback очень быстро отрабатывает, а количество запросов невелико – лучше всего указать флаг WT_EXECUTEINTIMERTHREAD. В этой ситуации функция будет вызвана в потоке, ожидающем таймера. Будьте осторожны – длительная блокировка пользовательской функции приведет к тому, что ожидающий поток не сможет обрабатывать приходящие запросы.

При указании флага WT_EXECUTEONLYONCE таймер будет установлен в сигнальное состояние только один раз.

Если вам больше не нужен таймер, его можно удалить из очереди с помощью функции DeleteTimerQueueTimer.

BOOL DeleteTimerQueueTimer( // хендл очереди таймеров HANDLE TimerQueue,    // хендл таймера HANDLE Timer, // хендл объекта, устанавливаемого в сигнальное состояние после удаления HANDLE CompletionEvent );

Если используется очередь по умолчанию, в качестве первого параметра нужно передать NULL. Второй параметр – хендл удаляемого таймера. Третий параметр может принимать следующие значения:

INVALID_HANDLE_VALUE – означает, что вызывающая функция будет заблокирована до тех пор, пока таймер не обработает все текущие запросы. Вы должны быть осторожны с этим значением, так как вызов функции удаления таймера в самой пользовательской функции приведет к взаимоблокировке (deadlock).

NULL – если вы не хотите ожидать завершения обработки всех текущих запросов. Функция DeleteTimerQueueTimer возвратит управление немедленно.

Допустимый хендл объекта – если необходимо синхронизировать окончание обработки текущих запросов. Функция DeleteTimerQueueTimer возвратит управление немедленно, но после окончания обработки запросов объект завершения устанавливается в сигнальное состояние.

Можно сразу удалить всю очередь таймеров с помощью следующей функции:

BOOL DeleteTimerQueueEx( // хендл очереди таймеров HANDLE TimerQueue,    // хендл объекта, устанавливаемого в сигнальное состояние после удаления HANDLE CompletionEvent );

Если удаляется очередь по умолчанию, в качестве первого параметра нужно передать NULL. Второй параметр имеет то же значение, что и в предыдущей функции DeleteTimerQueueTimer.

Кроме создания и удаления таймера в очереди, можно изменять некоторые его характеристики. Это делается вызовом функции ChangeTimerQueueTimer.

BOOL ChangeTimerQueueTimer( HANDLE TimerQueue, // хендл очереди таймеров HANDLE Timer,  // хендл таймера ULONG DueTime, // новое значение задержки перед вызовом ULONG Period   // новое значение периода вызова );

Функция вопросов не вызывает, однако нужно отметить, что она не оказывает влияния на «одноразовые» (one-shot) таймеры, при создании которых в качестве периода был указан 0.

Характеристика Значение
Начальное коли-чество потоков в пуле 1
Когда поток удаляется Когда удаляется последний таймер из очереди
Способ ожидания, используемый потоком Тревожное (alertable) ожидание
Поток просыпается при Приходе APC-запроса

Таблица 7. Описание характеристик работы объекта "очередь таймеров".

Вызов функции при переходе объекта в сигнальное состояние

В приложении часто возникает необходимость дождаться какого-либо объекта и выполнить определенное действие. Для многопоточных приложений приходится для каждого такого случая заводить отдельный поток, что нехорошо. В этом случае на помощь приходит функция RegisterWaitForSingleObject. Она позволяет вызывать произвольную пользовательскую функцию после того, как заданный объект перейдет в сигнальное состояние. Причем никаких потоков создавать не нужно, все делается автоматически. Рассмотрим прототип этой функции:

BOOL RegisterWaitForSingleObject( PHANDLE phNewWaitObject,  // адрес хендла объекта ожидания HANDLE hObject,           // хендл объекта WAITORTIMERCALLBACK Callback, // функция обратного вызова PVOID Context             // произвольный параметр ULONG dwMilliseconds,     // таймаут ULONG dwFlags             // флаги );

Первый параметр – это указатель на переменную, в которую будет возвращен хендл объекта ожидания. Нужно отметить, что на самом деле это не хендл объекта и его нельзя использовать, например, с функцией CloseHandle. Этот хендл можно использовать только для передачи функции UnregisterWait или UnregisterWaitEx (о них поговорим попозже). В качестве второго параметра нужно передать хендл объекта, перехода которого в сигнальное состояние ожидает эта функция. Третий параметр – адрес функции WaitOrTimerCallback, которую мы описывали раньше. Четвертый параметр – это любое значение, которое просто передается функции WaitOrTimerCallback. В качестве пятого параметра можно указать количество миллисекунд, которое определяет максимальное время ожидания объекта. После его истечения функция WaitOrTimerCallback будет вызвана со вторым параметром, равным TRUE. Если объект перешел в сигнальное состояние до истечения кванта времени, второй параметр функции WaitOrTimerCallback будет равным FASLE.

В качестве флагов можно указывать все описанные ранее значения и одно новое – WT_EXECUTEINWAITTHREAD. Его можно использовать, только если вы выполняете очень короткие операции, функция WaitOrTimerCallback будет вызвана в самом ожидающем потоке. Любая задержка в пользовательской функции приведет к тому, что поток не сможет обработать переход в сигнальное состояние объекта вовремя. Замечу, что при ожидании сигнального состояния события с ручным сбросом (manual reset event) не следует вызывать функцию PulseEvent, если не указаны флаги WT_EXECUTEINWAITTHREAD или WT_EXECUTEONLYONCE, так как в этом случае ожидающий поток не сможет обработать событие перехода объекта в сигнальное состояние.

Для остановки вызова пользовательской функции можно воспользоваться следующими функциями:

BOOL UnregisterWait(

// хендл ожидания

HANDLE WaitHandle   

);

 

BOOL UnregisterWaitEx(

// хендл ожидания

HANDLE WaitHandle,   

// хендл объекта, устанавливаемого в сигнальное состояние после удаления

HANDLE CompletionEvent

);

Характеристика

Значение

Начальное коли-чество потоков в пуле

1

Когда поток удаляется

Когда количество объектов равно нулю

Способ ожидания, используемый потоком

WaitForMultipleObjectsEx

Поток просыпается при

Переходе объекта ядра в сигнальное состояние
       

Таблица 8. Описание работы функции RegisterWaitForSingleObject

Заключение

Написание многопоточных серверных приложений – одна из самых сложных областей программирования. Как правило, для этого необходимо детально знать механизмы ОС, подсистему безопасности и функционирование служб (в большинстве случаев серверные приложения являются службами). В дальнейшем я, возможно, постараюсь осветить другие аспекты данной области. Надеюсь, этот материал был вам интересен.

Список литературы

Программирование серверных приложений для Windows 2000, Дж. Рихтер, Дж. Кларк.

Недокументированные возможности Windows NT, Коберниченко А.В.

NativeAPI, Гарри Неббет

Внутреннее устройство Windows 2000, Д. Соломон, М. Руссинович.

Недокументированные возможности Windows 2000. С. Шрайбер.

 



2019-07-03 217 Обсуждений (0)
Порт завершения ввода/вывода 0.00 из 5.00 0 оценок









Обсуждение в статье: Порт завершения ввода/вывода

Обсуждений еще не было, будьте первым... ↓↓↓

Отправить сообщение

Популярное:
Почему человек чувствует себя несчастным?: Для начала определим, что такое несчастье. Несчастьем мы будем считать психологическое состояние...
Как распознать напряжение: Говоря о мышечном напряжении, мы в первую очередь имеем в виду мускулы, прикрепленные к костям ...



©2015-2024 megaobuchalka.ru Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. (217)

Почему 1285321 студент выбрали МегаОбучалку...

Система поиска информации

Мобильная версия сайта

Удобная навигация

Нет шокирующей рекламы



(0.014 сек.)