Как выглядит многоядерный язык ассемблера?


когда-то, например, для записи ассемблера x86 у вас были бы инструкции с указанием "загрузить регистр EDX со значением 5", "увеличить регистр EDX" и т. д.

с современными процессорами, которые имеют 4 ядра (или даже больше), на уровне машинного кода это просто выглядит так, как будто есть 4 отдельных процессора (т. е. есть только 4 отдельных регистра "EDX")? Если да, то когда вы говорите "увеличить регистр EDX", что определяет, какой регистр EDX процессора увеличивается? Есть ли " процессор контекст" или "потоковая" концепция в ассемблере x86 сейчас?

Как работает связь / синхронизация между ядрами?

Если вы пишете операционную систему, какой механизм предоставляется через аппаратное обеспечение, чтобы вы могли планировать выполнение на разных ядрах? Это какая-то особая привилегированная инструкция(ы)?

Если вы пишете оптимизирующий компилятор / байт-код VM для многоядерного процессора, что вам нужно знать конкретно, скажем, x86, чтобы сделать это генерировать код, который эффективно работает на всех ядрах?

какие изменения были внесены в машинный код x86 для поддержки многоядерных функций?

10 195

10 ответов:

Это не прямой ответ на вопрос, но это ответ на вопрос, который появляется в комментариях. По сути, вопрос заключается в том, какую поддержку аппаратное обеспечение дает многопоточной работе.

Николас Флинт был прав, по крайней мере, в отношении x86. В многопоточные среды (многопоточность, многоядерные и многопроцессорные), то Bootstrap thread (обычно поток 0 в ядре 0 в процессоре 0) запускает выборку кода из адреса 0xfffffff0. Все остальные потоки запускаются в специальном состоянии сна под названием Жди СиПи. В рамках своей инициализации основной поток отправляет специальное межпроцессорное прерывание (IPI) по APIC, называемому SIPI (Startup IPI), каждому потоку, который находится в WFS. SIPI содержит адрес, с которого этот поток должен начать выборку кода.

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

Что касается фактической сборки, Как писал Николас, нет никакой разницы между сборками для однопоточного или многопоточного приложения. Каждый логический поток имеет свой собственный набор регистров, поэтому и пишу:

mov edx, 0

обновление EDX на запуск потока. Это невозможно. чтобы изменить EDX на другом процессоре с использованием одной инструкции сборки. Вам нужен какой-то системный вызов, чтобы попросить ОС сказать другой поток для запуска кода, который будет обновлять свой собственный EDX.

насколько я понимаю, каждое "ядро" - это полноценный процессор со своим собственным набором регистров. В принципе, BIOS запускает вас с одним ядром, а затем операционная система может "запускать" другие ядра, инициализируя их и указывая на код для запуска и т. д.

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

минимальный запуск Intel x86 голый металлический пример

Runnable голый металлический пример со всеми необходимыми шаблоном. Все основные части описаны ниже.

протестировано на Ubuntu 15.10 QEMU 2.3.0 и Lenovo ThinkPad T400.

The руководство по программированию Intel Manual Volume 3 System - 325384-056US сентябрь 2015 охватывает SMP в главах 8, 9 и 10.

таблица 8-1. "Трансляция init-SIPI-SIPI последовательность и выбор тайм-ауты " содержит пример, который в основном просто работает:

MOV ESI, ICR_LOW    ; Load address of ICR low dword into ESI.
MOV EAX, 000C4500H  ; Load ICR encoding for broadcast INIT IPI
                    ; to all APs into EAX.
MOV [ESI], EAX      ; Broadcast INIT IPI to all APs
; 10-millisecond delay loop.
MOV EAX, 000C46XXH  ; Load ICR encoding for broadcast SIPI IP
                    ; to all APs into EAX, where xx is the vector computed in step 10.
MOV [ESI], EAX      ; Broadcast SIPI IPI to all APs
; 200-microsecond delay loop
MOV [ESI], EAX      ; Broadcast second SIPI IPI to all APs
                    ; Waits for the timer interrupt until the timer expires

на этот код:

  1. большинство операционных систем сделает большинство этих операций невозможными из кольца 3 (пользовательские программы).

    поэтому вам нужно написать свое собственное ядро, чтобы свободно играть с ним: программа userland Linux не будет работать.

  2. сначала запускается один процессор, называемый процессором начальной загрузки (BSP).

    он должен разбудите другие (называемые процессорами приложений (AP)) через специальные прерывания, называемые Межпроцессорные прерывания (IPI).

    те прерывания могут быть сделаны путем программировать предварительный Programmable регулятор прерывания (APIC) через регистр команды прерывания (ICR)

    формат ICR задокументирован по адресу: 10.6 "выдача МЕЖПРОЦЕССОРНЫХ прерываний"

    IPI происходит, как только мы пишем в ICR.

  3. ICR_LOW определяется в 8.4.4 "пример инициализации MP" как:

    ICR_LOW EQU 0FEE00300H
    

    магическое значение 0FEE00300 - это адрес памяти ICR, как описано в таблице 10-1 "локальная карта адресов регистра APIC"

  4. в примере используется самый простой способ: он настраивает ICR для отправки широковещательных IPIs, которые доставляются всем другим процессорам, кроме текущего.

    но это также возможно, и рекомендуется некоторыми, чтобы получить информацию о процессорах через специальные структуры данных настройки BIOS, как таблицы ACPI или таблица конфигурации MP Intel и только просыпаются те, которые вам нужны один за другим.

  5. XX на 000C46XXH кодирует адрес первой инструкции, которую процессор будет выполнять как:

    CS = XX * 0x100
    IP = 0
    

    помните, что CS кратные адреса по 0x10, так что фактический адрес памяти первой инструкции:

    XX * 0x1000
    

    так что если например XX == 1 процессор начнет в 0x1000.

    затем мы должны убедиться, что в этом месте памяти есть 16-битный код реального режима, например:

    cld
    mov $init_len, %ecx
    mov $init, %esi
    mov 0x1000, %edi
    rep movsb
    
    .code16
    init:
        xor %ax, %ax
        mov %ax, %ds
        /* Do stuff. */
        hlt
    .equ init_len, . - init
    

    использование сценария компоновщика-это еще одна возможность.

  6. петли задержки раздражает часть, чтобы получить работу: нет супер простой способ сделать такие сны точно.

    возможные методы включают в себя:

    • PIT (используется в моем примере)
    • HPET
    • откалибруйте время занятого цикла с вышеуказанным, и используйте его вместо

    по теме: как отобразить номер на экране и и спать в течение одной секунды с сборкой DOS x86?

  7. я думаю, что начальный процессор должен быть в защищенном режиме, чтобы это работало, когда мы пишем адрес 0FEE00300H что слишком высоко для 16-бит

  8. для связи между процессорами, мы можем использовать spinlock на главном процессе, и доработать замок от второго сердечника.

    мы должны гарантировать, что запись в память будет выполнена, например, через wbinvd.

общее состояние между процессорами

8.7.1 "состояние логических процессоров" говорит:

следующие функции являются частью архитектурного состояния логических процессоров в составе процессоров Intel 64 или IA-32 поддержка технологии Intel Hyper-Threading. Функции можно разделить на три группы:

  • дублируется для каждого логического процессора
  • совместно используется логическими процессорами в физическом процессоре
  • общий или дублированный, в зависимости от реализации

следующие функции дублируются для каждого логического процессор:

  • регистры общего назначения (EAX, EBX, ECX, EDX, ESI, EDI, ESP и EBP)
  • сегментные регистры (CS, DS, SS, ES, FS и GS)
  • регистры EFLAGS и EIP. Обратите внимание, что регистры CS и EIP/RIP для каждого логического процессора указывают на поток команд для потока, выполняемого логическим процессором.
  • регистры FPU x87 (ST0 через ST7, слово состояния, слово управления, слово бирки, указатель операнда данных, и инструкция указатель)
  • регистры MMX (MM0 через MM7)
  • регистры XMM (XMM0 через XMM7) и регистр MXCSR
  • управляющие регистры и регистры указателей системных таблиц (GDTR, LDTR, IDTR, регистр задач)
  • отладочные регистры (DR0, DR1, DR2, DR3, DR6, DR7) и управление отладкой MSRs
  • глобальный статус проверки машины (IA32_MCG_STATUS) и возможность проверки машины (IA32_MCG_CAP) MSRs
  • тепловая модуляция часов и управление питанием ACPI MSRs
  • счетчик времени msrs
  • большинство других регистров MSR, включая таблицу атрибутов страницы (PAT). См. исключения ниже.
  • локальные регистры APIC.
  • дополнительные регистры общего назначения (R8-R15), регистры XMM (XMM8-XMM15), регистр управления, IA32_EFER on Процессоры Intel 64.

следующие функции являются общими логические процессоры:

  • регистры диапазона типов памяти (MTRRs)

являются ли следующие функции общими или дублированными, зависит от реализации:

  • IA32_MISC_ENABLE MSR (адрес MSR 1A0H)
  • архитектура проверки машины (MCA) MSRs (за исключением Ia32_mcg_status и Ia32_mcg_cap MSRs)
  • контроль производительности и счетчик MSRs

совместное использование кэша обсуждается по адресу:

потока Intel имеют обмен большего кэша и конвейера, чем отдельные стержни: https://superuser.com/questions/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858

ядро Linux 4.2

основное действие инициализации, кажется, в arch/x86/kernel/smpboot.c.

ARM примеры

ARM кажется немного проще в настройке, чем x86, поскольку он имеет меньше исторических накладных расходов, вот два минимальных примера для запуска:

TODO: просмотрите эти примеры и объясните их лучше здесь.

этот документ содержит некоторые рекомендации по использованию примитивов синхронизации ARM, которые затем можно использовать для забавных вещей несколько ядер: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf

неофициальная справка СМП stack overflow logo


когда-то, чтобы написать ассемблер x86, например, у вас были бы инструкции с указанием "загрузить регистр EDX со значением 5", "увеличить регистр EDX" и т. д. С современными процессорами, которые имеют 4 ядра (или даже больше), на уровне машинного кода это просто выглядит так, как будто есть 4 отдельных процессора (т. е. есть только 4 отдельных регистра "EDX")?

точно. Есть 4 наборы регистров, включая 4 отдельных указателя команд.

если да, то когда вы говорите "увеличить регистр EDX", что определяет, какой регистр EDX процессора увеличивается?

процессор, который выполнил эту инструкцию, естественно. Подумайте об этом как о 4 совершенно разных микропроцессорах, которые просто используют одну и ту же память.

есть ли понятие" контекст процессора "или" поток " в ассемблере x86 сейчас?

нет. Ассемблер просто переводит инструкции, как это всегда было. Никаких изменений там нет.

как работает связь / синхронизация между ядрами?

поскольку они разделяют одну и ту же память, это в основном вопрос логики программы. Хотя там сейчас есть межпроцессорное прерывание механизм, он не нужен и изначально не присутствовал в первом двухпроцессорном x86 системный.

если вы пишете операционную систему, какой механизм предоставляется через аппаратное обеспечение, чтобы вы могли планировать выполнение на разных ядрах?

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

это какая-то особая привилегированная инструкция(ы)?

нет. Ядра просто все работают в одной памяти с теми же старыми инструкциями.

если вы писали оптимизации компилятор / байт-код VM для многоядерного процессора, что вам нужно знать конкретно, скажем, x86, чтобы заставить его генерировать код, который эффективно работает на всех ядрах?

вы запускаете тот же код, что и раньше. Это ядро Unix или Windows, которое необходимо изменить.

вы можете резюмировать мой вопрос Так: "какие изменения были внесены в машинный код x86 для поддержки многоядерных функций?"

ничего не было необходимый. Первые системы SMP использовали тот же набор команд, что и юнипроцессоры. Теперь было много эволюции архитектуры x86 и миллионы новых инструкций, чтобы сделать все быстрее, но ни один из них не был необходимые для SMP.

для получения дополнительной информации см. Спецификация Мультипроцессора Intel.


обновление: на все последующие вопросы можно ответить, просто полностью согласившись с тем, что n-способ многоядерного процессора почти1 точно так же как n отдельные процессоры, которые просто разделяют одну и ту же память.2 не был задан важный вопрос:как программа написана для работы на более чем одном ядре для большей производительности? и ответ: он написан с использованием библиотеки потоков, как Pthreads. некоторые библиотеки потоков используют "зеленые потоки", которые не видны ОС, и те не получат отдельные ядра, но до тех пор, пока библиотека потоков использует функции потока ядра, ваша потоковая программа автоматически будет многоядерной.
1. Для обратной совместимости только первое ядро запускается при сбросе, и нужно сделать несколько вещей типа драйвера, чтобы запустить оставшиеся.
2. Они также разделяют все периферийные устройства, естественно.

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

также нет дополнительной инструкции, доступной только для операционной системы. Эти ядра идентичны одноядерным чипам. Каждое ядро запускает часть операционной системы, которая будет обрабатывать связь с общими областями памяти используется для обмена информацией, чтобы найти следующую область памяти для выполнения.

это упрощение, но оно дает вам основное представление о том, как это делается. подробнее о многоядерных и многопроцессорных системах on Embedded.com имеет много информации по этой теме ... Эта тема усложняется очень быстро!

Если вы писали оптимизации компилятор / байт-код виртуальной машины для многоядерного процессора CPU, что вам нужно знать конкретно про, скажем, x86 сделать он генерирует код, который работает эффективно по всем ядрам?

Как кто-то, кто пишет оптимизирующий компилятор/байт-код VMs, я могу помочь вам здесь.

вам не нужно знать ничего конкретно о x86, чтобы заставить его генерировать код, который эффективно работает на всех ядрах.

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

возможно, вам нужно что-то знать о x86, чтобы заставить его генерировать код, который эффективно работает на x86 в целом.

есть и другие вещи, которые было бы полезно для вас узнать:

вы должны узнать о средствах, которые предоставляет ОС (Linux или Windows или OSX), чтобы позволить вам запускать несколько потоков. Вы должны узнать об API распараллеливания, таких как OpenMP и Threading Building Blocks, или OSX 10.6 "Snow Leopard"предстоящий "Grand Central".

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

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

Это вообще не делается в машинных инструкциях; ядра притворяются отдельными процессорами и не имеют никаких специальных возможностей для общения друг с другом. Они общаются двумя способами:

  • они совместно используют физическое адресное пространство. Аппаратное обеспечение обрабатывает когерентность кэша, поэтому один процессор записывает адрес памяти, который другой читает.

  • они совместно с APIC (программируемый контроллер прерываний). Это памяти в физическую адресное пространство и может использоваться одним процессором для управления другими, включения или выключения их, отправки прерываний и т. д.

http://www.cheesecake.org/sac/smp.html это хорошая ссылка с глупым url.

основное различие между однопоточным и многопоточным приложением заключается в том, что первое имеет один стек, а второе-по одному для каждого потока. Код генерируется несколько иначе, так как компилятор будет считать, что регистры сегментов данных и стека (ds и ss) не равны. Это означает, что косвенное обращение через регистры ebp и esp, которые по умолчанию используются в регистре ss, также не будут использоваться по умолчанию для ds (потому что ds!=пароход.) И наоборот, косвенность через другие регистры, которые по умолчанию ds не будет по умолчанию для ss.

потоки разделяют все остальное, включая данные и код. Они также разделяют процедуры lib, поэтому убедитесь, что они потокобезопасны. Процедура, которая сортирует область в ОЗУ, может быть многопоточной, чтобы ускорить процесс. Затем потоки будут получать доступ, сравнивать и упорядочивать данные в одной и той же области физической памяти и выполнять один и тот же код, но с использованием разных локальных переменных для управления их соответствующей частью сортировки. Это, конечно, потому что потоки имеют разные стеки, в которых содержатся локальные переменные. Этот тип программирования требует тщательной настройки кода, чтобы уменьшить межъядерные коллизии данных (в кэшах и ОЗУ), что, в свою очередь, приводит к коду, который быстрее с двумя или более потоками, чем с одним. Конечно, не настроенный код часто будет быстрее с одним процессором, чем с двумя или более. Отладка является более сложной задачей, потому что стандартная точка останова "int 3" не будет применима, так как вы хотите прерывайте определенный поток и не все из них. Точки останова регистра отладки также не решают эту проблему, если вы не можете установить их на определенном процессоре, выполняющем определенный поток, который вы хотите прервать.

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

то, что было добавлено в каждую многопроцессорную архитектуру по сравнению с однопроцессорными вариантами, которые были до них,-это инструкции для синхронизации между ядрами. Кроме того, у вас есть инструкции по работе с когерентностью кэша, буферами промывки и аналогичными низкоуровневыми операциями, с которыми приходится иметь дело ОС. В случае одновременных многопоточных архитектур, таких как IBM POWER6, IBM Cell, Sun Niagara и Intel "Hyperthreading", вы также можете увидеть новые инструкции по приоритезации между потоками (например, устанавливая приоритеты и явно уступая процессору, когда нечего делать).

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