Diseño de sistema operativo tipo Unix: espacio de direcciones virtuales (6)

En el artículo anterior, examinamos los conceptos básicos del trabajo en modo protegido IA-32. Hoy es hora de aprender a trabajar con el espacio de direcciones virtuales.

Tabla de contenidos


Sistema de construcción (marca, gcc, gas). Arranque inicial (arranque múltiple). Lanzamiento (qemu). Biblioteca C (strcpy, memcpy, strext).

Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Creación de la biblioteca en modo kernel y modo de aplicación de usuario.

El registro del sistema del núcleo. Memoria de video Salida a la terminal (kprintf, kpanic, kassert).
Memoria dinámica, montón (kmalloc, kfree).

Organización de memoria y manejo de interrupciones (GDT, IDT, PIC, syscall). Excepciones
Memoria virtual (directorio de páginas y tabla de páginas).

Proceso. Planificador Multitarea Sistema de llamadas (kill, exit, ps).

El sistema de archivos del kernel (initrd), elf y sus componentes internos. Sistema de llamadas (exec).

Controladores de dispositivos de caracteres. Llamadas del sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).

Shell como un programa completo para el kernel.

Modo de protección del usuario (anillo3). Segmento de estado de la tarea (tss).

Memoria virtual


Se necesita memoria virtual para que cada proceso pueda aislarse de otro, es decir No pudo detenerlo. Si no hubiera memoria virtual, tendríamos que cargar archivos elfos en diferentes direcciones en la memoria cada vez. Pero como sabe, los archivos ejecutables pueden contener enlaces a direcciones específicas (absolutas). Por lo tanto, al compilar elf, ya se sabe en qué dirección en la memoria se cargará (ver secuencia de comandos del enlazador). Por lo tanto, no podemos cargar dos archivos elf sin memoria virtual. Pero incluso con la memoria virtual activada, hay bibliotecas dinámicas (como .so) que pueden cargarse en cualquier dirección. Se pueden descargar en cualquier dirección debido al hecho de que tienen una sección de reubicación. En esta sección, todos los lugares donde se utiliza el direccionamiento absoluto se registran, y el núcleo, al cargar dicho archivo elfo, debe corregir estas direcciones con plumas, es decir. Añádales la diferencia entre la dirección de descarga real y la deseada.

Consideraremos trabajar con páginas de 4 kilobytes. En esta situación, podemos abordar hasta 4 megabytes de RAM. Eso es suficiente para nosotros. El mapa de direcciones se verá así:

0-1 mb : no tocar.
1-2 mb : código y datos del núcleo.
2-3 mb : un montón de granos.
3-4 mb : páginas personalizadas de archivos elfos cargados.

La dirección lineal (obtenida del modelo plano) cuando la paginación de página está habilitada no es igual a la física. En cambio, la dirección se divide por el desplazamiento (bits bajos), el índice de las entradas en la tabla de páginas y el índice del directorio de páginas (bits altos). Cada proceso tendrá su propio directorio de páginas y, en consecuencia, tablas de páginas. Esto es lo que le permite organizar un espacio virtual de direcciones.

La entrada del directorio de páginas se ve así:

struct page_directory_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 zero : 1; u8 page_size : 1; u8 ignored : 1; u8 available : 3; u32 page_table_addr : 20; } attribute(packed); 

El elemento de la tabla de páginas se ve así:

 struct page_table_entry_t { u8 present : 1; u8 read_write : 1; u8 user_supervisor : 1; u8 write_through : 1; u8 cache_disabled : 1; u8 accessed : 1; u8 dirty : 1; u8 zero : 1; u8 global : 1; u8 available : 3; u32 page_phys_addr : 20; } attribute(packed); 

Para el núcleo, describiremos el directorio y la tabla de páginas como variables estáticas. Es cierto que existe el requisito de que estén alineados en el borde de la página.

 static struct page_directory_entry_t kpage_directory attribute(aligned(4096)); static struct page_table_entry_t kpage_table[MMU_PAGE_TABLE_ENTRIES_COUNT] attribute(aligned(4096)); 

Seguiremos el camino simple y haremos que todo el espacio de direcciones físicas sea accesible para el núcleo. El nivel de privilegio de las páginas del núcleo debe ser un supervisor para que nadie se suba a ellas. Los procesos ligeros que deben ejecutarse en modo kernel compartirán el mismo espacio de direcciones con el kernel. Con este proceso, tendremos una cola de ejecución pendiente para manejar las interrupciones pendientes. Pero más sobre eso en la lección sobre los controladores de dispositivos de caracteres. Todavía tenemos que realizar la multitarea antes de considerar este tema.

Cree un directorio de páginas del núcleo y una tabla de páginas correspondiente. Cuando el núcleo se inicializa, estará activo.

 /* * Api - init kernel page directory * Here assumed each entry addresses 4Kb */ extern void mmu_init() { memset(&kpage_directory, 0, sizeof(struct page_directory_entry_t)); /* set kernel page directory */ kpage_directory.zero = 1; kpage_directory.accessed = 0; kpage_directory.available = 0; kpage_directory.cache_disabled = 0; kpage_directory.ignored = 0; kpage_directory.page_size = 0; /* 4KB */ kpage_directory.present = 1; /* kernel pages always in memory */ kpage_directory.read_write = 1; /* read & write */ kpage_directory.user_supervisor = 1; /* kernel mode pages */ kpage_directory.write_through = 1; kpage_directory.page_table_addr = (size_t)kpage_table >> 12; /* set kernel table */ for (int i = 0; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { kpage_table[i].zero = 0; kpage_table[i].accessed = 0; kpage_table[i].available = 0; kpage_table[i].cache_disabled = 0; kpage_table[i].dirty = 0; kpage_table[i].global = 1; kpage_table[i].present = 1; /* kernel pages always in memory */ kpage_table[i].read_write = 1; /* read & write */ kpage_table[i].user_supervisor = 1; /* kernel mode pages */ kpage_table[i].write_through = 1; kpage_table[i].page_phys_addr = (i * 4096) >> 12; /* assume 4Kb pages */ } } 

Cuando carguemos los archivos elf, necesitaremos crear un directorio de páginas para el proceso del usuario. Puede hacer esto con la siguiente función:

 /* * Api - Create user page directory */ extern struct page_directory_entry_t* mmu_create_user_page_directory(struct page_table_entry_t* page_table) { struct page_directory_entry_t* upage_dir; upage_dir = malloc_a(sizeof(struct page_directory_entry_t), 4096); upage_dir->zero = 1; upage_dir->accessed = 0; upage_dir->available = 0; upage_dir->cache_disabled = 0; upage_dir->ignored = 0; upage_dir->page_size = 0; /* 4KB */ upage_dir->present = 1; upage_dir->read_write = 1; /* read & write */ upage_dir->user_supervisor = 0; /* user mode pages */ upage_dir->write_through = 1; upage_dir->page_table_addr = (size_t)page_table >> 12; /* assume 4Kb pages */ return upage_dir; } 

Por defecto, la tabla de páginas de proceso contendrá páginas del núcleo y entradas vacías para futuras páginas de proceso, es decir. registros con el indicador actual borrado y la dirección de página física en 0.

 /* * Api - Create user page table */ extern struct page_table_entry_t* mmu_create_user_page_table() { struct page_table_entry_t* upage_table; upage_table = malloc_a(sizeof(struct page_table_entry_t) * MMU_PAGE_TABLE_ENTRIES_COUNT, 4096); /* share kernel pages */ memcpy(upage_table, kpage_table, sizeof(struct page_table_entry_t) * MMU_KERNEL_PAGES_COUNT); /* fill user pages */ for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 0; /* not present as so as there is no user pages yet */ current->read_write = 1; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = 0; /* page is not present */ } return upage_table; } 

Necesitamos aprender cómo agregar nuevas páginas físicas a la tabla de páginas de proceso, ya que no habrá ninguna por defecto. Lo necesitaremos cuando carguemos archivos elf en la memoria, cuando carguemos segmentos descritos en los encabezados del programa. La función nos ayudará con esto:

 /* * Api - Occupy user page */ extern bool mmu_occupy_user_page(struct page_table_entry_t* upage_table, void* phys_addr) { for (int i = MMU_KERNEL_PAGES_COUNT; i < MMU_PAGE_TABLE_ENTRIES_COUNT; ++i) { struct page_table_entry_t* current; current = upage_table + i; if (current->present) { /* page is buzy */ continue; } current->zero = 0; current->accessed = 0; current->available = 0; current->cache_disabled = 0; current->dirty = 0; current->global = 1; current->present = 1; current->read_write = 1; /* read & write */ current->user_supervisor = 0; /* user mode page */ current->write_through = 1; current->page_phys_addr = (size_t)phys_addr >> 12; /* assume 4Kb pages */ return true; } return false; } 

El modo de paginación se activa y desactiva un bit en el registro de la bandera del procesador.

 /* * Enable paging * void asm_enable_paging(void *page_directory) */ asm_enable_paging: mov 4(%esp),%eax # page_directory mov %eax,%cr3 mov %cr0,%eax or $0x80000001,%eax # set PE & PG bits mov %eax,%cr0 ret /* * Disable paging * void asm_disable_paging() */ asm_disable_paging: mov %eax,%cr3 mov %cr0,%eax xor $0x80000000,%eax # unset PG bit mov %eax,%cr0 ret 

Después de haber aprendido cómo crear el espacio de direcciones de los procesos, necesitamos administrar de alguna manera las páginas físicas, cuáles están ocupadas y cuáles están libres. Hay un mecanismo de mapa de bits para esto, un bit por página. No describiremos páginas de hasta 3 megabytes, porque pertenecen al núcleo y siempre están ocupadas. Comenzamos a seleccionar páginas de usuario de 3 a 4 megabytes.

 static u32 bitmap[MM_BITMAP_SIZE]; 


Las páginas físicas se asignan y desasignan de acuerdo con las siguientes funciones. De hecho, simplemente encontramos el bit deseado en el mapa en la dirección física de la página y viceversa. El inconveniente es que tenemos un tamaño de celda de memoria limitado, por lo que debe usar dos coordenadas: número de byte y número de bit.

 /* * Api - allocate pages */ extern void* mm_phys_alloc_pages(u_int count) { /* find free pages */ for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { bool is_found = true; for (int j = 0; j < count; ++j) { is_found = is_found && !mm_get_bit(i + j); } if (is_found) { /* occupy */ for (int j = 0; j < count; ++j) { assert(!mm_get_bit(i + j)); mm_set_bit(i + j); } return (void *)mm_get_addr(i); } } return null; } /* * Api - free page */ extern bool mm_phys_free_pages(void* ptr, u_int count) { size_t address = (size_t)ptr; assert(address >= MM_AREA_START); assert(address % MM_PAGE_SIZE == 0); /* find page */ for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { size_t addr = mm_get_addr(i); if (addr == address) { /* free pages */ for (int j = 0; j < count; ++j) { assert(mm_get_bit(i + j)); mm_clear_bit(i + j); } return true; } } return false; } 

Esto es suficiente para introducir soporte completo para la memoria virtual en su núcleo.

Referencias


Detalles y explicaciones en el video tutorial .

El código fuente en el repositorio de git (necesita la rama de la lección 6).

Referencias


1. James Molloy. Haga rodar su propio sistema operativo de clones UNIX de juguete.
2. Dientes. Ensamblador para DOS, Windows, Unix
3. Kalashnikov. ¡Ensamblador es fácil!
4. Tanenbaum. Sistemas operativos Implementación y desarrollo.
5. Robert Love. Kernel de Linux Descripción del proceso de desarrollo.

Source: https://habr.com/ru/post/467759/


All Articles