Neste artigo, descobriremos como implementar o suporte à memória da página em nosso núcleo. Primeiro, estudaremos vários métodos para que os quadros da tabela de páginas físicas fiquem disponíveis para o kernel e discutiremos suas vantagens e desvantagens. Em seguida, implementamos a função de conversão de endereço e a função de criar um novo mapeamento.
Esta série de artigos publicados no 
GitHub . Se você tiver alguma dúvida ou problema, abra o ticket correspondente lá. Todas as fontes para o artigo estão 
neste segmento .
Outro artigo sobre paginação? 
Se você seguir esse ciclo, verá o artigo “Memória da página: nível avançado” no final de janeiro. Mas fui criticado por tabelas de páginas recursivas. Portanto, decidi reescrever o artigo, usando uma abordagem diferente para acessar quadros.Aqui está uma nova opção. O artigo ainda explica como as tabelas de páginas recursivas funcionam, mas usamos uma implementação mais simples e poderosa. Não excluiremos o artigo anterior, mas marcaremos como desatualizado e não o atualizaremos. 
Espero que você goste da nova opção!Conteúdo
1. Introdução
No 
último artigo, aprendemos sobre os princípios da memória de paginação e como funcionam as tabelas de página de quatro níveis no 
x86_64 . Também descobrimos que o carregador já configurou a hierarquia da tabela de páginas para nosso kernel, portanto, o kernel é executado em endereços virtuais. Isso aumenta a segurança porque o acesso não autorizado à memória causa uma falha na página em vez de alterar aleatoriamente a memória física.
O artigo acabou não conseguindo acessar as tabelas de páginas do nosso kernel, porque elas são armazenadas na memória física e o kernel já está sendo executado em endereços virtuais. Aqui continuamos o tópico e exploramos diferentes opções para acessar os quadros da tabela de páginas no kernel. Discutiremos as vantagens e desvantagens de cada um deles e, em seguida, escolheremos a opção apropriada para nosso núcleo.
O suporte ao carregador de inicialização é necessário, portanto, vamos configurá-lo primeiro. Em seguida, implementamos uma função que percorre toda a hierarquia de tabelas de páginas, a fim de converter endereços virtuais em físicos. Por fim, aprenderemos como criar novos mapeamentos em tabelas de páginas e como encontrar quadros de memória não utilizados para criar novas tabelas.
Atualizações de dependência
Este artigo requer que você registre o 
bootloader versão 0.4.0 ou superior e 
x86_64 versão 0.5.2 ou superior nas dependências. Você pode atualizar as dependências no 
Cargo.toml :
 [dependencies] bootloader = "0.4.0" x86_64 = "0.5.2" 
Para alterações nessas versões, consulte 
o log do carregador de inicialização e o 
log x86_64 .
Acesso a tabelas de páginas
Acessar tabelas de páginas do kernel não é tão fácil quanto parece. Para entender o problema, dê uma olhada na hierarquia de tabela de quatro níveis do artigo anterior:
O importante é que cada entrada da página armazene o endereço 
físico da próxima tabela. Isso evita a tradução desses endereços, o que reduz o desempenho e leva facilmente a loops sem fim.
O problema é que não podemos acessar diretamente endereços físicos do kernel, pois ele também funciona em endereços virtuais. Por exemplo, quando vamos para o endereço 
4 KiB , obtemos acesso ao endereço 
virtual 4 KiB , e não ao endereço 
físico onde a tabela de páginas do 4º nível está armazenada. Se queremos acessar o endereço físico de 
4 KiB , precisamos usar um endereço virtual, que é traduzido para ele.
Portanto, para acessar os quadros das tabelas de páginas, é necessário mapear algumas páginas virtuais para esses quadros. Existem diferentes maneiras de criar esses mapeamentos.
Mapeamento de identidade
Uma solução simples é a 
exibição idêntica de todas as tabelas de páginas .
Neste exemplo, vemos a exibição idêntica de quadros. Os endereços físicos das tabelas de páginas são ao mesmo tempo endereços virtuais válidos, para que possamos acessar facilmente as tabelas de páginas de todos os níveis, começando com o registro CR3.
No entanto, essa abordagem confunde o espaço de endereço virtual e dificulta a descoberta de grandes áreas contíguas de memória livre. Digamos que queremos criar uma área de memória virtual de 1000 KiB na figura acima, por exemplo, para 
exibir um arquivo na memória . Não podemos começar com a região de 
28 KiB , porque ela fica em uma página já ocupada em 
1004 KiB . Portanto, você precisará procurar mais até encontrarmos um fragmento grande e adequado, por exemplo, com 
1008 KiB . Há o mesmo problema de fragmentação da memória segmentada.
Além disso, a criação de novas tabelas de páginas é muito mais complicada, pois precisamos encontrar quadros físicos cujas páginas correspondentes ainda não foram usadas. Por exemplo, para o nosso arquivo, reservamos uma área de 1000 KiB de memória 
virtual , começando no endereço 
1008 KiB . Agora não podemos mais usar nenhum quadro com um endereço físico entre 
1000 KiB e 
2008 KiB , porque ele não pode ser exibido de forma idêntica.
Mapa de deslocamento fixo
Para evitar sobrecarregar o espaço de endereço virtual, você pode exibir as tabelas de páginas em uma 
área de memória separada . Portanto, em vez de identificar o mapeamento, mapeamos quadros com um deslocamento fixo no espaço de endereço virtual. Por exemplo, o deslocamento pode ser 10 TiB:

Ao alocar esse intervalo de memória virtual exclusivamente para a exibição de tabelas de páginas, evitamos os problemas de exibição idêntica. A reserva de uma área tão grande do espaço de endereço virtual só é possível se o espaço de endereço virtual for muito maior que o tamanho da memória física. No 
x86_64 isso não é um problema, porque o espaço de endereço de 48 bits é de 256 TiB.
Mas essa abordagem tem a desvantagem de que, ao criar cada tabela de páginas, você precisa criar um novo mapeamento. Além disso, ele não permite o acesso a tabelas em outros espaços de endereço, o que seria útil ao criar um novo processo.
Mapeamento completo da memória física
Podemos resolver esses problemas 
exibindo toda a memória física , e não apenas os quadros da tabela de página:

Essa abordagem permite que o kernel acesse memória física arbitrária, incluindo quadros de tabela de página de outros espaços de endereço. Um intervalo de memória virtual está reservado com o mesmo tamanho de antes, mas somente não há páginas sem correspondência nele.
A desvantagem dessa abordagem é que são necessárias tabelas de páginas adicionais para exibir a memória física. Essas tabelas de páginas devem ser armazenadas em algum lugar, para que elas usem parte da memória física, o que pode ser um problema em dispositivos com uma pequena quantidade de RAM.
No entanto, no x86_64, podemos usar 2 
páginas MiB 
enormes para exibir, em vez do tamanho padrão de 4 KiB. Portanto, para exibir 32 GiB de memória física, apenas 132 KiB por tabela de página são necessárias: apenas uma tabela de terceiro nível e 32 tabelas de segundo nível. Páginas enormes também são armazenadas em cache de maneira mais eficiente, porque usam menos entradas no buffer de tradução dinâmico (TLB).
Exibição temporária
Para dispositivos com muito pouca memória física, você só pode 
exibir tabelas de páginas temporariamente quando precisar acessá-las. Para comparações temporárias, é necessária uma exibição idêntica apenas da tabela de primeiro nível:
Nesta figura, uma tabela de nível 1 gerencia os 2 primeiros MiB de espaço de endereço virtual. Isso é possível porque o acesso a partir do registro CR3 é feito através de zero entradas nas tabelas dos níveis 4, 3 e 2. O registro com o índice 
8 converte a página virtual em 
32 KiB em um quadro físico em 
32 KiB , identificando a própria tabela de nível 1. Na figura, isso é mostrado por uma seta horizontal.
Ao escrever na tabela de nível 1 mapeada de forma idêntica, nosso kernel pode criar até 511 comparações de tempo (512 menos o registro necessário para o mapeamento de identidade). No exemplo acima, o kernel cria duas comparações de tempo:
- Mapeando uma entrada nula em uma tabela de nível 1 para um quadro em 24 KiB. Isso cria um mapeamento temporário da página virtual em0 KiBpara o quadro físico da tabela de nível 2 da página indicado pela seta pontilhada.
- Combine o nono registro de uma tabela de nível 1 com um quadro em 4 KiB. Isso cria um mapeamento temporário da página virtual em36 KiBpara o quadro físico da tabela de nível 4 da página indicada pela seta pontilhada.
Agora o kernel pode acessar uma tabela de nível 2 gravando em uma página que começa em 
0 KiB e uma tabela de nível 4 gravando em uma página que começa em 
33 KiB .
Portanto, o acesso a um quadro arbitrário da tabela de páginas com mapeamentos temporários consiste nas seguintes ações:
- Encontre uma entrada gratuita na tabela de nível 1 exibida de forma idêntica.
 
- Mapeie essa entrada para o quadro físico da tabela de páginas que queremos acessar.
 
- Acesse esse quadro através da página virtual associada à entrada.
 
- Defina o registro novamente como não utilizado, removendo o mapeamento temporário.
Com essa abordagem, o espaço de endereço virtual permanece limpo, pois as mesmas 512 páginas virtuais são usadas constantemente. A desvantagem é uma certa dificuldade, especialmente porque uma nova comparação pode exigir a alteração de vários níveis da tabela, ou seja, precisamos repetir o processo descrito várias vezes.
Tabelas de páginas recursivas
Outra abordagem interessante que não requer tabelas de páginas adicionais é 
a correspondência recursiva .
A idéia é traduzir alguns registros da tabela de quarto nível para ela mesma. Assim, reservamos uma parte do espaço de endereço virtual e mapeamos todos os quadros de tabela atuais e futuros para esse espaço.
Vejamos um exemplo para entender como tudo isso funciona:
A única diferença do exemplo no início do artigo é um registro adicional com o índice 
511 na tabela de nível 4, que é mapeado para o quadro físico 
4 KiB , localizado nesta tabela em si.
Quando a CPU entra nesse registro, ela não se refere à tabela de nível 3, mas novamente à tabela de nível 4. Isso é semelhante a uma função recursiva que se chama. É importante que o processador assuma que cada entrada na tabela de nível 4 aponta para uma tabela de nível 3. Portanto, agora ela trata a tabela de nível 4 como uma tabela de nível 3. Isso funciona porque as tabelas de todos os níveis em x86_64 têm a mesma estrutura.
Seguindo um registro recursivo uma ou mais vezes antes de iniciar a conversão real, podemos reduzir efetivamente o número de níveis pelos quais o processador passa. Por exemplo, se seguirmos o registro recursivo uma vez e depois formos para a tabela de nível 3, o processador achará que a tabela de nível 3 é uma tabela de nível 2. Seguindo em frente, ele considera a tabela de nível 2 como uma tabela de nível 1 e a tabela de nível 1 como mapeada quadro na memória física. Isso significa que agora podemos ler e gravar na tabela de nível 1 da página, porque o processador pensa que esse é um quadro mapeado. A figura abaixo mostra as cinco etapas dessa tradução:
Da mesma forma, podemos seguir uma entrada recursiva duas vezes antes de iniciar a conversão para reduzir o número de níveis passados para dois:
Vamos seguir este procedimento passo a passo. Primeiro, a CPU segue uma entrada recursiva na tabela de nível 4 e pensa que alcançou a tabela de nível 3. Em seguida, segue o registro recursivo novamente e pensa que alcançou o nível 2. Mas, na realidade, ainda está no nível 4. Em seguida, a CPU vai para o novo endereço e entra na tabela de nível 3, mas acha que já está na tabela de nível 1. Por fim, no próximo ponto de entrada da tabela de nível 2, o processador acha que acessou o quadro de memória física. Isso nos permite ler e gravar em uma tabela de nível 2.
Também são acessadas as tabelas dos níveis 3 e 4. Para acessar a tabela do nível 3, seguimos um registro recursivo três vezes: o processador pensa que já está na tabela do nível 1 e, na próxima etapa, atingimos o nível 3, que a CPU considera como um quadro mapeado. Para acessar a tabela de nível 4, basta seguir o registro recursivo quatro vezes até que o processador processe a tabela de nível 4 como um quadro mapeado (em azul na figura abaixo).
O conceito é difícil de entender a princípio, mas, na prática, funciona muito bem.
Cálculo de endereço
Assim, podemos acessar tabelas de todos os níveis seguindo um registro recursivo uma ou mais vezes. Como os índices em tabelas de quatro níveis são derivados diretamente do endereço virtual, endereços virtuais especiais devem ser criados para esse método. Como lembramos, os índices da tabela de páginas são extraídos do endereço da seguinte maneira:
Suponha que desejamos acessar uma tabela de nível 1 que exibe uma página específica. Como aprendemos acima, você precisa passar por um registro recursivo uma vez e depois pelos índices do 4º, 3º e 2º níveis. Para fazer isso, movemos todos os blocos de endereço um bloco para a direita e configuramos o índice do registro recursivo para o local do índice inicial do nível 4:
Para acessar a tabela de nível 2 desta página, movemos todos os blocos de índice dois blocos para a direita e configuramos o índice recursivo para o local dos dois blocos de origem: nível 4 e nível 3:
Para acessar a tabela de nível 3, fazemos o mesmo, apenas mudamos para a direita, já com três blocos de endereços.
Finalmente, para acessar a tabela de nível 4, mova todos os quatro blocos para a direita.
Agora você pode calcular endereços virtuais para tabelas de páginas dos quatro níveis. Podemos até calcular um endereço que aponte exatamente para uma entrada específica da tabela de páginas multiplicando seu índice por 8, o tamanho da entrada da tabela de páginas.
A tabela abaixo mostra a estrutura de endereços para acessar vários tipos de quadros:
| Endereço virtual para | Estrutura de endereço ( octal ) | 
|---|
| Page | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE | 
| Entrada na tabela de nível 1 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD | 
| Entrada em uma tabela de nível 2 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC | 
| Entrada em uma tabela de nível 3 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB | 
| Entrada na tabela de nível 4 | 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA | 
Aqui, 
é o índice de nível 4, 
é o nível 3, 
é o nível 2 e 
DDD é o índice de nível 1 para o quadro exibido, 
EEEE é o seu deslocamento. 
RRR é o índice do registro recursivo. Um índice (três dígitos) é convertido em um deslocamento (quatro dígitos) multiplicando por 8 (o tamanho da entrada da tabela da página). Com esse deslocamento, o endereço resultante aponta diretamente para a entrada da tabela de páginas correspondente.
SSSS são bits de expansão do dígito assinado, ou seja, são cópias do bit 47. Esse é um requisito especial para endereços válidos na arquitetura x86_64, que discutimos em um 
artigo anterior .
Os endereços são 
octais , pois cada caractere octal representa três bits, o que permite separar claramente os índices de 9 bits das tabelas em diferentes níveis. Isso não é possível no sistema hexadecimal, onde cada caractere representa quatro bits.
Código de ferrugem
Você pode construir esses endereços no código Rust usando operações bit a bit:
 
Esse código pressupõe que um mapeamento recursivo do último registro de nível 4 com o índice 
0o777 (511) seja correspondido recursivamente. Atualmente não é o caso, portanto o código ainda não funcionará. Veja abaixo como instruir o carregador a configurar um mapeamento recursivo.
Como alternativa à execução manual de operações bit a bit, você pode usar o tipo 
RecursivePageTable da caixa 
x86_64 , que fornece abstrações seguras para várias operações da tabela. Por exemplo, o código abaixo mostra como converter um endereço virtual em seu endereço físico correspondente:
 
Novamente, esse código requer um mapeamento recursivo correto. Com esse mapeamento, o 
level_4_table_addr ausente 
level_4_table_addr calculado como no primeiro exemplo de código.
O mapeamento recursivo é um método interessante que mostra o quão poderosa pode ser a correspondência através de uma única tabela. É relativamente fácil de implementar e requer apenas configuração mínima (apenas uma entrada recursiva), portanto, essa é uma boa opção para as primeiras experiências.
Mas tem algumas desvantagens:
- Uma grande quantidade de memória virtual (512 GiB). Isso não é um problema em um espaço de endereço grande de 48 bits, mas pode levar a um comportamento de cache abaixo do ideal.
 
- Facilmente, dá acesso apenas ao espaço de endereço atualmente ativo. O acesso a outros espaços de endereço ainda é possível alterando a entrada recursiva, mas a correspondência temporária é necessária para a alternância. Descrevemos como fazer isso em um artigo anterior (obsoleto).
 
- Depende muito do formato da tabela de páginas x86 e pode não funcionar em outras arquiteturas.
Suporte ao carregador de inicialização
Todas as abordagens descritas acima requerem alterações nas tabelas de páginas e nas configurações correspondentes. Por exemplo, para mapear a memória física de forma idêntica ou recursiva, mapeie registros de uma tabela de quarto nível. O problema é que não podemos fazer essas configurações sem acessar as tabelas de páginas.
Então, preciso de ajuda do gerenciador de inicialização. Ele tem acesso a tabelas de páginas, para que ele possa criar qualquer exibição necessária. Na sua implementação atual, o caixote do 
bootloader suporta as duas abordagens acima usando 
funções de carga :
- A função map_physical_memorymapeia a memória física completa em algum lugar do espaço de endereço virtual. Assim, o kernel obtém acesso a toda a memória física e pode aplicar uma abordagem com a exibição da memória física completa .
 
- Usando a função recursive_page_table, o carregador exibe recursivamente uma entrada da tabela de página de quarto nível. Isso permite que o kernel funcione de acordo com o método descrito na seção "Tabelas de páginas recursivas" .
Para o nosso kernel, escolhemos a primeira opção, porque é uma abordagem simples, independente de plataforma e mais poderosa (também dá acesso a outros quadros, não apenas a tabelas de páginas). Para suporte do gerenciador de inicialização, inclua a função em suas dependênciasmap_physical_memory: [dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]} 
Se esse recurso estiver ativado, o carregador de inicialização mapeia a memória física completa para algum intervalo não utilizado de endereços virtuais. Para passar um intervalo de endereços virtuais para o kernel, o carregador de inicialização passa a estrutura das informações de inicialização .Informações de inicialização
O engradado bootloaderdefine a estrutura do BootInfo com todas as informações passadas ao kernel. A estrutura ainda está sendo finalizada, portanto, pode haver algumas falhas ao atualizar para versões futuras incompatíveis com o semver . Atualmente, a estrutura possui dois campos: memory_mape physical_memory_offset:- O campo memory_mapfornece uma visão geral da memória física disponível. Ele informa ao kernel quanta memória física está disponível no sistema e quais áreas de memória estão reservadas para dispositivos como o VGA. Um cartão de memória pode ser solicitado no firmware do BIOS ou UEFI, mas apenas no início do processo de inicialização. Por esse motivo, o carregador deve fornecê-lo, porque o kernel não poderá mais receber essas informações. Um cartão de memória será útil mais adiante neste artigo.
 
- physical_memory_offsetrelata o endereço inicial virtual do mapeamento de memória física. Adicionando esse deslocamento ao endereço físico, obtemos o endereço virtual correspondente. Isso dá acesso do kernel à memória física arbitrária.
O carregador passa a estruturaBootInfopara o kernel como um argumento &'static BootInfopara a função _start. Adicione: 
É importante especificar o tipo de argumento correto, pois o compilador não conhece o tipo de assinatura correto da nossa função de ponto de entrada.Macro do ponto de entrada
Como a função _starté chamada externamente a partir do carregador, a assinatura da função não é verificada. Isso significa que podemos aceitar argumentos arbitrários sem erros de compilação, mas isso trava ou causa um comportamento indefinido do tempo de execução.Para garantir que a função do ponto de entrada sempre tenha a assinatura correta, a caixa bootloaderfornece uma macro entry_point. Reescrevemos nossa função usando esta macro: 
Você não precisa mais usar o ponto de entrada extern "C"ou no_mangle, já que a macro define para nós o ponto de entrada real do nível inferior _start. A função kernel_mainagora se tornou uma função Rust completamente normal, para que possamos escolher um nome arbitrário para ela. O importante é que ele seja verificado por tipo; portanto, se você usar a assinatura errada, por exemplo, adicionando um argumento ou alterando seu tipo, ocorrerá um erro de compilaçãoImplementação
Agora temos acesso à memória física e podemos finalmente começar a implementação do sistema. Primeiro, considere as tabelas de páginas ativas atuais nas quais o kernel é executado. Na segunda etapa, crie uma função de conversão que retorne o endereço físico para o qual esse endereço virtual está mapeado. Na última etapa, tentaremos modificar as tabelas de páginas para criar um novo mapeamento.Primeiro, crie um novo módulo no código memory: 
Para o módulo, crie um arquivo vazio src/memory.rs.Acesso a tabelas de páginas
No final do artigo anterior, tentamos examinar a tabela de páginas em que o kernel funciona, mas não conseguimos acessar o quadro físico apontado pelo registro CR3. Agora podemos continuar trabalhando a partir deste local: a função active_level_4_tableretornará um link para a tabela ativa de páginas do quarto nível: 
Primeiro, lemos o quadro físico da tabela ativa do 4º nível no registro CR3. Em seguida, pegamos seu endereço físico inicial e o convertemos em um endereço virtual adicionando physical_memory_offset. Por fim, converta o endereço em um ponteiro bruto *mut PageTablepelo método as_mut_ptre, em seguida, crie um link com segurança &mut PageTable. Em &mutvez disso &, criamos o link , porque mais adiante neste artigo modificaremos essas tabelas de páginas.Não há necessidade de inserir um bloco não seguro aqui, porque Rust considera todo o corpo unsafe fncomo um bloco grande e não seguro. Isso aumenta os riscos, porque é possível introduzir acidentalmente uma operação insegura nas linhas anteriores. Também dificulta a detecção de operações não seguras. Um RFC já foi criado para modificar esse comportamento do Rust.Agora podemos usar esta função para gerar os registros da tabela do quarto nível: 
Passamos physical_memory_offsetno campo correspondente da estrutura BootInfo. Em seguida, usamos uma função iterpara percorrer as entradas da tabela de páginas e um combinador enumeratepara adicionar um índice ia cada elemento. Somente entradas não vazias são exibidas, porque todas as 512 entradas não cabem na tela.Quando executamos o código, vemos o resultado: Vemos vários registros não vazios que são mapeados para várias tabelas de terceiro nível. Muitas áreas de memória são usadas porque são necessárias áreas separadas para código do kernel, pilha do kernel, tradução da memória física e informações de inicialização.Para percorrer as tabelas de páginas e olhar para a tabela de terceiro nível, podemos converter novamente o quadro exibido em um endereço virtual:
Vemos vários registros não vazios que são mapeados para várias tabelas de terceiro nível. Muitas áreas de memória são usadas porque são necessárias áreas separadas para código do kernel, pilha do kernel, tradução da memória física e informações de inicialização.Para percorrer as tabelas de páginas e olhar para a tabela de terceiro nível, podemos converter novamente o quadro exibido em um endereço virtual: 
Para visualizar as tabelas do segundo e primeiro níveis, repita esse processo, respectivamente, para registros do terceiro e do segundo níveis. Como você pode imaginar, a quantidade de código está crescendo muito rapidamente, portanto não publicaremos a lista completa.A travessia manual de tabelas é interessante porque ajuda a entender como o processador converte endereços. Mas geralmente só estamos interessados em exibir um endereço físico para um endereço virtual específico, então vamos criar uma função para isso.Tradução de endereços
Para converter um endereço virtual em um endereço físico, precisamos passar por uma tabela de página de quatro níveis até atingir o quadro mapeado. Vamos criar uma função que execute esta tradução de endereço: 
Nós nos referimos a uma função segura translate_addr_innerpara limitar a quantidade de código não seguro. Como observado acima, Rust considera todo o corpo unsafe fncomo um grande bloco inseguro. Invocando uma função segura, tornamos explícitas novamente cada operação unsafe.Uma função interna especial possui funcionalidade real: 
Em vez de reutilizar a função, relemos o active_level_4_tablequadro de quarto nível do registro CR3, porque isso simplifica a implementação do protótipo. Não se preocupe, melhoraremos a solução em breve.A estrutura VirtAddrjá fornece métodos para calcular índices em tabelas de páginas de quatro níveis. Armazenamos esses índices em uma pequena matriz, porque permite percorrer todas as tabelas for. Fora do loop, lembramos do último quadro visitado para calcular o endereço físico posteriormente. frameaponta para os quadros da tabela de páginas durante a iteração e para o quadro associado após a última iteração, ou seja, após passar o registro de nível 1.Dentro do loop, aplicamos novamentephysical_memory_offsetpara converter um quadro em um link da tabela da página. Em seguida, lemos o registro da tabela de páginas atual e usamos a função PageTableEntry::framepara recuperar o quadro correspondente. Se o registro não estiver mapeado para um quadro, retorne None. Se o registro exibir uma página enorme de 2 MiB ou 1 GiB, até agora teremos pânico.Então, vamos verificar a função de tradução em alguns endereços: 
Quando executamos o código, obtemos o seguinte resultado: Como esperado, com um mapeamento idêntico, o endereço é
Como esperado, com um mapeamento idêntico, o endereço é 0xb8000convertido no mesmo endereço físico. A página de código e a pilha são convertidas em endereços físicos arbitrários, que dependem de como o carregador criou o mapeamento inicial para o nosso kernel. O mapeamento physical_memory_offsetdeve apontar para o endereço físico 0, mas falha, porque a tradução usa páginas enormes para eficiência. Uma versão futura do gerenciador de inicialização pode aplicar a mesma otimização para as páginas do kernel e da pilha.Usando MappedPageTable
A tradução de endereços virtuais em endereços físicos é uma tarefa típica do kernel do SO, portanto, o engradado x86_64fornece uma abstração para ele. Ele já suporta páginas enormes e várias outras funções, exceto translate_addr, portanto, a usamos em vez de adicionar suporte a páginas grandes em nossa própria implementação.A base da abstração são duas características que definem várias funções de tradução da tabela de páginas:- A característica Mapperfornece funções que funcionam nas páginas. Por exemplo,translate_pagepara converter esta página em um quadro do mesmo tamanho, bem comomap_topara criar um novo mapeamento na tabela.
 
- A característica MapperAllSizesimplica aplicaçãoMapperpara todos os tamanhos de página. Além disso, fornece funções que funcionam com páginas de tamanhos diferentes, incluindotranslate_addrgeral outranslate.
Os traços definem apenas a interface, mas não fornecem nenhuma implementação. Agora, a caixax86_64fornece dois tipos que implementam características: MappedPageTablee RecursivePageTable. A primeira exige que cada quadro da tabela da página seja exibido em algum lugar (por exemplo, com um deslocamento). O segundo tipo pode ser usado se a tabela do quarto nível for exibida recursivamente.Temos toda a memória física mapeada para physical_memory_offsetque você possa usar o tipo MappedPageTable. Para inicializá-lo, crie uma nova função initno módulo memory: use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr;  
Não podemos retornar diretamente MappedPageTablede uma função porque é comum a um tipo de fechamento. Vamos contornar esse problema com uma construção de sintaxe impl Trait. Uma vantagem adicional é que você pode mudar o kernel para RecursivePageTablesem alterar a assinatura da função.A função MappedPageTable::newespera dois parâmetros: um link mutável para a tabela de páginas do nível 4 e um fechamento phys_to_virtque converte o quadro físico em um ponteiro da tabela de páginas *mut PageTable. Para o primeiro parâmetro, podemos reutilizar a função active_level_4_table. Para o segundo, criamos um fechamento que é usado physical_memory_offsetpara realizar a conversão.Também a tornamos uma active_level_4_tablefunção privada, porque a partir de agora será chamada apenas de init.Para usar o métodoMapperAllSizes::translate_addrem vez de nossa própria função memory::translate_addr, precisamos alterar apenas algumas linhas em kernel_main: 
Após o início, vemos os mesmos resultados de tradução de antes, mas agora apenas páginas grandes também funcionam: Como esperado, o endereço virtual é
Como esperado, o endereço virtual é physical_memory_offsetconvertido em um endereço físico 0x0. Usando a função de tradução para o tipo MappedPageTable, eliminamos a necessidade de implementar o suporte para páginas enormes. Também temos acesso a outras funções da página, como as map_toque serão usadas na próxima seção. Nesta fase, não precisamos mais da função memory::translate_addr, você pode excluí-la, se desejar.Crie um novo mapeamento
Até agora, analisamos apenas as tabelas de páginas, mas não mudamos nada. Vamos criar um novo mapeamento para uma página não exibida anteriormente.Usaremos a função map_toda característica Mapper, portanto, primeiro consideraremos essa função. A documentação diz que requer quatro argumentos: a página que queremos exibir; O quadro para o qual a página deve ser mapeada. conjunto de sinalizadores para escrever o distribuidor de quadro e tabela de páginas frame_allocator. Um alocador de quadros é necessário porque o mapeamento desta página pode exigir a criação de tabelas adicionais que precisam de quadros não utilizados como armazenamento de backup.Função create_example_mapping
O primeiro passo em nossa implementação é criar uma nova função create_example_mappingque mapeie esta página para o 0xb8000quadro físico do buffer de texto VGA. Selecionamos esse quadro porque facilita verificar se a exibição foi criada corretamente: basta escrever na página exibida recentemente e ver se ela aparece na tela.A função create_example_mappingfica assim: 
Além da página que pagevocê deseja mapear, a função espera uma instância de mappere frame_allocator. O tipo mapperimplementa a característica Mapper<Size4KiB>que o método fornece map_to. É Size4KiBnecessário um parâmetro geral , pois a característica Mapperé comum para a característica PageSize, trabalhando com páginas padrão de 4 KiB e páginas enormes de 2 MiB e 1 GiB. Queremos criar apenas 4 páginas KiB, para que possamos usá-lo em Mapper<Size4KiB>vez do requisito MapperAllSizes.Para comparação, defina o sinalizador PRESENT, pois é necessário para todas as entradas válidas e o sinalizador WRITABLEpara tornar a página exibida gravável. Desafiomap_toinseguro: você pode violar a segurança da memória com argumentos inválidos; portanto, você deve usar um bloco unsafe. Para obter uma lista de todos os sinalizadores possíveis, consulte a seção “Formato da tabela de páginas” do artigo anterior .A função map_topode falhar e, portanto, retorna Result. Como este é apenas um exemplo de código que não deve ser confiável, simplesmente o usamos expectpara entrar em pânico no caso de um erro. Se for bem sucedido, a função retorna do tipo MapperFlushque proporciona um método simples de limpeza recentemente foi apresentado no buffer de página da tradução dinâmica (TLB) pelo método flush. Assim Result, esse tipo aplica o atributo [ #[must_use]] aemitir um aviso se esquecermos acidentalmente de usá-lo .Fictício FrameAllocator
Para ligar create_example_mapping, você deve primeiro criar FrameAllocator. Como observado acima, a complexidade de criar uma nova exibição depende da página virtual que queremos exibir. No caso mais simples, já existe uma tabela de nível 1 para a página e precisamos apenas fazer um registro. No caso mais difícil, a página está em uma área de memória para a qual o nível 3 ainda não foi criado, portanto, primeiro você terá que criar tabelas de níveis 3, 2 e 1.Vamos começar com um caso simples e supor que você não precisa criar novas tabelas de páginas. Um distribuidor de quadros que sempre retorna é suficiente para isso None. Criamos uma EmptyFrameAllocatorfunção de exibição para teste: 
Agora você precisa encontrar uma página que possa ser exibida sem criar novas tabelas de páginas. O carregador é carregado no primeiro megabyte do espaço de endereço virtual, portanto, sabemos que para esta região há uma tabela válida de nível 1. No nosso exemplo, podemos selecionar qualquer página não utilizada nesta área de memória, por exemplo, a página no endereço 0x1000.Para testar a função, primeiro exibimos a página 0x1000e, em seguida, exibimos o conteúdo da memória: 
Primeiro, criamos um mapeamento para a página 0x1000, chamando uma função create_example_mappingcom um link mutável para instâncias mappere frame_allocator. Isso mapeia a página 0x1000para o quadro de buffer de texto VGA, portanto, devemos ver o que está escrito na tela.Em seguida, converta a página em um ponteiro bruto e escreva o valor no deslocamento 400. Não escrevemos no topo da página porque a linha superior do buffer VGA é diretamente deslocada da tela da seguinte maneira println. Escreva o valor 0x_f021_f077_f065_f04eque corresponde à string "New!" sobre um fundo branco Como aprendemos no artigo "Modo de texto VGA" , a gravação no buffer VGA deve ser volátil, por isso usamos o método write_volatile.Quando executamos o código no QEMU, vemos o seguinte resultado: Depois de escrever na página
Depois de escrever na página 0x1000, a inscrição "Novo!" .
Portanto, criamos com sucesso um novo mapeamento nas tabelas de páginas.Esse agrupamento funcionou porque já havia uma tabela de nível 1 para agrupamento 0x1000. Quando tentamos mapear uma página para a qual ainda não existe uma tabela de nível 1, a função map_tofalha porque tenta alocar quadros EmptyFrameAllocatorpara criar novas tabelas. Vemos que isso acontece quando tentamos exibir a página em 0xdeadbeaf000vez de 0x1000: 
Se isso for iniciado, ocorrerá um pânico com a seguinte mensagem de erro: panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 
Para exibir páginas que ainda não possuem uma tabela de nível 1, é necessário criar a correta FrameAllocator. Mas como você sabe quais quadros são gratuitos e quanta memória física está disponível?Seleção de quadro
Para novas tabelas de páginas, você precisa criar o distribuidor de quadros correto. Vamos começar com o esqueleto geral: 
O campo framespode ser inicializado com um iterador de quadro arbitrário. Isso permite que você simplesmente delegue chamadas ao allocmétodo Iterator::next.Para a inicialização, BootInfoFrameAllocatorusamos o cartão de memória memory_mapque o gerenciador de inicialização transfere como parte da estrutura BootInfo. Conforme explicado na seção Informações de inicialização , o cartão de memória é fornecido pelo firmware BIOS / UEFI. Ele pode ser solicitado apenas no início do processo de inicialização, portanto, o carregador de inicialização já chamou as funções necessárias.Um cartão de memória consiste em uma lista de estruturas MemoryRegionque contêm o endereço inicial, comprimento e tipo (por exemplo, não utilizados, reservados, etc.) de cada área de memória. Criando um iterador que produz quadros de áreas não utilizadas, podemos criar um válido BootInfoFrameAllocator.A inicialização BootInfoFrameAllocatorocorre em uma nova função init_frame_allocator: 
Esta função usa um combinador para converter o mapa inicial MemoryMapem um iterador dos quadros físicos usados:- Primeiro, chamamos o método iterpara converter o cartão de memória em um iteradorMemoryRegion. Em seguida, usamos o métodofilterpara pular regiões reservadas ou inacessíveis. O carregador atualiza o cartão de memória para todos os mapeamentos criados, para que os quadros usados pelo kernel (código, dados ou pilha) ou para armazenar informações sobre a inicialização já estejam marcados comoInUseou de forma semelhante. Assim, podemos ter certeza de que os quadrosUsablenão são usados em outros lugares .
 
- maprange Rust .
 
- : into_iter, 4096-step_by. 4096 (= 4 ) — , . , .flat_mapmap,Iterator<Item = u64>Iterator<Item = Iterator<Item = u64>>.
 
- PhysFrame,- Iterator<Item = PhysFrame>.- BootInfoFrameAllocator.
Agora você pode alterar a nossa funçãokernel_mainpara transmitir uma cópia BootInfoFrameAllocatorao invés EmptyFrameAllocator: 
Dessa vez, o mapeamento de endereços foi bem-sucedido e novamente vemos o preto e branco "Novo!" .
Nos bastidores, o método map_tocria tabelas de páginas ausentes da seguinte maneira:- Selecione um quadro não utilizado do quadro transmitido frame_allocator.
 
- Zero quadro para criar uma nova tabela de página vazia.
 
- Mapeie uma entrada da tabela de nível superior para esse quadro.
 
- Vá para o próximo nível da tabela.
Embora nossa funçãocreate_example_mappingseja apenas um exemplo de código, agora podemos criar novos mapeamentos para páginas arbitrárias. Isso será necessário para alocar memória e implementar multithreading em artigos futuros.Sumário
Neste artigo, aprendemos sobre vários métodos de acesso aos quadros físicos das tabelas de páginas, incluindo mapeamento de identidade, mapeamento de memória física completa, mapeamento temporário e tabelas de páginas recursivas. Optamos por exibir a memória física completa como um método simples e poderoso.Não podemos mapear a memória física do kernel sem acesso à tabela de páginas, portanto, o suporte ao carregador de inicialização é necessário. O rack bootloadercria os mapeamentos necessários por meio de funções adicionais de carga. Ele passa as informações necessárias para o kernel como um argumento &BootInfopara a função de ponto de entrada.Para nossa implementação, primeiro examinamos manualmente as tabelas de páginas, fizemos uma função de tradução e depois usamos o tipo de MappedPageTablecaixax86_64. Também aprendemos como criar novos mapeamentos na tabela de páginas e como fazê-los FrameAllocatorem um cartão de memória transmitido pelo gerenciador de inicialização.O que vem a seguir?
No próximo artigo, criaremos uma área de memória heap para o nosso kernel, que permitirá alocar memória e usar diferentes tipos de coleções .