Исследование модели памяти LinuxИсточник: IBM developerWorks Россия Викрам Шукла
Понимание моделей памяти, используемых в Linux, - это первый шаг к освоению на более высоком уровне структуры операционной системы Linux и ее реализации. В этой статье модели памяти Linux и управление памятью рассматриваются на ознакомительном уровне. Операционная система Linux использует монолитный подход, при котором определяется набор примитивов или системных вызовов для реализации служб операционной системы, таких как управление процессами, параллельная работа и управление памятью, в нескольких модулях, работающих в режиме супервизора. И хотя Linux в целях совместимости поддерживает модель модуля управления сегментами (segment control unit) как символическое представление, она использует эту модель на минимальном уровне. Основными задачами управления памятью являются:
Данная статья призвана помочь вам в освоении внутреннего устройства Linux с точки зрения управления памятью операционной системы. Рассматриваются следующие темы:
В данной статье не рассматриваются детально вопросы управления памятью в ядре Linux, но информация по общей модели памяти и по работе с ней должна дать вам основу для дальнейшего изучения. Внимание в статье уделяется архитектуре x86, но вы можете использовать эти материалы и с другими аппаратными реализациями. Архитектура памяти x86В x86-архитектуре память разделяется на три типа адресов:
CPU использует два модуля для преобразования логического адреса в его физический эквивалент. Первый называется модулем сегментации (segmented unit), а второй - модулем разделения на страницы (paging unit). Рисунок 1. Два модуля преобразуют адресное пространство Давайте исследуем модель модуля управления сегментами.
Общая модель модуля управления сегментамиСущность модели сегментации состоит в том, что память управляется при помощи набора сегментов. Естественно, каждый сегмент имеет отдельное адресное пространство. Сегмент состоит из двух компонентов:
Сегментированный адрес тоже состоит из двух компонентов - селектора сегмента и смещения в сегменте. Селектор сегмента указывает на используемый сегмент (то есть, значения базового адреса и длины), а компонент смещения указывает смещение от базового адреса для реального доступа к памяти. Физический адрес реального месторасположения памяти является суммой значений смещения и базового адреса. Если смещение превышает длину сегмента, система генерирует нарушение защиты. Подведем итоги:
Каждый сегмент - это 16-битное поле, называемое идентификатором сегмента или селектором сегмента. Процессоры семейства x86 содержат несколько программируемых регистров, называемых сегментными, которые хранят эти селекторы сегментов. Такими регистрами являются Рисунок 2. Взаимодействие дескрипторов сегментов и регистров сегментов Каждый раз, когда селектор сегмента загружается в сегментный регистр, соответствующий дескриптор сегмента загружается из памяти в соответствующий непрограммируемый регистр CPU. Каждый дескриптор сегмента имеет длину 8 байт и представляет один сегмент в памяти. Они хранятся в таблицах LDT или GDT. Запись элемента дескриптора сегмента содержит и указатель на первый байт в связанном сегменте, представленном полем Base, и 20-битное значение (поле Limit), которое представляет размер сегмента в памяти. Несколько других полей содержат специальные атрибуты, такие как уровень привилегий и тип сегмента ( Поскольку используются непрограммируемый регистр, к GDT или LDT не производится обращений до тех пор, пока не будет выполнена трансляция логического адреса в физический. Это ускоряет работу с памятью. Селектор сегмента содержит:
Поскольку дескриптор сегмента имеет длину 8 байт, его относительный адрес в GDT или LDT вычисляется путем умножения самых значимых 13 битов селектора сегмента на 8. Например, если GDT хранится по адресу 0x00020000, и поле Index, указанное селектором сегмента, равно 2, тогда адрес соответствующего дескриптора сегмента равно (2*8) + 0x00020000. Максимальное количество дескрипторов сегмента, которое может быть сохранено в GDT, равно (2^13 - 1) или 8191. На рисунке 3 показано графическое представление вычисления линейного адреса из логического. Рисунок 3. Получение линейного адреса из логического А теперь посмотрим, каковы отличия в Linux?
Модуль управления сегментами в LinuxВ Linux эта модель имеет небольшие отличия. Я уже говорил о том, что Linux использует модель сегментирования ограниченно (в основном для совместимости). В Linux все сегментные регистры указывают на один и тот же диапазон адресов сегментов - другими словами, каждый использует один и тот же набор линейных адресов. Это позволяет Linux использовать ограниченное число дескрипторов сегментов, то есть, все дескрипторы могут храниться в GDT. Двумя преимуществами этой модели являются:
На рисунке 4 показаны эти отличия. Рисунок 4. В Linux сегментные регистры указывают на один и тот же набор адресов Дескрипторы сегментовLinux использует следующие дескрипторы сегментов:
Рассмотрим детально каждый из них. Дескриптор сегмента кода ядра в GDT имеет следующие значения:
Линейный адрес для этого сегмента равен 4 GB. S =1 и type = 0xa обозначают сегмент кода. Селектор находится в регистре Дескриптор сегмента данных ядра имеет аналогичные значения за исключением поля Type, значение которого установлено в 2. Это указывает на то, что сегмент является сегментом данных и селектор хранится в регистре Сегмент кода пользователя используется совместно всеми процессами в пользовательском режиме. Соответствующий дескриптор сегмента, хранящийся в GDT, имеет следующие значения:
Доступ к соответствующему селектору сегмента в Linux осуществляется через макрос В дескрипторе сегмента данных пользователя есть только одно изменение по сравнению с предыдущим дескриптором - поле Type установлено в 2 и определяет сегмент данных, которые можно прочитать и записать. Доступ к соответствующему селектору сегмента в Linux осуществляется через макрос Кроме этих дескрипторов сегментов GDT содержит два дополнительных дескриптора сегментов для каждого созданного процесса - для сегментов TSS и LDT. Каждый дескриптор сегмента TSS указывает на отдельный процесс. TSS хранит информацию об аппаратном контексте для каждого CPU, который принимает участие в переключении контекста. Например, при переключении режимов Каждый процесс имеет свой собственный TSS-дескриптор, хранящийся в GDT. Значения этого дескриптора таковы:
Все процессы совместно используют сегмент LDT по умолчанию . По умолчанию он содержит нулевой дескриптор сегмента. Этот дескриптор сегмента LDT по умолчанию хранится в GDT. Сгенерированный ядром Linux LDT занимает 24 байта. По умолчанию всегда присутствуют три записи:
Вычисление TASKSЗнание Максимальное количество разрешенных в GDT записей может быть определено по следующей формуле:
Из 8192 дескрипторов сегментов Linux использует 6 дескрипторов сегментов, еще 4 дополнительных для APM-функций (функции расширенного управления питанием), а 4 записи в GDT остаются не использованными. Таким образом, конечное число возможных записей в GDT равно 8192 - 14, или 8180. В любой момент времени мы не можем иметь более чем 8180 записей в GDT, следовательно: 2 * Почему 2 * Это ограничение количества процессов в x86-архитектуре относилось к Linux 2.2, но после ядра 2.4 эта проблема была решена частично путем отказа от переключения аппаратного контекста (которое делало необходимым использование TSS), а также путем замены его переключением процесса. Теперь, давайте рассмотрим страничную модель.
Обзор модели страничной организацииМодуль управления страницами преобразует линейные адреса в физические (см. рисунок 1). Набор линейных адресов группируется вместе, образуя страницы. Эти линейные адреса непрерывны по своей природе - модуль управления страницами отображает эти наборы непрерывной памяти в соответствующий набор непрерывных физических адресов, называемых страничными фреймами. Обратите внимание на то, что модуль управления страницами представляет RAM разделенным на страничные фреймы фиксированного размера. По этой причине разбиение на страницы имеет следующие преимущества:
Структура данных, отображающая эти страницы на страничные фреймы, называется, таблицей страниц. Эти таблицы страниц хранятся в основной памяти и инициализируются ядром перед разрешением работы модуля управления страницами. На рисунке 5 показана таблица страниц. Рисунок 5. Таблица страниц определяет соответствие страниц страничным фреймам Обратите внимание на то, что набор адресов, содержащихся в Page1, соответствует определенному набору адресов, содержащихся в Page Frame1. Linux использует модуль разделения на страницы более интенсивно, чем модуль сегментации. Как мы видели ранее в разделе "Linux и сегментация", каждый дескриптор сегмента использует один и тот же набор адресов для линейной адресации, минимизируя необходимость использования модуля сегментации для преобразования логических адресов в линейные. Используя модуль разбиения на страницы более часто, чем модуль сегментации, Linux значительно облегчает управление памятью и обеспечивает переносимость между различными аппаратными платформами. Поля, используемые при разбиении страницПриведем описание полей, используемых для определения страниц в x86-архитектуре и помогающих управлять страницами в Linux. Модуль разбиения на страницы получает линейный адрес, который выдает модуль сегментации. Этот линейный адрес затем разбивается на следующие поля:
Преобразование линейных адресов в их соответствующее физическое месторасположение является процессом, состоящим из двух шагов. На первом шаге используется таблица преобразования, называемая Page Directory (осуществляет переход от Page Directory к Page Table), а на втором шаге используется таблица, называемая Page Table (Page Table плюс Offset для запрашиваемого фрейма страницы). Этот процесс изображен на рисунке 6. Рисунок 6. Поля адреса при разбиении на страницы В начале физический адрес Page Directory загружается в регистр Итак, физический адрес вычисляется следующим образом:
Поскольку Page Directory и Page Table занимают 10 бит, они могут адресовать максимум 1024*1024 KB, а Offset может адресовать до 2^12 (4096 байт). Таким образом, суммарное адресуемое пространство в Page Directory равно 1024*1024*4096 (2^32 ячеек памяти, что равно 4 GB). То есть, на x86-архитектурах адресуемое пространство ограничено 4 GB. Расширенное разбиение на страницыРасширенное разбиение на страницы получается при удалении таблицы преобразования Page Table; тогда разделение линейного адреса осуществляется между Page Directory (10 MSB) и Offset (22 LSB). 22 бит LSB формируют границу в 4 MB для страничного фрейма (2^22). Расширенное разбиение на страницы существует вместе с обычным разбиением и включается для отображения большого числа непрерывных линейных адресов в соответствующие физические адреса. Операционная система удаляет Page Table и обеспечивает, таким образом, расширенное разбиение на страницы. Это разрешается путем установки флага PSE (page size extension). 36-битный PSE расширяет поддержку 36-битного физического адреса со страницами размером 4 MB и поддерживает также 4-байтную запись страничных каталогов, предоставляя, таким образом, простой механизм адресации физической памяти выше 4 GB и не требуя больших изменений дизайна операционных систем. Такой подход имеет практические ограничения, касающиеся потребности разбиения на страницы.
Модель разбиения на страницы в LinuxРазбиение на страницы в Linux аналогично обычному разбиению, но x86-архитектура имеет трехуровневый табличный механизм, состоящий из:
Эта трехуровневая схема разбиения на страницы также реализована в Linux для поддержки областей памяти больших размеров. Если поддержка больших областей памяти не требуется, вы можете вернуться к двухуровневой схеме, установив pmd в "1". Эти уровни оптимизируются во время компиляции, разрешая второй и третий уровни (с использованием того же самого набора кода) простым разрешением или запретом промежуточного каталога. 32-битные процессоры используют pmd-разбиение, а 64-битые используют pgd-разбиение. В 64-битных процессорах:
Как видно из архитектуры, для адресации фактически используются 43 бита. То есть, на 64-битных процессорах реально доступно 2 в степени 43 ячеек виртуальной памяти. Каждый процесс имеет свой собственный набор каталогов страниц и таблиц страниц. Для обращения к страничному фрейму с реальными пользовательскими данными операционная система начинает с загрузки (на x86-архитектурах) pgd в регистр Каждая запись в таблице pgd указывает на страничный фрейм, содержащий массив записей pmd, которые, в свою очередь, указывают на страничный фрейм, содержащий pte, который, наконец, указывает на страничный фрейм, содержащий пользовательские данные. Если искомая страница находится в файле подкачки, в таблице pte сохраняется запись файла подкачки, которая используется (при ошибке отсутствия страницы) при поиске страничного фрейма для загрузки в память. На рисунке 8 показано, что мы добавляем смещение на каждом уровне таблиц страниц для отображения на соответствующую запись страничного фрейма. Эти смещения мы получаем, разбив линейный адрес, принятый из модуля сегментации. Для разбиения линейного адреса, соответствующего каждому компоненту таблицы страниц, в ядре используются различные макросы. Не рассматривая детально эти макросы, давайте посмотрим на схему разбиения линейного адреса. Рисунок 7. Линейные адреса имеют различные размеры Резервные страничные фреймыLinux резервирует несколько станичных фреймов для кода ядра и структур данных. Эти страницы никогда не сбрасываются в файл подкачки на диск. Линейные адреса с 0x0 до 0xc0000000 ( Это означает, что из 4 GB только 3 GB доступны для пользовательского приложения. Как разрешается разбиение на страницыМеханизм разбиения на страницы, используемый процессами Linux, имеет две фазы:
В фазе первоначальной загрузки за инициализацию разбиения на страницы отвечает вызов Определяются записи для двух таблиц, определенных в коде статически - Вторая фаза реализована в методе Отображение RAM выполняется между
Зона физической памятиЯ уже показал вам, что ядро Linux (на 32-битной архитектуре) делит виртуальную память в отношении 3:1 - 3 GB виртуальной памяти для пространства пользователя и 1 GB для пространства ядра. Код ядра и его структуры данных должны размещаться в этом 1 GB адресного пространства, но даже еще большим потребителем этого адресного пространства является виртуальное отображение физической памяти. Это происходит потому, что ядро не может манипулировать памятью, если она не отображена в его адресное пространство. Таким образом, максимальным количеством физической памяти, которое может обработать ядро, является объем памяти, который мог бы отобразиться в виртуальное адресное пространство ядра минус объем, необходимый для отображения самого кода ядра. В итоге Linux на основе x86-системы смогла бы работать с объемом менее 1 GB физической памяти. И это максимум. Поэтому, для обслуживания большого количества пользователей, для поддержки большего объема памяти, для улучшения производительности и для установки архитектурно-независимого способа описания памяти модель памяти Linux должна была совершенствоваться. Для достижения этих целей в более новой модели каждому CPU был назначен свой банк памяти. Каждый банк называется узлом ; каждый узел разделяется на зоны . Зоны (представляющие диапазоны памяти) далее разбивались на следующие типы:
Концепция узла реализована в ядре при помощи структуры Рисунок 8. Взаимоотношения между узлом, зоной и страничным фреймом Зона верхней области памяти появилась в системе управления памятью ядра тогда, когда были реализованы расширение виртуальной памяти Pentium II (для доступа к 64 GB памяти средствами PAE - Physical Address Extension - на 32-битных системах) и поддержка 4 GB физической памяти (опять же, на 32-битных системах). Эта концепция применима к платформам x86 и SPARC. Обычно эти 4 GB памяти делают доступными отображение (PAE - разработанное Intel расширение адресов памяти, разрешающее процессорам увеличить количество битов, которые могут быть использованы для адресации физической памяти, с 32 до 36 через поддержку в операционной системе приложений, использующих Address Windowing Extensions API.) Управление этой зоной физической памяти выполняется распределителем (allocator) зоны. Он отвечает за разделение памяти на несколько зон и рассматривает каждую зону как единицу для распределения. Любой конкретный запрос на распределение использует список зон, в которых распределение может быть предпринято, в порядке от наиболее предпочтительной к наименее предпочтительной. Например:
Список зон для такого распределения состоит из зон
ЗаключениеУправление памятью - это большой, комплексный и трудоемкий набор задач, один из тех, в которых разобраться непросто, потому что создание модели поведения системы в реальных условиях в многозадачной среде является трудной работой. Такие компоненты как планирование, разбиение на страницы и взаимодействие процессов предъявляют серьезные требования. Я надеюсь, что эта статья поможет вам разобраться в основных понятиях, необходимых для овладения задачами управления памятью в Linux, и обеспечит вас начальной информацией для дальнейших исследований. |