Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Пишем свою операционную систему. Немного улучшений менеджера памяти


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

Заголовочный файл memory_manager.h теперь будет выглядеть так:

#ifndef MEMORY_MANAGER_H
#define MEMORY_MANAGER_H

#include "stdlib.h"

#define PAGE_SIZE 0x1000
#define PAGE_OFFSET_BITS 12
#define PAGE_OFFSET_MASK 0xFFF
#define PAGE_TABLE_INDEX_BITS 10
#define PAGE_TABLE_INDEX_MASK 0x3FF

#define PHYADDR_BITS 32

#define PAGE_PRESENT		(1 << 0)
#define PAGE_WRITABLE		(1 << 1)
#define PAGE_USER		(1 << 2)
#define PAGE_WRITE_THROUGH	(1 << 3)
#define PAGE_CACHE_DISABLED	(1 << 4)
#define PAGE_ACCESSED		(1 << 5)

#define PAGE_MODIFIED		(1 << 6)
#define PAGE_GLOBAL		(1 << 8)

#define KERNEL_BASE 0xFFC00000
#define KERNEL_PAGE_TABLE 0xFFFFE000
#define TEMP_PAGE 0xFFFFF000
#define TEMP_PAGE_INFO (KERNEL_PAGE_TABLE + ((TEMP_PAGE >> PAGE_OFFSET_BITS) & PAGE_TABLE_INDEX_MASK) * sizeof(phyaddr))

#define USER_MEMORY_START ((void*)0)
#define USER_MEMORY_END ((void*)0x7FFFFFFF)
#define KERNEL_MEMORY_START ((void*)0x80000000)
#define KERNEL_MEMORY_END ((void*)(KERNEL_BASE - 1))

typedef size_t phyaddr;

typedef enum {
	VMB_RESERVED,
	VMB_MEMORY,
	VMB_IO_MEMORY
} VirtMemoryBlockType;

typedef struct {
	VirtMemoryBlockType type;
	void *base;
	size_t length;
} VirtMemoryBlock;

typedef struct {
	phyaddr page_dir;
	void *start;
	void *end;
	size_t block_count;
	VirtMemoryBlock *blocks;
} AddressSpace;

phyaddr kernel_page_dir;
size_t memory_size;
AddressSpace kernel_address_space;

void init_memory_manager(void *memory_map);

size_t get_free_memory_size();
phyaddr alloc_phys_pages(size_t count);
void free_phys_pages(phyaddr base, size_t count);

void temp_map_page(phyaddr addr);
bool map_pages(phyaddr page_dir, void *vaddr, phyaddr paddr, size_t count, unsigned int flags);
phyaddr get_page_info(phyaddr page_dir, void *vaddr);

void *alloc_virt_pages(AddressSpace *address_space, void *vaddr, phyaddr paddr, size_t count, unsigned int flags);
void free_virt_pages(AddressSpace *address_space, void *vaddr, size_t count, unsigned int flags);

#endif 

Расскажу про назначение всех флагов для таблицы страниц:

PAGE_PRESENT - Страница присутствует в оперативной памяти, т.е. спроецированна. Если этого флага нет, остальные биты даже не анализируются, а сразу генерируется исключение Page Fault (#14).
PAGE_WRITABLE - Страница доступна для записи. Если этого флага нет, то любая команда записи по этому адресу приведёт к Page fault.
PAGE_USER - Страница доступна для пользователя. Если этого флага нет, то любая команда доступа (не важно чтения или записи) к этой странице из кода с CPL = 3 спровоцирует Page Fault.
PAGE_WRITE_THROUGH - Управлением кешем: разрешение сквозной записи.
PAGE_CACHE_DISABLED - Управлением кешем: кеширование запрещено
PAGE_ACCESSED - К странице было обращение. Процессор лишь устанавливает этот флаг, но не сбрасывает его. Это задача самой ОС, если она использует этот бит для определения какие страницы редко используется, чтобы сбросить их в файл подкачки на диск.

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

PAGE_MODIFIED - Содержимое страницы было изменено. PAGE_ACCESSED обозначает лишь факт доступа к странице (как чтение, так и запись), а этот флаг необходимо именно для определения доступа на запись.
PAGE_GLOBAL - Глобальная страница. Элемент не выгружается из TLB при перезагрузке CR3. Полезный атрибут для страниц, содержащие системные структуры, общие для всех процессов системы. Например, в нашей ОС этот флаг можно применить для всех страниц из диапазона от 0x80000000 до 0xFFFFFFFF.

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

void init_interrupts() {
	map_pages(kernel_page_dir, idt, alloc_phys_pages(1), 1, PAGE_PRESENT | PAGE_WRITABLE | PAGE_GLOBAL);
	memset(idt, 0, 256 * sizeof(IntDesc));
        ...
	set_int_handler(irq_base, timer_int_handler, 0x8E);
	asm("sti");
}

Из-за TLB изменение элемента таблицы страниц может не быть применено сразу, если до этого было обращение к нему и он до сих пор в кеше. Для принудительного удаления из кеша одного элемента существует ассемблерная инструкция INVLPG. Напишем специальную функцию для внутреннего использования менеджером памяти в memory_manager.c:

static inline void flush_page_cache(void *addr) {
	asm("invlpg (,%0,)"::"a"(addr));
} 

Обновлять кеш следует в функции temp_map_page и map_pages, туда и добавим вызов этой функции:

void temp_map_page(phyaddr addr) {
	*((phyaddr*)TEMP_PAGE_INFO) = (addr & ~PAGE_OFFSET_MASK) | PAGE_VALID | PAGE_WRITABLE;
	flush_page_cache((void*)TEMP_PAGE);
}

bool map_pages(phyaddr page_dir, void *vaddr, phyaddr paddr, size_t count, unsigned int flags) {
	for (; count; count--) {
		phyaddr page_table = page_dir;
		char shift;
		for (shift = PHYADDR_BITS - PAGE_TABLE_INDEX_BITS; shift >= PAGE_OFFSET_BITS; shift -= PAGE_TABLE_INDEX_BITS) {
			unsigned int index = ((size_t)vaddr >> shift) & PAGE_TABLE_INDEX_MASK;
			temp_map_page(page_table);
			if (shift > PAGE_OFFSET_BITS) {
				page_table = ((phyaddr*)TEMP_PAGE)[index];
				if (!(page_table & PAGE_VALID)) {
					phyaddr addr = alloc_phys_pages(1);
					if (addr != -1) {
						temp_map_page(paddr);
						memset((void*)TEMP_PAGE, 0, PAGE_SIZE);
						temp_map_page(page_table);
						((phyaddr*)TEMP_PAGE)[index] = addr | PAGE_VALID | PAGE_WRITABLE | PAGE_USER;
						page_table = addr;
					} else {
						return false;
					}
				}
			} else {
				((phyaddr*)TEMP_PAGE)[index] = (paddr & ~PAGE_OFFSET_BITS) | flags;
				flush_page_cache(vaddr);
			}
		}
		vaddr += PAGE_SIZE;
		paddr += PAGE_SIZE;
	}
	return true;
}

Теперь можно приступить к теории менеджера виртуальной памяти.

Для него есть понятие AddressSpace - регион виртуальной памяти, в пределах которого выделяются виртуальные адреса. У ядра есть свой AddressSpace (от KERNEL_MEMORY_BASE до KERNEL_MEMORY_END), у каждого приложения свой (в личном каталоге страниц от USER_MEMORY_START до USER_MEMORY_END).

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

Для блока типа VMB_MEMORY был выделен блок физической памяти с помощью alloc_phys_pages, а для VMB_IO_MEMORY был указан конкретный физический адрес и освобождать его с помощью free_phys_pages не нужно.

В следующем выпуске мы рассмотрим конкретную реализацию функций alloc_virt_pages и free_virt_pages. До встречи! 


В избранное