3.Внутреннее устройство Windows (гл. 8-11)
Шрифт:
Серверное приложение обычно получает клиентские запросы через конечные точки, представляемые как описатели файлов. Пример — сокеты 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 (о которой мы уже рассказывали в главе 7) предоставляет несколько параметров для проверки правильности операций, связанных с вводом-выводом. Ha рис. 9-22 в окне Driver Verifier Manager (Диспетчер проверки драйверов) в Windows Server 2003 эти параметры помечены флажками.
Даже если вы не указываете никаких параметров, Verifier наблюдает за работой выбранных для верификации драйверов, следя за недопустимыми операциями, в том числе за вызовом функций пула памяти ядра при неправильном уровне IRQL, попытками повторного освобождения свободной памяти и запроса блоков памяти нулевого размера.
Рис. 9-22. Параметры Driver Verifier, относящиеся к операциям ввода-вывода
Параметры проверки ввода-вывода перечислены ниже.
• I/O Verification (Проверка ввода-вывода) Если этот параметр выбран, диспетчер ввода-вывода выделяет память под IRP-пакеты для проверяемых драйверов из специального пула и отслеживает его использование. Кроме того, Verifier вызывает крах системы по окончании обработки IRP с неправильным состоянием и при передаче неверного объекта «устройство» диспетчеру ввода-вывода. (B Windows 2000 этот параметр назывался I/O Verification Level 1).