### Мультипроцессор с общей памятью (shared-memory multiprocessor)
Мультипроцессор — компьютерная система, в которой два и более центральных про- цессора имеют полный доступ к общей оперативной памяти. Программа, запущенная на любом из центральных процессоров, видит обычное виртуальное пространство (имеющее, как правило, страничную организацию). Единственное необычное свойство, присущее этот системе, заключается в том, что центральный процессор может записать какое-нибудь значение в слово памяти, а затем считать это слово и получить другое значение (потому что другой центральный процессор его уже изменил). При должной организации это свойство формирует основу для межпроцессорного обмена данными: один центральный процессор записывает какие-нибудь данные в память, а другой их считывает из памяти.
Хотя у всех мультипроцессоров имеется свойство, позволяющее каждому центральному процессору обращаться ко всему пространству памяти, у некоторых мультипроцессоров есть еще одно свойство: каждое слово памяти может быть считано так же быстро, как и любое другое слово памяти. Такие машины называются UMA-мультипроцессорами (Uniform Memory Access — однородный доступ к памяти). В противоположность им NUMA-мультипроцессоры (Nonuniform Memory Access — неоднородный доступ к памяти) этим свойством не обладают.
Простейшие мультипроцессоры (рис. 8.2, а) основаны на использовании общей шины. Два и более центральных процессора и один и более модулей памяти используют для обмена данными одну и ту же шину. Когда центральному процессору нужно считать слово памяти, он сначала проводит проверку занятости шины. Если шина не занята, центральный процессор выставляет на ней адрес нужного ему слова, подает несколько управляющих сигналов и ждет, пока память не выставит нужное слово на шину.
Если шина занята, то центральный процессор, которому нужно считать слово или за- писать его в память, просто ждет, пока шина освободится. Именно в этом и заключается проблема такой архитектуры. При наличии двух или трех центральных процессоров спор за шину будет вполне управляемым, чего нельзя сказать о 32 или 64 процессорах. Система будет полностью ограничена пропускной способностью шины, а основная масса центральных процессоров будет простаивать большую часть времени.
### Мультипроцессоры NUMA
Число центральных процессоров для мультипроцессоров UMA с общей шиной огра- ничено, как правило, несколькими десятками, а мультипроцессоры с координатной или многоступенчатой коммутацией нуждаются в большом количестве дорогостоящей аппаратуры, и количество центральных процессоров в них ненамного больше.Чтобы задействовать более сотни центральных процессоров, нужно чем-то пожертвовать. Обычно в жертву приносится идея одинакового времени доступа ко всем модулям па- мяти. Эта уступка приводит к вышеупомянутой концепции мультипроцессоров NUMA.
NUMA-машины обладают тремя ключевыми характеристиками, которые присущи им всем и которые в совокупности отличают их от других мультипроцессоров:
1. В области видимости всех центральных процессоров находится единое адресное пространство.
2. Доступ к удаленной памяти осуществляется с помощью команд LOAD и STORE.
3. Доступ к удаленной памяти осуществляется медленнее, чем доступ к локальной.
Когда время доступа к удаленной памяти не скрыто (по причине отсутствия кэши- рования), система называется NC-NUMA (No Cache-coherent NUMA — NUMA без согласованного кэширования). При наличии согласованной кэш-памяти система назы- вается CC-NUMA (Cache-Coherent NUMA — NUMA с согласованным кэшированием).
В настоящее время самым популярным подходом при создании больших мультипро- цессоров CC-NUMA является мультипроцессор на основе каталогов (directory-based multiprocessor). Идея заключается в ведении базы данных, сообщающей, где находится каждая кэш-строка и каково ее состояние. При обращении к кэш-строке запрашива- ется база данных, чтобы определить, где она находится и какая информация в ней содержится — неизмененная или измененная. Поскольку эта база данных должна за- прашиваться при выполнении каждой команды, обращающейся к памяти, она должна поддерживаться исключительно быстродействующей специализированной аппарату- рой, откликающейся за доли такта шины
### Многоядерные микропроцессоры
Наряду с тем, что центральные процессоры могут иметь, а могут и не иметь общие мо- дули кэш-памяти (см. рис. 1.8), оперативную память они всегда используют совместно, и эта память обладает согласованностью в том смысле, что у каждого слова памяти всегда имеется однозначное значение. Для поддержки согласованности специальная схема обеспечивает режим работы, при котором изменение одним из центральных про- цессоров слова, присутствующего в двух или более кэшах, приводит к автоматическому и атомарному удалению его из всех кэшей. Этот процесс известен как отслеживание (snooping).
Еще одна проблема, связанная с реально большим количеством ядер, заключается в том, что оборудование, необходимое для сохранения согласованности их кэшей, становится очень сложным и весьма дорогостоящим. Многие инженеры беспокоятся
о том, что согласованность кэшей не сможет масштабироваться на многие сотни ядер. Некоторые даже становятся сторонниками того, что все мы вообще должны отказать- ся от этой затеи. Они опасаются, что стоимость протоколирования согласованности в оборудовании будет столь высока, что все эти великолепные новые ядра не помогут существенно поднять производительность, потому что процессор будет слишком за- нят поддержанием кэшей в согласованном состоянии. Хуже того, в связи с этим нужно будет потратить слишком много памяти на быстродействующий каталог. Эта проблема известна под именем барьера согласованности (coherency wall).
### Симметричные мультипроцессоры
В памяти присутствует только одна копия операционной системы, но ее может запустить любой центральный процессор. При осуществлении системного вызова центральный процес- сор, на котором произошел системный вызов, переходит в режим ядра и обрабатывает этот системный вызов. Модель SMP показана на рис. 8.9.
В этой модели баланс процессов и памяти осуществляется в динамическом режиме, по- скольку используется только один набор таблиц операционной системы. Узкое место, связанное с главным центральным процессором, за неимением такового также отсут- ствует, но возникают новые проблемы. В частности, если два или более центральных процессора запускают код операционной системы в одно и то же время, может произой- ти авария. Представьте, что два центральных процессора одновременно выбирают для работы один и тот же процесс или требуют одну и ту же свободную страницу памяти. Простейший способ обхода этих проблем заключается в связывании с каждой опера- ционной системой мьютекса (то есть блокировки), что превращает всю систему в одну большую критическую область. Когда центральному процессору требуется запустить код, он в первую очередь должен получить мьютекс. Если мьютекс заблокирован, про- цессор просто переходит в режим ожидания. Таким образом, в любой момент времени выполнять код операционной системы может любой, но только один центральный процессор. Такой подход носит название большой блокировки ядра (big kernel lock).
Эта модель работает не намного лучше модели «главный — подчиненный». Предпо- ложим еще раз, что 10 % всего рабочего времени тратится на выполнение кода опера- ционной системы. Если используются 20 центральных процессоров, то из них может выстроиться длинная очередь ожидающих доступа к коду операционной системы. К счастью, улучшить ситуацию довольно просто. Многие части операционной систе- мы независимы друг от друга. Например, если один центральный процессор запустит планировщик, другой в это же время займется обработкой системного вызова, связан- ного с файлом, а третий параллельно с этим будет обрабатывать ошибку отсутствия страницы, то проблем не возникнет.
Благодаря этому операционную систему можно разбить на несколько независимых критических областей, не взаимодействующих друг с другом. Каждая критическая об- ласть защищается собственным мьютексом, поэтому исполнять ее код в любой момент времени может только один из центральных процессоров. Таким образом можно достичь большей параллельности в работе. Но может сложиться ситуация, что некоторые табли- цы, например таблица процессов, используются программным кодом сразу в нескольких критических областях. Например, таблица процессов нужна планировщику, но она же нужна и обработчику системного вызова fork, а также нужна для обработки сигнала. Каждая таблица, которая может понадобиться коду сразу нескольких критических об- ластей, нуждается в собственном мьютексе. При этом в каждый момент времени код каж- дой критической области может исполняться только одним центральным процессором и к каждой критической таблице может обращаться только один центральный процессор.
Такая система используется большинством современных мультипроцессоров. Слож- ности создания операционной системы для такой машины связаны отнюдь не с тем, что создаваемый код сильно отличается от кода обычной операционной системы, а с тем, что разбить операционную систему на критические области, которые могут исполняться параллельно разными центральными процессорами без причинения друг другу каких-либо даже малейших косвенных проблем, довольно трудно. Кроме этого, каждая таблица, которая используется кодом в двух и более критических областях, должна иметь отдельную мьютексную защиту, и все фрагменты кода, использующие таблицу, должны использовать мьютекс корректно.
Более того, нужно приложить массу усилий, чтобы избежать взаимных блокировок. Если коду сразу в двух критических областях нужны таблица A и таблица B и один из фрагментов кода первым затребовал A, а другой первым затребовал B, то рано или поздно произойдет взаимная блокировка и никто не поймет почему. Теоретически всем таблицам можно присвоить целочисленное значение, и программный код во всех критических об- ластях может быть написан с учетом требования запроса таблиц по порядку возрастания их номеров. Эта стратегия позволит избежать взаимных блокировок, но потребует от программиста тщательно взвесить, какие таблицы кодом каких критических областей будут востребованы, и обеспечить выдачу запросов в правильном порядке.
Со временем вносимые в систему изменения могут привести к тому, что коду в кри- тической области потребуется ранее не нужная таблица. Если программист не в курсе прежних разработок и не понимает всей логики работы системы, то он будет стремиться лишь к захвату мьютекса таблицы в нужный момент и к его освобождению, когда на- добность в таблице исчезнет. Несмотря на кажущуюся логичность его устремлений, они могут привести к взаимным блокировкам, которые пользователями будут восприняты как зависание системы. Разобраться в этой ситуации не так-то просто, а удерживать ее под контролем многие годы в условиях смены программистов и того труднее.
### Синхронизация мультипроцессоров
Центральные процессоры, входящие в состав мультипроцессора, часто нуждаются в синхронизации. Только что была рассмотрена ситуация, при которой критические об- ласти ядра и таблицы должны быть защищены мьютексами. А теперь мы присмотримся к тому, как эта синхронизация работает в мультипроцессоре. Вскоре станет ясно, что здесь далеко не все так просто.
Для начала надо будет выбрать правильные примитивы синхронизации. Если про- цесс на однопроцессорной системе осуществляет системный вызов, который требует доступа к некой критической таблице, находящейся в ядре, то программный код ядра может до предоставления доступа к таблице просто запретить прерывания. Но на муль- типроцессорной системе запрет прерываний воздействует только на тот центральный процессор, который их и запретил. Остальные центральные процессоры продолжат свою работу и все равно смогут получить доступ к критической таблице. Следова- тельно, требуется подходящий мьютекс-протокол, соблюдаемый всеми центральными процессорами и гарантирующий работу взаимного исключения.
Основой любого практического мьютекс-протокола является специальная команда процессора, позволяющая провести проверку и установку значения за одну неделимую операцию. На рис. 2.16 был показан пример использования команды TSL (Test and Set Lock — проверить и установить блокировку) при реализации критических областей.
Было рассмотрено, что эта команда считывает слово памяти в регистр процессора. Одновременно она записывает 1 (или другое ненулевое значение) в слово памяти. Конечно, для выполнения операций чтения из памяти и записи в память требуются два цикла обращения к шине. На однопроцессорной машине команда TSL работает так, как и ожидалось, поскольку команда процессора не может быть прервана на полпути.
При правильных реализации и использовании команды TSL гарантируется работо- способное взаимное исключение. Но этот метод взаимной блокировки использует спин-блокировку (spin lock), потому что запрашивающий центральный процессор просто находится в коротком цикле, с максимально возможной скоростью тестируя блокировку. При этом совершенно напрасно тратится время запрашивающего цен- трального процессора (или процессоров), и, кроме того, может быть также слишком нагружена шина или память, существенно замедляя нормальное функционирование других центральных процессоров.
На первый взгляд может показаться, что конкуренция в борьбе за шину должна быть устранена за счет кэширования, но это не так. Теоретически, как только запрашива- ющий центральный процессор считал слово блокировки, он должен получить его копию в своем кэше. Пока ни один из других центральных процессоров не пытается использовать блокировку, запрашивающий центральный процессор должен обладать способностью выходить за пределы своего кэша. Когда центральный процессор, вла- деющий блокировкой, делает запись в слово, чтобы снять блокировку, протокол кэша автоматически аннулирует все копии этого слова в удаленных кэшах, требуя извлечь заново правильное значение.