…Продолжение:

В сфере высокопроизводительных серверов тоже намечается подобная тенденция: кроме основных ядер CPU, добавляются специальные блоки или сопроцессоры (например, для ускорения машинного обучения, шифрования, обработки сетевых пакетов). Это не совсем про RISC/CISC, но про интеграцию разных типов вычислительных ресурсов на одном кристалле, оптимизированных под разные нагрузки.

Энергоэффективность сегодня – ключевой показатель. Не случайно, ARM стала проникать даже в серверы: компания Amazon, например, разрабатывает серверные процессоры Graviton на ARM, получая выгоду в меньшем тепловыделении и стоимости владения. Оценки показывают, что ARM-решения могут давать до 40% экономии энергии при сопоставимых нагрузках. Даже на уровне настольных ПК мы видим фокус на производительность на ватт – хороший пример Apple M1/M2 чипов, где 10-ваттный процессор способен конкурировать с 45-ваттными Intel благодаря архитектурной эффективности.

Наконец, отметим тренд к распределению задач между специализированными процессорами. Центральный процессор уже не единственный «мозг»: ему помогают GPU (графические процессоры) для массово параллельных вычислений, TPU/NPU (тензорные процессоры или нейронные) для AI-алгоритмов, DSP для обработки сигналов и др. Однако в контексте курса ОС нас интересует прежде всего CPU и взаимодействие с ОС. Поэтому сейчас, разобрав основные характеристики современных процессоров, перейдем к тому, как процессор взаимодействует с остальной системой, особенно с оперативной памятью и операционной системой.

Роль процессора в системе: взаимодействие с памятью и операционной системой

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

Взаимодействие с памятью. Процессор обращается к памяти при каждом шаге работы: ему нужно получать инструкции программы, читать операнды, записывать результаты. Связь между CPU и памятью осуществляется через систему шин: шина адреса (процессор указывает, к какому адресу памяти он хочет обратиться), шина данных (по ней передаются сами данные или инструкции), шина управления (сигналы чтения/записи, подтверждения и пр.). Когда CPU выполняет команду выборки или загрузки/выгрузки данных, он ставит на шины соответствующие сигналы, и контроллер памяти осуществляет обмен. Основная память (RAM) хранит и сам код выполняющихся программ, и их данные, а процессор по мере надобности загружает части этих данных в кэш/регистры. Из-за того, что память гораздо медленнее CPU, введены упомянутые ранее многоуровневые кэши, предвыборка данных (prefetching) и прочие оптимизации. Но логически картина такова: процессор запрашивает – память отвечает.

При этом важный аспект – адресация памяти и роль ОС в управлении памятью. Современные процессоры обычно поддерживают виртуальную память – механизм, при котором каждая программа видит как бы свой непрерывный адресный простор, а аппаратный блок (Memory Management Unit, MMU) транслирует виртуальные адреса в физические адреса оперативной памяти. Эта трансляция осуществляется с помощью таблиц страниц, которые настраивает ОС. Зачем это нужно? Во-первых, для изоляции процессов: один процесс не может напрямую залезть в память другого, т.к. их виртуальные адреса привязаны к разным физическим рамкам и проверяются на каждое обращение. Во-вторых, для эффективного использования памяти: ОС может размещать программы разреженно, подгружать по частям (постранично), выгружать неиспользуемые части на диск (подкачка), а процессор с MMU будет автоматически делать нужные обращения (если программа обращается к не загруженной странице – возникает специальное прерывание отсутствия страницы, ОС догружает данные с диска и продолжает выполнение). Таким образом, процессор предоставляет механизм (MMU, режимы адресации), а ОС реализует политику управления памятью (какие страницы где хранить, когда перемещать и т.д.).

CPU и режимы выполнения. Процессор обычно имеет как минимум два режима: привилегированный (режим ядра) и пользовательский. В пользовательском режиме выполняется код приложений, и процессор аппаратно запрещает этому коду выполнять опасные действия: например, напрямую обращаться к устройствам ввода-вывода, изменять настройки MMU, или прерывать другие процессы. Если приложение пытается что-то такое сделать (например, выполнить привилегированную команду или обратиться к чужому адресу памяти), процессор генерирует исключение, и управление переходит к ОС (в режим ядра) для разбирательства. Операционная система работает в привилегированном режиме (режиме ядра) и поэтому имеет полный доступ к оборудованию. Этот механизм – основа безопасности и стабильности: сбойный пользовательский процесс не может обрушить всю систему, он изолирован и ограничен, а ОС контролирует доступ к ресурсам. Переключение между режимами происходит либо при системных вызовах (когда программа сознательно обращается к ОС через специальную инструкцию, например, int 0x80 в x86 или svc в ARM), либо при прерываниях/исключениях (непредвиденное событие, требующее вмешательства ядра). Процессор поддерживает эти переходы: при входе в прерывание он автоматически переключает режим на ядро, сохраняет указатель команд и статус, и начинает выполнять код обработчика, адрес которого хранится в специальной таблице (вектор прерываний). По завершении – возврат, режим переключается обратно на пользовательский, и приложение продолжает работу.

Планирование задач и переключение контекста. В многозадачной системе одна из важнейших функций ОС – планировщик процессов/потоков. Процессор обычно исполняет в каждый момент только один поток инструкций на ядро (SMT убираем из рассмотрения – там 2 потока, но все равно ограниченное число). При наличии десятков задач ОС должна распределять процессорное время между ними. Делается это с помощью вытесняющей многозадачности: ОС запускает задачу на CPU, дает ей поработать небольшой квант времени (например, 10-50 миллисекунд), затем прерывает (с помощью таймерного прерывания) и переключает на другую. То, как именно происходит переключение: как только поступает сигнал от таймера, процессор прерывает текущий процесс, переключается в режим ядра и передает управление ядру ОС. Код планировщика ОС сохраняет текущее состояние (регистры, счетчик команд, флаги – все, что определяет контекст выполнявшегося процесса) в память, в структуру, ассоциированную с этим процессом. Затем ОС выбирает следующую задачу, которую пора запустить (по своим алгоритмам планирования), загружает ее сохраненный контекст в соответствующие регистры и устанавливает счетчик команд на место, где та остановилась. Далее ОС переводит процессор обратно в пользовательский режим и передает управление этому (теперь уже другому) процессу. Новый процесс продолжает выполняться с того места, где был приостановлен, «не подозревая», что CPU за это время делал что-то еще. Все описанные действия занимают считанные микросекунды. Благодаря такой чередующейся активности, создается иллюзия одновременной работы множества программ – многозадачность. Роль процессора здесь – быстро выполнять команды, роль ОС – решать, какую работу дать процессору следующей и обеспечить сохранность/восстановление контекстов. Процессорные механизмы прерываний и режимов делают возможным безопасное переключение: CPU гарантирует, что при переходе в ядро управление передается ОС, и пользовательский код не может вмешаться. А ОС уже на CPU выполнила всю «грязную работу» по смене контекста, и затем доверила CPU исполнение нового приложения.

Взаимодействие с устройствами. Взаимодействие CPU и внешних устройств тоже опосредуется ОС. Обычно устройства (диски, сети, экраны и т.п.) имеют свои контроллеры, которые могут обмениваться данными с памятью (DMA) или через специальные порты. CPU предоставляет ОС инструкции или порты для управления устройствами. Например, чтобы передать данные на диск, ОС загружает адрес данных и команду в регистры контроллера (через специальные команды ввода-вывода или запись в память, отображенную на устройство). Далее устройство самостоятельно выполняет операцию (занимаясь вводом-выводом параллельно с работой CPU). Когда устройство закончило (или произошло событие, например, пришел пакет по сети), оно посылает прерывание CPU. Процессор по прерыванию заходит в ядро, которое вызывает соответствующий драйвер устройства. Драйвер считывает результаты, обслуживает событие – к примеру, передает полученные сетевые данные программе. Затем возвращается к прерванной задаче. Таким образом, CPU и периферия взаимодействуют асинхронно через механизмы прерываний и посредством драйверов ОС. Процессор в этом «оркестре» выступает исполнителем универсальных вычислений и координатором: он выполняет код ОС, который управляет устройствами, и сам же обрабатывает данные, поступившие от устройств.

Важный компонент – система команд привилегированного уровня: процессор имеет ряд инструкций, доступных только в режиме ядра, для настройки оборудования, управления памятью, выполнения специальных операций (например, HLT – остановка, STI/CLI – запрет/разрешение прерываний, управление контроллером прерываний, настройка сегментов/таблиц страниц и т.д.). ОС, работая в привилегированном режиме, использует эти инструкции для настройки системы. Пользовательские же программы ограничены базовым набором команд и не могут напрямую изменить, например, адресное пространство. Если бы процессор не разграничивал уровни, один неосторожный (или вредоносный) код мог бы вывести из строя всю систему. Поэтому процессор на аппаратном уровне поддерживает защиту памяти и разграничение прав, а ОС это активно эксплуатирует для стабильности и безопасности.

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

Чтобы закрепить, рассмотрим сценарий запуска программы на ОС. Пользователь кликает иконку приложения. ОС, работая на CPU, загружает исполняемый файл приложения с диска в память, создает процесс, резервирует ему страницы (настраивает MMU таблицы). Затем ОС планировщиком решает запустить этот процесс: инициализирует его контекст (регистры обнуляет, PC ставит на начало программы), переключает CPU на его выполнение. CPU начинает выполнять инструкции приложения, при необходимости обращаясь к памяти (что OS предусмотрительно этому процессу выделила). Когда приложение делает, скажем, запрос на ввод (системный вызов), CPU переключается в ядро, ОС берет управление, вызывает драйвер, тот ждет события. Пока ожидание – ОС может запланировать другой процесс на CPU. Так, CPU постоянно переключается между задачами по указанию ОС. В конечном счете, приложение завершается – тогда ОС освободит его память. А CPU перейдет на исполнение других задач.

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

На этом мы завершаем лекцию, объединившую основы микроэлектроники (на уровне транзисторов) с принципами работы процессора и его взаимодействия с программным обеспечением. Мы увидели, как из физических переключателей – транзисторов – вырастают логические схемы, из них – вычислительные блоки, затем целые процессоры, и как эти процессоры работают в компьютере под управлением операционной системы.

1. Структура MOSFET и представление логических 0 и 1

MOSFET (металло-оксидный полевой транзистор) – ключевой элемент современной цифровой электроники. Он состоит из четырех основных частей: затвора, канала, истока и стока (подложка обычно объединена с истоком). Затвор – управляющий электрод, отделенный от канала тонким слоем диэлектрика (оксид кремния). Канал образуется в полупроводниковой подложке между областями истока и стока.

Когда на затвор подается положительное напряжение относительно истока (для n-канального MOSFET), между затвором и подложкой возникает электрическое поле. Это поле «открывает» канал – притягивает носители заряда (электроны) к поверхности подложки под затвором, формируя проводящий слой. В результате между стоком и истоком появляется проводящий «мостик», и ток может течь от стока к истоку. Такой режим соответствует включенному транзистору (логическому «1» на затворе в контексте цифровой схемы). Если же напряжение на затворе отсутствует или недостаточно велико, канал закрыт – ток не проходит, что соответствует выключенному состоянию транзистора (логическому «0»). Таким образом, подавая высокий или низкий потенциал на затвор, мы заставляем MOSFET действовать как электронный переключатель: проводить ток (логическая «1») или блокировать его (логический «0»).

В цифровых интегральных схемах применяется технология CMOS, где каждый логический элемент реализуется парой комплементарных MOSFET (n-канальный и p-канальный). Например, в инверторе n-MOSFET соединяет выход с землей при лог.1 на входе, а p-MOSFET – с питанием при лог.0, что дает на выходе противоположный уровень. Подобным образом комбинации транзисторов позволяют получать нужные логические функции.

Аналогия: можно представить транзистор как электрический кран: затвор – это ручка, управляющая потоком. Когда «ручка» (затвор) повёрнута (напряжение есть), «вода» (электрический ток) течёт через канал; когда ручка закрыта (нет напряжения) – поток прекращается. Благодаря этой способности быстро переключаться между проводящим и запертым состоянием MOSFET служит базовым переключателем, реализующим двоичную логику 0/1 на аппаратном уровне.

2. Логические элементы на транзисторах: NAND, NOR, триггеры и мультиплексоры

Логические элементы – это электронные схемы, выполняющие базовые булевы операции. MOSFET-транзисторы позволяют строить такие элементы с помощью комбинаций включения транзисторов. Рассмотрим несколько примеров:

  • Элемент NOT (инвертор): состоит из одной пары транзисторов CMOS. Входной сигнал подается на затворы n- и p-канального MOSFET. При входе «1» n-транзистор открывается и соединяет выход с землей (логический 0), в то время как p-транзистор закрыт. При входе «0» – наоборот, p-MOSFET подключает выход к питанию (логическая 1), а n-MOSFET закрыт. Так реализуется инверсия сигнала.

  • Элемент NAND («НЕ-И»): реализуется парой p-канальных транзисторов параллельно между выходом и питанием, и парой n-канальных последовательно между выходом и землей. Только когда оба входа 1, оба n-MOSFET проводящие и тянут выход к 0 (через цепочку на землю), а p-MOSFET при 1 на входах закрыты – получается выход 0. Во всех других случаях хотя бы один из n-транзисторов разомкнут или хотя бы один p-транзистор замкнут на +V, поэтому выход подтягивается к 1. Это соответствует таблице истинности NAND: результат «0» лишь при обоих входах «1», иначе «1». NAND – универсальный элемент, на практике часто реализуется минимальным числом транзисторов (4 транзистора на 2 входа в CMOS).

  • Элемент NOR («НЕ-ИЛИ»): имеет обратную топологию – p-канальные транзисторы ставятся последовательно между +V и выходом, а n-канальные – параллельно между выходом и землей. Логика: при обоих входах «0» p-транзисторы обе проводящие (последовательно) и подтягивают выход к 1, n-транзисторы оба закрыты, давая выходу остаться высоким – получаем 1 (NOR истинна при 0,0). Если хотя бы один вход «1», соответствующий n-MOSFET открывается и замыкает выход на землю (0), в то время как хотя бы один p-MOSFET в цепи питания будет разомкнут, не давая выйти 1. В итоге NOR дает «1» только при обоих нулях на входах, иначе «0». NOR, как и NAND, является универсальным: из одного типа элемента можно собрать любую логическую функцию.

  • RS-триггер (бистабильный триггер на элементах NOR или NAND): триггер – это простейшая память на один бит, выполненная с обратной связью. Классический асинхронный RS-триггер можно построить из двух перекрестно соединенных NOR-элементов. Выход каждого NOR подается на второй вход другого, а свободные два входа служат S (Set, установка) и R (Reset, сброс). При подаче импульса на вход S триггер устанавливается (выход Q = 1), на вход R – сбрасывается (Q = 0). Когда оба входа неактивны (низкий уровень), схема сохраняет предыдущее состояние за счет перекрестной положительной обратной связи: выходы «запирают» друг друга в текущем сочетании 0/1. Реализовать такой триггер можно и на транзисторах: каждый NOR – это уже цепочка транзисторов, всего получится 8 MOSFET на RS-триггер. В синхронных триггерах (D-, JK-триггеры) добавляются схемы синхронизации (например, гейты или передающие транзисторы) для захвата входных данных по фронту тактового сигнала.

  • Мультиплексор – схема, выбирающая один из нескольких входов по управляющему коду. 2-в-1 мультиплексор можно реализовать на логических элементах: две AND и один OR. Например, при выборе между A и B по селектору S: первая AND пропускает A, если S=0 (через инверсию S на входе), вторая AND пропускает B, если S=1; выход OR складывает их, выдавая либо A, либо B. На уровне транзисторов мультиплексор можно реализовать через передающие транзисторы (pass-transistors): один MOSFET соединяет выход с A при S=0, другой – с B при S=1, а встречные транзисторы другого типа обеспечивают правильные уровни. Таким образом, в зависимости от состояния управляющего сигнала один из путей проводимости открывается и передает соответствующий входной сигнал на выход. Мультиплексоры играют важную роль в процессорах, направляя потоки данных между блоками.

Итого: используя MOSFET в различных комбинациях, мы получаем базовые логические «кирпичики» цифровой схемотехники. NAND и NOR – фундаментальные универсальные элементы, из которых можно синтезировать любые логические функции. Триггеры на транзисторах обеспечивают хранение 1 бита (память и элементы последовательной логики). Мультиплексоры позволяют переключать сигналы, выбирая источники данных. Эти примитивы далее комбинируются в более сложные узлы.

3. Функциональные блоки процессора: сумматор, счетчик, регистр, дешифратор

Из простых логических элементов можно построить более сложные функциональные узлы, составляющие «начинку» процессора. Рассмотрим некоторые из них:

  • Бинарный сумматор. Основой арифметики процессора служит двоичный сумматор. Простейшая версия – полусумматор, складывающий два однобитовых числа A и B без учета переноса. Он формирует два выхода: бит суммы S (сумма по модулю 2) и бит переноса C в следующий разряд. Логически: S = A XOR B, C = A AND B. Реализация: можно использовать 1 элемент XOR (исключающее ИЛИ) для S и 1 элемент AND (логическое И) для C. Для сложения многобитных чисел требуется полный сумматор, учитывающий входной перенос от младшего разряда. Полный сумматор строится из двух полусумматоров и одного OR: первый полусумматор складывает A и B, выдавая промежуточные s и c; второй складывает s и входной перенос p, а выходные переносы объединяются на OR как итоговый перенос. В итоге полный сумматор на три входа (A, B, p) выдает S и новый перенос. В цифровой схеме сумматоры каждого разряда объединяются каскадом – перенос с выхода каждого разряда идет на вход следующего. Так реализуется Ripple Carry Adder (сумматор с последовательным переносом). Его недостаток – задержка, равная времени прохождения цепочки переносов. Более быстрые схемы используют ускоренный перенос (carry lookahead) и прочие методы, но принцип остаётся: на логических элементах реализуются функции суммирования. Сумматор – ядро АЛУ (арифметико-логического устройства), выполняющего операции сложения, вычитания (через инверсию B и сложение с переносом 1), инкремента, а также логические операции.

  • Счетчик. Счетчик – это регистр, который автоматически изменяет свое значение, обычно увеличивая на 1 при каждом тактовом импульсе. Простейший двоичный счетчик можно собрать на последовательности T-триггеров (Toggle-триггеров), где каждый триггер переключает свое состояние на противоположное при поступлении импульса на вход. Один T-триггер работает как счётчик по модулю 2 (он переключается 0→1 или 1→0 каждый такт, считая два состояния). Чтобы считать далее (модуль 4, 8, 16 и т.д.), соединяют несколько триггеров последовательно: выход первого (младшего разряда) подают как тактовый импульс на следующий триггер и т.д. – получается регистр из триггеров, где каждый разряд представляет следующий бит числа. При каждом переполнении младшего разряда (переходе из 1 в 0) на соседнем происходит переключение. Таким образом, сочетание T-триггеров дает двоичный счетчик: на выходах триггеров формируется двоичное число, увеличивающееся на 1 при каждом фронте входного такта. Например, 4-триггерная цепочка считает от 0000₂ до 1111₂ (0–15). Такой счетчик – асинхронный (переключение разрядов идет каскадом с небольшой задержкой), что упрощает схему, но вызывает рассинхронизацию сигналов. В синхронных счетчиках всех триггеров тактируют одновременно, а переключение одних разрядов условно блокируется логикой, пока не произошли определенные события на младших. Например, для синхронного счетчика с параллельным переносом добавляют элементы И на входы старших триггеров, пропуская импульс только если все младшие биты стали 1. Так уменьшаются задержки. Счетчики широко применяются: от генерации адресов и отсчета циклов до делителей частоты.

  • Регистр. Регистр – комбинация нескольких триггеров, хранящая двоичный код (многоразрядное число). Каждый триггер хранит 1 бит, поэтому для n-битного регистра нужно n триггеров. Все триггеры синхронизируются общим тактовым сигналом, чтобы одновременно записывать новое значение. Например, регистр хранения на D-триггерах: на D-входы триггеров подается соответствующий бит входного числа, а с приходом тактового фронта все триггеры одновременно запоминают свои входные значения. Есть вход R (reset) для асинхронного сброса – установки всех битов в 0 (начальное состояние). Пока новый сигнал записи не поступает, регистр удерживает сохраненное слово неограниченно долго (при условии питания). Таким образом, регистр функционирует как элемент памяти, способный по команде сохранить многобитовую информацию и предоставить её на выходах. В процессорах регистры используются повсеместно: от внутренних вычислительных регистров (аккумуляторов, регистров общего назначения) до специальных (счетчик команд, регистр состояния) – все они по сути реализованы как массив триггеров. Отдельно отметим сдвиговые регистры – особый вид, где выход одного триггера подключен ко входу следующего. Сдвиговый регистр на каждом такте передвигает сохраненное двоичное число на один разряд влево или вправо, позволяя выполнять умножение или деление на 2, а также служит для последовательного ввода-вывода данных.

  • Дешифратор. Дешифратор – комбинационное устройство, преобразующее двоичный код на входе в унарный код на выходах. Проще говоря, он принимает n-битовый вход и активирует один из 2^n выходов, соответствующий числу, закодированному на входе. Например, 2-битный дешифратор имеет входы s0, s1 и 4 выхода z0..z3. Если на входе код 10₂ (=2), то выход z2 становится «1», а остальные «0». Реализация дешифратора: каждый выход yi можно задать логическим выражением, которое истинно только при определенной комбинации входов. Так, в 2-to-4 дешифраторе:

    • y0 = ¬s1 AND ¬s0 (активен при входе 00₂),

    • y1 = ¬s1 AND s0 (активен при 01₂),

    • y2 = s1 AND ¬s0 (для 10₂),

    • y3 = s1 AND s0 (для 11₂).
      Каждое такое выражение реализуется схемой И для прямых или инверсных сигналов s0/s1. Для инверсий входов используются инверторы. Таким образом, дешифратор строится из AND-элементов (по числу выходов) и инверторов на входах. В сумме, n-к-2^n дешифратор содержит 2^n выходных комбинационных логических функций. Зачем нужен дешифратор? В процессоре он используется, например, для адресации памяти – чтобы выбрать нужную ячейку или устройство. Адресная шина подается на дешифраторы в микросхемах памяти: поступивший адрес активирует единственный выход – линию выборки конкретной ячейки или блока памяти. Другой пример – дешифратор команд в управляющем устройстве: опкод (код операции) поступает на дешифратор, который активирует одну из многих линий – ту самую микрокоманду или ветвь управления, соответствующую данной инструкции. Таким образом, дешифраторы позволяют из компактного бинарного кода получить сигнал выбора одного из множества возможных объектов.

4. Шинная организация: адресная, управляющая и шина данных

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

  • Шина данных – двунаправленная многопроводная линия для передачи собственно данных между узлами (процессором, оперативной памятью, контроллерами ввода-вывода). Шина данных имеет ширину, равную разрядности процессора или доступного слова – например, 32-бит или 64-бит – это число бит, которое может передаваться одновременно за один цикл. Все устройства подключены параллельно к этим линиям, но за раз данные передает только одно устройство (тот, кто управляет шиной в данный момент). Например, при чтении из памяти: память выставляет считанное слово на шину данных, а процессор эти биты считывает; при записи – процессор выдает данные на шину, память принимает. Шина данных позволяет передачу в обоих направлениях (к процессору и от него). Пропускная способность (сколько байт/сек можно передать) зависит от ширины и частоты шины.

  • Шина адреса – набор линий, по которым передается адрес памяти или устройства. Адресная шина обычно выходит только от процессора (или другого мастер-устройства) к остальным компонентам – то есть однонаправленная: CPU указывает, к какой ячейке памяти или к какому порту ввода-вывода он обращается. Ширина адресной шины определяет максимально адресуемое пространство. Например, 20-битная шина адреса в ранних ПК позволяла адресовать 2^20 байт = 1 МБ памяти; современный 36-разрядный адрес – до 2^36 ≈ 68,7 млрд адресов. При обращении, процессор выставляет двоичный адрес на эти линии – вся память и устройства его одновременно «видят», но откликается только то устройство, которому принадлежит данный адрес. В памяти дешифраторы адреса выбирают нужную ячейку. Таким образом, адресная шина – это как «улица», по которой процессор указывает, куда должны пойти данные. Отметим, адресная шина может использоваться и для I/O устройств, либо совместно с памятью (MMIO), либо отдельной областью адресов – об этом далее.

  • Шина управления – линии для передачи управляющих сигналов и сигналов синхронизации. Эти сигналы определяют тип операции и координируют взаимодействие. В типичную шину управления входят сигналы: RD/WR (чтение или запись – указывает памяти, что мы делаем), MEM/IO (что адрес указывает на память или на устройство ввода-вывода, если разделены адресные пространства), ALE (адресный строб – в некоторых системах сигнал, фиксирующий момент валидности адреса на шине), ACK или READY (подтверждение от памяти/устройства, что данные готовы), сигналы тактирования и т.д. Например, чтобы прочитать память, CPU выставляет адрес на адресной шине, ставит сигналы READ = 1 (операция чтения) на шине управления; по этому сигналу блок памяти через некоторое время размещает данные на шине данных и подает сигнал готовности. Шина управления также несет синхроимпульсы (например, CLOCK) в синхронных системах для согласования моментов выборки сигналов. В итоге, комбинация битов на шине управления говорит: “это чтение из памяти по такому-то адресу” или “запись в устройство по такому адресу” и т.п., и позволяет подключенному модулю понять роль передаваемых адресов/данных.

Все три подсистемы – адрес, данные, управление – вместе образуют системную магистраль, по которой процессор взаимодействует с памятью и периферией. Типичная операция (например, цикл чтения из памяти) проходит поэтапно:

  1. Фаза адреса: Процессор ставит адрес на шину адреса, устанавливает линии управления (например, READ, тип цикла). Адресным линиям дается время стабилизироваться.

  2. Фаза ожидания/память: Память дешифрует адрес, подготавливает данные. Если она не готов мгновенно, может удерживать процессор (сигналом WAIT).

  3. Фаза данных: Память размещает данные на шине данных и утверждает сигнал готовности/валидности. Процессор считывает слово с шин данных на входные буферы.

  4. Завершение: Процессор убирает сигнал чтения, возможно, освобождает шину адреса (если мультиплексируется). Цикл завершен, шина готова к следующему обмену.

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

Важно отметить, что шина – общая среда, поэтому в каждый момент только один активный «ведущий» (master) должен передавать адрес и команды, иначе сигналы столкнутся. Процессор обычно главный инициатор (master) для доступа к памяти и портам. Если несколько мастеров (DMA-контроллеры, сопроцессоры), требуется механизм арбитража шины – решать, кто пользуется шиной в текущий момент.

Таким образом, системная шина связывает все части ЭВМ воедино. Адресная шина говорит «кому предназначены данные», шина данных переносит сами биты информации, шина управления сообщает «что делать с этими данными» и синхронизирует процесс. Вместе они обеспечивают согласованное общение CPU с памятью и устройствами в режиме, прозрачном для программиста (в программной модели это просто операция чтения/записи по адресу).

5. Ввод-вывод: шина устройств, порты и Memory-Mapped I/O (MMIO)

Процессор обменивается данными не только с оперативной памятью, но и с разнообразными периферийными устройствами: накопителями, дисплеем, сетевыми интерфейсами и пр. Эти устройства подключаются либо к общей системной шине, либо через специальные шины ввода-вывода (например, PCI, USB и др., через мосты). Рассмотрим две основные схемы адресации устройств: памятно-мапированный ввод-вывод (MMIO) и ввод-вывод через порты.

Memory-mapped I/O (ввод-вывод через память). В этом подходе устройства ввода-вывода включены в единое адресное пространство памяти. То есть некоторые физические адреса, которые процессор выдает на шину, на самом деле не относятся к ОЗУ, а направлены к регистрам устройств. С точки зрения процессора, доступ к устройству выглядит как чтение/запись по обычному адресу памяти – никакой разницы в командах. На аппаратном уровне различие в том, что блок логики (чипсет/контроллер) перехватывает диапазоны адресов, зарезервированные за устройствами, и перенаправляет транзакции на шины устройств. Устройства подключены к шине памяти и распознают свои «адреса», как ОЗУ распознает свои. Например, видеопамять или регистры видеокарты могут отображаться по адресам 0xA0000–0xBFFFF – CPU записывает по этому адресу, а данные идут не в ОЗУ, а в видеоконтроллер. Преимущество MMIO – унификация: любые инструкции и режимы доступа к памяти автоматически работают и с устройствами (можно использовать, например, команду MOV для записи байта в регистр периферии). Недостаток – выделяется часть адресного пространства на устройства, отнимая его от ОЗУ (в 32-битной адресации, например, часто верхние адреса 0xF0000000 и выше зарезервированы под устройства).

Port-mapped I/O (ввод-вывод через порты). Альтернативный подход – процессор имеет отдельное адресное пространство портов ввода-вывода, и специальные машинные инструкции для работы с ним. Так реализовано, например, в архитектуре x86. У процессора x86 имеются команды IN и OUT (а также вариации INS/OUTS) для чтения/записи портов. Порт ввода-вывода – это, по сути, адрес в отдельном 16-битном пространстве (диапазон 0x0000–0xFFFF у Intel). Эти адресные линии портов физически используются те же, что и для памяти, но сопровождаются специальным сигналом M/IO (Memory/IO) на шине управления, указывающим, что данный адрес нужно трактовать не как память, а как порт устройства. Устройства «слушают» шину, и если M/IO сигнализирует портовый доступ, адрес интерпретируется как номер порта – таким образом, периферия выделяет у себя регистры, доступные по номерам портов. Например, классическая клавиатура PC доступна по портам 0x60 (данные) и 0x64 (статус/управление). Процессор, выполняя IN AL, 60h, выставляет 0x0060 на адресные линии, ставит флаг «IO access» на шине управления, и контроллер клавиатуры отвечает байтом с кодом на шину данных. Адресное пространство портов обычно меньше (у x86 это 64KB) и разрядность порта меньше адресной шины памяти, но имеет свое назначение.

Два этих метода – раздельное и объединенное адресное пространство – имеют свои плюсы. Через порты: не «съедается» память, инструкции IN/OUT сразу ясно показывают, что идет работа с устройством, а не с ОЗУ. В ранних микропроцессорах с узкой шиной адреса (8- или 16-разрядной) отдельные порты позволяли адресовать больше устройств, чем позволила бы память. Через память: упрощается архитектура процессора (не нужно отдельного блока команд для портов), ускоряется доступ – можно применять любые команды чтения/записи, блочные, векторные, к устройствам как к памяти. Современные RISC-процессоры почти все используют MMIO, а x86 сохраняет поддержку портов по историческим причинам (для совместимости с legacy-устройствами типа контроллера клавиатуры, который традиционно висел на портах 0x60/64).

Как общается CPU с устройством? Рассмотрим пример. Предположим, у нас MMIO и некий регистр статуса устройства находится по физическому адресу 0xF000_0010. Процессор выполняет обычную машинную команду чтения из этого адреса (скажем, MOV EAX, [0xF0000010]). Контроллер шины замечает, что адрес принадлежит области устройств, и направляет запрос не в ОЗУ, а соответствующему устройству на шине ввода-вывода. Устройство, получив сигнал чтения, выставляет значение своего регистра на шину данных, и CPU его получает как будто из памяти. В случае портового ввода-вывода, CPU вместо MOV использует инструкцию IN – которая внутренне делает похожее: выдает адрес порта на адресные шины, ставит сигнал «IO», который активирует декодеры портов в устройстве. Далее устройство отвечает данными. В обоих случаях фактически происходит шина-взаимодействие, но с той разницей, что при портовом доступе архитектура CPU разводит команды по отдельным инструкциям и линиям, а при MMIO – унифицирует их с памятью.

В архитектуре x86 используются оба подхода: есть отдельные 16-битные порты (куда доступ через IN/OUT), а многие современные контроллеры (например, видеокарта, сетевой адаптер) доступны через память, маппя свои регистры в верхние адреса RAM. Более того, x86 поддерживает механизм проецирования портов в память (через специальные таблицы в защищенном режиме) – это позволяет программно обращаться к портам обычными инструкциями памяти, но аппаратно CPU все равно выполнит последовательность IN/OUT под капотом.

Шина ввода-вывода. Физически устройства могут быть подключены к той же системной шине (особенно в старых компьютерах или простых микроконтроллерах, где периферия «висят» на общей шине адреса/данных наряду с ОЗУ). В современных ПК архитектура более сложная: существует набор шин (PCIe, SATA, USB и пр.), соединенных с процессором через контроллеры и мосты. Но логически для процессора они образуют продолжение адресного пространства. Процессор через чипсет получает доступ к устройствам, не заботясь об их физической шине – будь то PCI Express или встроенный контроллер. Например, запись по адресу определенного регистра USB-контроллера чипсет трансформирует в транзакцию на шине PCIe. Это уровень ниже архитектуры CPU, но упомянем: шины ввода-вывода имеют собственные протоколы (PCI – параллельная, PCIe – последовательная, USB – последовательная с пакетами), но роль остается: доставка адресованных запросов на устройство и ответов от него.

MMIO vs Port I/O – итоги: ISA архитектуры вроде ARM, MIPS не имеют специальных команд для портов – все через обычные Load/Store по адресам (MMIO). Архитектура x86 имеет обе механики: программы могут общаться с некоторыми устройствами через порты (команды IN, OUT), но большинство современных периферий работает через MMIO (например, видео, сеть).

Ввод-вывод также связан с прерываниями и DMA, но это выходит за рамки данной темы. Важно понимать, что адресное пространство ввода-вывода – это часть архитектурного дизайна: либо совмещенное с памятью (единое пространство), либо отдельное. MMIO упрощает дизайн CPU и программиста – не надо думать о специальных инструкциях, можно мапить буферы устройств как массивы в памяти, но требует осторожности: чтение/запись по таким адресам может иметь побочные эффекты (запись – вызвать действие устройства, чтение – очистить флаги и т.п.). Отдельные порты – исторически позволяли оптимизировать использование ограниченных ресурсов и чётко разграничивать память и устройства, но усложняют комплект команд. На уровне же физической шины, и там, и там – CPU выдаёт адрес + сигналы управления, а устройство откликается.

6. Структура машинной команды: опкод, операнды, адресация

Машинная команда (инструкция) – это бинарный код, который процессор может декодировать и выполнить. Команда обычно состоит из нескольких полей, главные из них: код операции (опкод) и операнды.

  • Опкод (opcode) – это часть двоичного кода команды, определяющая, какую операцию нужно выполнить (например, сложение, загрузка из памяти, переход по условию и т.д.). Опкод – своего рода числовой идентификатор команды. Например, в простом наборе команд опкод 0001₂ может означать «ADD (сложить)», 0010₂ – «SUB (вычесть)», и т.д. Разрядность опкода зависит от размера набора команд; в CISC-архитектурах опкод может быть переменной длины.

  • Операнд(ы) – данные, над которыми проводится операция, или указание на них. В машинном коде операнды могут задаваться различными способами: напрямую значением (имmediate), указанием регистра, указанием адреса в памяти, либо косвенно (через регистр, содержащий адрес) – это и есть адресная часть команды. Формат конкретной команды определяет, сколько операндов и как они закодированы. Например, команда может иметь один байт опкода и два поля по 4 бита, указывающих регистры-операнды. В другом случае опкод может подразумевать неявные операнды (например, команда «инкремент» может не иметь явного поля операнда, так как работает всегда с конкретным регистром, или стеком).

Формат команды – способ разложения этих полей в битовом коде. Наборы команд (ISA) бывают фиксированного формата (каждая инструкция одинаковой длины, поля на одних и тех же позициях – как в ARM, MIPS: обычно 32 бита на команду, внутри которых несколько полей) и переменного формата (в x86 длина команды от 1 до 15 байт, опкод может быть 1-3 байта, за ним следуют модификаторы, поля операндов и т.д.). В фиксированном формате легче декодировать конвейером, а переменный формат экономит память, допуская короткие коды для часто используемых операций.

Опкод и операнды на примере: Рассмотрим условно простую архитектуру с 8-битными командами. Пусть старшие 3 бита – опкод, а младшие 5 бит – операнд (например, номер регистра или непосредственное значение). Команда 001 01010₂ – тут опкод 001₂ означает «LOAD регистр», а операнд 01010₂ указывает конкретный регистр и/или адрес. Процессор, получив такой код, распознает по 001 команду загрузки и считает оставшиеся биты за спецификатор операнда (что загрузить и куда). В реальности архитектуры сложнее: могут быть несколько адресных полей, префиксы, поля модов адресации и т.д.

Адресация операндов. Способы адресации – это соглашения, как интерпретировать поля операндов команды для нахождения фактических данных. Основные режимы адресации:

  • Неявная – операнд не прописан в коде, а подразумевается (например, команда POP всегда берет операнд со стека – стековый указатель используется неявно).

  • Непосредственная (immediate) – операнд (значение) встроен прямо в код команды. Например, команда включает константу 5, которую нужно использовать в вычислении.

  • Регистровая – поле операнда указывает один из внутренних регистров CPU. Например, ADD R1, R2 – опкод + номера R1 и R2 как операндов.

  • Прямая адресация – поле содержит непосредственный адрес памяти, где лежит операнд. Команда хранит адрес полностью (в пределах доступной длины).

  • Косвенная через регистр – команда содержит номер регистра, в котором находится адрес операнда. Например, MOV A, [R3] – возьми адрес из регистра R3.

  • Индексная, базовая, с смещением – модификации косвенной: команда хранит базовый регистр + константный сдвиг (смещение). Процессор суммирует содержимое регистра и смещение, получая эффективный адрес. Это удобно для работы с массивами.

  • PC-релятивная – адрес вычисляется относительно текущей позиции (PC – счетчика команд). Используется в командах перехода: смещение добавляется к текущему адресу, давая целевой адрес (относительная адресация).

ISA определяет, какие режимы поддерживаются. Например, в MIPS все команды работают либо с регистрами, либо immediate; в x86 есть богатый выбор – комбинации базового+индексного регистра, смещение, и пр. Адресная часть команды может состоять из нескольких полей: например, в x86 после опкода идет байт ModR/M, где закодировано, являются ли операнды регистровыми или памятью, и если памятью – то какой режим (сдвиг, индекс и т.д.).

Пример конкретной команды (для понимания): команда x86 вида ADD [EBX+4], EAX. Здесь:

  • Опкод (несколько бит) обозначает операцию ADD с операндом в памяти.

  • Следующий байт ModR/M указывает: один операнд – регистр EAX, второй – память по адресу EBX+смещение.

  • Далее в коде команды будет 4-байтовое смещение (константа 4).
    Процессор декодирует: операция сложения, взять значение EAX, сложить с значением по адресу (EBX+4), результат сохранить по тому же адресу. Обратите внимание: в x86 и подобных CISC одна команда может делать несколько действий – и адрес вычислить, и обращение к памяти, и арифметику. Поэтому микропроцессору внутри нужно выполнить серию микрошагов (см. микропрограммирование ниже).

Итого: структура команды – это опкод + спецификаторы операндов. Опкод говорит «что делать», операндные поля – «с чем делать и где взять/поместить результат». Архитектурный уровень (ISA) определяется совокупностью таких форматов команд и их семантикой. Программист (или компилятор) оперирует понятиями инструкций (ADD R1,R2; MOVE; JUMP addr и т.п.), а процессор видит их как последовательности бит, которые надо распознать на эти поля.

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

7. Микропрограммное управление в CISC: исполнение на уровне микрошагов

В сложных процессорах, особенно архитектуры CISC, многие машинные команды выполняют несколько элементарных операций. Например, инструкция может требовать обратиться к памяти, выполнить расчет адреса, произвести арифметику, изменить флаги – и всё это по одному мнемоническому названию. Как управлять столь сложной последовательностью действий внутри CPU? Одно из классических решений – использовать микропрограмму.

Микропрограммное управление – принцип построения управляющего устройства процессора, при котором каждая машинная команда реализуется как последовательность более простых элементарных шагов – микрокоманд – хранящихся в специальной памяти управления. Процессор содержит встроенную ПЗУ (или ППЗУ) – микропрограммную память (Control Store), где записаны микропрограммы для всех инструкций набора команд.

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

Что делает микрокоманда? Микрокоманда – это набор сигналов управления, подаваемых на разные узлы процессора в течение одного такта. Можно сказать, микрокоманда определяет микрооперации, выполняемые параллельно в данном такте. Например, типичная микрокоманда: «считать слово из памяти: выдать содержимое регистра адреса в шину адреса, инициировать цикл чтения, принять данные в регистр-буфер». Другая микрокоманда: «передать содержимое регистра A в ALU, содержимое регистра B в ALU, выполнить сложение, сохранить результат в регистр A». Микрокоманды формируются на уровне внутренних сигналов: они открывают шлюзы, мультиплексоры, запускают сумматор, управляют записью в регистр и т.д. В течение одного тактового интервала может выполняться несколько простейших микроопераций одновременно, если они не конфликтуют. Совокупность сигналов, активных в данный момент – и есть микрокоманда.

Микропрограмма – упорядоченная последовательность микрокоманд, необходимая для выполнения одной машинной инструкции. Она обычно завершается микрокомандой, которая сообщает блоку управления о завершении и готовности перейти к следующей машинной команде. Переходы внутри микропрограммы тоже возможны – например, для реализации ветвления (команда условного перехода: микропрограмма может проверить флаг и перейти на другой адрес микрокоманд). В микрокомандах могут быть предусмотрены специальные поля для условных переходов (ветвление микропрограммы) и поле адреса следующей микрокоманды.

Пример исполнения инструкции на микрошаги: Возьмем команду CISC ADD MEM, REG (сложить значение из памяти по адресу, заданному например в регистре MAR, с регистром R, результат сохранить в R). Её микропрограмма может выглядеть так (упрощенно):

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

  2. Микрооперация ожидания и загрузки: дождаться сигнала готовности памяти (может потребовать цикл ожидания), принять прочитанное слово в MBR (буфер регистра данных памяти).

  3. Микрооперация выполнения ALU: подать содержимое MBR и регистра R в входы ALU, включить режим сложения, записать результат обратно в регистр R. Одновременно обновить флаги состояния (Carry, Zero и др.) на основе результата.

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

Каждый из этих шагов – микрокоманда, выполняемая за такт-два. В целом машина выполнила макрокоманду ADD за, скажем, 4 микротакта. При этом внутри она управлялась как мини-автоматом, программа которого хранится в ПЗУ управления.

В классических CISC (типа Intel 8086, 80286, VAX и др.) практически все сложные инструкции реализованы микропрограммно. Это давало гибкость: можно изменить алгоритм выполнения инструкции, внести поддержку новых команд, просто обновив микропрограмму (если ПЗУ позволяет, либо заложив новую маску ROM при производстве). Также это экономило транзисторы: вместо большого числа жестких соединений логики для каждой команды достаточно один универсальный микропрограммный управляющий автомат.

Структура микропрограммного управления: обычно включает:

  • ПЗУ микрокоманд (control store) – хранит микрокоды.

  • Счетчик/адресный регистр микрокоманд – указывает текущую микрокоманду.

  • Блок условных переходов – получает входы от флагов, кодов состояния, чтобы решить, какой адрес микрокоманды выбрать далее (может быть поле в микрокоманде для условного перехода).

  • Декодеры сигналов – преобразуют поля микрокоманды в конкретные сигналы для исполнительных узлов (открыть тот или иной регистр, разрешить ALU операцию и т.п.).

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

Достоинства микропрограммирования: облегчает реализацию сложных инструкций, позволяет относительно быстро внедрять новые команды (особенно в эпоху, когда ПЗУ микрокода могли быть перепрошиваемыми, производитель мог выпускать обновления). Микропрограмма действует как прослойка между ISA и аппаратными ресурсами, что дает гибкость в изменении внутренней реализации без изменения внешнего поведения. Например, можно оптимизировать какие-то микрокоманды, заменив последовательность на более короткую, и выпустить новый степпинг CPU – снаружи все те же команды, но быстрее.

Недостатки: микропрограммная память добавляет аппаратные затраты (нужна очень быстрая ПЗУ, чтобы выдавать микрокоманды почти на частоте процессора). Временем доступа к ней и длиной микропрограмм ограничено быстродействие – на каждую базовую команду уходит несколько тактов, пока отработает вся серия микрокоманд. В эпоху RISC многие команды сделали настолько простыми, что одной-двумя микрокомандами можно справиться – тогда микропрограмма становится излишней. В суперскалярных и сверхконвейерных архитектурах громоздкие микрокоды усложняют параллельное исполнение.

Тем не менее, даже современные x86 содержат микрокод: сложные инструкции распадаются внутри на микропротокол, а некоторые простые напрямую соответствуют контролям. Микрокод также используется для исправления ошибок (если в реализации команды обнаружен баг, производитель может обновить микрокод, изменив последовательность микрокоманд, загружаемых при запуске процессора – как это делается в современных Intel/AMD через обновление микрокода CPU). Таким образом, микропрограммирование остается важным элементом логической архитектуры CISC-процессоров.

Подведем итог: микропрограммное управление – это внутренний «исполнитель сценария», который разыгрывает сценарий выполнения каждой инструкции шаг за шагом. На каждом шаге (микротакте) подаются необходимые сигналы на блоки процессора для выполнения одной микрооперации. Набор таких шагов (микрокоманд) – микропрограмма – эквивалентен выполнению исходной машинной команды. Всё это прозрачно для программиста: он видит лишь, что команда ADD выполнилась и дала результат, а то, что внутри пробежали 4 микрокоманды – скрытая реализация. Это отсылает нас к концепции разделения уровней: ISA – уровень архитектуры, а микроархитектура – конкретная реализация. Как раз в следующем разделе рассмотрим отличия между логической архитектурой и физической реализацией.

8. Время и синхронизация: фронты тактов, фазы и временные диаграммы

Время – критически важный параметр в работе цифровых схем. Процессор – синхронное устройство, в котором последовательность операций координируется глобальным тактовым сигналом (clock). Рассмотрим понятия, связанные со временем и тактированием:

  • Тактовый сигнал (Clock) – периодическая последовательность импульсов (обычно прямоугольной формы), задающая ритм работы процессора. Частота тактового генератора (например, 3 ГГц) определяет длительность одного такта (в данном случае ~0,33 нс). Не все действия происходят мгновенно; такт задает интервал, после которого схема гарантированно переходит к следующему состоянию. Каждый тактовый цикл состоит из двух фаз: уровень высокого сигнала (лог.1) и уровень низкого (лог.0). Обычно компоненты могут быть спроектированы так, что они реагируют либо на фронты (перепады сигнала), либо на уровни. В процессорах обычно хранимые данные (триггеры, регистры) обновляются на определенном фронте – на фронте такта.

  • Фронт (нарастающий и спадающий) – моменты переключения тактового сигнала с 0 на 1 и с 1 на 0. В цифровой технике говорят о positive edge (leading edge) – передний фронт (нарастающий, переход из 0 в 1) и negative edge – спад (переход 1 в 0). Триггеры в процессоре часто настроены реагировать на один из них (например, многие регистры захватывают входные данные строго по переднему фронту). Это означает, что все параллельные регистрационные события синхронизированы одной «черточкой» времени – фронтом.

  • Фазы такта. В некоторых архитектурах один такт условно делят на суб-интервалы (фазы) для упорядочения нескольких событий. Например, можно использовать двухфазный такт: два тактовых сигнала, сдвинутых по фазе на половину периода. Первым (C1) могут срабатывать одни группы элементов, вторым (C2) – другие. Это применяли в старых процессорах, где, скажем, в фазу 1 открываются адрестные регистры, в фазу 2 – сэмплируются данные и т.д. Современные синхронные CPU обычно обходятся одним глобальным тактом, но внутри одного цикла действительно происходит несколько условных этапов (например, конвейерные стадии, см. pipeline). Однако они согласованы фронтами: на каждый фронт следующего цикла приходится переход следующей стадии.

  • Временная диаграмма – график, показывающий изменение сигналов (шины адреса, данных, управления, внутренних сигналов) во времени. На диаграмме по горизонтали откладывается время, по вертикали логические уровни (0 или 1) для каждого сигнала. Диаграмма позволяет визуализировать, когда должны становиться валидными те или иные сигналы относительно такта. Например, временная диаграмма обмена с памятью (как мы упоминали) покажет: в момент T1 (первый такт цикла) адрес стабилен и сигнал READ активен; к началу T2 данные от памяти появляются на шине; в конце T2 происходит выборка этих данных процессором. Диаграммы помогают понимать и проектировать синхронные схемы, чтобы выдерживать времена установления и задержки.

  • Временные соотношения (тайминги): Есть понятия времени установки (setup time) и удержания (hold time) для триггеров: это требования, чтобы входной сигнал был стабильным некоторый интервал ДО и ПОСЛЕ активного фронта, иначе триггер может неверно сработать. Эти параметры определяют максимальную частоту работы: суммарная задержка комбинационной логики между регистрами плюс время установки должна укладываться в период такта. Если сигнал не успел установиться к нужному фронту, возникает сбой синхронизации.

  • Скважность и фаза: Скважность – соотношение длительности высокого и низкого уровней такта. Обычно 50/50, но бывают случаи, когда активная фаза короче/длиннее. Фазовый сдвиг между несколькими тактовыми сигналами позволяет, как сказано, получить дополнительные опорные точки времени внутри цикла. В двухфазном генераторе, например, фронт C1, потом фронт C2 делят цикл на два этапа. На рис. 3.20(b) из цитируемого текста показано, что вторичный такт C2 сдвинут так, что его фронты приходятся на середины интервала основного такта C1. Это дает четыре четверть-цикла (C1↑, C1↓, C2↑, C2↓) – точки, к которым можно привязать различные события, добиваясь нужной последовательности действий. Связывая различные микропроцессы с разными фронтами, инженеры гарантируют требуемый порядок их выполнения. В современных высокочастотных CPU чаще применяют разные внутренние такты для разных доменов (например, ядро, шина, памяти могут быть на разных частотах), а также технологии phase-locked loop (PLL), чтобы управлять фазами такта (сдвигами для выравнивания сигналов, особенно в DDR-памяти, где данные читаются по обоим фронтам такта).

  • Асинхронные сигналы и синхронизация: В процессоре, помимо основного такта, есть и асинхронные события (например, сигналы прерывания, готовность от периферии). Их нужно привести к тактовой сетке – обычно через элементы синхронизации, чтобы они “подстроились” к ближайшему фронту без метастабилитета. Это тоже часть временной логики: ассинхронные входы нельзя напрямую пускать на логику, их ловят триггером на такте.

Пример визуализации: Представьте, у нас есть сигнал CLK (тактовый) и сигнал DATA_VALID, говорящий, что на шине данных актуальная информация. На временной диаграмме мы можем отметить, что DATA_VALID становится «1» спустя, скажем, 10 нс после фронта CLK, и остается активным до конца полуцикла. Значит, схема должна так спроектирована, что приемник данных смотрит на них на спадающем фронте, когда DATA_VALID уже поднят. Так выстраиваются «оконца» времени, когда безопасно читать данные. В технических описаниях к микросхемам часто приведены такие диаграммы с пометками t_setup, t_hold и прочими.

Вывод: фронты тактов – ключевые ориентиры времени, на них «бьется» ритм процессов. Фазы такта могут разбивать цикл на подэтапы, если нужно последовательное выполнение нескольких шагов внутри одного цикла. Временные диаграммы – незаменимый инструмент для понимания, как сигналы взаимодействуют во времени, и для выявления конфликтов или гонок. Без удовлетворения временных требований цифровая схема может работать неверно, даже если логически соединена правильно.

Для нас, в рамках понимания работы CPU, важно знать: все действия синхронизированы тактом, на каждом такте конвейер (если есть) переходит к новой фазе выполнения инструкций, регистры хранят данные от фронта до фронта, а компоненты должны уложиться со своими операциями в отпущенное им «окно» времени. Нарушение таймингов приводит к сбоям – поэтому проектирование процессора включает статический временной анализ, проверку задержек.

В контексте учебной модели ЭВМ можно упрощенно считать, что каждая машинная команда выполняется за определенное число тактов (например, 4 такта: выборка, декодирование, исполнение, запись результата). На каждом такте происходит одна стадия. Как это ускорить – вопрос конвейера, к которому мы сейчас и перейдем.

9. Конвейер команд: стадии, конфликты и их разрешение

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

Типичные стадии конвейера: классический RISC-конвейер имеет 5 ступеней:

  1. IF (Instruction Fetch) – выборка команды из памяти (по адресу PC).

  2. ID (Instruction Decode) – декодирование опкода, чтение регистров-операндов из регистрового файла.

  3. EX (Execute) – выполнение операции в АЛУ (арифметика/логика, расчет адреса).

  4. MEM (Memory access) – обращение к памяти, если требуется (например, для команды загрузки/выгрузки данных).

  5. WB (Write Back) – запись результата обратно в регистр.

Каждая стадия выполняет свою часть, и на каждом такте может работать над разными инструкциями. Например, пока инструкция 1 находится на стадии EX, следующая инструкция 2 может параллельно быть на стадии ID, а еще следующая 3 – на IF. На временной диаграмме это выглядит как диагональ: инструкция 1: такт1-IF, такт2-ID, такт3-EX, такт4-MEM, такт5-WB; инструкция 2: такт2-IF, такт3-ID, такт4-EX, такт5-MEM, такт6-WB; и т.д.. После заполнения (pipeline fill) в каждом такте завершается одна инструкция (кроме пауз). Это дает значительный рост производительности: при 5 стадиях теоретически процессор в 5 раз быстрее, чем без конвейера, если не считать накладных потерь.

Однако конвейеризация вводит ряд проблем, называемых конфликтами (hazards). Конфликты возникают, когда параллельное выполнение нескольких команд сталкивается с ограничениями:

  • Структурные конфликты (structural hazards): если две инструкции требуют в один и тот же момент один и тот же аппаратный ресурс. Например, у нас единый блок памяти для инструкций и данных – тогда ситуация: инструкция A на стадии IF пытается читать следующую команду из памяти, а инструкция B на стадии MEM пытается читать операнд из памяти – конфликт за шину памяти. Решение: либо избегать такого проектом (раздвоить кэш на командный и данных – как и делают), либо ставить «задержку» конвейера в такой ситуации. Структурный конфликт – это аппаратное столкновение.

  • Конфликты по данным (data hazards): самая распространенная проблема. Возникает, когда одна инструкция зависит от результата другой, а они находятся одновременно в конвейере. Например:

    • Инструкция 1: ADD R1, R2 -> R3 (R3 = R1+R2).

    • Инструкция 2: SUB R4, R3 -> R5 (использует результат R3).
      В конвейере SUB (инстр.2) может начать выполняться (стадия EX) до того, как ADD (инстр.1) запишет результат в R3 (стадия WB). То есть SUB возьмет старое значение R3, еще не обновленное. Это конфликт по данным типа read-after-write (RAW) – команда читает операнд, который еще не записан предыдущей командой.
      Другие типы: WAR (write-after-read) и WAW (write-after-write) – реже в конвейере встречаются, т.к. чаще всего возникает именно потребность чтения нового результата.

Для разрешения RAW-конфликта применяют техники:

  • Байпас (forwarding / operand forwarding): если результат уже вычислен на стадии EX предыдущей команды, но еще не записан в регистр, его можно напрямую подать в следующую команду без ожидания записи. В примерe: как только ADD вычислил сумму (на стадии EX), вместо того чтобы ждать до WB, мы прокидываем эту сумму сразу во вход ALU для SUB, если распознали зависимость. Требуется дополнительное соединение – форвардинг-шина – из выхода ALU в вход ALU соседнего конвейерного слота. Большинство RISC-конвейеров так и делают, позволяя smягчить большинство конфликтов по данным.

  • Остановка конвейера (stall / bubble): если зависимость не разрешима форвардингом (например, если результат будет известен только после обращения к памяти), конвейер приостанавливается – вставляется пауза. Реализуется аппаратно: конвейерным регистрам запрещают сдвигаться, по сути не запускают новую инструкцию в следующий этап, а вместо конфликтующей операции вводят пустую «пузырь» (bubble). Пузырёк – это фиктивная неоперация (NOP), заполняющая конвейер до тех пор, пока нужные данные не будут готовы. Инструкции позади пузырька задерживаются, а впереди – выполняются, как были.

  • Компиляторные методы: на уровне программного кода оптимизирующий компилятор может пытаться развести зависимые команды по времени, вставляя несвязанные инструкции между ними или явные NOP, чтобы у процессора не было конфликта (этим активно занимались для ранних RISC без аппаратного форвардинга). Но в современных CPU аппаратные решения предпочтительнее.

Стоит отметить: не все зависимости видны просто как регистровые. Бывает зависимость через память: инструкция 1 записывает в память по адресу X, а инструкция 2 следующая читает по тому же адресу X – это тоже конфликт RAW, но его сложнее обнаружить (надо сравнить адреса). Процессоры могут иметь логику для отслеживания таких ситуаций и тоже вставлять остановы.

  • Конфликты по управлению (branch hazards): связаны с командами ветвления (переходов). В конвейере мы заранее начинаем подготавливать инструкции, которые могут оказаться «не теми», если предыдущая команда сделает прыжок. Пример: инструкция 5 – условный переход. Пока она на стадии ID/EX, процессор уже выдал на IF очередные команды, предположив, что переход не случится (или случится). Если предположение неверно, конвейер содержит неверные инструкции, которые начали выполняться по ошибочному пути. Необходимо их аннулировать (сброс конвейера, flush): как только выясняется исход перехода, все инструкции в конвейере, следующих после ветвления, помеченные как неправильные, должны быть отброшены, их результаты не учитываются, и загрузка продолжается с правильного адреса. Этот бросает конвейер назад, создавая паузу на несколько тактов (глубина конвейера, пока не заполнится заново). Чтобы уменьшить потери на ветвлениях, применяют:

    • Прогнозирование переходов (branch prediction): процессор пытается угадать исход перехода (например, по истории выполнения) и продолжает выполнять инструкции по наиболее вероятному пути. Если угадывает – паузы нет; если нет – конвейер проматывается назад (вводится пузырьки, очищаются неверные). Хороший предсказатель снижает частоту неправильных предположений.

    • Аппаратные задержки (branch delay slot): некоторые архитектуры (MIPS) определяют, что команда сразу после перехода всегда выполняется, независимо от того, совершился ли переход. Это фактически рассматривается как слот задержки – программист/компилятор помещает туда инструкцию, полезную или нейтральную, чтобы занять конвейер. Тем самым конвейер никогда не останавливается на этом такте: если переход произошел, следующая инструкция все равно выполнена (пусть бесполезная или подготовительная).

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

  • Конфликты по данным, тип антипорядок (WAR) и по записам (WAW): например, WAR – когда следующая инструкция пишет в регистр, который предыдущая еще должна прочитать, а в конвейере запись может случиться раньше чтения из-за перестановки стадий. Такие редкие случаи решаются либо консервативным упорядочением стадий (обычно чтение операндов происходит в ID, запись результатов – в WB последней, так что читать всегда раньше, чем предыдущая пишет – WAR исключен), либо системой именования (в суперскалярных вводят ренейминг регистров, чтобы убрать ложные зависимости по именам). Это выходит за рамки простого конвейера, но упоминаем для полноты.

Bubble (пузырёк) подробно: Пузырёк – это фиктивная операция, которая ничего не делает, но занимает одну или несколько стадий конвейера, позволяя разорвать конфликт. Вводится аппаратно либо программно как NOP. Например, при конфликте по данным, если нет форвардинга, процессор может автоматически вставить NOP после первой инструкции, чтобы в это время первая завершила запись и только затем вторая считала. На временной диаграмме это заметно: один такт конвейера ни одна полезная команда не завершается, зато рассинхронизация устранена. Пузырьки снижают эффективность (конвейер недогружен), но иногда неизбежны. Современные архитектуры стараются их избегать за счет форвардинга, динамического планирования, но полностью уйти нельзя – при неверном прогнозе ветвления три-четыре (а в глубоких конвейерах и десятки) пузырей заполняют конвейер, пока не начнут приходить новые корректные инструкции.

Итого: плюсы конвейера: существенно повышает производительность (больше инструкций в единицу времени). Минусы: усложняет организацию процессора – нужно решать конфликты, добавлять схемы предсказания, форвардинга, останова. Для программиста конвейер прозрачен – он лишь видит неравномерное время выполнения разных инструкций (например, ветвления «кажутся» медленнее из-за частых пустых циклов).

Второй эффект – сложности с точностью исключений и прерываний: если произошла ошибка (деление на 0, страничный промах) на поздней стадии одной инструкции, а за ней уже выполнялись следующие, процессору нужно аккуратно откатить то, что успели сделать последующие команды. Это требует буферизации результатов, чтобы не нарушить принцип последовательно-точного исполнения (архитектурно программа должна выполняться как будто командой за командой, даже если внутри конвейер). Этот аспект привел к появлению методов зарежимного выполнения (out-of-order) с буферизацией результатов до подтверждения – но это уже более сложная микроархитектура, выходящая за рамки нашего обсуждения.

Для нас важно понимать: построение конвейера – пример того, как физическая реализация (микроархитектура) отличается от простой логической модели. Логически инструкция занимает несколько этапов, а физически они накладываются с другими. CPU с конвейером при тех же частотах и тех же алгоритмах работает быстрее своего неконвейерного аналога, но при этом требует решений по устранению конфликтов (data hazards).

Одна из мер, понятная на уровне программирования, – вставка явных NOP (no operation) команд там, где это нужно. Ранние конвейерные RISC (например, MIPS I) требовали от компилятора вставлять NOP после команд загрузки из памяти, потому что данные из памяти становились доступны только через 1 цикл – в следующий цикл вставлялся NOP (load delay slot). Позже научились аппаратно заполнять эти слоты и устранять потребность в явных NOP, сделав архитектуру более прозрачной для программиста.

Пример рассуждения: если конвейер у нас 5-ступенчатый, а программа полностью линейна без зависимостей и ветвлений, мы теоретически получим выигрыш ~5 раз. Но если программа с кучей условий, цикл разгоняется не полностью – каждая ветка может вызывать flush конвейера. Производительность реальных программ зависит от их характеристик: уровень ILP (параллелизма команд), плотность ветвлений, удачливость предсказания. Современные x86 имеют конвейеры длиной ~14-19 стадий и сложнейшие предсказатели, чтобы загружать их по максимуму; при промахе по ветвлению они теряют ~15 тактов.

В контексте нашего курса: достаточно помнить, что конвейер – способ параллельного выполнения частей команд, и что он требует решать три вида конфликтов (структурные, по данным, по управлению). Решения: дублирование ресурсов (устранение структурных), форвардинг/ренейминг/сталл (для data hazards), прогноз и очистка (для branch hazards). И зачастую – вставка пузырьков (NOP) как простейший метод ждать, пока опасность минует.

10. Логическая архитектура vs физическая реализация: микроархитектура и ISA

В заключение обсудим важное различие между архитектурой набора команд (ISA) и микроархитектурой процессора. Это как раз та грань, которая отделяет логическую модель вычислительной машины от конкретного воплощения «в кремнии».

Instruction Set Architecture (ISA) – это описание того, что процессор умеет делать, вид со стороны программиста. В ISA входят: набор команд (список операций и их машинные коды), доступные архитектурные регистры (те, которые видны программе), формат данных (сколько бит в основных типах), правила адресации, модель памяти (например, поддерживает ли виртуальную память, каким образом) и т.д.. Архитектура определяет поведение программ: какие программы может выполнять CPU и какова их семантика. Например, архитектура ARMv7 имеет 16 регистров общего назначения, 32-битные инструкции различных форматов, инструкции условного выполнения, и определяет, как они должны работать. Любой процессор, реализующий ISA ARMv7, обязан корректно исполнять все эти инструкции так, как предписано (семантически).

Микроархитектура – это конкретный дизайн реализации этой архитектуры внутри процессора. Она описывает внутреннее устройство: сколько и какие блоки в CPU, как они соединены, есть ли конвейер и сколько стадий, размер и организация кеш-памяти, реализовано ли микропрограммирование или «жесткая» логика, сколько ALU и каких, выполняет ли процессор инструкции последовательно или вне порядка – все эти детали относятся к микроархитектуре. Принципиально, микроархитектура не влияет на результат и функциональность с точки зрения программ – она не меняет ISA, а лишь реализует ее тем или иным способом. Но микроархитектура влияет на производительность, энергопотребление, стоимость реализации и другие «непрограммные» характеристики.

Пример: компания Intel имеет архитектуру x86-64 (ISA называется Intel 64 или AMD64). Эту архитектуру реализуют совершенно разные микроархитектуры: «Nehalem», «Haswell», «SkyLake», «Zen» (от AMD) и др. Все они выполняют одни и те же x86-64 программы, но внутри отличаются. Например, старый Pentium 4 имел очень длинный 20-стадийный конвейер, а современный AMD Zen3 – короче. У Intel Core микроархитектура – три адресных конвейера, множество декодеров и буферов переименования; у Atom – упрощенная, менее параллельная, но энергосберегающая. Программа Doom 3 запустится и на Pentium IV, и на новейшем Threadripper – архитектура (ISA) одинакова, x86 – но скорость будет разной за счет микроархитектуры. Threadripper выполнит те же инструкции быстрее, потому что внутри больше исполнителей, лучше предсказание, больше кеш – хотя с точки зрения программы все то же самое.

Аналогия: ISA – это как спецификация автомобиля (например, «максимальная скорость 200 км/ч, бензиновый двигатель 2.0, 5 мест»), а микроархитектуры – разные модели машин, удовлетворяющие этой спецификации (может быть седан с турбонаддувом или внедорожник – оба по паспорту едут 200 и имеют 5 мест, но конструктивно очень разные). Для водителя (программиста) интерфейс один – руль, педали (команды, регистры) – а под капотом реализации отличаются.

Почему важно разделение:

  1. Совместимость: Благодаря четкому определению ISA, можно создавать семейства процессоров с разной микроархитектурой, которые бинарно совместимы. Пример – все процессоры x86-64 от 2003 года до сегодня запускают одни и те же программы без перекомпиляции. Архитектура стабильна, микроархитектура улучшается. Это позволяет не переписывать софт под каждый новый CPU – огромная практическая выгода.

  2. Модульность развития: Архитекторы ISA могут добавлять новые инструкции (расширения), но в целом, пока основа сохраняется, все старое ПО работает. А разработчики микроархитектуры свободны внедрять оптимизации: увеличивать глубину конвейера, добавлять параллельные блоки, изменять способы декодирования – все, что не меняет внешнего функционального поведения.

  3. Пример с микроархитектурой vs ISA: В 1990-х Intel столкнулась, что CISC x86 становится трудно разгонять. Решение – внутри их Pentium Pro реализовали микроархитектуру, транслирующую x86-команды в RISC-оподобные микровыполняемые операции. То есть ISA осталась x86, но физически процессор декодировал каждый сложный x86 опкод в несколько внутренних опций и выполнял их на RISC-ядре. Это позволило сочетать удобство CISC для программиста с скорость RISC внутри. Опять же, ISA не изменилась – софт ничего не заметил.

Отделенность микроархитектуры: Для программиста/компилятора микроархитектура почти невидима. Однако проявляется в том, сколько тактов занимают те или иные инструкции, есть ли задержки после определенных комбинаций – это учитывают при оптимизации кода. Например, зная размер кеша (микроархитектурный параметр), программист может пытаться оптимизировать размещение данных. Но функционально программа не должна зависеть: правильно написанный код выдаст верный результат на любом CPU данного ISA, разница лишь во времени и потреблении.

Физическая реализация: Это уже уровень, как микроархитектура воплощается в транзисторах – схемотехника, электрические параметры, тактирование. Например, взять микроархитектуру (блок-схему) и реализовать её с конкретными размерами транзисторов, топологией на кристалле. Инженер на этом уровне думает о таймингах сигналов, распределении тактовой сети по кристаллу, тепловыделении. Но это еще более низкий уровень детализации.

Таким образом, логическая архитектура (ISA) – это абстракция, на которой оперируют компиляторы, ассемблеры, программисты низкого уровня. Физическая архитектура (микро) – конкретное воплощение: например, микроархитектура Skylake реализует архитектуру x86-64. В рамках курса важно понимать разницу: когда мы говорим «у процессора есть 16 регистров» – это архитектурные регистры (видимые по ISA). Физически внутри может быть 160 регистров (система переименования), но они невидимы программе. Или, ISA может не иметь конвейера, а микроархитектура – конвейерная: для внешнего мира инструкция кажется выполняется последовательно, а внутри параллельно.

Микроархитектура определяет производительность, а ISA – совместимость и функционал. Вот почему, например, процессоры Atom и Core от Intel (обе x86 ISA) по-разному быстры: микроархитектуры разные – Core агрессивно внеочередной, широкий, Atom упрощенный для экономии энергии. Но любой x86 код запустится на обоих и даст одинаковый результат.

Подчеркнем: разделение архитектуры и реализации – фундаментальное в вычислительной технике. Архитектура (в узком смысле ISA) – это то, что описано в документации «для программиста», а реализация – это то, что на кристалле. Классический пример: Эмуляторы могут реализовать архитектуру одного процессора средствами другого – это полностью различающиеся микроархитектуры, но пока они верно исполняют ISA исходного CPU, программы «не узнают» разницы.

В нашем курсе под «архитектурой ЭВМ» часто понимается совокупность принципов организации (фон-Неймановская архитектура, магистраль, ISA). А под «организацией/реализацией» – конкретные схемные решения (например, конвейерный RISC с 5 стадиями, 2-уровневый кеш, микропрограммный CISC и т.д.). Зная теперь все уровни – от транзисторов до микрокода и конвейера – мы можем ценить изящество этой многоуровневой системы. Программы пишутся на уровне ISA (или выше), а выполняются на микростепах, при этом транзисторы молниеносно открываются и закрываются, представляя биты 0 и 1, тысячи раз за период тактов… В этом и состоит магия компьютерной инженерии: логика высокого уровня воплощена посредством огромного количества элементарных переключателей, строго организованных во времени и пространстве.

Резюме:

  • ISA – интерфейс между программой и процессором, определяет что делает процессор (команды, регистры, эффект команд).

  • Микроархитектура – определяет как это сделано внутри (конвейеры, ALU, контроллеры, кеши). Хорошие примеры: разные поколения CPU, разные семейства – ARM Cortex-A72 vs Cortex-A53 (обе ARMv8 ISA, но одна – высокопроизводительная сверхскалярная, другая – упрощенная энергосберегающая).

Отделение уровней позволяет эволюцию: улучшаем микроархитектуру – сохраняем совместимость (быстрее, но то же ISA). Или, если требуется более эффективный набор команд, проектируем новый ISA – но тогда нужен новый компилятор, новое ПО. Поэтому создание нового ISA – редкое событие; чаще улучшают то, что есть (например, x86 дополняют SIMD-инструкциями, ARM – расширениями NEON и т.п., сохраняя базу).

На этом мы завершаем лекцию. Мы прошли путь от MOSFET-транзистора – физического переключателя, представляющего биты, через логические элементы (реализующие булевы функции), построили из них сумматоры, триггеры, регистры, счетчики, дешифраторы – кирпичики процессора. Затем рассмотрели системные связи через шины – как процессор адресует память и устройства, как организован ввод-вывод (MMIO, порты). Изучили формат и исполнение команд: что внутри команды (опкод и операнды) и как сложные команды выполняются пошагово через микропрограмму в CISC-процессоре. Разобрались со временем: роль тактового сигнала, фронтов, фаз, и увидели, как важно соблюдение временных интервалов (таймингов). Далее – конвейер, позволивший выполнять несколько инструкций параллельно, но потребовавший решений для конфликтов (hazards) – от форвардинга до предсказания переходов и вставки NOP. И наконец, увязали это в понятия логической архитектуры vs. реализации – ISA против микроархитектуры, что позволяет одни и те же программы запускать на разных физических CPU.

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