Архитектура фон Неймана

Архитектура фон Неймана достаточно проста и универсальна. В чистом виде она применялась сначала на больших компьютерах 50-х, потом на мини-компьютерах 60-70-х, и наконец — в микрокомпьютерах 70-80-х. Хоть современные компьютеры заметно ушли от оригинальной фон-неймановской архитектуры, но как правило эти архитектуры — её развитие и дополнение, к тому же часть принципов всё ещё остаётся актуальными. Поэтому начинать изучать принципы построения различных компьютерных архитектур лучше всего с неё.

Составные части

править

В состав машины фон-неймановской архитектуры входят:

  • Оперативная память, состоящая из набора одинаковых ячеек, способных хранить некоторое число в определённых пределах. Оперативная память фон-неймановской машины имеет следующие особенности:
    • Она должна обеспечивать произвольный доступ. То есть в любой момент можно запросить доступ к любой существующей ячейке, передав её адрес.
    • Любая ячейка памяти может содержать как команду, так и какие-либо данные. Сами по себе ячейки памяти не имеют ни маркировки, ни какого-то специального разделения на команды и данные. То есть если из-за ошибки в программе управление будет передано в область, где должны храниться данные, или наоборот будет совершено какое-то действие над ячейкой, в которой хранится команда, это никак невозможно определить и заранее пресечь. С другой стороны, фон-неймановский компьютер может модифицировать программу в процессе её выполнения, но у такого приёма очень ограниченная сфера применения, и в целом пользоваться такой возможностью не рекомендуется.
  • Устройство управления, которое получает на вход команду и формирует на её основе последовательность управляющих сигналов, задействующих все прочие устройства компьютера.
  • Арифметическо-логическое устройство, которое по команде от УУ производит различные действия над числами — арифметические операции, сравнение целиком или по частям, и т.д.
  • Регистры процессора — это тоже некоторое количество ячеек памяти, к которым АЛУ и УУ могут обращаться напрямую, без запроса адреса, то есть они всегда «под рукой». Часть регистров являются регистрами общего назначения, а часть — специальные регистры. К специальным регистрам обычно относят: аккумулятор, программный счётчик, указатель стека, регистр флагов. Назначение и принцип работы каждого будет рассмотрен в следующей главе в процессе объяснения общего принципа выполнения программ.
  • Внешние устройства, или устройства ввода-вывода — устройства, которые получают данные из внешней среды, например, введённые пользователем, и возвращают результаты их обработки.
  • Шины — набор электрических соединений, к которым параллельно подключено большинство из перечисленных устройств. Как правило шин три:
    • Шина адреса — как правило по ней передаётся адрес при обращении к памяти, но также может и передаваться адрес внешнего устройства;
    • Шина данных — по ней передаются данные между частями машины: из ОЗУ в УУ или АЛУ, и обратно;
    • Шина управления — в отличие от шины адреса и данных, где каждый сигнал является разрядом какого-то числа, на шине управления каждый сигнал имеет своё значение.

АЛУ, регистры и устройство управления принято объединять в единое устройство, называемое центральным процессором.

Порядок работы

править

Пустая операция

править

Как правило, большинство фон-неймановских процессоров имеют одну (реже несколько) пустую команду, называемую NOP. Часто ей соответствует нулевое значение ячейки памяти. Эта команда может применяться для того, чтобы отмерять временны́е интервалы, заменять их отладочными командами и т.д. По сути эта команда ничего не делает. Последовательность её обработки — следующая:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Узнав, что делать ничего не требуется, УУ сразу переходит к завершающему этапу. Первый шаг этого этапа — увеличить на 1 значение регистра PC — программного счётчика;
  3. Подаёт команду на PC отправить своё значение в шину адреса;
  4. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

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

Операция сложения

править

Предположим, в следующей ячейке находится число, которое соответствует операции сложения. Цикл выполнения команды будет следующим:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Подаёт команду на АЛУ, чтобы оно сложило значение одного из регистров общего назначения с аккумулятором (а также прибавляем 1 если включен флаг переноса).
  3. Подаёт команду записать результат вычисления в аккумулятор;
  4. Подаёт команду записать некоторые свойства полученного результата в регистр флагов;
  5. Увеличивает на 1 значение программного счётчика;
  6. Подаёт команду на PC отправить своё значение в шину адреса;
  7. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

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

Операции чтения из памяти/записи в память

править

Частый случай — нужно получить для чего-то жёстко заданное константное значение. Так как архитектура фон Неймана не делает различий между командами и данными, такие значения как правило размещают сразу после самой команды:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Увеличивает на 1 значение программного счётчика;
  3. Подаёт в ОЗУ команду на чтение ячейки по этому адресу;
  4. Подаёт команду на один из регистров (в зависимости от кода команды) прочитать значение из шины данных;
  5. Увеличивает на 1 значение программного счётчика;
  6. Подаёт команду на PC отправить своё значение в шину адреса;
  7. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

Обратный случай — когда данные хранятся в отдельной от программы области памяти. Предположим, что адрес, откуда нужно прочитать данные, хранится в регистре общего назначения B, а значение нужно записать в регистр C. Тогда последовательность работы будет следующая:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Подаёт команду на регистр B отправить своё значение в шину адреса;
  3. Подаёт в ОЗУ команду на чтение ячейки по этому адресу;
  4. Подаёт команду на регистр C прочитать значение из шины данных;
  5. Увеличивает на 1 значение программного счётчика;
  6. Подаёт команду на PC отправить своё значение в шину адреса;
  7. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

Операция записи проходит аналогично, просто третьим шагом становится отправка значения регистра C на шину данных, а четвёртым — команда в ОЗУ на запись значения из шины данных.

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

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

Операции ввода-вывода

править

Операции ввода-вывода в целом аналогичны операциям чтения/записи в память. Разница лишь в том, что вместо подачи команды на обращение к памяти, в управляющую шину выдаётся поступает команда на обращение на ввод-вывод. Некоторые реализации архитектуры и вовсе не имеют специальных команд для ввода-вывода, вместо этого обращение к некоторому диапазону адресов определяется не как обращение к памяти, а как операции ввода-вывода.

С точки зрения прикладного программирования, стек — достаточно специфическая структура данных очень ограниченного применения. Не так уж и много в реальной жизни бывает ситуаций, когда данные нужно хранить в режиме «последний вошёл — первый вышел». Однако когда речь заходит о программировании непосредственно в командах процессора, стек становится очень удобной структурой данных. Удобной не для восприятия человеком, а для аппаратной реализации.

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

Простейшая команда — записать значение регистра общего назначения в стек, выглядит как-то так:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Увеличивает значение регистра указателя стека;
  3. Отправляет значение регистра указателя стека на шину адреса;
  4. Отправляет значение выбранного регистра общего назначения на шину данных;
  5. Подаёт в ОЗУ команду на запись значения из шины данных в ячейку по этому адресу;
  6. Увеличивает на 1 значение программного счётчика;
  7. Подаёт команду на PC отправить своё значение в шину адреса;
  8. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

В случае чтения пункты 2, 3, 4 и 5 выглядят так:

  1. УУ Отправляет значение регистра указателя стека на шину адреса;
  2. Подаёт в ОЗУ команду на чтение ячейки по этому адресу;
  3. Записывает в выбранный регистр значение из шины данных;
  4. Уменьшает значение регистра указателя стека;

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

Операция перехода

править

С точки зрения процессора, операция перехода — это банальная запись значения в программный счётчик. Например, безусловный переход по адресу, заданному в программе выглядит так:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Увеличивает на 1 значение программного счётчика;
  3. Подаёт команду на PC отправить своё значение в шину адреса;
  4. Подаёт в ОЗУ команду на чтение ячейки по этому адресу;
  5. Записывает прочитанное значение в PC;
  6. Подаёт команду на PC отправить своё значение в шину адреса;
  7. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

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

Операции вызова подпрограммы

править

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

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Увеличивает на 1 значение программного счётчика;
  3. Подаёт команду указателю стека увеличить своё значение;
  4. Подаёт команду указателю стека отправить своё значение в шину адреса;
  5. Подаёт в ОЗУ команду на запись ячейки по этому адресу;
  6. Подаёт команду на PC отправить своё значение в шину адреса;
  7. Подаёт в ОЗУ команду на чтение ячейки по этому адресу;
  8. Записывает прочитанное значение в PC;
  9. Подаёт команду на PC отправить своё значение в шину адреса;
  10. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

Подпрограмма должна завершаться командой возврата:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Подаёт команду указателю стека отправить своё значение в шину адреса;
  3. Подаёт в ОЗУ команду на чтение ячейки по этому адресу;
  4. Записывает прочитанное значение в PC;
  5. Уменьшает значение указателя стека;
  6. Увеличивает на 1 значение программного счётчика;
  7. Подаёт команду на PC отправить своё значение в шину адреса;
  8. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

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

Условные операции

править

Допустим, нам необходимо сравнить два числа, лежащие в двух регистрах общего назначения, а потом, если второе число больше первого — перейти к альтернативной ветке алгоритма, находящейся по заданному адресу, в противном случае продолжить исполнение. Такая операция будет состоять из двух команд. Сначала нужно сделать сравнение:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Подаёт команду на АЛУ, чтобы оно вычло значение одного из регистров общего назначения из аккумулятора.
  3. Подаёт команду записать некоторые свойства полученного результата в регистр флагов;
  4. Увеличивает на 1 значение программного счётчика;
  5. Подаёт команду на PC отправить своё значение в шину адреса;
  6. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

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

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

  • Флаг переноса — применяется для работы длинной арифметики. Установка этого флага при сложении, вычитании и некоторых других операциях поступает в следующий байт, либо в следующее машинное слово;
  • Флаг нуля — означает, что предыдущая операция завершилась с нулевым результатом. Так как операция сравнения по сути является операцией вычитания, то этот же флаг устанавливается, если два сравниваемых значения равны;
  • Флаг знака — означает, что результат предыдущей операции является отрицательным числом.

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

После сравнения необходимо осуществить условный переход.

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Увеличивает на 1 значение программного счётчика;
  3. Читает состояние флага знака в регистре флагов. Далее, в зависимости от его состояния:
  4. Если флаг знака установлен:
    1. Подаёт команду на PC отправить своё значение в шину адреса;
    2. Подаёт в ОЗУ команду на чтение ячейки по этому адресу;
    3. Записывает полученное значение в программный счётчик;
  5. Если же флаг не установлен:
    1. Увеличивает на 1 значение программного счётчика;
  6. Подаёт команду на PC отправить своё значение в шину адреса;
  7. Подаёт в ОЗУ команду на чтение ячейки по этому адресу.

Кроме команд условного перехода (а также условного вызова процедуры и условного возврата из процедур) могут существовать также команды условного выполнения отдельных команд.

Останов и прерывание

править

Во времена Фон Неймана компьютеры не были многозадачными, а использовались в основном для расчётов. После окончания расчёта и вывода результата машине следовало остановиться:

  1. Получив с шины данных ответ, УУ производит декодирование команды;
  2. Следующего шага просто нет, процессор переходит в режим ожидания.

Выход из этого состояния возможен либо посредством прерывания, либо перезагрузкой машины. Существовала легенда, что на каких-то машинах эта команда приводила к выходу машины из строя (за что её назвали Halt and Catch Fire)

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

  1. После окончания обработки предыдущей команды или в процессе ожидания УУ обнаруживает, что поступил сигнал о прерывании;
  2. УУ подаёт сигнал устройству обработки прерываний о том, что обработка прерывания началась[1]. При этом обработка прерывания может производиться разными способами, но чаще всего происходит вызов процедуры обработки прерывания по хранящемуся в специальном регистре адресу;
  3. Увеличивает на 1 значение программного счётчика;
  4. Подаёт команду указателю стека увеличить своё значение;
  5. Подаёт команду указателю стека отправить своё значение в шину адреса;
  6. Подаёт в ОЗУ команду на запись ячейки по этому адресу;
  7. Подаёт команду регистру, хранящему адрес процедуры обработки прерывания записать этот адрес в программный счётчик.

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

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

Проблемы архитектуры и пути их решения

править

Длительный цикл исполнения команды

править

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

Бутылочное горлышко доступа к памяти

править

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

Отсутствие указания на тип хранимого значения

править

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

Проблемы многозадачности

править

Архитектура фон Неймана в своём изначальном виде не предназначена для параллельного выполнения нескольких задач. С этим связаны основные проблемы:

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

Примечания

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