Мужик приходит устраиваться работать на стройку. Его спрашивает мастер:
— Что делать умеешь?
— Могу копать…
— А что еще?
— Могу не копать…

Не секрет, что современные процессоры работают очень быстро. Работа их заключается в постоянном извлечении из памяти инструкций и выполнения предписанных в них действий. Однако оказывается, по тем или иным причинам часто требуется притормозить этот процесс. В прикладных программах редко приходится задумываться о том, что при этом происходит с процессором. Но вот для создателей системного софта это далеко не праздный вопрос.


Неактивным процессор может быть не только для экономии энергии, но и в результате возникновения особых ситуаций, в процессе выполнения протоколов инициализации или как итог намеренных действий системных программ. Почему это интересно? При написании программных моделей (в том числе виртуальных машин) компьютерных систем, необходимо корректно моделировать переходы между состояниями виртуальных процессоров. В работе системных программ регулярно возникают ситуации, когда по тем или иным причинам ЦПУ должен «притормозить». Умение корректно использовать и моделировать эти ситуации зависит от знания и понимания спецификаций.


В статье фокус делается на программной стороне вопроса состояний процессора. Я не буду концентрироваться на деталях реализации (напряжения, пины, частоты и т.д.), так как 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

Да, если выполнить 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 состоит в том, что она повышает эффективность синхронизационных процессов на многопроцессорных системах.


Типичная ситуация в параллельных алгоритмах: поток А ожидает сигнала о готовности от потока Б, после чего оба они могут продолжить вычисления. В многопроцессорных системах А и Б будут исполняться на разных логических процессорах. Каким образом можно передать этот сигнал? Два варианта:


  1. Поместить А в неактивный режим (например, с помощью HLT). Затем Б использует межпроцессорное прерывание, которое выводит А из состояния сна. Однако посылка и обработка такого прерывания довольно дорога в терминах времени, т.к. она потребует нескольких переходов между режимами ядра и пользователя, да и путь сигнала прерывания будет неблизким.


  2. Поток А в «бесконечном» цикле проверяет содержимое некоторой ячейки памяти. Поток Б, желающий послать сообщение о готовности, записывает новое значение в эту ячейку, что выводит А из цикла. В этом случае задержка доставки сообщения меньше. Однако очевидно, что А проводит ожидание не самым энергоэффективным образом, сжигая такты, но не продвигаясь вперёд.

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, нужно иметь общее представление о том, как происходит инициализация в многопроцессорной системе. Проблема следующая: если все ядра, потоки и процессоры после включения питания бросятся исполнять один и тот же загрузочный код, то наступит бардак. В общих чертах довольно сложный и варьирующийся в деталях на разных платформах процесс можно описать следующим образом.


  1. После включения питания все логические процессоры включаются в гонку, в результате которой определяется один главный, т.н. загрузочный процессор (boot-strap processor, BSP). Все остальные процессоры обозначаются как прикладные процессоры (application processor, AP).


  2. BSP начинает исполнять загрузочный код из ROM по адресу 0xfffffff0.


  3. Все 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, получают ещё два неактивных режима.


  1. Первоочередная задача любой технологии, связанной с безопасностью — это установить, каким сущностям (коду, элементам среды исполнения) можно доверять, то есть выделить корень доверия. Проще всего это сделать, если в системе активен только один процессор, — в этом случае на оставшихся процессорах не сможет остаться недоверенных программ.


    Исполнение инструкции GETSEC[SENTER] на одном логическом процессоре вводит остальные процессоры в новое неактивное состояние SENTER Sleep. После этого программа, исполняющаяся на оставшемся активном процессоре, должна перевести систему в так называемое «заверенное» окружение (measured environment), Как только заверенное окружение готово, в нём могут работать и остальные процессоры. Для этого они выводятся из состояния SENTER sleep с помощью инструкции GETSEC[WAKEUP].


  2. Как всегда, в процессе работы доверенного кода возможны ошибки, связанные с доступом к неправильным ресурсам, исключениями или несхождениям результатов криптографических проверок. Возникают они или по вине нерадивых программистов, или из-за намеренных попыток нарушить работу заверенного окружения извне. Во втором случае целью является компрометация окружения с подстановкой недоверенного кода или получением секретов.


    При детектировании недопустимых событий в заверенном окружении процессор переводится в новое состояние — TXT-shutdown. Его отличительная особенность состоит в том, что информация о причине останова сохраняется в платформенных регистрах и выживает после перезагрузки, что позволяет проанализировать её впоследствии. Эх, вот бы и для обычного Triple Fault было бы что-то такое! Заметно помогло бы с диагностикой проблем.



Спасибо за внимание!

Поделиться с друзьями
-->

Комментарии (21)


  1. lorc
    11.05.2016 16:10
    +5

    Забавно наблюдать давление legacy, из-за которого всё усложняется количество возможных состояний процессора.

    В ARMах это всё как-то логичнее и консистентнее сделано. В том числе и поддержка режимов гипервизоа и защищенной среды, которые реализованны как отдельные режимы работы наравное с режимами обработки прерыванией или data abort.

    Один только тот факт, что каждое ядро знает свой core Id и в соответсвии с этим принимает решение о своем участии в процессе загрузки сильно облегчает дело. Никаких гонок и прочего. Просто core 0 грузится, а все остальные — висят в ROM-коде на WFE и ждут команды на старт.

    Управление энергопотреблением в случаях c ARM — это вообще не забота процессора. Процессор может только отключить клоки, когда переходит в WFE или WFI (или включать дополнительные, если используется NEON или VFP). А управление частотой и напряжением — забота отдельной логики на SoC. Соответсвенно, никаких аналогов C-states и P-states у процессора просто нет.

    И так далее. Мне кажется, что чем проще программная модель процессора, тем лучше программисту.


    1. Daimos
      11.05.2016 17:45

      И так далее. Мне кажется, что чем проще программная модель процессора, тем лучше программисту.

      Это все касается только программистов самой ОС — прикладным программистам это все знать практически бесполезно.


      1. lorc
        11.05.2016 18:24
        +1

        Ну этот пост явно не для прикладных программистов. Прикладные программисты сейчас настолько абстрагированы от железа, что для них архитектура процессора не играет вообще никакой роли. Т.е. раньше если прикладному программисту ещё было важно помнить хотя бы о sizeof(int) или endianess, то сейчас об этом практически не нужно задумываться.

        Но всё-таки есть люди которые пишут ОС, гипервизоры, драйвера, вирусы и другие интересные штуки. И вот им желательно иметь в голове модель работы процессора, что бы учитывать всякие крайние случаи, типа VMX-abort shutdown из статьи.


    1. VBKesha
      11.05.2016 19:36
      +2

      Один только тот факт, что каждое ядро знает свой core Id и в соответсвии с этим принимает решение о своем участии в процессе загрузки сильно облегчает дело. Никаких гонок и прочего. Просто core 0 грузится, а все остальные — висят в ROM-коде на WFE и ждут команды на старт.

      Только вот x86 может нести несколько процессоров на борту, и уже всё не так просто становится.
      Если дальше поискать то может оказаться что не только из за legacy всё это тянется.


      1. lorc
        11.05.2016 20:01
        +1

        Ну да, может нести несколько процессоров на борту. В SoC'ах на ARM это вообще сплошь и рядом происходит.
        В случае x86 всегда есть чипсет, который точно знает сколько у него процессорных сокетов, и во скольких установлены процессоры. Соотвественно чипсет и может раздать всем processor id по порядку.
        При чем, я уверен, что нечто подобное уже сделано, потому что BSP же должен как-то перечислить другие процессоры.


        1. VBKesha
          11.05.2016 21:29

          Скорей всего это усложнило бы чипсет, и возможно проще это дело решить на софтовом уровне.


          1. Duduka
            12.05.2016 06:07
            +1

            Это решается перемычками, вообще-то. Например, EISA-шина или PCI (если не учитывать навороченный программный переключатель). Тот процессор, который в первом слоте — получает сигнал от слота, а последующие — нет, и получают соответсвующие приоритет.ы… Голосование нужно только если «ядра» могут поломаться(не пройти тест на работоспособность), что важно для «удаленно» размещенного железа, но как видится, арбитр, очень сложный, не нужен, тем более только при старте, это шина, возможно, последовательного опроса состояния — стандартный мультиплексор. О каком усложнении идет речь, можно просто подавать напряжение на микросхемы последовательно пока идет инициализация.


          1. AlexanderG
            13.05.2016 10:41

            Либо отвести старшие биты core id под номер сокета, а младшие под номер ядра.


  1. Throwable
    11.05.2016 17:20
    +1

    Я помню магическую комбинацию команд CLI / HLT, которая в свое время наглухо весила комп с любой ОС. На Pentium-е уже такая фишка не работала.


    1. alexraven
      17.05.2016 18:41

      Это сработает только в реальном режиме работы процессора.


      1. Throwable
        17.05.2016 19:10
        -2

        Я могу ошибаться, но по-моему срабатывало как раз в виртуальном режиме: делаешь .com-файл из двух байт, и пускаешь его. И Win95 и OS/2 наглухо висли. Причем, вроде бы срабатывало только на процессорах AMD 486. На четверках Intel-а ничего не происходило.


  1. lovecraft
    11.05.2016 20:06
    +8

    Кроме того, монитор может запускать гостя в режиме non-root в состоянии shutdown (уж не знаю, зачем это может понадобиться).

    О, это очень нужная штука. Допустим, надо нам восстановить состояние VM на какой-то момент времени — запускаем VM с выключенным процессором, загружаем в RAM содержимое памяти VM из файла на диске, запускаем процессор. Или live migration — запускаем VM с выключенным CPU, копируем содержимое RAM из другой VM, запускаем процессор.


  1. CodeRush
    12.05.2016 13:57
    +2

    Спасибо за статью, уважаемый Atakua. Сколько не работай с x86 на уровне «ниже уже железо», все равно узнаешь массу нового.
    Если можно, напишите в следующий раз про состояния энергосбережения: C-State, P-State, NB/Mem P-State, T-State, EIST, CPPС, DPTF, вот это все. Понятно, что все есть в даташитах, но статья на русском будет полезна в любом случае.


    1. Atakua
      12.05.2016 21:07
      +1

      Про режимы энергосбережения попробую написать. Хотя я напрямую не работаю с ними, у меня под рукой есть довольно много публикаций на английском по этой теме (например, вот эта книга "Energy Efficient Servers. Blueprints for Data Center Optimization", бесплатная в PDF варианте). На русском, пожалуй, знаю только об одном бакалаврском дипломе (подготовленном в нашем отделе).


      Тема, конечно, необъятная, учитывая, что в секторах серверов, десктопов и встраиваемых систем на основе IA-32 детали реализации могут очень сильно отличаться.


      1. CodeRush
        12.05.2016 21:49
        +1

        Я тоже с ними почти не работаю, потому что в Embedded и Industrial все эти вещи стараются отключить, т.к. они плохо влияют на предсказуемость системы. Сколько Intel и AMD не стараются убрать это влияние, оно все равно есть и очень заметно для людей, пытающихся выжать из x86-64 хотя бы SoftRT.
        Тем не менее, «врага нужно знать в лицо», поэтому буду рад, если у вас все же получится написать что-нибудь по теме.


  1. Maxim007
    12.05.2016 14:11

    Всегда узнаёшь что-то в первый раз)


  1. Tiberius
    17.05.2016 13:53

    Извиняюсь за вопрос дилетанта, но…
    Давно уже подметил, что на OSX и Linux-based платформах компьютер по-настоящему засыпает, а вот с Win не всё так просто. Может быть стоит просветить массы?


    1. lovecraft
      17.05.2016 13:54
      +2

      Что значит «по настоящему засыпает»?


      1. Tiberius
        18.05.2016 00:38

        Ох, постараюсь объяснить…
        Например, на сколько я могу судить, OSX полностью отрубается HDD, которому потом требуется время на «раскрутку» (т.е. системе требуется время, чтобы проснуться), «засыпают» интерфейсы аля wifi и BT.
        Плюс режим бездействия. Частенько на OSX без нагрузки кулер процессора не работает — тишина и покой, а вот на Win всегда крутится.


        1. Atakua
          18.05.2016 09:36
          +2

          Если очень кратко, то:


          • Стандарт ACPI определяет интерфейсы управления энергопотреблением платформы (ЦПУ, материнская плата и периферийные устройства) для ОС.
          • Стандарт определяет состояния энергопотребления и производительности: для системы в целом (S0-S5), процессора (C- и P-состояния; эта статья лишь слегка коснулась C-состояний), периферийных устройств (D-состояния).
          • Стандарт не диктует обязательность реализации всех объявленных в нём режимов. Детали реализации режимов различаются для разной аппаратуры. Кроме того, операционные системы вольны реализовывать только некоторые из режимов сна.
          • За засыпание отдельных устройств отвечают их драйвера. Но что-то подсказывает мне, что обеспечение корректной поддержки D-состояний не стоит в первом приоритете у драйверописателей. Поэтому то, насколько глубоко заснёт конкретный девайс, может отличаться на разных ОС или ревизиях одной ОС.


        1. Daimos
          19.05.2016 21:15

          Ноутбук Dell — прошивка биоса А14 — кулер молчит и не крутится, когда нет сильной нагрузки. Пришло обновление биоса А15 — кулер крутиться всегда, при питании от сети 220В, даже без нагрузки и при минимальной температуре, ну только что медленно и чуть слышно его. Винда, естественно — не менялась при этом.
          Связано было с тем, что внутри ноута стоит стремный чип Нвидиа, который любил перегреваться и выходить из строя.