A Função da MMU na Abstração de Andereços
No desenvolvimento de sistemas operacionais como o HarmonyOS, a Unidade de Gerenciamento de Memória (MMU) atua como uma ponte essencial entre o software e o hardware. O sistema utiliza dois conceitos fundamentais de endereçamento:
- Endereço Virtual (VA): São endereços lineares atribuídos pelo compilador e pelo vinculador (linker). Cada processo enxerga um espaço de memória idêntico, permitindo o isolamento. A CPU opera sobre esses endereços, mas as requisições são interceptadas pela MMU.
- Endereço Físico (PA): Representa a localização real nos chips de RAM onde as instruções, constantes, variáveis globais e dados dinâmicos são armazenados.
A MMU utiliza tabelas de páginas (Page Tables) para converter VA em PA. Além da tradução, essas tabelas definem permissões de acesso (leitura, escrita, execução) e atributos de cache. O processo foca na manipulação dos 20 bits superiores do endereço virtual, enquanto os 12 bits enferiores (offset) permanecem inalterados, garantindo o alinhamento das páginas de 4KB.
Estrutura de Tabelas em Dois Níveis (L1 e L2)
O HarmonyOS implementa uma estrutura de tradução hierárquica para gerenciar o espaço de endereçamento de 32 bits (4GB). Essa estrutura é dividida em:
- Nível 1 (L1): Divide o espaço total em 4096 "seções" de 1MB cada. A base desta tabela é armazenada no registrador TTBR (Translation Table Base Register) do processador ARM.
- Nível 2 (L2): Subdivide uma seção de 1MB em 256 "páginas pequenas" de 4KB cada.
/* Tipos de descritores de primeiro nível no kernel */
#define TIPO_DESC_L1_INVALIDO (0x0 << 0)
#define TIPO_DESC_L1_TABELA_PAG (0x1 << 0)
#define TIPO_DESC_L1_SECAO (0x2 << 0)
#define MASCARA_DESC_L1 (0x3 << 0)
Consulta de Endereços Físicos
A função abaixo demonstra como o kernel traduz um endereço virtual em físico, verificando os níveis da tabela de páginas.
STATUS_T ConsultarMapeamentoMmu(const LosArchMmu *mmuContext, VADDR_T vAddr, PADDR_T *pAddr, UINT32 *flags) {
// Recupera a entrada de nível 1 usando os bits superiores do VA
PTE_T entradaL1 = BuscarEntradaL1(mmuContext->virtTtb, vAddr);
PTE_T entradaL2;
PTE_T *baseL2 = NULL;
if (ValidarEntradaInvalidaL1(entradaL1)) {
return LOS_ERRNO_VM_NOT_FOUND;
}
// Caso seja uma seção direta de 1MB
if (VerificarSeSecaoL1(entradaL1)) {
if (pAddr != NULL) {
*pAddr = EXTRAIR_ADDR_SECAO_L1(entradaL1) + (vAddr & (TAMANHO_SECAO_1MB - 1));
}
ExtrairFlagsMmu(entradaL1, flags);
}
// Caso a entrada aponte para uma tabela de segundo nível
else if (VerificarSeTabelaPaginaL1(entradaL1)) {
baseL2 = ObterBaseL2(entradaL1);
if (baseL2 == NULL) return LOS_ERRNO_VM_NOT_FOUND;
entradaL2 = BuscarEntradaL2(baseL2, vAddr);
if (VerificarPaginaPequenaL2(entradaL2)) {
if (pAddr != NULL) {
*pAddr = EXTRAIR_ADDR_PAGINA_L2(entradaL2) + (vAddr & (TAMANHO_PAGINA_4KB - 1));
}
ConverterFlagsL2(entradaL1, entradaL2, flags);
} else {
// Páginas grandes (64KB) não implementadas nesta versão
return LOS_ERRNO_VM_NOT_FOUND;
}
}
return LOS_OK;
}
Inicialização e Configuração de Segmentos do Kernel
Durante o boot, o kernel precisa configurar as permissões de suas próprias seções de memória (código, dados e BSS). Isso garante que a área de código seja apenas leitura e execução, enquanto a área de dados permita escrita.
STATIC VOID ConfigurarSecoesKernel(VOID) {
UINTPTR inicioTexto = (UINTPTR)&__text_start;
UINTPTR fimTexto = (UINTPTR)&__text_end;
UINTPTR inicioDados = (UINTPTR)&__ram_data_start;
UINTPTR fimBss = (UINTPTR)&__bss_end;
// Definição do mapeamento para o segmento de código (Text)
LosArchMmuInitMapping mapaTexto = {
.phys = SYS_MEM_BASE + inicioTexto - KERNEL_VMM_BASE,
.virt = inicioTexto,
.size = ALINHAR_4K(fimTexto - inicioTexto),
.flags = VM_MAP_PERM_READ | VM_MAP_PERM_EXECUTE,
.name = "k_text"
};
// Aplicação das configurações na MMU
LosVmSpace *espacoK = LOS_GetKVmSpace();
espacoK->archMmu.virtTtb = (PTE_T *)g_firstPageTable;
espacoK->archMmu.physTtb = TraduzirParaFisico(espacoK->archMmu.virtTtb);
// Mapeamento efetivo no hardware
LOS_ArchMmuMap(&espacoK->archMmu, mapaTexto.virt, mapaTexto.phys,
mapaTexto.size >> 12, mapaTexto.flags);
// Atualização do registrador de controle e limpeza de TLB
AtualizarTtbr0(espacoK->archMmu.physTtb);
LimparCacheTLB();
}
O Processo de Mapeamento (Map)
A operação de mapeamento consiste em preenhcer as tabelas de páginas com a relação correta entre endereços virtuais e físicos. Se o endereço estiver alinhado em 1MB, o kernel opta por mapeamentos de seção (L1) para maior eficiência. Caso contrário, utiliza o mapeamento granular de 4KB (L2).
status_t ExecutarMapeamentoMmu(LosArchMmu *mmu, VADDR_T vaddr, PADDR_T paddr, size_t paginas, UINT32 flags) {
INT32 mapeados = 0;
while (paginas > 0) {
// Tenta mapear como seção de 1MB se houver alinhamento e quantidade suficiente
if (ALINHADO_1MB(vaddr) && ALINHADO_1MB(paddr) && paginas >= 256) {
mapeados += CriarEntradaSecaoL1(mmu, &vaddr, &paddr, &paginas, flags);
} else {
// Mapeamento via tabela L2 (páginas de 4KB)
PTE_T entryL1 = ObterEntradaL1(mmu->virtTtb, vaddr);
if (VerificarInvalido(entryL1)) {
ProvisionarTabelaL2(mmu, &entryL1, vaddr);
}
mapeados += PreencherPaginasL2(entryL1, &vaddr, &paddr, &paginas, flags);
}
}
return mapeados;
}