Марк Руссинович - 1.Внутреннее устройство Windows (гл. 1-4)
Выбирая механизм синхронизации, вы должны учитывать в своей программе поведение синхронизирующих объектов. B таблице 3-10 показано, когда переходят в свободное состояние синхронизирующие объекты различных типов.
Когда объект переводится в свободное состояние, ожидающие его потоки обычно немедленно выходят из ждущего состояния. Однако, как показано на рис. 3-26, некоторые объекты диспетчера ядра и системные события ведут себя иначе.
Например, объект «событие уведомления» — в Windows API он называется событием со сбросом вручную (manual reset event) — используется для уведомления о каком-либо событии. Когда этот объект переводится в свободное состояние, все потоки, ожидающие его, освобождаются. Исключением является тот поток, который ждет сразу несколько объектов: он может продолжать ожидание, пока не освободятся дополнительные объекты.
B отличие от события мьютекс предусматривает возможность владения. Этот объект используется для взаимоисключающего доступа к ресурсу, поэтому единовременно только один поток может владеть мьютексом. При освобождении мьютекса ядро переводит его в свободное состояние и выбирает для выполнения один из ожидающих потоков. Выбранный ядром поток захватывает мьютекс, а остальные потоки остаются в ожидании.
События с ключом и критические секцииСинхронизирующий объект, впервые появившийся в Windows XP и названный событием с ключом (keyed event), заслуживает особого упоминания. Он помогает процессам справляться с нехваткой памяти при использовании критических секций. Это недокументированное событие позволяет потоку указать «ключ» в следующей ситуации-, данный поток должен пробуждаться, когда другой поток того же процесса освобождает событие с тем же ключом.
Windows-процессы часто используют функции критических секций — EnterCriticalSection и LeaveCriticalSection — для синхронизации доступа потоков к личным ресурсам процесса. Вызовы этих функций эффективнее прямого обращения к объектам «мьютекс», так как в отсутствие конкуренции они не заставляют переходить в режим ядра. При наличии конкуренции EnterCriticalSection динамически создает объект «событие», и поток, которому нужно захватить критическую секцию, ждет, когда поток, владеющий этой секцией, освободит ее вызовом LeaveCriticalSection.
Если создать объект «событие» для критической секции не удалось из-за нехватки системной памяти, EnterCriticalSection использует глобальное событие с ключом — CritSecOutOjMemoryEvent (в каталоге Ker-nel пространства имен диспетчера объектов). Если EnterCritica amp;ection вынуждена задействовать CritSecOutOjMemoryEvent вместо стандартного события, поток, ждущий критическую секцию, использует адрес этой секции как ключ. Это обеспечивает корректную работу функций критических секций даже в условиях временной нехватки памяти.
Мы не ставили себе задачу исчерпывающе описать все объекты исполнительной системы, а лишь хотели дать представление об их базовой функциональности и механизмах синхронизации. Об использовании этих объектов в Windows-программах см. справочную документацию Windows или четвертое издание книги Джеффри Рихтера «Windows для профессионалов».
Структуры данныхУчет ожидающих потоков и их объектов ожидания базируется на двух ключевых структурах данных: заголовках диспетчера (dispatcher headers) и блоках ожидания (wait blocks). Обе эти структуры определены в Ntddk.h, заголовочном файле DDK. Для удобства мы воспроизводим здесь эти определения.
Заголовок диспетчера содержит тип объекта, информацию о состоянии (занят/свободен) и список потоков, ожидающих этот объект. У каждого ждущего потока есть список блоков ожидания, где перечислены ожидаемые потоком объекты, а у каждого объекта диспетчера ядра — список блоков ожидания, где перечислены ожидающие его потоки. Этот список ведется так, что при освобождении объекта диспетчера ядро может быстро определить, кто ожидает данный объект. B блоке ожидания имеются указатели на объект ожидания, ожидающий поток и на следующий блок ожидания (если поток ждет более одного объекта). Он также регистрирует тип ожидания («любой» или «все») и позицию соответствующего элемента в таблице описателей, переданную потоком в функцию WaitForMultipleObjects (позиция 0 — если поток ожидает лишь один объект).
Ha рис. 3-27 показана связь объектов диспетчера ядра с блоками ожидания потоков. B данном примере поток 1 ждет объект В, а поток 2 — объекты A и В. Если объект A освободится, поток 2 не сможет возобновить свое выполнение, так как ядро обнаружит, что он ждет и другой объект. C другой стороны, при освобождении объекта B ядро сразу же подготовит поток 1 к выполнению, поскольку он не ждет никакие другие объекты.
ЭКСПЕРИМЕНТ: просмотр очередей ожидания
Хотя многие утилиты просмотра процессов умеют определять, находится ли поток в состоянии ожидания (отмечая в этом случае и тип ожидания), список объектов, ожидаемых потоком, можно увидеть только с помощью команды !process отладчика ядра. Например, следующий фрагмент вывода команды !process показывает, что поток ждет на объекте-событии.
Эти данные позволяют нам убедиться в отсутствии других потоков, ожидающих данный объект, поскольку указатели начала и конца списка ожидания указывают на одно и то же место (на один блок ожидания). Копия блока ожидания (по адресу 0x8a12a398) дает следующее:
Если в списке ожидания более одного элемента, вы можете выполнить ту же команду со вторым указателем в поле WaitListEntry каждого блока ожидания (команду !thread применительно к указателю потока в блоке ожидания) для прохода по списку и просмотра других потоков, ждущих данный объект.
Быстрые и защищенные мьютексыБыстрые мьютексы (fast mutexes), также известные как мьютексы исполнительной системы, обычно обеспечивают более высокую производительность, чем объекты «мьютекс». Почему? Дело в том, что быстрые мьютексы, хоть и построены на объектах событий диспетчера, в отсутствие конкуренции не требуют ожидания объекта «событие» (и соответственно спин-блокировок, на которых основан этот объект). Эти преимущества особенно ярко проявляются в многопроцессорной среде. Быстрые мьютексы широко используются в ядре и драйверах устройств.
Однако быстрые мьютексы годятся, только если можно отключить доставку обычных APC режима ядра. B исполнительной системе определены две функции для захвата быстрых мьютексов: ExAcquireFastMutex и ExAcquire-FastMutexUnsafe. Первая функция блокирует доставку всех APC, повышая IRQL процессора до уровня APC_LEVEL, а вторая — ожидает вызова при уже отключенной доставке обычных APC режима ядра (такое отключение возможно повышением IRQL до уровня «APC» или вызовом KeEnterCriticalRegiori). Другое ограничение быстрых мьютексов заключается в том, что их нельзя захватывать рекурсивно, как объекты «мьютекс».
Защищенные мьютексы (guarded mutexes) — новшество Windows Server 2003; они почти идентичны быстрым мьютексам (хотя внутренне используют другой синхронизирующий объект, KGATE). Захватить защищенные мьютексы можно вызовом функции KeAcquireGuardedMutex, отключающей доставку всех APC режима ядра через KeEnterGuardedRegion, а не KeEnterCritical-Region, которая на самом деле отключает только обычные APC режима ядра. Защищенные мьютексы недоступны вне ядра и используются в основном диспетчером памяти для защиты глобальных операций вроде создания страничных файлов, удаления определенных типов разделов общей памяти и расширения пула подкачиваемой памяти. (Подробнее о диспетчере памяти см. главу 7.)
Ресурсы исполнительной системыРесурсы исполнительной системы (executive resources) — это механизм синхронизации, который поддерживает разделяемый (совместный) и монопольный доступ и по аналогии с быстрыми мьютексами требует предварительного отлючения доставки обычных APC режима ядра. Они основаны на объектах диспетчера, которые используются только при наличии конкуренции. Ресурсы исполнительной системы широко применяются во всей системе, особенно в драйверах файловой системы.
Потоки, которым нужно захватить какой-либо ресурс для совместного доступа, ждут на семафоре, сопоставленном с этим ресурсом, а потоки, которым требуется захватить ресурс для монопольного доступа, — на событии. Семафор с неограниченным счетчиком применяется потому, что в первом случае можно пробудить все ждущие потоки и предоставить им доступ к ресурсу, как только этот семафор перейдет в свободное состояние (ресурс будет освобожден потоком, захватившим его в монопольное владение). Когда потоку нужен монопольный доступ к занятому на данный момент ресурсу, он ждет на синхронизирующем объекте «событие», так как при освобождении события пробуждается только один из ожидающих потоков.