- •Глава 5
- •5.1. Принципы параллельных вычислений
- •Простой пример
- •Взаимодействие процессов
- •Листинг 5.1. Взаимные исключения
- •5.2. Взаимоисключения
- •Листинг 5.3. Алгоритм Петерсона для двух процессов
- •5.3. Взаимоисключения: аппаратная поддержка
- •Специальные машинные команды
- •5.4. Семафоры
- •5.5. Мониторы
- •5.6. Передача сообщений
- •Синхронизация
- •Принцип работы очереди
- •5.7. Задача читателей/писателей
- •Приоритетное чтение
- •5.8. Резюме, ключевые термины и контрольные вопросы
- •5.9. Pекомендуемая литература
- •5.10. Задачи
5.1. Принципы параллельных вычислений
В однопроцессорной многозадачной системе процессы чередуются для создания иллюзии одновременного выполнения (см. рис. 2.12,а). Несмотря на то что при этом не достигается реальная параллельная работа процессов и, более того, имеются определенные накладные расходы, связанные с переключением между процессами, такое чередующееся выполнение обеспечивает немалые выгоды с точки зрения эффективности и структуризации программ. В многопроцессорных системах возможно не только чередование процессов, но и их перекрытие (см. рис. 2.12,6).
На первый взгляд может показаться, что чередование и перекрытие представляют собой принципиально отличающиеся режимы работы и, соответственно, при этом возникают различные проблемы. Однако в действительности обе технологии, которые можно рассматривать как примеры параллельных вычислений, порождают одинаковые проблемы. В однопроцессорных системах проблемы вытекают из основных характеристик многозадачных систем: невозможно предсказать относительную скорость выполнения процессов. Она зависит от других процессов, способа обработки прерываний операционной системой и стратегий планирования операционной системы. При этом возникают следующие трудности.
Разделение глобальных ресурсов чревато опасностями. Например, если два процесса используют одну глобальную переменную и оба выполняют чтение и запись этой переменной, то критическим оказывается порядок чтения и записи этой переменной разными процессами. Пример такой проблемы приведен в следующем подразделе.
Операционной системе трудно управлять распределением ресурсов оптимальным образом. Например, процесс А может затребовать и получить контроль над некоторым каналом ввода-вывода, после чего временно приостановить работу. Нежелательно, чтобы операционная система при этом блокировала канал и не давала другим процессам возможности использовать его — такая политика может привести к возникновению условий взаимоблокировки, описываемых в главе 6, "Взаимоблокировка и голодание".
3. Становится очень трудно обнаружить программную ошибку, поскольку обычно результат работы программы перестает быть детерминированным и воспроизводимым (см., например, [LEBL87, CARR89]).
Все описанные трудности имеются в наличии и в многопроцессорной системе, поскольку и в этом случае относительная скорость выполнения процессов непредсказуема. Многопроцессорная система, кроме того, должна быть в состоянии разрешить проблемы, возникающие вследствие одновременного выполнения нескольких процессов. Однако в основе своей эти проблемы те же, что и в однопроцессорной системе. Окончательно ясно это станет в процессе работы с материалом данной главы.
Простой пример
Рассмотрим следующую процедуру:
void echo()
{
chin = getchar();
chout = chin;
putchar(chout);
}
Она демонстрирует основные элементы отображения вводимых символов. Входной символ, получаемый с клавиатуры, сохраняется в переменной chin; после этого значение переменной chin присваивается переменной chout и выводится на экран. Данная процедура может неоднократно вызываться любым процессом для получения ввода пользователя и отображения его на экране.
Теперь представим, что у нас имеется однопроцессорная многозадачная система, поддерживающая единственного пользователя. Пользователь может переходить от одного приложения к другому; при этом все приложения используют одну и ту же клавиатуру и экран. Поскольку рассматриваемая нами процедура нужна всем приложениям, имеет смысл сделать ее разделяемой процедурой, загружаемой в часть памяти, глобальную для всех приложений (таким образом, имеется только одна копия процедуры, и тем самым экономится память).
Разделение основной памяти между процессами способствует эффективному и тесному взаимодействию процессов. Однако такое разделение может привести к проблемам. Рассмотрим последовательность событий.
Процесс Р1 вызывает процедуру echo и прерывается немедленно по выполнении функции getchar. В этот момент последний введенный символ х сохранен в переменной chin.
Активируется процесс Р2, который вызывает процедуру echo. Эта процедура выполняется до конца; при этом считывается с клавиатуры и выводится на экран очередной символ у.
Продолжается выполнение процесса Р1. Однако к этому моменту значение х в переменной chin перезаписано — теперь эта переменная содержит значение у, которое присваивается переменной chout и выводится на экран.
Таким образом, первый введенный символ благополучно теряется, зато второй оказывается выведенным на экран дважды. Проблема заключается в разделяемой глобальной переменной chin, к которой обращаются несколько процессов; если один процесс изменяет глобальную переменную и затем прерывается, другой может успеть изменить ее значение, перед тем как первый процесс им воспользуется. Предположим теперь, что выполнять процедуру одновременно процессы не могут. В таком случае описанная ранее последовательность действий выглядит иначе.
Процесс Р1 вызывает процедуру echo и прерывается немедленно по выполнении функции getchar. В этот момент последний введенный символ х сохранен в переменной chin.
Активируется процесс Р2, который вызывает процедуру echo. Однако, поскольку приостановленный процесс Р1 находится в процедуре echo, P2 блокируется от входа в данную процедуру. Следовательно, выполнение Р2 приостанавливается до тех пор, пока процедура echo не окажется свободной.
В некоторый более поздний момент продолжается выполнение процесса Р1, который завершает выполнение процедуры echo, выводя на экран верный символ — х.
После того как Р1 покидает echo, блокировка Р2 удаляется и позже, при возобновлении работы процесса Р2, им успешно выполняется процедура echo.
Урок, который следует извлечь из данного примера, заключается в том, что разделяемые глобальные переменные (как и другие разделяемые глобальные ресурсы) нуждаются в защите, и единственный способ сделать это состоит в управлении кодом, осуществляющим доступ к этим переменным. Если мы добьемся того, что в определенный момент времени только один процесс сможет входить в процедуру echo и она обязательно будет полностью выполнена вошедшим в нее процессом до того, как станет возможно выполнение ее другим процессом, то мы будем застрахованы от возникновения рассматриваемой ошибки. Каким образом этого добиться — основной вопрос данной главы.
Мы предполагали, что имеем дело с однопроцессорной многозадачной операционной системой. Пример продемонстрировал, что проблема может возникнуть даже в этом случае. В многопроцессорной системе возникает та же проблема защиты разделяемых ресурсов, которая имеет аналогичное решение. Сперва предположим, что механизм управления доступом к разделяемой глобальной переменной отсутствует.
Одновременно выполняются процессы Р1 и Р2 — каждый на своем процессоре. Оба процесса вызывают процедуру echo.
Происходят следующие события (события в одной строке происходят параллельно):
Процесс Р1 Процесс Р2
• in = getchar(); .
in = getchar();
chout = chin; chout = chin;
putchar(chout); .
putchar(chout);
•
В результате символ, введенный в процессе Р1, теряется до того, как будет выведен на экран, и обоими процессами выводится символ, считанный процессом Р2. Теперь добавим в систему механизм, гарантирующий, что в процедуре echo в любой момент времени может находиться только один процесс. В этом случае последовательность событий становится такой.
Одновременно выполняются процессы Р1 и Р2 — каждый на своем процессоре. Процесс Р1 вызывает процедуру echo.
В то время как процесс Р1 находится в процедуре echo, эту же процедуру вызывает процесс Р2. Однако, поскольку процесс Р1 находится в процедуре echo (неважно, выполняется ли в этот момент процесс Р1 или приостановлен), Р2 блокируется от входа в данную процедуру. Следовательно, выполнение Р2 приостанавливается до тех пор, пока процедура echo не окажется свободной.
В некоторый более поздний момент времени выполнение процессом Р1 процедуры echo завершается, после чего немедленно продолжается выполнение, процесса Р2 и им успешно выполняется процедура echo.
В однопроцессорной системе причина возникновения проблемы заключается в том, что прерывание может остановить выполнение процесса в произвольном месте. В многопроцессорной системе условия работы те же, но проблема может возникнуть и из-за того, что два выполняющихся одновременно процесса могут в один момент времени обратиться к одной глобальной переменной. Однако решение проблем обоих типов одно и то же: управление доступом к разделяемым ресурсам.
Участие операционной системы
Можно перечислить следующие вопросы конструирования и управления операционных систем, возникающие из-за наличия параллельных вычислений.
Операционная система должна быть способна отслеживать различные активные процессы. Это осуществляется при помощи управляющих блоков процессов, как описано в главе 4, "Потоки, симметричная мультипроцессорная обработка и микроядра".
Операционная система должна распределять и освобождать различные ресурсы для каждого активного процесса, в том числе следующие.
Процессорное время: это функция планирования, рассматриваемая в части 4, "Планирование".
Память: большинство операционных систем используют схему виртуальной памяти. Этот вопрос рассматривается в части 3, "Память".
Файлы: обсуждаются в главе 12, "Управление файлами".
Устройства ввода-вывода: обсуждаются в главе 11, "Управление вводом-выводом и дисковое планирование".
Операционная система должна защищать данные и физические ресурсы каждого процесса от непреднамеренного воздействия других процессов, что ключает использование технологий, применяющихся для работы с памятью, файлами и устройствами ввода-вывода. Обсуждение вопросов защиты можно найти в главе 15, "Безопасность".
4. Результат работы процесса не должен зависеть от скорости его выполнения по отношению к другим параллельно выполняющимся процессам. Этому вопросу посвящена данная глава.
Чтобы лучше понять вопросы независимости работы процессов от относительной скорости выполнения, рассмотрим сначала способы взаимодействия процессов.
