Давным-давно, например, для написания ассемблера x86, вы должны будете получить инструкции о том, что «загрузить регистр EDX со значением 5», «увеличить регистр EDX» и т. Д.
С современными процессорами, которые имеют 4 ядра (или даже больше), на уровне машинного кода это просто выглядит так, как будто есть 4 отдельных процессора (т.е. есть только 4 отдельных регистра "EDX")? Если так, когда вы говорите «увеличить регистр EDX», что определяет, какой регистр EDX ЦП увеличивается? Есть ли в ассемблере x86 понятие «контекст процессора» или «нить»?
Как работает связь / синхронизация между ядрами?
Если вы писали операционную систему, какой механизм предоставляется через оборудование, чтобы позволить вам планировать выполнение на разных ядрах? Это какая-то специальная привилегированная инструкция?
Если бы вы писали оптимизирующую виртуальную машину компилятора / байт-кода для многоядерного процессора, что вам нужно было бы знать конкретно о, скажем, x86, чтобы он генерировал код, эффективно работающий на всех ядрах?
Какие изменения были внесены в машинный код x86 для поддержки многоядерных функций?
Ответы:
Это не прямой ответ на вопрос, но это ответ на вопрос, который появляется в комментариях. По сути, вопрос в том, какую поддержку аппаратное обеспечение оказывает многопоточным операциям.
Николас Флинт был прав , по крайней мере, в отношении x86. В многопоточной среде (Hyper-Threading, Multi-Core или Multi-Processor) поток Bootstrap (обычно поток 0 в ядре 0 в процессоре 0) запускает выборку кода с адреса
0xfffffff0
. Все остальные потоки запускаются в специальном состоянии ожидания под названием Wait-for-SIPI . В рамках своей инициализации основной поток отправляет специальное межпроцессорное прерывание (IPI) через APIC, называемое SIPI (Startup IPI), каждому потоку в WFS. SIPI содержит адрес, с которого этот поток должен начать извлекать код.Этот механизм позволяет каждому потоку выполнять код с другого адреса. Все, что нужно, это программная поддержка для каждого потока, чтобы настроить свои собственные таблицы и очереди сообщений. ОС использует их для выполнения фактического многопоточного планирования.
Что касается фактической сборки, как писал Николас, нет никакой разницы между сборками для однопоточного или многопоточного приложения. Каждый логический поток имеет свой собственный набор регистров, поэтому пишем:
будет обновляться только
EDX
для текущего запущенного потока . Там нет никакого способа изменитьEDX
на другом процессоре, используя одну инструкцию по сборке. Вам нужен какой-то системный вызов, чтобы попросить ОС сообщить другому потоку о запуске кода, который будет обновлять свой собственныйEDX
.источник
Пример минимального запуска Intel x86 для неизолированного металла
Работоспособный пример из чистого металла со всеми необходимыми образцами . Все основные части описаны ниже.
Протестировано на Ubuntu 15.10 QEMU 2.3.0 и Lenovo ThinkPad T400 с реальным аппаратным гостем .
Руководство Intel по системному программированию, том 3, 325384-056RU, сентябрь 2015 г., посвящено SMP в главах 8, 9 и 10.
Таблица 8-1. «Последовательность широковещательной передачи INIT-SIPI-SIPI и выбор тайм-аутов» содержит пример, который в основном работает:
На этот код:
Большинство операционных систем сделает невозможным большинство этих операций из кольца 3 (пользовательские программы).
Так что вам нужно написать свое собственное ядро, чтобы свободно играть с ним: пользовательская программа Linux не будет работать.
Сначала запускается один процессор, называемый процессором начальной загрузки (BSP).
Он должен активировать другие (называемые процессорами приложений (AP)) через специальные прерывания, называемые межпроцессорными прерываниями (IPI) .
Эти прерывания могут быть сделаны путем программирования расширенного программируемого контроллера прерываний (APIC) через регистр команд прерывания (ICR)
Формат ICR задокументирован по адресу: 10.6 «ВЫПУСК МЕЖПРОЦЕССОРНЫХ ПРЕРЫВАНИЙ»
IPI происходит, как только мы пишем в ICR.
ICR_LOW определяется в 8.4.4 «Пример инициализации MP» как:
Магическое значение
0FEE00300
- это адрес памяти ICR, как описано в Таблице 10-1 «Карта адресов локального регистра APIC».В примере используется самый простой из возможных методов: он устанавливает ICR для отправки широковещательных IPI, которые доставляются всем другим процессорам, кроме текущего.
Но также возможно, и некоторые рекомендуют , получать информацию о процессорах через специальные структуры данных, настраиваемые BIOS, например таблицы ACPI или таблицу конфигурации Intel MP, и запускать только те, которые вам нужны, по очереди.
XX
в000C46XXH
кодирует адрес первой инструкции, которую процессор будет выполнять как:Помните, что CS умножает адреса на
0x10
, поэтому фактический адрес памяти первой инструкции:Так что если, например
XX == 1
, процессор будет начинаться с0x1000
.Затем мы должны убедиться, что в этом месте памяти выполняется 16-битный код реального режима, например:
Использование сценария компоновщика - еще одна возможность.
Петли задержки - раздражающая часть, чтобы начать работать: не существует супер простого способа точно сделать такие сны.
Возможные методы включают в себя:
Связанный: Как отобразить число на экране и так и поспать одну секунду со сборкой DOS x86?
Я думаю, что исходный процессор должен быть в защищенном режиме, чтобы это работало, когда мы пишем по адресу,
0FEE00300H
который слишком высок для 16-битДля связи между процессорами мы можем использовать спин-блокировку основного процесса и изменить блокировку со второго ядра.
Мы должны убедиться, что обратная запись в память выполнена, например, через
wbinvd
.Общее состояние между процессорами
8.7.1 «Состояние логических процессоров» гласит:
Совместное использование кэша обсуждается по адресу:
Гиперпотоки Intel имеют больший общий объем кэша и конвейера, чем отдельные ядра: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Ядро Linux 4.2
Основное действие инициализации, кажется, в
arch/x86/kernel/smpboot.c
.ARM минимальный работоспособный пример из неизолированного металла
Здесь я приведу минимальный исполняемый пример ARMv8 aarch64 для QEMU:
GitHub вверх по течению .
Собрать и запустить:
В этом примере мы помещаем CPU 0 в цикл спин-блокировки, и он завершается только тогда, когда CPU 1 освобождает спин-блокировку.
После спин-блокировки ЦП 0 выполняет вызов выхода из полухоста, который заставляет QEMU выйти.
Если вы запускаете QEMU только с одним процессором
-smp 1
, то симуляция просто навсегда зависает на спин-блокировке.CPU 1 разбудился с помощью интерфейса PSCI, более подробную информацию можно найти по адресу: ARM: Запустите / включите / включите другие ядра / AP CPU и передайте начальный адрес выполнения?
В апстрим-версии также есть несколько настроек, чтобы заставить его работать на gem5, так что вы также можете поэкспериментировать с характеристиками производительности.
Я не тестировал его на реальном оборудовании, поэтому не уверен, насколько это портативно. Следующая библиография Raspberry Pi может представлять интерес:
Этот документ содержит некоторые рекомендации по использованию примитивов синхронизации ARM, которые затем можно использовать для забавных вещей с несколькими ядрами: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf
Протестировано на Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.
Следующие шаги для более удобного программирования
Предыдущие примеры пробуждают вторичный процессор и выполняют базовую синхронизацию памяти с выделенными инструкциями, что является хорошим началом.
Но чтобы сделать многоядерные системы простыми в программировании, например, POSIX
pthreads
, вам также необходимо перейти к следующим более сложным темам:Программа установки прерывает и запускает таймер, который периодически решает, какой поток будет запущен сейчас. Это известно как вытесняющая многопоточность .
Такая система также должна сохранять и восстанавливать регистры потоков по мере их запуска и остановки.
Также возможно иметь не вытесняющие многозадачные системы, но они могут потребовать, чтобы вы изменили свой код так, чтобы каждый поток давал результат (например, с
pthread_yield
реализацией), и становится сложнее балансировать рабочие нагрузки.Вот несколько упрощенных примеров таймера с голым металлом:
иметь дело с конфликтами памяти. В частности, каждому потоку понадобится уникальный стек, если вы хотите кодировать на C или других языках высокого уровня.
Вы можете просто ограничить потоки фиксированным максимальным размером стека, но лучший способ справиться с этим - использование подкачки, которая позволяет создавать эффективные стеки «неограниченного размера».
Вот наивный простой пример aarch64, который взорвется, если стек станет слишком глубоким
Вот несколько веских причин использовать ядро Linux или другую операционную систему :-)
Примитивы синхронизации памяти пользователя
Хотя запуск / остановка / управление потоком, как правило, выходят за рамки области пользователя, вы можете использовать инструкции по сборке из потоков области пользователя для синхронизации обращений к памяти без потенциально более дорогих системных вызовов.
Конечно, вы должны предпочесть использовать библиотеки, которые переносят эти примитивы низкого уровня. Стандарт C ++ сам сделал большие успехи на тех
<mutex>
и<atomic>
заголовки, и , в частности , сstd::memory_order
. Я не уверен, охватывает ли он всю возможную семантику памяти, но это возможно.Более тонкая семантика особенно актуальна в контексте структур данных без блокировки , которые в некоторых случаях могут повысить производительность. Чтобы реализовать их, вам, вероятно, придется немного узнать о различных типах барьеров памяти: https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/
Например, в Boost есть несколько реализаций контейнеров без блокировки по адресу: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html.
Такие пользовательские инструкции также используются для реализации
futex
системного вызова Linux , который является одним из основных примитивов синхронизации в Linux.man futex
4.15 гласит:Само имя системного вызова означает «Fast Userspace XXX».
Вот минимальный бесполезный пример C ++ x86_64 / aarch64 со встроенной сборкой, который иллюстрирует базовое использование таких инструкций в основном для развлечения:
main.cpp
GitHub вверх по течению .
Возможный вывод:
Из этого мы видим, что
LDADD
инструкция x86 LOCK prefix / aarch64 сделала добавление атомарным: без него у нас есть условия гонки для многих добавлений, и общее количество в конце меньше, чем синхронизированные 20000.Смотрите также:
Протестировано в Ubuntu 19.04 amd64 и в пользовательском режиме QEMU aarch64.
источник
#include
(принимает это как комментарий), NASM, FASM, YASM не знают синтаксиса AT & T, поэтому они не могут быть ими ... так что же это?gcc
,#include
происходит от препроцессора Си. ИспользуйтеMakefile
предоставленное, как описано в разделе «Начало работы»: github.com/cirosantilli/x86-bare-metal-examples/blob/… Если это не сработает, откройте проблему GitHub.Насколько я понимаю, каждое «ядро» представляет собой законченный процессор с собственным набором регистров. По сути, BIOS запускает вас с одним запущенным ядром, а затем операционная система может «запускать» другие ядра, инициализируя их и указывая на код для запуска и т. Д.
Синхронизация осуществляется ОС. Как правило, на каждом процессоре для ОС выполняется отдельный процесс, поэтому многопоточность операционной системы отвечает за решение, какой процесс касается какой памяти, и что делать в случае конфликта памяти.
источник
Неофициальный SMP FAQ
Давным-давно, например, для написания ассемблера x86, вы должны будете получить инструкции о том, что «загрузить регистр EDX со значением 5», «увеличить регистр EDX» и т. Д. В современных процессорах, которые имеют 4 ядра (или даже больше) на уровне машинного кода это просто выглядит так, как будто есть 4 отдельных процессора (т.е. есть только 4 отдельных регистра "EDX")?
Именно. Существует 4 набора регистров, включая 4 отдельных указателя команд.
Если так, когда вы говорите «увеличить регистр EDX», что определяет, какой регистр EDX ЦП увеличивается?
Процессор, который выполнил эту инструкцию, естественно. Представьте, что это 4 совершенно разных микропроцессора, которые просто используют одну и ту же память.
Есть ли в ассемблере x86 понятие «контекст процессора» или «нить»?
Нет. Ассемблер просто переводит инструкции, как всегда. Там нет никаких изменений.
Как работает связь / синхронизация между ядрами?
Поскольку они совместно используют одну и ту же память, это в основном вопрос программной логики. Хотя теперь существует механизм межпроцессорных прерываний , он не является необходимым и изначально не присутствовал в первых двухпроцессорных системах x86.
Если вы писали операционную систему, какой механизм предоставляется через оборудование, чтобы позволить вам планировать выполнение на разных ядрах?
Планировщик на самом деле не меняется, за исключением того, что он немного более осторожен относительно критических секций и типов используемых блокировок. До SMP код ядра в конечном итоге вызывал планировщик, который просматривал очередь выполнения и выбирал процесс для запуска в качестве следующего потока. (Процессы в ядре очень похожи на потоки.) Ядро SMP выполняет один и тот же код по одному потоку за раз, просто теперь блокировка критических секций должна быть безопасной для SMP, чтобы два ядра не могли случайно выбрать тот же PID.
Это какие-то особые привилегированные инструкции?
Нет. Все ядра работают в одной и той же памяти с одинаковыми старыми инструкциями.
Если бы вы писали оптимизирующую виртуальную машину компилятора / байт-кода для многоядерного процессора, что вам нужно было бы знать конкретно о, скажем, x86, чтобы он генерировал код, эффективно работающий на всех ядрах?
Вы запускаете тот же код, что и раньше. Это ядро Unix или Windows, которое нужно изменить.
Вы можете обобщить мой вопрос так: «Какие изменения были внесены в машинный код x86 для поддержки многоядерной функциональности?»
Ничего не нужно было Первые системы SMP использовали тот же набор команд, что и однопроцессорные. Сейчас произошла значительная эволюция архитектуры x86 и появилось множество новых инструкций для ускорения работы, но ни одна из них не была необходима для SMP.
Для получения дополнительной информации см. Спецификацию многопроцессорного процессора Intel .
Обновление: на все последующие вопросы можно ответить, просто полностью признав, что многоядерный n- процессорный процессор - это почти 1 то же самое, что n отдельных процессоров, которые просто совместно используют одну и ту же память. 2 Был задан важный вопрос, который не задавался: как программа, написанная для запуска более чем на одном ядре, повышает производительность? И ответ таков: он написан с использованием библиотеки потоков, такой как Pthreads. Некоторые библиотеки потоков используют «зеленые потоки», которые не видны ОС, и они не получат отдельные ядра, но если библиотека потоков использует функции потоков ядра, ваша многопоточная программа автоматически будет многоядерной.
1. Для обеспечения обратной совместимости при перезагрузке запускается только первое ядро, и для запуска оставшихся необходимо выполнить несколько действий типа драйвера.
2. Они также разделяют все периферийные устройства, естественно.
источник
Как человек, который пишет оптимизирующие виртуальные машины компилятора / байт-кода, я могу помочь вам здесь.
Вам не нужно ничего конкретно знать о x86, чтобы он генерировал код, эффективно работающий на всех ядрах.
Однако вам может понадобиться знать о cmpxchg и его друзьях, чтобы написать код, который будет корректно работать на всех ядрах. Многоядерное программирование требует использования синхронизации и связи между потоками исполнения.
Возможно, вам нужно что-то знать о x86, чтобы он генерировал код, который эффективно работает на x86 в целом.
Есть и другие вещи, которые вам было бы полезно узнать:
Вы должны узнать о возможностях ОС (Linux, Windows или OSX), позволяющих запускать несколько потоков. Вы должны узнать об API-интерфейсах распараллеливания, таких как OpenMP и Threading Building Blocks, или о готовящемся выпуске "Grand Central" для OSX 10.6 "Snow Leopard".
Вы должны подумать, должен ли ваш компилятор выполнять автоматическое распараллеливание, или если автору приложений, скомпилированных вашим компилятором, нужно добавить специальный синтаксис или вызовы API в свою программу, чтобы использовать преимущества нескольких ядер.
источник
Каждое ядро выполняется из другой области памяти. Ваша операционная система направит ядро на вашу программу, а ядро выполнит вашу программу. Ваша программа не будет знать, что существует более одного ядра или на каком ядре она выполняется.
Также нет никаких дополнительных инструкций, доступных только для операционной системы. Эти ядра идентичны одноядерным чипам. Каждое ядро выполняет часть операционной системы, которая будет обрабатывать связь с общими областями памяти, используемыми для обмена информацией, чтобы найти следующую область памяти для выполнения.
Это упрощение, но оно дает вам базовое представление о том, как это делается. Подробнее о многоядерных и многопроцессорных системах на Embedded.com есть много информации по этой теме ... Эта тема очень быстро усложняется!
источник
Код сборки будет переведен в машинный код, который будет выполняться на одном ядре. Если вы хотите, чтобы он был многопоточным, вам придется использовать примитивы операционной системы, чтобы запускать этот код на разных процессорах несколько раз или разные куски кода на разных ядрах - каждое ядро будет выполнять отдельный поток. Каждый поток увидит только одно ядро, на котором он сейчас работает.
источник
Это не сделано в машинных инструкциях вообще; ядра претендуют на то, чтобы быть отдельными процессорами и не имеют никаких специальных возможностей для общения друг с другом. Есть два способа общения:
они разделяют физическое адресное пространство. Аппаратное обеспечение обрабатывает когерентность кэша, поэтому один процессор записывает в адрес памяти, который читает другой.
они совместно используют APIC (программируемый контроллер прерываний). Это память, отображаемая в физическое адресное пространство, и может использоваться одним процессором для управления другими, их включения или выключения, отправки прерываний и т. Д.
http://www.cheesecake.org/sac/smp.html - хорошая ссылка с глупым URL.
источник
Основное различие между одно- и многопоточным приложением состоит в том, что первое имеет один стек, а второе - один для каждого потока. Код генерируется несколько иначе, так как компилятор будет считать, что регистры сегментов данных и стека (ds и ss) не равны. Это означает, что косвенное обращение через регистры ebp и esp, которые по умолчанию к регистру ss, также не будут по умолчанию к ds (потому что ds! = Ss). И наоборот, косвенное обращение через другие регистры, которые по умолчанию равны ds, не будут равны ss.
Потоки разделяют все остальное, включая области данных и кода. Они также разделяют подпрограммы lib, поэтому убедитесь, что они потокобезопасны. Процедура, которая сортирует область в ОЗУ, может быть многопоточной, чтобы ускорить процесс. Затем потоки будут получать доступ, сравнивать и упорядочивать данные в одной и той же области физической памяти и выполнять один и тот же код, но с использованием разных локальных переменных для управления своей соответствующей частью сортировки. Это, конечно, потому что потоки имеют разные стеки, в которых содержатся локальные переменные. Этот тип программирования требует тщательной настройки кода, чтобы уменьшить количество конфликтов между ядрами (в кэш-памяти и оперативной памяти), что, в свою очередь, приводит к тому, что код работает быстрее с двумя или более потоками, чем с одним. Конечно, невыполненный код часто будет быстрее с одним процессором, чем с двумя или более. Отладка является более сложной задачей, потому что стандартная точка останова «int 3» не будет применяться, так как вы хотите прерывать определенный поток, а не все из них. Точки останова регистра отладки также не решают эту проблему, если только вы не можете установить их на конкретном процессоре, выполняющем конкретный поток, который вы хотите прервать.
Другой многопоточный код может включать разные потоки, выполняющиеся в разных частях программы. Этот тип программирования не требует такой же настройки и, следовательно, намного легче учиться.
источник
То, что было добавлено в каждую многопроцессорную архитектуру по сравнению с однопроцессорными вариантами, которые были до них, это инструкции по синхронизации между ядрами. Кроме того, у вас есть инструкции, чтобы иметь дело с когерентностью кэша, очищающими буферами и подобными операциями низкого уровня, с которыми сталкивается ОС. В случае одновременных многопоточных архитектур, таких как IBM POWER6, IBM Cell, Sun Niagara и Intel «Hyperthreading», вы также склонны видеть новые инструкции для определения приоритетов между потоками (например, установка приоритетов и явная уступка процессора, когда нечего делать) ,
Но основная однопоточная семантика одинакова, вы просто добавляете дополнительные возможности для синхронизации и связи с другими ядрами.
источник