Добавлен: 29.10.2018
Просмотров: 48018
Скачиваний: 190
146
Глава 2. Процессы и потоки
осуществляет системный вызов alarm, появляется смысл направить результирующий
сигнал к вызывающему потоку. Но когда потоки целиком реализованы в пользователь-
ском пространстве, ядро даже не знает о потоках и вряд ли сможет направить сигнал
к нужному потоку. Дополнительные сложности возникают в том случае, если у процесса
на данный момент есть лишь один необработанный аварийный сигнал и несколько по-
токов осуществляют системный вызов alarm независимо друг от друга.
Другие сигналы, например прерывания клавиатуры, не имеют определенного отноше-
ния к потокам. Кто их должен перехватывать? Один специально назначенный поток?
Или все потоки? А может быть, заново создаваемый всплывающий поток? Кроме того,
что произойдет, если один из потоков вносит изменения в обработчики сигналов, не
уведомляя об этом другие потоки? А что случится, если одному потоку потребуется
перехватить конкретный сигнал (например, когда пользователь нажмет
CTRL+C
), а дру-
гому потоку этот сигнал понадобится для завершения процесса? Подобная ситуация
может сложиться, если в одном или нескольких потоках выполняются стандартные
библиотечные процедуры, а в других — процедуры, созданные пользователем. Совер-
шенно очевидно, что такие требования потоков несовместимы. В общем, с сигналами не
так-то легко справиться и при наличии лишь одного потока, а переход к многопоточной
среде отнюдь не облегчает их обработку.
Остается еще одна проблема, создаваемая потоками, — управление стеком. Во многих
системах при переполнении стека процесса ядро автоматически предоставляет ему
дополнительное пространство памяти. Когда у процесса несколько потоков, у него
должно быть и несколько стеков. Если ядро ничего не знает о существовании этих
стеков, оно не может автоматически наращивать их пространство при ошибке стека.
Фактически оно даже не сможет понять, что ошибка памяти связана с разрастанием
стека какого-нибудь потока.
Разумеется, эти проблемы не являются непреодолимыми, но они наглядно демон-
стрируют, что простое введение потоков в существующую систему без существенной
доработки приведет к ее полной неработоспособности. Возможно, необходимый
минимум будет состоять в переопределении семантики системных вызовов и перепи-
сывании библиотек. И все это должно быть сделано так, чтобы сохранялась обратная
совместимость с существующими программами, когда все ограничивается процессом,
имеющим только один поток. Дополнительную информацию о потоках можно найти
в трудах Хаузера (Hauser et al., 1993), Марша (Marsh et al., 1991) и Родригеса (Rodrigues
et al., 2010).
2.3. Взаимодействие процессов
Довольно часто процессам необходимо взаимодействовать с другими процессами.
Например, в канале оболочки выходные данные одного процесса могут передаваться
другому процессу, и так далее вниз по цепочке. Поэтому возникает необходимость во
взаимодействии процессов, и желательно по хорошо продуманной структуре без ис-
пользования прерываний. В следующих разделах мы рассмотрим некоторые вопросы,
связанные со взаимодействием процессов, или межпроцессным взаимодействием
(InterProcess Communication (IPC)).
Короче говоря, будут рассмотрены три вопроса. Первый будет касаться уже упомянуто-
го примера: как один процесс может передавать информацию другому процессу. Второй
2.3. Взаимодействие процессов
147
коснется обеспечения совместной работы процессов без создания взаимных помех,
когда, к примеру, два процесса в системе бронирования авиабилетов одновременно
пытаются захватить последнее место в самолете для разных клиентов. Третий вопрос
коснется определения правильной последовательности на основе существующих вза-
имозависимостей: если процесс А вырабатывает данные, а процесс Б их распечатывает,
то процесс Б, перед тем как печатать, должен подождать, пока процесс А не выработает
определенные данные. Изучение всех трех вопросов начнется со следующего раздела.
Следует отметить, что два из этих трех вопросов также применимы и к потокам. Пер-
вый из них, касающийся передачи информации, применительно к потокам решается
значительно легче, поскольку потоки имеют общее адресное пространство (взаимодей-
ствующие потоки, реализованные в различных адресных пространствах, подпадают
под категорию взаимодействия процессов). А вот два других вопроса — относительно
исключения взаимных помех и правильной последовательности — в полной мере
применимы и к потокам: сходные проблемы и сходные методы их решения. Далее про-
блемы будут рассматриваться в контексте процессов, но нужно иметь в виду, что те же
проблемы и решения применяются и в отношении потоков.
2.3.1. Состязательная ситуация
В некоторых операционных системах совместно работающие процессы могут использо-
вать какое-нибудь общее хранилище данных, доступное каждому из них по чтению и по
записи. Это общее хранилище может размещаться в оперативной памяти (возможно,
в структуре данных ядра) или может быть представлено каким-нибудь общим файлом.
Расположение общей памяти не меняет характера взаимодействия и возникающих при
этом проблем. Чтобы посмотреть, как взаимодействие процессов осуществляется на
практике, давайте рассмотрим простой общеизвестный пример — спулер печати. Когда
процессу необходимо распечатать какой-нибудь файл, он помещает имя этого файла
в специальный каталог спулера.
Другой процесс под названием демон принтера периодически проверяет наличие фай-
лов для печати и в том случае, если такие файлы имеются, распечатывает их и удаляет
их имена из каталога.
Представьте, что в нашем каталоге спулера имеется большое количество областей
памяти с номерами 0, 1, 2..., в каждой из которых может храниться имя файла. Также
представьте, что есть две общие переменные: out, указывающая на следующий файл,
предназначенный для печати, и in, указывающая на следующую свободную область
в каталоге. Эти две переменные могли бы неплохо сохраняться в файле, состоящем
из двух слов и доступном всем процессам. В какой-то момент времени области от 0 до
3 пустуют (файлы уже распечатаны). Почти одновременно процессы А и Б решают,
что им нужно поставить файл в очередь на печать. Эта ситуация показана на рис. 2.15.
В правовом пространстве, где применимы законы Мэрфи, может случиться следую-
щее. Процесс А считывает значение переменной in и сохраняет значение 7 в локальной
переменной по имени next_ free_slot (следующая свободная область). Сразу же после
этого происходит прерывание по таймеру, центральный процессор решает, что про-
цесс А проработал достаточно долго, и переключается на выполнение процесса Б. Про-
цесс Б также считывает значение переменной in и также получает число 7. Он также
сохраняет его в своей локальной переменной next_ free_slot. К текущему моменту оба
процесса полагают, что следующей доступной областью будет 7. Процесс Б продолжает
148
Глава 2. Процессы и потоки
Рис. 2.15. Одновременное стремление двух процессов получить доступ к общей памяти
выполняться. Он сохраняет имя своего файла в области 7 и присваивает переменной
in обновленное значение 8. Затем он переходит к выполнению каких-нибудь других
действий. Через некоторое время выполнение процесса А возобновляется с того места,
где он был остановлен. Он считывает значение переменной next_ free_slot, видит там
число 7 и записывает имя своего файла в область 7, затирая то имя файла, которое
только что было в него помещено процессом Б. Затем он вычисляет next_ free_slot + 1,
получает значение 8 и присваивает его переменной in. В каталоге спулера нет внутрен-
них противоречий, поэтому демон печати не заметит никаких нестыковок, но процесс Б
никогда не получит вывода на печать.
Пользователь Б будет годами бродить вокруг принтера, тоскливо надеясь получить рас-
печатку, которой не будет никогда. Подобная ситуация, когда два или более процесса
считывают или записывают какие-нибудь общие данные, а окончательный результат
зависит от того, какой процесс и когда именно выполняется, называется состязатель-
ной ситуацией
. Отладка программ, в которых присутствует состязательная ситуация,
особой радости не доставляет. Результаты большинства прогонов могут быть вполне
приемлемыми, но до поры до времени, пока не наступит тот самый редкий случай, когда
произойдет нечто таинственное и необъяснимое. К сожалению, с ростом параллелизма
из-за все большего количества ядер состязательные ситуации встречаются все чаще.
2.3.2. Критические области
Как же избежать состязательной ситуации? Ключом к предупреждению проблемы
в этой и во многих других ситуациях использования общей памяти, общих файлов
и вообще чего-нибудь общего может послужить определение способа, при котором
в каждый конкретный момент времени доступ к общим данным для чтения и записи
может получить только один процесс. Иными словами, нам нужен способ взаимного
исключения
, то есть некий способ, обеспечивающий правило, при котором если общие
данные или файл используются одним процессом, возможность их использования
всеми другими процессами исключается. Описанные выше трудности произошли
благодаря тому, что процесс Б стал использовать общие переменные еще до того, как
процесс А завершил работу с ними. Выбор подходящих элементарных операций для
достижения взаимного исключения является основной проблемой конструирования
2.3. Взаимодействие процессов
149
любой операционной системы, и именно ее мы будем подробно рассматривать в сле-
дующих разделах.
Проблемы обхода состязательных ситуаций могут быть сформулированы также
в абстрактной форме. Какую-то часть времени процесс занят внутренними вычис-
лениями и чем-нибудь другим, не создающим состязательных ситуаций. Но иногда
он вынужден обращаться к общей памяти или файлам либо совершать какие-нибудь
другие значимые действия, приводящие к состязаниям. Та часть программы, в которой
используется доступ к общей памяти, называется критической областью или крити-
ческой секцией
. Если бы удалось все выстроить таким образом, чтобы никакие два
процесса не находились одновременно в своих критических областях, это позволило
бы избежать состязаний.
Хотя выполнение этого требования позволяет избежать состязательных ситуаций, его
недостаточно для того, чтобы параллельные процессы правильно выстраивали совмест-
ную работу и эффективно использовали общие данные. Для приемлемого решения
необходимо соблюдение четырех условий:
1. Два процесса не могут одновременно находиться в своих критических областях.
2. Не должны выстраиваться никакие предположения по поводу скорости или ко-
личества центральных процессоров.
3. Никакие процессы, выполняемые за пределами своих критических областей, не
могут блокироваться любым другим процессом.
4. Процессы не должны находиться в вечном ожидании входа в свои критические
области.
В абстрактном смысле необходимое нам поведение показано на рис. 2.16. Мы видим,
что процесс А входит в свою критическую область во время T
1
. Чуть позже, когда на-
ступает время T
2
, процесс Б пытается войти в свою критическую область, но терпит
неудачу, поскольку другой процесс уже находится в своей критической области, а мы
допускаем это в каждый момент времени только для одного процесса. Следовательно,
Рис. 2.16. Взаимное исключение использования критических областей
150
Глава 2. Процессы и потоки
Б временно приостанавливается до наступления времени T
3
, когда A покинет свою кри-
тическую область, позволяя Б тут же войти в свою критическую область. Со временем
(в момент T
4
) Б покидает свою критическую область, и мы возвращаемся в исходную
ситуацию, когда ни один из процессов не находится в своей критической области.
2.3.3. Взаимное исключение с активным ожиданием
В этом разделе будут рассмотрены различные предложения для достижения режима
взаимного исключения, при котором, пока один процесс занят обновлением общей
памяти и находится в своей критической области, никакой другой процесс не сможет
войти в свою критическую область и создать проблему.
Запрещение прерываний
В однопроцессорных системах простейшим решением является запрещение всех пре-
рываний каждым процессом сразу после входа в критическую область и их разрешение
сразу же после выхода из критической области. При запрещении прерываний не могут
осуществляться никакие прерывания по таймеру. Поскольку центральный процессор
переключается с одного процесса на другой в результате таймерных или каких-нибудь
других прерываний, то при выключенных прерываниях он не сможет переключиться
на другой процесс. Поскольку процесс запретил прерывания, он может исследовать
и обновлять общую память, не опасаясь вмешательства со стороны любого другого
процесса.
Но вообще-то этот подход не слишком привлекателен, поскольку абсолютно неразумно
давать пользовательским процессам полномочия выключать все прерывания. Пред-
ставьте, что получится, если один из них выключил и не включил прерывания? Это
может вызвать крах всей системы. Более того, если мы имеем дело с многопроцессор-
ной системой (с двумя или, может быть, несколькими центральными процессорами),
запрещение прерываний действует только на тот центральный процессор, на котором
выполняется запретительная инструкция. Все остальные процессоры продолжат свою
работу и смогут обращаться к общей памяти.
В то же время запрещение прерываний всего на несколько инструкций зачастую
является очень удобным средством для самого ядра, когда оно обновляет пере-
менные или списки. К примеру, когда прерывание происходит в момент изменения
состояния списка готовых процессов, может сложиться состязательная ситуация.
Вывод здесь следующий: запрещение прерываний в большинстве своем является по-
лезной технологией внутри самой операционной системы, но не подходит в качестве
универсального механизма взаимных блокировок для пользовательских процессов.
Благодаря увеличению количества многоядерных центральных процессоров даже на
недорогих персональных компьютерах возможности достижения взаимного исклю-
чения за счет запрещения прерываний даже внутри ядра сужаются. Уже становится
привычным наличие двухъядерных процессоров, на многих машинах имеются четыре
ядра, и не за горами распространение 8-, 16- или 32-ядерных процессоров. Запрещение
прерываний на одном центральном процессоре в многоядерных (то есть мультипро-
цессорных) системах не запрещает другим центральным процессорам препятствовать
операциям, выполняемым первым центральным процессором. Следовательно, возни-
кает потребность в применении более сложных схем.