Что называют базовым адресом сегмента памяти
Перейти к содержимому

Что называют базовым адресом сегмента памяти

  • автор:

Сегментная адресация памяти в 32-х разрядных системах

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

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

Под словом «память» я буду подразумевать, конечно же, оперативную память компьютера. О жестком диске речь будет идти только в момент упоминания подкачки.

Деление памяти на сегменты

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

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

Селектор сегмента

Первое из двух чисел в логическом адресе называется селектором, он 16-ти разрядный, среди которых старшие 13 бит являются индексом(порядковым номером) сегмента в таблице дескрипторов. Остальные три бита делятся следующим образом: 1 бит называется TI и указывает на тип дескрипторной таблицы(0 — глобальная, 1 — локальная), еще два бита отданы под RPL — запрашиваемый уровень привилегий.

Селектор в сегментной модели памяти

При обращении к нужному дескриптору RPL будет сравниваться с DPL этого сегмента. Как вы уже могли догадаться, DPL(Descriptor privilege level) это уровень привилегий дескриптора.

Кстати, о доступе. Благодаря делению памяти на сегменты появилась возможность разграничивать права доступа и пресекать подлые попытки переписать чужую память, это стало настоящим прорывом. Всего существует 4 уровня привилегий: 0-2 соответствуют супервизору, при этом на нулевом уровне доступны самые привилегированные действия, 3 — уровень пользователя(пользовательских программ).

Глобальная таблица дескрипторов — GDT

Чуть более подробно об этой таблице. Все сегменты хранятся в определенной структуре, называемой GDT — Global Descriptor Table. Из названия видно, что она представляет из себя простую таблицу. Хранится в оперативной памяти всегда, подгружается при запуске операционной системы, на ее начало указывает специальный регистр GDTR. Кстати, это один из немногих регистров процессора, который хранит в себе физический адрес. И он же хранит размер таблицы, таким образом мы и операционная система всегда можем иметь доступ к нашим сегментам.

Из размерности поля индекса в селекторе можно посчитать максимальное возможное количество дескрипторов в таблице — 2 13 = 8192 дескриптора.

Размер дескриптора равен 8 байтам или 32 битам. Вот как выглядит такая запись, называемая дескриптором.

Базовый адрес есть ни что иное как физический адрес сегмента в памяти. Об остальных битах подробно расписано на википедии , я не стану повторяться.

Локальная таблица дескрипторов

Кроме GDT еще существует LDT — Local Descriptor Table — которая отвечает за сегменты каждого отдельного процесса. Ссылки на все LDT содержатся в GDT, но в любой локальной таблице не может быть ссылок на другую локальную. А во всем остальном локальные таблицы не отличаются от глобальной.

Трансляция адресов в сегментной модели памяти

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

  1. Первым делом сравниваются RPL селектора и DPL сегмента, если запрашиваемый уровень привилегий меньше или равен уровню доступа сегмента, то доступ разрешен.
  2. Теперь мы должны взять базовый адрес сегмента и прибавить к нему смещение из логического адреса.
  3. Проверить, что полученный физический адрес не выходит за границы сегмента. Для этого существует поле limit в дескрипторе.
  4. Если все условия соблюдены, мы получаем доступ к ячейке памяи по физическому адресу(база сегмента + смещение).

Трансляция адресов в сегментной модели памяти

Заключение

С появлением дескрипторов отпала сама главная головная боль программистов и ужас системных администраторов — появились права доступа к памяти. Но остались еще подводные камни, достаточно затратно следить за сегментами из-за того, что у них произвольный размер. Побороться с этим вызвалась так называемая «страничная» память. Но о ней уже в следующем выпуске, спасибо за внимание!

Сегментная адресация памяти

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

Селектором называется число (в x86 — 16-битное), однозначно определяющее сегмент. Селектор загружается в сегментные регистры.

В реальном и защищённом режимах x86-процессора функционирование сегментной адресации отличается.

Oops something went wrong:

Сегментная адресация памяти

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

В подавляющем большинстве компьютерных систем для работы с некоторой ячейкой памяти необходимо как-то указать ее адрес, как правило 16-, 32- или 64-разрядное число. Количество бит в адресе часто называют разрядностью системы. Часто дополнительно используется механизм «трансляции страниц», который отображает области виртуальной памяти пользовательского приложения в физическую память, которой управляет операционная система. Но в каждый момент времени активна только одна «таблица страниц» и с точки зрения приложения (а во многом и с точки зрения ядра ОС) память остается плоской.

Рассмотрим старый процессор Intel 86/88/186. Размер регистров этих процессоров всего 16 бит, что позволяет адресовать только 64 килобайта памяти. Когда эти микросхемы разрабатывались, такого размера памяти уже не хватало для многих приложений, а 32-разрядные процессоры были слишком дороги. Проблему решили добавив в архитекруру сегментные регистры. При обращении к памяти по 16-битному адресу (хранящемуся в регистре общего назначения или прямо в коде команды) прибавлялось значение сегментного регистра, сдвинутое на 4 бита (что то же самое, умноженное на 16) и полученное значение использовалось как физический адрес. Такой подход позволял адресовать до одного мегабайта памяти. В архитектуре персональных компьтеров IBM PC, созданных на базе этих процессров, часть адресного пространства была зарезервирована для системных нужд, а пользовательским приложениям и ОС было доступно до 640 килобайт. Но не все так просто.

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

Как можно реализовать язык C на i86? Какой выбрать размер указателя на ячейку памяти? Первый вариант — использовть 16-битный адрес. Архитектура процессора поддерживала 4 сегментных регистра — CS, DS, ES и SS. CS всегда содержит сегмент кода. Язык C не предоставляет стандартных средств для модификации кода на лету, что позволяет компилятору и операционной системе более менее вольно с ним обращаться. Если весь код помещается в 64 КБ, то все хорошо, если нет — компилятор вынужден использовать более дорогие межсегментные вызовы и переходы, но это сравнительно небольшая цена. Некоторую сложность создают указатели на функции, которые требуются по стандарту C. Интерфейс функций вызываемых в пределах одного сегмента и с помощью межсегментных вызовов отличался — межсегментные вызовы дополнительно сохраняли в стеке старое значение CS, а для локальных вызовов это не требовалось. Кроме того размер адреса функции был разный — для внутрисегментной достаточно было 16-бит, межсегментная должна хранить сегмент и смещение в этом сегменте. (Компилятор мог бы располагать функции на адресах, кратных 16, и ссылаться только по значению сегмента, но я не видел реализацию C или другого языка, которая бы так умела.)

Регистр DS предназначен для сегмента данных — его использовали по умолчанию практически все команды работы с памятью (кроме некоторых строковых команд, использовавших ES, и обращений в стек, о которых чуть позже). Таким образом если для данных хватало 64 КБ, то можно все их хранить в одном сегменте и использовать 16-битный указатель.

Большинство компиляторов (но не все) на каждый вызов функции создают stack frame, область стека, которая содержит адрес возврата, аргументы и локальные переменные. В i86 для стека используется сегментный регистр SS. Операции push/pop, вызовы подпрограмм и выход из них неявно использовали регистр SP (stack pointer). В i86 не было команд, явно использующих для адресации SP, поэтому компилятор в начале подпрограммы копировал значение SP в регистр BP (base pointer), и для работы с аргументами и локальными переменными использовал его. При адресациии через BP по умолчанию использовался сегмент стека. В i386 появилась возможность явно использовать SP для обращения в память, в этом случае также использовался сегмент стека. Таким образом архитектура позволяла независимо использовать 64 КБ данных и 64 КБ стека, а таже реализовать многозадачность с моделью «общий хип, частные стеки», но.

Но язык C требует возможности получения указателя на произвольный объект, глобальный, аллокированный в сегменте данных или на стеке, в том числе на локальные переменные и аргументы функций. Таким образом, если мы хотим использовать 16-битный указатель, мы должны объединить сегменты данных и стека.

Компиляторы C для i86 поддерживали несколько моделей памяти, каждая из которых имела свои недостатки и ограничения. Кроме того, независимо от модели поддерживалось несколько дополнительных типов указателей — far и huge, которые хранили сегмент и смещение. Отличие было в поддержке адресной арифметики. Far предполагал что все адресуемые через него данные находятся в том же сегменте. Huge поддерживал полноценную адресную арифметику, для чего компилятор генерировал дополнительный код, проверяющий пересечение границы сегмента и пересчитывающий значение сегмента при необходимости.

Таким образом на x86 терялись главные преимущества языка C — эффективность и переносимость.

Архитектура x86 явно использовала 20-битную адресацию памяти и расширить ее не теряя совместимости с существующими приложениями было не просто. Тем не менее в i286 было реализовано интересное решение. Сегмент теперь использовался не как базовый адрес, а как индекс в таблице, по этому индексу извлекались тип, права доступа, размер сегмента и адрес в физической памяти. Значение селектора сегмента делилось на три поля — 13-битный индекс в таблице сегментов, 1-битный признак локальности и 2 бита отводилось для упавления правами. В зависимости от признака локальности, описание сегмента бралось из глобальной или локальной для задачи таблицы дескрипторов сегментов. Дескриптор задачи сам являлся дескриптором сегмента особого типа. Таким образом операционная система могла создать до 2^13 задач, каждая из которых владела бы 2^13 сегментами, каждый по 64 КБ, что составляет аж 4096 террабайт виртуальной памяти. Но структура таблицы поддерживала только 24-битный физический адрес начала сегмента и физической памяти можно было использовать только 16 мегабайт. В дополнение к увеличению доступной памяти была реализована защита памяти, что позволило запускать вполне серьезные операционные системы — Unix (Xenix), Windows 3.1, OS/2.

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

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

Настоящий прорыв в архитектуре x86 случился с появлением процессора i386. Процессор стал 32-битным, 32-битными стали и регистры общего назначения, и размеры сегментов. Также увеличился объем доступной оперативной памяти вместе с битностью адреса начала сегмента. В дополнение появилось еще два сегментных регистра — FS и GS, что должно было немного снизить потери на операциях загрузки сегментов. Но старые программы не могли использовать 32-битной адресации.

Кроме 32-битного режима в i386 появилась трансляция страниц. Это позволило запускать современные системы — Unix и Windows NT. 4 гигабайт адресуемой памяти хватает любым приложениям, и использование сегментов уже не требовалось, все можно поместить в один сегмент. Архитектура Unix была рассчитана на плоскую память языка C (включая код и данные, для удобства реализации загрузчика), и этот подход быстро был поддержан в других ОС.

В архитектуре amd64 использование сегментов сильно ограничили. Регистры CS, DS, ES и SS всегда указывают на всю виртуальную память задачи. Доступны только FS и GS, которые некоторые ОС используют для хранения локальных данных нитей.

x86 — хорошо известная, но не единственная архитектура с сегментной адресацией. Эта статья была задумана с целью немного рассказать про Plessey System 250 и Intel iAPX 432.

Тип сегмента x86 определял содержит он код или данные. В данные попадали и обычные данные, и указатели, и селекторы сегментов. Права на сегмент проверялись при попытке загрузить его в сегментный регистр или выполнить на него переход. А если пойти дальше, и специфицировать, какие сегменты могут содержать ссылки на другие сегменты, а какие не могут? Так и было сделано в Plessey System 250 (PP250).

PP250 — очень древняя система, выпущенная в 1974 году (для сравнения Intel 8086 начал производиться в 1978). К сожалению, я не смог найти описания ее системы команд или готовый эмулятор, поэтому описываю архитектуру памяти исходя из найденных статей. В этой системе поддерживалось несколько типов сегментов, в зависимости от типа сегмент содержал код, данные или ссылки на другие сегменты. Так как у прикладной программы не было возможности получить ссылку на сегмент из сегмента данных, отделять селектор и дескриптор сегментов, как это сделано в Intel 286+, необходимости не было. Также у разработчиков оставалась возможность менять формат ссылок на сегмент в разных версиях системы не теряя совместимости прикладного ПО (например, при увеличении количества доступной памяти), но, на сколько я знаю, она так и не была реализована.

Во многих языках программирования есть возможность создавать пользовательские типы данных, которые объединяют несколько объектов других, как встроенных, так и пользовательских типов, включая ссылочные типы. В PP250 такой объект не получается поместить в один сегмент, так как сегмент мог содержать либо ссылки, либо данные. Эта проблема была решена в Intel iAXP 432.

Intel iAXP 432 с ОС iMAX 432 начал разрабатываться в 1975 и был выпущен в 1981. Архитектура iAXP 432 оказала заметное влияние на организацию памяти в Intel 286, выпущенном годом позже. Основным ЯП для этой системы был язык Ada, но предполагалась поддержка и других языков, в частности Lisp. Одним из преимуществ таких архитектур является различимость указателей и данных, что упрощает реализацию важной для Lisp сборки мусора.

Сегмент iAPX 432 состоял из двух частей — области ссылок и области данных. Таким образом на уровне системы команд не было возможности попытаться превратить данные в указатель (в PP250 попытка загрузить ссылку из сегмента данных могла быть отслежена только во время исполнения).

Интересно, что разделение указателей и данных встречалось и в традиционных архитектурах, рассчитанных на плоскую память. Например в процессоре Motorola 68000 было два регистровых файла — регистры данных и регистры адресов. С точки зрения разработчика аппаратуры это упрощает реализацию конвейера и экономит биты в кодах команд. Правда с точки зрения разработчиков компиляторов это только усложняло алгоритмы распределения регистров. Похожая схема реализована в некоторый CPU для встраиваемых систем, таких как Blackfin.

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

  • ретрокомпьютеры
  • сегменты
  • capability-based security
  • capability maturity model

Сегментация памяти (Схема памяти компьютера)

Представляю, Вам, перевод статьи одного из разработчиков PHP, в том числе версии 7 и выше, сертифицированного инженера ZendFramework. В данный момент работает в SensioLabs и большую часть занимается низкоуровневыми вещами, в том числе программированием в С под Unix. Оригинал статьи здесь.

Ошибка Сегментации: (Компьютерная верстка памяти)

Несколько слов, о чем эта запись в блоге

Я планирую в будущем писать технические статьи о PHP, связанные с глубоким пониманием памяти. Мне нужно, чтобы мои читатели имели такие знания, которые им помогут понять некоторые концепции дальнейшего объяснения. Для того, чтобы ответить на этот вопрос, нам придется перемотать время назад в 1960-е года. Я собираюсь объяснить вам, как работает компьютер, а точнее, как происходит доступ к памяти в современном компьютере, а затем вы поймете, из-за чего происходит это странное сообщение об ошибке — Segmentation Fault.

То, что вы будете читать здесь, краткое изложение основ дизайна компьютерной архитектуры. Я не буду заходить слишком далеко, если это не нужно, и буду использовать хорошо известные формулировки, так что, кто работает с компьютером каждый день может понять такие важные понятия о том, как работает ПК. Существует много книг о компьютерной архитектуре. Если вы хотите углубиться дальше в этой теме, я предлагаю вам достать некоторые из них и начать читать. Кроме того, откройте исходный код ядра ОС и изучите его, будь то ядро Linux, или любое другое.

Немного истории computer science

Еще в то время, когда компьютерами были огромные машины, тяжелее тонны, внутри вы могли бы найти один процессор с чем-то вроде 16Kb оперативной памяти (RAM). Не будем углубляться дальше =) В этот период компьютер стоил примерно $ 150 000 и мог выполнить ровно одну задачу в момент времени. Если бы мы в то время нарисовали схему его памяти, то архитектура выглядела следующим образом:

image

  • Область памяти операционной системы(4kb)
  • Область памяти запущенных процессов (12Kb)

Но одна из главных проблем такой модели заключается в том, что компьютер (стоимостью $ 150 000) мог выполнять только одну задачу единовременно, и эта задача ужасно долго выполнялась: целые дни, чтобы обработать несколько Кб данных. По такой огромной цене явно не представляется возможным купить несколько машин, чтобы выполнить несколько процедур одновременно. Так что люди пытались распределить ресурсы машины. Это было время рождения многозадачности. Имейте в виду, что тогда было ещё очень рано говорить о многопроцессорных ПК. Как мы можем заставить одну машину с одним процессором, и на самом деле решить несколько различных задач? Ответ на этот вопрос — планирование. Пока один процесс занят в ожидании ввода / вывода (ожидает прерывание), процессор может запустить другой процесс. Мы не будем говорить о планировании на всех уровнях (слишком широкой теме), только о памяти. Если машина может выполнять несколько задач, один процесс за другим, то это означает, что память будет распределяться примерно таким образом:

image

Обе, задачи А и B, сохраняются в оперативной памяти, так как копировать их туда и обратно на диск — слишком ресурсозатратный процесс. Данные должны оставаться в оперативной памяти на постоянной основе, так как их соответствующие задачи по-прежнему работают. Планировщик дает некоторое время процессора, то задаче А, то B, и т.д… каждый раз предоставляя доступ к её области памяти. Но подождите, здесь есть проблема. Когда один программист будет писать код задачи B, он должен знать адреса границ памяти. Например, задача В будет располагаться с 10Kb памяти до 12Кб. Поэтому каждый адрес в программе должен жестко записан именно в эти пределы. Если машина затем будет выполнять ещё 3 задачи, то адресное пространство будет разделено на большее количество областей, и границы памяти задачи В сдвинутся. Программисту пришлось бы переписать свою программу, чтобы использовать меньше памяти, и переписывать каждый адрес указателя памяти.

Здесь очевидна так же другая проблема: что, если задача B получит доступ к памяти задачи А? Это легко может произойти, потому что, когда вы манипулируете указателями, небольшая ошибка при вычислении приводит к совершенно другому адресу. Это может испортить данные задачи А (перезаписать их). Также существует проблема безопасности, что если задача А работает с очень чувствительными данными? Нет никакого способа, чтобы предотвратить чтение задачей B некоторой области памяти задачи А. И последнее: что, если задача B по ошибке перезапишет память ОС? Например, память ОС от 0Kb до 4Kb, в случае если задача B перепишет этот участок, то операционная система наверняка потерпит крах.

Адресное пространство

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

Каждая система в настоящее время организована с таким макетом памяти как “код — стек — куча”, и это расстраивает.

Адресное пространство содержит все задачи (процессы) которые необходимо запустить:

  • Код: это машинные инструкции, которые процессор должен обработать
  • Данные: данные машинных команд которые обрабатываются вместе с ними.

image

  • Стек — это область памяти, где программа хранит информацию о вызываемых функциях, их аргументах и каждой локальной переменной в функции.
  • Куча — это область памяти где программист волен делать всё что угодно.
  • Код — область памяти, где будут храниться инструкции ЦП скомпилированной программы. Эти инструкции генерируются компилятором, но могут быть написаны вручную. Обратите внимание, что сегмент кода обычно делится на три части (текст, данные и BSS), но мы не будем так глубоко погружаться.
  • Код всегда фиксированного размера создаётся компилятором и будет весить (в нашем примере) 1Кб.
  • Стек, однако, является изменяемой зоной, так как программа работает. Когда функции вызываются, стек расширяется, при возвращении из функции: стек уменьшается.
  • Куча тоже является изменяемой зоной, когда программист выделяет память из кучи (malloc()), она расширяется, а когда программист освобождает память обратно (free()), куча сужается.

Виртуализация памяти

Если задача А получила адресное пространство, такое, которое мы видели, так же как и задача B. То как мы можем разместить их в памяти? Это кажется странным, но адресное пространство задачи А начинается от 0KB до 16Kb, так же как и задачи B. Хитрость заключается в создании виртуальной среды. На самом деле картина размещения А и В в памяти:

image

Когда задача А попытается получить доступ к памяти в своем адресном пространстве, например индекс 11К, где-то в своем собственном стеке, операционная система выполнит хак и на самом деле загрузит индекс памяти 1500, потому что индекс 11K принадлежит задаче B. На самом деле, всё адресное пространство каждой программы в памяти — это просто виртуальная память. Каждая программа, работающая на компьютере, обращается к виртуальным адресам, с помощью некоторых аппаратных чипов ОС будет обманывать процесс, когда он будет пытаться получить доступ к любой зоне памяти.

ОС виртуализирует память и гарантирует, что любая задача не сможет получить доступ к памяти, которой не владеет. Виртуализация памяти позволяет изолировать процесс. Задача А больше не может получить доступ к памяти задачи B и не сможет получить доступ к памяти ОС. И все это полностью прозрачно для задач на уровне пользователя, благодаря тоннам сложного кода ядра ОС. Таким образом, операционная система обслуживает каждый запрос памяти процесса. Это работает очень эффективно — даже если запущено слишком много различных программ. Для достижения этой цели процесс получает помощь от аппаратного обеспечения, главным образом от процессора и некоторых электронных компонентов вокруг него, таким как блок управления памятью (MMU ). MMU появился в начале 70-х годов, вместе с IBM, как отдельные чипы. Сейчас они встраиваются непосредственно в наши чипы CPU и являются обязательными для любой современной ОС для запуска. На самом деле, операционная система не выполняет тонны операций, а полагается на некоторые аппаратные особенности поведения, которые облегчат доступ к памяти.

Вот небольшая программа на C, показывающая некоторые адреса памяти:

#include #include int main (int argc, символ ** argv)

На моей LP64 x86_64 машине он показывает:

Code is at 0x40054c Stack is at 0x7ffe60a1465c Heap is at 0x1ecf010

Мы можем видеть, что адрес стека находится намного выше адреса кучи, а код расположен ниже стека. Но каждый из этих 3-х адресов являются подделками: в физической памяти, по адресу 0x7ffe60a1465c точно не расположено целое число 3. Помните, что каждая программа пользователя манипулирует адресами виртуальной памяти, тогда как программы уровня ядра, таких как само ядро ​​ОС (или аппаратный код драйвера) могут манипулировать физическими адресами RAM.

Трансляция адреса

Адресная трансляция — это формулировка, за которой скрывается некая магическая техника. Аппаратное обеспечение (ММУ)транслирует каждый виртуальный адрес программы уровня пользователя в правильный физический адрес. Таким образом, операционная система помнит для каждой задачи соответствие между его виртуальными адресами и физическими. И это является сложной задачей. Операционная система управляет всей памятью задач на уровне пользователя для каждого требования доступа к памяти, обеспечивая тем самым полную иллюзию процессу. Таким образом, операционная система преобразует всю физическую память в полезную, мощную и простую абстракцию.

Давайте подробно рассмотрим простой сценарий:
Когда процесс запускается, операционная система бронирует фиксированную область физической памяти, скажем, 16KB. Затем сохраняет начальный адрес этого пространства в специальную переменную, называемую базой. Потом устанавливает другую специальную переменную, называемую границами (или пределом) ширины пространства — 16КB. Далее операционная система сохранит эти два значения в таблице процессов, называемой PCB (Process Control Block).
А вот как выглядит процесс виртуального адресного пространства:

image

А вот его физическое изображение:

image

ОС решила сохранить его в физической памяти в диапазоне адресов от 4K до 20K. Таким образом, базовый адрес устанавливается в 4K, а предел установлен на 4 + 16 = 20К. Когда этот процесс планируется (учитывая некоторое время процессора), операционная система считывает обратно предельные значения из PCB и копирует их в конкретные регистры процессора. Когда CPU во время работы попытается загрузить, например, виртуальный адрес 2K (что-то в его куче), CPU добавит этот адрес базы, полученный от операционной системы. Таким образом, процесс доступа памяти приведет к физическому местоположению 2K + 4K = 6К.

Физический адрес = виртуальный адрес + предел

Если полученный физический адрес (6К) находится вне границ (-4K | 20K-), это означает, что процесс попытался получить доступ к неправильному участку памяти, которым он не владеет. Процессор сгенерирует исключение, и поскольку в ОС есть обработчик исключений для этого события, ОС активируется процессором и будет знать, что исключение памяти только что произошло на CPU. Затем ОС по умолчанию передаст сигнал поврежденному процессу “SIGSEGV”. Ошибка сегментации, которая по умолчанию (это может быть изменено) завершит задачу с сообщением — “Произошел сбой в работе с недопустимым доступом к памяти”.

Перемещение памяти

Еще лучше, если задача А не запланирована, это означает, что она извлекается из CPU, так как планировщик попросил запустить другую задачу (скажем, задачу B). При выполнении задачи B ОС может свободно переместить всё физическое адресное пространство задачи A. Операционная система часто получает время процессора при выполнении пользовательских задач. Когда завершается последний системный вызов, управление ЦП возвращается к ОС, и до выполнения системного вызова ОС может делать все, что захочет, управляя памятью, перемещая все пространство процесса в различные физические слоты карт памяти.

Это относительно просто: операционная система перемещает область 16K в другую 16K свободного пространства, и просто обновляет базовые и связанные переменные задачи A. Когда задача будет возобновлена ​​и передана CPU, процесс преобразования адреса по-прежнему будет работать, но это не приведет к тому же физическому адресу, как раньше.

Задача А не заметила ничего, с её точки зрения, её адресное пространство по-прежнему начинается от 0К до 16К. Операционная система и аппаратное обеспечение MMU берут полный контроль над каждым доступом к памяти для задачи А, давая ей полную иллюзию. Программист задачи A манипулирует своими разрешенными виртуальными адресами, от 0 до 16, а MMU будет заботиться о позиционировании в физической памяти.

Образ памяти после перемещения будет выглядеть следующим образом:

image

В настоящее время программист больше не должен задаваться вопросом, где его программа находиться в оперативной памяти, если другая задача работает рядом с его собственной, и какими адресами манипулировать. Это делает сама ОС вместе с очень производительным и умным аппаратным средством — “Блок управления памяти” (MMU).

Сегментация памяти

Обратите внимание на внешний вид слова «сегментация» — мы близки к объяснению почему происходят ошибки сегментации. В предыдущей главе мы объяснили о трансляции и перемещении памяти, но модель, которую мы использовали имеет свои недостатки:
Мы предположили, что каждое виртуальное адресное пространство фиксируется шириной 16Kb. Очевидно, что это не так в действительности. Операционная система должна поддерживать список физической памяти свободных слотов (шириной 16 Kb), чтобы иметь возможность найти место для любого нового процесса с просьбой начать или переместить запущенный процесс. Как сделать это эффективно, чтобы не замедлить всю систему? Как вы можете видеть, каждый процесс занимает участок памяти 16 Kb, даже если процесс не использует всё своё адресное пространство, что очень вероятно. Эта модель явно тратит много памяти, процесс потребляет1KB памяти, а его участок памяти в физической памяти 16Кб. Такие отходы называют внутренней фрагментацией: память зарезервирована, но никогда не используется.

Для решения некоторых из этих проблем, мы собираемся погрузиться в более сложную организацию памяти в ОС — сегментацию. Сегментацию легко понять: мы расширяем идею «базы и границ» трех логических сегментов памяти: кучи, кода и стека; каждого процесса — вместо того, чтобы просто рассматривать образ памяти в качестве одного уникального объекта.

При такой концепции, память между стеком и кучей больше не тратится впустую. Вот так:

image

Теперь легко заметить, что свободное пространство в виртуальной памяти для задач А больше не выделяется в физической памяти, использование памяти становится гораздо более эффективным. Единственным отличием является то, что в настоящее время для любой задачи, ОС должна запоминать не одну пару баз/границ, а три: по одной паре для каждого типа сегмента. ММУ заботится о переводе, так же, как и раньше, и теперь поддерживает, 3 базовых величин, и 3 границ.

Например, здесь, куча задачи A имеет базу 126K и границы 2К. Затем задача просит доступ к виртуальному адресу 3KB, в куче; физический адрес 3Kb — 2Кб (начало кучи) = 1Kb + 126K (смещение) = 127K. 127K находится перед 128К — это правильный адрес памяти, который может быть выполнен.

Совместное использование сегментов

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

image

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

Поскольку код не является изменяемым, сегменты кода все созданы с флагом разрешения RX. Процесс может загрузить эту область памяти для выполнения, но любой процесс, попытавшийся писать в эту область памяти, будет завершен ОС. Остальные два сегмента: куча и стек являются RW, процессы могут читать и писать от своего собственного стека / кучи, но они не могут выполнять код из него (это предотвращает недостатки безопасности программы, где плохой пользователь может захотеть повредить кучу или стек, чтобы ввести код для запуска, которому будет доступно ядро ОС. Это невозможно, так как куча и стек сегментов часто не являются исполняемыми. Обратите внимание, что это было не всегда, так как требует дополнительной поддержки аппаратного обеспечения для эффективной работы, это называется «бит NX» под процессором Intel). Сегменты памяти разрешений изменяемы во время выполнения: задача может потребовать mprotect () от ОС. Эти сегменты памяти отчетливо видны под Linux, используйте утилиты /proc//maps или /usr/bin/pmap

Ниже приведен пример для PHP:

$ pmap -x 31329 0000000000400000 10300 2004 0 r-x-- php 000000000100e000 832 460 76 rw--- php 00000000010de000 148 72 72 rw--- [ anon ] 000000000197a000 2784 2696 2696 rw--- [ anon ] 00007ff772bc4000 12 12 0 r-x-- libuuid.so.0.0.0 00007ff772bc7000 1020 0 0 ----- libuuid.so.0.0.0 00007ff772cc6000 4 4 4 rw--- libuuid.so.0.0.0 . . 

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

Самое большое преимущество разделяемых объектов под Linux (и Unix) — это экономия памяти. Возможно также создать общую зону, ведущую к общему физическому сегменту памяти, с помощью системного вызова mmap(). Буква ‘s’, появившаяся рядом с этой областью, означает “разделяемая”.

Пределы Сегментации

Мы видели, что сегментация решает проблему не используемой виртуальной памяти. Когда процесс не использует некоторый объем памяти, этот процесс не отображается в физическую память благодаря сегментам, которые соответствуют выбранной памяти. Тем не менее, это не совсем верно. Что делать, если процесс требует 16Kb кучи? ОС, скорее всего, создаст сегмент физической памяти размером 16КБ. Но если пользователь затем освобождает 2Кб такой памяти? Тогда, ОС должна уменьшить сегмент 16Кб до 14kb. Что делать, если теперь программист спрашивает 30Kb кучи? Старый сегмент 14Kb теперь должен вырасти до 30 КБ, но может ли он это сделать? Другие сегменты теперь могут окружать наш сегмент 14kb, препятствуя его росту. Тогда ОС придется искать свободное пространство 30 КБ, а затем перемещать сегмент.

image

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

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

image

Но такой алгоритм уплотнения сложен для CPU, и в это время ни один пользовательский процесс не сможет получить ​​CPU: операционная система полностью загружена реорганизацией своей физической памяти, таким образом, система становится непригодной для использования. Сегментация памяти решает множество проблем управления памятью и многозадачности, но она также показывают реальные недостатки. Таким образом, существует необходимость в расширении возможностей сегментации и исправлении этих недостатков. Отсюда образовалось ещё одно понятие — “пагинация памяти”.

Пагинация памяти

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

Разбивка решает эту проблему с помощью простой концепции: что если для каждого физического распределения ядро ​​будет выделять фиксированный размер? Страницы — это сегменты физической памяти фиксированного размера. Если операционная система использует распределения фиксированного размера, то намного легче управлять пространством, что в итоге сводит на нет фрагментацию памяти.
Давайте покажем пример еще раз, предполагая небольшое 16Кб виртуальное адресное пространство, чтобы облегчить представление:

image

С пагинацией мы не говорим о кучи, стеке или сегменте кода. Мы разделим весь процесс виртуальной памяти на зоны фиксированного размера: мы называем их страницы. На приведенном выше примере мы разделили адресное пространство в виде 4-х страниц, 4КБ каждый.

Затем мы делаем то же самое с физической памятью.

image

И операционная система просто хранит то, что называется «Таблица процессорных страниц» — связь между одной страницей виртуальной памяти процесса и базовой физической страницей (который мы называем «страница кадра»).

image

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

Давайте покажем пример преобразования адресов с нумерацией страниц. Виртуальное адресное пространство размером 16Kb, т.е. нам нужно 14 бит для представления адреса (2 ^ 14 = 16Kb). Размер страницы 4 Кб, так что нам нужно 4kb (16/4), чтобы выбрать нашу страницу:

image

Теперь, когда процесс хочет загрузить, например, адрес 9438 (из 16384 возможностей), это дает 10.0100.1101.1110 в двоичной системе, что приводит к следующему:

image

То есть 1246й байт виртуальной страницы 2 ( «0100.1101.1110» го байта в «10» й странице). Теперь, операционная система должна искать эту страницу таблицы процессов, чтобы узнать на какой странице 2я карта. Согласно тому, что мы предложили, страница 2 — это 8К байт в физической памяти. Таким образом, виртуальный адрес 9438 приводит к физическому адресу 9442 (8k + 1246 смещение). Мы нашли наши данные! Как мы уже говорили, существует лишь одна таблица страниц для каждого процесса, поскольку каждый процесс будет иметь свои собственные переводы адреса, так же, как с сегментами. Но подождите, где эти таблицы страниц хранятся на самом деле? Угадайте . в физической памяти, да, а где же ей ещё быть? Если таблицы страниц сами хранятся в памяти, следовательно, для каждого доступа к памяти, память должна иметь доступ для извлечения VPN. Таким образом, со страницами один доступ к памяти равен фактически двум доступам к памяти: один для извлечения записи таблицы страниц, и один, чтобы получить доступ к «реальным» данным. Зная, что доступ к памяти медленный процесс, получаем — что это не лучшая идея.

Переводно-ассоциативный буфер: TLB

Использование поискового вызова в качестве механизма ядра для поддержки виртуальной памяти может привести к высокой производительности накладных расходов. Измельчение адресного пространства на небольшие фиксированного размера единицы (страницы) требует большого количества информации о карте. Потому что информация о карте, как правило, хранится в физической памяти. Логический поиск подкачки требует дополнительного поиска в памяти для каждого виртуального адреса, сгенерированного программой. И тут опять приходят аппаратные средства, чтобы ускорить и помочь ОС. В пагинации, как и в сегментации, аппаратные средства помогают ядру ​​ОС перевести адреса эффективным приемлемым способом. TLB является частью MMU, всего лишь простой кэш некоторых VPN переводов. TLB будет предотвращать доступ ОС к памяти от доступа к таблице страниц процесса, для получения физического адреса памяти из виртуальной. Аппаратное обеспечение MMU будет срабатывать на каждом виртуальном доступе к памяти, извлекая VPN из этого адреса, и выполняя поиск TLB, если он совпадает для этого конкретного VPN. Если он совпал, то он просто выполнил свою роль. Если он получает промах, то он будет искать таблицу страниц процесса, и если ссылка на память действительна, она обновит TLB, так что любой дальнейший доступ памяти этой страницы попадет в TLB.

Как и в любом кэше, вы чаще попадете, чем получите промах, так как ситуация промах вызывает страницу поиска, вследствие чего, доступ к памяти будет медленным. Вы догадаетесь об этом. Чем больше страниц, тем лучше TLB попадает, но тем больше неиспользуемого пространства вы будете иметь на каждой странице. Здесь должен быть найден компромисс. Современные ядра используют различные размеры страницы. Ядро ​​Linux может использовать то, что называется «огромные страницы», страницы, размером 2Mb что больше традиционных 4Kb. Кроме того, информацию лучше хранить в смежных адресах памяти. Если вы сократите свои данные в памяти, то скорее всего будете страдать от пропусков TLB или от переполнения TLB. Это то, что называется Эффективность пространственной локальности TLB: Данные, находящиеся непосредственно рядом с вами, могут быть сохранены в той же физической странице, таким образом, получая выгоду из текущего доступа к памяти из TLB.

Для переключения контекста TLB хранит также то, что называется ASID в каждой из своих записей. Это идентификатор адресного пространства, который является чем-то вроде PID. Каждый запланированный процесс имеет свой собственный ASID, следовательно TLB может управлять любым адресом процесса без риска недействительных адресов из других процессов. Также, если пользовательский процесс попытается загрузить неверный адрес, адрес, скорее всего, не будет сохранен в TLB, следовательно произойдет промах в поиске в записи таблицы страниц процесса. Теперь адрес сохраниться, но с неверным битом. Под X86 каждый перевод использует 4 байта, таким образом, имеется много бит. И нередко можно найти действительный бит вместе с другими составными, такими как грязный бит, биты защиты, опорного бита и т.д.… Если запись помечена как недопустимая, ОС по умолчанию использует SIGSEGV, что приводит к «ошибке сегментации», даже если здесь мы больше не говорим о сегментах. Вы должны знать, что подкачка более сложна в современных ОС, чем я объяснил. Современные ОС, как правило, используют многоуровневые таблицы страниц, многостраничные размеры, а также важную концепцию, которую мы не будем объяснять. Страница выселения, известная как «swaping» (Процесс, когда ядро обменивает страницы памяти на дисковое пространство, чтобы эффективно использовать главную память и создать иллюзию для пользовательских процессов, что основная память не ограничена в пространстве).

Вывод

Теперь вы знаете, что находится под сообщением «Ошибка сегментации». Операционная система использует сегменты для отображения виртуальной памяти в физической памяти. Когда процесс ядра хочет получить доступ к некоторой памяти, он выдает требование о том, чтобы MMU переводил на физический адрес памяти. Но если этот адрес не правильный, из пределов физического сегмента, или если права защиты сегмента не подходящие, то ОС по умолчанию посылает сигнал процессу, вызвавшего отказ: SIGSEGV, который имеет обработчик по умолчанию. Он убивает процесс и выводит сообщение: «Ошибка сегментации”. В других операционных системах (предполагаю) часто сообщение выглядит как „Общая неисправность защиты“. С Linux — нам повезло, мы имеем возможность доступа к исходному коду, здесь есть место исходного кода для X86 / 64 платформ, которое управляет ошибками доступа к памяти, так же здесь и SIGSEGV. Если вы заинтересованы в разработке сегментов для X86 / 64 платформ, вы можете посмотреть на их определение в ядре Linux. Вы также найдете интересный материал о пейджинговой памяти, которая поддерживает более длинный путь сегментирования памяти, чем использование классических сегментов. Мне понравилось писать эту статью, она перенесла меня в конец девяностых, когда я программировал свой первый CPU: Motorola 68HC11 с использованием C, VHDL и прямого монтажа. Я не программировал виртуальное управления памятью ОС, а использовал физические адреса напрямую (моему процессору не нужны такие сложные механизмы). Затем я подался в Web; но мои первые знания пришли от электроники, систем которые мы используем каждый день…

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *