Сегменты, страницы и системные вызовы

  О, порождение Земли и Тьмы, мы приказываем тебе отречься...- твердым, повелительным тоном начал Гальдер.
Смерть кивнул.
— ДА, ДА, ЗНАЮ Я ВСЕ ЭТО. ВЫЗЫВАЛИ-ТО ЧЕГО'' Т. Пратчетт

Реализовав страничную или сегментную виртуапьную память, мы сталкиваемся с той же проблемой, о которой шла речь в разд. Системы с базовой виртуальной адресацией: пользовательские программы не имеют доступа к адресным пространствам друг друга и к таблице дескрипторов, но ведь им надо иметь возможность вызывать системные сервисы и передавать им параметры!
Один из основных способов решения этой проблемы сродни методу, применяемому в системах с базовой адресацией: вводятся два режима работы процессора, "системный" и "пользовательский", в которых используются разные таблицы дескрипторов. Переключение из пользовательского режима в системный осуществляется специальной командой, которая вызывает определенную процедуру в системном адресном пространстве.
Известную сложность при этом представляет передача параметров: как отмечалось выше, пользовательское адресное пространство может быть отображено на физические адреса весьма причудливым образом. Для разрешения указателя в этом адресном пространстве нам либо необходимо анализировать пользовательскую таблицу дескрипторов вручную, либо каким-то способом временно переключить диспетчер памяти на использование этой таблицы.
Большинство процессоров с диспетчером памяти предоставляют команд, копирования данных из пользовательского адресного пространства в системное и обратно. Конечно же. доступны эти команды только из системного режима.
Некоторые архитектуры предоставляют и более изощренные способы pеализации системных вызовов и передачи управления между системой и прикладной программой.

Виртуальная память и режимы процессора VAX
Например, миникомпьютеры VAX имеют четыре режима работы процессора (в порядке возрастания прав доступа): режим пользователя (User), режим супервизора (Supervisor), режим исполнителя (Executive) и режим ядра (Kernel) Режим работы определяется битами 22 и 23 в слове состояния процессора (рис. 5.5). Каждый из режимов имеет собственный указатель стека. Операционная система VAX/VMS использует три из доступных режимов (пользовательский, исполнительный и ядра), a BSD Unix - только два.

Каждая страница адресного пространства может иметь различные права доступа для разных режимов. При этом соблюдаются следующие правила.

  1. 1. Допустимы только права чтения и записи.
  2. 2. Право записи всегда выдается вместе с правом чтения.
  3. 3. Каждый более привилегированный режим всегда имеет те же права, которые имеют менее привилегированные режимы, плюс, возможно, какие-то еще. Такая организация доступа называется кольцами защиты.

Рис. 5.5. Слово состояния процессора VAX

Все допускаемые этими правилами комбинации прав могут быть закодированы при помощи четырех бит в дескрипторе страницы (табл. 5.1). Переключение режимов осуществляется командами СНМК, СНМЕ, CHMS и CHMU. Эти команды помещают слово состояния процессора и счетчик команд в стек, соответствующий новому режиму, сохраняют предыдущий режим в специальном поле слова состояния (зачем нужно сохранять режим в двух местах, мы поймем чуть позже), устанавливают новый режим и, наконец, передают управление по фиксированному адресу, аналогично команде SYSCALL в системах с двумя уровнями доступа. Передача управления по фиксированному адресу позволяет нам защититься от бесконтрольного повышения уровня доступа, а все предыдущие операции дают возможность вернуться на предыдущий уровень доступа с одновременной передачей управления, используя специальную команду REI, которая восстанавливает и счетчик команд, и слово состояния. Команды СHМ(Х) обычно используются для повышения уровня доступа, a REI может использоваться только для его понижения или возврата на тот же уровень (рис. 5.6).

Рис. 5.6. Переключение режимов процессора VAX

Таблица 5.1. Коды защиты для различных режимов доступа процессора VAX. Цитируется по [Прохоров, 1990]

Код Kernel Executive Supervisor User
0000 - - - -
0001 Не предсказуем
0010 RW - - -
0011 R - - -
0100 RW RW RW RW
0101 RW RW - -
0110 RW R - -
0111 R R - -
1000 RW RW RW -
1001 RW RW R -
1010 RW R R -
1011 R R R -
1100 RW RW RW R
1101 RW RW R RW
1110 RW R R R
1111 R R R R

R — право чтения, W — право записи,------отсутствие прав.

32-битное адресное пространство процессора VAX разбито на четыре части, каждая объемом по гигабайту. Первый гигабайт адресов предназначен для кода и данных пользовательской программы, второй — для пользовательского стека, третий — для системы, четвертый не используется (рис. 5.7). Каждая из частей имеет собственный указатель на таблицу дескрипторов страниц. Важно отметить, что деление адресного пространства на таблицы не обязательно связано с правами доступа на отдельные страницы — в системной таблице могут быть страницы, доступные для записи из пользовательского режима (на практике этого никогда не бывает, но на уровне диспетчера памяти контроля за этим не реализовано), а в пользовательской — страницы, доступные только ядру.

Рис. 5.7. Адресное пространство VAX

Системная таблица страниц одна во всей системе и содержится в адресных пространствах всех задач. Напротив, пользовательские таблицы у каждой задачи свои (внимательный читатель может отметить определенную параллель между этой структурой и описанным в разд. Банки памяти переключателем банков памяти, который присутствует во всех банках). Для того чтобы упростить системе управление пользовательскими таблицами дескрипторов, эти таблицы хранятся не в физическом, а в виртуальном системном адресном пространстве, и при доступе к ним происходит двойная трансляция адреса.
Ядро системы, таким образом, присутствует в адресных пространствах всех задач. Многие системные модули (например, функция для получения текущего реального времени) доступны для чтения из пользовательского режима и могут вызываться непосредственно, как обычные процедуры. Адреса точек входа этих процедур размещены в специальной таблице в начале системного адресного пространства (рис. 5.8). Другие системные модули (например, подсистема работы с файлами, RMS — Record Management Service (Служба управления записями)) требуют повышения уровня доступа: действительно, если одна из задач работает с файлами с ограниченным доступом, было бы неразумно позволять всем остальным задачам видеть используемые при этом системные буферы. Точки входа этих процедур размещаются в той же таблице, что и прямо вызываемые системные подпрограммы, но тела этих процедур состоят только из двух команд: переключения режима процессора и возврата.

Рис. 5.8. Точки входа системных подпрограмм VAX/VMS

Процедура, работающая в "повышенном" (более привилегированном) режиме процессора, имеет полный доступ ко всем данным режимов с более низким уровнем доступа. Благодаря этому мы можем передать привилегированной процедуре указатель, и она доберется до наших данных простым разрешением этого указателя, без каких бы то ни было специальных команд.
Впрочем, при таком подходе возникает определенная проблема. Поскольку система и пользователь находятся в одном адресном пространстве, пользователь может "подсунуть" системе указатель на страницу, к которой сам не имеет доступа — например, попросить считать нечто из файла в системный сегмент данных. Для исключения таких ситуаций VAX предоставляет команды PROBSR и PROBEW, которые проверяют, существует ли доступ к указанной странице в предыдущем режиме работы процессора. Как мы помним, предыдущий режим сохраняется не только в стеке, но и в слове состояния процесса, и нужно это именно для таких проверок.
Видно, что обойтись без специальных команд все-таки не удалось. К тому же платой за принятое в VAX техническое решение оказалось сокращение полезного адресного пространства задачи в два, а на самом деле даже в четыре (кому нужен стек размером 1 Гбайт?) раза. В 70-е годы, когда разрабатывался VAX, это еще не казалось проблемой.

Уровни доступа 80286
Чуть дальше в близком направлении продвинулись разработчики 80286: у этого процессора уровень доступа определяется старшими двумя битами селектора сегмента (рис. 5.9). Код, исполняющийся в сегменте с уровнем доступа 2, имеет доступ ко всем сегментам своего и более низких уровней. Межсегментный переход с повышением уровня доступа возможен лишь через сегменты со специальным дескриптором, так называемые шлюзы (gate).

Рис. 5.9. Структура адреса процессора i80286

В этой архитектуре для проверки прав доступа к сегменту в предыдущем режиме работы не нужны специальные команды, достаточно проверки селектора сегмента.
80286, хотя и предоставлял почти полноценную сегментную адресацию, не имел сегментных отказов, поэтому использовать все преимущества виртуальной памяти на этом процессоре было невозможно. Вторым недостатком было отсутствие режима совместимости с 8086 — не существовало возможности создать такую таблицу трансляции, которая бы воспроизводила специфическую структуру адресного пространства этого процессора. Отчасти это было обусловлено и использованием битов в селекторе сегмента для задания прав доступа. Обе ОС, которые разрабатывались для этого процессора,— Win16 и OS/2 1 .х — большого успеха не имели.
OS/2 использует три режима доступа: пользовательский, системные DLL и собственно ядро, из четырех, предоставляемых архитектурой х86 (рис. 5.10).
Windows NT (которая начинала свою карьеру как OS/2 New Technology) первоначально проектировалась как переносимая ОС. Требование переносимости на RISC-архитектуры с двумя уровнями привилегий заставило разработчиков отказаться от уровня системных DLL. Позже фирма Microsoft постепенно отказалась от поддержки всех аппаратных архитектур, кроме х86 (дольше всех они держались за DEC Alpha), но двухуровневая структура доступа так и осталась в новых версиях этой системы — Windows 2000/XP.

Рис. 5.10. Уровни доступа в OS/2

Системы семейства Unix используют х86 как нормальную 32-разрядную машину с двухуровневым доступом: пользовательской задаче выделяется один сегмент, ядру— другой. 4-х гигабайтового сегмента х86, разбитого на страницы размером по 4 Кбайт, достаточно для большинства практических целей. Например, в Linux системный вызов исполняется командой int 8Oh. Селектор пользовательского сегмента помещается в регистр FS. Для доступа к этому сегменту из модулей ядра используются процедуры memcpy_from_fs и memcpy_to_fs.

Многоуровневый доступ, основанный на концепции колец, не имеет принципиальных преимуществ по сравнению с двумя уровнями привилегий. Как и в двухуровневой системе, пользовательские модули вынуждены полностью доверять привилегированным, привилегированные же модули не могут защититься даже от ошибок в собственном коде. Самое лучшее, что может сделать Windows XP, обнаружив попытку обращения к недопустимому адресу в режиме ядра, — это нарисовать на синем экране дамп регистров процессора. В OS/2, фатальная ошибка в привилегированных модулях, исполняемых во втором кольце защиты, не обязательно приводит к остановке ядра, но подсистема, в которой произошла ошибка, оказывается неработоспособна. Если испорчен пользовательский интерфейс или сетевая подсистема, система в целом становится бесполезной и нуждается в перезагрузке.
Кроме того, разделение адресных пространств создает сложности при разделении кода и данных между процессами: разделяемые объекты могут оказаться отображены в разных процессах на разные адреса, поэтому в таких объектах нельзя хранить указатели (подробнее см. разд. Разделяемые библиотеки). Стремление обойти эти трудности и создать систему, в которой сочетались бы преимущества как единого (легкость и естественность межпроцессного взаимодействия), так и раздельных (защита процессов друг от друга) адресных пространств, многие годы занимало умы разработчиков аппаратных архитектур.
Например, машины фирмы Burroughs предоставляли пользователю и системе единое адресное пространство, разбитое на сегменты (единую таблицу дескрипторов сегментов). При этом возникает вопрос: если все задачи используют одну таблицу, как может получиться, что разные задачи имеют разные права на один и тот же сегмент?
Решение этого вопроса состоит в том, что права доступа кодируются не дескриптором, а селектором сегмента. Таким образом, разные права доступа на один сегмент — это, строго говоря, разные адреса. Понятно, что такое разделение работает лишь постольку, поскольку пользовательская программа не может формировать произвольные селекторы. В компьютерах Burroughs это достигалось теговой архитектурой: каждое слово памяти снабжалось дополнительными битами, тегом (tag), который определял тип данных, хранимых в этом слове, и допустимые над ним операции. Битовые операции над указателями не допускались. Благодаря этому задача не могла сформировать не только произвольные права доступа, но и вообще произвольный указатель.
Аналогичным образом реализована защита памяти в AS/400 [redbooks.ibm.com sg242222.pdf). Пользовательские программы имеют общее адресное пространство. Первоначально это общее пространство имен, в процессе же преобразования имени в адрес бинарное представление селектора сегмента снабжается и битами доступа, которые затем обрабатываются точно так же, как в машинах Burroughs, и точно так же недоступны пользовательскому коду для модификации.
Реализации языка С для этой архитектуры допускают использование указателей только в пределах одного сегмента кода или данных, но не позволяют формировать средствами С произвольные селекторы сегментов, остальные же языки, используемые на этой платформе (Cobol, RPG, языки хранимых процедур СУБД) вообще не имеют указателя как понятия.
Еще дальше в том же направлении продвинулись разработчики фирмы Intel, создавая экспериментальный микропроцессор 1АРХ432, описанный в следующем разделе.