Файл: Debian Таненбаум Бос.pdf

ВУЗ: Не указан

Категория: Книга

Дисциплина: Операционные системы

Добавлен: 29.10.2018

Просмотров: 48016

Скачиваний: 190

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
background image

136  

 Глава 2. Процессы и потоки 

Рис. 2.10. Набор потоков: а — на пользовательском уровне; б — управляемый ядром

за исключением того, что в ней содержатся лишь свойства, принадлежащие каждому 
потоку, такие как счетчик команд потока, указатель стека, регистры, состояние и т. д. 
Таблица потоков управляется системой поддержки исполнения программ. Когда поток 
переводится в состояние готовности или блокируется, информация, необходимая для 
возобновления его выполнения, сохраняется в таблице потоков, точно так же, как ядро 
хранит информацию о процессах в таблице процессов.

Когда поток совершает какие-то действия, которые могут вызвать его локальную 
блокировку, например ожидание, пока другой поток его процесса не завершит какую-
нибудь работу, он вызывает процедуру системы поддержки исполнения программ. 
Эта процедура проверяет, может ли поток быть переведен в состояние блокировки. 
Если может, она сохраняет регистры потока (то есть собственные регистры) в таблице 
потоков, находит в таблице поток, готовый к выполнению, и перезагружает регистры 
машины сохраненными значениями нового потока. Как только будут переключены 
указатель стека и счетчик команд, автоматически возобновится выполнение ново-
го потока. Если машине дается инструкция сохранить все регистры и следующая 
инструкция — загрузить все регистры, то полное переключение потока может быть 
осуществлено за счет всего лишь нескольких инструкций. Переключение потоков, 
осуществленное таким образом, по крайней мере на порядок, а может быть, и больше, 
быстрее, чем перехват управления ядром, что является веским аргументом в пользу 
набора потоков, реализуемого на пользовательском уровне.

Но у потоков есть одно основное отличие от процессов. Когда поток на время оста-
навливает свое выполнение, например когда он вызывает thread_yield, код процедуры 
thread_yield может самостоятельно сохранять информацию о потоке в таблице пото-
ков. Более того, он может затем вызвать планировщик потоков, чтобы тот выбрал для 
выполнения другой поток. Процедура, которая сохраняет состояние потока, и пла-
нировщик — это всего лишь локальные процедуры, поэтому их вызов намного более 
эффективен, чем вызов ядра. Помимо всего прочего, не требуется перехват управления 


background image

2.2. Потоки   

137

ядром, осуществляемый инструкцией trap, не требуется переключение контекста, кэш 
в памяти не нужно сбрасывать на диск и т. д. Благодаря этому планировщик потоков 
работает очень быстро.

У потоков, реализованных на пользовательском уровне, есть и другие преимущества. 
Они позволяют каждому процессу иметь собственные настройки алгоритма плани-
рования. Например, для некоторых приложений, которые имеют поток сборщика 
мусора, есть еще один плюс — им не следует беспокоиться о потоках, остановленных 
в неподходящий момент. Эти потоки также лучше масштабируются, поскольку потоки 
в памяти ядра безусловно требуют в ядре пространства для таблицы и стека, что при 
очень большом количестве потоков может вызвать затруднения.

Но несмотря на лучшую производительность, у потоков, реализованных на пользова-
тельском уровне, есть ряд существенных проблем. Первая из них — как реализовать 
блокирующие системные вызовы. Представьте, что поток считывает информацию 
с клавиатуры перед нажатием какой-нибудь клавиши. Мы не можем разрешить по-
току осуществить настоящий системный вызов, поскольку это остановит выполнение 
всех потоков. Одна из главных целей организации потоков в первую очередь состояла 
в том, чтобы позволить каждому потоку использовать блокирующие вызовы, но при 
этом предотвратить влияние одного заблокированного потока на выполнение других 
потоков. Работая с блокирующими системными вызовами, довольно трудно понять, 
как можно достичь этой цели без особого труда.

Все системные вызовы могут быть изменены и превращены в неблокирующие (напри-
мер, считывание с клавиатуры будет просто возвращать нуль байтов, если в буфере на 
данный момент отсутствуют символы), но изменения, которые для этого необходимо 
внести в операционную систему, не вызывают энтузиазма. Кроме того, одним из аргу-
ментов за использование потоков, реализованных на пользовательском уровне, было 
именно то, что они могут выполняться под управлением существующих операционных 
систем. Вдобавок ко всему изменение семантики системного вызова read потребует 
изменения множества пользовательских программ.

В том случае, если есть возможность заранее сообщить, будет ли вызов блокирующим, 
существует и другая альтернатива. В большинстве версий UNIX существует системный 
вызов select, позволяющий сообщить вызывающей программе, будет ли предполагае-
мый системный вызов read блокирующим. Если такой вызов имеется, библиотечная 
процедура read может быть заменена новой процедурой, которая сначала осуществляет 
вызов процедуры select и только потом — вызов read, если он безопасен (то есть не будет 
выполнять блокировку). Если вызов read будет блокирующим, он не осуществляется. 
Вместо этого запускается выполнение другого потока. В следующий раз, когда система 
поддержки исполнения программ получает управление, она может опять проверить, 
будет ли на этот раз вызов read безопасен. Для реализации такого подхода требуется 
переписать некоторые части библиотеки системных вызовов, что нельзя рассматривать 
в качестве эффективного и элегантного решения, но все же это тоже один из вариантов. 
Код, который помещается вокруг системного вызова с целью проверки, называется кон-
вертом

 (jacket), или оболочкой, или оберткой (wrapper).

С проблемой блокирующих системных вызовов несколько перекликается проблема 
ошибки отсутствия страницы. Мы изучим эту проблему в главе 3. А сейчас достаточно 
сказать, что компьютеры могут иметь такую настройку, что в одно и то же время в опе-
ративной памяти находятся не все программы. Если программа вызывает инструкции 
(или переходит к инструкциям), отсутствующие в памяти, возникает ошибка обраще-


background image

138  

 Глава 2. Процессы и потоки 

ния к отсутствующей странице и операционная система обращается к диску и получает 
отсутствующие инструкции (и их соседей). Это называется ошибкой вызова отсутству-
ющей страницы. Процесс блокируется до тех пор, пока не будет найдена и считана не-
обходимая инструкция. Если ошибка обращения к отсутствующей странице возникает 
при выполнении потока, ядро, которое даже не знает о существовании потоков, как 
и следовало ожидать, блокирует весь процесс до тех пор, пока не завершится дисковая 
операция ввода-вывода, даже если другие потоки будут готовы к выполнению.

Использование набора потоков, реализованного на пользовательском уровне, связано 
еще с одной проблемой: если начинается выполнение одного из потоков, то никакой 
другой поток, принадлежащий этому процессу, не сможет выполняться до тех пор, 
пока первый поток добровольно не уступит центральный процессор. В рамках единого 
процесса нет прерываний по таймеру, позволяющих планировать работу процессов по 
круговому циклу (поочередно). Если поток не войдет в систему поддержки выполнения 
программ по доброй воле, у планировщика не будет никаких шансов на работу.

Проблему бесконечного выполнения потоков можно решить также путем передачи 
управления системе поддержки выполнения программ за счет запроса сигнала (преры-
вания) по таймеру с периодичностью один раз в секунду, но для программы это далеко не 
самое лучшее решение. Возможность периодических и довольно частых прерываний по 
таймеру предоставляется не всегда, но даже если она и предоставляется, общие издержки 
могут быть весьма существенными. Более того, поток может также нуждаться в прерыва-
ниях по таймеру, мешая использовать таймер системе поддержки выполнения программ.

Другой наиболее сильный аргумент против потоков, реализованных на пользователь-
ском уровне, состоит в том, что программистам потоки обычно требуются именно в тех 
приложениях, где они часто блокируются, как, к примеру, в многопоточном веб-сервере. 
Эти потоки часто совершают системные вызовы. Как только для выполнения системно-
го вызова ядро осуществит перехват управления, ему не составит особого труда занять-
ся переключением потоков, если прежний поток заблокирован, а когда ядро займется 
решением этой задачи, отпадет необходимость постоянного обращения к системному 
вызову select, чтобы определить безопасность системного вызова read. Зачем вообще 
использовать потоки в тех приложениях, которые, по существу, полностью завязаны на 
скорость работы центрального процессора и редко используют блокировку? Никто не 
станет всерьез предлагать использование потоков при вычислении первых n простых 
чисел или при игре в шахматы, поскольку в данных случаях от них будет мало проку.

2.2.5. Реализация потоков в ядре

Теперь давайте рассмотрим, что произойдет, если ядро будет знать о потоках и управ-
лять ими. Как показано на рис. 2.10, б, здесь уже не нужна система поддержки ис-
полнения программ. Также здесь нет и таблицы процессов в каждом потоке. Вместо 
этого у ядра есть таблица потоков, в которой отслеживаются все потоки, имеющиеся 
в системе. Когда потоку необходимо создать новый или уничтожить существующий 
поток, он обращается к ядру, которое и создает или разрушает путем обновления та-
блицы потоков в ядре.

В таблице потоков, находящейся в ядре, содержатся регистры каждого потока, 
состояние и другая информация. Вся информация аналогична той, которая ис-
пользовалась для потоков, создаваемых на пользовательском уровне, но теперь она 
содержится в ядре, а не в пространстве пользователя (внутри системы поддержки 


background image

2.2. Потоки   

139

исполнения программ). Эта информация является подмножеством той информа-
ции, которую поддерживают традиционные ядра в отношении своих однопоточных 
процессов, то есть подмножеством состояния процесса. Вдобавок к этому ядро 
поддерживает также традиционную таблицу процессов с целью их отслеживания.

Все вызовы, способные заблокировать поток, реализованы как системные, с более 
существенными затратами, чем вызов процедуры в системе поддержки исполнения 
программ. Когда поток блокируется, ядро по своему выбору может запустить либо 
другой поток из этого же самого процесса (если имеется готовый к выполнению по-
ток), либо поток из другого процесса. Когда потоки реализуются на пользовательском 
уровне, система поддержки исполнения программ работает с запущенными потоками 
собственного процесса до тех пор, пока ядро не заберет у нее центральный процессор 
(или не останется ни одного готового к выполнению потока).

Поскольку создание и уничтожение потоков в ядре требует относительно более весо-
мых затрат, некоторые системы с учетом складывающейся ситуации применяют более 
правильный подход и используют свои потоки повторно. При уничтожении потока он 
помечается как неспособный к выполнению, но это не влияет на его структуру данных, 
имеющуюся в ядре. Чуть позже, когда должен быть создан новый поток, вместо этого 
повторно активируется старый поток, что приводит к экономии времени. Повторное 
использование потоков допустимо и на пользовательском уровне, но для этого нет 
достаточно веских оснований, поскольку издержки на управление потоками там зна-
чительно меньше.

Для потоков, реализованных на уровне ядра, не требуется никаких новых, неблокиру-
ющих системных вызовов. Более того, если один из выполняемых потоков столкнется 
с ошибкой обращения к отсутствующей странице, ядро может с легкостью проверить 
наличие у процесса любых других готовых к выполнению потоков и при наличии та-
ковых запустить один из них на выполнение, пока будет длиться ожидание извлечения 
запрошенной страницы с диска. Главный недостаток этих потоков состоит в весьма 
существенных затратах времени на системный вызов, поэтому, если операции над по-
токами (создание, удаление и т. п.) выполняются довольно часто, это влечет за собой 
более существенные издержки.

Хотя потоки, создаваемые на уровне ядра, и позволяют решить ряд проблем, но спра-
виться со всеми существующими проблемами они не в состоянии. Что будет, к при-
меру, когда произойдет разветвление многопоточного процесса? Будет ли у нового 
процесса столько же потоков, сколько у старого, или только один поток? Во многих 
случаях наилучший выбор зависит от того, выполнение какого процесса запланиро-
вано следующим. Если он собирается вызвать команду exec, чтобы запустить новую 
программу, то, наверное, правильным выбором будет наличие только одного потока. 
Но если он продолжит выполнение, то лучше всего было бы, наверное, воспроизвести 
все имеющиеся потоки.

Другой проблемой являются сигналы. Стоит вспомнить, что сигналы посылаются 
процессам, а не потокам, по крайней мере, так делается в классической модели. Какой 
из потоков должен обработать поступающий сигнал? Может быть, потоки должны за-
регистрировать свои интересы в конкретных сигналах, чтобы при поступлении сигнала 
он передавался потоку, который заявил о своей заинтересованности в этом сигнале? 
Тогда возникает вопрос: что будет, если на один и тот же сигнал зарегистрировались 
два или более двух потоков? И это только две проблемы, создаваемые потоками, а ведь 
на самом деле их значительно больше.


background image

140  

 Глава 2. Процессы и потоки 

2.2.6. Гибридная реализация

В попытках объединить преимущества создания потоков на уровне пользователя и на 
уровне ядра была исследована масса различных путей. Один из них (рис. 2.11) заклю-
чается в использовании потоков на уровне ядра, а затем нескольких потоков на уровне 
пользователя в рамках некоторых или всех потоков на уровне ядра. При использова-
нии такого подхода программист может определить, сколько потоков использовать на 
уровне ядра и на сколько потоков разделить каждый из них на уровне пользователя. 
Эта модель обладает максимальной гибкостью.

Рис. 2.11. Разделение на пользовательские потоки в рамках потока ядра

При таком подходе ядру известно только о потоках самого ядра, работу которых оно 
и планирует. У некоторых из этих потоков могут быть несколько потоков на пользова-
тельском уровне, которые расходятся от их вершины. Создание, удаление и планирова-
ние выполнения этих потоков осуществляется точно так же, как и у пользовательских 
потоков, принадлежащих процессу, запущенному под управлением операционной 
системы, не способной на многопоточную работу. В этой модели каждый поток на 
уровне ядра обладает определенным набором потоков на уровне пользователя, которые 
используют его по очереди.

2.2.7. Активация планировщика

Хотя потоки на уровне ядра по ряду ключевых позиций превосходят потоки на уровне 
пользователя, они, несомненно, более медлительны. Поэтому исследователи искали 
способы улучшения ситуации без потери их положительных свойств. Далее мы опишем 
один из таких способов, изобретенный Андерсоном (Anderson et al., 1992), который 
называется активацией планировщика. Родственная работа рассматривается Элдером 
и Скоттом (Edler et al.,1988; Scott et al., 1990).

Цель работы по активации планировщика заключается в имитации функциональных 
возможностей потоков на уровне ядра, но при лучшей производительности и более 
высокой гибкости, свойственной пакетам потоков, реализуемых в пользовательском 
пространстве. В частности, пользовательские потоки не должны выполнять специаль-