Поскольку второй поток получил значение указателя на конец очереди до того, как первый поток завершил его обновление, второй вставил свои данные в то же место, что и первый. Таким образом, данные первого потока были перезаписаны другими данными, а один участок очереди остался пустым. Хотя рис. 3-23 иллюстрирует, что могло бы случиться в многопроцессорной системе, аналогичную ошибку было бы нельзя исключить и в однопроцессорной системе — при переключении контекста на второй поток до того, как первый поток успел бы обновить указатель на конец очереди.
Разделы кода, обращающиеся к неразделяемым ресурсам, называются критическими секциями (critical sections). B критической секции единовременно может выполняться только один поток. Пока один поток записывает в файл, обновляет базу данных или модифицирует общую переменную, доступ к этому ресурсу со стороны других потоков запрещен. Псевдокод, показанный на рис. 3-23, представляет собой критическую секцию, которая некорректно обращается к разделяемой структуре данных без взаимоисключения.
Взаимоисключение, важное для всех операционных систем, особенно значимо (и запутанно) в случае операционной системы с жестко связанной симметричной мультипроцессорной обработкой (tightly-coupled symmetric multiprocessing), например в Windows, в которой один и тот же системный код, выполняемый на нескольких процессорах одновременно, разделяет некоторые структуры данных, хранящиеся в глобальной памяти. B Windows поддержка механизмов, с помощью которых системный код может предотвратить одновременное изменение двумя потоками одной и той же структуры, возлагается на ядро. Оно предоставляет специальные примитивы взаимоисключения, используемые им и остальными компонентами исполнительной системы для синхронизации доступа к глобальным структурам данных.
Так как планировщик синхронизирует доступ к своим структурам данных при IRQL уровня «DPC/dispatch», ядро и исполнительная система не могут полагаться на механизмы синхронизации, которые могли бы привести к ошибке страницы или к перераспределению процессорного времени при IRQL уровня «DPC/dispatch» или выше (эти уровни также известны под названием «высокий IRQL»). Из следующих разделов вы узнаете, как ядро и исполнительная система используют взаимоисключение для защиты своих глобальных структур данных при высоком IRQL и какие механизмы синхронизации и взаимоисключения они применяют при низких уровнях IRQL (ниже «DPC/dispatch»).
Синхронизация ядра при высоком IRQL
Ядро должно гарантировать, что в каждый момент только один процессор выполняет код в критической секции. Критическими секциями ядра являются разделы кода, модифицирующие глобальные структуры данных, например базу данных диспетчера ядра или его очередь DPC Операционная система не смогла бы корректно работать, если бы ядро не гарантировало взаимоисключающий доступ потоков к этим структурам данных.
B этом плане больше всего проблем с прерываниями. Так, в момент обновления ядром глобальной структуры данных может возникнуть прерывание, процедура обработки которого изменяет ту же структуру. B простых однопроцессорных системах развитие событий по такому сценарию исключается путем отключения всех прерываний на время доступа к глобальным данным, однако в ядре Windows реализовано более сложное решение. Перед использованием глобального ресурса ядро временно маскирует прерывания, обработчики которых используют тот же ресурс. Для этого ядро повышает IRQL процессора до самого высокого уровня, используемого любым потенциальным источником прерываний, который имеет доступ к глобальным данным. Например, прерывание на уровне «DPC/dispatch» приводит к запуску диспетчера ядра, использующего диспетчерскую базу данных. Следовательно, любая другая часть ядра, имеющая дело с этой базой данных, повышает IRQL до уровня «DPC/dispatch», маскируя прерывания того же уровня перед обращением к диспетчерской базе данных.
Эта стратегия хорошо работает в однопроцессорных системах, но не годится для многопроцессорных конфигураций. Повышение IRQL на одном из процессоров не исключает прерываний на другом процессоре, а ядро должно гарантировать взаимоисключающий доступ на всех процессорах.
Взаимоблокирующие операции
Простейшая форма механизмов синхронизации опирается на аппаратную поддержку безопасных операций над целыми значениями и выполнения сравнений в многопроцессорной среде. Сюда относятся такие функции, как InterlockedIncrement, InterlockedDecrement, InterlockedExcbange и Interlocked-CompareExchange. Скажем, функция InterlockedDecrement, использует префикс х86-инструкции lock (например, lock xadd) для блокировки многопроцессорной шины на время операции вычитания, чтобы другой процессор, модифицирующий тот же участок памяти, не смог выполнить свою операцию в момент между чтением исходных данных и записью их нового (меньшего) значения. Эта форма базовой синхронизации используется ядром и драйверами.
Читать дальше
Конец ознакомительного отрывка
Купить книгу