Unix2019b/Организация памяти на x86-64
Процессоры архитектуры x86-64 поддерживают два основных режима работы: Long mode («длинный» режим) и Legacy mode («унаследованный», режим совместимости с 32-битным x86).
Long mode
«Длинный» режим — «родной» для процессоров x86-64. Этот режим даёт возможность воспользоваться всеми дополнительными преимуществами, предоставляемыми архитектурой. Для использования этого режима необходима 64-битная операционная система.
Этот режим позволяет выполнять 64-битные программы; также (для обратной совместимости) предоставляется поддержка выполнения 32-битного кода, например, 32-битных приложений, хотя 32-битные программы не смогут использовать 64-битные системные библиотеки, и наоборот. Чтобы справиться с этой проблемой, большинство 64-разрядных операционных систем предоставляют два набора необходимых системных файлов: один — для родных 64-битных приложений, и другой — для 32-битных программ.
Когда вы используете 64-битную операционную систему (Windows, Linux или какую-либо другую), ваш процессор работает в длинном режиме. 32-битные ОС теряют популярность и используются всё реже, так как не позволяют использовать весь потенциал современного железа. Так, дистрибутив Ubuntu Linux уже начиная с версии 17.10 не выпускается в 32-битном исполнении. Windows Server 2008 стала последней серверной ОС от Microsoft, которая имела 32-битную версию, и Server 2012 существует только 64-битная.
Legacy mode
Данный «унаследованный» режим позволяет процессору выполнять инструкции, рассчитанные для процессоров x86, и предоставляет полную совместимость с 32-битным кодом и операционными системами. В этом режиме процессор ведёт себя точно так же, как x86-процессор, например Athlon или Pentium III, и дополнительные функции, предоставляемые архитектурой x86-64 (например, дополнительные регистры), недоступны. В этом режиме 64-битные программы и операционные системы работать не будут.
Этот режим включает в себя подрежимы:
- Реальный режим (real mode)
- Защищённый режим (protected mode)
- Режим виртуального 8086 (virtual 8086 mode)
Реальный режим использовался в MS-DOS, в реальном режиме выполнялся код BIOS при загрузке компьютера.
Защищённый режим используется в 32-битных версиях современных многозадачных операционных систем (например, обычная 32-битная Windows XP работает в защищённом режиме, как и 32-битная версия Ubuntu 16.04).
Режим виртуального 8086 — подрежим защищённого, предназначался главным образом для создания т. н. «виртуальных DOS-машин». Если из 32-битной версии Windows вы запускаете 16-битное DOS-приложение, то работает эмулятор NTVDM (NT Virtual DOS Machine), который использует этот режим процессора. Другой эмулятор, DOSBox, не использует этот режим V86, а выполняет полную эмуляцию. Заметим, что в 64-битных версиях Windows эмулятор NTVDM был исключён, поэтому напрямую запустить на выполнение 16-битный com- или exe-файл стало невозможно (тем не менее, можно использовать тот же DOSBox или другой гипервизор для полной эмуляции реального режима).
Переход между режимами
Из длинного режима нельзя перейти в реальный или режим виртуального 8086 без перезагрузки. Поэтому, как уже отмечено, в 64-битных версиях Windows не работает NTVDM и нельзя запускать 16-битные программы.
Самый современный процессор x86-64 полностью поддерживает реальный режим. Если загрузка выполняется через BIOS, то код загрузчика (из сектора #0) исполняется в реальном режиме. Однако если вместо BIOS используется UEFI, то переход в Long mode происходит ещё раньше, и никакого кода в реальном режиме уже не выполняется. Можно считать, что современный компьютер сразу начинает работать в 64-битном длинном режиме.
Поэтому далее нас будет интересовать только длинный режим.
Трансляция адресов в памяти
Упрощённо говоря, процессор обращается к памяти через шину. Адресами памяти, которыми обмениваются в шине, являются физические адреса, то есть сырые числа от нуля до верхней границы доступной физической памяти (например, до 2 33 , если у вас установлено 8 ГБ оперативки). Ранее между процессором и микросхемами памяти располагался северный мост — отдельный чип, но в реализации Intel начиная с микроархитектуры Sandy Bridge он интегрирован на кристалл процессора.
Физические адреса являются конкретными и окончательными — без трансляции, без подкачки, без проверки привилегий. Вы выставляете их на шину и всё: выполняется чтение или запись.
Однако в современной операционной системе программы используют абстрацкию — виртуальное адресное пространство. Каждая программа пишется в такой модели, что она выполняется одна, всё пространство принадлежит ей, код использует адреса логической памяти, которые должны быть оттранслированы в физические адреса до того, как будет выполнен доступ к памяти. Концептуально трансляция выглядит следующим образом:
Это не физическая схема, а только описание процесса преобразования адресов. Такая трансляция осуществляется всякий раз, когда CPU выполняет инструкцию, которая ссылается на адрес памяти.
Логический адрес на x86 состоит из двух частей: селектора сегмента и смещения внутри сегмента. Процесс трансляции включает два шага:
- учёт сегментного селектора и переход от смещения внутри сегмента к некоторому линейному адресу;
- перевод линейного адреса в физический.
Спрашивается, зачем нужен первый шаг и зачем нужны эти сегменты, почему бы напрямую не использовать линейные адреса в программе? Это результат эволюции. Чтобы действительно понять смысл сегментации x86, нам нужно вернуться в 1978 год.
Сегментация
Реальный режим
16-битный процессор 8086 использовал 16-битные регистры и мог напрямую адресовать только 2 16 байт памяти. Инженеры придумывали, как же можно заставить его работать с большим объёмом памяти, не расширяя разрядность регистров.
Были придуманы сегментные регистры, которые должны были задавать, к какому именно 64-килобайтному куску памяти относится данный 16-битный адрес.
Решение выглядит логичным: сначала вы устанавливаете сегментный регистр, по сути говоря “так, я хочу работать с куском памяти начиная с адреса X”; затем 16-битный адрес уже используется как смещение в рамках этого куска.
Всего предусматривалось сначала четыре 16-битных сегментных регистра, потом добавили ещё два:
- CS = Code Segment
- DS = Data Segment
- ES = Extra (или Destination) Segment
- SS = Stack Segment
- FS
- GS
Названия этих регистров связаны с назначением. При выполнении инструкций они загружаются из сегмента кода. При обращении к стеку (инструкции push/pop) неявно используется сегмент стека (при работе с регистрами SP и BP). Некоторые инструкции (так называемые «строковые») используют фиксированные сегменты, например инструкция movs копирует из DS:(E)SI в ES:(E)DI.
Для вычисления линейного адреса ячейки памяти процессор вычисляет физический адрес начала сегмента — умножает сегментную часть виртуального адреса на число 16 (или, что то же самое, сдвигает её влево на 4 бита), а затем складывает полученное число со смещением от начала сегмента. Таким образом, сегменты частично перекрывались, и всего можно было адресовать около 1 МБ физической памяти. Спрашивается, почему не умножать значение сегментного регистра сразу на 65536, ведь тогда можно было бы адресовать 4 ГБ памяти. Тогда это было не нужно и только растило стоимость чипа.
В реальном режиме отсутствует защита памяти и разграничение прав доступа.
Программы были маленькие, поэтому их стек и код полностью помещались в 64 КБ, не было проблем. В языке C тех древних времён обычный указатель был 16-битный и указывал относительно сегмента по умолчанию, однако существовали также far-указатели, которые включали в себя значение сегментного регистра. Призраки этих far-указателей преследуют нас в названиях типов в WinAPI (например LPVOID — long (far) pointer to void).
#include int main(){ char far *p =(char far *)0x55550005; char far *q =(char far *)0x53332225; *p = 80; (*p)++; printf("%d",*q); return 0; }
Тут оба указателя указывают на один и тот же физический адрес 0x55555.
Защищённый режим
В 32-битном защищенном режиме также используется сегментированная модель памяти, однако уже организованная по другому принципу: расположение сегментов описывается специальными структурами (таблицами дескрипторов), расположенными в оперативной памяти.
Сегменты памяти также выбираются все теми же сегментными регистрами. Значение сегментного регистра (сегментный селектор) больше не является сырым адресом, но вместо этого представляет собой структуру такого вида:
Существует два типа дескрипторных таблиц: глобальная (GDT) и локальная (LDT). Глобальная таблица описывает сегменты операционной системы и разделяемых структур данных, у каждого ядра своя. Локальная таблица может быть определена для каждой конкретной задачи (процесса). Бит TI равен 0 для GDT и 1 для LDT. Индекс задаёт номер дескриптора в таблице дескрипторов сегмента. Поле RPL расшифровывается как Requested Privilege Level.
Сама таблица представляет собой просто массив, содержащий 8-байтные записи (дескрипторы сегмента), где каждая запись описывает один сегмент и выглядит так:
Помимо базового адреса сегмента дескрипторы содержат размер сегмента (точнее, максимально доступное смещение) и различные атрибуты сегментов, использующиеся для защиты памяти и определения прав доступа к сегменту для различных программных модулей. Базовый адрес представляет собой 32-битный линейный адрес, указывающий на начало сегмента, а лимит определяет, насколько большой сегмент. Добавление базового адреса к адресу логической памяти дает линейный адрес (никакого умножения на 16 уже нет). DPL (Descriptor Privilege Level) — уровень привилегий дескриптора; это число от 0 (наиболее привилегированный, режим ядра) до 3 (наименее привилегированный, пользовательский режим), которое контролирует доступ к сегменту.
Когда CPU находится в 32-битных режимах, регистры и инструкции могут в любом случае адресовать всё линейное адресное пространство. Итак, почему бы не установить базовый адрес в ноль и позволить логическим адресам совпадать с линейными адресами? Intel называет это «плоской моделью», и это именно то, что делают современные ядра операционных систем под x86. Это эквивалентно отключению сегментации.
Понятно, что раз таблицы GDT и LDT лежат в памяти, каждый раз ходить в них за базовым адресом долго. Поэтому сегментные дескрипторы кешируются в специальных регистрах в момент загрузки (в тот момент, когда происходит запись в сегментный селектор).
Местоположение GDT в памяти указывается процессору посредством инструкции lgdt.
Длинный режим
На архитектуре x86-64 в длинном (64-битном) режиме сегментация не используется. Для четырёх сегментных регистров (CS, SS, DS и ES) базовый адрес принудительно выставляются в 0. Сегментные регистры FS и GS по-прежнему могут иметь ненулевой базовый адрес (но он стал 64-битным и может быть установлен через отдельные моделезависимые регистры (MSR)). Это позволяет ОС использовать их для служебных целей.
Например, Microsoft Windows на x86-64 использует GS для указания на Thread Environment Block, маленькую структурку для каждого потока, которая содержит информацию об обработке исключений, thread-local-переменных и прочих per-thread-сведений. Аналогично, ядро Linux использует GS-сегмент для хранения данных per-CPU.
Посмотрим на таблицы сегментных дескрипторов. Таблица LDT на самом деле вышла из употребления и сейчас не используется. В таблице GDT в современных системах есть как минимум пять записей:
- Null — первая ячейка не используется (сделано, чтобы нулевое значение селектора было зарезервированным [1]);
- Kernel Code;
- Kernel Data;
- User Code;
- User Data.
Практика: просмотр регистров
(gdb) info registers rax 0x40052d 4195629 rbx 0x0 0 rcx 0x0 0 rdx 0x7fffffffde78 140737488346744 rsi 0x7fffffffde68 140737488346728 rdi 0x1 1 rbp 0x7fffffffdd80 0x7fffffffdd80 rsp 0x7fffffffdd80 0x7fffffffdd80 r8 0x7ffff7dd4e80 140737351863936 r9 0x7ffff7dea700 140737351952128 r10 0x7fffffffdc10 140737488346128 r11 0x7ffff7a32e50 140737348054608 r12 0x400440 4195392 r13 0x7fffffffde60 140737488346720 r14 0x0 0 r15 0x0 0 rip 0x400531 0x400531 eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0
Мифы о сегментных регистрах
Сегментные регистры остались в прошлом, в 64-битном длинном режиме они окончательно выпилены. Это неправда. Регистры существуют, даже GDB их печатает.
Раньше сегментные регистры были 16-битные, а сейчас они уже 64-битные. Это неправда, регистры CS, SS, DS, ES и пр. всегда были 16-битными и остаются такими, у них нет расширенных E- или R-версий.
Сегодня в этих регистрах всегда записан ноль. Это тоже неправда, там записан индекс сегментного дескриптора в таблице, а уже в этом дескрипторе базовый адрес равен тождественно нулю. Из приведённого вывода GDB очевидно, что CS и SS не равны нулю. К тому же биты RPL играют свою роль.
Раз сегментные регистры не нужны, я могу в своей программе делать с ними разные арифметические операции. Это неправда, регистры не относятся к регистрам общего назначения и их не получится использовать в качестве операндов в любых инструкциях.
Ладно, я смогу использовать их для хранения произвольных данных, а именно 16-битных чисел. Так тоже не выйдет, потому что при любом обращении к памяти процессор всё равно обращается к сегментному селектору и проверяет сегментный дескриптор.
Кольца защиты
Вы, вероятно, знаете интуитивно, что приложения имеют ограниченные полномочия на компьютерах Intel x86 и что только код операционной системы может выполнять определенные задачи. Как это работает? Уровни привилегий x86 — механизм, с помощью которого ОС и ЦП ограничивают возможности программ пользовательского режима.
Существует четыре уровня привилегий: от 0 (наиболее привилегированных) до 3 (наименее привилегированных). В любой момент времени процессор x86 работает на определенном уровне привилегий, который определяет, что код может и не может сделать. Эти уровни привилегий часто описываются как защитные кольца, причем самое внутреннее кольцо соответствует самым высоким привилегиям. [2]
Большинство современных ОС на x86 используют только Ring 0 и Ring 3.
Кольца управляют доступом к памяти. Код с уровнем привилегий i может смотреть только данные уровня i и выше (менее привилегированных).
На кольце 0 можно делать всё. На кольце 3, например, нельзя:
- изменить текущее кольцо защиты (иначе весь механизм был бы бесполезен);
- изменить таблицу страниц;
- зарегистрировать обработчик прерываний;
- выполнить ввод-вывод инструкциями in и out;
- .
Текущий уровень привилегий (CPL) определяется сегментным дескриптором кода. Если сейчас исполняется сегмент кода с уровнем привилегий 3, значит, исполняется пользовательский код. Если исполняется код с уровнем привилегий 0 — исполняется код ядра.
При обращении к памяти проверяется неравенство
max(CPL, RPL) ≤ DPL,
- CPL — текущий уровень привилегий,
- RPL — записан в сегментном регистре (селекторе),
- DPL — записан в сегментном дескрипторе.
Еси неравенство ложно, генерируется ошибка general protection fault (GPF).
Как мы ранее выяснили, системные вызовы позволяют менять текущий уровень привилегий, поэтому эта операция достаточно тяжёлая.
Страничная организация памяти
Страничная память — способ организации виртуальной памяти, при котором виртуальные адреса отображаются на физические постранично.
В семействе x86 поддержка появилась с поколения 386, оно же первое 32-битное поколение.
Если сегментация сейчас практически не используется, то таблицы страниц, наоборот, используются вовсю во всех современных операционных системах. Его важно понимать, так как с особенностями страничной организации можно прямо или косвенно столкнуться при решении прикладных задач.
Страницы
Виртуальная память делится на страницы. Размер размера страницы задается процессором и обычно на x86-64 составляет 4 KiB. Это означает, что управление памятью в ядре выполняется с точностью до страницы. Когда вам понадобится новая память, ядро предоставит вам одну или несколько страниц. При освобождении памяти вы вернёте одну или несколько страниц. Каждый более гранулярный API (например malloc) реализуется в пространстве пользователя.
Физическая память также поделена на страницы.
Виртуальное адресное пространство
Хотя виртуальные адреса имеют разрядность в 64 бита, текущие реализации (и все чипы, которые находятся на стадии проектирования) не позволяют использовать всё виртуальное адресное пространство из 2 64 байт (16 экзабайт). Это будет примерно в четыре миллиарда раз больше виртуального адресного пространства на 32-битных машинах. В обозримом будущем большинству операционных систем и приложений не потребуется такое большое адресное пространство, поэтому внедрение таких широких виртуальных адресов просто увеличит сложность и расходы на трансляцию адреса без реальной выгоды. Поэтому AMD решила, что в первых реализациях архитектуры фактически при трансляции адресов будут использоваться только младшие 48 бит виртуального адреса.
Кроме того, спецификация AMD требует, что старшие 16 бит любого виртуального адреса, биты с 48-го по 63-й, должны быть копиями бита 47 (по принципу sign extension). Если это требование не выполняется, процессор будет вызывать исключение. Адреса, соответствующие этому правилу, называются «канонической формой». Канонические адреса в общей сложности составляют 256 терабайт полезного виртуального адресного пространства. Это по-прежнему в 65536 раз больше, чем 4 ГБ виртуального адресного пространства 32-битных машин.
Это соглашение допускает при необходимости масштабируемость до истинной 64-разрядной адресации. Многие операционные системы (включая семейство Windows NT и GNU/Linux) берут себе старшую половину адресного пространства (пространство ядра) и оставляют младшую половину (пользовательское пространство) для кода приложения, стека пользовательского режима, кучи и других областей данных. Конструкция «канонического адреса» гарантирует, что каждая совместимая с AMD64 реализация имеет, по сути, две половины памяти: нижняя половина «растет вверх» по мере того, как становится доступнее больше виртуальных битов адреса, а верхняя половина — наоборот, вверху адресного пространства и растет вниз.
Первые версии Windows для x64 даже не использовали все 256 ТБ; они были ограничены только 8 ТБ пользовательского пространства и 8 ТБ пространства ядра. Всё 48-битное адресное пространство стало поддерживаться в Windows 8.1, которая была выпущена в октябре 2013 года.
Структура таблицы страниц
Ставится задача транслировать 48-битный виртуальный адрес в физический. Она решается аппаратным обеспечением — блоком управления памятью (memory management unit, MMU). Этот блок является частью процессора. Чтобы транслировать адреса, он использует структуры данных в оперативной памяти, называемые таблицами страниц.
Вместо двухуровневой системы таблиц страниц, используемой системами с 32-битной архитектурой x86, системы, работающие в длинном режиме, используют четыре уровня таблицы страниц.
Возможные размеры страниц:
- 4 КБ (2 12 байт) — наиболее часто используется (как и в x86)
- 2 МБ (2 21 байт)
- 1 ГБ (2 30 байт)
Пусть для определённости размер страницы равен 4 КБ. Значит, младшие 12 битов адреса кодируют смещение внутри страницы и не изменяются, а старшие биты используются для определения адреса начала страницы.
CR3 — это специальный регистр процессора. В записях каждой таблицы лежит физический адрес начала таблицы следующего уровня.
Полная иерархия сопоставления страниц размером 4 КБ для всего 48-битного пространства займет немногим больше 512 ГБ ОЗУ (около 0.195% от виртуального пространства 256 ТБ).
Кеширование
Таблицы страниц хранятся в оперативной памяти. Если при каждом обращении по виртуальному адресу выполнять полностью трансляцию адресов, это будет работать очень медленно. Поэтому в процессоре реализуется специальный кеш под названием «буфер ассоциативной трансляции» (Translation lookaside buffer, TLB).
На практике вероятность промаха TLB невысока и составляет в среднем от 0,01% до 1%.
Практика: как скоро оно упадёт?
Понятно, что данный код по стандарту некорректен, содержит Undefined Behavior, а раз так, то компилятор может сделать что угодно, например не упасть вообще. Но тем не менее, если запускать на x86-64, то падает оно в определённый момент.
#include #include char buf[1]; #define PAGE_SIZE 4096 int main() { char* ptr = buf; for (;;) { int offset = (intptr_t)ptr % PAGE_SIZE; printf("%p: offset = %d\n", ptr, offset); *ptr = 'a'; // Segmentation fault expected! ++ptr; } return 0; }
Литература
Тема 3.9. Исполнение программного кода
Задача центрального процессора — выполнять программы (программный код), находящиеся в основной памяти (кэш-памяти). Он вызывает команды из памяти (кэш-памяти), определяет их тип, а затем выполняет их одну за другой. Компоненты соединены шиной, представляющей собой набор параллельно связанных проводов, по которым передаются адреса, данные и сигналы управления.
Рис.26 Действие при работе с МП
Когда команды извлекаются из кэша (или основной памяти), их необходимо декодировать и отправить на исполнение.
Как видно, весь процесс обработки команды состоит из четырех шагов, что и определяет так называемый 4 – ступенчатый процесс (конвейер).
1. Извлечение из кэша (оперативной памяти).
2. Декодирование (разборка команды).
3. Исполнение команды (применение действий).
4. Запись в кэш (оперативную память).
Каждую из этих ступеней команда должна проходить ровно за один такт. Поэтому чем быстрее каждая из ступеней выполняет свои функции, тем быстрее работает весь процессор и тем выше его тактовая частота. Выполнение всех этих четырех команд определяет цикл. Большинство процессоров действительно исполняют команды за один цикл, но существуют сложные команды, для которых требуется несколько циклов. При исполнении сложных команд различные устройства задействуют собственные исполнительные конвейеры, тем самым, добавляя еще несколько ступеней к основному конвейеру процессора. Количество ступеней определяет глубину конвейера.
Программный код — это последовательность команд, или инструкций, каждая из которых определенным образом закодирована и расположена в целом числе смежных байтов памяти. Каждая инструкция обязательно имеет операционную часть, несущую процессору информацию о требуемых действиях. Операндная часть, указывающая процессору, где находится его «предмет труда» — операнды, — может присутствовать в явном или неявном виде и даже отсутствовать. Операндная часть может описывать от нуля до двух операндов, участвующих в выполнении данной инструкции (есть инструкции, в которых, помимо двух операндов, задается еще и параметр инструкции). Здесь могут быть сами значения операндов (непосредственные операнды); явные или неявные указания на регистры процессора, в которых находятся операнды; адрес (или его составная часть) ячейки памяти или порта ввода-вывода; регистры процессора, участвующие в формировании адреса, и разные комбинации этих компонентов. Длина инструкции 32-битного процессора семейства х86 может быть от 1 до 12 байтов (а с префиксами — и до 17 байтов) и определяется типом инструкции. Исторически сложившийся формат инструкций х86 довольно сложен, и «понять», сколько байтов занимает конкретная инструкция, процессор может, лишь декодировав ее первые 1-3 байта. Инструкции могут предшествовать префиксы (к счастью, всегда однобайтные, но их может быть несколько), указывающие на изменение способа адресации, размера операнда или/и необходимость многократного (по счетчику и условию) повторения для данной инструкции. Адрес (логический) текущей исполняемой инструкции хранится в специальном регистре — указателе инструкций (InstructionPointer, IP), который соответствует счетчику команд фон-неймановской машины. После исполнения так называемой линейной инструкции этот указатель увеличивает свое значение на ее длину, то есть указывает на начало следующей инструкции. Линейная инструкция не нарушает порядок выполнения инструкций, определяемый последовательностью их расположения в памяти (по нарастанию адреса). Помимо линейных инструкций существуют инструкции передачи управления, среди которых различают инструкции переходов и вызовов процедур. Эти инструкции в явном или неявном виде содержат информацию об адресе следующей выполняемой инструкции, который может указывать на относительно произвольную ячейку памяти.
Инструкции переходов и вызовов могут быть безусловными (ни от чего не зависящими) и условными. Произойдет ли условный переход (вызов) или нет, зависит от состояния флагов (признаков) на момент исполнения данной инструкции. Если переход (вызов) не состоится, то исполняется инструкция, расположенная в памяти вслед за текущей. Вызов процедуры характерен тем, что перед ним процессор сохраняет в стеке (стек — это область ОЗУ) адрес следующей инструкции, и на этот адрес передается управление после завершения исполнения процедуры (этот адрес извлекается из стека при выполнении инструкции возврата). Переход выполняется безвозвратно.
Последовательность исполнения инструкций, предписанная программным кодом, может быть нарушена под воздействием внутренних или внешних (относительно процессора) причин. К внутренним причинам относятся исключения (exceptions) — особые ситуации, возникающие при выполнении инструкций. Наглядным примером исключения является попытка деления на ноль. При возникновении условия исключения процессор автоматически выполняет вызов процедуры обработки исключения, после которой он может вернуться к повторному исполнению инструкции, породившей исключение, или следующей за ней. Вариант поведения зависит от типа произошедшего исключения. Исключения широко используются современными операционными системами. На основе обработки исключений строится система виртуальной памяти и реализуются многие функции многозадачных операционных систем.
Внешними причинами изменения нормальной последовательности инструкций являются аппаратные прерывания — вызовы процедур под воздействием электрических сигналов на специальные выводы процессора или по получении сообщения по специальному интерфейсу контроллера прерываний. Эти сигналы могут подаваться совершенно неожиданно для исполняемой программы; правда, у программиста есть возможность заставить процессор (компьютер) игнорировать все прерывания или их часть.
Исполнение программного кода
Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:
Все современные вычислительные машины построены по принципам и имеют структуру, предложенную еще в 40–х годах академиком Джоном Фон Нейманом.
Согласно первому принципу ЭВМ состоит из ряда устройств, взаимодействующих друг с другом в процессе решения задачи. Рассмотрим кратко основные устройства и их функции (рис. 2.1).
Рис.2.1. Структурная схема ЭВМ
Арифметико–логическое устройство (АЛУ) предназначено для выполнения предусмотренных в ЭВМ арифметических и логических операций. Участвующие в операциях данные выбираются из ОЗУ, результаты операций отсылаются в ОЗУ. Для ускорения выборки операндов (данных, участвующих в операциях) АЛУ может снабжаться собственной местной памятью (сверхоперативным запоминающим устройством – СОЗУ) на небольшое число данных (в сравнении с ОЗУ), но обладающей быстродействием, превышающим быстродействие ОЗУ. При этом результаты операций, если они участвуют в последующих операциях, могут не отсылаться в ОЗУ, а храниться в СОЗУ. Оперативная память вместе с СОЗУ представляет собой единый массив памяти, непосредственно доступный процессору для записи и чтения данных, а также считывания программного кода. К настоящему времени для оптимизации работы созданы процессоры с несколькими уровнями (от одного до трех) кэширования ОЗУ (несколькими СОЗУ).
Устройство управления (УУ) – координирует работу процессора, посылая в определенной временной последовательности управляющие сигналы в устройства ЭВМ, обеспечивая их соответствующее функционирование и взаимодействие друг с другом.
Оперативная память (ОЗУ) – реализуется, как правило, на модулях (микросхемах) динамической памяти. ОЗУ служит для хранения программы, исходных данных задачи, промежуточных и конечных результатов решения задачи.
Память ЭВМ к настоящему времени приобрела довольно сложную структуру и «расползлась» по многим компонентам. Кроме оперативной, память включает также и постоянную (ПЗУ), из которой можно только считывать команды и данные, и некоторые виды специальной памяти (например видеопамять графического адаптера). Вся эта память вместе с оперативной располагается в едином пространстве с линейной адресацией. В любом компьютере обязательно есть постоянная память, в которой хранится программа начального запуска компьютера и минимальный необходимый набор сервисов (например: ROM BIOS).
Все узлы ЭВМ не входящие в ядро называются периферийными . Они обеспечивают расширение возможностей ЭВМ, облегчают пользование ими. В состав периферийных (внешних) устройств могут входить следующие узлы.
Внешняя память (устройства хранения данных, например, дисковые) – память, имеющая относительно невысокое быстродействие, но по сравнению с ОЗУ существенно более высокую емкость. Внешняя память предназначена для записи данных с целью последующего считывания (возможно, и на другом компьютере). От рассмотренной выше памяти, называемой также внутренней, устройства хранения отличаются тем, что процессор не имеет непосредственного доступа к данным по линейному адресу. Доступ к данным на устройствах хранения выполняется с помощью специальных программ, обращающихся к контроллерам этих устройств. В силу того что быстродействие внешней памяти значительно ниже быстродействия АЛУ, последнее в процессе работы взаимодействует лишь с ОЗУ, получая из него команды и данные, отсылая в эту память результаты операций. Часто при решении сложных задач емкость ОЗУ оказывается недостаточной. В этих случаях в процессе решения задач данные определенными порциями могут пересылаться из внешней памяти в ОЗУ, откуда они затем выбираются для обработки в АЛУ.
Устройства ввода/вывода (УВВ) служат для преобразования информации из внутреннего представления в компьютере (биты и байты) в форму, доступную окружающим, и обратно. Под окружающими понимаем как людей, так и другие машины (например технологическое оборудование, которым управляет компьютер). К устройствам ввода относятся клавиатура, мышь, джойстик, микрофон, сканер, видеокамера, различные датчики; к устройствам вывода – дисплей, принтер, плоттер, акустические системы (наушники), исполнительные механизмы. Список устройств ввода/вывода безграничен – благодаря фантазии и техническому прогрессу в него входят все новые и новые устройства; так, например, шлем виртуальной реальности из области фантастики вышел в производственно–коммерческую. Устройства хранения к УВВ относить некорректно, поскольку здесь преобразования информации ради доступности внешнему миру не происходит. Устройства хранения вместе с УВВ можно объединить общим понятием периферийные устройства. Существует еще большой класс коммуникационных устройств, предназначенных для передачи информации между компьютерами и (или) их частями. Эти устройства обеспечивают, например, соединение компьютеров в локальные сети или подключение терминала (это УВВ) к компьютеру через пару модемов. Периферийные и коммуникационные устройства снабжаются контроллерами или адаптерами, которые доступны процессору.
Ядром автоматизированного средства контроля является специализированный вычислитель – блок вычислителя цифрового (БВЦ ТАКТ51.51.000). Блок вычислителя цифрового предназначен для управления системой ТАКТ51, а также для обработки информации при проверке изделия и самопроверке работоспособности ТАКТ51. БВЦ относится к классу малых одноадресных управляющих специализированных цифровых вычислительных машин.
- ввод программы с 8–дорожечной перфоленты в оперативное запоминающее устройство и хранение программы в ОЗУ;
- обмен данными с периферийными блоками;
- математическую обработку результатов измерений, полученных с периферийных блоков;
- выявление и обработку неисправностей, возникающих в системе;
- взаимодействие оператора с БВЦ путем операций ручного управления и наблюдения посредством ПУ и ПО;
- отсчет текущего времени работы БВЦ.
По существу блок вычислителя цифрового – это электронная вычислительная машина, автоматически выполняющая интерпретацию программы (алгоритма) в виде физических процессов, назначением которых является реализация арифметических и логических операций над информацией, представленной в цифровой форме.
- восприятие вводимой в машину информации – исходных данных и программы решения задач;
- хранение введенной информации и выдачу ее в требуемые моменты времени, обусловленные программой;
- выполнение арифметических и логических операций;
- выдачу по программе результатов вычислений в удобной для восприятия форме;
- автоматическое управление вычислительным процессом в соответствии с введенной программой.
Для выполнения перечисленных функций в состав БВЦ входят: устройство ввода, запоминающее устройство (память), процессор, устройства вывода (являются периферийными по отношению к БВЦ и, по существу, не входят непосредственно в состав БВЦ).
Структура БВЦ изображена на рис.2.2.
Рис.2.2. Структура БВЦ ТАКТ51.
Устройство ввода (ПБВД–5) обеспечивает фотоэлектрическое считывание информации с восьмидорожечной перфоленты и представление считанной информации в двоичной форме в виде электрических сигналов, воспринимаемых оперативной памятью.
Запоминающее устройство (платы ОЗУ и ПЗУ) служит для хранения информации, необходимой для производства вычислений. В памяти размещаются программы, задающие порядок вычислений, и данные, представляющие исходные значения, промежуточные и конечные результаты вычислений.
Процессор – это центральное устройство БВЦ. Процессор «воспринимает» программу и на ее основе управляет работой всех устройств БВЦ, инициируя выполнение действий в памяти и устройствах ввода–вывода. Функцией процессора является выборка команд из памяти и их выполнение.
Периферийными устройствами вывода (цифропечатающее устройство ПЦПУ–6, устройство вывода перфоленточное ПУВЛ–1) обеспечивается вывод информации из памяти для ее последующего использования (печать цифр и символов, пробивка отверстий на ленте).
Работа БВЦ протекает следующим образом. Программа и исходные данные, представленные на носителе информации (перфоленте), считываются устройством ввода ПБВД–5 и загружаются в память, в адреса, указанные на перфоленте. Выполнение программы сводится к последовательной выборке команд из памяти и их выполнению средствами процессора и устройств ввода–вывода.
Программа проверки изделия или самопроверки системы ТАКТ51 определяет объем и последовательность операций, выполняемых ТАКТ51. БВЦ вводит программу проверки с перфоленты в оперативную память и в соответствии с программой выдает команды на управляющие и измерительные блоки системы, принимает и обрабатывает информацию с измерительных блоков, а результаты проверки выводит на печать на ЦПУ и/или перфорацию на УВЛ.
Команда представляет собой цифровой код, преобразующийся в БВЦ в управляющие сигналы. Для БВЦ определен 13–разрядный двоичный формат команды, показанный на рис.2.3:
Операционные системы
Аппаратное обеспечение компьютера – это интересно, это высокие технологии и т.д. Однако, без соответствующего программного обеспечения, это не более, чем груда дорогостоящего железа (точнее меди, аллюминия, а так же кремния и пластика).
По большому счету, ПО может работать с железом напрямую. В каком-то смысле это даже проще. Так часто делают, например, при разработке встраиваемых систем (скажем, микроволновок). Однако, любое ПО общего назначения должно бы выполняться на различном оборудовании одного класса, и тут работа с железом напрямую превращается в ад. Тут нам на помощь приходит операционная система.
С точки зрения программиста, основная задача операционной системы – предоставить универсальный интерфейс для работы с типовым оборудованием, такими как ПЗУ, ОЗУ, устройства ввода-вывода.
Задачи, которые фактически выполняет операционная система:
- Управление временем ЦП
- Управление памятью
- Управление периферийными устройствами
- Управление вводом-выводом
Так же ОС предоставляет API (интерфейс программирования приложений) для доступа к этим функциям.
Операционные системы можно разделить на 4 типа:
- Реального времени
- Однозадачные
- Многозадачные
- Многопользовательские
Замечу, что когда я говорю о “работать одновременно”, я не обязательно имею ввиду удаленно. В свое время я собирал машину с несколькими мониторами, мышами и клавиатурами на одном системном блоке, причем за каждым набором мог работать отдельный пользователь.
Основное отличие многозадачных от многопользовательских систем заключено в деталях реализации.
Ядро ОС
Ядро ОС – это центральная часть современных операционных систем. По сути, это программа, изолирующая аппаратную часть от всей остальной программной части. На x86-подобных процессорах, ядро ОС переводит процессор в защищенный режим и выполняется в нулевом кольце привилегий, и обеспечивает управление аппаратными ресурсами и запуск приложений в других кольцах привелегий.
Существуют различные подходы к архитектуре ядра. Два наиболее полярных (sic!) это:
Монолитное ядро все ядро исполняется в едином адресном пространстве. Это значительно ускоряет выполнение операций уровня ядра, однако ошибки в компонентах ядра могут приводить к краху всей системы или порче данных. Микроядерная архитектура ядро системы обеспечивает механизмы межпроцессового взаимодействия и низкоуровневый доступ к оборудованию. Основная работа выполняется в процессах, работающих с более низкими привилегиями (на пользовательском уровне). Это позволяет изолировать подсистемы друг от друга, и крах, скажем, сетевой подсистемы приведет к отказу сети, а не всей ОС в целом.
Так же существуют гибридные подходы, например, модульные ядра. Большинство существующих на данный момент систем используют именно гибридные подходы.
Процессы и потоки
Программный код представляет из себя набор инструкций для процессора и ОС. Однако сам по себе он инертен и ничего не делает, пока ОС не запустит процесс выполнения данного программного кода.
Стандарт ISO 9000:2000 определяет процесс как совокупность взаимосвязанных и взаимодействующих действий, преобразующих входящие данные в исходящие. По сути, это исполняемый экземпляр программы.
Часто в понятие процесс включается не только, собственно процесс исполнения программы, но и ресурсы, этой программой используемые, а именно:
- Образ исполняемого машинного кода, связанного с программой
- Память, включающая исполняемый код, процессо-специфичные данные (ввод и вывод), стек вызовов, и т.н. “кучу”.
- Дескрипторы ресурсов, выделенных процессу операционной системой.
- Атрибуты безопасности, такие как владелец и разрешения.
- Состояние процессора (контекст), такое как значение регистров, адресация физической памяти, и т.п.
ОС хранит информацию о процессе в структурах, называемых Process Control Block (PCB, блок управления процессом).
Так же, в зависимости от ОС, процесс может иметь несколько потоков выполнения. Их можно себе представлять как “под-процессы”, индивидуальные задачи внутри процесса, каждая из которых имеет собственный контекст исполнения, но общую память и дескрипторы ресурсов.
В большинстве реализаций, поток исполнения является ресурсом процесса, но при этом имеет собственный блок управления, называемый Thread Control Block (как правило, несколько урезанный по сравнению с PCB).
Управление временем ЦП
Управление временем ЦП сводится к двум взаимосвязанным задачам:
- Обеспечение каждого процесса временем ЦП для нормальной работы
- Максимально эффективное использование времени процессора
Кроме процессов приложений, система обычно выполняет множество сервисных процессов, или демонов, как их иногда называют. Демоны могут выполнять самые разные функции, например, управлять сетью, или обеспечивать связь между процессами. Основное отличие от процессов приложений в том, что демоны не взаимодействуют непосредственно с пользователем, работающим с системой, а выполняются в фоновом режиме.
Исполнение процесса наиболее просто описывается в однозадачной системе:
- Процесс создается операционной системой
- Процесс выполняется
- Процесс завершается
Исполнение процесса в таком случае может быть приостановлено только для обработки прерываний.
Очевидная проблема однозадачного подхода – простой ЦП в случаях, когда активный процесс, скажем, ожидает ввода пользователя или устройства. Для более эффективного использования процессроного времени используется многозадачность.
Существует несколько подходов к реализации многозадачности, в частности:
- Кооперативная
- Вытесняющая
- Гибридная
Кооперативная многозадачность полагается на то, что текущий исполняемый процесс сообщит ОС о том, что готов освободить время ЦП для работы других процессов. До этого момента переключения процессов не происходит. В зависимости от ОС, от процесса так же может требоваться сохранить свой контекст выполнения до передачи управления ОС, и восстановить его, когда управление будет передано обратно. Очевидный минус такого подхода состоит в том, что программная ошибка (либо просто неаккуратно написанная программа) легко может блокировать работу всей ОС. Однако, этот подход может быть очень удобен при разработке приложений реального времени, когда критическая задача не должна быть прервана никакой другой.
Прямо противоположный подход – это вытесняющая многозадачность. В данном случае ОС полностью берет на себя задачу управления временем выполнения процессов, создавая с точки зрения программного интерфейса иллюзию однозадачной системы. Наиболее распространенный вариант реализации вытесняющей многозадачности состоит в регулярной генерации прерывания, переключающего систему в контекст ядра. Ядро системы сохраняет текущий контекст выполнения, загружает новый контекст выполнения и передает управление другому процессу. При этом, выполнение любой программы может быть прервано в любой момент. Этот подход приобрел известную популярность, поскольку, во-первых, сильно упрощает написание программ, поддерживающих многозадачность, а во-вторых гарантирует продолжение работы системы даже в случае программных ошибок, приводящих к “зависаниям”.
Различные гибридные подходы позволяют части процессов выполняться в кооперативном режиме, в то время как для всех остальных применяется вытесняющая многозадачность. Такие подходы чаще всего используются в многозадачных ОС, требующих режима реального времени для отдельных задач.
Для описания работы процесса в многозадачной системе, воспользуемся диаграммой состояний.
Различные реализации могут иметь различное число состояний процесса, однако сейчас наиболее распространенным является вариант пяти состояний, приведенных на рисунке:
ОС начинает выполнение процесса с составления PCB и загрузки образа исполняемого кода. Затем ОС переводит процесс в состояние ожидания. При очередной генерации прерывания переключения задачи, ОС загружает содержимое регистров и т.п. из PCB и переводит процесс в состояние выполнения. При следующем прерывании переключения задачи, будет загружен PCB другого процесса и т.д. Если процесс ожидает ввода пользователя 1 или устройства, то он переключается в состояние “блокирован”, и сообщает ОС условия “разблокирования”. Когда эти условия выполняются, ОС автоматически переключает процесс в состояние ожидания. Блокированному процессу время не выделяется. При завершении, процесс устанавливает состояние “уничтожен”, и ОС при очередном переключении в контекст ядра, освобождает ресурсы, ссылки на которые есть в PCB и удаляет сам PCB.
Отдельно следует сказать о файле подкачки, или свопе. В целях экономии оперативной памяти, ОС может переносить память, выделенную процессу из ОЗУ в ПЗУ. Это относится только к процессам, которые находятся в ожидании или заблокированны. Тогда при очередном переключении на процесс, ОС должна загрузить память процесса в ОЗУ (возможно выгрузив память другого процесса в ПЗУ), затем загрузить PCB и только после этого передать управление самому процессу. Как не сложно догадаться, это не самая быстра операция. В современных ОС, память процесса может быть выгружена в своп по частям, что позволяет выносить в своп только редко используемые области памяти.
Существуют разные подходы к выделению времени процессам. Большинство современных ОС используют приоритезацию процессов, и выделяют время в первую очередь тем процессам, которые имеют более высокий приоритет. Особенно интересным становится вопрос выделения времени в многопроцессорных системах. Так, в частности, может быть испольвзован симметричный и асимметричный подходы.
В симметричном подходе, каждый процессор используестя по очереди всеми процессами: при очередном переключении процесса, ОС просто загружает процесс на следующий процессор.
При ассиметричном, процессы “привязываются” к процессорам, иногда эксклюзивно, иногда нет. В некоторых случаях, отдельный процессор выделяется под нужды ОС. Это нередко приводит к измеримому ускорению работы, поскольку процессор кэширует содержимое оперативной памяти, и при постоянном переключении процессов, кэш оказывается неактуальным, требуя обращения к основной оперативной памяти 2 .
С другой стороны, если какой-то из процессов, привязанных к процессору, требует много процессорного времени, это может плохо сказываться на работе других процессов, привязанных к этому процессору, в то время как при симметричном подходе, ОС автоматически сбалансирует распределение процессорного времени.
Большинство современных ОС используют гибридный подход: используется симметричное распределение времени с возможностью, при необходимости, привязки конкретных процессов к конкретным процессорам.
- Есть старая шутка, что с точки зрения программиста, пользователь – это устройство, в ответ на любой запрос возвращающее случайный набор символов.↩︎
- Это представляет гораздо меньшую проблему на системах с одним многоядерным процессором, поскольку часто ядра используют общий кэш (по крайней мере, на последнем уровне).↩︎