No artigo anterior, examinamos os conceitos básicos de trabalho no modo protegido IA-32. Hoje é hora de aprender a trabalhar com o espaço de endereço virtual.
Sumário
Construa o sistema (marca, gcc, gás). Inicialização inicial (inicialização múltipla). Iniciar (qemu). Biblioteca C (strcpy, memcpy, strext).
Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Construindo a biblioteca no modo kernel e no modo de aplicativo do usuário.
O log do sistema do kernel. Memória de vídeo Saída para o terminal (kprintf, kpanic, kassert).
Memória dinâmica, heap (kmalloc, kfree).
Organização da memória e manipulação de interrupções (GDT, IDT, PIC, syscall). Exceções
Memória virtual (diretório e tabela de páginas).Processo. Planejador Multitarefa. Chamadas do sistema (interrupção, saída, ps).
O sistema de arquivos do kernel (initrd), elf e seus internos. Chamadas do sistema (exec).
Drivers de dispositivo de caracteres. Chamadas do sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).
Shell como um programa completo para o kernel.
Modo de proteção do usuário (anel3). Segmento de status da tarefa (tss).
Memória virtual
A memória virtual é necessária para que cada processo possa ser isolado de outro, ou seja, não conseguiu detê-lo. Se não houvesse memória virtual, teríamos que carregar arquivos elf em diferentes endereços na memória a cada vez. Mas como você sabe, os arquivos executáveis podem conter links para endereços específicos (absolutos). Portanto, ao compilar elf, já se sabe em qual endereço da memória ele será carregado (consulte o script do vinculador). Portanto, não podemos carregar dois arquivos elf sem memória virtual. Porém, mesmo com a memória virtual ativada, existem bibliotecas dinâmicas (como .so) que podem ser carregadas em qualquer endereço. Eles podem ser baixados em qualquer endereço devido ao fato de terem uma seção realocada. Nesta seção, todos os locais onde o endereçamento absoluto é usado são registrados, e o kernel, ao carregar um arquivo elf, deve corrigir esses endereços com canetas, ou seja, adicione a eles a diferença entre o endereço de download real e o desejado.
Consideraremos trabalhar com páginas de 4 kilobytes. Nesta situação, podemos endereçar até 4 megabytes de RAM. Isso é o suficiente para nós. O mapa de endereços ficará assim:
0-1 mb : não toque.
1-2 mb : dados de código e kernel.
2-3 mb : um monte de núcleos.
3-4 mb : páginas personalizadas de arquivos elf carregadas.
O endereço linear (obtido do modelo simples) quando a paginação da página está ativada não é igual ao físico. Em vez disso, o endereço é dividido pelo deslocamento (bits baixos), o índice das entradas na tabela de páginas e o índice do diretório da página (bits altos). Cada processo terá seu próprio diretório de páginas e, consequentemente, tabelas de páginas. É isso que permite organizar um espaço de endereço virtual.
A entrada do diretório da página é semelhante a esta:
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);
O elemento da tabela da página fica assim:
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 o kernel, descreveremos o diretório e a tabela de páginas como variáveis estáticas. É verdade que há um requisito de que eles estejam alinhados na borda da 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));
Vamos seguir o caminho simples e tornar todo o espaço físico do endereço acessível ao kernel. O nível de privilégio das páginas do kernel deve ser um supervisor, para que ninguém suba nelas. Processos leves que devem ser executados no modo kernel compartilharão o mesmo espaço de endereço com o kernel. Com esse processo, teremos uma fila de execução pendente para lidar com interrupções pendentes. Mas mais sobre isso na lição sobre drivers de dispositivos de caracteres. Ainda precisamos realizar multitarefa antes de considerar este tópico.
Crie um diretório de páginas do kernel e uma tabela de páginas correspondente. Quando o kernel é inicializado, ele estará ativo.
extern void mmu_init() { memset(&kpage_directory, 0, sizeof(struct page_directory_entry_t)); 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; kpage_directory.present = 1; kpage_directory.read_write = 1; kpage_directory.user_supervisor = 1; kpage_directory.write_through = 1; kpage_directory.page_table_addr = (size_t)kpage_table >> 12; 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; kpage_table[i].read_write = 1; kpage_table[i].user_supervisor = 1; kpage_table[i].write_through = 1; kpage_table[i].page_phys_addr = (i * 4096) >> 12; } }
Quando carregamos os arquivos elf, precisamos criar um diretório de páginas para o processo do usuário. Você pode fazer isso com a seguinte função:
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; upage_dir->present = 1; upage_dir->read_write = 1; upage_dir->user_supervisor = 0; upage_dir->write_through = 1; upage_dir->page_table_addr = (size_t)page_table >> 12; return upage_dir; }
Por padrão, a tabela de páginas do processo conterá páginas do kernel e entradas vazias para futuras páginas do processo, ou seja, registros com o sinalizador atual limpo e o endereço da página física em 0.
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); memcpy(upage_table, kpage_table, sizeof(struct page_table_entry_t) * MMU_KERNEL_PAGES_COUNT); 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; current->read_write = 1; current->user_supervisor = 0; current->write_through = 1; current->page_phys_addr = 0; } return upage_table; }
Precisamos aprender como adicionar novas páginas físicas à tabela de páginas do processo, pois não haverá nenhuma por padrão. Precisamos disso ao carregar arquivos elf na memória, quando carregamos segmentos descritos nos cabeçalhos do programa. A função nos ajudará com isso:
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) { 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; current->user_supervisor = 0; current->write_through = 1; current->page_phys_addr = (size_t)phys_addr >> 12; return true; } return false; }
O modo de paginação é ativado e desativado um pouco no registro de sinalizador do processador.
/* * 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
Depois de aprendermos a criar o espaço de endereço dos processos, precisamos gerenciar de alguma forma as páginas físicas, qual delas está ocupada e qual é gratuita. Existe um mecanismo de bitmap para isso, um bit por página. Não descreveremos páginas de até 3 megabytes, porque elas pertencem ao kernel e estão sempre ocupadas. Começamos a selecionar páginas de usuário de 3 a 4 megabytes.
static u32 bitmap[MM_BITMAP_SIZE];
As páginas físicas são alocadas e desalocadas de acordo com as seguintes funções. De fato, simplesmente encontramos o bit desejado no mapa no endereço físico da página e vice-versa. O inconveniente é que temos um tamanho de célula de memória limitado, portanto, você deve usar duas coordenadas: número de bytes e número de bits.
extern void* mm_phys_alloc_pages(u_int count) { 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) { 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; } 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); for (int i = 0; i < MM_DYNAMIC_PAGES_COUNT; ++i) { size_t addr = mm_get_addr(i); if (addr == address) { for (int j = 0; j < count; ++j) { assert(mm_get_bit(i + j)); mm_clear_bit(i + j); } return true; } } return false; }
Isso é suficiente para introduzir suporte completo à memória virtual no seu kernel.
Referências
Detalhes e explicações no
tutorial em
vídeo .
O código fonte
no repositório git (você precisa da ramificação lição6).
Referências
1. James Molloy. Role seu próprio sistema operacional clone do UNIX de brinquedo.
2. Dentes. Assembler para DOS, Windows, Unix
3. Kalashnikov. Assembler é fácil!
4. Tanenbaum. Sistemas operacionais. Implementação e desenvolvimento.
5. Robert Love. Kernel Linux Descrição do processo de desenvolvimento.