Что делает центральный процессор, когда ему нечего делать
Мужик приходит устраиваться работать на стройку. Его спрашивает мастер:
— Что делать умеешь?
— Могу копать…
— А что еще?
— Могу не копать…
Не секрет, что современные процессоры работают очень быстро. Работа их заключается в постоянном извлечении из памяти инструкций и выполнения предписанных в них действий. Однако оказывается, по тем или иным причинам часто требуется притормозить этот процесс. В прикладных программах редко приходится задумываться о том, что при этом происходит с процессором. Но вот для создателей системного софта это далеко не праздный вопрос.
Неактивным процессор может быть не только для экономии энергии, но и в результате возникновения особых ситуаций, в процессе выполнения протоколов инициализации или как итог намеренных действий системных программ. Почему это интересно? При написании программных моделей (в том числе виртуальных машин) компьютерных систем, необходимо корректно моделировать переходы между состояниями виртуальных процессоров. В работе системных программ регулярно возникают ситуации, когда по тем или иным причинам ЦПУ должен «притормозить». Умение корректно использовать и моделировать эти ситуации зависит от знания и понимания спецификаций.
В статье фокус делается на программной стороне вопроса состояний процессора. Я не буду концентрироваться на деталях реализации (напряжения, пины, частоты и т.д.), так как 1) они существенно различаются между поколениями и моделями процессоров даже одной архитектуры, тогда как программный интерфейс остаётся обратно совместимым; 2) они не видны напрямую программам и ОС. Это попытка просуммировать информацию, разбросанную по многим страницам справочника Intel IA-32 and Intel 64 Software Developer Manual.
Начнём с простой и всем знакомой ситуации — процессор включён, бодр и весел.
Активное состояние
Самое обычное состояние процессора, в котором он продолжает исполнять инструкции одну за другой. При этом современные процессоры могут динамически варьировать частоту своего тактового генератора для нужд управления энергопотреблением. Используя принятую терминологию, в активном режиме логический процессор остаётся в состоянии C0, но может изменять P-состояния.
Частично этим процессом можно управлять программно, из BIOS, ОС или прикладных программ. Однако последнее слово в управлении при этом остаётся вне контроля программ, запущенных на центральном процессоре.
Во всех остальных режимах, описываемых дальше, процессор не исполняет инструкции.
HLT
Первый из неактивных режимов, появившихся ещё в родоначальнике серии Intel 8086, связан с одноимённой инструкцией процессора. Исполнив эту инструкцию, процессор останавливает свою работу, уже не исполняя следующую команду. Начиная с Intel 80486 DX4 в этом режиме энергопотребление ЦПУ уменьшается по сравнению с активным режимом. Как конкретно это делается — зависит от реализации.
Сам по себе выйти из подобного сонного состояния процессор не может. Требуется внешнее событие. Это может быть обычное прерывание от устройства, немаскируемое прерывание (NMI), прерывание системного режима (SMI) или же варианты инициализирующих сигналов — INIT или RESET.
Да, если выполнить HLT в режиме SMM (system management mode), в котором по умолчанию блокируются все прерывания и немаскируемые прерывания. После этого только RESET сможет вновь запустить обработку машинных команд.
Формально режим после HLT обозначается как C1.
MWAIT и другие энергосберегающие режимы
Идея с особым режимом для энергосбережения центрального процессора получила дальнейшее развитие в виде новой инструкции MWAIT. В отличие от HLT, которая не имеет операндов, MWAIT принимает два значения в регистрах EAX и ECX. При этом в EAX содержится описание желаемого энергосберегающего состояния, численные значения для C-state и C-substate.
Регистр ECX определяет необязательные подсказки (hints) для указанного в команде варианта неактивного режима. В настоящий момент описывается только один такой хинт — флаг в нулевом бите. О его назначении будет сказано чуть ниже.
В остальном поведение процессора после исполнения аналогично HLT: процессор останавливает работу до прибытия внешних сигналов. В отличие от HLT, достигаемая в случае MWAIT экономия энергии может быть больше. Если HLT — это состояние C1, то с помощью MWAIT можно запросить переход процессора в более глубокий сон — состояния C2, C3… C6 и т.д. Каждое такое состояние может иметь под-состояния. Конкретные допустимые комбинации варьируются, и для конкретной модели процессора описываются в пятом листе инструкции CPUID.
Кроме тонкого управления энергопотреблением неактивного состояния, более интересное назначение MWAIT состоит в том, что она повышает эффективность синхронизационных процессов на многопроцессорных системах.
Типичная ситуация в параллельных алгоритмах: поток А ожидает сигнала о готовности от потока Б, после чего оба они могут продолжить вычисления. В многопроцессорных системах А и Б будут исполняться на разных логических процессорах. Каким образом можно передать этот сигнал? Два варианта:
-
Поместить А в неактивный режим (например, с помощью HLT). Затем Б использует межпроцессорное прерывание, которое выводит А из состояния сна. Однако посылка и обработка такого прерывания довольно дорога в терминах времени, т.к. она потребует нескольких переходов между режимами ядра и пользователя, да и путь сигнала прерывания будет неблизким.
- Поток А в «бесконечном» цикле проверяет содержимое некоторой ячейки памяти. Поток Б, желающий послать сообщение о готовности, записывает новое значение в эту ячейку, что выводит А из цикла. В этом случае задержка доставки сообщения меньше. Однако очевидно, что А проводит ожидание не самым энергоэффективным образом, сжигая такты, но не продвигаясь вперёд.
MWAIT в паре с инструкцией MONITOR призвана устранить недостаток второго подхода. Команда MONITOR принимает адрес в памяти в качестве своего аргумента, после чего процессор начинает «мониторить» его, ожидая записи из других потоков. Если такая запись произойдёт в то время, пока процессор находится в сонном состоянии из-за MWAIT, то он будет выведен из него.
Таким образом, состояние сна, созданное с помощью MWAIT, может быть прервано по двум причинам: внешние прерывания или запись в ячейку памяти, помеченную с помощью MONITOR. Но что будет, если прерывания были запрещены на момент исполнения MWAIT?
В первых реализациях MONITOR/MWAIT прибытие прерывания не привело бы к выходу из состояния сна. Оказалось, что такое поведение не очень удобно. Поэтому на современных процессорах MWAIT реализует расширение, включаемое с помощью бита ECX[0], которое позволяет даже запрещённым прерываниям выводить процессор из неактивного состояния.
Хочу подчеркнуть несколько «необязательный» характер поведения MWAIT. Выход из неактивного состояния может происходить по различным, не всегда контролируемым текущим приложением причинам. Программы, использующие её, должны быть спроектированы так, чтобы корректно работать, даже если выходы из сонного состояния будут происходить спонтанно. Поэтому в первом приближении MWAIT можно считать вариантом NOP — ничего не делающей инструкции. Это довольно типично для синхронизационных примитивов класса условная переменная (conditional variable). Алгоритмы, их использующие, обязаны корректно работать в условиях возможности паразитных пробуждений.
На этом закончим с функциональностью управления энергопотреблением. Перейдём к особенностям работы процессора в первые и последние моменты перед его включением и перезагрузкой. Как оказывается, при этому он также может находиться в неактивных режимах.
Wait-for-SIPI
Эта довольно неловкое название расшифровывается как «ожидание сигнала SIPI». SIPI, в свою очередь, является аббревиатурой для «Start-up IPI». Наконец, IPI — это «inter-processor interrupt», межпроцессорное прерывание. Чтобы понять, зачем было введено состояние wait-for-SIPI, нужно иметь общее представление о том, как происходит инициализация в многопроцессорной системе. Проблема следующая: если все ядра, потоки и процессоры после включения питания бросятся исполнять один и тот же загрузочный код, то наступит бардак. В общих чертах довольно сложный и варьирующийся в деталях на разных платформах процесс можно описать следующим образом.
-
После включения питания все логические процессоры включаются в гонку, в результате которой определяется один главный, т.н. загрузочный
процессор (boot-strap processor, BSP). Все остальные процессоры обозначаются как прикладные процессоры (application processor, AP). -
BSP начинает исполнять загрузочный код из ROM по адресу 0xfffffff0.
- Все AP переводятся в режим wait-for-SIPI, ожидая, когда BSP пришлёт им SIPI. Это произойдёт тогда, когда критическая часть системной инициализации будет проведена с помощью кода, исполненного на BSP: построение ACPI-таблиц в памяти, назначение уникальных идентификаторов APIC ID. Альтернативно, BSP может никого больше и не будить, если, скажем, многопроцессорность была вручную выключена в BIOS.
В состоянии wait-for-SIPI процессор не исполняет инструкции. Кроме того, он игнорирует внешние прерывания от устройств, сигналы INIT и NMI, задерживает SMI-прерывания. Фактически, единственное, что должно выводить его из этого состояния — это сигнал SIPI. Отмечу, что спецификации ничего не говорят про энергопотребление в этом режиме.
Хочу отметить, что при дальнейшей загрузки системы, все AP могут снова быть выключены и включены несколько раз. Например, загрузчик ОС может быть написан только для одного потока, да и сами ОС обычно предпочитают вводить процессоры в бой по одному. При этом состояние wait-for-SIPI уже не используется — в дело идёт HLT или просто бесконечный цикл на AP.
Большинству программистов, даже системным, не придётся встречать режим wait-for-SIPI в своей практике, просто потому что он случается однократно и довольно рано в процессе работы любой системы. Однако у этого правила есть исключение. Что произойдёт, если запускается виртуальная машина, использующая аппаратную поддержу виртуализации Intel VT-x, с несколькими логическими процессорами? Оказывается, что в режиме VMX non-root (гостевая система) процессор также можно помещать в различные режимы. Кроме активного, поддерживаются неактивные режимы HLT, Shutdown (о нём чуть дальше) и wait-for-SIPI. В этом состоянии поведение процессора очень похоже на то, что происходит и при обычной инициализации AP. А именно: он ничего не делает, игнорирует многие приходящие сигналы, и только при появлении SIPI выходит из гостевого режима в хозяйский (происходит VM-exit). Отмечу, что решение о том, использовать ли механизм SIPI, зависит от конкретного монитора виртуальных машин; на практике, некоторые их них реализуют собственный протокол пробуждения BSP и AP внутри ВМ.
Shutdown
Увы, код, который пишут люди, не безупречен. Серьёзные ошибки в прикладных программах чаще всего приводят к их завершению под зорким надзором операционной системы. Но вот кто позаботится о самой ОС, если она оступится? В качестве её «надзирателей» могут выступать программные мониторы вирутальных машин или, если они не используются, сама аппаратура, т.е. процессор и его особые состояния. О них и поговорим.
Типичная ситуация при работы любой программы — возникновение исключительной ситуации (interruption). Она далеко не всегда и вовсе не обязательно обозначает ошибку; прерывание текущей программы может быть временным, связанным с работой внешних устройств, или быть инициированно самим приложением намеренно, чтобы запросить у ОС некоторые сервисы (см. классификацию таких ситуаций в моём комментарии).
При возникновении исключительной ситуации происходит переключение состояния процессора, в чём-то похожее на очень усложнённый вызов процедуры. Нас сейчас не интересуют его детали (эта статья не об исключениях), важен лишь факт, что в этом процессе что-то может пойти не так — возникнуть исключение при попытки обработки исключения. В спецификации Intel IA-32 этот случай именуется Double Fault — двойной промах. Как и другие исключения, он имеет свой номер (8) и свою запись в системной таблице прерываний. ОС может настроить для него свой собственый програмный обработчик.
Но что будет, если и при попытке перехода в обработку Double Fault возникнет исключение? Гадать не нужно — такая ситуация зовётся Triple Fault, тройной промах. Вот только для него обработчика уже не предусмотрено; вместо этого процессор переводится в режим shutdown — останов.
Этот режим похож на состояние после HLT. В нём процессор прекращает исполнение инструкций до прибытия сигналов NMI, SMI, RESET или INIT. Что на самом деле произойдёт с системой в состоянии shutdown, зависит от реализации. Например, может быть включен световой индикатор на передней панели, сгенерировано немаскируемое прерывание для того, чтобы записать диагностическую информацию, проведена перезагрузка системы (горячая или холодная), или сгенерирован сигнал SMI.
Пожалуй, самая частая реакция на переход процессора в режим shutdown — это перезагрузка всей системы. В Linux намеренное переведение процессора в режим останова — это один из шести методов (последний, как самый отчаянный) обработать запрос на reboot.
Как и в случае с wait-for-SIPI, виртуализация добавляет ньюансов в поведение процессора в режиме shutdown. Тройной промах в режиме non-root, конечно же, не перезагружает всю систему. Он вызывает VM-exit, позволяя монитору ВМ обработать ситуацию в «глючной» гостевой системе. Кроме того, монитор может запускать гостя в режиме non-root в состоянии shutdown (уж не знаю, зачем это может понадобиться).
Ещё про Shutdown
Очень внимательный читатель документации может обнаружить, что некоторые выходы VM-exit с нарушенным состоянием процессора могут перевести процессор в так называемый режим VMX-abort shutdown. Он настолько суров, что из него процессор может вывести только RESET; все остальные сигналы он игнорирует.
Хочу отметить, что обычный Triple Fault в системном коде вызвать достаточно просто, достаточно просто не настраивать системные таблицы и немного подождать. Первое же разрешённое прерывание приведёт к (не)желаемому эффекту и перезагрузке.
А вот событие VMX-abort с последующим остановом получить не так просто. Возникнуть оно может только во время выхода из гостя в монитор (переход из non-root в root). Прежде чем выйти, надо войти (осуществить VM-entry). Но только при входе в non-root проводится огромное число проверок, в том числе таких, что запрещают работу с неконсистентным состоянием. Если что-то было настроено неверно, то попытка входа в гостевую ВМ сразу вернётся с кодом ошибки. Во время работы гость значительно ограничен в правах и самостоятельно разрушить системные структуры обычно не может. Другими словами, обычно ошибка в программе монитора проявляется раньше, при входе. Необходимо быть очень изобретательным (например, напортачить с изоляцией памяти или модель-специфичными регистрами), чтобы получить ошибку именно при VM-exit.
Экзотика: SENTER sleep и TXT shutdown
Напоследок, стоит упомянуть о расширении SMX (safer mode extensions), являющимся программным интерфейсом к набору платформенных технологий Intel TXT (trusted execution technology). Процессоры, поддерживающие SMX, получают ещё два неактивных режима.
- Первоочередная задача любой технологии, связанной с безопасностью — это установить, каким сущностям (коду, элементам среды исполнения) можно доверять, то есть выделить корень доверия. Проще всего это сделать, если в системе активен только один процессор, — в этом случае на оставшихся процессорах не сможет остаться недоверенных программ.
Исполнение инструкции GETSEC[SENTER] на одном логическом процессоре вводит остальные процессоры в новое неактивное состояние SENTER Sleep. После этого программа, исполняющаяся на оставшемся активном процессоре, должна перевести систему в так называемое «заверенное» окружение (measured environment), Как только заверенное окружение готово, в нём могут работать и остальные процессоры. Для этого они выводятся из состояния SENTER sleep с помощью инструкции GETSEC[WAKEUP].
- Как всегда, в процессе работы доверенного кода возможны ошибки, связанные с доступом к неправильным ресурсам, исключениями или несхождениям результатов криптографических проверок. Возникают они или по вине нерадивых программистов, или из-за намеренных попыток нарушить работу заверенного окружения извне. Во втором случае целью является компрометация окружения с подстановкой недоверенного кода или получением секретов.
При детектировании недопустимых событий в заверенном окружении процессор переводится в новое состояние — TXT-shutdown. Его отличительная особенность состоит в том, что информация о причине останова сохраняется в платформенных регистрах и выживает после перезагрузки, что позволяет проанализировать её впоследствии. Эх, вот бы и для обычного Triple Fault было бы что-то такое! Заметно помогло бы с диагностикой проблем.
Спасибо за внимание!