Использование портов завершения Высокоуровневая схема работы порта завершения представлена на рис. 9-20. Порт завершения создается вызовом Windows-функции CreateIoCompletionPort. Потоки, блокированные на порте завершения, считаются сопоставленными с ним и пробуждаются по принципу LIFO («последним пришел – первым вышел»), т. е. следующий пакет достается потоку, заблокированному последним. Стеки потоков, блокируемых в течение длительного времени, могут быть выгружены в страничный файл. B итоге, если с портом сопоставлено больше потоков, чем нужно для обработки текущих заданий, система автоматически минимизирует объем памяти, занимаемой слишком долго блокируемыми потоками.
Серверное приложение обычно получает клиентские запросы через конечные точки, представляемые как описатели файлов. Пример – сокеты Windows Sockets 2 (Winsock2) или именованные каналы. Создавая конечные точки своих коммуникационных связей, сервер сопоставляет их с портом завершения, и серверные потоки ждут входящие запросы, вызывая для этого порта функцию GetQueuedCompletionStatus. Получив пакет из порта завершения, поток начинает обработку запроса и становится активным. B процессе обработки данных поток может часто блокироваться, например из-за необходимости считать данные из файла или записать их в него, а также из-за синхронизации с другими потоками. Windows обнаруживает такие действия и выясняет, что одним активным потоком на порте завершения стало меньше. Поэтому, как только поток блокируется и становится неактивным, операционная система пробуждает другой ждущий на порте завершения поток (если в очереди есть пакет).
Microsoft рекомендует устанавливать максимальное число активных потоков на порте завершения примерно равным числу процессоров в системе. Имейте в виду, что это значение может быть превышено. Допустим, вы задали, что максимальное значение должно быть равно 1. При поступлении клиентского запроса выделенный для его обработки поток становится активным. Поступает второй запрос, но второй поток не может продолжить его обработку, так как лимит уже достигнут. Затем первый поток блокируется в ожидании файлового ввода-вывода и становится неактивным. Тогда освобождается второй поток и, пока он активен, завершается файловый ввод-вывод для первого потока, в результате чего первый поток вновь активизируется. C этого момента и до блокировки одного из потоков число активных потоков превышает установленный лимит на 1.
API, предусмотренный для порта завершения, также позволяет серверному приложению ставить в очередь порта завершения самостоятельно определенные пакеты завершения; для этого предназначена функция PostQueuedCompletionStatus. Сервер обычно использует эту функцию для уведомления своих потоков о внешних событиях, например о необходимости корректного завершения работы.
Как работает порт завершения ввода-вывода Windows-приложения создают порты завершения вызовом Windows-функции CreateIoCompletionPort с указанием NULL вместо описателя порта завершения. Это приводит к выполнению системного сервиса NtCreateIoComple-tion. Объект IoCompletion исполнительной системы, построенный на основе синхронизующего объекта ядра, называется очередью. Таким образом, системный сервис создает объект «порт завершения» и инициализирует объект «очередь» в памяти, выделенной для порта. (Указатель на порт ссылается и на объект «очередь», так как последний находится в начальной области памяти порта.) Максимальное число сопоставленных с портом потоков, которые могут быть активны, указывается в объекте «очередь» при его инициализации; это значение, которое было передано в CreateIoCompletionPort. Для инициализации объекта «очередь» порта завершения NtCreateIoCompletion вызывает функцию KeInitializeQueue.
Когда приложение обращается к CreateIoCompletionPort для связывания описателя файла с портом, вызывается системный сервис NtSetInformationFile, которому передается описатель этого файла. При этом класс информации для NtSetInformationFile устанавливается как FileCompletionInformation, и, кроме того, эта функция принимает описатель порта завершения и параметр CompletionKey, ранее переданный в CreateIoCompletionPort. Функция NtSetInformationFile производит разыменование описателя файла для получения объекта «файл» и создает структуру данных контекста завершения.
Указатель на эту структуру NtSetInformationFile помещает в поле Com-pletionContext объекта «файл». По завершении асинхронной операции ввода- вывода для объекта «файл» диспетчер ввода-вывода проверяет, отличается ли поле CompletionContext от NULL. Если да, он создает пакет завершения и ставит его в очередь порта завершения вызовом KeInsertQueue; при этом в качестве очереди, в которую помещается пакет, указывается порт. (Здесь объект «порт завершения» – синоним объекта «очередь».)
Когда серверный поток вызывает GetQueuedCompletionStatus, выполняется системный сервис NtRemoveIoCompletion. После проверки параметров и преобразования описателя порта завершения в указатель на порт NtRemoveIoCompletion вызывает KeRemoveQueue.
Как видите, KeRemoveQueue и KeInsertQueue - это базовые функции, обеспечивающие работу порта завершения. Они определяют, следует ли активизировать поток, ждущий пакет завершения ввода-вывода. Объект «очередь» поддерживает внутренний счетчик активных потоков и хранит такое значение, как максимальное число активных потоков. Если при вызове потоком KeRemoveQueue текущее число активных потоков равно максимуму или превышает его, данный поток будет включен (в порядке LIFO) в список потоков, ждущих пакет завершения. Список потоков отделен от объекта «очередь». B блоке управления потоком имеется поле для указателя на очередь, сопоставленную с объектом «очередь»; если это поле пустое, поток не связан с очередью.
Windows отслеживает потоки, ставшие неактивными из-за ожидания на каких-либо объектах, отличных от порта завершения, по указателю на очередь, присутствующему в блоке управления потоком. Процедуры планировщика, в результате выполнения которых поток может быть блокирован (KeWaitForSingleObject, KeDelayExecutionThread и т. д.), проверяют этот указатель. Если он не равен NULL, они вызывают функцию KiActivateWaiterQueue, которая уменьшает счетчик числа активных потоков, сопоставленных с очередью. Если конечное число меньше максимального и в очереди есть хотя бы один пакет завершения, первый поток из списка потоков очереди пробуждается и получает самый старый пакет. И напротив, всякий раз, когда после блокировки пробуждается поток, связанный с очередью, планировщик выполняет функцию KiUnwaitTbread, увеличивающую счетчик числа активных потоков очереди.
Наконец, в результате вызова Windows-функции PostQueuedCompletionStatus выполняется системный сервис NtSetIoCompletion, который просто вставляет с помощью KeInsertQueue специальный пакет в очередь порта завершения.
Порт завершения в действии показан на рис. 9-21. Хотя к обработке пакетов завершения готовы два потока, максимум, равный 1, допускает активизацию только одного потока, связанного с портом завершения. Таким образом, на этом порте завершения блокируется два потока.
Driver Verifier Утилита Driver Verifier (о которой мы уже рассказывали в главе 7) предоставляет несколько параметров для проверки правильности операций, связанных с вводом-выводом. Ha рис. 9-22 в окне Driver Verifier Manager (Диспетчер проверки драйверов) в Windows Server 2003 эти параметры помечены флажками.