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 em 0 KiB
para 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 em 36 KiB
para 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_memory
mapeia 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ências map_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 bootloader
define 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_map
e physical_memory_offset
:- O campo
memory_map
fornece 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_offset
relata 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 estrutura BootInfo
para o kernel como um argumento &'static BootInfo
para 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 bootloader
fornece 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_main
agora 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_table
retornará 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 PageTable
pelo método as_mut_ptr
e, em seguida, crie um link com segurança &mut PageTable
. Em &mut
vez 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 fn
como 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_offset
no campo correspondente da estrutura BootInfo
. Em seguida, usamos uma função iter
para percorrer as entradas da tabela de páginas e um combinador enumerate
para adicionar um índice i
a 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:
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_inner
para limitar a quantidade de código não seguro. Como observado acima, Rust considera todo o corpo unsafe fn
como 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_table
quadro 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 VirtAddr
já 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. frame
aponta 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_offset
para 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::frame
para 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 é 0xb8000
convertido 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_offset
deve 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_64
fornece 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
Mapper
fornece funções que funcionam nas páginas. Por exemplo, translate_page
para converter esta página em um quadro do mesmo tamanho, bem como map_to
para criar um novo mapeamento na tabela.
- A característica
MapperAllSizes
implica aplicação Mapper
para todos os tamanhos de página. Além disso, fornece funções que funcionam com páginas de tamanhos diferentes, incluindo translate_addr
geral ou translate
.
Os traços definem apenas a interface, mas não fornecem nenhuma implementação. Agora, a caixa x86_64
fornece dois tipos que implementam características: MappedPageTable
e 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_offset
que você possa usar o tipo MappedPageTable. Para inicializá-lo, crie uma nova função init
no módulo memory
: use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr;
Não podemos retornar diretamente MappedPageTable
de 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 RecursivePageTable
sem alterar a assinatura da função.A função MappedPageTable::new
espera dois parâmetros: um link mutável para a tabela de páginas do nível 4 e um fechamento phys_to_virt
que 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_offset
para realizar a conversão.Também a tornamos uma active_level_4_table
função privada, porque a partir de agora será chamada apenas de init
.Para usar o métodoMapperAllSizes::translate_addr
em 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 é physical_memory_offset
convertido 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_to
que 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_to
da 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_mapping
que mapeie esta página para o 0xb8000
quadro 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_mapping
fica assim:
Além da página que page
você deseja mapear, a função espera uma instância de mapper
e frame_allocator
. O tipo mapper
implementa a característica Mapper<Size4KiB>
que o método fornece map_to
. É Size4KiB
necessá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 WRITABLE
para tornar a página exibida gravável. Desafiomap_to
inseguro: 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_to
pode falhar e, portanto, retorna Result
. Como este é apenas um exemplo de código que não deve ser confiável, simplesmente o usamos expect
para entrar em pânico no caso de um erro. Se for bem sucedido, a função retorna do tipo MapperFlush
que 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 EmptyFrameAllocator
funçã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 0x1000
e, em seguida, exibimos o conteúdo da memória:
Primeiro, criamos um mapeamento para a página 0x1000
, chamando uma função create_example_mapping
com um link mutável para instâncias mapper
e frame_allocator
. Isso mapeia a página 0x1000
para 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_f04e
que 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 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_to
falha porque tenta alocar quadros EmptyFrameAllocator
para criar novas tabelas. Vemos que isso acontece quando tentamos exibir a página em 0xdeadbeaf000
vez 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 frames
pode ser inicializado com um iterador de quadro arbitrário. Isso permite que você simplesmente delegue chamadas ao alloc
método Iterator::next
.Para a inicialização, BootInfoFrameAllocator
usamos o cartão de memória memory_map
que 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 MemoryRegion
que 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 BootInfoFrameAllocator
ocorre em uma nova função init_frame_allocator
:
Esta função usa um combinador para converter o mapa inicial MemoryMap
em um iterador dos quadros físicos usados:- Primeiro, chamamos o método
iter
para converter o cartão de memória em um iterador MemoryRegion
. Em seguida, usamos o método filter
para 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 como InUse
ou de forma semelhante. Assim, podemos ter certeza de que os quadros Usable
não são usados em outros lugares .
map
range Rust .
- :
into_iter
, 4096- step_by
. 4096 (= 4 ) — , . , . flat_map
map
, Iterator<Item = u64>
Iterator<Item = Iterator<Item = u64>>
.
PhysFrame
, Iterator<Item = PhysFrame>
. BootInfoFrameAllocator
.
Agora você pode alterar a nossa função kernel_main
para transmitir uma cópia BootInfoFrameAllocator
ao 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_to
cria 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ção create_example_mapping
seja 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 bootloader
cria 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 &BootInfo
para 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 MappedPageTable
caixax86_64
. Também aprendemos como criar novos mapeamentos na tabela de páginas e como fazê-los FrameAllocator
em 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 .