Реализации POSIX Threads API
Стандарт POSIX допускает различные подходы к реализации многопоточности в рамках одного процесса. Возможны три основных подхода:
- В пользовательском адресном пространстве, когда нити в пределах процесса переключаются собственным планировщиком
- Реализация при помощи системных нитей, когда переключение между нитями осуществляется ядром, так же, как и переключение между процессами.
- Гибридная реализация, когда процессу выделяют некоторое количество системных нитей, но процесс имеет собственный планировщик в пользовательском адресном пространстве. Как правило, при этом количество пользовательских нитей в процессе может превосходить количество системных нитей.
Пользовательские нити
Реализация планировщика в пользовательском адресном пространстве не представляет больших сложностей; наброски реализаций таких планировщиков приводятся во многих учебниках по операционным системам, в том числе в [Иртегов 2002]. Учебная многозадачная ОС Minix может быть собрана и запущена в виде задачи под обычной Unix-системой. Главным достоинством пользовательского планировщика считается тот факт, что он может быть реализован без изменений ядра системы.
При практическом применении такого планировщика, однако, возникает серьезная проблема. Если какая-то из нитей процесса исполняет блокирующийся системный вызов, блокируется весь процесс. Устранение этой проблемы требует серьезных изменений в механизме взаимодействия диспетчера системных вызовов с планировщиком операционной системы. То есть главное достоинство пользовательского планировщика при этом будет утеряно.
Другим недостатком пользовательских нитей является то, что они не могут воспользоваться несколькими процессорами на однопроцессорной машине – ведь процесс всегда планируется только на одном процессоре!
Наиболее известная реализация пользовательских нитей – это волокна (fibers) в
Win 32?. Считается, что волокна дешевле системных нитей
Win 32?, хотя данных, подтверждающих это утверждение практическими измерениями, у меня нет. Волокна могут использоваться совместно с системными нитями
Win 32?, но при этом волокна привязаны к определенной нити и исполняются только в контексте этой нити. Волокна не должны исполнять блокирующиеся системные вызовы; попытка сделать это приведет к блокировке нити. Некоторые системные вызовы
Win 32? имеют неблокирующиеся аналоги, предназначенные для использования в волокнах, но далеко не все. Это резко ограничивает применение волокон в реальных приложениях.
Системные нити
Ядро типичной современной ОС уже имеет планировщик, способный переключать процессы. Переделка этого планировщика для того, чтобы он мог переключать несколько нитей в пределах одного процесса, также не представляет больших сложностей. При этом возможны два подхода к такой переделке.
В рамках первого подхода, системные нити выступают как подчиненная по отношению к процессу сущность. Идентификатор нити состоит из идентификатора родительского процесса и собственного идентификатора нити. Идентификатор нити локален по отношению к процессу, т.е. нити разных процессов могут иметь одинаковые идентификаторы. Такой подход реализует большинство систем, реализующих системные нити – IBM MVS-OS/390/zOS, VAX/VMS-
Open VMS?, OS/2,
Win 32?, многие Unix-системы, в том числе и Solaris. В Solaris и многих других Unix-системах (IBM AIX, HP/UX) системные нити называются LWP (Light-Weight Process, «легкие процессы»).
Solaris 10 использует системные нити, так что каждой нити POSIX Threads API соответствует собственный LWP. Старые версии Solaris использовали гибридный подход, который рассматривается в следующем разделе.
В рамках другого подхода, системные нити являются сущностями того же уровня, что процесс. Иногда все, что объединяет нити одного процесса – это общее адресное пространство. Наиболее известная ОС, использующая такой подход – Linux. В Linux, нити выглядят как отдельные записи в таблице процессов и отдельные строки в выводе команд top(1) и ps(1), имеют собственный идентификатор процесса.
В старых версиях Linux это приводило к своеобразным проблемам при реализации POSIX Threads API; так, в большинстве Unix-систем завершение процесса системным вызовом exit(2) приводит к немедленному завершению всех его нитей; в Linux вплоть до 2.4 завершалась только текущая нить. В Linux 2.6 был внесен ряд изменений в ядро, приблизивших семантику многопоточности к стандарту POSIX. Эти изменения известны как NPT (Native POSIX Threads).
Наш курс рассчитан на стандартную семантику POSIX Threads API. При программировании для старых (2.4 и младше) версий ядра Linux необходимо изучить особенности поведения этих систем по документации, поставляющейся с системой, или по другим источникам.
Гибридная реализация
В гибридной реализации многопоточный процесс имеет несколько LWP и планировщик в пользовательском адресном пространстве. Этот планировщик переключает пользовательские нити между свободными LWP, подобно тому, как системный планировщик в многопроцессорной системе переключает процессы и системные нити между свободными процессами. При этом, как правило, процесс имеет больше пользовательских нитей, чем у него есть LWP.
Причина, по которой этот подход нашел практическое применение – это убеждение разработчиков первых многопоточных версий Unix, что пользовательские нити дешевле системных, требуют меньше ресурсов для своего исполнения.
При планировании пользовательских нитей возникает проблема блокирующихся системных вызовов. Когда какая-то нить вызывает блокирующийся системный вызов, соответствующий LWP блокируется и на некоторое время выпадает из работы. В старых версиях Solaris эта проблема решалась следующим образом: многопоточная библиотека всегда имела выделенную нить, которая не вызывала блокирующихся системных вызовов никогда. Когда ядро системы обнаруживало, что все LWP процесса заблокированы, оно посылало процессу сигнал SIGWAITING. Библиотечная нить перехватывала этот сигнал и, если это допускалось настройками библиотеки, создавала новый LWP.
Таким образом, если все пользовательские нити исполняли блокирующиеся системные вызовы, то количество LWP могло сравняться с количеством пользовательских нитей. Можно предположить, что от компания Sun отказалась от гибридной реализации многопоточности именно потому, что обнаружилось, что такое происходит со многими реальными прикладными программами.
В старых версиях Solaris поддерживался довольно сложный API, позволявший управлять количеством LWP и политикой планирования нитей между ними. Так, можно было привязать нить к определенному LWP. Этот API был частью Solaris Native Threads и нестандартным расширением POSIX Threads API. В рамках данного курса этот API не изучается.
Многие современные Unix-системы, в том числе IBM AIX, HP/UX, SCO
Unix Ware? используют гибридную реализацию POSIX Thread API.