Este artigo explica como o kernel do sistema operacional pode acessar os quadros de memória física. Estudaremos a função de conversão de endereços virtuais em físicos. Também descobriremos como criar novos mapeamentos nas tabelas de páginas.
Este blog está publicado no
GitHub . Se você tiver alguma dúvida ou problema, abra o ticket correspondente lá. Todas as fontes do artigo estão
aqui .
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 melhora a segurança, mas surge o problema: como acessar endereços físicos reais armazenados nas entradas da tabela de páginas ou no
CR3
?
Na primeira seção do artigo, discutiremos o problema e as diferentes abordagens para resolvê-lo. Em seguida, implementamos uma função que percorre a hierarquia das tabelas de páginas para converter endereços virtuais em físicos. Por fim, aprenda como criar novos mapeamentos nas tabelas de páginas e encontre os quadros de memória não utilizados para criar novas tabelas.
Atualizações de dependência
Para funcionar, você precisa do
x86_64
versão 0.4.0 ou posterior. Atualize a dependência em nosso
Cargo.toml
:
[dependencies] x86_64 = "0.4.0" # or later
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.
1. 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.
2. Outra opção é
transmitir tabelas de páginas apenas temporariamente quando você 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 é realizado a partir do registro CR3 através de entradas nulas 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 correspondia ao registro nulo de uma tabela de nível 1 com um quadro em
24 KiB
. Isso criou um mapeamento temporário da página virtual em
0 KiB
para o quadro físico da tabela de nível da página 2 indicada pela seta pontilhada. Agora o kernel pode acessar a tabela de nível 2 gravando em uma página que começa com
0 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.
3. Embora ambas as abordagens acima funcionem, existe um terceiro método:
tabelas de páginas recursivas . Ele combina as vantagens de ambas as abordagens: compara constantemente todos os quadros das tabelas de páginas sem exigir comparações temporárias e também mantém as páginas mapeadas lado a lado, evitando a fragmentação do espaço de endereço virtual. Este é o método que vamos usar.
Tabelas de páginas recursivas
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 suponha que cada registro na tabela de nível 4 aponte para uma tabela de nível 3. Portanto, agora ele trate 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 no 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.
Implementação
Depois de toda essa teoria, podemos finalmente prosseguir com a implementação. Convenientemente, o carregador gerou não apenas tabelas de páginas, mas também uma exibição recursiva no último registro da tabela de nível 4. O carregador fez isso porque, caso contrário, haveria um problema de galinha ou ovo: precisamos acessar a tabela de nível 4 para criar um mapa recursivo mas não podemos acessá-lo sem nenhuma exibição.
Já usamos esse mapeamento recursivo no final do artigo anterior para acessar a tabela de nível 4 através do endereço codificado
0xffff_ffff_ffff_f000
. Se convertermos esse endereço em octal e compará-lo com a tabela acima, veremos que ele corresponde exatamente à estrutura do registro na tabela de nível 4 com
RRR
=
0o777
,
AAAA
=
0
e os bits de extensão do sinal
1
:
estrutura: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
endereço: 0o_177777_777_777_777_777_0000
Graças ao conhecimento de tabelas recursivas, agora podemos criar endereços virtuais para acessar todas as tabelas ativas. E faça a função de transmissão.
Tradução de endereços
Como primeira etapa, crie uma função que converta um endereço virtual em um endereço físico, passando pela hierarquia das tabelas de páginas:
Primeiro, introduzimos variáveis para o índice recursivo (511 =
0o777
) e os bits de extensão de sinal (cada um é 1). Em seguida, calculamos os índices das tabelas de páginas e o deslocamento por meio de operações bit a bit, conforme indicado na ilustração:
A próxima etapa é calcular os endereços virtuais das tabelas de quatro páginas, conforme descrito na seção anterior. Em seguida, na função, convertemos cada um desses endereços em links de
PageTable
. Essas são operações inseguras porque o compilador não pode saber que esses endereços são válidos.
Após o cálculo do endereço, usamos o operador de indexação para exibir o registro na tabela de nível 4. Se esse registro for zero, não haverá tabela de nível 3 para esse registro de nível 4. Isso significa que
addr
não
addr
mapeado para nenhuma memória física. Então, retornamos
None
. Caso contrário, sabemos que existe uma tabela de nível 3. Em seguida, repetimos o procedimento, como no nível anterior.
Depois de verificar três páginas de um nível superior, podemos finalmente ler o registro da tabela de nível 1, que indica o quadro físico com o qual o endereço é mapeado. Como a última etapa, adicione o deslocamento da página e retorne o endereço.
Se tivéssemos certeza de que o endereço estava mapeado, poderíamos acessar diretamente a tabela de nível 1 sem consultar as páginas de um nível superior. Mas como não sabemos disso, primeiro precisamos verificar se existe uma tabela de nível 1; caso contrário, nossa função retornará um erro de falta de página para endereços não correspondentes.
Experimente
Vamos tentar usar a função de tradução para endereços virtuais em nossa função
_start
:
Após o início, vemos o seguinte resultado:
Como esperado, o endereço 0xb8000 associado ao identificador é convertido no mesmo endereço físico. A página de código e a pilha são convertidas em alguns endereços físicos arbitrários, que dependem de como o carregador criou o mapeamento inicial para o nosso kernel.
RecursivePageTable
x86_64 fornece um tipo
RecursivePageTable
que implementa abstrações seguras para várias operações da tabela de páginas. Usando esse tipo, você pode implementar a função
translate_addr
muito mais concisa:
O tipo
RecursivePageTable
encapsula totalmente os rastreamentos inseguros da tabela da página, portanto, o código
unsafe
na função
translate_addr
não é mais necessário. A função
init
permanece insegura devido à necessidade de garantir a correção do
level_4_table_addr
passado.
Nossa função
_start
deve ser atualizada para assinar novamente a função da seguinte maneira:
Agora, em vez de passar
LEVEL_4_TABLE_ADDR
para
translate_addr
e acessar as tabelas de páginas por meio de ponteiros brutos não seguros, passamos referências ao tipo
RecursivePageTable
. Assim, agora temos uma abstração segura e uma semântica clara de propriedade. Isso garante que não seremos capazes de alterar acidentalmente a tabela de páginas no acesso compartilhado, pois a alteração requer a posse exclusiva de
RecursivePageTable
.
Esta função fornece o mesmo resultado que a função de tradução original escrita manualmente.
Tornando os recursos não seguros mais seguros
memory::init
é uma função insegura: requer um bloco para chamá-lo unsafe
, porque o chamador deve garantir que certos requisitos sejam atendidos. No nosso caso, o requisito é que o endereço transmitido seja mapeado com precisão para o quadro físico da tabela de páginas de nível 4. Todo o corpo da função insegura é colocadono bloco unsafe
para que todos os tipos de operações sejam executados sem a criação de blocos adicionais unsafe
. Portanto, não precisamos de um bloco não seguro para desreferenciar level_4_table_ptr
: pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr;
O problema é que não vemos imediatamente quais partes são inseguras. Por exemplo, sem olhar para a definição de uma função, RecursivePageTable::new
não podemos dizer se é segura ou não. Portanto, é muito fácil ignorar acidentalmente algum código não seguro.Para evitar esse problema, você pode adicionar uma função interna segura:
Agora, o bloco é unsafe
novamente necessário para desreferenciar level_4_table_ptr
, e imediatamente vemos que essas são as únicas operações inseguras. Atualmente, o Rust possui uma RFC aberta para alterar essa propriedade malsucedida de funções não seguras.Crie um novo mapeamento
Quando lemos as tabelas de páginas e criamos a função de conversão, o próximo passo é criar um novo mapeamento na hierarquia de tabelas de páginas.A complexidade desta operação depende da página virtual que queremos exibir. No caso mais simples, já existe uma tabela de páginas de nível 1 para esta página e precisamos apenas fazer uma entrada. No caso mais difícil, a página está na área de memória para a qual o nível 3 ainda não existe; portanto, primeiro você precisa criar novas tabelas de nível 3, nível 2 e nível 1.Vamos começar com um caso simples, quando você não precisa criar novas tabelas. 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
. Usamos o 0xb8000
quadro do buffer de texto VGA como o quadro desejado . É muito fácil verificar como funciona a tradução de endereços.Nós o implementamos em uma nova função create_maping
no módulo memory
:
A função aceita uma referência mutável para RecursivePageTable
(ela será alterada) e FrameAllocator
, explicada abaixo. Em seguida, aplica a função map_to
na bandeja Mapper
para mapear a página no endereço 0x1000
com o quadro físico no endereço 0xb8000
. A função não é segura, porque é possível violar a segurança da memória com argumentos inválidos.Além dos argumentos page
e frame
, a função map_to
recebe mais dois argumentos. O terceiro argumento é o conjunto de sinalizadores para a tabela de páginas. Definimos o sinalizador PRESENT
necessário para todas as entradas válidas e o sinalizador WRITABLE
para gravabilidade.O quarto argumento deve ser alguma estrutura que implemente a característica FrameAllocator
. Este argumento é necessário pelo métodomap_to
porque a criação de novas tabelas de páginas pode exigir quadros não utilizados. A implementação requer o traço argumento Size4KiB
, como tipos Page
e PhysFrame
são universal para a característica PageSize
, trabalhando com 4 páginas padrão KiB e com enormes páginas 2 MiB / 1 IB.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
com pânico quando ocorre um erro. Se for bem-sucedida, a função retornará um tipo MapperFlush
que fornece uma maneira fácil de limpar a página correspondida recentemente do método TLB (associative translation buffer) flush
. CurtirResult
, o tipo usa o atributo #[must_use]
e emite um aviso se esquecermos acidentalmente de aplicá-lo.Como sabemos que o endereço 0x1000
não requer novas tabelas de páginas, ele FrameAllocator
sempre pode retornar None
. Para testar a função, crie isto EmptyFrameAllocator
:
(Se o erro 'o método allocate_frame
não é um membro da característica FrameAllocator
' aparecer , você precisará atualizar x86_64
para a versão 0.4.0.)Agora podemos testar a nova função de conversão:
Primeiro, criamos um mapeamento para a página no endereço 0x1000
, chamando a função create_example_mapping
com um link mutável para a instância RecursivePageTable
. Isso traduz a página 0x1000
em um buffer de texto VGA, portanto veremos algum resultado na tela.Em seguida, escrevemos um valor nesta página 0xf021f077f065f04e
, que corresponde à linha "Novo!" sobre um fundo branco Só não é necessário escrever esse valor diretamente na parte superior da página 0x1000
, porque a linha superior se moverá a seguir da tela println
e gravará no deslocamento 0x900
, localizado aproximadamente no meio da tela. Como sabemos 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 o executamos no QEMU, vemos o seguinte:A inscrição na tela.O código funcionou porque já havia uma tabela de nível 1 para exibir a página 0x1000
. Se tentarmos traduzir uma página para a qual essa tabela ainda não existe, a função map_to
retornará um erro, porque tentará selecionar quadros para criar novas tabelas de páginas EmptyFrameAllocator
. Veremos isso se tentarmos traduzir a página em 0xdeadbeaf000
vez de 0x1000
:
Ao iniciar, um pânico começa com a seguinte mensagem de erro: entre em pânico em 'map_to falhou: FrameAllocationFailed', /.../result.rs:999haps
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?Informações de inicialização
Computadores diferentes têm quantidades diferentes de memória física e áreas diferentes reservadas por dispositivos como VGA. Somente o firmware BIOS ou UEFI sabe exatamente quais áreas de memória podem ser usadas e quais são reservadas. Os dois padrões de firmware fornecem funções para obter um cartão de alocação de memória, mas só podem ser chamados no início do download. Portanto, nosso gerenciador de inicialização já solicitou essas (e outras) informações do BIOS.Para passar informações ao kernel do sistema operacional, o carregador como argumento ao chamar a função _start
fornece um link para a estrutura de informações da inicialização. Adicione este argumento à nossa função:
A estrutura BootInfo
ainda está sendo finalizada, portanto, não se surpreenda com falhas ao atualizar para versões futuras do gerenciador de inicialização que não são compatíveis com o semver . No momento em que tem três campos p4_table_addr
, memory_map
e package
:- O campo
p4_table_addr
contém um endereço virtual recursivo da tabela de páginas de nível 4. Graças a isso, não é necessário registrar o endereço com força 0o_177777_777_777_777_777_0000
.
- O campo
memory_map
é de maior interesse, pois contém uma lista de todas as áreas de memória e seu tipo (não utilizado, reservado ou outros).
- O campo
package
é a função atual para associar dados adicionais ao carregador. A implementação não está concluída, portanto podemos ignorá-la por enquanto.
Antes de usar o campo memory_map
para criar o caminho certo FrameAllocator
, queremos garantir o tipo correto de argumento boot_info
.Macro entry_point
Como _start
é chamada externamente, a assinatura da função não é verificada. Isso significa que argumentos arbitrários não levarão a erros de compilação, mas podem causar uma falha ou comportamento indefinido do tempo de execução.Para verificar a assinatura, a caixa bootloader
para definir a função Rust como um ponto de entrada usa uma macro entry_point
com tipos validados. Reescrevemos nossa função para esta macro:
Para ponto de entrada, você não precisa mais usar extern "C"
ou no_mangle
, uma vez que a macro define o ponto de entrada real de baixo nível _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. É importante que ele já esteja digitado, para que ocorra um erro de compilação se você alterar a assinatura da função, por exemplo, adicionando um argumento ou alterando seu tipo.Observe que agora estamos enviando para um memory::init
endereço codificado, mas boot_info.p4_table_addr
. Portanto, o código funcionará mesmo que a versão futura do carregador de inicialização selecione outra entrada na tabela da tabela no nível 4 da página para exibição recursiva.Seleção de quadro
Agora, graças às informações do BIOS, temos acesso ao cartão de alocação de memória, para que você possa fazer um distribuidor de quadros normal. Vamos começar com o esqueleto geral:
O campo é frames
inicializado por um iterador de quadro arbitrário . Isso permite que você simplesmente delegue chamadas alloc
para o método Iterator :: next .A inicialização BootInfoFrameAllocator
ocorre em uma nova função init_frame_allocator
:
Esta função, usando um combinador, converte o mapa de alocação de memória original em um iterador dos quadros físicos usados:iter
MemoryRegion
. filter
, . , , (, ) , InUse
. , , - .
map
range Rust .
- A terceira etapa é a mais difícil: convertemos cada intervalo em um iterador usando o método
into_iter
e, em seguida, selecionamos cada 4096º endereço com step_by
. Como o tamanho da página é de 4096 bytes (4 KiB), obtemos o endereço do início de cada quadro. A página do carregador alinha todas as áreas de memória usadas, portanto, não precisamos de um código de alinhamento ou arredondamento. Substituindo map
por flat_map
, chegamos ao Iterator<Item = u64>
invés Iterator<Item = Iterator<Item = u64>>
.
- No estágio final, converteremos os endereços iniciais em tipos
PhysFrame
para criar o necessário Iterator<Item = PhysFrame>
. Em seguida, use esse iterador para criar e retornar um novo BootInfoFrameAllocator
.
Agora nós podemos mudar a nossa função kernel_main
para transmitir sua instância BootInfoFrameAllocator
vez EmptyFrameAllocator
:
Agora a tradução do endereço foi bem-sucedida - e novamente vemos a mensagem em preto e branco “Novo!” Na tela .
Nos bastidores, o método map_to
cria tabelas de páginas ausentes da seguinte maneira:- Extrai um quadro não utilizado de
frame_allocator
.
- Corresponde a uma entrada da tabela de nível superior com esse quadro. O quadro agora está acessível através de uma tabela de páginas recursiva.
- Zera o quadro para criar uma nova tabela de páginas vazia.
- Vai para a próxima tabela de nível.
Embora nossa função create_maping
seja apenas um exemplo, agora podemos criar novos mapeamentos para páginas arbitrárias. Isso é muito útil ao alocar memória e implementar multithreading em artigos futuros.Sumário
Neste artigo, você aprendeu como usar uma tabela recursiva de nível 4 para converter todos os quadros em endereços virtuais computáveis. Usamos esse método para implementar a função de conversão de endereço e criar um novo mapeamento nas tabelas de páginas.Vimos que a criação de novos mapeamentos requer quadros não utilizados para novas tabelas. Esse distribuidor de quadros pode ser implementado com base nas informações do BIOS que o gerenciador de inicialização passa para o nosso kernel.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 .