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

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

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

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

Добавлен: 29.10.2018

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

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

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

2.2. Потоки   

141

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

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

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

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

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

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


background image

142  

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

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

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

2.2.8. Всплывающие потоки

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

Возможен и совершенно иной подход, при котором поступление сообщения вынуждает 
систему создать новый поток для его обработки. Такой поток (рис. 2.12) называется

Рис. 2.12. Создание нового потока при поступлении сообщения: 

а — до поступления; б — после поступления


background image

2.2. Потоки   

143

всплывающим

. Основное преимущество всплывающих потоков заключается в том, 

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

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

2.2.9. Превращение однопоточного кода в многопоточный

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

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

Рассмотрим в качестве примера переменную errno, поддерживаемую UNIX. Когда 
процесс (или поток) осуществляет системный вызов, терпящий неудачу, код ошибки 
помещается в errno. На рис. 2.13 поток 1 выполняет системный вызов access, чтобы 
определить, разрешен ли доступ к конкретному файлу. Операционная система возвра-
щает ответ в глобальной переменной errno. После возвращения управления потоку 1
но перед тем, как он получает возможность прочитать значение errno, планировщик 
решает, что поток 1 на данный момент времени вполне достаточно использовал время 
центрального процессора и следует переключиться на выполнение потока 2. Поток 2 
выполняет вызов open, который терпит неудачу, что вызывает переписывание значе-
ния переменной errno, и код access первого потока утрачивается навсегда. Когда чуть 
позже возобновится выполнение потока 1, он считает неверное значение и поведет 
себя некорректно.

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


background image

144  

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

Рис. 2.13. Конфликт потоков при использовании глобальной переменной

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

Рис. 2.14. У потоков могут быть закрытые глобальные переменные

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


background image

2.2. Потоки   

145

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

create_global("bufptr");

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

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

set global("bufptr", &buf);

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

bufptr = read_global("bufptr");

Она возвращает адрес для доступа к данным, хранящимся в глобальной переменной.

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

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

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

А теперь рассмотрим сигналы. Некоторые сигналы по своей логике имеют отношение 
к потокам, а некоторые не имеют к ним никакого отношения. К примеру, если поток