Совместное использование памяти
В однозадачных системах основная память разделяется на две части: одна часть — для операционной системы (резидентный монитор, ядро), а вторая - для выполняющейся в текущий момент времени программы. В многозадачных системах "пользовательская" часть памяти должна быть распределена для размещения нескольких процессов. Эта задача распределения выполняется операционной системой динамически и известна под названием управления памятью (memory management).
Эффективное управление памятью жизненно важно для многозадачных систем. Если в памяти располагается только небольшое число процессов, то большая часть времени все эти процессы будут находиться в состоянии ожидания завершения операций ввода-вывода, и загрузка процессора будет низкой. Таким образом, желательно эффективное распределение памяти, позволяющее разместить в ней как можно больше процессов.
При рассмотрении различных механизмов и стратегий, связанных с управлением памятью, полезно помнить требования, которым они должны удовлетворять:
- Перемещение.
- Защита.
- Совместное использование.
- Логическая организация.
- Физическая организация.
В многозадачной системе доступная основная память разделяется на множество процессов. Обычно программист не знает заранее, какие программы будут резидентно находиться в основной памяти во время работы разрабатываемой им программы. Для максимизации загрузки процессора желательно иметь много процессов, готовых к исполнению, для чего требуется возможность загрузки активных процессов из основной памяти. Требование, чтобы выгруженная программа была вновь загружена в то же самое место, где находилась и работала, было бы слишком сильным ограничением. Крайне желательно, чтобы она могла быть перемещена (relocate) в другую область памяти. таким образом, заранее неизвестно, где именно будет размещена программа. Кроме того, программа может быть перемещена из одной области памяти в другую при свопинге.
Каждый процесс должен быть защищен от нежелательного воздействия других процессов случайного или преднамеренного. Следовательно, код других процессов не должен иметь возможности без разрешения обращаться к памяти данного процесса для чтения или записи. Однако удовлетворение требованию перемещаемости усложняет задачу защиты. Поскольку расположение программы в основной памяти непредсказуемо, проверка абсолютных адресов во время компиляции невозможна. Кроме того, в большинстве языков программирования возможно динамическое вычисление адресов во время исполнения. Любой механизм защиты должен иметь достаточную гибкость, для того чтобы обеспечить возможность нескольким процессам обращаться к одной и области основной памяти. Система управления памятью должна, таким образом, обеспечивать управляемый доступ к разделяемым областям памяти, при этом никоим образом не ослабляя зашиты памяти.
Фактически всегда основная память в компьютерной системе организована как линейное (одномерное) адресное пространство, состоящее из последовательности байт или слов. Аналогично организована и вторичная память на своем физическом уровне. Хотя такая организация и отражает особенности используемого аппаратного обеспечения, она не соответствует способу, которым обычно создаются программы. Большинство программ организованы в виде модулей, некоторые из которых неизменны, а другие содержат данные, которые могут быть изменены. Если операционная система и аппаратное обеспечение компьютера могут эффективно работать с пользовательскими программами и данными, представленными модулями, то это обеспечивает ряд преимуществ.
Модули могут быть созданы и скомпилированы независимо друг от друга при этом все ссылки из одного модуля во второй разрешаются системой во время работы программы.
Разные модули могут получить разные степени защиты (только для чтения, только для исполнения) за счет весьма умеренных накладных расходов.
Возможно применение механизма, обеспечивающего совместное использование модулей разными процессами. Основное достоинство обеспечения совместного использования на уровне модулей заключается в том» что они соответствуют взгляду программиста на задачу и, следовательно, ему проще определить, требуется или нет совместное использование того или иного модуля.
Инструментом, наилучшим образом удовлетворяющим данным требованиям, является сегментация, которая будет рассмотрена в данной главе среди прочих методов управления памятью.
Память компьютера разделяется как минимум на два уровня: основная и вторичная. Основная память обеспечивает быстрый доступ по относительно высокой цене; кроме того, она энергозависима, т.е. не обеспечивает долговременного хранения. Вторичная память медленнее и дешевле основной и обычно энергонезависима. Следовательно, вторичная память большой емкости может служить для долговременного хранения программ и данных, а основная память меньшей емкости — для хранения программ и данных, использующихся в текущий момент.
В такой двухуровневой структуре основной заботой системы становится организация потоков информации между основной и вторичной памятью. Ответственность за эти потоки может быть возложена и на отдельного программиста, но это непрактично и нежелательно по следующим причинам:
- Основной памяти может быть недостаточно для программы и ее данных.
- В многозадачной среде программист при разработке программы не знает, какой объем памяти будет доступен программе и где эта память будет располагаться.
Таким образом, очевидно, что задача перемещения информации между двумя уровнями памяти должна возлагаться на операционную систему. Эта задача является сущностью управления памятью.
Защита памяти.
Для возможности создания надежных мультипрограммных ОС в процессорах семейства i80x86 имеется несколько механизмов зашиты. Это и разделение адресных пространств задач, и введение уровней привилегий для сегментов кода и сегментов данных. Вес это позволяет обеспечить как защиту задач друг от друга, гак и защиту самой операционной системы от прикладных задач, защиту одной части ОС от других ее компонентов, защиту самих задач от некоторых своих собственных ошибок.
Защита адресного пространства задач осуществляется относи гелыю легко за счет того, что каждая задача может иметь свое собственное локальное адресное пространство. Операционная система должна корректно манипулировать таблицами трансляции сегментов (дескрипторными таблицами) и таблицами трансляции страничных кадров. Сами таблицы дескрипторов как сегменты данных (а соответственно, в свою очередь, и как страничные кадры) относятся к адресному пространству операционной системы и имеют соответствующие привилегии доступа; исправлять их задачи не могут. Этими информационными структурами процессор пользуется сам, на аппаратном уровне, без возможности их читать и редактировать из пользовательских приложений. Если используется модель плоской памяти, то возможность микропроцессора контролировать обращения к памяти только внутри текущего сегмента фактически не используется, и остается и основном только механизм отображения страничных кадров. Выход за пределы страничного кадра невозможен, поэтому фиксируется только выход за пределы своего сегмента. В этом случае приходится полагаться только на систему программирования, которая должна корректно распределять программные модули в пределах единого неструктурированного адресного пространства задачи. Поэтому при создании мпогопоточных приложений, когда каждая задача (в данном случае — поток) может испортить адресное пространство другой .задачи, эта проблема становится очень сложной, особенно если не использовать системы программирования на языках высокого уровня.
Однако для организации взаимодействия задач, имеющих разные виртуальные адресные пространства, необходимо, как мы уже говорили, иметь общее адресное пространство, И здесь, для обеспечения защиты самой ОС, а значит, и повышения надежности всех вычислении, используется механизм защиты сегментов с помощью уровней привилегий.
Для того чтобы запретить пользовательским задачам модифицировать области памяти, принадлежащие самой ОС, необходимо иметь специальные средства. Одного разграничения адресных пространств через механизм сегментов мало, ибо можно указывать различные значения адреса начала сегмента и тем самым получать доступ к чужим сегментам. Другими словами, необходимо в явном виде разграничивать системные сегменты данных и кода от сегментов, принадлежащих пользовательским программам. Поэтому были введены два основных режима работы процессора: пользователя и супервизора. Большинство современных процессоров имеют по крайней мере два этих режима. Так, в режиме супервизора программа может выполнять все действия и иметь доступ по любым адресам, тогда как в пользовательском режиме должны быть ограничения, с тем чтобы обнаруживать и пресекать запрещенные действия, перехватывая их и передавая управление супервизору ОС. Часто в пользовательском режиме запрещается выполнение команд ввода/вывода и некоторых других, чтобы гарантировать, что только ОС выполняет эти операции. Можно сказать, что эти два режима имеют разные уровни привилегий. Указания уровня привилегий используются два бита, поэтому код 00 обозначает самый высший уровень, а код 11(2) (-3) — самый низший. Самый высокий уровень привилегий предназначен для операционной системы (прежде всего, для ядра ОС), самый низкий — для прикладных задач пользователя. Промежуточные уровни привилегий введены для большей свободы системных программистов в организации надежных вычислений при создании ОС и иного системного ПО. Предполагалось, что уровень с номером (кодом) 1 может быть использован, например, для системного сервиса — программ обслуживания аппаратуры, драйверов, работающих с портами ввода/вывода. Уровень привилегий с кодом 2 может быть использован для создания пользовательских интерфейсов, систем управления базами данных и т.п., то есть для реализации специальных системных функций, которые по отношению к супервизору ОС ведут себя как обычные приложения. Так, например, система OS/2 использует три уровня привилегий: с нулевым уровнем привилегий исполняется код самой ОС, на втором уровне исполняются системные процедуры подсистемы ввода/вывода, на третьем уровне исполняются прикладные задачи пользователей. Однако чаще всего на практике используются только дна уровня — нулевой и третий. Таким образом, упомянутый режим супервизора для микропроцессоров i80x86 соответствует выполнению кода с уровнем привилегий 0 (его обозначают так: PL01)- Подводя итог, можно констатировать, что именно уровень привилегий задач определяет, какие команды в них можно использовать и какое подмножество сегментов и/или страниц в их адресном пространстве они могут обрабатывать.
В микропроцессорах i80x86 имеются не два, а четыре уровня привилегий. Часто уровни привилегий называют кольцами защиты, поскольку это иногда помогает объяснить принцип действия самого механизма; поэтому говорит, ч го некоторый программный модуль «исполняется в кольце защиты с таким-то номером».
Основными системными объектами, которыми манипулирует процессор при работе в защищенном режиме, являются дескрипторы. Дескрипторы сегментов содержат информацию об уровне привилегии соответствующего сегмента кода или данных. Уровень привилегии исполняющейся задачи определяется значением поля привилегии, находящегося в дескрипторе ее текущего кодового сегмента. Напомним, что в каждом дескрипторе сегмента (см. рис.3.3) имеется поле DPL в байте прав доступа, которое и определяет уровень привилегии связанного с ним сегмента. Таким образом, поле DPL текущего сегмента кода становится полем С PL2. При обращении к какому-нибудь сегменту в соответствующем селекторе указывается запрашиваемый уровень привилегий RPL3 (см. рис. 3.4).
В пределах одной задачи используются сегменты с различным уровнем привилегии и в определенные моменты времени выполняются или обрабатываются сегменты с соответствующими им уровнями привилегии. Механизм проверки привилегий работает в ситуациях, которые можно назвать межсегментными переходами (обращениями). Это доступ к сегменту данных или стековому сегменту, межсегментные передачи управления в случае прерываний (и особых ситуаций), при использовании команд CALL, JMP, INT, IRET, RET. В таких межсегментных обращениях участвуют два сегмента: целевой сегмент (к которому мы обращаемся) и текущий сегмент кода, из которого идет обращение.
Функции доступа.
Первым шагом в создании активного процесса является загрузка программы в оперативную память и создание образа процесса. Приложение состоит из ряда скомпилированных или ассемблированных модулей в виде объектного кода. Эти модули связываются для разрешения всех ссылок между ними, а также обращений к библиотечным подпрограммам (которые могут быть внедрены в программу или быть совместно используемым кодом, представляемым операционной системой). Основными функциями доступа и распределения оперативной памяти системы пользуются загрузчики и компоновщики. В их задачи входит организация размещения и связи между собой отдельных модулей программы(процесса).
Абсолютный загрузчик требует, чтобы данный загружаемый модуль располагался в одном и том же месте в памяти. Следовательно, в модуле, передаваемом для загрузки, все обращения должны быть к конкретным, или относительным, адресам основной памяти.
Рис. 7- Загрузка
Назначение определенных адресов ссылкам к памяти в программе может быть выполнено либо программистом, либо автоматически в процессе компиляции или ассемблирования. У такого подхода имеется ряд серьезных недостатков. Во-первых, каждый программист должен знать стратегию размещения модулей в основной памяти. Во-вторых, при любых изменениях в программе, которые включают вставку или удаление кода или данных, требуется соответствующим образом изменить все адреса. Поэтому желательно, чтобы все адреса в памяти были выражены символьно, с тем чтобы в процессе компиляции или ассемблирования разрешить эти символьные ссылки. Каждая ссылка на команду или элемент данных изначально представлена символом. При подготовке модуля к абсолютной загрузке ассемблер или компилятор преобразуют все эти ссылки в конкретные адреса.
Компоновщик получает в качестве входа набор объектных модулей и генерирует на их основе загружаемый модуль объединением множества модулей кода и данных для последующей передачи его загрузчику. В каждом объектном модуле могут иметься ссылки на память в других модулях. Каждая такая ссылка в нескомпонованном модуле может быть выражена только символьно. Компоновщик создает единый загружаемый модуль, который объединяет все отдельные о6ъектные модули. Kaждая ссылка из одного модуля в другой должна быть разрешена и транслирована из символьного адpeca в ссылку на ячейку памяти загружаемого модуля.
Природа компоновки адресов зависит от типа создаваемого модуля и времени компоновки. Если требуется создание перемещаемого модуля, то компоновка обычно выполняется следующим образом. Каждый скомпилированный объектный модуль создается со ссылками относительно начала объектного модуля. Все эти модули объединяются в единый перемещаемый загружаемый модуль, в котором все ссылки даны относительно начала единого модуля. Такой модуль можно использовать для переносимой загрузки или динамической загрузки времени исполнения.
Предположим, наступило время загрузки нового процесса в память. Операционная система начинает ее с размещения в памяти только одного или нескольких блоков, включая блок, содержащий начало программы. Часть располагающегося в некоторый момент времени в основной памяти, называется резидентным множеством процесса. Во время выполнения процесса все происходит, гак, как если бы все ссылки были только на резидентное множество процесса. При помощи таблицы сегментов или страниц процессор всегда может определить, располагается ли блок, к которому требуется обращение в основной памяти. Если процессор сталкивается с логическим адресом, который не находится в основной памяти, он генерирует прерывание, свидетельствующее об ошибке доступа к памяти. Операционная система переводит процесс в заблокированное состояние и получает управление.
Чтобы продолжить выполнение прерванного процесса, операционной системе необходимо загрузить в основную память блок, содержащий вызвавший проблемы логический адрес. Для этого операционная система использует запрос на чтение с диска (во время выполнения которого может продолжаться выполнение других процессов). После того как необходимый блок загружен в основную память, выполняется прерывание ввода-вывода, передающее управление операционной системе, которая, в свою очередь, переводит заблокированный процесс в состояние готовности.