Глава_1 (1085730), страница 2
Текст из файла (страница 2)
Но существует одно серьезное отличие потоков от процессов. В тот момент, когда поток завершает на время свою работу, например, когда он вызывает процедуру thread_yield, программа thread_yield может сама сохранить информацию о потоке в таблице потоков. Более того, она может после этого вызвать планировщик потоков для выбора следующего потока. Процедура, сохраняющая информацию о потоке, и планировщик являются локальными процедурами, и их вызов существенно более эффективен, чем вызов ядра. Не требуются прерывание, переключение контекста, сохранение кэша и т. п., что существенно ускоряет переключение потоков. Потоки, реализованные на уровне пользователя, имеют и другие преимущества.
Они позволяют каждому процессу иметь собственный алгоритм планирования. Для некоторых приложений, например приложений с потоком «сборки мусора», оказывается удобным не задумываться о том, что поток может остановиться в неподходящий момент. Эти приложения также лучше масштабируются, поскольку потоки ядра неизменно занимают некоторое пространство в таблице и стековое пространство в ядре, что может стать проблемой в случае большого числа потоков.
Несмотря на более высокую производительность, с реализацией потоков на уровне пользователя связаны и некоторые серьезные проблемы. Первой из них является проблема реализации блокирующих системных запросов. Предстаньте, что поток начинает считывание с клавиатуры до того, как была нажата хотя бы одна клавиша. Было бы неприемлемо позволить потоку выполнить системный запрос, поскольку это остановило бы все потоки. Одной из основных целей использования потоков было предоставление возможности каждому потоку использовать блокирующие запросы, но так, чтобы один блокированный поток не мешал остальным. Не очень понятно, как достичь этой цели, если использовать блокирующие системные запросы.
Можно сделать все системные запросы не блокирующими (как, например, запрос на чтение с клавиатуры read, который возвращал бы 0 байт в случае отсутствия данных), но это потребует неприемлемых изменений операционной системы. Вспомните, ведь одним из основных аргументов в пользу потоков на уровне пользователя была возможность работы в существующих операционных системах. К тому же изменение семантики запроса read повлекло бы за собой изменения во многих пользовательских программах.
Оказалось, что существует альтернатива, если имеется возможность узнавать заранее, последует ли за запросом блокировка. В некоторых версиях UNIX есть системный запрос select, позволяющий узнать о наличии или отсутствии блокировки у последующего запроса read. При наличии такого системного запроса библиотечную процедуру read можно заменить новой, сначала выполняющей запрос select, а потом запрос read, если за ним не последует блокировки. Если блокировка должна произойти, то запрос не выполняется и запускается другой поток. В следующий момент, когда система поддержки исполнения программ получит управление она может проверить еще раз, последует ли за запросом read блокировка. Такой подход требует перезаписи части библиотеки системных запросов, неэффективен и не отличается изяществом, но выбор не так уж велик. Код, который помещается вокруг системного запроса для проверки на блокировку, называется чехлом (jacket) или упаковкой (wrapper).
Схожей проблемой является ошибка из-за отсутствия страницы. Мы рассмотрим ее подробнее в главе 4. На данный момент нам важно, что компьютер можно настроить так, чтобы не вся программа находилась и основной памяти. Если программа производит вызов или переход к той инструкции, которой нет в памяти, возникает ошибка из-за отсутствия страницы, и операционная система берет недостающую часть программы с диска. Именно эта ситуация называется ошибкой из-за отсутствия страницы. Процесс блокируется, пока необходимая инструкция не будет найдена и считана. Если поток приводит к ошибке из-за отсутствия страницы, ядро, не знающее о существовании потоков, блокирует весь процесс целиком, до завершения операции чтения с диска, несмотря на наличие остальных нормально функционирующих потоков.
Еще одной проблемой потоков на уровне пользователя является тот факт, что при запуске одного потока ни один другой поток не будет запущен, пока первый поток добровольно не отдаст процессор. Внутри одного процесса нет прерываний по таймеру, в результате чего невозможно создать планировщик для поочередного выполнения потоков. Планировщик ничего не сможет сделать, пока поток не окажется в системе поддержки исполнения программ по собственному желанию.
Одним из решений этой проблемы может стать ежесекундное прерывание, передающее управление системе поддержки исполнения программ, но этот способ достаточно груб и неудобен. Периодические прерывания по таймеру с более высокой частотой не всегда возможны, а если и возможны, то издержки все равно будут существенными. Более того, возможно, что потоку необходимы свои прерывания но таймеру, которые будут конфликтовать с прерываниями системы поддержки исполнения программ.
Еще один, и, возможно, наиболее серьезный аргумент против использования потоков на уровне пользователя состоит в том, что программисты хотят использовать потоки именно в тех приложениях, в которых потоки часто блокируются, например в многопоточном web-сервере. Эти потоки все время посылают системные запросы, и ядру, перехватившему управление, чтобы выполнить системный запрос, не составит труда заодно переключить потоки, если один из них блокирован. При этом исключается необходимость постоянных обращений к системе с запросом select для проверки наличия или отсутствия блокировки у последующего запроса read. А для приложений, которые полностью ограничены возможностями процессора и редко блокируются, потоки вообще не нужны. Вряд ли кто-либо станет всерьез применять потоки для вычисления первых п простых чисел или игры в шахматы.
Реализация потоков в ядре
Теперь рассмотрим ситуацию, в которой ядро знает о существовании потоков и управляет ими. В этом случае система поддержки исполнения программ не нужна, как показано на рис. 2.9, б. Нет необходимости и в наличии таблицы потоков в каждом процессе, вместо этого есть единая таблица потоков, отслеживающая все потоки системы. Если потоку необходимо создать новый поток или завершить имеющийся, он выполняет запрос ядра, который создает или завершает поток, внося изменения в таблицу потоков.
Таблица потоков, находящаяся в ядре, содержит регистры, состояние и другую информацию о каждом потоке. Информация та же, что и в случае управления потоками на уровне пользователя, только теперь она располагается не в пространстве пользователя (внутри системы поддержки исполнения программ), а в ядре. Эта информация является подмножеством информации, которую традиционное ядро хранит о каждом из своих однопоточных процессов (то есть подмножеством состояния процесса). Дополнительно ядро содержит обычную таблицу процессов, отслеживающую все процессы системы.
Все запросы, которые могут блокировать поток, реализуются как системные запросы, что требует значительно больших временных затрат, чем вызов процедуры системы поддержки исполнения программ. Когда поток блокируется, ядро но желанию запускает другой поток из этого же процесса (если есть поток в состоянии готовности) либо поток из другого процесса. При управлении потоками на уровне пользователя система поддержки исполнения программ запускает потоки из одного процесса, пока ядро не передает процессор другому процессу (или пока не кончаются потоки, находящиеся в состоянии готовности).
Поскольку создание и завершение потоков в ядре требует относительно больших расходов, некоторые системы используют повторное использование потоков. После завершения поток помечается как нефункционирующий, но в остальном его структура данных, хранящаяся в ядре, не затрагивается. Позже, когда нужно создать новый поток, реанимируется отключенный поток, что позволяет сэкономить на некоторых накладных расходах. При управлении потоками на уровне пользователя повторное использование потоков тоже возможно, но поскольку накладных расходов, связанных с управлением потоками, в этом случае существенно меньше, то и смысла в этом меньше.
Управление потоками в ядре не требует новых не блокирующих системных запросов, более того, если один поток вызвал ошибку из-за отсутствия страницы, ядро легко может проверить, есть ли в этом процессе потоки в состоянии готовности. И запустить один из них, пока требуемая страница считывается с диска. Основным недостатком управления потоками в ядре является существенная цена системных запросов, поэтому постоянные операции с потоками (создание, завершение и т, п.) приведут к увеличению накладных расходов.
Смешанная реализация
С целью совмещения преимуществ реализации потоков на уровне ядра и на уровне пользователя были опробованы многие способы смешанной реализации. Один из методов заключается в использовании управления ядром и последующем мультиплексировании потоков на уровне пользователя, как показано на рис. 2.10.
В такой модели ядро знает только о потоках своего уровня и управляет ими. Некоторые из этих потоков могут содержать по нескольку потоков пользовательского уровня, мультиплексированных поверх них. Потоки пользовательского уровня создаются, завершаются и управляются так же, как потоки уровня пользователя в процессе, запущенном в не поддерживающей многопоточность системе. Предполагается, что у каждого потока ядра есть набор потоков на уровне пользователя, которые используют его по очереди.
Мультиплексирование потоков пользователя из потока ядра
Рис. 2.10. Мультиплексирование потоков пользователя в потоках ядра
Активация планировщика
Многие исследователи старались совместить преимущества реализации потоков на уровне ядра (простота реализации) и реализации потоков на уровне пользователя (высокая производительность). Ниже мы рассмотрим один из таких подходов, разработанный Андерсоном [12], который называется активацией планировщика. Соответствующие разработки описаны в [104, 296].
Целью активации планировщика является имитация функциональности потоков ядра, но с большей производительностью и гибкостью, свойственной потокам уровня пользователя. В частности, пользовательские потоки не должны выполнять специальные системные запросы без блокировки или заранее должны проверять, не вызовет ли запрос блокировку. Тем не менее, когда поток блокируется системным запросом или ошибкой из-за отсутствия страницы, должна оставаться возможность запустить другой поток этого же процесса (если такой есть и находится в состоянии готовности).
Увеличение эффективности достигается за счет уменьшения количества ненужных переходов между пространством пользователя и ядром. Например, если поток блокирован в ожидании действий другого потока, совершенно не обязательно обращаться к ядру, что позволяет избежать накладных расходов по переходу «пользователь-ядро». Система поддержки исполнения программ, работающая в пространстве пользователя, может блокировать синхронизирующий поток и самостоятельно выбрать другой.
При использовании активации планировщика ядро назначает каждому процессу некоторое количество виртуальных процессоров и позволяет системе поддержки исполнения программ (в пространстве пользователя) распределять потоки по процессорам. Этот метод можно использовать и в мультипроцессорной системе, заменяя виртуальные процессоры реальными. Исходное число виртуальных процессоров, соответствующих одному процессу, равно единице, но процесс может запросить больше процессоров и позже вернуть их. Ядро также может забрать виртуальный процессор у одного процесса и отдать другому, более нуждающемуся в нем в данный момент.
В основе механизма работы этой схемы лежит следующее утверждение. Если ядро знает, что поток блокирован (например, если он выполнил блокирующий системный запрос или вызвал ошибку из-за отсутствия страницы), ядро оповещает об этом систему поддержки исполнения программ процесса, пересылая через стек в качестве параметров номер потока в запросе и описание случившегося. Оповещение происходит при помощи активации ядром в определенном начальном адресе системы поддержки исполнения программ, что приблизительно аналогично сигналу в UNIX. Этот метод называется upcall («вызов вверх», также иногда именуемый обратным вызовом - callback - в противоположность обычным вызовам, производящимся из верхних уровней в нижние).