Symbian developer community

 
wiki

Symbian OS Internals/7. Memory Models/ru

From Symbian Developer Community

Jump to: navigation, search

Автор главы: Эндрю Фолке (Andrew Thoelke)

"Память - это то, что остается, когда что-нибудь происходит, но не совсем проходит"
Эдвард де Боно (Edward de Bono)


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

В данной главе мы исследуем высокоуровневые сервисы памяти, предоставляемые ядром EKA2, и то, как ядро взаимодействует с физической памятью для осуществления этих сервисов. Для изолирования ядра операционной системы от различных физических реализаций памяти в устройствах, все соответствующие взаимодействия с электронной реализацией памяти устройства переданы отдельному архитектурному модулю, который мы назвали моделью памяти (memory model). По ходу описания различных моделей памяти, поставляемых с EKA2, вы больше узнаете о том как они используют адресное пространство памяти (memory address space), или, как его еще называют, "карту памяти" (memory map), а так же поймете как они влияют на общее поведение системы.

Contents

Модель памяти

На уровне приложений, а так же во многих приложениях на стороне ядра, одним из главных способов использования памяти является выделение памяти из свободных областей при помощи операторов new или malloc. Однако существуют еще более фундаментальные сервисы памяти, формирующие базу для такого выделения памяти.

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

  1. Управление физическими ресурсами памяти: RAM, MMU и кэши (caches)
  2. Выделение виртуальной и физической памяти
  3. Управление адресным пространством памяти каждого из процессов
  4. Изоляция процессов, и защита памяти самого ядра
  5. Аспекты, касающиеся работы с памятью загрузчика приложений (software loader)

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

  • Число процессов в системе должно быть ограничено физическими ресурсами, а не моделью памяти. И по количеству должно точно превышать 64
  • Каждый процесс должен иметь большое адресное пространство памяти: от 1 до 2 GB
  • Объемы исполняемого кода, который может быть загружен процессом, должен ограничиваться только имеющимся объемом ROM- и RAM-памяти

Во время разработок мы обнаружили, что эффективность реализации перечисленных выше сервисов зависит от электронной реализации архитектуры памяти в самом устройстве. В частности, быстрое и лаконичное решение для одного устройства, может оказаться слишком медленным или жадным до памяти для другого. Так как одной из целей EKA2 была готовность к портированию на новые устройства, включающие новые блоки управления памятью (MMU) и архитектуры памяти, мы вынесли из ядра весь код, реализующий различные модели памяти, и снабдили этот код общим интерфейсом. В результате мы получили целый блок кода, который мы назвали моделью памяти. Как мы уже рассказывали в 1-й главе Знакомство с EKA2, сама модель памяти имеет многоуровневую структуру. Здесь на рис. 7.1 мы повторно представим ключевые части уже знакомой вам иллюстрации:

Рис. 7.1 Уровни модели памяти

На самом верхнем уровне мы разделяем реализацию EKA2 для настоящих устройств и эмулятора. В первом случае EKA2 является операционной системой, владеющей центральным процессором, памятью, периферией, и загрузчиками системы из исполняемых образов в ROM-памяти. Во втором случае уже эмулирующая операционная система предоставляет базовые сервисы EKA2, включая выделение памяти и загрузку программ. Поэтому самый верхний уровень и называется платформенным (platform layer).

Как уже было сказано в 1-й главе Знакомство с EKA2, существует несколько способов проектирования MMU и кэша. В Symbian OS мы хотели получить наилучшую производительность, а так же наиболее эффективный способ использования памяти, поэтому различия в архитектурах устройств привели нас к различным архитектурам в моделях памяти. На данный момент набор этих архитектур выглядит следующим образом:

Отстутствие MMU (блока управления памятью) Прямая модель памяти (direct memory model)
Виртуально индексированный кэш (virtually tagged cache) Замещающая модель памяти (moving memory model)
Физически индексированный кэш (physically tagged cache) Многоуровневая модель памяти (multiple memory model)
Эмулятор Эмуляторная модель памяти

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

Блоки управления памятью и кэши

MMU

Перед рассказом о том как EKA2 использует RAM-память устройства для предоставления сервисов памяти всей операционной системе и приложениям, стоит объяснить как электронные устройства предоставляет память программам.

EKA2 является 32-битной операционной системой, поэтому все адреса ячеек памяти могут быть представлены в виде 32-битного регистра. 32-битная презентация адреса ячейки памяти ограничивает объем одновременно доступной памяти до 4 GB. В действительности, на момент написания книги, мобильные телефоны располагали гораздо меньшими объемами физической памяти, обычно от 16 до 32 MB.

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

  1. Во встроенной операционной системе разработчик может определить максимальный объем памяти, необходимый каждой из компонент. Так же в последствии во время компиляции, разработчик может выделить точный объем памяти, необходимый каждой из компонент. На деле это все означает, что во время создания продукта разработчикам будут известны точные объемы необходимой RAM-памяти. Подобное статическое выделение памяти неприемлемо в случае с открытой платформой.
  2. Существуют определенные типы приложений, которые в теории могли бы использовать всю доступную память для максимальной выгоды пользователя, как например браузеры во время обработки сложных веб-сайтов. Снабжение каждого из таких приложений специальным объемом RAM-памяти может произвести на свет очень дорогое решение, в особенности учитывая тот факт, что большинство времени такая память и вовсе не будет использоваться.
  3. Встроенные приложения могут быть протестированы настолько, насколько это будет необходимо производителю устройства. Однако позднее установленные программы от сторонних разработчиков, могут подорвать стабильность и целостность устройства. Плохо написанная, или вовсе вредоносная программа может причинить тяжкий вред если ей будут позволены прямые манипуляции с памятью операционной системы.

Вышеприведенные проблемы делают важным использование специального устройства, доступного в более дорогих устройствах, а именно - блока управления памятью (memory management unit), или MMU. MMU отвечает за взаимодействие между центральным процессором и физической реализацией памяти устройства, которая обычно заключается в контроллере памяти, а так же одном или нескольких чипах памяти.

Далее мы поведем рассказ о различных ключевых особенностях MMU, а так же о том как их использует ядро EKA2.

Виртуальные адреса, и трансляция адреса

Одним из ключевых сервисов MMU является абстракция того, что программа считает адресом ячейки памяти, и того, каким настоящим физическим адресом обладает эта ячейка внутри RAM-памяти. При этом следует сказать, что первый упомянутый адрес считается виртуальным, а второй - физическим.

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

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

Самой распространенной структурой, способной хранить такую карту трансляции, является многоуровневая директория страниц (multi-level page directory). И устройства, на которых может работать операционная система Symbian, обычно поддерживают двухуровневые директории страниц. В данный момент некоторые из процессоров наивысшего класса используют при своей работе MMU с тремя и более уровнями директорий страниц. В частности, это касается 64-битных процессоров, способных использовать более чем 32-битные адреса виртуальной памяти. Для наглядности на рис. 7.2 представлен один и возможных вариантов многоуровневой директории страниц.

Рис 7.2 Многоуровневая директория страниц

При использовании двухуровневой директории, первый уровень обычно считается директорией страниц (page directory). В идеальном случае для отображения памяти (memory mapping) используется всего лишь одна директория страниц, однако на практике это не всегда так (и в последующих главах мы отдельно рассмотрим такие случаи). Сама же директория страниц - это всего лишь список ссылок на таблицы второго уровня.

Директория второго уровня содержит таблицы страниц (page tables). При отображении памяти такие таблицы обычно содержат от нескольких десятков до нескольких сотен элементов, каждый из которых является ссылкой на соответствующую физическую страницу в памяти устройства.

Физическая память устройства делится на страницы памяти (memory pages) или, как их еще называют, фреймы (frames). Устройства MMU обычно поддерживают несколько различных размеров физической страницы: в ядре EKA2 предпочитается использование физических страниц размером от 4 KB до 1 MB, однако при необходимости, можно воспользоваться и другими размерами страниц.

Возможно только наглядный пример является одним из наилучших способов понять как при помощи многоуровневой директории страниц виртуальный адрес транслируется в физический. Для демонстрации принципов работы трансляции адреса мы рассмотрим пример, в котором MMU процессора ARM будет ссылаться на физическую страницу памяти размером 4 KB. Такой процесс трансляции виртуального адреса так же еще называют проходом через таблицу страниц (page table walking).

Предположим, что у некой программы есть строка текста Hello world, размещенная по адресу 0x87654321, и программа запускает инструкцию чтения этих данных. Рис. 7.3 демонстрирует работу, проделываемую блоком MMU при нахождении страницы памяти, содержащей нужную нам строку текста.

Рис 7.3 Алгоритм трансляции виртуального адреса

На данный момент блоки MMU в процессорах ARM имеют директории страниц (page directory) с элементами в количестве 212, или 4096 штук. Каждый из этих элементов, в свою очередь, имеет 4 байта в длину, поэтому размер всей директории страниц составляет 16 KB.

Учитывая тот факт, что у нас 32-битная система, и физически мы можем использовать 232 адресов памяти, то каждый из 4096 элементов директории страниц будет отвечать за 232 / 212 = 220 байт, или 1 MB физического адресного пространства.

Размером физической страницы (или фрейма) мы выбрали 4 KB, поэтому в 1 MB их умещается 220 / 212 = 28, или 256 штук. Именно поэтому каждая из таблиц второй директории (page table) содержит ровно 28, или 256 элементов. А так как размер каждого из элементов таблицы страниц равен 4-м байтам, то размер отдельной таблицы страниц составляет 1 KB.

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

Во время следующего шага MMU извлекает адрес директории страниц, хранимый в базовом регистре таблицы трансляции (Translation Table Base Register), или TTBR, и находит ее. 12 самых верхних бита виртуального адреса, - в нашем случае 0x876, - используются как индекс для нахождения соответствующего элемента директории страниц. Найденный таким образом элемент содержит адрес соответствующей таблицы страниц.

Затем, используя 8 бит из середины виртуального адреса, - в нашем случае 0x54, - MMU находит индекс элемента в только что опеределенной таблице страниц (page table), и считывает его. Значение извлеченного элемента таблицы страниц представляет собой физический адрес страницы памяти (memory page), в области которой расположены наши данные.

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

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

Буферы ассоциативной трансляции

Алгоритм трансляции адреса, о котором мы только что рассказали, является весьма простым. Однако если учесть, что для каждой операции загрузки или записи данных ему требуются две дополнительные операции доступа к внешней памяти, легко понять почему такой алгоритм является медленным и неэффективным. Для решения этой проблемы чипы MMU имеют кэш для самых последних успешных трансляций адресов. Такой кэш называется буфером ассоциативной трансляции (Translation Look-aside Buffer), или TLB. Очень часто в этом кэше сохраняются 32 или 64 страницы памяти, доступ к которым осуществлялся во время последних операций. Хранение таких страниц позволяет MMU очень быстро транслировать адрес, относящийся к одной из этих страниц.

Как и все остальные кэши, ядро должно обновлять TLB для того чтобы сохранялась целостность как соответствующих таблиц страниц, так и страниц, хранящихся в TLB.

Буферы ассоциативной трансляции оказались настолько эффективны, что некоторые чипы MMU вовсе не реализуют в своей электронике алгоритм прохода таблицы. Напротив, вместо этого они запускают процедуру исключительной ситуации в центральном процессоре (CPU exception), и ждут пока соответствующий программный обработчик из операционной системы не совершит операцию трансляции адреса и не загрузит соответствующую страницу памяти в TLB. Лишь после успешной загрузки страницы памяти в TLB, MMU продолжит выполнение операции чтения или записи данных. И хотя ядро EKA2 в состоянии поддерживать такой тип MMU, все же для эффективного решения MMU должен уметь поддерживать программируемые операции работы с памятью. Например, если MMU зарезервирует некую область виртуальных адресов как прямо привязанную к физическим адресам, то в этом случае блоку MMU вовсе не потребуется использовать буфер ассоциативной трансляции. И благодаря этому алгоритм прохода таблицы сможет работать с директориями и таблицами страниц без запуска лишних исключений отсутствия TLB.

Области виртуального адреса

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

Зачем нам может понадобиться такая техника?

Формат исполняемого кода в Symbian OS как раз и является основой для этих причин. Точнее, то, как код ссылается на данные. Symbian использует размещаемый формат кода (relocated code format), в котором код использует настоящие (виртуальные) адреса объектов данных. Такой формат кода отличается от перемещаемого кода (relocatable code), в котором ссылки на данные даются относительно какого-нибудь внешнего значения, обычно - относительно значения зарезервированного для таких целей регистра. Забавно, что только размещаемый код требует данных о своем размещении внутри формата исполняемого файла для того, чтобы загрузчик операционной системы смог правильно настроить все прямые ссылки внутри этого кода.

Рассмотрим, к примеру, приложение TERCET.EXE, имеющее глобальную переменную lasterror, сохраняющую код последней ошибки в программе. Как только программа будет загружена, отредактирована по связям (linked), и размещена в памяти устройства, у нее появится несколько блоков памяти. В этих блоках памяти и будет находиться переменная lasterror, адрес которой будет определен уже операционной системой (см. рис. 7.4).

Рис. 7.4 Память, используемая для работы TERCET.EXE

И тут все выглядит вполне прилично: у нас есть блок памяти для кода программы, размещенный по виртуальному адресу 0xF0000000; другой блок памяти для данных программы, в котором находится наша переменная lasterror, и который размещен по виртуальному адресу 0x00500000. Для программы так же будут созданы и другие блоки памяти, как например стек, и динамическая память, или как ее еще называют, "куча" (heap). Однако эти блоки памяти не адресуются прямо кодом программы, в отличие от глобальной переменной lasterror.

Теперь представим, что операционная система должна запустить второй экземпляр программы TERCET.EXE, параллельно первыму. Одним из определений процесса операционной системы является его независимое адресное пространство, и как отдельный процесс, вторая копия программы TERCET.EXE должна иметь свой собственный поток, стек, "кучу" и свои собственные копии глобальных переменных.

Для достижения этой цели мы могли бы создать вторую копию программного кода, и разместить его сегменты памяти по другим адресам (см. рис. 7.5). При этом стоит заметить, что нам пришлось бы продублировать исполняемый код таким образом, чтобы второй экземпляр программы ссылался уже на другую область размещения переменной lasterror. Однако Symbian OS не делает этого по двум причинам. Во-первых, дублирование исполняемого кода требует бóльших объемов RAM-памяти, которой и так обычно недостаточно. Во-вторых, что более критично, встроенные программы обычно исполняются прямо "на месте" из флеш-памяти (executed in place, или XIP), и поэтому уже имеют настроенные адреса для кода и данных. Более того, для втроенных программ мы вовсе запретили переразмещение данных, чтобы сэкономить объемы необходимой флеш-памяти. Поэтому мы не можем просто так создавать копии кода, и размещать их в памяти устройства по новым адресам.

Рис. 7.5 Память, используемая для работы TERCET.EXE

По этим причинам в Symbian OS оба экземпляра программы TERCET.EXE будут использовать один и тот же блок памяти с исполняемым кодом. А это в свою очередь будет подразумевать, что адрес переменной lasterror тоже будет одинаковым для всех процессов. В нашем случае, 0x00500840 (см. рис. 7.6).

Рис. 7.6 Запуск двух экземпляров TERCET.EXE с общим блоком исполняемого кода

Однако нам по-прежнему нужны два экземпляра программы TERCET.EXE с раздельными блоками памяти для переменных таким образом, чтобы любой из процессов всегда смог найти свой экземпляр переменной, размещенной по виртуальному адресу 0x00500840. Поэтому нам нужен механизм, который позволил бы транслировать один и тот же виртуальный адрес в два различных физических адреса, соответствующих каждому из процессов, исполняемых операционной системой.

Решение такой задачи заключается в поддержке индивидуальных отображений виртуальных адресов в физические для каждого из исполняемых процессов. Такое отображение называется контекстом памяти процесса (process memory context). И как уже было описано в Symbian OS Internals/3. Threads, Processes and Libraries Потоки, процессы и библиотеки, при запуске нового потока, нам необходимо решить, будет ли новый поток запущен в том же процессе что и старый поток, или нет. Если новый поток будет запущен в новом процессе, контекст памяти (memory context) должен быть изменен таким образом, чтобы в новом процессе и потоке происходило правильное отображение виртуальных адресов в физические.

Как на самом деле в Symbian OS реализуется смена контекста памяти, зависит от используемого типа MMU, и мы изучим эти реализации несколько позже при рассмотрении различных моделей памяти.

Защита памяти

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

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

  • Защитить данные ядра от прямых или косвенных атак со стороны пользовательских программ
  • Защитить устройства, использующие отображаемый в памяти ввод-вывод данных (memory mapped I/O), от прямого использования пользовательскими программами
  • Разрешить процессу чтение и запись своей собственной памяти, и при этом запретить ему модификацию памяти любого другого процесса
  • Запретить изменение кода приложений после из загрузки в память при помощи пометки их кода как "только для чтения"
  • При соответствующей поддержке MMU, можно запретить запуск содержимого "кучи" и стека в качестве исполняемого кода, получая таким образом защиту от различных атак переполнения буфера
  • Создавать защищенную память, доступную только из одного или нескольких процессов

Рис. 7.7 демонстрирует все эти концепции, показывая какие блоки памяти должны быть открытыми для доступа потоку при его исполнении в пользовательском (user mode) или привилегированном режиме (kernel mode). В верхнем ряду на рисунке показана память, используемая ядром и двумя пользовательскими программами А и В при наличии у них некоторого общего кода и данных.

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

Рис. 7.7 демонстрирует идеальные варианты использования памяти, и как мы узнаем далее, разные модели памяти предоставляют программам менее разграниченный доступ к памяти своих соседей. Однако такие послабления были сделаны со всем возможными предосторожностями, чтобы по-прежнему оставить защиту памяти устройства на первом месте.

Рис 7.7 Память, доступная потокам в пользовательском и привилегированном режимах

Ошибки доступа к страницам памяти

MMU позволяет отображать нам всю оперативную память устройства (скажем, 16 MB) в значительно бóльшее 4 GB пространство виртуальных адресов. Ясно, что большинство виртуальных адресов не может быть прямо связано с физической памятью. Что же произойдет, если мы попытаемся поспользоваться одним из таких адресов?

Проходя при трансляции адреса через таблицы страниц, MMU может обнаружить элемент, обозначенный пустым, или не существующим в директории или таблицы страниц. В таком случае, в зависимости от того, идет ли речь о коде или о данных, MMU попросит центральный процессор сделать упреждающую выборку данных из памяти (CPU prefetch), либо запускает исключение недоступности данных (data abort exception).

Что-то очень похожее произойдет и в том случае, если MMU обнаружит что центральный процессор не имеет в данный момент достаточных прав на доступ к содержимому некоторой страницы памяти.

Такая ситуация в ядре EKA2 обычно приведет к остановке пользовательского потока с кодом ошибки KERN-EXEC 3 (необработанное исключение), либо к перезапуску всей операционной системы, если таким потоком оказался поток на стороне ядра. Эта ситуация была детально описана нами в 6-й главе Прерывания и исключения.

Все операционные системы для персональных компьютеров используют отображения адресов через MMU и выдают ошибки доступа к страницам памяти (page faults) для достижения определенной цели, а именно - реализации замещения страниц памяти по требованию (demand paging). Для операционной системы замещение страниц по требованию является весьма эффективным способом сэмулировать бóльшие объемы физической памяти, чем у нее есть на самом деле. Такая симуляция возможна благодаря сохранению устревших страниц памяти на жестком диске компьютера, и освобождению физической оперативной памяти для нужд текущей программы. При этом отображение страниц памяти должно быть изменено таким образом, чтобы операционная система знала, которая из страниц памяти уже была сохранена на жесткий диск, и стала недоступна из памяти компьютера. И когда какая-нибудь программа попытается получить доступ к странице, отсутствующей в оперативной памяти, специальный обработчик ошибок доступа загрузит ее с жесткого диска, и программа сможет продолжить свою работу.

Однако ядро EKA2 не поддерживает замещение страниц памяти по требованию программ.

Кэш

Кэш (cache) по своей важности является вторым элементом электронной подсистемы памяти. Кэш - это очень быстрая память, размещаемая внутри центрального процессора, и расчитанная на предоставление данных в течении 1-го или 2-х машинных циклов. В кэше содержатся данные из ячеек недавно использованной памяти, благодаря чему уменьшается количество запросов к внешней памяти, и улучшается общая производительность и эффективность работы процессора.

В некоторых деталях мы уже описывали кэш во 2-й главе Аппаратное обеспечение Symbian OS.

Интерфейс модели памяти

Модель памяти является отдельным архитектурным блоком ядра EKA2. Благодаря модели памяти, все остальное ядро может быть почти полностью независимым от используемой архитектуры памяти и поддерживаемой электроники. Для осуществления такой инкапсуляции, модель памяти определяет стандартный API, который должны адаптировать все реализуемые модели памяти.

Основной API заключен в двух классах: P и M, определенных в файле kern_priv.h. Класс P определяет API, предоставляемый платформенным уровнем в EKA2 (platform layer), в то время как класс M определяет API, предоставляемый уровнем модели (model layer):

 
class P
{
public:
static TInt InitSystemTime();
static void CreateVariant();
static void StartExtensions();
static void KernelInfo(TProcessCreateInfo& aInfo, TAny*& aStack, TAny*& aHeap);
static void NormalizeExecutableFileName(TDes& aFileName);
static void SetSuperPageSignature();
static TBool CheckSuperPageSignature();
static DProcess* NewProcess();
};
 
class M
{
public:
static void Init1();
static void Init2();
static TInt InitSvHeapChunk(DChunk* aChunk, TInt aSize);
static TInt InitSvStackChunk();
static TBool IsRomAddress(const TAny* aPtr);
static TInt PageSizeInBytes();
static void SetupCacheFlushPtr(TInt aCache, SCacheInfo& c);
static void FsRegisterThread();
static DCodeSeg* NewCodeSeg(TCodeSegCreateInfo& aInfo);
};
 

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

M::Init1() Во время этой инициализирующей фазы, обратный вызов (callback) переключения контекста процесса (process context switch) будет зарегистрирован на планировщика задач (scheduler). Обратный вызов будет использован для всех изменений адресного пространства, вызванных переключением контекста.
M::SetupCacheFlushPtr() Возвращает адрес памяти, которым должен воспользоваться менеджер кэша для очистки кэша от данных.

Наиболее интересными методами из всех представленных тут являются методы P::NewProcess() и M::NewCodeSeg(). Они не возвращают объекты классов DProcess и DCodeSeg, однако возвращают объекты, унаследованные от них. В 3-й главе "Потоки, процессы и библиотеки", мы бегло ознакомились с классом DProcess. Однако следует учесть, что этот класс имеет целый ряд виртуальных функций, а среди них - и настоящие фабричные функции DProcess::NewChunk() и DProcess::NewThread(), созданные для получения классов, унаследованных от DChunk и DThread.

И львиную долю API для взаимодействия между общими уровнями ядра и модели памяти, предоставляют всего четыре класса: DProcess, DThread, DChunk и DCodeSeg.

DChunk

Термин блок (chunk) в Symbian OS имеет фундаментальное значение, и характеризует минимальный объем памяти, выделяемый операционной системой за пределами модели памяти.

Блок представляет собою последовательный ряд адресуемой, или зарезервированной памяти (addressable or reserved memory), часть которой будет составлять доступная, или выделенная память (accessible or committed memory). В системах без MMU все используемые адреса являются физическими, поэтому выделенный блок памяти будет целиком доступен для использования.

В системах с MMU, Symbian OS предоставляет блоки памяти трех базовых типов в зависимости от того, какое подмножество адресов занимает выделенная память:

  1. Обычный блок (normal chunk). Блоки данного типа имеют монолитную область используемой памяти, начало которой размещено в базовом адресе блока, а окончание - по некоторому конечному адресу, значение которого кратно размеру страницы памяти MMU.
  2. Двухконечный блок (double-ended chunk). Блоки данного типа имеют монолитную область используемой памяти, начало и конец которой расположены недалеко от границ блока зарезервированной памяти. При этом значения начального и конечного адресов кратны размеру страницы памяти MMU.
  3. Прерывистый блок (disconnected chunk). Блоки данного типа представляют собой ряд страниц памяти MMU внутри области зарезервированной памяти. Страницы в прерывистом блоке размещены таким образом, чтобы использование незанятой памяти могло происходить произвольным образом.

Хотя становится совершенно ясно, что обычный блок памяти (normal chunk) - это всего лишь специальный случай двухконечного блока (double-ended chunk), а обычный и двухконечный блоки - всего лишь специальные случаи прерывистого блока (disconnected chunk), мы все же решили разделить их по указанным типам. Все дело в том, что два первых типа блоков встречаются очень часто, и мы можем реализовать их более эффективным способом, нежели чем просто реализовать их на основе прерывистого блока памяти. Рис. 7.8 демонстрирует типы блоков памяти, а так же общую терминологию, используемую для описания их характеристик.

Как и с другими типами ресурсов ядра, для отдельного процесса вы можете создавать локальные (local), или приватные (private) блоки памяти. Так же можно создавать и глобальные блоки. Локальные блоки нельзя отображать в памяти другого процесса, поэтому операционная система употребляет такие блоки для любой памяти, не предназначенной для совместного использования. В противоположность этому, глобальные блоки можно отображать в памяти одного или нескольких процессов. Процесс может открывать и отображать в своей памяти глобальные именованные блоки, в то время как для доступа к неименованному глобальному блоку, процессу потребуется хэндлер на этот блок, переданный каким-либо другим процессом.

Рис 7.8 Базовые типы блоков памяти

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

Значение Описание
EKernelData Существует один-единственный блок памяти этого типа, используемый для хранения глобальных данных всех программ на стороне ядра, работающих без загрузки в оперативную память (XIP), а так же для работы стека пустого потока (null thread), и динамической "кучи" ядра. Виртуальный адрес этого блока зависит от используемой модели памяти, однако он извлекается из заголовка ROM-памяти, и устанавливается во время создания ROM-памяти устройства. Блок памяти данного типа так же требуется для расчета рабочих адресов данных, используемых кодом, работающим прямо из ROM-памяти.
EKernelStack Один блок данного типа используется для размещение стеков всех потоков, работающих на стороне ядра. Отличие от EKernelData заключается лишь в том, что диапазон адресов для этого блока определяется динамически во время загрузки операционной системы.
EKernelCode В системе существует всего один блок данного типа. Он используется для всего кода на стороне ядра, который исполняется из оперативной памяти системы, как например драйвера устройств, загружаемые с диска. В отличии от предыдущего блока, он подразумевает использование разрешений на исполнение кода (execute permissions), а так же управление кэшем инструкций (I-cache).
EUserCode Ядро использует блоки данного типа для выделения памяти или отображения страниц памяти для любого пользовательского кода, исполняемого из оперативной памяти. В свою очередь, модель памяти определяет как эти блоки будут использованы, и как внутри них будет размещаться код.
ERamDrive Если в системе существует RAM-привод (RAM drive), то он будет находится в блоке данного типа. Виртуальный адрес RAM-привода определяется моделью памяти, что позволяет восстанавливать содержимое привода после перезагрузки приложений.
EUserData Блоки памяти универсального назначения для пользовательских процессов. Ядро использует блоки данного типа для переменных, стеков и "куч" пользовательских программ. Блоки данного типа могут быть либо приватными, либо общедоступными для одного или нескольких процессов.
EDllData Блоки данного типа формируют память для изменяемых статических переменных (writable static variables) пользовательских DLL. Виртуальный адрес данного блока должен фиксироваться моделью памяти по той причине, что на основе этого виртуального адреса будут расчитываться рабочие адреса данных для кода, исполняемого из ROM-памяти. DLL-ы, работающие из оперативной памяти, будут сами определять адреса своих данных во время своей загрузки в память. Каждый из пользовательских процессов, связанный или загружающий DLL с изменяемыми статическими переменными, будет иметь хотя бы один блок данного типа.
EUserSelfModCode Данный тип является специальным типом пользовательского блока памяти, позволяющим хранить исполняемый код. К примеру, JIT-компилятор в Java может использовать такой блок для хранения заранее скомпилированных инструкций. Этот тип блоков отличается от EUserData использованием прав на доступ (access permissions), а так же способом управления кэшем.
ESharedKernelSingle / ESharedKernelMultiple / ESharedIo Ядро предоставляет блоки данного типа для общедоступной памяти, используемой совместно драйверами устройств и пользовательскими программами. О отличии от блоков других пользовательских типов, отображение этих блоков в памяти устройства происходит только при помощи программ на стороне ядра, что в свою очередь делает их пригодыми для прямого доступа различными электронными устройствами.
ESharedKernelMirror Некоторые модели памяти при помощи независимого отображения перемещают общедоступные блоки в контекст памяти ядра. В этом случае блок данного типа как раз будет хранить все детали дополнительного отображения в памяти.

Ниже приведено определение класса DChunk:

 
class DChunk : public DObject
{
public:
 
enum TChunkAttributes
{
ENormal = 0x00,
EDoubleEnded = 0x01,
EDisconnected = 0x02,
EConstructed = 0x04,
EMemoryNotOwned = 0x08
};
 
enum TCommitType
{
ECommitDiscontiguous = 0,
ECommitContiguous = 1,
ECommitPhysicalMask = 2,
ECommitDiscontiguousPhysical = ECommitDiscontiguous|ECommitPhysicalMask,
ECommitContiguousPhysical = ECommitContiguous|ECommitPhysicalMask,
};
 
DChunk();
~DChunk();
TInt Create(SChunkCreateInfo& aInfo);
inline TInt Size() const {return iSize;}
inline TInt MaxSize() const {return iMaxSize;}
inline TUint8 *Base() const {return iBase;}
inline TInt Bottom() const {return iStartPos;}
inline TInt Top() const {return iStartPos+iSize;}
inline DProcess* OwningProcess() const {return iOwningProcess;}
 
public:
virtual TInt AddToProcess(DProcess* aProcess);
virtual TInt DoCreate(SChunkCreateInfo& aInfo) = 0;
virtual TInt Adjust(TInt aNewSize) = 0;
virtual TInt AdjustDoubleEnded(TInt aBottom, TInt aTop) = 0;
virtual TInt CheckAccess() = 0;
virtual TInt Commit(TInt aOffset, TInt aSize,
TCommitType aCommitType = DChunk::ECommitDiscontiguous, TUint32* aExtraArg = 0) = 0;
virtual TInt Allocate(TInt aSize, TInt aGuard = 0, TInt aAlign = 0) = 0;
virtual TInt Decommit(TInt aOffset, TInt aSize) = 0;
virtual TInt Address(TInt aOffset, TInt aSize, TLinAddr& aKernelAddress) = 0;
virtual TInt PhysicalAddress(TInt aOffset, TInt aSize, TLinAddr& aKernelAddress,
TUint32& aPhysicalAddress, TUint32* aPhysicalPageList = NULL) = 0;
 
public:
DProcess* iOwningProcess;
TInt iSize;
TInt iMaxSize;
TUint8* iBase;
TInt iAttributes;
TInt iStartPos;
TUint iControllingOwner;
TUint iRestrictions;
TUint iMapAttr;
TDfc* iDestroyedDfc;
TChunkType iChunkType;
};
 

Следующая таблица описывает назначение некоторых ключевых членов класса DChunk:

Член Описание
iOwningProcess Если блок памяти когда-либо отображался в памяти какого-нибудь процесса, данный элемент будет представлять собою процесс, создавший и владеющий данным блоком. Иначе значение этого элемента устанавливается в NULL.
iSize Размер использованной памяти внутри блока. Обратите внимание, что в случае прерывистого блока (disconnected chunk), размер использованной памяти не включает просветы, образованные неиспользованными областями.
iMaxSize Зарезервированный размер адресного пространства блока. Обычно значение этого элемента представляет собою настоящий размер блока памяти, и может быть несколько больше запрошенного значения, так как зависит от размеров страниц памяти, поддерживаемых MMU.
iBase Виртуальный адрес первого зарезервированного байта внутри блока памяти. Данный адрес может изменяться в зависимости от того, какой пользовательский процесс исполняется в данный момент, а так же какой контекст памяти был использован. Поэтому прямое использование значения этого элемента может и не привести к желаемым результатам!
iAttributes Ряд флагов, повествующих об определенных характеристиках блока. Некоторые из них весьма просты: например, является ли блок двухконечным (double ended), прерывистым (disconnected), или просто не обладающим своей памятью (memory not owned). Другие из атрибутов уже характерны для конкретной используемой модели памяти: например, если блок имеет фиксированный доступ (fixed access), фиксированный адрес (fixed address), и код - в скользящей модели (moving model); и выделение адреса и флаги способа отображения памяти в многоуровневой модели (multiple model).
iStartPos Смещение к первому использованному байту в двухконечном блоке (double-ended chunk). Не используется в других типах блоков.
iControllingOwner Идентификатор процесса, устанавливающего ограничение на блок.
iRestrictions Ряд флагов, определяющих какие операции могут быть выполнены над блоком. Например, данный элемент используется для запрещения корректировки блока пользовательскими программами. Блоки общего доступа описаны в главе 7.5.3.2.
iMapAttr Флаги, контролирующие отображение блока в памяти. Используется только для блоков общего доступа.
iDestroyedDfc Позволяет блоку вызвать отложенную функцию (или DFC) после своего полного уничтожения. Уничтожение блока - процесс асинхронный, и зависит от всех ссылок, выданных на этот блок. Через вызов отложенной функции блок позволяет сообщить устройству памяти, что память, отображаемая блоком, больше им не используется, и что устройство может вновь забрать ее себе.
iChunkType Тип использования данного блока памяти. Хранит значения перечисления TChunkType, о которых мы уже говорили.

API блока памяти полностью определяется на основании смещений (offsets) от его базового адреса (base address). Смещения используются из-за того факта, что базовым адресом блока является виртуальный адрес, который может меняться в зависимости от используемого контекста памяти. В частности, разные процессы могут иметь разные базовые адреса для одного и того же блока, а ядро может найти блок памяти и по другому виртуальному адресу, нежели чем пользовательский код. Точные обстоятельства смены виртуального адреса зависят от конкретной модели памяти.

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

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

Adjust() Устанавливает конец области используемой памяти внутри обычного блока. В зависимости от указанного размера, данная функция самостоятельно занимает или освобождает необходимое количество страниц памяти, чтобы блок памяти стал равен указанному размеру.
AdjustDoubleEnded() Сдвигает один или оба конца области используемой памяти внутри двухконечного блока.
Commit() Выделяет страницы памяти внутри указанной области. Если какая-либо из страниц уже является выделенной, функция сообщает об ошибке. Поэтому для использования этой функции всегда рекомендуется указание смещений, выравненых по страницам памяти.
Decommit() Освобождает страницы памяти внутри указанной области. Невыделенные страницы памяти, найденные в указанной области, будут проигнорированны функцией без каких-либо сообщений об ошибке.
Allocate() Резервирует и выделяет область памяти указанного размера. На выбор может зарезервировать пробную область памяти (еще не выделенную), и попросить систему о выравнивании по размеру, бóльшему чем одна страница памяти.

DCodeSeg

Сегмент кода (code segment) отвечает за содержимое загружаемого образа исполняемого файла (таким файлом обычно является EXE или DLL). Содержимое исполняемого образа формируется перемещаемым кодом, а так же данными, доступными как для чтения, так и для записи (если такие данные присутствуют в исполняемом файле). Начальные значения данных, преднозначенных для записи, будут храниться в оперативной памяти, чтобы таким образом помочь избежать их лишних перезагрузок из исполняемых образов во время создания новых экземпляров исполняемого файла. Так как Symbian OS не рекомендует использовать в программах изменяемые статические данные (writable static data), общий объем таких данных не должен приводить к значительным тратам памяти.

В зависимости от используемой модели памяти, сегмент кода может предоставлять память исполняемому коду несколькими способами. В некоторых случаях один-единственный прерывистый блок (disconnected chunk) может обеспечить памятью все используемые сегменты кода, а иногда сегменту кода необходимо напрямую управлять своими страницами памяти.

В целях оптимизации, код, являющийся частью исполняемой на месте ROM-памяти (XIP ROM), обычно не имеет своего сегмента кода на стороне ядра. По крайней мере до тех пор, пока на сегмент кода не будет явно ссылаться объекты классов DProcessor или DLibrary.

Ниже приводятся дополнительные обязанности сегмента кода:

  • Хранение важной информации для сегмента кода. Например, местоположение кода и данных, их размеры и рабочий адрес (run address); таблица обработчиков исключений (exception handlers); точка входа исполняемого кода; директория экспорта, и т.д.
  • Поддержка записей, касающихся зависимостей с другими сегментами кода. Такие зависимости берут свое начало в импортировании функций из других DLL. Следует отметить, что такие зависимости могут быть взаимными, так как DLL-ы могут совершенно законно импортировать функции друг у друга. Зависимости подобного рода могут использоваться для определения тех сегментов кода, которые необходимо подключить к исполняемому процессу, или отключить от него в результате загрузки или выгрузки какой-нибудь DLL. Так же подобные зависимости необходимы для обнаружения неиспользуемых сегментов кода, и их последующего уничтожения.
  • Отображение сегмента кода в адресном контексте процесса при загрузке и выгрузке DLL-ов. То, как это происходит на самом деле, и будет ли происходить вообще, зависит от модели памяти, выбирающей способы размещения и отображения сегментов кода.

10 глава Загрузчик, детально рассказывает о том как используются сегменты кода для управления исполняемым кодом.

Нелишним будет сообщить, что ядро EKA1 не имеет объекта DCodeSeg. В ядре EKA1 полномочия объекта DCodeSeg были частично реализованы в классе DLibrary, и частично - в классе DChunk. В то время такое решение вполне подходило для имеющихся процессорных архитектур и доступных объемов ROM-памяти. Полное переустройство данной области ядра EKA2 было обусловлено желанием воспользоваться совершенно другой моделью памяти для процессорной архитектуры ARMv6, а так же необходимостью куда более гибкого управления сотнями исполняемых программ, загружаемых из динамической флэш-памяти (non-XIP flash). Ядро EKA2 по-прежнему использует объект DLibrary, однако этот объект на стороне ядра целиком реализует лишь пользовательский интерфейс RLibrary, помогающий осуществить доступ к динамическому коду.

DProcess

Для операционной системы процесс является контейнером одного или нескольких потоков (см. 3-ю главу Потоки, процессы и библиотеки), а так же реализацией образа исполняемого файла (см. 10-ю главу Загрузчик). Однако мы уже могли убедиться, что процесс так же является владельцем отдельного, защищенного контекста памяти. В действительности это означает, что процесс не только управляет принадлежащей ему памятью (либо памятью, которой он делится с другими процессами), но и управляет отображением этой памяти в свое собственное виртуальное пространство.

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

  • Переключение процессного контекста: во время переключения предыдущий контекст и защита памяти должны быть удалены из MMU, а на их место установлены новые. Модификация отображения виртуальных адресов в физические обычно требует замены одного или нескольких регистров MMU, а так же проверок корректности элементов буфера ассоциативной трансляции (TLB) и содержимого кэша.
  • Прекращение работы процесса. Модель памяти должна освобождать все занимаемые или доступные процессу ресурсы памяти, и возвращать их системе. Сбой при возвращении этих ресурсов вероятнее всего приведет к медленным утечкам системной памяти, и как следствие - к последующей перезагрузке всей системы.
  • Межпроцессное взаимодействие (IPC), то есть передача данных от процесса к процессу. Когда потоку необходимо прочитать или записать данные в память, принадлежащую другому процессу, - а это так же случаи когда потоку на стороне ядра нужно прочитать или записать данные в память на стороне пользователя, - модель памяти должна найти и отобразить эту память для передачи данных.

DThread

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

AllocateSupervisorStack() / FreeSupervisorStack() / AllocateUserStack() / FreeUserStack() Управление пользовательскими (user-mode) и ядерными (kernel-mode или supervisor) стеками потоков зависит от конкретной модели памяти. Модели памяти, использующие MMU, обычно размещают все стеки потоков каждого из процессов в отдельном прерывистом блоке памяти (disconnected chunk) на пользовательской стороне. Каждый из стеков потоков ограничен специальной незаполняемой страницей памяти, служащей для отслеживания ситуаций переполнения стека. Таким же образом и ядро размещает все ядерные стеки потоков в одном блоке памяти на стороне ядра с защитными страницами памяти между ними.
ReadDesHeader() / RawRead() / RawWrite() Данные методы помогают ядру при чтении и записи данных в пользовательскую память какого-либо из потоков. Методы могут произвести все необходимые проверки чтобы убедиться в том, что указанная удаленная память действительно является частью пользовательского адресного пространства заданного потока. Так же эти методы могут выполнить проверку локального буфера памяти на нахождение его внутри пользовательского контекста памяти исполняемого потока. Для драйверов устройств вся подобная функциональность доступна через ряд методов Kern::ThreadDesRead() и Kern::ThreadRawRead(), которые дополнительно обрабатывают исключения, вызванные использованием неотображенных адресов памяти. Так же благодаря указанным методам класс RMessagePtr2 осуществляет передачу буферов с данными между клиентами и серверами на стороне пользователя.
ExcIpcHandler() Данный метод является обработчиком исключений (exception handler), осуществляющий свою деятельность на основе ранее упомянутого межпроцессного копирования данных. Данный обработчик исключений используется в связке с ловушкой исключений (exception trap) (см. упоминание об XTRAP в 6-й главе Прерывания и исключения). При использовании этого метода исключение, вызванное использованием неправильного "удаленного" адреса, будет инерперетировано просто как ошибка, в то время как использование неправильного "локального" адреса будет интерпретировано как программная ошибка, и приведет к возникновению паники программы.
RequestComplete() Благодаря этому методу ядро реализует такие шаблоны программирования в Symbian OS как TRequestStatus, User::WaitForRequest() и активные объекты. Через данный метод в системе осуществляются все синхронные запросы. Сам метод, для осуществления своей работы, записывает в память запрашивающего потока 32-битное статусное слово. Так как данный метод является базовым для всех межпотоковых коммуникаций, его производительность приобретает первостепенное значение. Поэтому модель памяти обычно реализует эту функцию как специальный случай записи данных в пространтство памяти другого потока.

Модели памяти

До этого момента мы обращали свое внимание на ряд проблем, касающихся открытости Symbian OS по отношению к приложениям сторонних разработчиков, а так же устойчивости операционной системы против плохо написанных или вредоносных программ. Однако до этого момента мы избегали описания точных деталей решения этих проблем в ядре EKA2. На самом же деле наилучшее решение этих проблем зависит от устройства электронных схем, отвечающих за управление памятью. В данный момент существуют два очень разных решения реализации подсистемы памяти первого уровня для процессоров ARM. Это, в свою очередь, и привело к созданию двух совершенно разных моделей памяти для Symbian OS, работающей на процессорах ARM.

Первый вариант модели памяти был разработан для ядра EKA1, использующей третью версию ахитектуры ARM (ARMv3) в одни из первых дней жизни Symbian OS. По мере того как мы будем описывать причины и реализацию этой модели памяти, вы начнете понимать почему она была названа "замещающей" моделью памяти (moving memory model). Разработка и оптимизация первой версии происходила постепенно, вплоть до варианта для пятой версии архитектуры ARM (ARMv5), и использовалась в обоих ядрах EKA1 и EKA2.

В шестой версии архитектуры ARM (ARMv6) компания ARM совершила радикальные изменения устройства MMU и кэша. Для Symbian OS стала очевидной замена "замещающей" модели памяти (moving model) на новую, "многоуровневую" модель памяти (multiple memory model), которая могла бы извлечь всю выгоду из большинства усовершенствований процессоров ARMv6, и таким образом предоставить Symbian OS улучшенную производительность, надежность и устойчивость.

При знакомстве с обеими моделями памяти мы будем так же рассматривать и электронную архитектуру, благодаря которой каждая из моделей памяти осуществляет свои сервисы. Так же будет демонстрироваться и карта памяти (memory map) для описания процесса выделения виртуального адресного пространства операционной системе.

Для полноты повествования так же будут описаны две другие модели памяти, используемые в EKA2: "прямая" модель памяти (direct memory model), позволяющая ядру EKA2 работать без MMU; и "эмуляторная" модель памяти (emulator memory model), используемая в эмуляторах, приближающихся по своим характеристикам к настоящим устройствам.

Замещающая модель памяти

Данная модель памяти была разработана специально для процессоров ARM, реализующих архитектуры вплоть до ARMv5.

Аппаратное обеспечение

Книга "ARM Architecture Reference Manual" (автора Дэйва Сеала (Dave Seal), от издательства Addison-Wesley Professional) детально повествует о подсистеме памяти архитектуры ARMv5. В данной главе мы опишем лишь те особенности, которые имеют большое влияние на устройство и функциональность реализуемой модели памяти.

Отображение виртуального адреса

В архитектуре ARMv5 директория страниц (page directory) верхнего уровня содержит 4096 элементов, каждый из которых имеет 4 байта в длину. Таким образом, вся директория страниц занимает 16 KB. Многие из операционных систем, предоставляющие индивидуальные адресные пространства для каждого из процессов, используют простую технику для выделения отдельной директории страниц для каждого из них: при смене контекста между процессами, система всего лишь заменяет содержимое базового регистра MMU (или как его еще называют, регистра TTBR). Однако в устройствах с ограниченными объемами RAM-памяти, выделение 16 KB на каждый из процессов является весьма непомерным, поэтому было необходимо разработать альтернативную схему управления множественными адресными пространствами.

Защита

ARMv5 использует две системы для защиты памяти от несанкционированного доступа.

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

Вторая система защиты основана на областях определения, или доменах (domains). Архитектура ARMv5 поддерживает до 16 доменов. В такой системе каждый элемент директории страниц (page directory) имеет специальное поле, определяющее к какому из доменов принадлежит этот элемент (напомним, представляющий собою некоторый диапазон адресов). Таким образом, каждая из отображаемых страниц располагается точно в одном из доменов. Блок MMU имеет специальный регистр, контролирующий текущий доступ к каждому домену на основе трех возможных устрановок: доступ к домену запрещен, и при попытке обращения приводит к сбою; доступ к домену всегда разрешен, и права доступа к таблице страниц игнорируются; либо доступ к домену осуществляется на основе прав доступа к таблице страниц. Применение доменов позволяет легко осуществлять большие модификации карты памяти. При этом эффективность обработки прав на доступ к памяти достигается небольшими изменениями элементов директории страниц и регистра контроля доступа к доменам (domain access control register, или DACR).

Кэши

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

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

Рис. 7.9 иллюстрирует проблему омонимов в ARMv5. MMU использует виртуальные адреса для идентификации каждого из элементов данных в кэше. Поэтому поддержку нескольких перекрывающихся адресных пространств можно осуществить только благодаря удалению из кэша всех виртуальных адресов и соответствующих им данных во время переключения процессных контекстов. При этом удаляемые данные нужно будет скопировать обратно в основную память устройства. Иначе, если в кэше не заменить виртуальные адреса и данные, исполняемый процесс будет видеть (и использовать) память, отведенную для другого процесса.

Рис. 7.9 Омонимы в ARMv5

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

Таким образом, переключение контекста, меняющее виртуальную память, влияет как на производительность системы, так и на потребление энергии устройства.

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

Рис. 7.10 Синонимы в ARMv5

Как и почти во всех новых высокопроизводительных процессорах, кэши для данных и инструкций разделены друг от друга (иногда такое разделение называется "гарвардским кэшем", или Harvard cache). (В 17-й главе Режим реального времени, мы обсудим влияние различных типов кэша на производительность системы.) При использовании "гарвардского кэша", помимо известных выгод от его использования, замещающей модели памяти (moving memory model) не нужно будет каждый раз обновлять кэш инструкций при смене контекста.

Идея

Замещающая модель памяти (moving memory model) использует одну-единственную директорию страниц (page directory) для отображения памяти всей операционной системы. Во время смены контекста модель памяти осуществляет поддержку множественных перекрывающихся процессных адресных пространств при помощи простой замены соответствующих блоков памяти (то есть замены их виртуальных адресов), от чего и получила свое название - "замещающая".

При помощи простых арифметических действий легко убедиться, что каждый элемент директории страниц отвечает за отображение 1 MB адресного пространства. Изменение домена, указанного в элементе директории страниц, предоставляет системе весьма простой контроль над доступом к соответствующему адресному пространству. Модель памяти может легко изменять как доступное в данный момент адресное пространство, так и правила доступа к нему благодаря простой замене соответствующего элемента директории страниц, и восстановления значения этого элемента при обратной смене контекста. Таким образом, вся операция укладывается в запись двух 32-битных значений.

Предположим к примеру, что у нас имеется таблица страниц (page table), отображающая страницы, совершенно недоступные для пользовательских программ, и доступные для чтения и записи для программ в привелигированном режиме. Предположим, что во втором элементе директории страниц мы создаем запись, приписываемую домену 0, и указываем регистру DACR (регистру контроля доступа к доменам) игнорировать права доступа к этому домену. Так как права на доступ к страницам игнорируются, пользовательские и привилегированые программы теперь могут без ограничений использовать страницы памяти в адресном пространстве от 0x00100000 до 0x001FFFFF (то есть внутри второго мегабайта всей памяти). Предположим, что во время смены контекста мы удаляем этот элемент директории страниц, и создаем новый в седьмой позиции. При этом домен устанавливается в значение 1, а регистр DACR настраивается на режим проверки прав доступа к страницам домена. После очистки элементов TLB (буфера ассоциативной трансляции), соответствовавших предыдущему диапазону адресов, мы больше не можем использовать адрес 0x00100000 для доступа к памяти. Однако мы можем воспользоваться адресом 0x00600000, но только в привелигированном режиме, так как для домена 1 будут проверяться права на доступ к страницам памяти. Рис. 7.11 демонстрирует результат действия этих простых изменений на директорию страниц.

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

Такой способ работы явился одной из основных предпосылок для реализации концепции отдельного блока памяти (chunk), описанного в параграфе 7.3.1. И в данной модели памяти, на самых высших уровнях, блок является отдельной единицей замещаемой памяти (moving memory).

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

Рис. 7.11 Смена отображения памяти при помощи модификации директории страниц

Для избежания очистки всего кэша, замещающая модель памяти использует и некоторые другие особенности ARMv5, в частности, домены и разделенные кэши данных и инструкций. Однако при этом системе по-прежнему нельзя целиком избежать процесса очистки кэша, который составляет значительную часть времени выполнения программ в Symbian OS.

Следует отметить, что ARMv5 предоставляет разработчикам альтернативу множественным директориям страниц (multiple page directories), или замещаемым таблицам страниц (moving page tables) в виде "быстрых расширений смены контекта", или Fast Context Switch Extensions. В режиме FCSE блок MMU транслирует виртуальный адрес еще до того, как будет производить обычную трансляцию адреса с использованием таблиц страниц, тем самым избегая дорогостоящей очистки кэша при смене контекста. При этом MMU заменяет 7 верхних битов виртуального адреса на значение регистра FCSE PID, если все 7 верхих бита виртуального адреса являются нулями. На деле же это означает, что виртуальные адреса в диапазоне от 0x00000000 до 0x02000000 будут отображаться в отдельных диапазонах памяти размером 32 MB, прежде чем таблицы страниц будут проходиться с целью трансляции виртуального адреса. При этом все, что нужно сделать для смены процессного контекста, - это изменить значение регистра FCSE PID. И хотя такая техника весьма популярна среди открытых операционных систем, работающих на ARMv5, использование регистра FCSE PID снижает количество одновременно исполняемых процессов в системе до 127 (равное количеству ненулевых значений 7-битного регистра FCSE PID). При этом размер виртуального адресного пространства, доступного каждому из процессов, ограничивается 32 MB (не говоря уже о том, что указанное адресное пространство будет так же включать в себя и исполняемый код). Поэтому для ядра Symbian OS и был использован другой способ отображения памяти, значительно уменьшивший столь непригодные ограничения.

Дизайн

Как уже было сказано, замещающая модель памяти (moving memory model) использует одну-единственную директорию страниц (page directory) для отображения всей памяти операционной системы. Данная глава в общих чертах расскажет вам о дизайне замещающей модели памяти.

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

В замещающей модели выделенная память всегда принадлежит одной странице в таблице страниц (single-page table). Таблица страниц (page table), в свою очередь, принадлежит отдельному блоку памяти (chunk). Таким образом блок всегда отвечает за все выделенные мегабайты пространства виртуальных адресов, а базовый адрес блока будет всегда выравниваться по мегабайтной границе.

Каждый из блоков памяти всегда размещается в одной той же области карты памяти. Таким образом, все таблицы страниц, которые связаны с ним, будут представлены соответствующими ссылками внутри элементов директории страниц (page directory). Одним из последствий этого является то, что в системе никогда не может существовать одновременно более чем 4096 таблиц страниц памяти.

Последнее правило не совсем ясно получается из перечисленных характеристик памяти. Память, недоступная для исполняемого в данный момент процесса, не всегда должна присутствовать на карте памяти. Однако значительная часть работы Symbian OS требует использования межпроцессных взаимодействий, которые как раз и основаны на возможности доступа к памяти неисполняемого в данный момент процесса. На тех же принципах реализуются системы клиентов и серверов, и ввода-вывода потоков, и если бы мы могли предоставлять ядру прямой доступ к необходимой памяти, алгоритмы этих систем значительно бы упроситились.

По умолчанию блоки данных для какого-либо процесса являются замещаемыми (moving chunks), и для них выделяется два диапазона адресов. Первый диапазон представлен адресом секции данных (data section address), или рабочим адресом (run address). Адрес секции данных является виртуальным адресом, и используется самим процессом. Первый диапазон по длине равен максимальному размеру блока данных. Второй диапазон используется из-за необходимости сохранять виртуальный адрес блока данных постоянным, независимо от того, будет ли блок расти или уменьшаться. Когда замещаемый блок отображается в памяти другого процесса, модель памяти не может гарантировать того, что его виртуальный адрес останется таким же, каким он был во время работы своего процесса. Поэтому адрес секции данных будет разным для каждого из процессов, имеющих доступ к блоку памяти.

Второй адрес, используемый блоком данных, является адресом секции ядра (kernel section address), или домашним адресом (home address). Адрес секции ядра так же является виртуальным адресом, но он используется блоком только когда его родной процесс не исполняется, а текущий процесс - не является "фиксированным" (fixed). (За разъяснением принципа работы фиксированных процессов обратитесь к следующей главе, посвященной оптимизации.) Элементы директории страниц резервируются только для секции ядра, и только для памяти, на самом деле использованной внутри блока. Если позднее к блоку будут добавлены дополнительные таблицы страниц, для блока будет выделен новый адрес секции ядра. Новый адрес секции ядра не будет представлять собою никаких проблем, так как он лишь кратковременно используется для межпроцессных доступов к памяти.

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

Защита

Во время использования техники замещения памяти (memory moving technique), показанной на рис. 7.11, для защиты текущего процесса и памяти, которая ему не должна быть видна, используются два домена. Память, которую обычно нужно защищать от текущего процесса, является либо памятью ядра (kernel memory), либо памятью, принадлежащей другим процессам. И хотя для осуществления такой защиты замещающей модели памяти было бы более очевидным использовать права на доступ к страницам памяти (page permissions), во время переключения контекста модификации прав на доступ к страницам потребовали бы изменения каждого элемента всех затрагиваемых таблиц страниц (page tables). При использовании же доменов, от модели памяти потребуется лишь изменение небольшого количества элементов директории страниц (page directory).

Большинство блоков памяти используют такие права на доступ, которые запрещают их использование в пользовательском режиме (user mode), и разрешают доступ на запись и чтение в привелигированных режимах (supervisor modes). Блоки, недоступные текущему процессу, относятся к домену 1, в то время как блоки, доступные текущему процессу, относятся к домену 0. При этом регистр контроля доступа к доменам (domain access control register) устанавливается таким образом, чтобы разрешить доступ к домену 0 (полностью игнорируя биты, определяющие права на доступ), и заставить MMU проверять права на доступ к блокам памяти домена 1. Это и приводит к желаемому эффекту: разрешить процессу в пользовательском режиме использовать свою собственную память (блоки памяти с секции данных); и запретить ему доступ к любой другой памяти, если, конечно, процесс не использует привелигированные режимы. Однако некоторые из блоков памяти используют немного другие права на доступ, чтобы повысить надежность Symbian OS:

  • После своей загрузки, все блоки памяти, содержащие код, отмечаются как доступные "только для чтения". Это позволяет защитить их от случайных или злоумышленных модификаций
  • Отображение памяти RAM-диска (RAM drive) происходит в домене 3. По умолчанию весь домен 3 устанавливается как "полностью недоступный" (no access) для предотвращения повреждения диска даже со стороны ядра. И лишь только медиа-драйверу (media driver) временно разрешается доступ к этому домену во время изменения содержимого RAM-диска.

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

Рис. 7.12 Память, доступная потоку при использовании замещающей модели памяти

Оптимизации

Каждый раз, когда какая-либо операция требует замещения хотя бы одного блока памяти, модель памяти должна очистить кэш и TLB (буфер ассоциативной трансляции). Поэтому модель памяти пытается уменьшить то количество блоков, которое необходимо замещать.

  • Так как код во всех процессах исполняется из одного и того же адреса, для размещения сегментов кода используется глобальный блок памяти (global chunk). При этом код, загруженный одним процессом, становится видимым всей операционной системе. И хотя такой шаг может привести к снижению устойчивости всей системы, он позволяет избежать очень затратной операции установки прав на доступ ко всему коду, загруженному в RAM-память, а так же избежать очистки буферов ассоциативной трансляции. Таким образом, при переключении контекста замещающей модели памяти никогда не нужно будет очищать кэш инструкций (I-cache), и в итоге общая производительность системы значительно улучшится.
  • Некоторые из блоков фиксируются в памяти, и их виртуальный адрес никогда не меняется. В таких случаях для определения прав доступа к блоку используются домены, и путем модификации регистра DACR (регистра контроля доступа к доменам) определяются все процессы, которым разрешен доступ к блоку памяти. Это позволяет уменьшить количество блоков, которые необходимо заместить при переключении контекста.
  • Важные и часто используемые серверные процессы можно обозначить как "фиксированные" (fixed). В таком случае модель памяти размещает блоки данных для этих процессов в секции ядра (kernel section), а не в секции данных (data section), как это обычно происходит. И поэтому такие блоки данных никогда не будут замещаться при переключении контекста. По возможности модель памяти так же создает специальный домен MMU для защиты памяти такого процесса. В результате, как при переключении контекста на фиксированный процесс, так и переключении контекста с него, от модели памяти не потребуется очистки кэша данных (D-cache), и даже можно будет сохранить содержимое буфера TLB. Единственным ограничением такого метода является возможность запускать лишь один-единственный экземпляр фиксированного процесса. Однако данное ограничение вполне рационально для большинства серверных процессов, работающих в операционной системе. Типичными процессами, которые мы обозначили как фиксированные, являются файловый сервер (file server), сервер коммуникаций (comms server), сервер окон (window server), сервер шрифтов и битмапов (font/bitmap server), а так же сервер баз данных (database server). Когда фиксированные процессы эффективно используются внутри устройства, это ощутимо улучшает общую производительность системы.
Карта памяти

На рис. 7.13 и 7.14 представлено как замещающая модель памяти делит пространство виртуальных адресов. Демонстрируемые диаграммы не соблюдают масштаб, и очень большие области карты памяти в них были просто укорочены. Иначе на диаграммах мы смогли бы уместить всего три или четыре области.

Алгоритмы

Для понимания того как работает данная модель памяти, очень полезно пройти через пару операций, и посмотреть что будет происходить.

Переключение процессного контекста

Модель памяти использует планировщик потоков с обратным вызовом (thread scheduler with a callback). Обратный вызов должен использоваться при переключении адресного пространства. Далее мы просто опишем процессы, возникающие при вызове планировщиком обратного вызова.

Переключение пользовательского адресного пространства (user-mode address space) в замещающей модели памяти является сложной операцией, и может потребовать значительного времени: чаще всего, более 100 микросекунд. Для уменьшения влияния этой медленной операции на работающее в режиме реального времени ядро EKA2, переключение адресного пространства выполняется с использованием вытесняющей многозадачности (preemption).

Рис. 7.13 Полная карта памяти для замещающей модели

Рис. 7.14 Использование памяти при замещающей модели

Пользовательское адресное пространство (user-mode address space) является общедоступным объектом данных на стороне ядра. Во время использования IPC, или при перемещении данных между драйверами устройств, несколько потоков могут иметь доступ к пользовательской памяти (user-mode memory) различных процессов. Поэтому изменение и использование пользовательского адресного пространства должно быть защищено мьютексом (mutex). Для этих целей замещающая модель памяти использует системный замок (system lock). Выбор системного замка в качестве мьютекса серьезно повлиял на программы ядра, и в частности, на саму модель памяти. Так, во время доступа к пользовательской памяти (user-mode memory) какого-либо из процессов, программе необходимо удерживать системный замок, чтобы сохранить в целостности пространство пользовательской памяти.

Переключение контекста является настолько длительной операцией, что удержание системного замка во время ее выполнения может повлиять на ухудшение качеств реального времени всей операционной системы, так как потокам ядра так же необходимо использовать системный замок при переносе данных в пользовательскую память и из нее. Данную проблему мы попытались решить регулярными проверками на наличие потока, ожидающего доступа к системному замку во время переключения контекста. Если такой поток существует, то переключение контекста просто прерывается, и ожидающему потоку разрешается выполнить свою работу. При этом пользовательское адресное пространство остается в полуцелостном состоянии: программы ядра могут находить и использовать любые блоки памяти пользовательского пространства, однако когда пользовательский поток (user-mode thread) снова начнет свою работу, это потребует еще бóльших усилий для завершения переключения адресного пространства.

Работа описанной ранее оптимизации с использованием фиксированных процессов основана на сохранении моделью памяти информации, касающейся некоторых процессов. К таким процессам относятся:

Переменная Описание
TheCurrentProcess Характеризует процесс, владеющий потоком, который в данный момент стоит в очереди на исполнение (currently scheduled thread).
TheCurrentVMProcess Последний запускавшийся пользовательский процесс. Этот процесс владеет картой пользовательской памяти, и данная память является доступной.
TheCurrentDataSectionProcess Пользовательский процесс, имеющий хотя бы один замещаемый блок памяти в общем диапазоне адресов (common address range), т.е. внутри секции данных (data section).
TheCompleteDataSectionProcess Пользовательский процесс, имеющий все свои замещаемые блоки памяти в секции данных.

Из-за прерванного переключения контекста, или прекращения работы процесса, некоторые из этих переменных могут иметь значение NULL.

Алгоритм, используемый для переключения процессного контекста, действует следующим образом:

  1. Если новый процесс является фиксированным, перейти к шагу 6
  2. Если новый процесс не является TheCompleteDataSectionProcess, тогда очистить кэш данных, так как придется перемещать по крайней мере один блок памяти
  3. Если какой-нибудь другой процесс, кроме нового, использует секцию данных (data section), тогда переместить все его блоки памяти в домашнюю секцию (home section), и установить для них защиту
  4. Если какой-нибудь другой процесс, кроме нового, был последним из пользовательских процессов, тогда установить защиту на все его блоки памяти
  5. Переместить блоки памяти нового процесса в секцию данных (если они еще туда не перемещены), и снять с них защиту. Перейти к шагу 8
  6. [Фиксированный процесс] Установить защиту на блоки памяти процесса TheCurrentVMProcess
  7. Снять защиту с блоков памяти нового процесса
  8. Очистить буфер TLB, если хоть один блок памяти был замещен, или были изменены права на доступ
Выполнение запросов потока

Извещение потока о выполнении его запроса (thread request complete) является сигнальным механизмом, лежащим в основе всех межпоточных связей (inter-thread communications) между пользовательскими программами, драйверами устройств и серверами. При этом модель памяти отвечает за реализацию части этого механизма, а именно - за установку финального значения статуса запроса (request status) какого-либо из потоков. Статус запроса представляет собой 32-битную переменную, размещаемую в пользовательской памяти запрашивающего потока (requesting thread). Сигнальный поток (signaling thread) устанавливает значение этой переменной при помощи метода DThread::RequestComplete(), котому помимо значения переменной так же передается и ее адрес. Когда модель памяти получит доступ к системному замку, она обязательно вызовет метод DThread::RequestComplete(), и значение будет установлено.

В замещающей модели памяти данная операция является весьма простой, так как вся пользовательская память (user-mode memory) находится в прямой видимости на карте памяти либо в секции данных (data section), либо в домашней секции (home section). Метод DThread::RequestComplete() при своем вызове начнет поиск указанного адреса среди блоков памяти процесса, и запишет указанное значение по адресу памяти, отображаемой в данный момент.

Многоуровневая модель памяти

Многоуровневая модель памяти (multiple memory model) была разработана для поддержки (и расширения) новой системы MMU, используемой в процессорах архитектуры ARMv6. Однако данная модель памяти может быть применена не только для архитектуры ARMv6, но и для систем MMU других популярных процессоров, таких как Intel x86 и Renesas SuperH.

Аппаратное обеспечение

Как и в случае с архитектурой памяти ARMv5, за всеми деталями касательно реализации подсистемы памяти 1-го уровня процессоров ARMv6, вам следует обратиться к книге "ARM Architecture Reference Manual".

Отображение виртуального адреса

Как и в архитектуре ARMv5, директория страниц (page directory) верхнего уровня содержит 4096 записей. Однако в отличии от ARMv5, директория страниц для ARMv6 может быть разбита на две части. Благодаря специальной настройке регистра контроля MMU, или как его еще называют TTBCR, к первой части директории можно отнести первые 32, 64, ..., 2048 или 4096 записи директории страниц, после чего остаток записей будет отнесен ко второй части директории страниц. Для работы с двумя частями директории страниц, MMU теперь использует два регистра TTBR: TTBR0 и TTBR1.

MMU так же использует 8-битный регистр идентификации пространства приложения (application space identifier register), или ASID. Если данный регистр будет обновляться уникальными значениями для каждого из процессов, а блоки памяти будут помечаться как относящиеся к тому или иному процессу, тогда записи буфера ассоциативной трансляции, или TLB, будут так же содержать в себе значение этого регистра. В результате при смене контекста нам не придется каждый раз уничтожать записи TLB, так как у нового процесса будет уже другое значение регистра ASID, и он не сможет использовать записи TLB для предыдущего процесса.

Защита

Хотя архитектура ARMv6 по-прежнему поддерживает концепцию доменов, их использование считается уже устаревшей технологией, так как операционная система может воспользоваться более улучшенной функциональностью новой системы MMU. В то же самое время, ARM усовершенствовала права доступа в таблице страниц (page table permissions), добавив бит, полностью запрещающий исполнение (never-execute bit). Когда этот бит установлен, соответствующая страница памяти не может быть использована для вызова команд (instruction fetching). При правильном использовании, этот бит позволяет избежать использования содержимого стека и динамической памяти в качестве исполняемого кода, что существенно усложняет возможность взлома системы при помощи переполнения буфера.

Кэши

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

Благодаря использованию физических тэгов внутри кэша, так же удалось эффективно решить проблемы, связанные с множественными отображениями (multiple mappings). Когда один и тот же виртуальный адрес ссылается на разные физические адресы (омоним), кэш по-прежнему может хранить эти физические адресы внутри себя, так как каждому из них теперь отводится отдельный тэг (см. рис. 7.15). Так же, если два виртуальных адреса будут ссылаться на один и тот же физический адрес (синоним), то благодаря физическим тэгам они будут ссылаться на одну и ту же ячейку данных внутри кэша. Таким образом будет решена и эта логическая проблема. Однако удачное решение проблемы синонимов при помощи физических тэгов - это еще не все. Использование виртуального адреса в качестве индекса кэша дает еще еще одно преимущество при решении проблемы синонимов, но речь об этом пойдет немного далее.

Идея

Свойства MMU для процессоров ARMv6 позволяют устранить несколько ограничений замещающей модели памяти, причем без компромиссов относительно функциональности устройства или требований к самой операционной системе.

Раздвоенная директория страниц в ARMv6 позволяет нам воспользоваться одной ее частью для нужд процессов. Однако на этот раз, вместо использования 16 KB для каждого из процессов, мы можем воспользоватья всего лишь некоторой частью директории страниц, а остаток директории страниц отдать под нужды глобальной памяти, и памяти ядра. Ядро EKA2 всегда использует верхнюю половину памяти (2 GB) для памяти ядра и глобальной памяти, а нижнюю половину - для памяти процессов. Такое решение ограничивает объем памяти, доступной отдельному процессу, до 8 KB, однако взамен дает до 2 GB виртуального адресного пространства каждому из процессов.

Для устройств с небольшим объемом RAM-памяти (меньшим, чем 32 MB) мы пошли еще дальше, и сделали доступным для каждого из процессов лишь 1 GB виртуального адресного пространства, параллельно уменьшив объем памяти, доступной отдельному процессу, до 4 KB. Таким образом, название модели памяти - "многоуровневая", - пришло от факта использования нескольких директорий страниц.

Многоуровневая модель памяти (multiple memory model) использует регистры идентификации пространства приложения, или регистры ASID, для решения проблемы отображения одного и того же виртуального адреса в разные физические адресы. А наличие физических тэгов в кэше позволяет корректно отображать одинаковые виртуальные или физические адреса без необходимости очищать кэш от данных. Рис. 7.15 демонстрирует как TLB (буфер ассоциативной трансляции) и кэш могут одновременно хранить внутри контексты памяти нескольких процессов, даже когда процессы используют один и тот же виртуальный адрес.

Рис. 7.15 Омонимы в ARMv6

Если сравнить замещающую модель памяти (moving memory model) с данной, то мы обнаружим, что многоуровневая модель:

  • Все еще дает каждому из процессов до 2 GB виртуального адресного пространства
  • Требует умеренных объемов памяти для каждого из процессов (4 или 8 KB)
  • Не требует очистки кэша или TLB при переключении контекста
  • Не делает код загруженной программы видимым всем остальным программам
  • Отмечает память, хранящую данные, как недоступную для выполнения инструкций кода

Главное преимущество этой модели памяти заключается в улучшении производительности всей системы благодаря отсутствию очисток кэша при переключении контекста. Учитывая отношение производительности памяти к постоянно растущим объемам кэша и быстродействию процессора, данная модель памяти так же представляет собой наилучший вариант для будущего развития.

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

Пересмотр проблемы синонимов

Хотя многоуровневая модель памяти (multiple memory model) и является улучшением над замещающей моделью (moving memory model), она все же не лишена своих сложностей. Наиболее неуклюжая особенность многоуровневой модели памяти касается решения проблемы синонимов. А именно - использование второго, или альтернативного виртуального адреса для одного и того же физического адреса. Проблема исходит из использования виртуального адреса в качестве начального индекса, благодаря которому в кэше сначала выбирается небольшое количество элементов, а затем, среди этих элементов уже осуществляется поиск данных с использованием физического адреса. На рис. 7.16 показана идеальная ситуация отображения синонимов, когда оба виртуальных адреса отображаются в один и тот же элемент кэша и данных.

Однако индексация кэша происходит при помощи нижних битов виртуального адреса. По очевидным причинам, нижние 12 бит виртуального и физического адреса будут одинаковыми, если мы используем страницы памяти размером 4 KB. Что бы могло произойти, если бы кэш для своей индексации использовал 13 бит?

Предположим, что на страницу, размещенную по физическому адресу 0x00010000, ссылаются два виртуальных адреса: 0x10000000 и 0x20001000. Когда мы записываем данные в память по адресу 0x10000230, для соответствующей ячейки внутри кэша устанавливается индексное значение 0x230 (нижние 13 бит адреса), а значение физического тэга для этой ячейки становится равным 0x00010230. И если мы теперь попытаемся прочесть данные по адресу 0x20001230 (что, согласно нашему отображению, является одной и той же ячейкой памяти), кэш начнет искать ячейку с индексом 0x1230, и не найдет предыдущую запись. В результате, кэш будет содержать обе записи, ссылающиеся на один и тот же изначальный физический адрес. Запись, обведенная пунктиром на рис. 7.16 как раз иллюстрирует вторую запись. Таким образом здесь мы видим проблему, которую уже посчитали решенной.

И если кэш будет достаточно маленьким, или индексные множества (index sets) внутри кэша будут большими (такое явление обычно называют "ассоциативностью кэша", или cache associativity), тогда для виртуального индекса нельзя использовать более чем 12 битов виртуального адреса. В этом случае каждому физическому адресу внутри кэша будет соответствовать его уникальное индексное множество. Если же для индексов кэша будут использованы 13 или более бит виртуального адреса, тогда один и тот же физический адрес может быть обнаружен в нескольких индексных множествах. Который из этих индексных множеств будет содержать необходимый нам физический адрес, будет зависеть лишь от используемого виртуального адреса. В этой ситуации один или несколько бит виртуального адреса, определяющие который из индексных множеств должен будет использован, считаются определителями "цвета" страницы памяти (color of the page).

Рис. 7.16 Синонимы в ARMv6

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

Дизайн

В некоторых отношениях дизайн многоуровневой модели памяти (multiple memory model) является более простым, так как в ней не нужно выяснять где и какая память находится в какой-либо момент времени. Если вы знаете какой из процессов обладает памятью, и у вас есть виртуальный адрес, то все дело будет заключаться лишь в работе с директорией страниц (page directory) этого процесса для выяснения местоположения его памяти. При этом следует конечно же помнить, что в директории страниц хранятся физические адреса, а для работы с таблицей страниц (page table) эти адреса придется конвертировать в виртуальные.

В многоуровневой модели памяти концепция блока памяти (chunk) для всего дизайна является менее фундаментальной. Дизайн многоуровневой модели вообще не требует наличия такого объекта. Однако так как концепция блока памяти используется главным интерфейсом между моделью памяти и ядром, блоки памяти по-прежноему формируют составляющую часть данной модели памяти.

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

Процесс ядра (kernel process) владеет глобальной директорией страниц (global page directory), и хранит ее адрес в регистре TTBR1. Все страницы памяти, отображаемые этой директорией страниц, будут глобальными, поэтому MMU создает на их основе глобальные записи в TLB. В свою очередь, глобальные записи в TLB становятся видимыми любому процессу.

Каждому пользовательскому процессу (user-mode process) данная модель памяти назначает свой ASID (регистр идентификации пространства приложения). Процессор ARMv6 поддерживает только 256 регистров ASID, поэтому операционная система сможет одновременно запустить максимум 256 процессов. Данное число процессов считается достаточным для успешной работы системы. Схема ограничения количества процессов так же накладывает ограничения на число процессных, или локальных директорий страниц, которые в данной модели памяти размещаются в виде простого и удобного массива. Память для локальной директории страниц выделяется только когда регистр ASID используется процессом. Когда процесс выполняет свою работу, регистр TTBR0 настраивается на локальную директорию страниц этого процесса.

В зависимости от типа блока памяти, модель памяти разместит его либо в глобальной директории страниц, либо - в локальной. К памяти, размещаемой в глобальной директории относятся:

  • XIP ROM, так как данный код должен быть видим всем процессам
  • Все процессы совместно используют все свои локальные данные, поэтому эти данные нужно размещать в глобальной директории
  • Любой поток, исполняемый в привелигированном режиме (supervisor mode), должен иметь доступ к данным ядра (kernel data), поэтому он размещается в глобальном блоке памяти (global chunk)

К примерам памяти, размещаемой в локальной директории, относятся:

  • Блоки памяти стека и "кучи", приватные для каждого из процессов
  • Блоки памяти для совместного использования (shared chunks), которые могут быть открыты другими процессами
  • Код программ, загружаемых в RAM-память

Два последних пункта представляют собой память, которую операционная система может отображать в более чем одном процессе. В отличии от замещающей модели памяти (moving memory model), блоки памяти, совместно используемые несколькими пользовательскими процессами, имеют один и тот же базовый адрес во всех процессах. Этот одинаковый базовый адрес многоуровневая модель памяти (multiple memory model) получает при помощи специального генератора адресов (address allocator), применяющегося для всей памяти совместного использования. Генератор адресов так же проверяет, чтобы совместно используемая память не страдала от проблемы "цвета" страниц памяти, так как базовый виртуальный адрес будет одинаковым для всех процессов. В замещающей модели памяти объекты класса DProcess содержат список всех блоков, к которым они имеют доступ на данный момент. Так же в замещающей модели памяти при переключении контекста необходимо убедиться, что нужный блок памяти доступен программе, и что проверка адреса (address lookup) разрешена во время отсутствия этого процесса в контексте. В многоуровневой модели памяти процессы тоже используют списки блоков, однако используют их лишь для подсчета связей между блоками и процессами. Когда между блоком и процессами исчезает последняя связь, этот блок тут же удаляется из карты памяти. Во время работы программы локальная директория страниц процесса отображает в себе используемый блок памяти, а так же предоставляет модели памяти возможность проверки адресов во время отсутствия процесса в контексте.

Модель памяти так же содержит обратное отображение между совместно используемым блоком (shared chunk) и процессом, который его открыл. Таким образом, модель памяти может отображать корректировки размера блока памяти во всех затрагиваемых директориях страниц.

Защита

В многоуровневой модели обеспечение защиты памяти процесса осуществляется проще, чем в замещающей модели, которая для этого требует использования доменов. Многоуровневые директории страниц (multiple page directories) обеспечивают львиную долю защиты: память, приватная для одного из процессов, не отображается на карте памяти во время работы другого процесса. Использование регистров ASID, а так же физических тэгов кэша, позволяет гарантировать доступ к соответствующим данным кэша и отображениям памяти только их процессу-владельцу. Таким образом, в отличии от замещающей модели памяти, многоуровневая модель дает полный доступ к памяти, отображаемой локальной директорией страниц (local page directory). В то же самое время, для доступа к данным ядра, отображаемым глобальной директорией страниц (global page directory), многоуровневая модель памяти требует привилегированных полномочий. Многоуровневая модель так же устанавливает биты на запрещение исполнения любого содержимого памяти данных, в том числе стеков и "куч". Таким образом система защищается от атак на переполнение буфера и исполнения его содержимого в устройстве. Код пользовательских программ, выполняемый из оперативной памяти (non-XIP), отображается в локальной директории страниц, а не в глобальной. Благодаря этому модель памяти может ограничить видимость такого кода рамками процесса, который загрузил этот код для своих нужд. Все это приводит к тому, что доступ к памяти становится идентичным идеальной ситуации, описанной в разделе 2.1.3 данной главы.

Карта памяти

На рис. 7.17 и 7.18 представлено как многоуровневая модель памяти делит пространство виртуальных адресов. Следует учесть, что в данном случае размер локальной директории страниц (local page directory) был выбран равным 8 KB. И, опять же, представленные диаграммы не выдержаны в одном масштабе.

Заключительное слово о блоках памяти

Кто-нибудь может предположить, что блок памяти (chunk) является очень высокоуровневым интерфейсом, описывающим и контролирующим память, выделенную для процесса, и отображаемую им. И что более простой низкоуровневый интерфейс мог бы предоставить бóльшую гибкость при меньших сложностях, и что разработка прерывистого блока памяти (disconnected chunk) демонстрирует потребность в возрасающей гибкости и поддержке альтернативных стратегий выделения памяти.

Рис. 7.17 Вся карта памяти для многоуровневой модели

В многостраничной модели памяти была предпринята попытка отойти от принципа принадлежности всей памяти одному блоку во время работы с программным кодом, загруженным в память. Однако так как замещающая модель памяти (moving memory model) использует блоки для описания всей имеющейся памяти, и покуда Symbian OS поддерживает ARMv5, блоки по-прежнему будут использоваться в качестве основных инструментов описания памяти, отображаемой процессом. Кроме того, блоки используются в качестве абстрактного интерфейса между стандартными программами ядра и моделью памяти. И конечно, даже если блоки не будут больше использоваться электронными устройствами памяти, сам блок памяти всегда будет формировать часть пользовательского интерфейса управления памятью.

Алгоритмы

Для многоуровневой модели памяти (multiple memory model) мы опишем те же самые операции, что были описаны для замещающей модели (moving model), чтобы проиллюстрировать дизайн модели.

Рис. 7.18 Детали управления памятью для многоуровневой модели

Переключение процессного контекста

Устройство ARMv6 делает переключение адресного пространства весьма простой операцией. Эта операция стала настолько быстрой, что теперь уже не нужно использовать вытесняющую многозадачность (preemption). Тем самым переключение процесса стало немного медленнее обычного переключения потока. Перключение процессного контекста требует модификации двух регистров MMU:

  • TTBR0 настраивается на директорию страниц нового процесса
  • CONTEXTID настраивается на ASID нового процесса

Единственная дополнительная работа должна быть выполнена в случаях когда новый процесс содержит в себе самомодифицируемые (self-modifying) пользовательские блоки кода, а сам новый процесс еще не исполнялся на процессоре. В таких случаях операция переключения адресного пространства до своего завершения должна объявить недействительной динамическую таблицу прогнозирования ветвлений (dynamic branch prediction table).

Выполнение запросов потока

В отличии от замещающей модели памяти (moving memory model), в многоуровневой модели памяти уведомление потоков о завершении их запросов (thread request complete) становится более сложной задачей. Сложности возникают из-за памяти, через которую мы уведомляем поток, и которая теперь становится недоступной текущему адресному пространству. Функция уведомления потоков о выполнении их запросов может позволить себе использовать другую, более быструю технику записи данных в другое адресное пространство, нежели чем использовать обычное копирование данных через IPC. Теперь этой функции не нужно одновременно отображать память сигнального и запрашивающего процессов (signalling and requesting process). Напротив, текущий поток наноядра (nanokernel thread) меняет свое адресное пространство, позволяя быстро и эффективно принять на себя контекст памяти целевого потока (target thread). Этот ловкий трюк осуществляется моделью памяти при помощи установки в регистры TTBR0 и CONTEXTID значений, соответствующих целевому потоку с отключенными прерыванями. В то же самое время, модель памяти обновляет значение iAddressSpace текущего потока для того, чтобы был восстановлен правильный контекст памяти, если следующая операция будет прервана. И теперь, когда текущий поток уже "прыгнул" в адресное пространство целевого процесса, он может записать результат выполнения запроса потока до настройки MMU на возвращение в изначальный адресный контекст. При этом во время записи данных в статус запроса потока (request status), следует отлавливать ситуации использования неправильного адреса памяти. Системный замок (system lock) будет активизирован, поэтому метод RequestComplete() будет отлавливать любое исключение, а затем - пытаться повторить неудавшуюся попытку записи данных при восстановлении адресного пространства.

Прямая модель

Прямая модель памяти (direct memory model) не использует MMU, поэтому операционная система применяет прямое отображение виртуальных адресов в физические. Хотя данная модель памяти позволяет Symbian OS работать на устройствах без MMU, в конечных продуктах она все же не используется, так как без поддержки MMU операционная система приобретает слишком много ограничений. Например:

  • При использовании прямой модели памяти, во время создания продукта производитель должен будет разделить физическую память между всеми запускаемыми процессами операционной системы, так как блоки памяти не смогут увеличиваться или уменьшаться без поддержки MMU. Такое ограничение сделает весьма затруднительным поддержку ряда приложений и систем, требовательных к памяти, без наличия в устройсте огромных объемов RAM-памяти
  • При использовании прямой модели, как между пользовательскими процессами, так и между приложениями ядра и пользователя, полностью исчезает защита памяти, тем самым делая всю систему меннее устойчивой. И уже было бы весьма непредусмотрительным разрешить такому устройству установку дополнительных программ после выпуска на рынок

Однако в то же самое время, существуют ситуации, когда прямая модель становится очень полезной для запуска некоторой части Symbian OS без использования MMU. В частности, для запуска ядра и файлового сервера (file server) при портировании EKA2 на новый процессор, или на целое семейство процессоров.

Такие задачи портирования становятся намного легче, если в системе изначально отключен MMU. Благодаря этому отладку работы основных компонент поддержки электронной платы устройства можно производить без одновременной отладки устройств памяти. Как только ядро EKA2 начнет работать на новом устройстве, команда портирования сможет включить в устройстве MMU, и начать независимо отслеживать все проблемы, касающиеся работы памяти.

Эмуляторная модель

Как вы уже наверное догадались, эмуляторная модель памяти (emulator memory model) была разработана специально для эмулятора, работающего в операционной системе Windows. Для достижения ряда целей, касающихся разработки и демонстрации работы кода на эмуляторе, мы пришли к некоторым компромисам, затрагивающим эмуляцию поведения настоящих моделей памяти.

Только в эмуляторной модели памяти вы найдете самые большие отличия между настоящим и эмулируемым ядром операционнной системы.

Сам эмулятор не работает на отдельной электронной плате для PC, а запускается как отдельный процесс операционной системы Windows. В результате низкоуровневая поддержка памяти в эмуляторе использует стандартные Windows API для управления памятью.

Отображение виртуального адреса

Эмулятор запускается в виде отдельного процесса Win32, с тем лишь отличием, что такой процесс для работы со всей памятью эмулятора имеет диапазон виртуальных адресов, равный всего лишь 2 GB. На настоящем же устройстве каждая из программ, запускаемая в Symbian OS, имеет обычно диапазон виртуальных адресов, равный примерно 1 GB.

Для обеспечения модели программирования блока памяти, эмулятор использует низкоуровневый Windows API - VirtualAlloc(), который может резервировать, передавать и освобождать страницы адресного пространства процесса. Благодаря этому API осуществляется эмуляция постраничного выделения RAM-памяти отдельному блоку памяти, а так же осуществляется приблизительная оценка объемов RAM-памяти, используемой операционной системой в каждый отдельный момент времени. Однако стоит отметить, что эмулятор не выделяет подобным образом всю используемую им память.

Эмулятор использует формат Windows DLL, а так же загрузчик Windows - LoadLibrary() и сопутствующие ему компоненты, чтобы позволить осуществлять отладку кода в эмуляторе при помощи стандартных сред разработки в Windows. В результате, Windows может выделять и управлять памятью сегментов кода, а так же статическими данными библиотек DLL и исполняемых файлов EXE.

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

Защита

Эмулятор работает внутри одного процесса Windows, и таким образом, работает внутри одного адресного пространства. Вся память, выделенная эмулятору, становится доступной любому процессу эмулируемой Symbian OS. В результате эмулятор не предоставляет никакой защиты памяти между процессами Symbian OS, или между памятью "пользователя" или "ядра".

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

API для программиста

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

  • Блок памяти (chunk) формирует базовый API почти для всего выделения и владения памятью как на стороне процессов ядра, так и на стороне пользовательских процессов
  • Одним из самых главных клиентов блоков памяти является класс RHeap, который выделяет память на основе блока памяти. У класса RHeap есть версии как для ядра, так и для пользовательских программ. По умолчанию, реализация стандартных функций выделения памяти C++ и C в Symbian OS так же использует этот класс
  • Программам, работающим на стороне ядра, так же доступны низкоуровневые API как для выделения памяти, так и для прямого доступа к ней (DMA). Данные API включают в себя поддержку физически смежной RAM-памяти, а так же буферов ввода-вывода (I/O buffers) и блоков памяти (chunks) совместного использования

Блоки памяти

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

За пределами исполняемого образа ядра (EKERN.EXE), программы ядра явно используют блоки только для создания блоков совместного пользования (shared chunks), но об этом мы будем говорить несколько позднее. Пользовательский API для блоков памяти формируется классом RChunk:

 
class RChunk : public RHandleBase
{
public:
enum TRestrictions
{
EPreventAdjust = 0x01
};
 
public:
inline TInt Open(...);
IMPORT_C TInt CreateLocal(...);
IMPORT_C TInt CreateLocalCode(...);
IMPORT_C TInt CreateGlobal(...);
IMPORT_C TInt CreateDoubleEndedLocal(...);
IMPORT_C TInt CreateDoubleEndedGlobal(...);
IMPORT_C TInt CreateDisconnectedLocal(...);
IMPORT_C TInt CreateDisconnectedGlobal(...);
IMPORT_C TInt Create(...);
IMPORT_C TInt SetRestrictions(TUint aFlags);
IMPORT_C TInt OpenGlobal(...);
IMPORT_C TInt Open(RMessagePtr2,...);
IMPORT_C TInt Open(TInt);
IMPORT_C TInt Adjust(TInt aNewSize) const;
IMPORT_C TInt AdjustDoubleEnded(TInt aBottom, TInt aTop) const;
IMPORT_C TInt Commit(TInt anOffset, TInt aSize) const;
IMPORT_C TInt Allocate(TInt aSize) const;
IMPORT_C TInt Decommit(TInt anOffset, TInt aSize) const;
IMPORT_C TUint8* Base() const;
IMPORT_C TInt Size() const;
IMPORT_C TInt Bottom() const;
IMPORT_C TInt Top() const;
IMPORT_C TInt MaxSize() const;
inline TBool IsReadable() const;
inline TBool IsWritable() const;
};
 

Данный класс следует стандартному шаблону хэндлера (handle), используемому во всех ресурсах ядра. Он представляет собой достаточно простой API, половина методов которого относится к инициализации хэндлера либо в результате создания нового блока памяти, либо в результате получения доступа к уже существующему блоку памяти. Разные версии методов используются для создания различных типов блока памяти, а так же для определения его видимости. Другая половина методов класса либо предоставляет доступ к атрибутам блока памяти (как, например, к его базовому адресу внутри адресного пространства процесса), либо реализует пользовательский API к различным корректирующим методам, перечисленным в разделе 3.1 данной главы.

Программистам достаточно редко приходится прямо использовать блоки для выделения памяти, кроме случаев использования глобальных блоков памяти (global chunks) в качестве реализации памяти совместного использования между процессами. Чаще всего блоки используются в качестве основы управления памятью внутри какого-нибудь распределителя памяти.

Распределители свободной и динамической памяти

Распределитель памяти (allocator) - это объект, исполняющий запросы на получение и выделение памяти программам. За каждым вызовом операторов "new" и "delete" в Cи++, или функциями malloc() и free() в Cи, стоит распределитель. Распределитель занят получением памяти от операционной системы, - обычно в виде многостраничных блоков, - и разделением этой памяти на более маленькие части, чтобы приложение смогло использовать выделенную память более эффективным образом.

API распределителей памяти

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

malloc() или оператор new Выделяет и возвращает блок памяти по крайней мере запрошенного размера, либо значение NULL, если запрос не может быть выполнен. Распределитель должен дать гарантию правильного выделения памяти для объектов любого типа. Так, например, двоичный интерфейс прикладных программ (ABI) для архитектуры ARM требует использования 8-байтного выравнивания.
free() или оператор delete Освобождает блок памяти, выделенный до этого фукнциями malloc() или realloc(). После освобождения блока памяти, программа больше не должна его использовать.
realloc() Увеличивает или уменьшает блок памяти, выделенный до этого методами malloc() или realloc(). При этом функция сохраняет содержимое блока, и возвращает указатель на обновленный блок памяти. Следует учесть, что подобную функциональность можно легко реализовать при помощи методов malloc(), memcpy() и free(), однако некоторые программы требуют исполнения такой функциональности прямо "на месте", чтобы таким образом избежать потенциально дорогостоящего копирования памяти.

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

Представленный простой API не описывает поведение распределителя свободной памяти при работе с несколькими потоками. Стандарты языка не определяют поведения распределителя в данной ситуации, так как однопроцессной программе возможно пришлось бы потратить часть своей производительности при использовании такого "потоко-безопасного" распределителя памяти. Таким образом, вопрос безопасности потоков оставлен реализации, и чуть позже мы вернемся к вопросу о том, как эту проблему решает Symbian OS.

Стратегии распределителей

Если мы исследуем проблему деления страниц RAM-памяти на блоки разных размеров, то обнаружим, что существуют различные техники структурирования и разделения выделяемой памяти, а так же различные алгоритмы для выбора конкретной части этой памяти для удовлетворения каждого из запросов на выделение памяти.

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

  • Многие распределители свободной памяти (то есть распределители, поддерживающие оператор new в Си++, или функции malloc() и free() в Cи) предполагают, что память представляет собой непрерывный адресный диапазон, и что запрос дополнительных страниц памяти расширяет область используемой памяти, расположенной "наверху". Такую функциональность можно реализовать при помощи обычного блока памяти, и стандартный распределитель "кучи" (heap allocator) в Symbian OS является таким распределителем
  • Некоторые менеджеры памяти для интерпретируюмых программных систем, таких как Java, используют для объектов двойную систему хэндлеров и тел объектов, и поэтому для своей работы требуют использования двух динамически изменяемых и продолжительных областей памяти. Такие двойные области памяти могут быть реализованы двухконечным блоком (double-ended chunk), в котором один диапазон растет вверх, а другой - вниз
  • Более продвинутые распределители могут и вовсе не требовать использования непрерывных областей памяти, и при этом будут в состоянии возвращать страницы памяти операционной системе, если программа перестала ими пользоваться. В итоге, использование памяти в операционной системе только улучшится. Для реализации таких распределителей в Symbian OS используются прерывистый блок памяти (disconnected chunk)

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

Распределители памяти в Symbian OS

В ходе разработок мы обнаружили, что Symbian OS должна достигнуть двух целей при помощи своих распределителей:

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

Первая цель была достигнута в ядре EKA1 при помощи класса RHeap. В качестве распределителя общего назначения ядро EKA2 так же использует класс RHeap, однако при этом ядро реализует и вторую цель посредством абстрактного класса для распределителя. Этот абстрактный класс определен интерфейсом MAllocator в файле e32cmn.h:

 
class MAllocator
{
public:
virtual TAny* Alloc(TInt)=0;
virtual void Free(TAny*)=0;
virtual TAny* ReAlloc(TAny*, TInt, TInt =0)=0;
virtual TInt AllocLen(const TAny*) const =0;
virtual TInt Compress()=0;
virtual void Reset()=0;
virtual TInt AllocSize(TInt&) const =0;
virtual TInt Available(TInt&) const =0;
virtual TInt DebugFunction(TInt, TAny*, TAny*)=0;
};
 

Первые три метода класса представляют собой базовый API распределителя, описанный ранее, однако операционная система ждет от распределителя еще нескольких дополнительных сервисов, которые мы приводим в следующей таблице:

Alloc() Основная функция выделения памяти, являющаяся базовой для функции malloc(), и ей подобным
Free() Основная функция освобождения памяти, являющаяся базовой для функции free(), и ей подобным
ReAlloc() Функция перевыделения памяти, являющаяся базовой для функции realloc(). Данная функция имеет третий параметр, используемый для контролирования поведения распределителя в некоторых ситуациях. Благодаря этому параметру распределитель становится совместим с программами, которые ошибочно предполагают, что все распределители ведут себя так же, как и изначальная функция RHeap::ReAlloc()
AllocLen() Возвращает длину выделенного блока памяти. Значение длины выделенного блока как минимум равно длине запрошенного блока памяти, однако иногда может значительно ее превысить
Compress() Возвращает операционной системе все неиспользуемые страницы памяти, если это возможно. Данная функция является устаревшей, однако она по-прежнему присутствует для обеспечения совместимости с ядром EKA1. Для ядра EKA2 считается, что распределители памяти должны автоматически возвращать операционной системе неиспользуемые страницы памяти при вызове метода Free()
Reset() Возвращает всю выделенную память операционной системе. По сути, данная функция заменяет вызов метода Free() для каждого выделенного блока памяти
AllocSize() Возвращает количество блоков и число байт, выделенных в текущий момент данным распределителем
Available() Возвращает для данного распределителя количество неиспользованных байт, и размер наибольшего блока памяти, для выделения которого распределителю не потребовалось бы запрашивать у операционной системы дополнительные страницы памяти
DebugFunction() Обеспечивает возможности дополнительной диагностики, инструментария и запуска принудительных отказов распределителя во время работы отладочных версий Symbian OS

На практике конкретный распределитель памяти будет унаследован от класса RAllocator. Класс RAllocator целиком определяет поведение, которое от него ждут различные API для работы со свободным простанством в Symbian OS. Класс RAllocator так же предоставляет наследникам другую дополнительную и полезную функциональность, как например вызовы функций семейства User::Leave() при невозможности выделить необходимый объем памяти, вместо простого возвращения значения NULL. Класс RAllocator так же определяет ожидаемую от него операционной системой поддержку принудительных отказов. Ниже представлено определение класса RAllocator в файле e32cmn.h:

 
class RAllocator : public MAllocator
{
public:
enum TAllocFail
{
ERandom,
ETrueRandom,
ENone,
EFailNext,
EReset
};
 
enum TDbgHeapType { EUser, EKernel };
enum TAllocDebugOp {ECount, EMarkStart, EMarkEnd, ECheck, ESetFail, ECopyDebugInfo};
enum TReAllocMode
{
ENeverMove=1,
EAllowMoveOnShrink=2
};
enum TFlags {ESingleThreaded=1, EFixedSize=2};
enum {EMaxHandles=32};
 
public:
inline RAllocator();
TInt Open();
void Close();
TAny* AllocZ(TInt);
TAny* AllocZL(TInt);
TAny* AllocL(TInt);
TAny* AllocLC(TInt);
void FreeZ(TAny*&);
TAny* ReAllocL(TAny*, TInt, TInt=0);
TInt Count() const;
TInt Count(TInt&) const;
void Check() const;
void __DbgMarkStart();
TUint32 __DbgMarkEnd(TInt);
TInt __DbgMarkCheck(TBool, TInt, const TDesC8&, TInt);
void __DbgMarkCheck(TBool, TInt, const TUint8*, TInt);
void __DbgSetAllocFail(TAllocFail, TInt);
 
protected:
virtual void DoClose();
 
protected:
TInt iAccessCount;
TInt iHandleCount;
TInt* iHandles;
TUint32 iFlags;
TInt iCellCount;
TInt iTotalAllocSize;
};
 

В данном случае мы на шаг или даже на два впереди от тех API, которые используются программистами для управления памятью. Стандартные функции управления памятью для Си и Си++ реализуются в Symbian OS при помощи статических методов класса User:

malloc() или оператор new User::Alloc()
free() или оператор delete User::Free()
realloc() User::ReAlloc()

Представленным функциям класса User необходимо использовать объект распределителя, который и будет выполнять за них всю работу. Функция User::Allocator() как раз предоставляет такую возможность, возвращая ссылку на объект класса RAllocator, который в свою очередь и будет использоваться в качестве текущего распределителя вызывающего потока.

Класс User так же имеет и другие функции, отвечающие за манипуляцию и доступ к текущему распределителю. Ниже представлена часть его соответсвующего API:

 
class User : public UserHeap
{
public:
static TInt AllocLen(const TAny*);
static TAny* Alloc(TInt);
static TAny* AllocL(TInt);
static TAny* AllocLC(TInt);
static TAny* AllocZ(TInt);
static TAny* AllocZL(TInt);
static TInt AllocSize(TInt&);
static TInt Available(TInt&);
static TInt CountAllocCells();
static TInt CountAllocCells(TInt&);
vstatic void Free(TAny*);
static void FreeZ(TAny*&);
static TAny* ReAlloc(TAny*, TInt, TInt);
static TAny* ReAllocL(TAny*, TInt, TInt);
static RAllocator& Allocator();
static RAllocator* SwitchAllocator(RAllocator*);
};
 

В данном случае мы можем видеть почти полное соответствие этого API с API класса RAllocator. Класс User реализует все эти функции одним и тем же способом: он получает объект текущего распределителя, и вызывает его соответствующую функцию.

Текущий распределитель можно заменить на любой другой при помощи функции User::SwitchAllocator(), которая дополнительно возвращает указатель на предыдущий объект распределителя данного потока. Для замены текущего потока могут послужить несколько причин, как например:

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

RHeap - распределитель по умолчанию

Symbian OS предоставляет разработчикам единственную реализацию распределителя памяти - класс RHeap, который экономно использует память, и обычно имеет хорошую производительность. Класс RHeap используется для управления свободными ресурсами как на стороне пользователя, так и на стороне ядра. Данный распределитель памяти можно описать как использующий "первый подходящий блок с наименьшим адресом из списка свободных" (first fit, address ordered, free list allocator). По сути, RHeap является простой структурой данных с очень простыми алгоритмами выделения и освобождения памяти.

Класс RHeap может поддерживать различные модели использования:

  • Использоваться для заранее выделенной памяти при реализации "кучи" фиксированного размера, либо использоваться для блока памяти (chunk) при реализации "кучи" динамического размера
  • Использоваться в однопоточной или многопоточной среде с наличием легковесных замкóв (light-weight locks)
  • Использоваться для выборочной регулировки ячеек памяти (selectable cell alignment)

Динамический RHeap использует обычный блок памяти, поэтому имеет единственную область используемой памяти (committed memory). Внутри этой области будут находиться как выделенные, так и свободные блоки памяти. Каждому блоку предшествует 32-битное слово, хранящее его длину. Распределителю не нужно следить за выделенными блоками, так этим должна заниматься программа. Эта же программа должна освобождать блоки после их использования. Однако распределитель должен следить за всеми свободными блоками, и он осуществляет эту слежку при помощи связывания всех свободных блоков в один список - список свободных блоков (free list). Для осуществления работы списка свободных блоков, распределителем используется пространство внутри свободного блока - его первое 32-битное слово.

Свободные блоки, оказывающиеся в памяти по соседству друг с другом, сливаются в один свободный блок. Таким образом, в любой момент времени "куча", или область динамической памяти формируется по одной и той же повторяющейся модели: один или несколько блоков используемой памяти, за которой следует один монолитный блок свободной памяти. При этом список свободных блоков (free list) представляет собой односвязный список (singly linked queue), элементы которого упорядочены по их адресам (address order). Благодаря такой организации списка, алгоритму освобождения памяти (de-allocation algorithm) становится легче определить когда освобождаемый блок памяти находится по соседству с уже свободными блоками памяти.

Алгоритм выделения памяти (allocation algorithm) начинает искать в списке свободные блоки, начиная с его начала и до тех пор, пока не будет обнаружен блок свободной памяти, достаточный для удовлетворения заданного запроса. Если распределителем будет обнаружен такой блок свободной памяти, то он будет тут же разделен на область выделенной памяти, и на область остаточного блока свободной памяти, информация о котором будет сохранена в списке свободных блоков. Иногда оказывается, что размер блока как раз подходит для удовлетворения запроса (или когда остаточный блок свободной памяти оказывается настолько мал, что его нецелесообразно сохранять в списке свободных блоков), тогда блок целиком переходит в пользование запрашивающей программы. Если распределитель не находит достаточно большого блока свободной памяти, тогда им предпринимается попытка увеличить размеры всего используемого блока памяти (chunk), с размещением новой свободной памяти в конце "кучи".

Алгоритм освобождения памяти (de-allocation algorithm) проверяет список для нахождения свободных блоков, соседствующих с освобождаемым блоком. Если такие блоки находятся до или после освобождаемого, то они сливаются в один свободный блок. Иначе освобождаемый блок просто добавляется в список свободных как отдельный свободный блок.

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

Одним из недостатков данной структуры данных является то, что большие свободные блоки, лежащие внутри динамической памяти, или "кучи", не возвращаются обратно операционной системе, так как распределитель может освобождать свободную память, расположенную лишь в самом конце "кучи". Однако данная структура данных в целом очень экономно использует память - примерно 4 байта на каждую выделенную ячейку памяти. А учитывая запросы на выравнивание в современных компиляторах, трата памяти повышается примерно до 8 байт.

Таким образом, несмотря на все свои ограничения, класс RHeap является отличным универсальным распределителем памяти для почти всех программ операционной системы. И когда вам потребуется более высокая производительность или экономия памяти, вы всегда сможете создать специальный распределитель для отдельных приложений.

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

Во многих случаях, когда приложение должно передать данные из одного контекста памяти в другой, как например, от одного процесса к другому, или из контекста пользователя в контекст ядра, наиболее удобным способом реализации этой передачи является копирование данных. Такое копирование может быть осуществлено в контролируемой манере, чтобы прогарантировать передачу данных от передающего контекста памяти, со всеми сопутствующими извещениями об ошибках, вместо простого завершения работы ошибившийся программы. Однако когда объемы передаваемых данных велики, или мало время, требуемое для передачи данных, гораздо легче передать саму память, чем копировать данные. Примерами таких ситуаций могут послужить передача многоканальных и высокочастотных аудио-данных в "миксер", или скачиваение больших файлов через USB.

Для любой ситуации, в которой мы должны совместно использовать память между двумя пользовательскими процессами, мы можем воспользоваться одним или несколькими глобальнымм блоками памяти (global chunk). К глобальным блокам памяти можно получить доступ как со стороны приложений ядра, так и прямо через DMA (прямой доступ к памяти). Однако у такого подхода есть одна проблема.

Блоки, о которых мы до сих пор говорили, обладают тем свойством, что их память динамически выделяется и освобождается по запросу со стороны пользовательских программ. Как например, когда ядро увеличивает блок динамической памяти (heap chunk), чтобы удовлетворить запрос на выделение большого объема памяти, или освобождает некоторые страницы памяти стека после прекращения работы потока. Таким образом становится ясно, что страница памяти, отображаемая в данный момент времени потоком ядра или DMA, может оказаться невидимой для другого потока, и доступ к такой памяти может привести к краху всей системы. Случай неотображаемой памяти во время использования DMA является и вовсе особенно трудным для диагностики, так как DMA работает с физическими адресами, и использует физическую память. И дефект может быть обнаружен только когда модель памяти переопределит память другому процессу, после чего сама же и пострадает от "случайной" порчи памяти.

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

Буферы ввода-вывода совместного использования

Одним из наипростейших объектов памяти нового типа является буфер ввода-вывода совместного использования (shared I/O buffer). Программы ядра, как например драйвер устройства, могут выделить для себя буфер ввода-вывода фиксированного размера, а затем отображать и скрывать этот буфер из адресного пространства пользовательских процессов.

Одним из наибольших ограничений таких буферов является невозможность отображать их более чем в одном пользовательском процессе одновременно. Буферы ввода-вывода совместного использования использовались в ядре EKA1, но были заменены в ядре EKA2 на более продуманные объекты совместно используемых блоков памяти (shared chunk). Поэтому использование буферов ввода-вывода совместного использования в ядре EKA2 объявлено устаревшим.

Совместно используемый блок памяти

Совместно используемый блок памяти (shared chunk) является более сложным, а потому более могущественным объектом совместно используемой памяти, который может быть использован практически во всех сценариях совместного использования памяти. Совместно используемый блок памяти очень похож на глобальный или прерывистый блок памяти (disconnected chunk), описанные в разделе 3.1 данной главы, за одним очень существенным различием: память такого блока может быть отдана в использование и освобождена лишь исключительно кодом на стороне ядра, а не пользовательским кодом.

Совместно используемый блок памяти может стать решением проблемы, когда:

  • Память должна быть выделена и контроллируема программами на стороне ядра
  • Память должна быть безопасной для использования программами обслуживания прерываний (interrupt service routine, или ISR), и DMA
  • Память может быть одновременно отображена в несколько пользовательских процессах
  • Память может быть последовательно отображена в несколько пользовательских процессах
  • Объект памяти может быть перемещен пользовательским кодом от процесса к процессу, или к другому драйверу устройства

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

За более подробной информацией о том, как совместно используемые блоки могут быть использованы драйверами устройств, обратитесь к 13-й главе, Поддержка периферии.

Глобальные и анонимные блоки памяти

Как уже было сказано, глобальные блоки памяти (global chunks) представляют собой самый удобный способ организации совместно используемой памяти между пользовательскими процессами. Анонимный блок памяти (anonymous chunk), в свою очередь, является всего лишь глобальным блоком памяти, но без имени. Из-за отсутствия имени, анонимный блок памяти нельзя найти и использовать со стороны других процессов. Однако ограниченное значение анонимных блоков для организации совместно используемой памяти между ядром и пользовательскими программами уже было упомянуто в данной главе.

Глобальные блок памяти вероятно будет решением проблем, когда:

  • Память должна быть выделена и контроллируема программами на стороне ядра
  • Память не доступна программами обслуживания прерываний (ISR), или через DMA
  • Память может быть одновременно отображена в несколько пользовательских процессах

Еще раз следует указать, что одновременное открытие в двух процессах совместно используемого блока памяти (shared chunk) не всегда гарантирует того, что у них будет один и тот же адрес для данных. Вообще, закрытие блока памяти, и его последующее открытие в одной и той же программе может привести к получению совершенно разных базовых адресов для одного и того же блока памяти!

Сервис публикации и подписки

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

Опять же, в некоторых случаях глобальный блок памяти (global chunk) смог бы послужить решением для такой проблемы. Однако если объем данных невелик, то становится невозможным сохранять хэндлер блока или адрес данных от одного обращения к данным к другому; и если для доступа к данным потребуется еще и дополнительный контроль, то тут уже нужен будет совсем другой подход.

Сервис публикации и подписки (publish and subscribe) может быть решением в подобной ситуации, так как этот сервис можно представить в виде списка "глобальных переменных", доступ к которым могут получать как программы на стороне ядра, так и на стороне пользователя. Данный сервис так же осуществляет контроль доступа к каждой из переменных на основе архитектуры безопасности платформы (platform security architecture), и некоторых гарантий режима реального времени (real-time guarantees). За детальным описанием сервиса публикации и подписки обратитесь к 4-й главе, Межпоточное взаимодействие.

Выделение памяти

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

Память ядра

Замещающая модель памяти Многоуровневая модель памяти Эмуляторная модель памяти
Стек привелигированного режима Размещается в виде страниц внутри прерывистого блока под названием "SvStack" на стороне ядра с защитными, неиспользуемыми, 2-килобайтными страницами по границам. В эмуляторе нет привелигированного "режима".
Код драйверов, работающих из оперативной памяти Размещается в виде страниц внутри глобального прерывистого блока под называнием "KERN$CODE". Размещается в виде страниц внутри прерывистого блока под называнием "$CODE" на стороне ядра. Размещается загрузчиком Windows.
Статические данные драйверов, работающих из оперативной памяти Размещаются на "куче" ядра. Размещаются загрузчиком Windows.
Свободное пространство и ячейки "кучи" Размещаются на "куче" ядра, которая в свою очередь использует динамический блок памяти под названием "SvHeap".
Адресное пространство ввода-вывода Некоторые области адресного пространства отображаются загрузчиком операционной системы в зарезервированных регионах. Другие части создаются в виде отображений в секции ядра. Не имеется в наличии.
Ввод-вывод и буферы совместного использования Создаются в виде блоков памяти внутри секции ядра. Создаются при помощи блока памяти.

Пользовательская память

Замещающая модель памяти Многоуровневая модель памяти Эмуляторная модель памяти
Стек пользовательского режима Размещается в виде страниц внутри процессного прерывистого блока под названием "$DAT" с защитными, неиспользуемыми, 8-килобайтными страницами по границам. Размещается Windows во время создания потока.
Код, работающий из оперативной памяти Размещается и отображается в виде страниц внутри глобального прерывистого блока под названием "USER$CODE". Размещается в виде страниц в секции пользовательского кода (User Code section). Отображается в процесс при помощи процессного прерывистого блока под названием "$CODE". Размещается загрузчиком Windows.
Статические данные EXE Размещаются в виде страниц в начале процессного блока под названием "$DAT". Размещаются загрузчиком Windows.
Статические данные DLL Размещаются в виде страниц внутри процессного прерывистого блока под названием "DLL$DATA". Размещаются загрузчиком Windows.
Свободное пространство и ячейки "кучи" Размещаются внутри текущего распределителя (allocator). По умолчанию таким распределителем является "куча" внутри безымянного, приватного и динамического блока памяти.
Блоки памяти Дополнительные блоки памяти (chunks) могут быть созданы и размещены в пользовательской секции адресного пространства.
Локальная память потока Каждое слово локальной памяти потока (Thread Local Storage, или TLS) размещается на карте "кучи" ядра. Доступ к данным TLS требует разрешения со стороны ядра.
Переменные сервиса публикации и подписки Данные переменных сервиса публикации и подписки (publish & subscribe) размещаются внутри "кучи" ядра, благодаря чему к ним осуществляется как доступ, так и защита.

Ситуации нехватки памяти

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

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

Обработка неудачного запроса на выделение памяти

Как уже было сказано ранее, Symbian OS не поддерживает замещение страниц памяти по требованию программ, и в сравнении с настольными системами обладает маленькими объемами физической памяти. Поэтому такая комбинация подразумевает, что ядро, а так же все системные и пользовательские программы должны быть готовы к тому, что их запросы на выделение памяти будут время от времени получать отказы. В итоге, все программы для Symbian OS должны создаваться с проверками ошибок нехватки памяти (Out of Memory, или OOM), а так же их корректной, и по возможности, элегантной обработкой.

Кроме корректной обработки неудавшихся попыток выделить память, сервер или приложение так же должно умело управлять всей своей используемой памятью. Длительно работающие сервисы (такие как ядро) должны освобождать память, отведенную для какого-нибудь ресурса, после того, как программа перестала им пользоваться. Иначе в системе начнется медленная "утечка" памяти, которая в конце-концов приведет к израсходованию памяти, и как результат - отказу системы.

Для пользовательской стороны механизм сбросов (leave mechanism), их ловушек (TRAP), а так же стек очистки (cleanup stack) предоставляют весь необходимый инструментарий для работы с выделяемой памятью, а так же восстановления работоспособности программы после неудачных попыток выделения памяти. Упомянутые сервисы хорошо описаны в таких книгах, как "Symbian OS C++ for Mobile Phones" ("Professional Development on Constrained Devices" Ричарда Харрисона (Richard Harrison) от издательства Symbian Press).

Само ядро EKA2 не используется механизмы сбросов, очистки стека и ловушек TRAP. В этом EKA2 очень сильно отличается от ядра EKA1, внутри которого были использованы ловушки TRAP. Наш опыт показал, что использование ловушек TRAP, сбросов и стека очистки внутри пользовательских программ делает их код более простым, читаемым и, чаще всего, более компактным. Однако данный опыт не распространяется на реализацию ядра EKA2: наличие частой синхронизации и вытесняющей многозадачности (preemption) практически во всем коде чаще всего требует более сложных проверок на ошибки и восстанавливающего кода. В добавок, оптимизации для ускорения важных операций или уменьшения "встряски" от смены контекста уничтожают ту симмертрию, которая столь необходима для использования протокола помещения и извлечения из стека очистки.

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

Критические секции потока

Критические секции потока (thread critical sections) не имеют никакой связи с синхронизационным примитивом на пользовательской стороне, классом RCriticalSection. Наоборот, критические секции потока - это области кода потока, во время которых поток не может быть односторонне приостановлен или уничтожен: поток удовлетворит просьбу о своем приостановлении или уничтожении только когда он пройдет критическую секцию своего кода. Ядро интенсивно использует критические секции кода, чтобы быть поток смог до конца произвести все необходимые модификации общедоступных структур данных в ядре, и чтобы эти модификации не были прерваны из-за остановки потока. Удержание быстрого мьютекса (fast mutex) так же устанавливает поток в безусловную критическую секцию, так как планировщик (scheduler) не сможет заблокировать этот поток, или удалить его из списка готовых к выполнению потоков (ready list).

Отлавливание исключений

Внутри своей критической секции потоку запрещаются любые действия, которые могли бы привести к уничтожению этого потока ядром системы, как например, из-за паникования по причине неправильных пользовательских аргументов, или из-за возникновения ситуации исключения. Последний из вариантов может возникнуть если сервис ядра должен будет скопировать данные из памяти пользовательской программы, а предоставленный указатель окажется недействительным. Подобная ошибка и делает копирование пользовательских данных трудным, в особенности когда поток должен в то же самое время удерживать системный замóк (system lock), который является безусловной критической секцией потока. Само ядро EKA2 предоставляет разработчикам систему отлова исключений и ловушек, называемую XTRAP, которая ведет себя так же как и пользовательская система сбросов и ловушек TRAP, однако в отличии от нее позволяет отлавливать исключительные ситуации аппаратного уровня (hardware exceptions), как например, сгенерированные некорректным доступом к памяти. Ядро чаще всего использует XTRAP для безопасного копирования пользовательской памяти во время нахождения потока внутри критической секции. При этом любые возникшие ошибки приведут лишь к безопасному завершению критической секции потока, и дальнейшем сообщении о возникшей ошибке.

Временные объекты

Иногда потоку нужно создать временный объект во время исполнительного вызова со стороны ядра (kernel executive call). Так как ссылка на такой объект будет находиться в регистрах и стеке вызова потока, поток мог бы войти в критическую секцию кода для предотвращения утечки памяти из-за своего возможного уничтожения. При этом критические секции потока делают обработку ошибок более сложной, так как требуют отловки исключений и отсрочки сообщения об ошибке до завершения критической секции кода. Однако и тут мы предоставляем потоку некоторую помощь: каждый объект класса DThread на стороне ядра имеет два члена для хранения ссылок на объекты типа DObject. Благодаря первому из них поток может сохранять ссылку на временный объект, а благодаря второму - сохранять временную ячейку "кучи". Если временный объект был создан, то во время завершения работы потока члены iTempObj и iExt-TempObj будут закрыты, а член iTempAlloc будет удален. В свою очередь код ядра может использовать эти члены для "владения" временными объектами во время исполнительного вызова, позволяя таким образом потоку раньше выйти из своей критической секции.

Управление системной памятью

Вполне возможно создать отдельное приложение, которое будет корректно управлять своей собственной память, обрабатывать ошибки нехватки памяти, а так же изменять свое поведение согласно объемам доступной памяти. Однако отдельное приложение не в состоянии определить когда ему следует освободить некоторую некритичную память (к примеру, кэш), чтобы смогло заработать какое-нибудь другое приложение.

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

Модель памяти подсчитывает количество неиспользованных страниц памяти, и когда их количество становится меньше определенного значения, ядро через объекты класса RChangeNotifier извещает подписчиков при помощи сообщения EChangesFreeMemory. Когда объем свободной памяти вырастает, ядро вновь извещает своих подписчиков. В добавок, при любом неудачном запросе на выделение RAM-памяти ядро извещает своих подписчиков при помощи сообщения EChangesOutOfMemory.

Два значения, определяющие когда модели памяти нужно извещать своих подписчиков, устанавливаются при помощи функции UserSvr::SetMemoryThresholds() библиотеки EUSER.

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

  • Менеджер может попросить у приложений сократить потребление памяти. К примеру, браузер может уменьшить объем используемого RAM-кэша, а виртуальная машина Java может запустить сборку мусора (garbage collect), и сжать свое пространство памяти
  • Менеджер может попросить (или потребовать) у неиспользуемых приложений и сервисов операционной системы сохранить текущие данные, и завершить свою работу. Такой способ экономии памяти хорошо подходит к телефонам, так как концепция использования многих приложений одновременно по-прежнему относится к компьютерам, а не к телефонам

И если приложения будут использовать такой механизм получения запросов, то этот механизм будет специфичен конкретному используемому пользовательскому интерфейсу.

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

Заключение

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

Мы так же показали как используется MMU для обеспечения защиты памяти между процессами. В следующей главе мы расскажем как на основе этой защиты построена система безопасности всей операционной системы.


Translated Article
Translated: 26 April 2010
See changes since translation: 28366
Revisions since translation: _

Comments

Sign in to comment…