Design de SO semelhante ao Unix - Espaço de Endereço Virtual (6)

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.

 /* * 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 */ } } 

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:

 /* * 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 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.

 /* * 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; } 

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:

 /* * 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; } 

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.

 /* * 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; } 

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.

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


All Articles