Как использовать APIC для создания IPI для пробуждения точек доступа для SMP в сборке x86?

В среде после загрузки (без ОС), как использовать BSP (первое ядро ​​/процессор) для создания IPI для AP (все остальные ядра/процессоры)? По существу, как один проснется и задает указатель инструкции для других ядер при запуске с одного?

Ответ 1

ПРЕДУПРЕЖДЕНИЕ: Я предположил здесь 80x86. Если это не 80x86, то я не знаю: -)

Сначала вам нужно выяснить, сколько других процессоров существует и какие их идентификаторы APIC, и определить физический адрес локальных APIC. Для этого вы разбираете таблицы ACPI (см. MADT/APIC в спецификации ACPI). Если вы не можете найти допустимые таблицы ACPI (например, компьютер слишком старый), существует более старая "спецификация MultiProcessor", которая определяет свои собственные таблицы с одинаковой информацией в ней. Обратите внимание, что "Спецификация MultiProcessor" устарела (и есть несколько компьютеров с манекенными таблицами MultiProcessor), поэтому вам нужно сначала проверить таблицы ACPI.

Следующий шаг - определить, какой тип локального APIC у вас есть. Есть 3 случая - старые внешние "82489DX" локальные APIC (не встроенные в сам процессор), xAPIC и x2APIC.

Начните с проверки CPUID, чтобы определить, является ли локальный APIC x2APIC. Если у вас есть 2 варианта - вы можете использовать x2APIC, или вы можете использовать "режим совместимости xAPIC". Для "режима совместимости xAPIC" вы можете использовать только 8-битные идентификаторы APIC и не сможете поддерживать компьютеры с большим количеством процессоров (например, 255 или более процессоров). Я бы рекомендовал использовать x2APIC (даже если вы не заботитесь о компьютерах с большим количеством процессоров) быстрее. Если вы используете режим x2APIC, вам необходимо переключить локальный APIC в этот режим.

В противном случае, если его не x2APIC, прочитайте локальный регистр версии APIC. Если локальная версия APIC равна 0x10 или выше, то ее xAPIC, а если она 0x0F или ниже, то это внешний APIC "82489DX".

Старый внешний "82489DX" локальный APIC был использован на 80486 и более старых компьютерах, и они чрезвычайно редки (они были очень редки 20 лет назад, затем большинство умерших и/или их заменили и выбросили с тех пор). Поскольку для запуска других ЦП используется другая последовательность, и потому что компьютеры, которые имеют эти локальные APIC, крайне редки (например, вы, вероятно, никогда не сможете протестировать свой код), имеет смысл не беспокоиться о поддержке этих компьютеров. Если вы поддерживаете эти старые компьютеры вообще; Я бы рекомендовал рассматривать их как "только для одного процессора" и просто не запускать никаких других CPU/s, если локальный APIC "82489DX". По этой причине я не буду описывать метод, используемый для их запуска здесь (он описан в Intel "Спецификация MultiProcess", если вам интересно).

Для xAPIC и x2APIC последовательность для запуска другого ЦП по существу одинакова (только разные способы доступа к локальным APIC - MSR или карте памяти). Я бы рекомендовал использовать (например) указатели на функции, чтобы скрыть эти различия; так что более поздний код может вызвать функцию "отправить IPI" через. указатель функции без заботы, если локальный APIC - x2APIC или xAPIC.

Чтобы запустить другой CPU, вам необходимо отправить ему последовательность IPI (Inter Processor Interrupts). Метод Intel выглядит следующим образом:

Send an INIT IPI to the CPU you're starting
Wait for 10 ms
Send a STARTUP IPI to the CPU you're starting
Wait for 200 us
Send another STARTUP IPI to the CPU you're starting
Wait for 200 us
Wait for started CPU to set a flag (so you know it started)
    If flag was set by other CPU, other CPU was started successfully
    Else if time-out, other CPU failed to start

Есть две проблемы с методом Intel. Часто другой процессор запускается первым STARTUP IPI, и в некоторых случаях это может привести к проблемам (например, если другой код запуска CPU делает что-то вроде total_CPUs++;, то каждый процессор может выполнить его дважды. Чтобы избежать этой проблемы, вы можете добавьте дополнительную синхронизацию (например, другой процессор ожидает флаг "Я знаю, вы начали", который должен быть установлен первым процессором до его продолжения). Вторая проблема с методом Intel - это измерение этих задержек. Обычно ОС запускает другие процессоры, а затем цифры вне зависимости от того, какие функции поддерживают ЦП и какое оборудование присутствует, и не имеет точной установки таймера /s, чтобы измерить те 200 задержек точно.

Чтобы избежать этих проблем; Я использую альтернативный метод, который выглядит следующим образом:

Send an INIT IPI to the CPU you're starting
Wait for 10 ms
Send a STARTUP IPI to the CPU you're starting
Wait for started CPU to set a flag (so you know it started) with a short timeout (e.g. 1 ms)
    If flag was set by other CPU, other CPU was started successfully
    Else if time-out
        Send another STARTUP IPI to the CPU you're starting
        Wait for started CPU to set a flag with a long timeout (e.g. 200 ms)
            If flag was set by other CPU, other CPU was started successfully
            Else if time-out, other CPU failed to start
If CPU started successfully
    Set flag to tell other CPU it can continue

Также обратите внимание, что вам нужно запускать процессоры отдельно. Я видел, как люди запускали все процессоры одновременно, используя функцию "широковещательный IPI для всех, кроме себя" - это неправильно, и вы неловко (не делайте этого, если не пишете прошивку). Проблема заключается в том, что некоторые процессоры могут быть неисправными (например, не удалось их BIST/встроенный самотестирование), и некоторые процессоры могут быть отключены (например, гиперпоточность при отключении гиперпотока в прошивке); и метод "широковещательного IPI для всех, кроме самостоятельно" может запускать процессоры, которые никогда не запускались.

Наконец, для компьютеров с большим количеством процессоров может потребоваться относительно много времени, чтобы запустить их все, если вы запускаете их по одному за раз. Например, если для запуска каждого процессора требуется 11 мс, а 128 процессоров, то это займет 1,4 секунды. Если вы хотите быстро загрузиться, есть способы избежать этого. Например, первый процессор может запустить второй процессор, тогда 1-й и 2-й процессоры могут запускать 3-й и 4-й процессоры, тогда эти четыре процессора могут запускать следующие четыре процессора и т.д. Таким образом, вы можете запустить 128 процессоров за 77 мс вместо 1,4 секунды.

Примечание. Я бы рекомендовал только запускать процессоры по одному и убедиться, что это работает до того, как вы попытаетесь выполнить какой-либо "параллельный запуск" (после этого вы можете беспокоиться о том, что после этого вы знаете, что все работает).

Адрес, который начнет выполнять другой CPU/s, закодирован в поле "вектор" IP-адреса STARTUP. ЦП начнет выполнение кода (в реальном режиме) с помощью CS = vector * 256 и IP = 0. Векторное поле является 8-битным, поэтому самый старший начальный адрес, который вы можете использовать, - 0x000FF000 (0xFF00: 0x0000 в реальном режиме). Однако это прежняя область ROM (на практике исходный адрес должен быть ниже). Как правило, вы копируете небольшой кусок кода запуска в подходящий адрес; где код запуска обрабатывает синхронизацию (например, установка флага "Я начал", который может видеть другой ЦП и ожидающего, чтобы ему сказали, что он ОК, чтобы продолжить), а затем делает такие вещи, как включение защищенного/длинного режима и создание стека перед переходом на запись в нормальном коде ОС. Этот маленький кусок кода запуска называется "батутом запуска AP". Это также делает "параллельный запуск" немного сложным; поскольку каждый запущенный процессор нуждается в собственных/отдельных флагах синхронизации и стеке; и поскольку эти вещи обычно реализуются с помощью переменных в батуте (например, mov esp,[cs:stackTop]), это означает, что в конечном итоге с несколькими батутами.