O processo de inicialização no Xv6 é gerenciado pela função principal do kernel, que coordena a configuração essencial do sistema operacional.
Ao iniciar, a função principal executa rotinas para alocar memória física, estabelecer a tabela de páginas do kernel e ativar o mecanismo de paginação, garantindo que o hardware esteja devidamente preparado para operações de memória virtual.
Configuração da Memória Física
A rotina de inicialização da memória física conifgura um alocador de páginas. Ela define um lock para sincronização e prepara o espaço livre entre o final do kernel e um limite de memória física predefinido.
void inicializar_memoria() {
configurar_lock(&alocador.lock, "alocador");
void *inicio_livre = obter_fim_kernel();
void *fim_memoria = (void*)LIMITEMEMORIA;
liberar_intervalo(inicio_livre, fim_memoria);
}
A função auxiliar percorre o intervalo em blocos de tamanho de página, liberando cada bloco para uso futuro. As páginas são arredondadas para cima para alinhamento correto.
void liberar_intervalo(void *inicio, void *fim) {
char *ptr = (char*)ARREDONDAR_PAGINA((uint64)inicio);
for (; ptr + TAMANHO_PAGINA <= (char*)fim; ptr += TAMANHO_PAGINA)
liberar_pagina(ptr);
}
Ao liberar uma página, a função verifica o alinhamento e a faixa de endereço válido. Preenche a memória com valores não utilizados para detectar referências inválidas e adiciona a página à lista livre do alocador.
void liberar_pagina(void *endereco) {
struct no_livre *no;
if (((uint64)endereco % TAMANHO_PAGINA) != 0 || (char*)endereco < obter_fim_kernel() ||
(uint64)endereco >= LIMITEMEMORIA)
panic("endereco_invalido");
memset(endereco, 1, TAMANHO_PAGINA);
no = (struct no_livre*)endereco;
adquirir_lock(&alocador.lock);
no->proximo = alocador.lista_livre;
alocador.lista_livre = no;
liberar_lock(&alocador.lock);
}
Criação da Tabela de Páginas do Kernel
A tabela de páginas do kernel é construída para mapear endereços físicos a virtuais, incluindo dispositivos e pilhas de processos. A estrutura da tabela segue o formato de três níveis com nove bits por índice.
typedef uint64 *tabela_paginas;
tabela_paginas criar_tabela_kernel() {
tabela_paginas tabela = (tabela_paginas) alocar_pagina();
memset(tabela, 0, TAMANHO_PAGINA);
mapear_dispositivo(tabela, ENDERECO_UART, ENDERECO_UART, TAMANHO_PAGINA, PERMISSAO_LEITURA_ESCRITA);
configurar_pilhas_processos(tabela);
return tabela;
}
O mapeamento de endereços virtuais a físicos é feito por uma função que percorre a tabela de páginas, criando entradas conforme necessário. Cada entrada armazena o endereço físico deslocado e as permissões.
int adicionar_mapeamento(tabela_paginas tabela, uint64 virtual, uint64 fisico, uint64 tamanho, int permissoes) {
uint64 endereco, ultimo;
uint64 *entrada;
if ((virtual % TAMANHO_PAGINA) != 0 || (tamanho % TAMANHO_PAGINA) != 0 || tamanho == 0)
return -1;
endereco = virtual;
ultimo = virtual + tamanho - TAMANHO_PAGINA;
for (;;) {
entrada = buscar_entrada(tabela, endereco, 1);
if (!entrada)
return -1;
if (*entrada & BIT_VALIDO)
return -1;
*entrada = converter_para_entrada(fisico) | permissoes | BIT_VALIDO;
if (endereco == ultimo)
break;
endereco += TAMANHO_PAGINA;
fisico += TAMANHO_PAGINA;
}
return 0;
}
A função auxiliar para navegar na tabela de páginas percorre os níveis, alocando novas tabelas se necessário e retornando um ponteiro para a entrada do nível mais baixo.
uint64 *buscar_entrada(tabela_paginas tabela, uint64 virtual, int alocar) {
if (virtual >= MAXIMO_ENDERECO_VIRTUAL)
return NULL;
for (int nivel = 2; nivel > 0; nivel--) {
uint64 *entrada = &tabela[obter_indice(nivel, virtual)];
if (*entrada & BIT_VALIDO) {
tabela = (tabela_paginas)extrair_fisico(*entrada);
} else {
if (!alocar || (tabela = (uint64*)alocar_pagina()) == NULL)
return NULL;
memset(tabela, 0, TAMANHO_PAGINA);
*entrada = converter_para_entrada((uint64)tabela) | BIT_VALIDO;
}
}
return &tabela[obter_indice(0, virtual)];
}
As pilhas de processo são mapeadas em endereços virtuais altos, com páginas de guarda para evitar transbordamento.
void configurar_pilhas_processos(tabela_paginas tabela) {
struct processo *proc;
for (proc = tabela_processos; proc < &tabela_processos[MAXIMO_PROCESSOS]; proc++) {
void *fisico = alocar_pagina();
if (!fisico)
panic("falha_alocacao");
uint64 virtual = calcular_pilha_virtual(proc - tabela_processos);
mapear_dispositivo(tabela, virtual, (uint64)fisico, TAMANHO_PAGINA, PERMISSAO_LEITURA_ESCRITA);
}
}
Ativação da Paginação
Após configurar a tabela de páginas, o registro de controle de paginação é atualizado para usar o endereço da tabela do kernel. Isso requer uma instrução especial para invalidar cache de endereços traduzidos (TLB) e garantir consistência.
void ativar_paginacao() {
sincronizar_tlb();
escrever_reg_satp(criar_satp(tabela_kernel));
sincronizar_tlb();
}
A sincronização do TLB é realizada por uma instrução de assembly que limpa todas as entradas armazenadas em cache, forçando o hardware a reconsultar a tabela de páginas para acessos futuros.
static inline void sincronizar_tlb() {
asm volatile("sfence.vma zero, zero");
}
Gerenciamento Dinâmico de Memória
O alocador de memória física opera com uma lista livre protegida por um lock, permitindo alocação e liberação de páginas de 4KB. Ao liberar memória, os dados são sobrescritos para mitigar problemas de segurança e facilitar a detecção de erros.
Funções como sbrk ajustam o tamanho da memória de um processo, enquanto exec carrega novos programas no espaço de endereços. Rotinas adicionais facilitam a cópia de dados entre espaço do usuário e kernel, verificando limites e permissões em cada transferência.
int copiar_para_kernel(tabela_paginas tabela, char *destino, uint64 origem, uint64 tamanho) {
uint64 n, bloco_virtual, bloco_fisico;
while (tamanho > 0) {
bloco_virtual = ALINHAR_PAGINA_BAIXO(origem);
bloco_fisico = obter_fisico(tabela, bloco_virtual);
if (!bloco_fisico)
return -1;
n = TAMANHO_PAGINA - (origem - bloco_virtual);
if (n > tamanho)
n = tamanho;
memmove(destino, (void *)(bloco_fisico + (origem - bloco_virtual)), n);
tamanho -= n;
destino += n;
origem = bloco_virtual + TAMANHO_PAGINA;
}
return 0;
}
Para cópias de strings, a operação continua até encontrar um caractere nulo ou atingir o limite máximo, garantindo segurança na manipulação de dados do usuário.
int copiar_string_para_kernel(tabela_paginas tabela, char *destino, uint64 origem, uint64 maximo) {
uint64 n, bloco_virtual, bloco_fisico;
int encontrou_nulo = 0;
while (!encontrou_nulo && maximo > 0) {
bloco_virtual = ALINHAR_PAGINA_BAIXO(origem);
bloco_fisico = obter_fisico(tabela, bloco_virtual);
if (!bloco_fisico)
return -1;
n = TAMANHO_PAGINA - (origem - bloco_virtual);
if (n > maximo)
n = maximo;
char *ptr = (char *)(bloco_fisico + (origem - bloco_virtual));
while (n > 0) {
if (*ptr == '\0') {
*destino = '\0';
encontrou_nulo = 1;
break;
}
*destino = *ptr;
--n;
--maximo;
ptr++;
destino++;
}
origem = bloco_virtual + TAMANHO_PAGINA;
}
return encontrou_nulo ? 0 : -1;
}
A operação inversa, de cópia para o espaço do usuário, valida permissões de escrita e presença na tabela de páginas antes de cada transferência.
int copiar_para_usuario(tabela_paginas tabela, uint64 destino, char *origem, uint64 tamanho) {
uint64 n, bloco_virtual, bloco_fisico;
uint64 *entrada;
while (tamanho > 0) {
bloco_virtual = ALINHAR_PAGINA_BAIXO(destino);
if (bloco_virtual >= MAXIMO_ENDERECO_VIRTUAL)
return -1;
entrada = buscar_entrada(tabela, bloco_virtual, 0);
if (!entrada || !(*entrada & BIT_VALIDO) || !(*entrada & BIT_USUARIO) || !(*entrada & BIT_ESCRITA))
return -1;
bloco_fisico = extrair_fisico(*entrada);
n = TAMANHO_PAGINA - (destino - bloco_virtual);
if (n > tamanho)
n = tamanho;
memmove((void *)(bloco_fisico + (destino - bloco_virtual)), origem, n);
tamanho -= n;
origem += n;
destino = bloco_virtual + TAMANHO_PAGINA;
}
return 0;
}