Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Руководство по созданию простой UNIX-подобной ОС

6. Страничная организация памяти

Оригинал: "6. Paging"
Автор: James Molloy
Дата публикации: 2008
Перевод: Н.Ромоданов
Дата перевода: январь 2012 г.

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

6.1. Виртуальная память (теория)

Если вы знаете, что такое виртуальная память, вы можете пропустить этот раздел.

Если в linux вы создадите крошечную программу, например,

int main(char argc, char **argv)
{
  return 0;
} 

, откомпилируете ее, а затем запустите 'objdump -f', вы можете увидеть нечто, похожее на следующее:

jamesmol@aubergine:~/test> objdump -f a.out

a.out:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482a0 

Обратите внимание на начальный адрес программы 0x80482a0, что эквивалентно приблизительно 128 Мб в адресном пространстве. Может показаться странным, но эта программа будет работать отлично на машинах с объемом оперативной памяти, меньшем 128 Мб.

То, что программа на самом деле «видит», когда она читает из памяти и пишет в память, является виртуальным адресным пространством. Часть виртуального адресного пространства отображается в физическую память, а часть - не отображается. Если вы пытаетесь получить доступ к неотображаемой части, то процессор выдаст сигнал о некорректном обращении к памяти page fault, операционная система ловит его, и в POSIX-системе выдает сигнал SIGSEGV, за которым непосредственно следует сигнал SIGKILL.

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

Виртуальная память этого типа полностью зависит от поддержки аппаратным обеспечением. Ее нельзя эмулировать программно. К счастью, в архитектуре x86 есть именно такая возможность. Этот модуль называется MMU (Memory Management Unit- Устройство управление памятью) и он обрабатывает все операции по отображению памяти, связанные с сегментацией и страничной организацией памяти, образуя слой между процессором и памятью (на самом деле, это часть процессора, но это всего лишь деталь реализации).

6.2. Страничная организация памяти как конкретная реализация виртуальной памяти

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

Страничная организация представляет собой разделение виртуального адресного пространства.

6.2.1. Запись, описывающая страницу

В каждом процессе обычно имеются различные наборы отображения страниц, так что пространства виртуальной памяти не зависят друг от друга. В архитектуре x86 (32-разрядной) размеры страниц зафиксированы в 4 Кб по размеру. У каждой страницы есть соответствующее слово дескриптора, которое сообщает процессору, какому кадру страница сопоставляется. Обратите внимание, что страницы и кадры должны быть выровнены по границе в 4Кб (4KB будет 0x1000 байтов), при этом младшие 12 битов 32-разрядного слова всегда равны нулю. Это используется архитектурой для хранения в них информации о странице, например, присутствует ли она в памяти в режиме ядра или в пользовательском режиме и т.д. Формат этого слова изображен на рисунке.

Поля этого слова довольно просты, так что давайте быстрее пробежимся по ним.

P - Установлено, если страница находится в памяти.

R/W - Если установлено, то на страницу можно выполнять запись. Если не установлено, то страница доступна только для чтения. Это поле не используется, когда код работает в режиме ядра (если не установлен флаг CR0).

U/S - Если установлено, то это пользовательский режим. В противном случае, это страница в режиме супервизора (ядра). Код пользовательского режима не может выполнять запись на страницы, находящиеся в режиме ядра, или читать из них.

Reserved - Для внутреннего использования процессором и это поле не следует менять.

A - Установлено, если к странице был доступ (выполнялось обращение процессора).

D - Установлено, если на станице выполнялась запись (страница изменена).

AVAIL - Эти три бита не используются и доступны в режиме ядра.

Page frame address - старшие 20 битов адреса фрейма в физической памяти.

6.2.2. Директории / таблицы страниц

Возможно, вы подсчитали на калькуляторе и выяснили, что чтобы создать таблицу, отображающую каждую страницу размером 4KB одним 32-битный дескриптором, для 4 ГБ адресного пространства потребуется 4 Мбайт оперативной памяти. Так или иначе, но это правда.

4 Мб могут показаться большими накладными расходами, и надо быть справедливым, так оно и есть. Если у вас есть 4 ГБ физической памяти, то это не много. Однако, если вы работаете на компьютере, у которого 16 МБ оперативной памяти, вы сразу потеряли четверть доступной памяти! Нам нужно что-нибудь получше, что будет занимать объем, пропорциональный объему имеющейся у вас оперативной памяти.

Да, у нас нет этого. Но в Intel придумали что-то похожее - они используют 2-х уровневую систему. Процессор получает информацию из директория страниц, большой таблицы размером в 4KB, каждая запись которой указывает на таблицу страниц. Таблица страниц опять же имеет размер в 4KB и каждая запись является записью таблицы страниц, приведенной выше.

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

6.2.3. Подключение страничной организации памяти

Подключить страничную организацию памяти исключительно просто.

  1. Скопируйте место вашего директория страниц в регистр CR3. Это должен быть, конечно, физический адрес.
  2. Установите бит PG в регистре CR0. Вы можете сделать это с помощью операции OR и операнда 0x80000000.

6.3. Некорректное обращение к памяти - page fault

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

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

Прерывание некорректного обращения к памяти имеет номер 14, и, если обратиться к главе 3, мы видим, что вырабатывается код ошибки. Эта ошибка дает нам довольно много информации о том, что произошло.

Бит 0 - Если установлен, то проблема возникла не из-за того, что страница не присутствовала в физической памяти. Если не установлен, то страница не присутствует в физической памяти.

Бит 1 - Если установлен, то это значит, что операция, из-за которой возникла проблема, была операцией записи, в противном случае это была операция чтения.

Бит 2 - Если установлен, то прерывание произошло, когда процессор работал в пользовательском режиме. В противном случае он работал в режиме ядра.

Бит 3 - Если установлен, то это означает, что проблема была вызвана тем, что были перезаписаны зарезервированные биты.

Бит 4 -Если установлен, то это значит, что проблема возникла в момент выборки команд.

6.4. Применяем все на практике

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

6.4.1. Простое управление памятью с помощью команды выделения памяти malloc

Если вы знаете язык C++, вы, возможно, слышали о команде 'placement new' (выделить новую память). Это версия команды new, у которой есть параметр. Вместо вызова команды malloc, которая используется обычно, команда new создает объект по указанному адресу. Мы будем использовать очень похожую концепцию.

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

Поскольку нам нужно выделять память при загрузке ядра довольно рано, можно предположить, что ничего, кроме команд kmalloc() и kfree() нам не потребуется. Это существенно все упрощает. У нас может быть просто указатель (адрес размещения) для некоторой свободной памяти, который мы передаем обратно в requestee, а затем увеличиваем. Таким образом:

u32int kmalloc(u32int sz)
{
  u32int tmp = placement_address;
  placement_address += sz;
  return tmp;
} 

Этого, на самом деле, будет достаточно. Тем не менее, у нас есть еще одно требование. Когда мы размещаем таблицы и директории страниц, они должны быть выровнены по границам страниц. Так что мы можем собрать следующее:

u32int kmalloc(u32int sz, int align)
{
  if (align == 1 && (placement_address & 0xFFFFF000)) // Если адрес еще не выровнен по границе страниц
  {
    // Align it.
    placement_address &= 0xFFFFF000;
    placement_address += 0x1000;
  }
  u32int tmp = placement_address;
  placement_address += sz;
  return tmp;
} 

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

u32int kmalloc(u32int sz, int align, u32int *phys)
{
  if (align == 1 && (placement_address & 0xFFFFF000)) // Если адрес еще не выровнен по границе страниц
  {
    // Align it.
    placement_address &= 0xFFFFF000;
    placement_address += 0x1000;
  }
  if (phys)
  {
    *phys = placement_address;
  }
  u32int tmp = placement_address;
  placement_address += sz;
  return tmp;
} 

Великолепно. Это все, что нужно для простого управления памятью. В моем коде я, на самом деле, (в эстетических целях) переименовал kmalloc в kmalloc_int (для kmalloc_internal). Еще у меня есть несколько функций-обверток:

u32int kmalloc_a(u32int sz);  // выделяет страницу.
u32int kmalloc_p(u32int sz, u32int *phys); // возвращает физический адрес.
u32int kmalloc_ap(u32int sz, u32int *phys); // выделяет страницу и возвращает физический адрес.
u32int kmalloc(u32int sz); // Обычная функция. 

Я просто чувствую, этот интерфейс лучше, чем просто указывать 3 параметра при каждом выделении памяти из кучи в ядре! Эти определения должны быть в файлах kheap.h/kheap.c.

6.4.2. Необходимые определения

В файле paging.h должны находиться определения некоторых структур, которые сделают нашу жизнь проще.

#ifndef PAGING_H
#define PAGING_H

#include "common.h"
#include "isr.h"

typedef struct page
{
   u32int present    : 1;   // Страница присутствует в памяти
   u32int rw         : 1;   // Если сброшен, то страница только для чтения, если установлен, то страница для чтения и записи
   u32int user       : 1;   // Если сброшен, то уровень супервизора
   u32int accessed   : 1;   // Было ли обращение к странице после последнего ее обновления?
   u32int dirty      : 1;   // Выполнялась ли запись на страницу после последнего ее обновления?
   u32int unused     : 7;   // Все неиспользуемые и зарезервированные биты
   u32int frame      : 20;  // Адрес фрейма (сдвинут вправо на 12 бит)
} page_t;

typedef struct page_table
{
   page_t pages[1024];
} page_table_t;

typedef struct page_directory
{
   /**
      Массив указателей на таблицы страниц.
   **/
   page_table_t *tables[1024];
   /**
      Массив указателей на таблицы страниц, о которых говорилось выше, но указатели указывают *физическое*
      местоположение, используемое при загрузке в регистр CR3.
   **/
   u32int tablesPhysical[1024];
   /**
      Физический адрес tablesPhysical. Его потребуется использовать в случае,
      когда мы получаем в ядре память типа куча, а директорий может находиться
      в любом месте виртуальной памяти.
   **/
   u32int physicalAddr;
} page_directory_t;

/**
  Настройка среды окружения, директориев страниц и т.д. и 
  включение страничной организации памяти.
**/
void initialise_paging();

/**
  Загружает указанны директорий страниц в регистр CR3.
**/
void switch_page_directory(page_directory_t *new);

/**
  Поиск указателя на необходимую страницу.
  Если make == 1 в таблице страниц, в которой эта страница должна располагаться,
  то страница не создана - создайте страницу!
**/
page_t *get_page(u32int address, int make, page_directory_t *dir);

/**
  Обаботчик некорректного обращения к страницам.
**/
void page_fault(registers_t regs); 

Обратите внимание на элементы tablesPhysical и physicalAddr из page_table_t. Что они там делают?

Элемент physicalAddr на самом деле только для того случая, когда мы клонируем директории страниц (объясним позднее в наших руководствах). Запомните, что в этот момент новый директорий будет иметь адрес в виртуальной памяти, который не совпадает с адресом физической памяти. Нам понадобится физический адрес для того, чтобы сообщить его процессору в случае, если мы когда-либо захотим переключать директории.

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

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

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

6.4.3. Размещение фреймов физической памяти

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

Если вы не знаете, что такое bitset (также называемый битовой картой), вы должны прочитать о нем по ссылке, приведенной выше. В реализациях bitset есть только 3 функции - set (установить), test (проверить) и clear (сбросить). Я также должен реализовать функцию с тем, чтобы по битовой карте эффективно найти первый свободный фрейм. Посмотрите на нее и разберитесь, почему она эффективна. Моя реализация этих функций приведена ниже. Я не собираюсь приводить объяснения - это общая концепция, не связанная с ядром. Если у вас есть сомнения, поищите в Google реализацию bitset, либо, на крайний случай, обратитесь на форум osdev.net.

// Набор bitset для фреймов.
u32int *frames;
u32int nframes;

// Определено в kheap.c
extern u32int placement_address;

// В алгоритмах для bitset используются макросы.
#define INDEX_FROM_BIT(a) (a/(8*4))
#define OFFSET_FROM_BIT(a) (a%(8*4))

// Статическая функция для установки бита в наборе bitset для фреймов
static void set_frame(u32int frame_addr)
{
   u32int frame = frame_addr/0x1000;
   u32int idx = INDEX_FROM_BIT(frame);
   u32int off = OFFSET_FROM_BIT(frame);
   frames[idx] |= (0x1 << off);
}

// Статическая функция для сброса бита в наборе bitset для фреймов
static void clear_frame(u32int frame_addr)
{
   u32int frame = frame_addr/0x1000;
   u32int idx = INDEX_FROM_BIT(frame);
   u32int off = OFFSET_FROM_BIT(frame);
   frames[idx] &= ~(0x1 << off);
}

// Статическая функция для проверки, установлен ли бит
static u32int test_frame(u32int frame_addr)
{
   u32int frame = frame_addr/0x1000;
   u32int idx = INDEX_FROM_BIT(frame);
   u32int off = OFFSET_FROM_BIT(frame);
   return (frames[idx] & (0x1 << off));
}

// Статическая функция для поиска первого свободного фрейма
static u32int first_frame()
{
   u32int i, j;
   for (i = 0; i < INDEX_FROM_BIT(nframes); i++)
   {
       if (frames[i] != 0xFFFFFFFF) // нечего не освобождаем, сразу выходим.
       {
           // по меньшей мере, здесь один свободный бит
           for (j = 0; j < 32; j++)
           {
               u32int toTest = 0x1 << j;
               if ( !(frames[i]&toTest) )
               {
                   return i*4*8+j;
               }
           }
       }
   }
} 

Надеюсь, что код не должен вызвать слишком много сюрпризов. Это просто упражнения по работе с битами. Затем мы переходим к функциям выделения и освобождения фреймов. Теперь, когда у нас есть эффективная реализация bitset, эти функции будут всего в несколько строк!

// Функция выделения фрейма.
void alloc_frame(page_t *page, int is_kernel, int is_writeable)
{
   if (page->frame != 0)
   {
       return; // Фрейм уже выделен, сразу возвращаемся.
   }
   else
   {
       u32int idx = first_frame(); // idx теперь является индексом первого свободного фрейма.
       if (idx == (u32int)-1)
       {
           // PANIC это всего лишь макрос, которые выдает на экран сообщение, а затем переходит в бесконечный цикл.
           PANIC("No free frames!");
       }
       set_frame(idx*0x1000); // Этот фрейм теперь наш!
       page->present = 1; // Помечаем его как присутствующий.
       page->rw = (is_writeable)?1:0; // Можно ли для страницы выполнять запись?
       page->user = (is_kernel)?0:1; // Находится ли страница в пользовательском режиме?
       page->frame = idx;
   }
}

// Function to deallocate a frame.
void free_frame(page_t *page)
{
   u32int frame;
   if (!(frame=page->frame))
   {
       return; // Указанной страницы теперь фактически нет в выделенном фрейме!
   }
   else
   {
       clear_frame(frame); // фрейм теперь снова свободен.
       page->frame = 0x0; // Страницы теперь во фрейме нет.
   }
} 

Обратите внимание, что макрос PANIC просто вызывает глобальную функцию, которая называется panic, с аргументами __FILE__ и __LINE__. Функция panic печатает их и переходит в бесконечный цикл, останавливая все исполнение.

6.4.4. Наконец-то код для страничной организации памяти

void initialise_paging()
{
   // Размер физической памяти. Сейчас мы предполагаем,
   // что размер равен 16 MB.
   u32int mem_end_page = 0x1000000;

   nframes = mem_end_page / 0x1000;
   frames = (u32int*)kmalloc(INDEX_FROM_BIT(nframes));
   memset(frames, 0, INDEX_FROM_BIT(nframes));

   // Давайте создадим директорий страниц.
   kernel_directory = (page_directory_t*)kmalloc_a(sizeof(page_directory_t));
   memset(kernel_directory, 0, sizeof(page_directory_t));
   current_directory = kernel_directory;

   // Нам нужна карта идентичности (физический адрес = виртуальный адрес) с адреса
   // 0x0 до конца используемой памяти с тем, чтобы у нас к ним был прозрачный 
   // доступ как если бы страничная организация памяти не использовалась.
   // ЗАМЕТЬТЕ, что мы преднамеренно используем цикл while.
   // Внутри тела цикла мы фактически изменяем адрес placement_address
   // с помощью вызова функции kmalloc(). Цикл while используется здесь, т.к. выход
   // из цикла динамически, а не один раз после запуска цикла.
   int i = 0;
   while (i < placement_address)
   {
       // Код ядра можно читать из пользовательского режима, но нельзя в него записывать.
       alloc_frame( get_page(i, 1, kernel_directory), 0, 0);
       i += 0x1000;
   }
   // Прежде, чем включить страничное управление памятью, нужно зарегистрировать
   // обработчик некорректного обращения к памяти - page fault.
   register_interrupt_handler(14, page_fault);

   // Теперь включаем страничную организацию памяти!
   switch_page_directory(kernel_directory);
}

void switch_page_directory(page_directory_t *dir)
{
   current_directory = dir;
   asm volatile("mov %0, %%cr3":: "r"(&dir->tablesPhysical));
   u32int cr0;
   asm volatile("mov %%cr0, %0": "=r"(cr0));
   cr0 |= 0x80000000; // Enable paging!
   asm volatile("mov %0, %%cr0":: "r"(cr0));
}

page_t *get_page(u32int address, int make, page_directory_t *dir)
{
   // Помещаем адрес в индекс.
   address /= 0x1000;
   // Находим таблицу страниц, в которой есть этот адрес.
   u32int table_idx = address / 1024;
   if (dir->tables[table_idx]) // Если эта таблица уже назначена
   {
       return &dir->tables[table_idx]->pages[address%1024];
   }
   else if(make)
   {
       u32int tmp;
       dir->tables[table_idx] = (page_table_t*)kmalloc_ap(sizeof(page_table_t), &tmp);
       memset(dir->tables[table_idx], 0, 0x1000);
       dir->tablesPhysical[table_idx] = tmp | 0x7; // PRESENT, RW, US.
       return &dir->tables[table_idx]->pages[address%1024];
   }
   else
   {
       return 0;
   }
}

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

Функция switch_page_directory делает именно то, что говорит название (переключение директория страниц). Она берет директорий страниц и переключается на него. Она делает это при помощи перемещения в регистр CR3 адреса элемента номера tablesPhysical этого директория. Помните, что номер tablesPhysical указывает на массив физических адресов. После этого она сначала получает содержимое регистра CR0, затем выполняет операцию OR с битом PG (0x80000000), а затем перезаписывает значение регистра. В результате включается страничная организация памяти, а также осуществляется сброс кэша с директорием страниц.

Функция get_page возвращает указатель запись страницы для конкретного адреса. В функцию также можно передать параметр make. Если make равен 1 и таблица страницы, в которой должна находиться запись о запрашиваемой странице, еще не была создана, то она создается. В противном случае, функция просто вернет 0. Если таблица уже назначена, функция найдет запись о странице и вернет ее. Если ее нет (и make == 1), то будет предпринята попытка создать ее.

Наша функция kmalloc_ap ищет блок памяти, который выровнен по границе страницы, и запоминает место его физического расположения. Физическое расположение сохраняется в 'tablesPhysical' (после того, как несколько битов сообщат процессору, что страница присутствует, в нее можно делать запись и она доступна пользователю), а виртуальное месторасположение запоминается в 'tables'.

Функция initialise_paging, во-первых, создаст bitset для фреймов и с помощью команды memset заполняет его нулями. Затем она для директория страниц выделяет пространство (которое выровнено по краю страницы). После этого, она размещает фреймы так, что доступ к любой странице будет отображаться во фрейм с соответствующим линейным адресом, называемым идентичным отображением. Это сделано для небольшой части адресного пространства, так что код ядра может работать, как обычно. Она регистрирует обработчик прерывания для неверного обращения к странице (смотрите ниже), а затем включает страничную организацию памяти.

6.4.5. Обработчик неверного обращения к странице

void page_fault(registers_t regs)
{
   // Возникло прерывания неверного обращения к странице - page fault.
   // Адрес прерывания запоминается в регистре CR2.
   u32int faulting_address;
   asm volatile("mov %%cr2, %0" : "=r" (faulting_address));

   // Код ошибки подробно сообщит нам о том, что случилось.
   int present   = !(regs.err_code & 0x1); // Страница отсутствует
   int rw = regs.err_code & 0x2;           // Операция записи?
   int us = regs.err_code & 0x4;           // Процессор находится в пользовательском режиме?
   int reserved = regs.err_code & 0x8;     // В записи страницы переписаны биты, зарезервированные для нужд процессора?
   int id = regs.err_code & 0x10;          // Причина во время выборки инструкции?

   // Выдача сообщения об ошибке.
   monitor_write("Page fault! ( ");
   if (present) {monitor_write("present ");}
   if (rw) {monitor_write("read-only ");}
   if (us) {monitor_write("user-mode ");}
   if (reserved) {monitor_write("reserved ");}
   monitor_write(") at 0x");
   monitor_write_hex(faulting_address);
   monitor_write("\n");
   PANIC("Page fault");
} 

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

6.4.6. Тестирование

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

main.c

int main(struct multiboot *mboot_ptr)
{
   // Инициализирует все6 значения ISRs и сегментацию
   init_descriptor_tables();
   // Инициализирует экран (очищает его)
   monitor_clear();

   initialise_paging();
   monitor_write("Hello, paging world!\n");

   u32int *ptr = (u32int*)0xA0000000;
   u32int do_page_fault = *ptr;

   return 0;
} 

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

Поздравляю! Вы все сделали! Теперь вы можете перейти к следующему руководству - создание памяти типа куча для работающего ядра. Исходный код для этого руководства можно получить здесь.


Назад К началу Вперед