Особенности поддержки различных многопроцессорных архитектур и соответствующих программных систем
Наличие в системе нескольких процессоров (процессорных ядер) может значительно увеличить общую производительность. При этом степень прироста производительности сильно зависит как от аппаратных особенностей, так и от алгоритмических. Так, далеко не каждый алгоритм может быть эффективно распараллелен.
В общем случае многопроцессорные системы могут иметь следующие формы:
На программно-аппаратном уровне поддержка многопроцессорности может включать несколько режимов работы:
Данный вид управления вычислительными ресурсами является одним из самых ранних, поскольку асимметричная многопроцессорность гораздо ближе к обычным однопроцессорным системам, чем к другим классам многопроцессорных конфигураций. В первую очередь она обеспечивает относительно простой способ портирования устаревшего кода и прямой механизм управления выделенным процессором.
ASMP может быть:
Из этих двух видов систем распределенная сетевая модель ЗОСРВ «Нейтрино» наилучшим образом сочетается именно с гомогенной многопроцессорной средой исполнения. Приложения, работающие на одном процессоре, могут прозрачно связываться с приложениями и системными сервисами (драйверами устройств, стеками протоколов, подсистемами) на другом процессоре без избыточной вычислительной нагрузки, связанной с традиционными средствами межпроцессорного взаимодействия.
Для построения гетерогенных систем необходимо либо реализовать собственный механизм взаимодействия, либо выбирать такие ОС, которые позволяют использовать стандартизованные механизмы коммуникации (скорее всего на основе стека протоколов TCP/IP). Чтобы избежать возникновения конфликтов доступа к ресурсам, эти операционные системы также должны предоставлять стандартизованный механизм доступа к разделяемым аппаратным компонентам.
В случае с ASMP, разработчик системы должен определить как организовать разделение аппаратных ресурсов, используемых приложениями и Операционными Системами, между процессорами. Обычно, подобное выделение ресурсов осуществляется статически во время загрузки системы и включает в себя распределение диапазонов физической памяти, использование устройств периферии и обработку прерываний. Хотя система может выделять ресурсы динамически, использование такого подхода приведет к усложнению координации между процессорами.
В ASMP системах процессы всегда исполняются на совокупности процессоров, к которым относится соответствующая ОС, даже если другие процессоры свободны (выполняют поток idle). В результате таким конфигурациям присущ систематический дисбаланс вычислительной нагрузки. Для решения этой проблемы, ОС может располагать встроенными средствами миграции из одного экземпляра родственной ОС в другой и, соответственно, миграции между процессорами. Однако, эти механизмы связаны со сложными практиками синхронизации состояний экземпляров программы, их прерыванием и возобновлением на другом процессоре. Сами по себе такие миграции достаточно сложны даже в пределах экземпляров одной и той же ОС. Если же требуется подобный механизм для принципиально различных систем, то эта задача многократно усложняется, если не становится неосуществимой.
Распределение ресурсов в многоядерных системах может оказаться сложной задачей, если одни программные компоненты не знают, как другие компоненты используют их. Симметричная многопроцессорность (SMP) решает эту проблему, позволяя исполнять общую копию Операционной Системы на всех процессорах. Поскольку единая ОС централизовано управляет всеми ресурсами системы, это позволяет распределять их между несколькими процессорами почти без вмешательства разработчика приложений. ЗОСРВ «Нейтрино» предоставляет обширный перечень встроенных механизмов синхронизации и IPC, включающих pthread_mutex_lock(), pthread_mutex_unlock(), pthread_spin_lock() и pthread_spin_unlock(). Эти примитивы одинаково эффективно функционируют как в однопроцессорных системах, так и в SMP и позволяют безопасно управлять ресурсами.
Выполнение единой Операционной Системы в режиме SMP позволяет динамически выделять ресурсы определенным приложениями, а не ассоциировать их с процессорами, расширяя, таким образом, возможности использования вычислительных ресурсов. Также, это позволяет встроенным инструментам трассировки осуществлять мониторинг и анализ всей многопроцессорной системы в целом, что дает разработчику ценную информацию для последующей отладки и оптимизации приложений.
Например, может отслеживаться миграция потоков между процессорами, системные вызовы, события планирования, передача сообщений между приложениями и другие события, осуществляя всё это с привязкой ко временным меткам.
Синхронизация приложений в SMP-системах также существенно упрощается благодаря возможности использования стандартных примитивов системной библиотеки, а не сложных специализированных механизмов IPC. Микроядро позволяет потокам одного приложения исполняться параллельно на нескольких процессорах, делая доступной для него всю имеющуюся вычислительную мощность многоядерной системы. Механизмы миграции, вытеснения и приоритетного планирования потоков позволяют быть уверенными в том, что процессорное время будет предоставлено в первую очередь именно тем потокам и процессам, которое нуждается в нем первостепенно.
ЗОСРВ «Нейтрино», являясь микроядерной ОС, выносит на уровень прикладных процессов, действующими в качестве менеджеров ресурсов, сервисы файловых систем, символьного ввода/вывода, сетевой стек и т.п. За счет выбора соответствующей версии микроядра SMP-режим обеспечивается автоматически для всех процессов без изменения программного кода. Возможность параллельного исполнения в этом случае смогут как различные процессы, так и отдельные потоки многопоточных приложений. Более того, даже однопоточные процессы получают неявные преимущества от функционирования в режиме симметричной многопроцессорности, так как их потоки будут иметь больше вычислительных ресурсов в единицу времени, а клиентские и серверные процессы получат возможность одновременного исполнения.
Версия микроядра с поддержкой симметричной многопроцессорности отличается наличием постфикса -smp в своем имени:
Версия ядра procnto-smp, конечно, будет также работать и на однопроцессорных системах с архитектурой x86.
Микроядро содержит небольшой объем платформо-зависимого кода. Код, который определяет возможности системы, выделен в модуль запуска системы startup-*, занимающийся системной инициализацией, детектированием доступной памяти и т.п. Накопленная информация размещается в таблице памяти, используемой микроядром и всеми процессами (только для чтения). startup-* модуль выполняет следующие функции:
![]() | Модуль startup-bios предназначен для работы в системах, совместимых со спецификацией Intel MP Spec версии 1.4 и выше. |
После сброса системы код перезапуска выполняет только один процессор. Этот процессор называется загрузочным процессором (Boot Processor, BP). Для каждого дополнительного обнаруженного процессора от имени загрузочного процессора модуль startup-* будет выполнять следующие действия:
Последовательность загрузочных действий для архитектур, отличных от x86, аналогична рассмотренной в предыдущем параграфе. Однако, для большинства архитектур и платформ используется специальная загрузочная программа (например, startup-mvp или startup-bcm1250) и конкретные функции могут слегка отличаться. В частности, программа начальной загрузки на устройстве с архитектурой PowerPC выполняет следующие действия:
Для каждого обнаруженного дополнительного процессора выполняются следующие действия:
После запуска дополнительных процессоров и их освобождения от цикла ожидания, все процессоры считаются равноправными и готовыми к планированию потоков.
Алгоритм планирования применяется в соответствии с теми же правилами, которые действуют в однопроцессорных системах. Это значит, что поток в состоянии READY с наивысшим приоритетом будет назначаться на исполнение на свободный процессор. Если возникает другой поток, готовый к выполнению с наивысшим приоритетом, он будет назначен соответствующему процессору. Если в качестве целевого может выступать более одного процессора, тогда микроядро стремится запустить поток на том процессоре, на котором он выполнялся до этого. Такой механизм мягкой привязки (soft affinity) используется для того, чтобы уменьшить число миграций потоков между процессорами, снижая тем самым накладные расходы на обеспечение когерентности локальных кэшей различных процессоров.
В SMP-системе планировщик может проявлять некоторую гибкость в принятии решения о перепланировании для того, чтобы оптимизировать использование кэшей и минимизировать миграцию потоков между процессорами. В нарушение одного из принципов планирования, в некоторых случаях возможен сценарий, когда готовый высокоприоритетный поток не будет назначаться на свободный процессор во время исполнения связанного с ним низкоприоритетного потока. Это также укладывается в логику обеспечения soft affinity потоков. Поясним это на примере одного из возможных сценариев.
Допустим, что в 2-х процессорной SMP-системе имеется лишь 2 потока, один из которых является низкоприоритетным сервером, а второй высокоприоритетным клиентом. В начальный момент времени сервер заблокирован на системном вызове MsgReceive(); клиент, исполняясь на нулевом процессоре, обращается к серверу с помощью MsgSend() и блокируется. Вполне очевидно, что в рамках логики мягкой привязки потоков, серверный поток целесообразно запустить на том же процессоре, на котором ранее исполнялся клиент. Очевидно, что требуется это из соображений наличия в локальном кэше нулевого процессора передаваемой клиентом порции данных. Сервер, разблокируясь (и наследуя приоритет клиента), обрабатывает полученный запрос и в конечном счете вызывает MsgReply(), которые не является блокирующим вызовом. В этот момент клиент также разблокируется и, по идее, должен быть отправлен на исполнение на первый процессор с копированием данных согласно процедуре согласования локальных кэшей процессоров. Следствием этого будет последующая миграция сервера вслед за клиентом в рамках отмеченного выше механизма. Очевидно, что такое развитие событий будет приводить к непрерывной миграции обоих потоков по процессорам.
Именно в рамках этого этапа межзадачного взаимодействия планировщик потоков позволяет себе некоторую оптимизирующую вольность. Вместо того, чтобы сразу же перевести клиентский поток в состояние READY, он позволяет серверу поработать еще несколько тактов процессорного времени, поскольку в подавляющем числе случаев он практически мгновенно заблокируется самостоятельно (см. псевдокод типичного серверного потока далее) и лишь во время блокировки серверного потока переводит клиента в состояние готовности:
void *server( ... ){while ( true ){recvid = MsgReceive( ... );...MsgReply( ... );}}
Это позволяет избежать совершенно неоправданной избыточной миграции связанных между собой потоков. Конечно же, сервер не обязан строго следовать указанному псевдокоду и по этой причине отпущенный ему квант времени "пост-обработки" сильно ограничен.
Можно заметить, что правила планирования в реальном времени, которые применяются на однопроцессорных системах, в полной мере применимы и в SMP-системах.
В однопроцессорной системе в каждый момент времени в микроядре может выполняться только один поток. Большинство операций, выполняемых ядром, имеют очень небольшую продолжительность. Микроядро разработано полностью вытесняемым и перезапускаемым для тех операций, которые требуют большого времени. Такая архитектура делает микроядро очень компактным и быстродействующим и не требует применения большого количества мелких блокировок. Стоит отметить, что использование множества блокировок в основном коде ядра заметно снижает его быстродействие, ведь каждая блокировка, как правило, требует обращения к процессорной шине, а это может приводить к остановке процессора.
В SMP-системах микроядро отвечает той же философии: в вытесняемом и перезапускаемом ядре должен находиться только один поток. Вход в ядро доступен для любого процессора, но только один из них может получить доступ к нему в каждый момент времени.
В большинстве систем время, расходуемое на выполнение кода микроядра, составляет незначительную долю от общей вычислительной нагрузки на процессор. Поэтому возникновение конфликтов становится, скорее, исключением, чем правилом. Это в особой степени относится к микроядру, в котором традиционные сервисы операционной системы (например, файловые системы), представляют собой отдельные процессы и не являются его частью.
Процессоры взаимодействуют между собой посредством межпроцессорных прерываний (Inter-Processor Interrupt, IPI). IPI позволяют эффективно осуществлять планирование и управление потоками на множестве процессоров. Например, межпроцессорное прерывание часто требуется в ситуациях, когда:
Для управления доступом к разделяемым структурам данных потоки и процессы используют стандартные примитивы синхронизации: мьютексы, условные переменные, семафоры и т.п. Эти примитивы одинаковым образом работают как на однопроцессорных, так и в многопроцессорных системах.
Достаточно часто возникает необходимость обеспечить синхронизацию доступа к разделяемым структурам данных между обработчиком прерывания и потоком, связанным с этим обработчиком. Традиционные примитивы синхронизации, используемые между потоками, не могут применяться в контексте обработчика прерываний (см. таблицу Безопасность использования в описаниях соответствующих вызовов системной библиотеки). Существует два решения этой проблемы:
Для организации второго решения в однопроцессорной системе достаточно обеспечить маскирование соответствующего прерывания в потоке:
InterruptMask( intr );// Критическая секция кодаInterruptUnmask( intr );
В SMP-системе, однако, этот код не будет иметь ожидаемого эффекта, поскольку поток и обработчик могут выполняться одновременно на разных процессорах.
![]() | В однопроцессорных систем вместо маскирования регулярно применялся следующий подход:
В SMP-системах это не является безопасным, поскольку реализовано на уровне полного отключения прерываний на уровне отдельно взятого процессора. |
Одним из корректных решений может стать явная привязка потока к тому же процессору, что и обработчик (см. параграф SMP-системы с ограниченной миграцией). Это, однако, сводит второе решение к первому.
Более эффективное решение заключается в том, чтобы использовать блокировку, доступную как для потока, так и для обработчика прерываний. Она обеспечивается посредством следующего примитива, доступного и в однопроцессорных и в многопроцессорных системах:
В однопроцессорных системах необходимости в использовании данного примитива синхронизации нет.
Данный вид управления ресурсами в SMP-системе позволяет жестко определить на каком процессоре определенный поток может быть исполнен. В противоположность механизму мягкой привязки здесь применяется жесткая привязка (hard affinity) потоков к процессорам, что уже не является компетенцией микроядра, а полностью зависит от разработчика системы и существенно ограничивает диапазон разрешенной миграции.
Жесткая привязка может быть осуществлена как в отношении одного конкретного потока (что позволяет успешно воспользоваться рассмотренными ранее вызовами InterruptMask() и InterruptUnmask()), так и в отношении всех. Привязка осуществляется по маске процессоров, что позволяет задействовать как один процессор, так и их произвольную группу.
В сравнении с полной свободой SMP-модели с неограниченной миграцией потоков, данный подход имеет следующие преимущества:
ЗОСРВ «Нейтрино» поддерживает жесткую привязку потоков путем назначения маски разрешенных процессоров (runmask). Каждый установленный в ней бит означает разрешенный для миграции потока процессор. По умолчанию, маска разрешенных процессоров заполняется единицами, что соответствует любому процессору. Значение 0x01
разрешает поток исполняться только на первом процессоре.
По умолчанию, потомки процесса или потока не наследуют runmask, для этого существует отдельная маска – inherit runmask.
Использование этих масок позволяет разработчику системы оптимизировать производительность системы. Так, например, можно назначить на отдельный процессор все процессы, которые не имеют требований реального времени (процессы общего назначения, не являющиеся системообразующими). В общем случае, однако, это является избыточным.
Маска разрешенных процессоров для нового потока или процесса может быть задана следующим образом:
SPAWN_EXPLICIT_CPU
в поле flags при вызове функции spawn().
Маска разрешенных процессоров существующего потока или процесса может быть установлена следующим образом:
_NTO_TCTL_RUNMASK
или _NTO_TCTL_RUNMASK_GET_AND_SET_INHERIT
.
Выбор программной организации многопроцессорной системы зависит в первую очередь от задач, которые требуется решить:
Как иллюстрирует следующая таблица, возможность выбора любой из этих моделей многопроцессорности позволяет добиться оптимального баланса между производительностью, масштабируемостью и совместимостью:
Особенности | SMP с неограниченной миграцией потоков | SMP с ограниченной миграцией потоков | ASMP |
---|---|---|---|
Прозрачное разделение ресурсов | Да | Да | – |
Масштабируемость | Да | Да | С ограничениями |
Перенос legacy-приложений | В большинстве случаев | Да | Да |
Использование нескольких ОС | – | – | Да |
Жесткая привязка к процессорам | – | Да | Да |
Обмен сообщениями между ядрами | Быстро (средствами ОС) | Быстро (средствами ОС) | Медленно (средствами приложения) |
Синхронизация потоков между CPU | Да | Да | – |
Балансировка нагрузки | Да | Да | – |
Общесистемная отладка | Да | Да | – |
Общесистемная оптимизация | Да | Да | – |
Предыдущий раздел: перейти