Análise do Código de Inicialização e Memória do Kernel Xv6

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

Tags: xv6 kernel gerenciamento de memória paginação RISC-V

Publicado em 8-3 05:26