В операционной системе Solaris реализован необычный многоуровневый подход к управлению потоками, способствующий значительной гибкости использования процессорных ресурсов.
Многопоточная архитектура
В операционной системе Solaris используются четыре отдельные концепции, связанные с потоками.
- Процесс. Это обычный процесс UNIX, который включает в себя пользовательское адресное пространство, стек и управляющий блок процесса.
- Потоки на пользовательском уровне. Эти потоки реализуются с помощью библиотеки потоков в адресном пространстве процесса; они невидимы для операционной системы. Потоки на пользовательском уровне играют роль интерфейса для параллелизма приложений.
- Облегченные процессы. Облегченный процесс (lightweight process — LWP) можно рассматривать как отображение между потоками на пользовательском уровне и потоками ядра. Каждый из облегченных процессов поддерживает один или несколько потоков на пользовательском уровне и отображает их в один поток ядра. Планирование облегченных процессов производится ядром независимо. В многопроцессорной системе облегченные процессы могут выполняться параллельно на нескольких процессорах.
- Потоки ядра. Эти потоки являются фундаментальными элементами; планирование и выполнение каждого из них может осуществляться на одном из системных процессоров.
На рис. 4.15 проиллюстрирована взаимосвязь между этими четырьмя элементами. Заметим, что каждому облегченному процессу всегда соответствует один поток ядра. Облегченный процесс видим для приложения в рамках процесса. Таким образом, структуры данных облегченного процесса существуют в рамках адресного пространства соответствующего им процесса. В то же время каждый облегченный процесс связан с единственным диспетчеризуемым потоком ядра, а структуры данных этого потока ядра поддерживаются в адресном пространстве ядра.
Рис. 4.15. Пример многопоточной архитектуры операционной системы Solaris
В нашем примере процесс 1 состоит из одного потока на пользовательском уровне, связанного с одним облегченным процессом. Таким образом, выполняется один поток, что соответствует процессу в традиционной системе UNIX. Приложение использует такую структуру процесса, если для него не нужен параллелизм. Процесс 2 соответствует стратегии, в которой применяются только потоки на пользовательском уровне. Все эти потоки поддерживаются одним потоком ядра, поэтому в каждый момент времени может выполняться только один поток на пользовательском уровне. Такая структура полезна тогда, когда приложение лучше всего программировать с применением параллелизма, но не используя при этом параллельное выполнение нескольких потоков. В процессе 3 имеется несколько потоков, которым соответствует меньшее количество облегченных процессов. Вообще говоря, в операционной системе Solaris потоки на пользовательском уровне можно отображать в такое же или меньшее количество облегченных процессов. Это позволяет приложению указывать степень параллелизма на уровне ядра, которая будет поддерживаться для данного процесса. В процессе 4 реализовано взаимно однозначное соответствие между потоками и облегченными процессами. В такой структуре параллелизм на уровне ядра является полностью видимым для приложения. В процессе 5 демонстрируется комбинированный подход. В нем несколько потоков на пользовательском уровне отображаются в несколько облегченных процессов, и вместе с этим один облегченный процесс закреплен за одним потоком на пользовательском уровне.
На рисунке отсутствует изображение потоков ядра, не имеющих связи с облегченными процессами. Эти потоки создаются, запускаются и удаляются ядром для выполнения с их помощью определенных системных функций. Реализация системных функций с помощью потоков ядра вместо процессов ядра снижает накладные расходы по переключению в ядре (переключение потоков требует меньше ресурсов, чем переключение процессов).
Мотивация
Совместное использование потоков на пользовательском уровне и на уровне ядра дает возможность программисту выбрать вариант параллелизма, который будет наиболее эффективным для данного приложения и подойдет для него больше других.
В некоторых программах используется логический параллелизм, который позволяет упростить и структурировать код, но для которого не нужен аппаратный параллелизм. Например, приложение со многими окнами, из которых только одно является активным, можно эффективно реализовать в виде набора потоков на пользовательском уровне, которым отвечает один облегченный процесс. Преимущество использования для таких приложений только потоков на пользовательском уровне заключается в их эффективности. Потоки на пользовательском уровне можно создавать, уничтожать, блокировать, активизировать и т.д. без участия ядра. Если бы ядру было известно о каждом потоке на пользовательском уровне, оно должно было бы размещать структуры данных для каждого потока, а также выполнять переключение потоков. Как мы видели в табл. 4.1, при переключении потоков на уровне ядра потребляется больше ресурсов, чем при переключении потоков на пользовательском уровне.
Если в приложение входят потоки, которые могут быть блокированы, например, при выполнении ввода-вывода, то заманчиво использовать несколько облегченных процессов, поддерживающих такое же или большее количество потоков на пользовательском уровне. Чтобы в рамках этого же процесса могли выполняться и другие потоки, не нужно никакого вмешательства приложения или библиотеки потоков. Если один из потоков процесса будет блокирован, другие его потоки могут выполняться в оставшихся облегченных процессах.
В некоторых приложениях эффективным является взаимно однозначное отображение потоков на уровне ядра и облегченных процессов. Например, можно реализовать такое параллельное вычисление элементов массива, чтобы элементы, стоящие в разных строках, вычислялись в разных потоках. Если каждому потоку на пользовательском уровне соответствует свой облегченный процесс, то переключать потоки при вычислениях не потребуется.
Для некоторых приложений подходит смешанное использование потоков, постоянно связанных с облегченными процессами, и несвязанных потоков (при этом несколько облегченных процессов совместно используются несколькими потоками). Например, при реализации прикладной системы реального времени программист может захотеть, чтобы одни потоки обладали приоритетом в масштабе всей системы и планировались в реальном времени, а другие — выполняли функции в фоновом режиме и могли совместно использовать один или небольшое количество облегченных процессов.
Структура процессов
На рис. 4.16 приводится общее сравнение структуры процессов в традиционной операционной системе UNIX со структурой процессов в операционной системе Solaris. В типичных реализациях UNIX в структуру процесса входят такие составляющие: идентификатор процесса, идентификаторы пользователя, таблица диспетчеризации сигналов; дескрипторы файлов, схема распределения памяти, структура состояния процессора. В операционной системе Solaris эта базовая структура остается, но в ней блок состояния процессора заменен списком структур, в котором для каждого облегченного процесса имеется свой блок данных.
Рис. 4.16. Структура процесса в UNIX и в Solaris 2.x [LEWI96]
В структуру данных облегченного процесса входят такие элементы:
- идентификатор облегченного процесса;
- приоритет данного облегченного процесса (и, следовательно, потока ядра, который его поддерживает);
- маска сигналов, предоставляющая ядру информацию о том, какие сигналы могут быть восприняты процессом;
- сохраненные значения регистров пользовательского уровня (когда облегченный процесс не выполняется);
- стек ядра данного облегченного процесса, в который входят аргументы системного вызова, результаты и коды ошибок каждого уровня;
- данные по использованию ресурсов и профилированию;
- указатель на соответствующий поток ядра;
- указатель на структуру процесса.
Выполнение потоков
На рис. 4.17 показана облегченная схема состояний выполнения потоков на пользовательском уровне и облегченных процессов. Выполнением потоков на пользовательском уровне управляет библиотека потоков. Рассмотрим сначала несвязанные состояния, т.е. состояния, которые совместно используют несколько облегченных процессов. Несвязанный поток может находиться в одном из четырех состояний: работоспособный, активный, ожидания или остановленный. Если поток на пользовательском уровне находится в активном состоянии, он закрепляется за облегченным процессом и выполняется до тех пор, пока выполняется лежащий в его основе поток ядра. Поток на пользовательском уровне может выйти из активного состояния по нескольким причинам. Рассмотрим активный поток Т1 на пользовательском уровне. Возможны такие события.
- Синхронизация. Чтобы скоординировать свои действия с другими потоками, поток Т1 вызывает один из примитивов, которые обсуждаются в главе 5, "Параллельные вычисления: взаимоисключения и многозадачность", а сам переходит в состояние ожидания. После выполнения условия синхронизации поток Т1 переходит в работоспособное состояние.
- Приостановка. Любой из потоков (включая поток Т1) может стать причиной приостановки потока Т1 и его перехода в состояние остановки. Поток Т1 остается в этом состоянии, пока другой поток не сгенерирует запрос на продолжение его выполнения, который и переводит остановленный поток в работоспособное состояние.
- Вытеснение. Активный поток (Т1 или какой-то другой) выполняет некоторое действие, благодаря которому становится работоспособным другой поток (Т2) с более высоким приоритетом. Если Т1 является активным потоком с наиболее низким приоритетом, он вытесняется и переходит в состояние готовности, а поток Т2 закрепляется за освободившимся облегченным процессом.
- Уступка. Если поток Т1 выполняет библиотечную команду thr_yield (), планировщик потоков этой библиотеки проверяет, есть ли другой работоспособный поток (Т2) с тем же приоритетом. Если такой поток есть, Т1 переходит в работоспособное состояние, а поток Т2 закрепляется за освободившимся облегченным процессом. В противном случае продолжает выполняться поток Т1.
Рис. 4.17. Состояния потоков и облегченных процессов в Solaris
Во всех перечисленных выше случаях при выходе потока Т1 из активного состояния библиотека потоков выбирает другой несвязанный поток в работоспособном состоянии и запускает его на освободившемся облегченном процессе.
На рис. 4.17 показана также диаграмма состояний облегченных процессов. Эту диаграмму можно рассматривать как детализацию активного состояния потока на пользовательском уровне, так как несвязанному потоку облегченный процесс назначается только тогда, когда он находится в активном состоянии. Диаграмма состояний облегченного процесса почти не требует пояснений. Активный поток выполняется лишь тогда, когда его облегченный процесс находится в состоянии выполнения. Если активный поток делает блокирующий системный вызов, его облегченный процесс переходит в состояние блокировки. Однако на пользовательском уровне этот поток остается связанным с данным облегченным процессом до тех пор, пока библиотека потоков оставляет его в активном состоянии.
Взаимосвязь связанных потоков с облегченными процессами несколько другая. Например, если связанный поток на пользовательском уровне переходит в состояние ожидания наступления синхронизирующего события, его облегченный процесс тоже должен перестать выполняться.
Прерывания в роли потоков
В большинстве операционных систем приняты две основные формы асинхронной деятельности: процессы и прерывания. Процессы (или потоки) взаимодействуют друг с другом и управляют использованием совместных структур данных с помощью различных примитивов, обеспечивающих взаимоисключения (когда в каждый момент времени только один процесс может выполнять определенный код или осуществлять доступ к определенным данным) и синхронизирующих их выполнение. Прерывания синхронизируются путем их предотвращения на некоторое время. В операционной системе Solaris эти две концепции объединяются в одной модели потоков ядра; прерывания в такой модели преобразуются в потоки ядра.
Эти преобразования выполняются для сокращения накладных расходов. Обработчики прерываний часто манипулируют данными, которые используются совместно с остальной частью ядра. Поэтому во время работы процедуры ядра, осуществляющей доступ к этим данным, прерывания должны быть заблокированы, даже если большинство прерываний не оказывают влияния на эти данные. Обычно для этого приходится повышать уровень приоритета прерываний, чтобы блокировать прерывания на время выполнения подпрограммы. После завершения подпрограммы уровень приоритета понижается. Все эти операции отнимают время. В многопроцессорной системе проблема усиливается. Ядро должно защищать большее количество объектов, и ему может понадобиться блокировать прерывания на всех процессорах.
Решение, принятое в операционной системе Solaris, выглядит так.
- Для обработки прерываний в системе Solaris используются потоки ядра. Как и любой другой поток ядра, поток прерывания обладает своим собственным идентификатором, приоритетом, контекстом и стеком.
- Ядро управляет доступом к структурам данных и синхронизирует потоки прерываний с помощью примитивов взаимоисключений (рассматривающихся в главе 5, "Параллельные вычисления: взаимоисключения и многозадачность"). Таким образом, для обработки прерываний используются обычные методы синхронизации потоков.
- Потокам прерываний присваиваются более высокие приоритеты, чем всем другим типам потоков ядра.
Если происходит прерывание, оно передается определенному процессору, а выполняющийся на этом процессоре поток закрепляется. Закрепленный поток не может перейти на другой процессор; его контекст сохраняется, и процесс приостанавливается до тех пор, пока не будет обработано прерывание. После этого процессор приступает к выполнению потока прерывания. В наличии всегда имеется запас деактивированных потоков прерываний, так что новый поток создавать не нужно. Затем исполняется поток, в котором происходит обработка прерывания. Если программе обработки понадобится доступ к структуре данных, которая каким-то образом заблокирована и используется другим потоком, поток прерывания должен ждать. Поток прерывания может быть вытеснен только другим потоком прерывания с более высоким приоритетом.
Опыт использования потоков прерываний в операционной системе Solaris свидетельствует о том, что такой подход обеспечивает производительность, превосходящую производительность традиционных методов обработки прерываний [KLEI95].