diff --git a/blog/config.toml b/blog/config.toml index c1fbc62b..b96caaf1 100644 --- a/blog/config.toml +++ b/blog/config.toml @@ -34,7 +34,19 @@ skip_anchor_prefixes = [ subtitle = "Philipp Oppermann's blog" author = { name = "Philipp Oppermann" } default_language = "en" -languages = ["en", "zh-CN", "zh-TW", "fr", "ja", "fa", "ru", "ko", "ar", "es"] +languages = [ + "en", + "ar", + "es", + "fa", + "fr", + "ja", + "ko", + "pt-BR", + "ru", + "zh-CN", + "zh-TW", +] [translations] lang_name = "English (original)" @@ -279,3 +291,28 @@ support_me = """ comment_note = """ ¿Tienes algún problema, quieres compartir comentarios o discutir más ideas? ¡No dudes en dejar un comentario aquí! Por favor, utiliza inglés y sigue el código de conducta de Rust. Este hilo de comentarios se vincula directamente con una discusión en GitHub, así que también puedes comentar allí si lo prefieres. """ + +# Portuguese (Brazil) +[languages.pt-BR] +title = "Escrevendo um OS em Rust" +description = "Esta série de blog cria um pequeno sistema operacional na linguagem de programação Rust. Cada post é um pequeno tutorial e inclui todo o código necessário." +[languages.pt-BR.translations] +lang_name = "Português (Brasil)" +toc = "Tabela de Conteúdos" +all_posts = "« Todos os Posts" +comments = "Comentários" +comments_notice = "Por favor, deixe seus comentários em inglês se possível." +readmore = "ler mais »" +not_translated = "(Esta postagem ainda não foi traduzida.)" +translated_content = "Conteúdo Traduzido:" +translated_content_notice = "Esta é uma tradução comunitária do post _original.title_. Pode estar incompleta, desatualizada ou conter erros. Por favor, reporte qualquer problema!" +translated_by = "Traduzido por" +translation_contributors = "Com contribuições de" +word_separator = "e" +support_me = """ +

Apoie-me

+

Criar e manter este blog e as bibliotecas associadas dá muito trabalho, mas eu realmente gosto de fazê-lo. Ao me apoiar, você me permite investir mais tempo em novo conteúdo, novos recursos e manutenção contínua. A melhor forma de me apoiar é me patrocinar no GitHub. Obrigado!

+""" +comment_note = """ +Teve algum problema, quer deixar um feedback ou discutir mais ideias? Fique à vontade para deixar um comentário aqui! Por favor, use o inglês e siga o código de conduta do Rust. Este tópico de comentários está diretamente vinculado a uma discussão no GitHub, então você também pode comentar lá se preferir. +""" diff --git a/blog/content/_index.pt-BR.md b/blog/content/_index.pt-BR.md new file mode 100644 index 00000000..42106574 --- /dev/null +++ b/blog/content/_index.pt-BR.md @@ -0,0 +1,13 @@ ++++ +template = "edition-2/index.html" ++++ + +

Escrevendo um OS em Rust

+ +
+ +Esta série de posts do blog cria um pequeno sistema operacional na [linguagem de programação Rust](https://www.rust-lang.org/). Cada post é um pequeno tutorial e inclui todo o código necessário, então você pode acompanhar se quiser. O código-fonte também está disponível no [repositório Github](https://github.com/phil-opp/blog_os) correspondente. + +Último post: + +
\ No newline at end of file diff --git a/blog/content/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md new file mode 100644 index 00000000..a8501223 --- /dev/null +++ b/blog/content/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md @@ -0,0 +1,599 @@ ++++ +title = "Um Binário Rust Independente" +weight = 1 +path = "pt-BR/freestanding-rust-binary" +date = 2018-02-10 + +[extra] +chapter = "O Básico" + +# Please update this when updating the translation +translation_based_on_commit = "624f0b7663daca1ce67f297f1c450420fbb4d040" + +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +O primeiro passo para criar nosso próprio kernel de sistema operacional é criar um executável Rust que não vincule a biblioteca padrão. Isso torna possível executar o código Rust no [bare metal] sem um sistema operacional subjacente. + +[bare metal]: https://en.wikipedia.org/wiki/Bare_machine + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na banch [`post-01`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-01 + + + +## Introdução +Para escrever um kernel de sistema operacional, precisamos de código que não dependa de nenhum recurso do sistema operacional. Isso significa que não podemos usar threads, arquivos, memória heap, rede, números aleatórios, saída padrão ou qualquer outro recurso que exija abstrações do sistema operacional ou hardware específico. O que faz sentido, já que estamos tentando escrever nosso próprio sistema operacional e nossos próprios drivers. + +Isso significa que não podemos usar a maior parte da [biblioteca padrão do Rust], mas há muitos recursos do Rust que _podemos_ usar. Por exemplo, podemos usar [iteradores], [closures], [pattern matching], [option] e [result], [formatação de string] e, claro, o [sistema de ownership]. Esses recursos tornam possível escrever um kernel de uma maneira muito expressiva e de alto nível, sem nos preocuparmos com [undefined behavior] ou [memory safety]. + +[option]: https://doc.rust-lang.org/core/option/ +[result]:https://doc.rust-lang.org/core/result/ +[Rust standard library]: https://doc.rust-lang.org/std/ +[iteradores]: https://doc.rust-lang.org/book/ch13-02-iterators.html +[closures]: https://doc.rust-lang.org/book/ch13-01-closures.html +[pattern matching]: https://doc.rust-lang.org/book/ch06-00-enums.html +[formatação de string]: https://doc.rust-lang.org/core/macro.write.html +[sistema de ownership]: https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html +[undefined behavior]: https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs +[memory safety]: https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention + +Para criar um kernel de sistema operacional em Rust, precisamos criar um executável que possa ser executado sem um sistema operacional subjacente. Esse executável é frequentemente chamado de executável “autônomo” ou “bare-metal”. + +Este post descreve as etapas necessárias para criar um binário Rust independente e explica por que essas etapas são necessárias. Se você estiver interessado apenas em um exemplo mínimo, pode **[ir para o resumo](#resumo)**. + +## Desativando a biblioteca padrão +Por padrão, todos as crates Rust vinculam a [biblioteca padrão], que depende do sistema operacional para recursos como threads, arquivos ou rede. Ela também depende da biblioteca padrão C `libc`, que interage intimamente com os serviços do sistema operacional. Como nosso plano é escrever um sistema operacional, não podemos usar nenhuma biblioteca dependente de um sistema operacional. +Portanto, temos que desativar a inclusão automática da biblioteca padrão por meio do [atributo `no_std`]. + +[biblioteca padrão]: https://doc.rust-lang.org/std/ +[atributo `no_std`]: https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html + +Começamos criando um novo projeto de binário cargo. A maneira mais fácil de fazer isso é através da linha de comando: + +``` +cargo new blog_os --bin --edition 2024 +``` + +Eu nommei o projeto `blog_os`, mas claro que você pode escolher o seu próprio nome. A flag `--bin` especifica que queremos criar um executável binário (em contraste com uma biblioteca) e a flag `--edition 2024` especifica que queremos usar a [edição 2024] de Rust para nossa crate. Quando executamos o comando, o cargo cria a seguinte estrutura de diretório para nós: + +[edição 2024]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/index.html + +``` +blog_os +├── Cargo.toml +└── src + └── main.rs +``` + +O `Cargo.toml` contém a configuração da crate, por exemplo o nome da crate, o autor, o número da [versão semântica] e dependências. O arquivo `src/main.rs` contém o módulo raiz da nossa crate e nossa função `main`. Você pode compilar sua crate através de `cargo build` e então executar o binário compilado `blog_os` na subpasta `target/debug`. + +[versão semântica]: https://semver.org/ + +### O Atributo `no_std` + +Agora nossa crate implicitamente vincula a biblioteca padrão. Vamos tentar desativar isso adicionando o [atributo `no_std`]: + +```rust +// main.rs + +#![no_std] + +fn main() { + println!("Olá, mundo!"); +} +``` + +Quando tentamos compilá-lo agora (executando `cargo build`), o seguinte erro ocorre: + +``` +error: cannot find macro `println!` in this scope + --> src/main.rs:4:5 + | +4 | println!("Olá, mundo!"); + | ^^^^^^^ +``` + +A razão deste erro é que a [macro `println`] é parte da biblioteca padrão, que não incluímos mais. Então não conseguimos mais imprimir coisas. Isso faz sentido, já que `println` escreve no [standard output], que é um descritor de arquivo especial fornecido pelo sistema operacional. + +[macro `println`]: https://doc.rust-lang.org/std/macro.println.html +[standard output]: https://en.wikipedia.org/wiki/Standard_streams#Standard_output_.28stdout.29 + +Então vamos remover o println!() e tentar novamente com uma função main vazia: + +```rust +// main.rs + +#![no_std] + +fn main() {} +``` + +``` +> cargo build +error: `#[panic_handler]` function required, but not found +error: language item required, but not found: `eh_personality` +``` + +Agora o compilador está pedindo uma função `#[panic_handler]` e um _item de linguagem_. + +## Implementação de Panic + +O atributo `panic_handler` define a função que o compilador deve invocar quando ocorre um [panic]. A biblioteca padrão fornece sua própria função de tratamento de panic, mas em um ambiente `no_std` precisamos defini-la nós mesmos: + +[panic]: https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html + +```rust +// in main.rs + +use core::panic::PanicInfo; + +/// Esta função é chamada em caso de pânico. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +O parâmetro [`PanicInfo`][PanicInfo] contém o arquivo e a linha onde o panic aconteceu e a mensagem de panic opcional. A função nunca deve retornar, então é marcada como uma [função divergente] ao retornar o [tipo “never”] `!`. Não há muito que possamos fazer nesta função por enquanto, então apenas fazemos um loop infinito. + +[PanicInfo]: https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html +[função divergente]: https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions +[tipo “never”]: https://doc.rust-lang.org/nightly/std/primitive.never.html + +## O Item de Linguagem `eh_personality` + +Items de linguagem são funções e tipos especiais necessários internamente pelo compilador. Por exemplo, a trait [`Copy`] é um item de linguagem que diz ao compilador quais tipos têm [_semântica de cópia_][`Copy`]. Quando olhamos para a [implementação][copy code], vemos que tem o atributo especial `#[lang = "copy"]` que o define como um item de linguagem (_Language Item_ em inglês). + +[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html +[copy code]: https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299 + +Enquanto é possível fornecer implementações customizadas de items de linguagem, isso deve ser feito apenas como último recurso. A razão é que items de linguagem são detalhes de implementação altamente instáveis e nem mesmo verificados de tipo (então o compilador não verifica se uma função tem os tipos de argumento corretos). Felizmente, há uma forma mais estável de corrigir o erro de item de linguagem acima. + +O [item de linguagem `eh_personality`] marca uma função que é usada para implementar [stack unwinding]. Por padrão, Rust usa unwinding para executar os destructores de todas as variáveis da stack vivas em caso de [panic]. Isso garante que toda memória usada seja liberada e permite que a thread pai capture o panic e continue a execução. Unwinding, no entanto, é um processo complicado e requer algumas bibliotecas específicas do SO (por exemplo, [libunwind] no Linux ou [tratamento estruturado de exceção] no Windows), então não queremos usá-lo para nosso sistema operacional. + +[item de linguagem `eh_personality`]: https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45 +[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php +[libunwind]: https://www.nongnu.org/libunwind/ +[tratamento estruturado de exceção]: https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling + +### Desativando o Unwinding + +Existem outros casos de uso também para os quais unwinding é indesejável, então Rust fornece uma opção para [abortar no panic] em vez disso. Isso desativa a geração de informações de símbolo de desenrolar e reduz consideravelmente o tamanho do binário. Há múltiplos locais onde podemos desativar o unwinding. A forma mais fácil é adicionar as seguintes linhas ao nosso `Cargo.toml`: + +```toml +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" +``` + +Isso define a estratégia de panic para `abort` tanto para o perfil `dev` (usado para `cargo build`) quanto para o perfil `release` (usado para `cargo build --release`). Agora o item de linguagem `eh_personality` não deve mais ser necessário. + +[abortar no panic]: https://github.com/rust-lang/rust/pull/32900 + +Agora corrigimos ambos os erros acima. No entanto, se tentarmos compilar agora, outro erro ocorre: + +``` +> cargo build +error: requires `start` lang_item +``` + +Está faltando o item de linguagem `start` no nosso programa, que define o ponto de entrada. + +## O Atributo `start` + +Alguém pode pensar que a função `main` é a primeira função chamada quando você executa um programa. No entanto, a maioria das linguagens tem um [sistema de runtime], que é responsável por coisas como coleta de lixo (por exemplo, em Java) ou threads de software (por exemplo, goroutines em Go). Este runtime precisa ser chamado antes de `main`, já que ele precisa se inicializar a si mesmo. + +[sistema de runtime]: https://en.wikipedia.org/wiki/Runtime_system + +Em um binário Rust típico que vincula a biblioteca padrão, a execução começa em uma biblioteca de runtime C chamada `crt0` ("C runtime zero"), que configura o ambiente para uma aplicação C. Isso inclui criar um stack e colocar os argumentos nos registradores certos. O runtime C então invoca o [ponto de entrada do runtime Rust][rt::lang_start], que é marcado pelo item de linguagem `start`. Rust tem apenas um runtime muito mínimo, que cuida de algumas poucas coisas como configurar guardas de estouro do stack ou imprimir um backtrace ao fazer panic. O runtime então finalmente chama a função `main`. + +[rt::lang_start]: https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73 + +Nosso executável independente não tem acesso ao runtime Rust e ao `crt0`, então precisamos definir nosso próprio ponto de entrada. Implementar o item de linguagem `start` não ajudaria, já que ainda exigiria `crt0`. Em vez disso, precisamos sobrescrever diretamente o ponto de entrada `crt0`. + +### Sobrescrevendo o Ponto de Entrada (Entry Point) +Para dizer ao compilador Rust que não queremos usar a cadeia normal de ponto de entrada, adicionamos o atributo `#![no_main]`. + +```rust +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +/// Esta função é chamada em caso de pânico. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +Você pode notar que removemos a função `main`. A razão é que um `main` não faz sentido sem um runtime subjacente que o chame. Em vez disso, estamos agora sobrescrevendo o ponto de entrada do sistema operacional com nossa própria função `_start`: + +```rust +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + loop {} +} +``` + +Ao usar o atributo `#[unsafe(no_mangle)]`, desativamos [mangling de nomes] para garantir que o compilador Rust realmente produza uma função com o nome `_start`. Sem o atributo, o compilador geraria algum símbolo criptografado como `_ZN3blog_os4_start7hb173fedf945531caE` para dar a cada função um nome único. O atributo é necessário porque precisamos dizer o nome da função do ponto de entrada ao linker no próximo passo. + +Também temos que marcar a função como `extern "C"` para dizer ao compilador que ele deve usar a [convenção de chamada C] para esta função (em vez da convenção de chamada Rust não especificada). A razão de nomear a função `_start` é que este é o nome do ponto de entrada padrão para a maioria dos sistemas. + +[mangling de nomes]: https://en.wikipedia.org/wiki/Name_mangling +[convenção de chamada C]: https://en.wikipedia.org/wiki/Calling_convention + +O tipo de retorno `!` significa que a função é divergente, ou seja, não é permitida retornar nunca. Isso é necessário porque o ponto de entrada não é chamado por nenhuma função, mas invocado diretamente pelo sistema operacional ou bootloader. Então em vez de retornar, o ponto de entrada deve por exemplo invocar a [chamada de sistema `exit`] do sistema operacional. No nosso caso, desligar a máquina poderia ser uma ação razoável, já que não há nada mais a fazer se um binário independente retorna. Por enquanto, cumprimos o requisito fazendo um loop infinito. + +[chamada de sistema `exit`]: https://en.wikipedia.org/wiki/Exit_(system_call) + +Quando executamos `cargo build` agora, recebemos um feio erro de _linker_. + +## Erros do Linker + +O linker é um programa que combina o código gerado em um executável. Como o formato executável difere entre Linux, Windows e macOS, cada sistema tem seu próprio linker que lança um erro diferente. A causa fundamental dos erros é a mesma: a configuração padrão do linker assume que nosso programa depende do runtime C, o que não é o caso. + +Para resolver os erros, precisamos dizer ao linker que ele não deve incluir o runtime C. Podemos fazer isso passando um certo conjunto de argumentos ao linker ou compilando para um alvo bare metal. + +### Compilando para um Alvo Bare Metal + +Por padrão, Rust tenta construir um executável que seja capaz de executar no seu ambiente de sistema atual. Por exemplo, se você estiver usando Windows em `x86_64`, Rust tenta construir um executável `.exe` Windows que usa instruções `x86_64`. Este ambiente é chamado seu sistema "host". + +Para descrever ambientes diferentes, Rust usa uma string chamada [_target triple_]. Você pode ver o target triple do seu sistema host executando `rustc --version --verbose`: + +[_target triple_]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple + +``` +rustc 1.91.0 (f8297e351 2025-10-28) +binary: rustc +commit-hash: f8297e351a40c1439a467bbbb6879088047f50b3 +commit-date: 2025-10-28 +host: x86_64-unknown-linux-gnu +release: 1.91.0 +LLVM version: 21.1.2 +``` + +A saída acima é de um sistema Linux `x86_64`. Vemos que o triple `host` é `x86_64-unknown-linux-gnu`, que inclui a arquitetura de CPU (`x86_64`), o vendor (`unknown`), o sistema operacional (`linux`), e a [ABI] (`gnu`). + +[ABI]: https://en.wikipedia.org/wiki/Application_binary_interface + +Ao compilar para nosso triple host, o compilador Rust e o linker assumem que há um sistema operacional subjacente como Linux ou Windows que usa o runtime C por padrão, o que causa os erros do linker. Então, para evitar os erros do linker, podemos compilar para um ambiente diferente sem nenhum sistema operacional subjacente. + +Um exemplo de tal ambiente bare metal é o target triple `thumbv7em-none-eabihf`, que descreve um sistema [embarcado] [ARM]. Os detalhes não são importantes, tudo o que importa é que o target triple não tem nenhum sistema operacional subjacente, o que é indicado pelo `none` no target triple. Para ser capaz de compilar para este alvo, precisamos adicioná-lo em rustup: + +[embarcado]: https://en.wikipedia.org/wiki/Embedded_system +[ARM]: https://en.wikipedia.org/wiki/ARM_architecture + +``` +rustup target add thumbv7em-none-eabihf +``` + +Isso baixa uma cópia da biblioteca padrão std (e core) para o sistema. Agora podemos compilar nosso executável independente para este alvo: + +``` +cargo build --target thumbv7em-none-eabihf +``` + +Ao passar um argumento `--target`, nós fazemos uma compilação [cross compile] nosso executável para um sistema alvo bare metal. Como o sistema alvo não tem sistema operacional, o linker não tenta vincular o runtime C e nossa compilação é bem-sucedida sem nenhum erro de linker. + +[cross compile]: https://en.wikipedia.org/wiki/Cross_compiler + +Esta é a abordagem que usaremos para construir nosso kernel de SO. Em vez de `thumbv7em-none-eabihf`, usaremos um [alvo customizado] que descreve um ambiente bare metal `x86_64`. Os detalhes serão explicados no próximo post. + +[alvo customizado]: https://doc.rust-lang.org/rustc/targets/custom.html + +### Argumentos do Linker + +Em vez de compilar para um sistema bare metal, também é possível resolver os erros do linker passando um certo conjunto de argumentos ao linker. Esta não é a abordagem que usaremos para nosso kernel, portanto esta seção é opcional e fornecida apenas para completude. Clique em _"Argumentos do Linker"_ abaixo para mostrar o conteúdo opcional. + +
+ +Argumentos do Linker + +Nesta seção discutimos os erros do linker que ocorrem no Linux, Windows e macOS, e explicamos como resolvê-los passando argumentos adicionais ao linker. Note que o formato executável e o linker diferem entre sistemas operacionais, então que um conjunto diferente de argumentos é necessário para cada sistema. + +#### Linux + +No Linux, o seguinte erro de linker ocorre (encurtado): + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x12): undefined reference to `__libc_csu_fini' + /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x19): undefined reference to `__libc_csu_init' + /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start': + (.text+0x25): undefined reference to `__libc_start_main' + collect2: error: ld returned 1 exit status +``` + +O problema é que o linker inclui a rotina de inicialização do runtime C por padrão, que também é chamada `_start`. Ela requer alguns símbolos da biblioteca padrão C `libc` que não incluímos devido ao atributo `no_std`, portanto o linker não consegue resolver estas referências. Para resolver isso, podemos dizer ao linker que ele não deve vincular a rotina de inicialização C passando a flag `-nostartfiles`. + +Uma forma de passar atributos de linker via cargo é o comando `cargo rustc`. O comando se comporta exatamente como `cargo build`, mas permite passar opções para `rustc`, o compilador Rust subjacente. `rustc` tem a flag `-C link-arg`, que passa um argumento ao linker. Combinados, nosso novo comando de compilação se parece com isso: + +``` +cargo rustc -- -C link-arg=-nostartfiles +``` + +Agora nossa crate compilada como um executável independente no Linux! + +Não precisávamos especificar o nome da nossa função de ponto de entrada explicitamente, já que o linker procura por uma função com o nome `_start` por padrão. + +#### Windows + +No Windows, um erro de linker diferente ocorre (encurtado): + +``` +error: linking with `link.exe` failed: exit code: 1561 + | + = note: "C:\\Program Files (x86)\\…\\link.exe" […] + = note: LINK : fatal error LNK1561: entry point must be defined +``` + +O erro "entry point must be defined" (ponto de entrada deve ser definido) significa que o linker não consegue encontrar o ponto de entrada. No Windows, o nome do ponto de entrada padrão [depende do subsistema usado][windows-subsystems]. Para o subsistema `CONSOLE`, o linker procura por uma função nomeada `mainCRTStartup` e para o subsistema `WINDOWS`, ele procura por uma função nomeada `WinMainCRTStartup`. Para sobrescrever o padrão e dizer ao linker para procurar por nossa função `_start` em vez disso, podemos passar um argumento `/ENTRY` ao linker: + +[windows-subsystems]: https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol + +``` +cargo rustc -- -C link-arg=/ENTRY:_start +``` + +Do formato de argumento diferente, vemos claramente que o linker Windows é um programa completamente diferente do linker Linux. + +Agora um erro de linker diferente ocorre: + +``` +error: linking with `link.exe` failed: exit code: 1221 + | + = note: "C:\\Program Files (x86)\\…\\link.exe" […] + = note: LINK : fatal error LNK1221: a subsystem can't be inferred and must be + defined +``` + +Este erro ocorre porque os executáveis Windows podem usar [subsistemas] diferentes[windows-subsystems]. Para programas normais, eles são inferidos dependendo do nome do ponto de entrada: Se o ponto de entrada é nomeado `main`, o subsistema `CONSOLE` é usado, e se o ponto de entrada é nomeado `WinMain`, o subsistema `WINDOWS` é usado. Como nossa função `_start` tem um nome diferente, precisamos especificar o subsistema explicitamente: + +``` +cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console" +``` + +Usamos o subsistema `CONSOLE` aqui, mas o subsistema `WINDOWS` funcionaria também. Em vez de passar `-C link-arg` múltiplas vezes, usamos `-C link-args` que leva uma lista de argumentos separados por espaço. + +Com este comando, nosso executável deve compilar com sucesso no Windows. + +#### macOS + +No macOS, o seguinte erro de linker ocorre (encurtado): + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: entry point (_main) undefined. for architecture x86_64 + clang: error: linker command failed with exit code 1 […] +``` + +Esta mensagem de erro nos diz que o linker não consegue encontrar uma função de ponto de entrada com o nome padrão `main` (por alguma razão, todas as funções são prefixadas com um `_` no macOS). Para definir o ponto de entrada para nossa função `_start`, passamos o argumento de linker `-e`: + +``` +cargo rustc -- -C link-args="-e __start" +``` + +A flag `-e` especifica o nome da função de ponto de entrada. Como todas as funções têm um `_` adicional prefixado no macOS, precisamos definir o ponto de entrada para `__start` em vez de `_start`. + +Agora o seguinte erro de linker ocorre: + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: dynamic main executables must link with libSystem.dylib + for architecture x86_64 + clang: error: linker command failed with exit code 1 […] +``` + +macOS [não oferece suporte oficial a binários vinculados estaticamente] e requer que programas vinculem a biblioteca `libSystem` por padrão. Para sobrescrever isto e vincular um binário estático, passamos a flag `-static` ao linker: + +[não oferece suporte oficial a binários vinculados estaticamente]: https://developer.apple.com/library/archive/qa/qa1118/_index.html + +``` +cargo rustc -- -C link-args="-e __start -static" +``` + +Isso ainda não é suficiente, pois um terceiro erro de linker ocorre: + +``` +error: linking with `cc` failed: exit code: 1 + | + = note: "cc" […] + = note: ld: library not found for -lcrt0.o + clang: error: linker command failed with exit code 1 […] +``` + +Este erro ocorre porque programas no macOS vinculam a `crt0` ("C runtime zero") por padrão. Isto é similar ao erro que tivemos no Linux e também pode ser resolvido adicionando o argumento de linker `-nostartfiles`: + +``` +cargo rustc -- -C link-args="-e __start -static -nostartfiles" +``` + +Agora nosso programa deve compilar com sucesso no macOS. + +#### Unificando os Comandos de Compilação + +Agora temos diferentes comandos de compilação dependendo da plataforma host, o que não é o ideal. Para evitar isto, podemos criar um arquivo nomeado `.cargo/config.toml` que contém os argumentos específicos de plataforma: + +```toml +# in .cargo/config.toml + +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-arg=-nostartfiles"] + +[target.'cfg(target_os = "windows")'] +rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"] + +[target.'cfg(target_os = "macos")'] +rustflags = ["-C", "link-args=-e __start -static -nostartfiles"] +``` + +A key `rustflags` contém argumentos que são automaticamente adicionados a cada invocação de `rustc`. Para mais informações sobre o arquivo `.cargo/config.toml`, veja a [documentação oficial](https://doc.rust-lang.org/cargo/reference/config.html). + +Agora nosso programa deve ser compilável em todas as três plataformas com um simples `cargo build`. + +#### Você Deveria Fazer Isto? + +Enquanto é possível construir um executável independente para Linux, Windows e macOS, provavelmente não é uma boa ideia. A razão é que nosso executável ainda espera por várias coisas, por exemplo que uma pilha seja inicializada quando a função `_start` é chamada. Sem o runtime C, alguns desses requisitos podem não ser atendidos, o que pode causar nosso programa falhar, por exemplo através de um segmentation fault. + +Se você quiser criar um binário mínimo que execute em cima de um sistema operacional existente, incluindo `libc` e definindo o atributo `#[start]` conforme descrito [aqui](https://doc.rust-lang.org/1.16.0/book/no-stdlib.html) é provavelmente uma melhor ideia. + +
+ +## Resumo + +Um binário Rust independente mínimo se parece com isto: + +`src/main.rs`: + +```rust +#![no_std] // Não vincule a biblioteca padrão do Rust +#![no_main] // desativar todos os pontos de entrada no nível Rust + +use core::panic::PanicInfo; + +#[unsafe(no_mangle)] // não altere (mangle) o nome desta função +pub extern "C" fn _start() -> ! { + // essa função é o ponto de entrada, já que o vinculador procura uma função + // denominado `_start` por padrão + loop {} +} + +/// Esta função é chamada em caso de pânico. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +``` + +`Cargo.toml`: + +```toml +[package] +name = "crate_name" +version = "0.1.0" +authors = ["Author Name "] + +# o perfil usado para `cargo build` +[profile.dev] +panic = "abort" # desativar o unwinding do stack em caso de pânico + +# o perfil usado para `cargo build --release` +[profile.release] +panic = "abort" # desativar o unwinding do stack em caso de pânico +``` + +Para construir este binário, precisamos compilar para um alvo bare metal como `thumbv7em-none-eabihf`: + +``` +cargo build --target thumbv7em-none-eabihf +``` + +Alternativamente, podemos compilá-lo para o sistema host passando argumentos adicionais de linker: + +```bash +# Linux +cargo rustc -- -C link-arg=-nostartfiles +# Windows +cargo rustc -- -C link-args="/ENTRY:_start /SUBSYSTEM:console" +# macOS +cargo rustc -- -C link-args="-e __start -static -nostartfiles" +``` + +Note que isto é apenas um exemplo mínimo de um binário Rust independente. Este binário espera por várias coisas, por exemplo, que um stack seja inicializado quando a função `_start` é chamada. **Portanto para qualquer uso real de tal binário, mais passos são necessários**. + +## Deixando `rust-analyzer` Feliz + +O projeto [`rust-analyzer`](https://rust-analyzer.github.io/) é uma ótima forma de obter autocompletar e suporte "ir para definição" (e muitos outros recursos) para código Rust no seu editor. +Funciona muito bem para projetos `#![no_std]` também, então recomendo usá-lo para desenvolvimento de kernel! + +Se você estiver usando a funcionalidade [`checkOnSave`](https://rust-analyzer.github.io/book/configuration.html#checkOnSave) de `rust-analyzer` (habilitada por padrão), ela pode relatar um erro para a função panic do nosso kernel: + +``` +found duplicate lang item `panic_impl` +``` + +A razão para este erro é que `rust-analyzer` invoca `cargo check --all-targets` por padrão, que também tenta construir o binário em modo [teste](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) e [benchmark](https://doc.rust-lang.org/rustc/tests/index.html#benchmarks). + +
+ +### Os dois significados de "target" + +A flag `--all-targets` é completamente não relacionada ao argumento `--target`. +Há dois significados diferentes do termo "target" no `cargo`: + +- A flag `--target` especifica o [_alvo de compilação_] que deve ser passado ao compilador `rustc`. Isso deve ser definido como o [target triple] da máquina que deve executar nosso código. +- A flag `--all-targets` referencia o [_alvo do package] do Cargo. Pacotes Cargo podem ser uma biblioteca e binário ao mesmo tempo, então você pode especificar de qual forma você gostaria de construir sua crate. Além disso, Cargo também tem alvos de package para [exemplos](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#examples), [testes](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#tests), e [benchmarks](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#benchmarks). Esses alvos de pacote podem coexistir, então você pode construir/verificar a mesma crate por exemplo em modo biblioteca ou modo teste. + +[_alvo de compilação_]: https://doc.rust-lang.org/rustc/targets/index.html +[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple +[_alvo do package]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html + +
+ +Por padrão, `cargo check` apenas constrói o _biblioteca_ e os alvos de pacote _binário_. +No entanto, `rust-analyzer` escolhe verificar todos os alvos de pacote por padrão quando [`checkOnSave`](https://rust-analyzer.github.io/book/configuration.html#checkOnSave) é habilitado. +Esta é a razão pela qual `rust-analyzer` relata o erro de `lang item` acima que não vemos em `cargo check`. +Se executarmos `cargo check --all-targets`, vemos o erro também: + +``` +error[E0152]: found duplicate lang item `panic_impl` + --> src/main.rs:13:1 + | +13 | / fn panic(_info: &PanicInfo) -> ! { +14 | | loop {} +15 | | } + | |_^ + | + = note: the lang item is first defined in crate `std` (which `test` depends on) + = note: first definition in `std` loaded from /home/[...]/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-8df6be531efb3fd0.rlib + = note: second definition in the local crate (`blog_os`) +``` + +A primeira `note` nos diz que o item de linguagem panic já está definido na crate `std`, que é uma dependência da crate `test`. +A crate `test` é automaticamente incluída ao construir uma crate em [modo teste](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#tests). +Isso não faz sentido para nosso kernel `#![no_std]` já que não há forma de suportar a biblioteca padrão em bare metal. +Então este erro não é relevante para nosso projeto e podemos seguramente ignorá-lo. + +A forma apropriada de evitar este erro é especificar em nosso `Cargo.toml` que nosso binário não suporta construção em modos `test` e `bench`. +Podemos fazer isso adicionando uma seção `[[bin]]` em nosso `Cargo.toml` para [configurar a construção](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target) do nosso binário: + +```toml +# no Cargo.toml + +[[bin]] +name = "blog_os" +test = false +bench = false +``` + +Os colchetes duplos ao redor de `bin` não é um erro, isto é como o formato TOML define chaves que podem aparecer múltiplas vezes. +Como uma crate pode ter múltiplos binários, a seção `[[bin]]` pode aparecer múltiplas vezes em `Cargo.toml` também. +Esta é também a razão para o campo `name` obrigatório, que precisa corresponder ao nome do binário (para que `cargo` saiba quais configurações devem ser aplicadas a qual binário). + +Ao definir os campos [`test`](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-test-field) e [`bench` ](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-bench-field) para `false`, instruímos `cargo` a não construir nosso binário em modo teste ou benchmark. +Agora `cargo check --all-targets` não deve lançar mais erros, e a implementação de `checkOnSave` de `rust-analyzer` também deve estar feliz. + +## O que vem a seguir? + +O [próximo post] explica os passos necessários para transformar nosso binário independente em um kernel mínimo do sistema operacional. Isso inclui criar um alvo customizado, combinar nosso executável com um bootloader, e aprender como imprimir algo na tela. + +[próximo post]: @/edition-2/posts/02-minimal-rust-kernel/index.md diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.pt-BR.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.pt-BR.md new file mode 100644 index 00000000..1d134922 --- /dev/null +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.pt-BR.md @@ -0,0 +1,35 @@ ++++ +title = "Desabilitando a Red Zone" +weight = 1 +path = "pt-BR/red-zone" +template = "edition-2/extra.html" + +[extra] +# Please update this when updating the translation +translation_based_on_commit = "9d079e6d3e03359469d6cf1759bb1a196d8a11ac" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +A [red zone] é uma otimização da [System V ABI] que permite que funções usem temporariamente os 128 bytes abaixo do seu stack frame sem ajustar o ponteiro de pilha: + +[red zone]: https://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64#the-red-zone +[System V ABI]: https://wiki.osdev.org/System_V_ABI + + + +![stack frame com red zone](red-zone.svg) + +A imagem mostra o stack frame de uma função com `n` variáveis locais. Na entrada da função, o ponteiro de pilha é ajustado para abrir espaço na pilha para o endereço de retorno e as variáveis locais. + +A red zone é definida como os 128 bytes abaixo do ponteiro de pilha ajustado. A função pode usar esta área para dados temporários que não são necessários entre chamadas de função. Assim, as duas instruções para ajustar o ponteiro de pilha podem ser evitadas em alguns casos (por exemplo, em pequenas funções folha). + +No entanto, esta otimização leva a problemas enormes com exceções ou interrupções de hardware. Vamos assumir que uma exceção ocorre enquanto uma função usa a red zone: + +![red zone sobrescrita pelo handler de exceção](red-zone-overwrite.svg) + +A CPU e o handler de exceção sobrescrevem os dados na red zone. Mas estes dados ainda são necessários pela função interrompida. Então a função não funcionará mais corretamente quando retornarmos do handler de exceção. Isso pode levar a bugs estranhos que [levam semanas para depurar]. + +[levam semanas para depurar]: https://forum.osdev.org/viewtopic.php?t=21720 + +Para evitar tais bugs quando implementarmos tratamento de exceções no futuro, desabilitamos a red zone logo de início. Isso é alcançado adicionando a linha `"disable-redzone": true` ao nosso arquivo de configuração de alvo. \ No newline at end of file diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.pt-BR.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.pt-BR.md new file mode 100644 index 00000000..a397fe80 --- /dev/null +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.pt-BR.md @@ -0,0 +1,50 @@ ++++ +title = "Desabilitando SIMD" +weight = 2 +path = "pt-BR/disable-simd" +template = "edition-2/extra.html" + +[extra] +# Please update this when updating the translation +translation_based_on_commit = "9d079e6d3e03359469d6cf1759bb1a196d8a11ac" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Instruções [Single Instruction Multiple Data (SIMD)] são capazes de realizar uma operação (por exemplo, adição) simultaneamente em múltiplas palavras de dados, o que pode acelerar programas significativamente. A arquitetura `x86_64` suporta vários padrões SIMD: + +[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD + + + +- [MMX]: O conjunto de instruções _Multi Media Extension_ foi introduzido em 1997 e define oito registradores de 64 bits chamados `mm0` até `mm7`. Esses registradores são apenas aliases para os registradores da [unidade de ponto flutuante x87]. +- [SSE]: O conjunto de instruções _Streaming SIMD Extensions_ foi introduzido em 1999. Em vez de reutilizar os registradores de ponto flutuante, ele adiciona um conjunto de registradores completamente novo. Os dezesseis novos registradores são chamados `xmm0` até `xmm15` e têm 128 bits cada. +- [AVX]: As _Advanced Vector Extensions_ são extensões que aumentam ainda mais o tamanho dos registradores multimídia. Os novos registradores são chamados `ymm0` até `ymm15` e têm 256 bits cada. Eles estendem os registradores `xmm`, então por exemplo `xmm0` é a metade inferior de `ymm0`. + +[MMX]: https://en.wikipedia.org/wiki/MMX_(instruction_set) +[unidade de ponto flutuante x87]: https://en.wikipedia.org/wiki/X87 +[SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions +[AVX]: https://en.wikipedia.org/wiki/Advanced_Vector_Extensions + +Ao usar tais padrões SIMD, programas frequentemente podem acelerar significativamente. Bons compiladores são capazes de transformar loops normais em tal código SIMD automaticamente através de um processo chamado [auto-vetorização]. + +[auto-vetorização]: https://en.wikipedia.org/wiki/Automatic_vectorization + +No entanto, os grandes registradores SIMD levam a problemas em kernels de SO. A razão é que o kernel tem que fazer backup de todos os registradores que usa para a memória em cada interrupção de hardware, porque eles precisam ter seus valores originais quando o programa interrompido continua. Então, se o kernel usa registradores SIMD, ele tem que fazer backup de muito mais dados (512-1600 bytes), o que diminui notavelmente o desempenho. Para evitar esta perda de desempenho, queremos desabilitar os recursos `sse` e `mmx` (o recurso `avx` é desabilitado por padrão). + +Podemos fazer isso através do campo `features` na nossa especificação de alvo. Para desabilitar os recursos `mmx` e `sse`, nós os adicionamos prefixados com um menos: + +```json +"features": "-mmx,-sse" +``` + +## Ponto Flutuante +Infelizmente para nós, a arquitetura `x86_64` usa registradores SSE para operações de ponto flutuante. Assim, todo uso de ponto flutuante com SSE desabilitado causa um erro no LLVM. O problema é que a biblioteca core do Rust já usa floats (por exemplo, ela implementa traits para `f32` e `f64`), então evitar floats no nosso kernel não é suficiente. + +Felizmente, o LLVM tem suporte para um recurso `soft-float` que emula todas as operações de ponto flutuante através de funções de software baseadas em inteiros normais. Isso torna possível usar floats no nosso kernel sem SSE; será apenas um pouco mais lento. + +Para ativar o recurso `soft-float` para o nosso kernel, nós o adicionamos à linha `features` na nossa especificação de alvo, prefixado com um mais: + +```json +"features": "-mmx,-sse,+soft-float" +``` \ No newline at end of file diff --git a/blog/content/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md new file mode 100644 index 00000000..456a0afd --- /dev/null +++ b/blog/content/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md @@ -0,0 +1,504 @@ ++++ +title = "Um Kernel Rust Mínimo" +weight = 2 +path = "pt-BR/minimal-rust-kernel" +date = 2018-02-10 + +[extra] +chapter = "O Básico" +# Please update this when updating the translation +translation_based_on_commit = "95d4fbd54c6b0e5a874981558c0cc1fe85d31606" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Neste post, criamos um kernel Rust mínimo de 64 bits para a arquitetura x86. Construímos sobre o [binário Rust independente] do post anterior para criar uma imagem de disco inicializável que imprime algo na tela. + +[binário Rust independente]: @/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-02`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-02 + + + +## O Processo de Boot +Quando você liga um computador, ele começa a executar código de firmware que está armazenado na [ROM] da placa-mãe. Este código executa um [teste automático de inicialização], detecta a RAM disponível e pré-inicializa a CPU e o hardware. Depois, ele procura por um disco inicializável e começa a inicializar o kernel do sistema operacional. + +[ROM]: https://en.wikipedia.org/wiki/Read-only_memory +[teste automático de inicialização]: https://en.wikipedia.org/wiki/Power-on_self-test + +No x86, existem dois padrões de firmware: o "Basic Input/Output System" (**[BIOS]**) e o mais novo "Unified Extensible Firmware Interface" (**[UEFI]**). O padrão BIOS é antigo e ultrapassado, mas simples e bem suportado em qualquer máquina x86 desde os anos 1980. UEFI, em contraste, é mais moderno e tem muito mais recursos, mas é mais complexo de configurar (na minha opinião, pelo menos). + +[BIOS]: https://en.wikipedia.org/wiki/BIOS +[UEFI]: https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface + +Atualmente, fornecemos apenas suporte para BIOS, mas suporte para UEFI também está planejado. Se você gostaria de nos ajudar com isso, confira o [issue no Github](https://github.com/phil-opp/blog_os/issues/349). + +### Boot BIOS +Quase todos os sistemas x86 têm suporte para boot BIOS, incluindo máquinas mais novas baseadas em UEFI que usam um BIOS emulado. Isso é ótimo, porque você pode usar a mesma lógica de boot em todas as máquinas do último século. Mas essa ampla compatibilidade é ao mesmo tempo a maior desvantagem do boot BIOS, porque significa que a CPU é colocada em um modo de compatibilidade de 16 bits chamado [modo real] antes do boot, para que bootloaders arcaicos dos anos 1980 ainda funcionem. + +Mas vamos começar do início: + +Quando você liga um computador, ele carrega o BIOS de uma memória flash especial localizada na placa-mãe. O BIOS executa rotinas de teste automático e inicialização do hardware, então procura por discos inicializáveis. Se ele encontra um, o controle é transferido para seu _bootloader_, que é uma porção de 512 bytes de código executável armazenado no início do disco. A maioria dos bootloaders é maior que 512 bytes, então os bootloaders são comumente divididos em um primeiro estágio pequeno, que cabe em 512 bytes, e um segundo estágio, que é subsequentemente carregado pelo primeiro estágio. + +O bootloader tem que determinar a localização da imagem do kernel no disco e carregá-la na memória. Ele também precisa mudar a CPU do [modo real] de 16 bits primeiro para o [modo protegido] de 32 bits, e então para o [modo longo] de 64 bits, onde registradores de 64 bits e a memória principal completa estão disponíveis. Seu terceiro trabalho é consultar certas informações (como um mapa de memória) do BIOS e passá-las ao kernel do SO. + +[modo real]: https://en.wikipedia.org/wiki/Real_mode +[modo protegido]: https://en.wikipedia.org/wiki/Protected_mode +[modo longo]: https://en.wikipedia.org/wiki/Long_mode +[segmentação de memória]: https://en.wikipedia.org/wiki/X86_memory_segmentation + +Escrever um bootloader é um pouco trabalhoso, pois requer linguagem assembly e muitos passos pouco intuitivos como "escrever este valor mágico neste registrador do processador". Portanto, não cobrimos a criação de bootloader neste post e em vez disso fornecemos uma ferramenta chamada [bootimage] que anexa automaticamente um bootloader ao seu kernel. + +[bootimage]: https://github.com/rust-osdev/bootimage + +Se você estiver interessado em construir seu próprio bootloader: Fique ligado, um conjunto de posts sobre este tópico já está planejado! + +#### O Padrão Multiboot +Para evitar que todo sistema operacional implemente seu próprio bootloader, que é compatível apenas com um único SO, a [Free Software Foundation] criou um padrão de bootloader aberto chamado [Multiboot] em 1995. O padrão define uma interface entre o bootloader e o sistema operacional, para que qualquer bootloader compatível com Multiboot possa carregar qualquer sistema operacional compatível com Multiboot. A implementação de referência é o [GNU GRUB], que é o bootloader mais popular para sistemas Linux. + +[Free Software Foundation]: https://en.wikipedia.org/wiki/Free_Software_Foundation +[Multiboot]: https://wiki.osdev.org/Multiboot +[GNU GRUB]: https://en.wikipedia.org/wiki/GNU_GRUB + +Para tornar um kernel compatível com Multiboot, basta inserir um chamado [cabeçalho Multiboot] no início do arquivo do kernel. Isso torna muito fácil inicializar um SO a partir do GRUB. No entanto, o GRUB e o padrão Multiboot também têm alguns problemas: + +[cabeçalho Multiboot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format + +- Eles suportam apenas o modo protegido de 32 bits. Isso significa que você ainda tem que fazer a configuração da CPU para mudar para o modo longo de 64 bits. +- Eles são projetados para tornar o bootloader simples em vez do kernel. Por exemplo, o kernel precisa ser vinculado com um [tamanho de página padrão ajustado], porque o GRUB não consegue encontrar o cabeçalho Multiboot caso contrário. Outro exemplo é que as [informações de boot], que são passadas ao kernel, contêm muitas estruturas dependentes de arquitetura em vez de fornecer abstrações limpas. +- Tanto o GRUB quanto o padrão Multiboot são documentados apenas esparsamente. +- O GRUB precisa estar instalado no sistema host para criar uma imagem de disco inicializável a partir do arquivo do kernel. Isso torna o desenvolvimento no Windows ou Mac mais difícil. + +[tamanho de página padrão ajustado]: https://wiki.osdev.org/Multiboot#Multiboot_2 +[informações de boot]: https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format + +Por causa dessas desvantagens, decidimos não usar o GRUB ou o padrão Multiboot. No entanto, planejamos adicionar suporte Multiboot à nossa ferramenta [bootimage], para que seja possível carregar seu kernel em um sistema GRUB também. Se você estiver interessado em escrever um kernel compatível com Multiboot, confira a [primeira edição] desta série de blog. + +[primeira edição]: @/edition-1/_index.md + +### UEFI + +(Não fornecemos suporte UEFI no momento, mas adoraríamos! Se você gostaria de ajudar, por favor nos diga no [issue do Github](https://github.com/phil-opp/blog_os/issues/349).) + +## Um Kernel Mínimo +Agora que sabemos aproximadamente como um computador inicializa, é hora de criar nosso próprio kernel mínimo. Nosso objetivo é criar uma imagem de disco que imprima um "Hello World!" na tela quando inicializada. Fazemos isso estendendo o [binário Rust independente] do post anterior. + +Como você deve se lembrar, construímos o binário independente através do `cargo`, mas dependendo do sistema operacional, precisávamos de nomes de ponto de entrada e flags de compilação diferentes. Isso ocorre porque o `cargo` compila para o _sistema host_ por padrão, ou seja, o sistema em que você está executando. Isso não é algo que queremos para nosso kernel, porque um kernel que executa em cima de, por exemplo, Windows, não faz muito sentido. Em vez disso, queremos compilar para um _sistema alvo_ claramente definido. + +### Instalando o Rust Nightly +O Rust tem três canais de lançamento: _stable_, _beta_ e _nightly_. O Livro do Rust explica a diferença entre esses canais muito bem, então dê uma olhada [aqui](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains). Para construir um sistema operacional, precisaremos de alguns recursos experimentais que estão disponíveis apenas no canal nightly, então precisamos instalar uma versão nightly do Rust. + +Para gerenciar instalações do Rust, eu recomendo fortemente o [rustup]. Ele permite instalar compiladores nightly, beta e stable lado a lado e facilita a atualização deles. Com rustup, você pode usar um compilador nightly para o diretório atual executando `rustup override set nightly`. Alternativamente, você pode adicionar um arquivo chamado `rust-toolchain` com o conteúdo `nightly` ao diretório raiz do projeto. Você pode verificar que tem uma versão nightly instalada executando `rustc --version`: O número da versão deve conter `-nightly` no final. + +[rustup]: https://www.rustup.rs/ + +O compilador nightly nos permite optar por vários recursos experimentais usando as chamadas _feature flags_ no topo do nosso arquivo. Por exemplo, poderíamos habilitar a [macro `asm!`] experimental para assembly inline adicionando `#![feature(asm)]` no topo do nosso `main.rs`. Note que tais recursos experimentais são completamente instáveis, o que significa que versões futuras do Rust podem alterá-los ou removê-los sem aviso prévio. Por esta razão, só os usaremos se absolutamente necessário. + +[macro `asm!`]: https://doc.rust-lang.org/stable/reference/inline-assembly.html + +### Especificação de Alvo +O Cargo suporta diferentes sistemas alvo através do parâmetro `--target`. O alvo é descrito por uma chamada _[target triple]_, que descreve a arquitetura da CPU, o vendor, o sistema operacional e a [ABI]. Por exemplo, o target triple `x86_64-unknown-linux-gnu` descreve um sistema com uma CPU `x86_64`, sem vendor claro, e um sistema operacional Linux com a ABI GNU. O Rust suporta [muitos target triples diferentes][platform-support], incluindo `arm-linux-androideabi` para Android ou [`wasm32-unknown-unknown` para WebAssembly](https://www.hellorust.com/setup/wasm-target/). + +[target triple]: https://clang.llvm.org/docs/CrossCompilation.html#target-triple +[ABI]: https://stackoverflow.com/a/2456882 +[platform-support]: https://forge.rust-lang.org/release/platform-support.html +[custom-targets]: https://doc.rust-lang.org/nightly/rustc/targets/custom.html + +Para nosso sistema alvo, no entanto, precisamos de alguns parâmetros de configuração especiais (por exemplo, nenhum SO subjacente), então nenhum dos [target triples existentes][platform-support] se encaixa. Felizmente, o Rust nos permite definir [nosso próprio alvo][custom-targets] através de um arquivo JSON. Por exemplo, um arquivo JSON que descreve o target `x86_64-unknown-linux-gnu` se parece com isto: + +```json +{ + "llvm-target": "x86_64-unknown-linux-gnu", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": 64, + "target-c-int-width": 32, + "os": "linux", + "executables": true, + "linker-flavor": "gcc", + "pre-link-args": ["-m64"], + "morestack": false +} +``` + +A maioria dos campos é exigida pelo LLVM para gerar código para aquela plataforma. Por exemplo, o campo [`data-layout`] define o tamanho de vários tipos integer, floating point e pointer. Então há campos que o Rust usa para compilação condicional, como `target-pointer-width`. O terceiro tipo de campo define como a crate deve ser construída. Por exemplo, o campo `pre-link-args` especifica argumentos passados ao [linker]. + +[`data-layout`]: https://llvm.org/docs/LangRef.html#data-layout +[linker]: https://en.wikipedia.org/wiki/Linker_(computing) + +Também visamos sistemas `x86_64` com nosso kernel, então nossa especificação de alvo será muito similar à acima. Vamos começar criando um arquivo `x86_64-blog_os.json` (escolha qualquer nome que você goste) com o conteúdo comum: + +```json +{ + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": 64, + "target-c-int-width": 32, + "os": "none", + "executables": true +} +``` + +Note que mudamos o SO no `llvm-target` e no campo `os` para `none`, porque executaremos em bare metal. + +Adicionamos as seguintes entradas relacionadas à compilação: + +```json +"linker-flavor": "ld.lld", +"linker": "rust-lld", +``` + +Em vez de usar o linker padrão da plataforma (que pode não suportar alvos Linux), usamos o linker multiplataforma [LLD] que vem com o Rust para vincular nosso kernel. + +[LLD]: https://lld.llvm.org/ + +```json +"panic-strategy": "abort", +``` + +Esta configuração especifica que o alvo não suporta [stack unwinding] no panic, então em vez disso o programa deve abortar diretamente. Isso tem o mesmo efeito que a opção `panic = "abort"` no nosso Cargo.toml, então podemos removê-la de lá. (Note que, em contraste com a opção Cargo.toml, esta opção de alvo também se aplica quando recompilamos a biblioteca `core` mais adiante neste post. Então, mesmo se você preferir manter a opção Cargo.toml, certifique-se de incluir esta opção.) + +[stack unwinding]: https://www.bogotobogo.com/cplusplus/stackunwinding.php + +```json +"disable-redzone": true, +``` + +Estamos escrevendo um kernel, então precisaremos lidar com interrupções em algum momento. Para fazer isso com segurança, temos que desabilitar uma certa otimização do ponteiro de stack chamada _"red zone"_, porque ela causaria corrupção do stack caso contrário. Para mais informações, veja nosso post separado sobre [desabilitando a red zone]. + +[desabilitando a red zone]: @/edition-2/posts/02-minimal-rust-kernel/disable-red-zone/index.md + +```json +"features": "-mmx,-sse,+soft-float", +``` + +O campo `features` habilita/desabilita recursos do alvo. Desabilitamos os recursos `mmx` e `sse` prefixando-os com um menos e habilitamos o recurso `soft-float` prefixando-o com um mais. Note que não deve haver espaços entre flags diferentes, caso contrário o LLVM falha ao interpretar a string de features. + +Os recursos `mmx` e `sse` determinam suporte para instruções [Single Instruction Multiple Data (SIMD)], que frequentemente podem acelerar programas significativamente. No entanto, usar os grandes registradores SIMD em kernels de SO leva a problemas de desempenho. A razão é que o kernel precisa restaurar todos os registradores ao seu estado original antes de continuar um programa interrompido. Isso significa que o kernel tem que salvar o estado SIMD completo na memória principal em cada chamada de sistema ou interrupção de hardware. Como o estado SIMD é muito grande (512-1600 bytes) e interrupções podem ocorrer com muita frequência, essas operações adicionais de salvar/restaurar prejudicam consideravelmente o desempenho. Para evitar isso, desabilitamos SIMD para nosso kernel (não para aplicações executando em cima!). + +[Single Instruction Multiple Data (SIMD)]: https://en.wikipedia.org/wiki/SIMD + +Um problema com desabilitar SIMD é que operações de ponto flutuante em `x86_64` exigem registradores SIMD por padrão. Para resolver este problema, adicionamos o recurso `soft-float`, que emula todas as operações de ponto flutuante através de funções de software baseadas em inteiros normais. + +Para mais informações, veja nosso post sobre [desabilitando SIMD](@/edition-2/posts/02-minimal-rust-kernel/disable-simd/index.md). + +```json +"rustc-abi": "x86-softfloat" +``` + +Como queremos usar o recurso `soft-float`, também precisamos dizer ao compilador Rust `rustc` que queremos usar a ABI correspondente. Podemos fazer isso definindo o campo `rustc-abi` para `x86-softfloat`. + +#### Juntando Tudo +Nosso arquivo de especificação de alvo agora se parece com isto: + +```json +{ + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": 64, + "target-c-int-width": 32, + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float", + "rustc-abi": "x86-softfloat" +} +``` + +### Construindo nosso Kernel +Compilar para nosso novo alvo usará convenções Linux, já que o linker-flavor ld.lld instrui o llvm a compilar com a flag `-flavor gnu` (para mais opções de linker, veja [a documentação do rustc](https://doc.rust-lang.org/rustc/codegen-options/index.html#linker-flavor)). Isso significa que precisamos de um ponto de entrada chamado `_start` como descrito no [post anterior]: + +[post anterior]: @/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md + +```rust +// src/main.rs + +#![no_std] // não vincule a biblioteca padrão do Rust +#![no_main] // desativar todos os pontos de entrada no nível Rust + +use core::panic::PanicInfo; + +/// Esta função é chamada em caso de pânico. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[unsafe(no_mangle)] // não altere (mangle) o nome desta função +pub extern "C" fn _start() -> ! { + // essa função é o ponto de entrada, já que o vinculador procura uma função + // denominado `_start` por padrão + loop {} +} +``` + +Note que o ponto de entrada precisa ser chamado `_start` independentemente do seu SO host. + +Agora podemos construir o kernel para nosso novo alvo passando o nome do arquivo JSON como `--target`: + +``` +> cargo build --target x86_64-blog_os.json + +error[E0463]: can't find crate for `core` +``` + +Falha! O erro nos diz que o compilador Rust não consegue mais encontrar a [biblioteca `core`]. Esta biblioteca contém tipos básicos do Rust como `Result`, `Option` e iteradores, e é implicitamente vinculada a todas as crates `no_std`. + +[biblioteca `core`]: https://doc.rust-lang.org/nightly/core/index.html + +O problema é que a biblioteca core é distribuída junto com o compilador Rust como uma biblioteca _pré-compilada_. Então ela é válida apenas para target triples host suportados (por exemplo, `x86_64-unknown-linux-gnu`) mas não para nosso alvo customizado. Se quisermos compilar código para outros alvos, precisamos recompilar `core` para esses alvos primeiro. + +#### A Opção `build-std` + +É aí que entra o [recurso `build-std`] do cargo. Ele permite recompilar `core` e outras crates da biblioteca padrão sob demanda, em vez de usar as versões pré-compiladas enviadas com a instalação do Rust. Este recurso é muito novo e ainda não está finalizado, então é marcado como "unstable" e disponível apenas em [compiladores Rust nightly]. + +[recurso `build-std`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std +[compiladores Rust nightly]: #instalando-o-rust-nightly + +Para usar o recurso, precisamos criar um arquivo de [configuração cargo] local em `.cargo/config.toml` (a pasta `.cargo` deve estar ao lado da sua pasta `src`) com o seguinte conteúdo: + +```toml +# em .cargo/config.toml + +[unstable] +build-std = ["core", "compiler_builtins"] +``` + +Isso diz ao cargo que ele deve recompilar as bibliotecas `core` e `compiler_builtins`. Esta última é necessária porque é uma dependência de `core`. Para recompilar essas bibliotecas, o cargo precisa de acesso ao código-fonte do rust, que podemos instalar com `rustup component add rust-src`. + +
+ +**Nota:** A chave de configuração `unstable.build-std` requer pelo menos o Rust nightly de 15-07-2020. + +
+ +Depois de definir a chave de configuração `unstable.build-std` e instalar o componente `rust-src`, podemos executar novamente nosso comando de compilação: + +``` +> cargo build --target x86_64-blog_os.json + Compiling core v0.0.0 (/…/rust/src/libcore) + Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core) + Compiling compiler_builtins v0.1.32 + Compiling blog_os v0.1.0 (/…/blog_os) + Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs +``` + +Vemos que `cargo build` agora recompila as bibliotecas `core`, `rustc-std-workspace-core` (uma dependência de `compiler_builtins`) e `compiler_builtins` para nosso alvo customizado. + +#### Intrínsecos Relacionados a Memória + +O compilador Rust assume que um certo conjunto de funções embutidas está disponível para todos os sistemas. A maioria dessas funções é fornecida pela crate `compiler_builtins` que acabamos de recompilar. No entanto, existem algumas funções relacionadas a memória nessa crate que não são habilitadas por padrão porque normalmente são fornecidas pela biblioteca C no sistema. Essas funções incluem `memset`, que define todos os bytes em um bloco de memória para um valor dado, `memcpy`, que copia um bloco de memória para outro, e `memcmp`, que compara dois blocos de memória. Embora não precisássemos de nenhuma dessas funções para compilar nosso kernel agora, elas serão necessárias assim que adicionarmos mais código a ele (por exemplo, ao copiar structs). + +Como não podemos vincular à biblioteca C do sistema operacional, precisamos de uma maneira alternativa de fornecer essas funções ao compilador. Uma possível abordagem para isso poderia ser implementar nossas próprias funções `memset` etc. e aplicar o atributo `#[unsafe(no_mangle)]` a elas (para evitar a renomeação automática durante a compilação). No entanto, isso é perigoso, pois o menor erro na implementação dessas funções pode levar a undefined behavior. Por exemplo, implementar `memcpy` com um loop `for` pode resultar em recursão infinita porque loops `for` implicitamente chamam o método da trait [`IntoIterator::into_iter`], que pode chamar `memcpy` novamente. Então é uma boa ideia reutilizar implementações existentes e bem testadas em vez disso. + +[`IntoIterator::into_iter`]: https://doc.rust-lang.org/stable/core/iter/trait.IntoIterator.html#tymethod.into_iter + +Felizmente, a crate `compiler_builtins` já contém implementações para todas as funções necessárias, elas estão apenas desabilitadas por padrão para não colidir com as implementações da biblioteca C. Podemos habilitá-las definindo a flag [`build-std-features`] do cargo para `["compiler-builtins-mem"]`. Como a flag `build-std`, esta flag pode ser passada na linha de comando como uma flag `-Z` ou configurada na tabela `unstable` no arquivo `.cargo/config.toml`. Como queremos sempre compilar com esta flag, a opção do arquivo de configuração faz mais sentido para nós: + +[`build-std-features`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features + +```toml +# em .cargo/config.toml + +[unstable] +build-std-features = ["compiler-builtins-mem"] +build-std = ["core", "compiler_builtins"] +``` + +(O suporte para o recurso `compiler-builtins-mem` foi [adicionado muito recentemente](https://github.com/rust-lang/rust/pull/77284), então você precisa pelo menos do Rust nightly `2020-09-30` para ele.) + +Nos bastidores, esta flag habilita o [recurso `mem`] da crate `compiler_builtins`. O efeito disso é que o atributo `#[unsafe(no_mangle)]` é aplicado às [implementações `memcpy` etc.] da crate, o que as torna disponíveis ao linker. + +[recurso `mem`]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55 +[implementações `memcpy` etc.]: https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69 + +Com esta mudança, nosso kernel tem implementações válidas para todas as funções exigidas pelo compilador, então ele continuará a compilar mesmo se nosso código ficar mais complexo. + +#### Definir um Alvo Padrão + +Para evitar passar o parâmetro `--target` em cada invocação de `cargo build`, podemos sobrescrever o alvo padrão. Para fazer isso, adicionamos o seguinte ao nosso arquivo de [configuração cargo] em `.cargo/config.toml`: + +[configuração cargo]: https://doc.rust-lang.org/cargo/reference/config.html + +```toml +# em .cargo/config.toml + +[build] +target = "x86_64-blog_os.json" +``` + +Isso diz ao `cargo` para usar nosso alvo `x86_64-blog_os.json` quando nenhum argumento `--target` explícito é passado. Isso significa que agora podemos construir nosso kernel com um simples `cargo build`. Para mais informações sobre opções de configuração do cargo, confira a [documentação oficial][configuração cargo]. + +Agora podemos construir nosso kernel para um alvo bare metal com um simples `cargo build`. No entanto, nosso ponto de entrada `_start`, que será chamado pelo bootloader, ainda está vazio. É hora de mostrar algo na tela a partir dele. + +### Imprimindo na Tela +A maneira mais fácil de imprimir texto na tela neste estágio é o [buffer de texto VGA]. É uma área de memória especial mapeada para o hardware VGA que contém o conteúdo exibido na tela. Normalmente consiste em 25 linhas que cada uma contém 80 células de caractere. Cada célula de caractere exibe um caractere ASCII com algumas cores de primeiro plano e fundo. A saída da tela se parece com isto: + +[buffer de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode + +![saída de tela para caracteres ASCII comuns](https://upload.wikimedia.org/wikipedia/commons/f/f8/Codepage-437.png) + +Discutiremos o layout exato do buffer VGA no próximo post, onde escreveremos um primeiro pequeno driver para ele. Para imprimir "Hello World!", só precisamos saber que o buffer está localizado no endereço `0xb8000` e que cada célula de caractere consiste em um byte ASCII e um byte de cor. + +A implementação se parece com isto: + +```rust +static HELLO: &[u8] = b"Hello World!"; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + let vga_buffer = 0xb8000 as *mut u8; + + for (i, &byte) in HELLO.iter().enumerate() { + unsafe { + *vga_buffer.offset(i as isize * 2) = byte; + *vga_buffer.offset(i as isize * 2 + 1) = 0xb; + } + } + + loop {} +} +``` + +Primeiro, convertemos o inteiro `0xb8000` em um [ponteiro bruto]. Então [iteramos] sobre os bytes da [byte string] [static] `HELLO`. Usamos o método [`enumerate`] para obter adicionalmente uma variável em execução `i`. No corpo do loop for, usamos o método [`offset`] para escrever o byte da string e o byte de cor correspondente (`0xb` é um ciano claro). + +[iterar]: https://doc.rust-lang.org/stable/book/ch13-02-iterators.html +[static]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate +[byte string]: https://doc.rust-lang.org/reference/tokens.html#byte-string-literals +[ponteiro bruto]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer +[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset + +Note que há um bloco [`unsafe`] em torno de todas as escritas de memória. A razão é que o compilador Rust não pode provar que os ponteiros brutos que criamos são válidos. Eles poderiam apontar para qualquer lugar e levar à corrupção de dados. Ao colocá-los em um bloco `unsafe`, estamos basicamente dizendo ao compilador que temos absoluta certeza de que as operações são válidas. Note que um bloco `unsafe` não desativa as verificações de segurança do Rust. Ele apenas permite que você faça [cinco coisas adicionais]. + +[`unsafe`]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html +[cinco coisas adicionais]: https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#unsafe-superpowers + +Quero enfatizar que **esta não é a maneira como queremos fazer as coisas em Rust!** É muito fácil bagunçar ao trabalhar com ponteiros brutos dentro de blocos unsafe. Por exemplo, poderíamos facilmente escrever além do fim do buffer se não tivermos cuidado. + +Então queremos minimizar o uso de `unsafe` o máximo possível. O Rust nos dá a capacidade de fazer isso criando abstrações seguras. Por exemplo, poderíamos criar um tipo de buffer VGA que encapsula toda a unsafety e garante que seja _impossível_ fazer algo errado de fora. Desta forma, precisaríamos apenas de quantidades mínimas de código `unsafe` e poderíamos ter certeza de que não violamos [memory safety]. Criaremos tal abstração de buffer VGA segura no próximo post. + +[memory safety]: https://en.wikipedia.org/wiki/Memory_safety + +## Executando nosso Kernel + +Agora que temos um executável que faz algo perceptível, é hora de executá-lo. Primeiro, precisamos transformar nosso kernel compilado em uma imagem de disco inicializável vinculando-o com um bootloader. Então podemos executar a imagem de disco na máquina virtual [QEMU] ou inicializá-la em hardware real usando um pendrive USB. + +### Criando uma Bootimage + +Para transformar nosso kernel compilado em uma imagem de disco inicializável, precisamos vinculá-lo com um bootloader. Como aprendemos na [seção sobre boot], o bootloader é responsável por inicializar a CPU e carregar nosso kernel. + +[seção sobre boot]: #o-processo-de-boot + +Em vez de escrever nosso próprio bootloader, que é um projeto por si só, usamos a crate [`bootloader`]. Esta crate implementa um bootloader BIOS básico sem nenhuma dependência C, apenas Rust e assembly inline. Para usá-lo para inicializar nosso kernel, precisamos adicionar uma dependência nele: + +[`bootloader`]: https://crates.io/crates/bootloader + +```toml +# em Cargo.toml + +[dependencies] +bootloader = "0.9" +``` + +**Nota:** Este post é compatível apenas com `bootloader v0.9`. Versões mais novas usam um sistema de compilação diferente e resultarão em erros de compilação ao seguir este post. + +Adicionar o bootloader como uma dependência não é suficiente para realmente criar uma imagem de disco inicializável. O problema é que precisamos vincular nosso kernel com o bootloader após a compilação, mas o cargo não tem suporte para [scripts pós-compilação]. + +[scripts pós-compilação]: https://github.com/rust-lang/cargo/issues/545 + +Para resolver este problema, criamos uma ferramenta chamada `bootimage` que primeiro compila o kernel e o bootloader, e então os vincula juntos para criar uma imagem de disco inicializável. Para instalar a ferramenta, vá para seu diretório home (ou qualquer diretório fora do seu projeto cargo) e execute o seguinte comando no seu terminal: + +``` +cargo install bootimage +``` + +Para executar `bootimage` e construir o bootloader, você precisa ter o componente rustup `llvm-tools-preview` instalado. Você pode fazer isso executando `rustup component add llvm-tools-preview`. + +Depois de instalar `bootimage` e adicionar o componente `llvm-tools-preview`, você pode criar uma imagem de disco inicializável voltando para o diretório do seu projeto cargo e executando: + +``` +> cargo bootimage +``` + +Vemos que a ferramenta recompila nosso kernel usando `cargo build`, então automaticamente pegará quaisquer mudanças que você fizer. Depois, ela compila o bootloader, o que pode demorar um pouco. Como todas as dependências de crate, ele é compilado apenas uma vez e então armazenado em cache, então compilações subsequentes serão muito mais rápidas. Finalmente, `bootimage` combina o bootloader e seu kernel em uma imagem de disco inicializável. + +Após executar o comando, você deve ver uma imagem de disco inicializável chamada `bootimage-blog_os.bin` no seu diretório `target/x86_64-blog_os/debug`. Você pode inicializá-la em uma máquina virtual ou copiá-la para um pendrive USB para inicializá-la em hardware real. (Note que este não é uma imagem de CD, que tem um formato diferente, então gravá-la em um CD não funciona). + +#### Como funciona? +A ferramenta `bootimage` executa os seguintes passos nos bastidores: + +- Ela compila nosso kernel para um arquivo [ELF]. +- Ela compila a dependência do bootloader como um executável autônomo. +- Ela vincula os bytes do arquivo ELF do kernel ao bootloader. + +[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format +[rust-osdev/bootloader]: https://github.com/rust-osdev/bootloader + +Quando inicializado, o bootloader lê e analisa o arquivo ELF anexado. Ele então mapeia os segmentos do programa para endereços virtuais nas tabelas de página, zera a seção `.bss` e configura um stack. Finalmente, ele lê o endereço do ponto de entrada (nossa função `_start`) e salta para ele. + +### Inicializando no QEMU + +Agora podemos inicializar a imagem de disco em uma máquina virtual. Para inicializá-la no [QEMU], execute o seguinte comando: + +[QEMU]: https://www.qemu.org/ + +``` +> qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin +``` + +Isso abre uma janela separada que deve se parecer com isto: + +![QEMU mostrando "Hello World!"](qemu.png) + +Vemos que nosso "Hello World!" está visível na tela. + +### Máquina Real + +Também é possível escrevê-lo em um pendrive USB e inicializá-lo em uma máquina real, **mas tenha cuidado** para escolher o nome correto do dispositivo, porque **tudo naquele dispositivo será sobrescrito**: + +``` +> dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX && sync +``` + +Onde `sdX` é o nome do dispositivo do seu pendrive USB. + +Depois de escrever a imagem no pendrive USB, você pode executá-la em hardware real inicializando a partir dele. Você provavelmente precisará usar um menu de boot especial ou alterar a ordem de boot na configuração do BIOS para inicializar a partir do pendrive USB. Note que atualmente não funciona para máquinas UEFI, já que a crate `bootloader` ainda não tem suporte UEFI. + +### Usando `cargo run` + +Para facilitar a execução do nosso kernel no QEMU, podemos definir a chave de configuração `runner` para o cargo: + +```toml +# em .cargo/config.toml + +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" +``` + +A tabela `target.'cfg(target_os = "none")'` se aplica a todos os alvos cujo campo `"os"` do arquivo de configuração de alvo está definido como `"none"`. Isso inclui nosso alvo `x86_64-blog_os.json`. A chave `runner` especifica o comando que deve ser invocado para `cargo run`. O comando é executado após uma compilação bem-sucedida com o caminho do executável passado como o primeiro argumento. Veja a [documentação do cargo][configuração cargo] para mais detalhes. + +O comando `bootimage runner` é especificamente projetado para ser utilizável como um executável `runner`. Ele vincula o executável dado com a dependência do bootloader do projeto e então lança o QEMU. Veja o [Readme do `bootimage`] para mais detalhes e opções de configuração possíveis. + +[Readme do `bootimage`]: https://github.com/rust-osdev/bootimage + +Agora podemos usar `cargo run` para compilar nosso kernel e inicializá-lo no QEMU. + +## O que vem a seguir? + +No próximo post, exploraremos o buffer de texto VGA em mais detalhes e escreveremos uma interface segura para ele. Também adicionaremos suporte para a macro `println`. \ No newline at end of file diff --git a/blog/content/edition-2/posts/03-vga-text-buffer/index.pt-BR.md b/blog/content/edition-2/posts/03-vga-text-buffer/index.pt-BR.md new file mode 100644 index 00000000..225a71bf --- /dev/null +++ b/blog/content/edition-2/posts/03-vga-text-buffer/index.pt-BR.md @@ -0,0 +1,703 @@ ++++ +title = "Modo de Texto VGA" +weight = 3 +path = "pt-BR/vga-text-mode" +date = 2018-02-26 + +[extra] +chapter = "O Básico" +# Please update this when updating the translation +translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +O [modo de texto VGA] é uma maneira simples de imprimir texto na tela. Neste post, criamos uma interface que torna seu uso seguro e simples ao encapsular toda a unsafety em um módulo separado. Também implementamos suporte para as [macros de formatação] do Rust. + +[modo de texto VGA]: https://en.wikipedia.org/wiki/VGA-compatible_text_mode +[macros de formatação]: https://doc.rust-lang.org/std/fmt/#related-macros + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-03`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-03 + + + +## O Buffer de Texto VGA +Para imprimir um caractere na tela em modo de texto VGA, é preciso escrevê-lo no buffer de texto do hardware VGA. O buffer de texto VGA é um array bidimensional com tipicamente 25 linhas e 80 colunas, que é renderizado diretamente na tela. Cada entrada do array descreve um único caractere da tela através do seguinte formato: + +Bit(s) | Valor +------ | ---------------- +0-7 | Ponto de código ASCII +8-11 | Cor do primeiro plano +12-14 | Cor do fundo +15 | Piscar + +O primeiro byte representa o caractere que deve ser impresso na [codificação ASCII]. Para ser mais específico, não é exatamente ASCII, mas um conjunto de caracteres chamado [_página de código 437_] com alguns caracteres adicionais e pequenas modificações. Para simplificar, continuaremos chamando-o de caractere ASCII neste post. + +[codificação ASCII]: https://en.wikipedia.org/wiki/ASCII +[_página de código 437_]: https://en.wikipedia.org/wiki/Code_page_437 + +O segundo byte define como o caractere é exibido. Os primeiros quatro bits definem a cor do primeiro plano, os próximos três bits a cor do fundo, e o último bit se o caractere deve piscar. As seguintes cores estão disponíveis: + +Número | Cor | Número + Bit Brilhante | Cor Brilhante +------ | ------------- | ---------------------- | -------------- +0x0 | Preto | 0x8 | Cinza Escuro +0x1 | Azul | 0x9 | Azul Claro +0x2 | Verde | 0xa | Verde Claro +0x3 | Ciano | 0xb | Ciano Claro +0x4 | Vermelho | 0xc | Vermelho Claro +0x5 | Magenta | 0xd | Rosa +0x6 | Marrom | 0xe | Amarelo +0x7 | Cinza Claro | 0xf | Branco + +O bit 4 é o _bit brilhante_, que transforma, por exemplo, azul em azul claro. Para a cor de fundo, este bit é reaproveitado como o bit de piscar. + +O buffer de texto VGA é acessível via [I/O mapeado em memória] no endereço `0xb8000`. Isso significa que leituras e escritas naquele endereço não acessam a RAM, mas acessam diretamente o buffer de texto no hardware VGA. Isso significa que podemos lê-lo e escrevê-lo através de operações normais de memória naquele endereço. + +[I/O mapeado em memória]: https://en.wikipedia.org/wiki/Memory-mapped_I/O + +Note que hardware mapeado em memória pode não suportar todas as operações normais de RAM. Por exemplo, um dispositivo poderia suportar apenas leituras byte a byte e retornar lixo quando um `u64` é lido. Felizmente, o buffer de texto [suporta leituras e escritas normais], então não precisamos tratá-lo de maneira especial. + +[suporta leituras e escritas normais]: https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip + +## Um Módulo Rust +Agora que sabemos como o buffer VGA funciona, podemos criar um módulo Rust para lidar com a impressão: + +```rust +// em src/main.rs +mod vga_buffer; +``` + +Para o conteúdo deste módulo, criamos um novo arquivo `src/vga_buffer.rs`. Todo o código abaixo vai para nosso novo módulo (a menos que especificado o contrário). + +### Cores +Primeiro, representamos as diferentes cores usando um enum: + +```rust +// em src/vga_buffer.rs + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Color { + Black = 0, + Blue = 1, + Green = 2, + Cyan = 3, + Red = 4, + Magenta = 5, + Brown = 6, + LightGray = 7, + DarkGray = 8, + LightBlue = 9, + LightGreen = 10, + LightCyan = 11, + LightRed = 12, + Pink = 13, + Yellow = 14, + White = 15, +} +``` +Usamos um [enum estilo C] aqui para especificar explicitamente o número para cada cor. Por causa do atributo `repr(u8)`, cada variante do enum é armazenada como um `u8`. Na verdade, 4 bits seriam suficientes, mas Rust não tem um tipo `u4`. + +[enum estilo C]: https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html + +Normalmente o compilador emitiria um aviso para cada variante não utilizada. Ao usar o atributo `#[allow(dead_code)]`, desabilitamos esses avisos para o enum `Color`. + +Ao [derivar] as traits [`Copy`], [`Clone`], [`Debug`], [`PartialEq`] e [`Eq`], habilitamos [semântica de cópia] para o tipo e o tornamos imprimível e comparável. + +[derivar]: https://doc.rust-lang.org/rust-by-example/trait/derive.html +[`Copy`]: https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html +[`Clone`]: https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html +[`Debug`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html +[`PartialEq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html +[`Eq`]: https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html +[semântica de cópia]: https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types + +Para representar um código de cor completo que especifica as cores de primeiro plano e de fundo, criamos um [newtype] em cima de `u8`: + +[newtype]: https://doc.rust-lang.org/rust-by-example/generics/new_types.html + +```rust +// em src/vga_buffer.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] +struct ColorCode(u8); + +impl ColorCode { + fn new(foreground: Color, background: Color) -> ColorCode { + ColorCode((background as u8) << 4 | (foreground as u8)) + } +} +``` +A struct `ColorCode` contém o byte de cor completo, contendo as cores de primeiro plano e de fundo. Como antes, derivamos as traits `Copy` e `Debug` para ela. Para garantir que o `ColorCode` tenha exatamente o mesmo layout de dados que um `u8`, usamos o atributo [`repr(transparent)`]. + +[`repr(transparent)`]: https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent + +### Buffer de Texto +Agora podemos adicionar estruturas para representar um caractere da tela e o buffer de texto: + +```rust +// em src/vga_buffer.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(C)] +struct ScreenChar { + ascii_character: u8, + color_code: ColorCode, +} + +const BUFFER_HEIGHT: usize = 25; +const BUFFER_WIDTH: usize = 80; + +#[repr(transparent)] +struct Buffer { + chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT], +} +``` +Como a ordenação dos campos em structs padrão é indefinida em Rust, precisamos do atributo [`repr(C)`]. Ele garante que os campos da struct sejam dispostos exatamente como em uma struct C e, portanto, garante a ordenação correta dos campos. Para a struct `Buffer`, usamos [`repr(transparent)`] novamente para garantir que ela tenha o mesmo layout de memória que seu único campo. + +[`repr(C)`]: https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc + +Para realmente escrever na tela, agora criamos um tipo writer: + +```rust +// em src/vga_buffer.rs + +pub struct Writer { + column_position: usize, + color_code: ColorCode, + buffer: &'static mut Buffer, +} +``` +O writer sempre escreverá na última linha e deslocará as linhas para cima quando uma linha estiver cheia (ou no `\n`). O campo `column_position` acompanha a posição atual na última linha. As cores de primeiro plano e de fundo atuais são especificadas por `color_code` e uma referência ao buffer VGA é armazenada em `buffer`. Note que precisamos de um [lifetime explícito] aqui para dizer ao compilador por quanto tempo a referência é válida. O lifetime [`'static`] especifica que a referência é válida durante toda a execução do programa (o que é verdade para o buffer de texto VGA). + +[lifetime explícito]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax +[`'static`]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime + +### Impressão +Agora podemos usar o `Writer` para modificar os caracteres do buffer. Primeiro criamos um método para escrever um único byte ASCII: + +```rust +// em src/vga_buffer.rs + +impl Writer { + pub fn write_byte(&mut self, byte: u8) { + match byte { + b'\n' => self.new_line(), + byte => { + if self.column_position >= BUFFER_WIDTH { + self.new_line(); + } + + let row = BUFFER_HEIGHT - 1; + let col = self.column_position; + + let color_code = self.color_code; + self.buffer.chars[row][col] = ScreenChar { + ascii_character: byte, + color_code, + }; + self.column_position += 1; + } + } + } + + fn new_line(&mut self) {/* TODO */} +} +``` +Se o byte é o byte de [newline] `\n`, o writer não imprime nada. Em vez disso, ele chama um método `new_line`, que implementaremos mais tarde. Outros bytes são impressos na tela no segundo caso `match`. + +[newline]: https://en.wikipedia.org/wiki/Newline + +Ao imprimir um byte, o writer verifica se a linha atual está cheia. Nesse caso, uma chamada `new_line` é usada para quebrar a linha. Então ele escreve um novo `ScreenChar` no buffer na posição atual. Finalmente, a posição da coluna atual é avançada. + +Para imprimir strings inteiras, podemos convertê-las em bytes e imprimi-los um por um: + +```rust +// em src/vga_buffer.rs + +impl Writer { + pub fn write_string(&mut self, s: &str) { + for byte in s.bytes() { + match byte { + // byte ASCII imprimível ou newline + 0x20..=0x7e | b'\n' => self.write_byte(byte), + // não faz parte da faixa ASCII imprimível + _ => self.write_byte(0xfe), + } + + } + } +} +``` + +O buffer de texto VGA suporta apenas ASCII e os bytes adicionais da [página de código 437]. Strings Rust são [UTF-8] por padrão, então podem conter bytes que não são suportados pelo buffer de texto VGA. Usamos um `match` para diferenciar bytes ASCII imprimíveis (um newline ou qualquer coisa entre um caractere de espaço e um caractere `~`) e bytes não imprimíveis. Para bytes não imprimíveis, imprimimos um caractere `■`, que tem o código hexadecimal `0xfe` no hardware VGA. + +[página de código 437]: https://en.wikipedia.org/wiki/Code_page_437 +[UTF-8]: https://www.fileformat.info/info/unicode/utf8.htm + +#### Experimente! +Para escrever alguns caracteres na tela, você pode criar uma função temporária: + +```rust +// em src/vga_buffer.rs + +pub fn print_something() { + let mut writer = Writer { + column_position: 0, + color_code: ColorCode::new(Color::Yellow, Color::Black), + buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + }; + + writer.write_byte(b'H'); + writer.write_string("ello "); + writer.write_string("Wörld!"); +} +``` +Primeiro ele cria um novo Writer que aponta para o buffer VGA em `0xb8000`. A sintaxe para isso pode parecer um pouco estranha: Primeiro, convertemos o inteiro `0xb8000` como um [ponteiro bruto] mutável. Então o convertemos em uma referência mutável ao desreferenciá-lo (através de `*`) e imediatamente emprestar novamente (através de `&mut`). Esta conversão requer um [bloco `unsafe`], pois o compilador não pode garantir que o ponteiro bruto é válido. + +[ponteiro bruto]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer +[bloco `unsafe`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html + +Então ele escreve o byte `b'H'` nele. O prefixo `b` cria um [byte literal], que representa um caractere ASCII. Ao escrever as strings `"ello "` e `"Wörld!"`, testamos nosso método `write_string` e o tratamento de caracteres não imprimíveis. Para ver a saída, precisamos chamar a função `print_something` da nossa função `_start`: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + vga_buffer::print_something(); + + loop {} +} +``` + +Quando executamos nosso projeto agora, um `Hello W■■rld!` deve ser impresso no canto inferior _esquerdo_ da tela em amarelo: + +[byte literal]: https://doc.rust-lang.org/reference/tokens.html#byte-literals + +![QEMU exibindo um `Hello W■■rld!` amarelo no canto inferior esquerdo](vga-hello.png) + +Note que o `ö` é impresso como dois caracteres `■`. Isso ocorre porque `ö` é representado por dois bytes em [UTF-8], que ambos não se enquadram na faixa ASCII imprimível. Na verdade, esta é uma propriedade fundamental do UTF-8: os bytes individuais de valores multi-byte nunca são ASCII válido. + +### Volatile +Acabamos de ver que nossa mensagem foi impressa corretamente. No entanto, pode não funcionar com futuros compiladores Rust que otimizam de forma mais agressiva. + +O problema é que escrevemos apenas no `Buffer` e nunca lemos dele novamente. O compilador não sabe que realmente acessamos memória do buffer VGA (em vez de RAM normal) e não sabe nada sobre o efeito colateral de que alguns caracteres aparecem na tela. Então ele pode decidir que essas escritas são desnecessárias e podem ser omitidas. Para evitar esta otimização errônea, precisamos especificar essas escritas como _[volatile]_. Isso diz ao compilador que a escrita tem efeitos colaterais e não deve ser otimizada. + +[volatile]: https://en.wikipedia.org/wiki/Volatile_(computer_programming) + +Para usar escritas volatile para o buffer VGA, usamos a biblioteca [volatile][volatile crate]. Esta _crate_ (é assim que os pacotes são chamados no mundo Rust) fornece um tipo wrapper `Volatile` com métodos `read` e `write`. Esses métodos usam internamente as funções [read_volatile] e [write_volatile] da biblioteca core e, portanto, garantem que as leituras/escritas não sejam otimizadas. + +[volatile crate]: https://docs.rs/volatile +[read_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.read_volatile.html +[write_volatile]: https://doc.rust-lang.org/nightly/core/ptr/fn.write_volatile.html + +Podemos adicionar uma dependência na crate `volatile` adicionando-a à seção `dependencies` do nosso `Cargo.toml`: + +```toml +# em Cargo.toml + +[dependencies] +volatile = "0.2.6" +``` + +Certifique-se de especificar a versão `0.2.6` do `volatile`. Versões mais novas da crate não são compatíveis com este post. +`0.2.6` é o número de versão [semântico]. Para mais informações, veja o guia [Specifying Dependencies] da documentação do cargo. + +[semântico]: https://semver.org/ +[Specifying Dependencies]: https://doc.crates.io/specifying-dependencies.html + +Vamos usá-lo para tornar as escritas no buffer VGA volatile. Atualizamos nosso tipo `Buffer` da seguinte forma: + +```rust +// em src/vga_buffer.rs + +use volatile::Volatile; + +struct Buffer { + chars: [[Volatile; BUFFER_WIDTH]; BUFFER_HEIGHT], +} +``` +Em vez de um `ScreenChar`, agora estamos usando um `Volatile`. (O tipo `Volatile` é [genérico] e pode envolver (quase) qualquer tipo). Isso garante que não possamos escrever nele "normalmente" acidentalmente. Em vez disso, temos que usar o método `write` agora. + +[genérico]: https://doc.rust-lang.org/book/ch10-01-syntax.html + +Isso significa que temos que atualizar nosso método `Writer::write_byte`: + +```rust +// em src/vga_buffer.rs + +impl Writer { + pub fn write_byte(&mut self, byte: u8) { + match byte { + b'\n' => self.new_line(), + byte => { + ... + + self.buffer.chars[row][col].write(ScreenChar { + ascii_character: byte, + color_code, + }); + ... + } + } + } + ... +} +``` + +Em vez de uma atribuição típica usando `=`, agora estamos usando o método `write`. Agora podemos garantir que o compilador nunca otimizará esta escrita. + +### Macros de Formatação +Seria bom suportar as macros de formatação do Rust também. Dessa forma, podemos facilmente imprimir diferentes tipos, como inteiros ou floats. Para suportá-las, precisamos implementar a trait [`core::fmt::Write`]. O único método necessário desta trait é `write_str`, que se parece bastante com nosso método `write_string`, apenas com um tipo de retorno `fmt::Result`: + +[`core::fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html + +```rust +// em src/vga_buffer.rs + +use core::fmt; + +impl fmt::Write for Writer { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.write_string(s); + Ok(()) + } +} +``` +O `Ok(())` é apenas um Result `Ok` contendo o tipo `()`. + +Agora podemos usar as macros de formatação `write!`/`writeln!` embutidas do Rust: + +```rust +// em src/vga_buffer.rs + +pub fn print_something() { + use core::fmt::Write; + let mut writer = Writer { + column_position: 0, + color_code: ColorCode::new(Color::Yellow, Color::Black), + buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + }; + + writer.write_byte(b'H'); + writer.write_string("ello! "); + write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap(); +} +``` + +Agora você deve ver um `Hello! The numbers are 42 and 0.3333333333333333` na parte inferior da tela. A chamada `write!` retorna um `Result` que causa um aviso se não usado, então chamamos a função [`unwrap`] nele, que entra em panic se ocorrer um erro. Isso não é um problema no nosso caso, pois escritas no buffer VGA nunca falham. + +[`unwrap`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap + +### Newlines +Agora, simplesmente ignoramos newlines e caracteres que não cabem mais na linha. Em vez disso, queremos mover cada caractere uma linha para cima (a linha superior é excluída) e começar no início da última linha novamente. Para fazer isso, adicionamos uma implementação para o método `new_line` do `Writer`: + +```rust +// em src/vga_buffer.rs + +impl Writer { + fn new_line(&mut self) { + for row in 1..BUFFER_HEIGHT { + for col in 0..BUFFER_WIDTH { + let character = self.buffer.chars[row][col].read(); + self.buffer.chars[row - 1][col].write(character); + } + } + self.clear_row(BUFFER_HEIGHT - 1); + self.column_position = 0; + } + + fn clear_row(&mut self, row: usize) {/* TODO */} +} +``` +Iteramos sobre todos os caracteres da tela e movemos cada caractere uma linha para cima. Note que o limite superior da notação de intervalo (`..`) é exclusivo. Também omitimos a linha 0 (o primeiro intervalo começa em `1`) porque é a linha que é deslocada para fora da tela. + +Para finalizar o código de newline, adicionamos o método `clear_row`: + +```rust +// em src/vga_buffer.rs + +impl Writer { + fn clear_row(&mut self, row: usize) { + let blank = ScreenChar { + ascii_character: b' ', + color_code: self.color_code, + }; + for col in 0..BUFFER_WIDTH { + self.buffer.chars[row][col].write(blank); + } + } +} +``` +Este método limpa uma linha sobrescrevendo todos os seus caracteres com um caractere de espaço. + +## Uma Interface Global +Para fornecer um writer global que possa ser usado como uma interface de outros módulos sem carregar uma instância `Writer` por aí, tentamos criar um `WRITER` static: + +```rust +// em src/vga_buffer.rs + +pub static WRITER: Writer = Writer { + column_position: 0, + color_code: ColorCode::new(Color::Yellow, Color::Black), + buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, +}; +``` + +No entanto, se tentarmos compilá-lo agora, os seguintes erros ocorrem: + +``` +error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants + --> src/vga_buffer.rs:7:17 + | +7 | color_code: ColorCode::new(Color::Yellow, Color::Black), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error[E0396]: raw pointers cannot be dereferenced in statics + --> src/vga_buffer.rs:8:22 + | +8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant + +error[E0017]: references in statics may only refer to immutable values + --> src/vga_buffer.rs:8:22 + | +8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values + +error[E0017]: references in statics may only refer to immutable values + --> src/vga_buffer.rs:8:13 + | +8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values +``` + +Para entender o que está acontecendo aqui, precisamos saber que statics são inicializados em tempo de compilação, ao contrário de variáveis normais que são inicializadas em tempo de execução. O componente do compilador Rust que avalia tais expressões de inicialização é chamado de "[const evaluator]". Sua funcionalidade ainda é limitada, mas há trabalho contínuo para expandi-la, por exemplo no RFC "[Allow panicking in constants]". + +[const evaluator]: https://rustc-dev-guide.rust-lang.org/const-eval.html +[Allow panicking in constants]: https://github.com/rust-lang/rfcs/pull/2345 + +O problema com `ColorCode::new` seria solucionável usando [funções `const`], mas o problema fundamental aqui é que o const evaluator do Rust não é capaz de converter ponteiros brutos em referências em tempo de compilação. Talvez funcione algum dia, mas até lá, precisamos encontrar outra solução. + +[funções `const`]: https://doc.rust-lang.org/reference/const_eval.html#const-functions + +### Lazy Statics +A inicialização única de statics com funções não const é um problema comum em Rust. Felizmente, já existe uma boa solução em uma crate chamada [lazy_static]. Esta crate fornece uma macro `lazy_static!` que define um `static` inicializado lazily. Em vez de calcular seu valor em tempo de compilação, o `static` se inicializa lazily quando é acessado pela primeira vez. Assim, a inicialização acontece em tempo de execução, então código de inicialização arbitrariamente complexo é possível. + +[lazy_static]: https://docs.rs/lazy_static/1.0.1/lazy_static/ + +Vamos adicionar a crate `lazy_static` ao nosso projeto: + +```toml +# em Cargo.toml + +[dependencies.lazy_static] +version = "1.0" +features = ["spin_no_std"] +``` + +Precisamos do recurso `spin_no_std`, já que não vinculamos a biblioteca padrão. + +Com `lazy_static`, podemos definir nosso `WRITER` static sem problemas: + +```rust +// em src/vga_buffer.rs + +use lazy_static::lazy_static; + +lazy_static! { + pub static ref WRITER: Writer = Writer { + column_position: 0, + color_code: ColorCode::new(Color::Yellow, Color::Black), + buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + }; +} +``` + +No entanto, este `WRITER` é praticamente inútil, pois é imutável. Isso significa que não podemos escrever nada nele (já que todos os métodos de escrita recebem `&mut self`). Uma solução possível seria usar um [static mutável]. Mas então cada leitura e escrita nele seria unsafe, pois poderia facilmente introduzir data races e outras coisas ruins. Usar `static mut` é altamente desencorajado. Até houve propostas para [removê-lo][remove static mut]. Mas quais são as alternativas? Poderíamos tentar usar um static imutável com um tipo de célula como [RefCell] ou até [UnsafeCell] que fornece [mutabilidade interior]. Mas esses tipos não são [Sync] \(com boa razão), então não podemos usá-los em statics. + +[static mutável]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable +[remove static mut]: https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437 +[RefCell]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt +[UnsafeCell]: https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html +[mutabilidade interior]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html +[Sync]: https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html + +### Spinlocks +Para obter mutabilidade interior sincronizada, usuários da biblioteca padrão podem usar [Mutex]. Ele fornece exclusão mútua bloqueando threads quando o recurso já está bloqueado. Mas nosso kernel básico não tem nenhum suporte de bloqueio ou mesmo um conceito de threads, então também não podemos usá-lo. No entanto, há um tipo realmente básico de mutex na ciência da computação que não requer nenhum recurso de sistema operacional: o [spinlock]. Em vez de bloquear, as threads simplesmente tentam bloqueá-lo novamente e novamente em um loop apertado, queimando assim tempo de CPU até que o mutex esteja livre novamente. + +[Mutex]: https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html +[spinlock]: https://en.wikipedia.org/wiki/Spinlock + +Para usar um spinning mutex, podemos adicionar a [crate spin] como uma dependência: + +[crate spin]: https://crates.io/crates/spin + +```toml +# em Cargo.toml +[dependencies] +spin = "0.5.2" +``` + +Então podemos usar o spinning mutex para adicionar [mutabilidade interior] segura ao nosso `WRITER` static: + +```rust +// em src/vga_buffer.rs + +use spin::Mutex; +... +lazy_static! { + pub static ref WRITER: Mutex = Mutex::new(Writer { + column_position: 0, + color_code: ColorCode::new(Color::Yellow, Color::Black), + buffer: unsafe { &mut *(0xb8000 as *mut Buffer) }, + }); +} +``` +Agora podemos deletar a função `print_something` e imprimir diretamente da nossa função `_start`: + +```rust +// em src/main.rs +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + use core::fmt::Write; + vga_buffer::WRITER.lock().write_str("Hello again").unwrap(); + write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap(); + + loop {} +} +``` +Precisamos importar a trait `fmt::Write` para poder usar suas funções. + +### Segurança +Note que temos apenas um bloco unsafe no nosso código, que é necessário para criar uma referência `Buffer` apontando para `0xb8000`. Depois disso, todas as operações são seguras. Rust usa verificação de limites para acessos a arrays por padrão, então não podemos escrever acidentalmente fora do buffer. Assim, codificamos as condições necessárias no sistema de tipos e somos capazes de fornecer uma interface segura para o exterior. + +### Uma Macro println +Agora que temos um writer global, podemos adicionar uma macro `println` que pode ser usada de qualquer lugar na base de código. A [sintaxe de macro] do Rust é um pouco estranha, então não tentaremos escrever uma macro do zero. Em vez disso, olhamos para o código-fonte da [macro `println!`] na biblioteca padrão: + +[sintaxe de macro]: https://doc.rust-lang.org/nightly/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming +[macro `println!`]: https://doc.rust-lang.org/nightly/std/macro.println!.html + +```rust +#[macro_export] +macro_rules! println { + () => (print!("\n")); + ($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*))); +} +``` + +Macros são definidas através de uma ou mais regras, semelhantes a braços `match`. A macro `println` tem duas regras: A primeira regra é para invocações sem argumentos, por exemplo, `println!()`, que é expandida para `print!("\n")` e, portanto, apenas imprime um newline. A segunda regra é para invocações com parâmetros como `println!("Hello")` ou `println!("Number: {}", 4)`. Ela também é expandida para uma invocação da macro `print!`, passando todos os argumentos e um newline `\n` adicional no final. + +O atributo `#[macro_export]` torna a macro disponível para toda a crate (não apenas o módulo em que é definida) e crates externas. Ele também coloca a macro na raiz da crate, o que significa que temos que importar a macro através de `use std::println` em vez de `std::macros::println`. + +A [macro `print!`] é definida como: + +[macro `print!`]: https://doc.rust-lang.org/nightly/std/macro.print!.html + +```rust +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*))); +} +``` + +A macro se expande para uma chamada da [função `_print`] no módulo `io`. A [variável `$crate`] garante que a macro também funcione de fora da crate `std` ao se expandir para `std` quando é usada em outras crates. + +A [macro `format_args`] constrói um tipo [fmt::Arguments] dos argumentos passados, que é passado para `_print`. A [função `_print`] da libstd chama `print_to`, que é bastante complicado porque suporta diferentes dispositivos `Stdout`. Não precisamos dessa complexidade, pois só queremos imprimir no buffer VGA. + +[função `_print`]: https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698 +[variável `$crate`]: https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate +[macro `format_args`]: https://doc.rust-lang.org/nightly/std/macro.format_args.html +[fmt::Arguments]: https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html + +Para imprimir no buffer VGA, apenas copiamos as macros `println!` e `print!`, mas as modificamos para usar nossa própria função `_print`: + +```rust +// em src/vga_buffer.rs + +#[macro_export] +macro_rules! print { + ($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*))); +} + +#[macro_export] +macro_rules! println { + () => ($crate::print!("\n")); + ($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*))); +} + +#[doc(hidden)] +pub fn _print(args: fmt::Arguments) { + use core::fmt::Write; + WRITER.lock().write_fmt(args).unwrap(); +} +``` + +Uma coisa que mudamos da definição original de `println` é que também prefixamos as invocações da macro `print!` com `$crate`. Isso garante que não precisamos importar a macro `print!` também se quisermos usar apenas `println`. + +Como na biblioteca padrão, adicionamos o atributo `#[macro_export]` a ambas as macros para torná-las disponíveis em todo lugar na nossa crate. Note que isso coloca as macros no namespace raiz da crate, então importá-las via `use crate::vga_buffer::println` não funciona. Em vez disso, temos que fazer `use crate::println`. + +A função `_print` bloqueia nosso `WRITER` static e chama o método `write_fmt` nele. Este método é da trait `Write`, que precisamos importar. O `unwrap()` adicional no final entra em panic se a impressão não for bem-sucedida. Mas como sempre retornamos `Ok` em `write_str`, isso não deve acontecer. + +Como as macros precisam ser capazes de chamar `_print` de fora do módulo, a função precisa ser pública. No entanto, como consideramos isso um detalhe de implementação privado, adicionamos o [atributo `doc(hidden)`] para ocultá-la da documentação gerada. + +[atributo `doc(hidden)`]: https://doc.rust-lang.org/nightly/rustdoc/write-documentation/the-doc-attribute.html#hidden + +### Hello World usando `println` +Agora podemos usar `println` na nossa função `_start`: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + loop {} +} +``` + +Note que não precisamos importar a macro na função main, porque ela já vive no namespace raiz. + +Como esperado, agora vemos um _"Hello World!"_ na tela: + +![QEMU imprimindo "Hello World!"](vga-hello-world.png) + +### Imprimindo Mensagens de Panic + +Agora que temos uma macro `println`, podemos usá-la na nossa função panic para imprimir a mensagem de panic e a localização do panic: + +```rust +// em main.rs + +/// Esta função é chamada em caso de pânico. +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} +``` + +Quando agora inserimos `panic!("Some panic message");` na nossa função `_start`, obtemos a seguinte saída: + +![QEMU imprimindo "panicked at 'Some panic message', src/main.rs:28:5](vga-panic.png) + +Então sabemos não apenas que um panic ocorreu, mas também a mensagem de panic e onde no código aconteceu. + +## Resumo +Neste post, aprendemos sobre a estrutura do buffer de texto VGA e como ele pode ser escrito através do mapeamento de memória no endereço `0xb8000`. Criamos um módulo Rust que encapsula a unsafety de escrever neste buffer mapeado em memória e apresenta uma interface segura e conveniente para o exterior. + +Graças ao cargo, também vimos como é fácil adicionar dependências em bibliotecas de terceiros. As duas dependências que adicionamos, `lazy_static` e `spin`, são muito úteis no desenvolvimento de SO e as usaremos em mais lugares em posts futuros. + +## O que vem a seguir? +O próximo post explica como configurar o framework de testes unitários embutido do Rust. Criaremos então alguns testes unitários básicos para o módulo de buffer VGA deste post. \ No newline at end of file diff --git a/blog/content/edition-2/posts/04-testing/index.pt-BR.md b/blog/content/edition-2/posts/04-testing/index.pt-BR.md new file mode 100644 index 00000000..7b193a28 --- /dev/null +++ b/blog/content/edition-2/posts/04-testing/index.pt-BR.md @@ -0,0 +1,1044 @@ ++++ +title = "Testes" +weight = 4 +path = "pt-BR/testing" +date = 2019-04-27 + +[extra] +chapter = "O Básico" +comments_search_term = 1009 +# Please update this when updating the translation +translation_based_on_commit = "33b7979468235b8637584e91e4c599cef37d9687" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Este post explora testes unitários e de integração em executáveis `no_std`. Usaremos o suporte do Rust para frameworks de teste customizados para executar funções de teste dentro do nosso kernel. Para reportar os resultados para fora do QEMU, usaremos diferentes recursos do QEMU e da ferramenta `bootimage`. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-04`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-04 + + + +## Requisitos + +Este post substitui os posts (agora deprecados) [_Unit Testing_] e [_Integration Tests_]. Ele assume que você seguiu o post [_A Minimal Rust Kernel_] depois de 27-04-2019. Principalmente, ele requer que você tenha um arquivo `.cargo/config.toml` que [define um alvo padrão] e [define um executável runner]. + +[_Unit Testing_]: @/edition-2/posts/deprecated/04-unit-testing/index.md +[_Integration Tests_]: @/edition-2/posts/deprecated/05-integration-tests/index.md +[_A Minimal Rust Kernel_]: @/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md +[define um alvo padrão]: @/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md#definir-um-alvo-padrao +[define um executável runner]: @/edition-2/posts/02-minimal-rust-kernel/index.pt-BR.md#usando-cargo-run + +## Testes em Rust + +Rust tem um [framework de testes integrado] que é capaz de executar testes unitários sem a necessidade de configurar nada. Basta criar uma função que verifica alguns resultados através de assertions e adicionar o atributo `#[test]` ao cabeçalho da função. Então `cargo test` automaticamente encontrará e executará todas as funções de teste da sua crate. + +[framework de testes integrado]: https://doc.rust-lang.org/book/ch11-00-testing.html + +Para habilitar testes para nosso binário kernel, podemos definir a flag `test` no Cargo.toml como `true`: + +```toml +# em Cargo.toml + +[[bin]] +name = "blog_os" +test = true +bench = false +``` + +Esta [seção `[[bin]]`](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#configuring-a-target) especifica como o `cargo` deve compilar nosso executável `blog_os`. O campo `test` especifica se testes são suportados para este executável. Definimos `test = false` no primeiro post para [deixar o `rust-analyzer` feliz](@/edition-2/posts/01-freestanding-rust-binary/index.pt-BR.md#deixando-rust-analyzer-feliz), mas agora queremos habilitar testes, então o definimos de volta para `true`. + +Infelizmente, testes são um pouco mais complicados para aplicações `no_std` como nosso kernel. O problema é que o framework de testes do Rust usa implicitamente a biblioteca [`test`] integrada, que depende da biblioteca padrão. Isso significa que não podemos usar o framework de testes padrão para nosso kernel `#[no_std]`. + +[`test`]: https://doc.rust-lang.org/test/index.html + +Podemos ver isso quando tentamos executar `cargo test` no nosso projeto: + +``` +> cargo test + Compiling blog_os v0.1.0 (/…/blog_os) +error[E0463]: can't find crate for `test` +``` + +Como a crate `test` depende da biblioteca padrão, ela não está disponível para nosso alvo bare metal. Embora portar a crate `test` para um contexto `#[no_std]` [seja possível][utest], é altamente instável e requer alguns hacks, como redefinir a macro `panic`. + +[utest]: https://github.com/japaric/utest + +### Frameworks de Teste Customizados + +Felizmente, Rust suporta substituir o framework de testes padrão através do recurso instável [`custom_test_frameworks`]. Este recurso não requer bibliotecas externas e, portanto, também funciona em ambientes `#[no_std]`. Funciona coletando todas as funções anotadas com um atributo `#[test_case]` e então invocando uma função runner especificada pelo usuário com a lista de testes como argumento. Assim, dá à implementação controle máximo sobre o processo de teste. + +[`custom_test_frameworks`]: https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html + +A desvantagem comparada ao framework de testes padrão é que muitos recursos avançados, como [testes `should_panic`], não estão disponíveis. Em vez disso, cabe à implementação fornecer tais recursos ela mesma se necessário. Isso é ideal para nós, pois temos um ambiente de execução muito especial onde as implementações padrão de tais recursos avançados provavelmente não funcionariam de qualquer forma. Por exemplo, o atributo `#[should_panic]` depende de stack unwinding para capturar os panics, que desabilitamos para nosso kernel. + +[testes `should_panic`]: https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic + +Para implementar um framework de testes customizado para nosso kernel, adicionamos o seguinte ao nosso `main.rs`: + +```rust +// em src/main.rs + +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] + +#[cfg(test)] +pub fn test_runner(tests: &[&dyn Fn()]) { + println!("Running {} tests", tests.len()); + for test in tests { + test(); + } +} +``` + +Nosso runner apenas imprime uma breve mensagem de debug e então chama cada função de teste na lista. O tipo de argumento `&[&dyn Fn()]` é uma [_slice_] de referências a [_trait object_] da trait [_Fn()_]. É basicamente uma lista de referências a tipos que podem ser chamados como uma função. Como a função é inútil para execuções não-teste, usamos o atributo `#[cfg(test)]` para incluí-la apenas para testes. + +[_slice_]: https://doc.rust-lang.org/std/primitive.slice.html +[_trait object_]: https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html +[_Fn()_]: https://doc.rust-lang.org/std/ops/trait.Fn.html + +Quando executamos `cargo test` agora, vemos que ele agora é bem-sucedido (se não for, veja a nota abaixo). No entanto, ainda vemos nosso "Hello World" em vez da mensagem do nosso `test_runner`. A razão é que nossa função `_start` ainda é usada como ponto de entrada. O recurso de frameworks de teste customizados gera uma função `main` que chama `test_runner`, mas esta função é ignorada porque usamos o atributo `#[no_main]` e fornecemos nosso próprio ponto de entrada. + +
+ +**Nota:** Atualmente há um bug no cargo que leva a erros de "duplicate lang item" no `cargo test` em alguns casos. Ocorre quando você definiu `panic = "abort"` para um profile no seu `Cargo.toml`. Tente removê-lo, então `cargo test` deve funcionar. Alternativamente, se isso não funcionar, então adicione `panic-abort-tests = true` à seção `[unstable]` do seu arquivo `.cargo/config.toml`. Veja o [issue do cargo](https://github.com/rust-lang/cargo/issues/7359) para mais informações sobre isso. + +
+ +Para corrigir isso, primeiro precisamos mudar o nome da função gerada para algo diferente de `main` através do atributo `reexport_test_harness_main`. Então podemos chamar a função renomeada da nossa função `_start`: + +```rust +// em src/main.rs + +#![reexport_test_harness_main = "test_main"] + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + #[cfg(test)] + test_main(); + + loop {} +} +``` + +Definimos o nome da função de entrada do framework de testes como `test_main` e a chamamos do nosso ponto de entrada `_start`. Usamos [compilação condicional] para adicionar a chamada a `test_main` apenas em contextos de teste porque a função não é gerada em uma execução normal. + +Quando agora executamos `cargo test`, vemos a mensagem "Running 0 tests" do nosso `test_runner` na tela. Agora estamos prontos para criar nossa primeira função de teste: + +```rust +// em src/main.rs + +#[test_case] +fn trivial_assertion() { + print!("trivial assertion... "); + assert_eq!(1, 1); + println!("[ok]"); +} +``` + +Quando executamos `cargo test` agora, vemos a seguinte saída: + +![QEMU imprimindo "Hello World!", "Running 1 tests" e "trivial assertion... [ok]"](qemu-test-runner-output.png) + +A slice `tests` passada para nossa função `test_runner` agora contém uma referência à função `trivial_assertion`. Da saída `trivial assertion... [ok]` na tela, vemos que o teste foi chamado e que foi bem-sucedido. + +Após executar os testes, nosso `test_runner` retorna à função `test_main`, que por sua vez retorna à nossa função de ponto de entrada `_start`. No final de `_start`, entramos em um loop infinito porque a função de ponto de entrada não tem permissão para retornar. Isso é um problema, porque queremos que `cargo test` saia após executar todos os testes. + +## Saindo do QEMU + +Agora, temos um loop infinito no final da nossa função `_start` e precisamos fechar o QEMU manualmente em cada execução de `cargo test`. Isso é infeliz porque também queremos executar `cargo test` em scripts sem interação do usuário. A solução limpa para isso seria implementar uma maneira adequada de desligar nosso SO. Infelizmente, isso é relativamente complexo porque requer implementar suporte para o padrão de gerenciamento de energia [APM] ou [ACPI]. + +[APM]: https://wiki.osdev.org/APM +[ACPI]: https://wiki.osdev.org/ACPI + +Felizmente, há uma saída de emergência: O QEMU suporta um dispositivo especial `isa-debug-exit`, que fornece uma maneira fácil de sair do QEMU do sistema guest. Para habilitá-lo, precisamos passar um argumento `-device` ao QEMU. Podemos fazer isso adicionando uma chave de configuração `package.metadata.bootimage.test-args` no nosso `Cargo.toml`: + +```toml +# em Cargo.toml + +[package.metadata.bootimage] +test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04"] +``` + +O `bootimage runner` anexa os `test-args` ao comando QEMU padrão para todos os executáveis de teste. Para um `cargo run` normal, os argumentos são ignorados. + +Junto com o nome do dispositivo (`isa-debug-exit`), passamos os dois parâmetros `iobase` e `iosize` que especificam a _porta I/O_ através da qual o dispositivo pode ser alcançado do nosso kernel. + +### Portas I/O + +Existem duas abordagens diferentes para comunicação entre a CPU e hardware periférico no x86, **I/O mapeado em memória** e **I/O mapeado em porta**. Já usamos I/O mapeado em memória para acessar o [buffer de texto VGA] através do endereço de memória `0xb8000`. Este endereço não é mapeado para RAM, mas para alguma memória no dispositivo VGA. + +[buffer de texto VGA]: @/edition-2/posts/03-vga-text-buffer/index.pt-BR.md + +Em contraste, I/O mapeado em porta usa um barramento I/O separado para comunicação. Cada periférico conectado tem um ou mais números de porta. Para comunicar com tal porta I/O, existem instruções especiais de CPU chamadas `in` e `out`, que recebem um número de porta e um byte de dados (também há variações desses comandos que permitem enviar um `u16` ou `u32`). + +O dispositivo `isa-debug-exit` usa I/O mapeado em porta. O parâmetro `iobase` especifica em qual endereço de porta o dispositivo deve viver (`0xf4` é uma porta [geralmente não utilizada][lista de portas I/O x86] no barramento IO do x86) e o `iosize` especifica o tamanho da porta (`0x04` significa quatro bytes). + +[lista de portas I/O x86]: https://wiki.osdev.org/I/O_Ports#The_list + +### Usando o Dispositivo de Saída + +A funcionalidade do dispositivo `isa-debug-exit` é muito simples. Quando um `value` é escrito na porta I/O especificada por `iobase`, ele faz com que o QEMU saia com [status de saída] `(value << 1) | 1`. Então, quando escrevemos `0` na porta, o QEMU sairá com status de saída `(0 << 1) | 1 = 1`, e quando escrevemos `1` na porta, ele sairá com status de saída `(1 << 1) | 1 = 3`. + +[status de saída]: https://en.wikipedia.org/wiki/Exit_status + +Em vez de invocar manualmente as instruções assembly `in` e `out`, usamos as abstrações fornecidas pela crate [`x86_64`]. Para adicionar uma dependência nessa crate, a adicionamos à seção `dependencies` no nosso `Cargo.toml`: + +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/ + +```toml +# em Cargo.toml + +[dependencies] +x86_64 = "0.14.2" +``` + +Agora podemos usar o tipo [`Port`] fornecido pela crate para criar uma função `exit_qemu`: + +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html + +```rust +// em src/main.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } +} +``` + +A função cria um novo [`Port`] em `0xf4`, que é o `iobase` do dispositivo `isa-debug-exit`. Então ela escreve o código de saída passado para a porta. Usamos `u32` porque especificamos o `iosize` do dispositivo `isa-debug-exit` como 4 bytes. Ambas as operações são unsafe porque escrever em uma porta I/O geralmente pode resultar em comportamento arbitrário. + +Para especificar o status de saída, criamos um enum `QemuExitCode`. A ideia é sair com o código de saída de sucesso se todos os testes foram bem-sucedidos e com o código de saída de falha caso contrário. O enum é marcado como `#[repr(u32)]` para representar cada variante por um inteiro `u32`. Usamos o código de saída `0x10` para sucesso e `0x11` para falha. Os códigos de saída reais não importam muito, desde que não colidam com os códigos de saída padrão do QEMU. Por exemplo, usar código de saída `0` para sucesso não é uma boa ideia porque ele se torna `(0 << 1) | 1 = 1` após a transformação, que é o código de saída padrão quando o QEMU falha ao executar. Então não poderíamos diferenciar um erro do QEMU de uma execução de teste bem-sucedida. + +Agora podemos atualizar nosso `test_runner` para sair do QEMU após todos os testes terem sido executados: + +```rust +// em src/main.rs + +fn test_runner(tests: &[&dyn Fn()]) { + println!("Running {} tests", tests.len()); + for test in tests { + test(); + } + /// novo + exit_qemu(QemuExitCode::Success); +} +``` + +Quando executamos `cargo test` agora, vemos que o QEMU fecha imediatamente após executar os testes. O problema é que `cargo test` interpreta o teste como falhado mesmo que tenhamos passado nosso código de saída `Success`: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.03s + Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be +Building bootloader + Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader) + Finished release [optimized + debuginfo] target(s) in 1.07s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4, + iosize=0x04` +error: test failed, to rerun pass '--bin blog_os' +``` + +O problema é que `cargo test` considera todos os códigos de erro diferentes de `0` como falha. + +### Código de Saída de Sucesso + +Para contornar isso, `bootimage` fornece uma chave de configuração `test-success-exit-code` que mapeia um código de saída especificado para o código de saída `0`: + +```toml +# em Cargo.toml + +[package.metadata.bootimage] +test-args = […] +test-success-exit-code = 33 # (0x10 << 1) | 1 +``` + +Com esta configuração, `bootimage` mapeia nosso código de saída de sucesso para o código de saída 0, para que `cargo test` reconheça corretamente o caso de sucesso e não conte o teste como falhado. + +Nosso test runner agora fecha automaticamente o QEMU e reporta corretamente os resultados do teste. Ainda vemos a janela do QEMU abrir por um tempo muito curto, mas não é suficiente para ler os resultados. Seria bom se pudéssemos imprimir os resultados do teste no console em vez disso, para que ainda possamos vê-los após o QEMU sair. + +## Imprimindo no Console + +Para ver a saída do teste no console, precisamos enviar os dados do nosso kernel para o sistema host de alguma forma. Existem várias maneiras de conseguir isso, por exemplo, enviando os dados por uma interface de rede TCP. No entanto, configurar uma pilha de rede é uma tarefa bastante complexa, então escolheremos uma solução mais simples em vez disso. + +### Porta Serial + +Uma maneira simples de enviar os dados é usar a [porta serial], um antigo padrão de interface que não é mais encontrado em computadores modernos. É fácil de programar e o QEMU pode redirecionar os bytes enviados pela porta serial para a saída padrão do host ou um arquivo. + +[porta serial]: https://en.wikipedia.org/wiki/Serial_port + +Os chips que implementam uma interface serial são chamados [UARTs]. Existem [muitos modelos de UART] no x86, mas felizmente as únicas diferenças entre eles são alguns recursos avançados que não precisamos. Os UARTs comuns hoje são todos compatíveis com o [UART 16550], então usaremos esse modelo para nosso framework de testes. + +[UARTs]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter +[muitos modelos de UART]: https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#Models +[UART 16550]: https://en.wikipedia.org/wiki/16550_UART + +Usaremos a crate [`uart_16550`] para inicializar o UART e enviar dados pela porta serial. Para adicioná-la como dependência, atualizamos nosso `Cargo.toml` e `main.rs`: + +[`uart_16550`]: https://docs.rs/uart_16550 + +```toml +# em Cargo.toml + +[dependencies] +uart_16550 = "0.2.0" +``` + +A crate `uart_16550` contém uma struct `SerialPort` que representa os registradores UART, mas ainda precisamos construir uma instância dela nós mesmos. Para isso, criamos um novo módulo `serial` com o seguinte conteúdo: + +```rust +// em src/main.rs + +mod serial; +``` + +```rust +// em src/serial.rs + +use uart_16550::SerialPort; +use spin::Mutex; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref SERIAL1: Mutex = { + let mut serial_port = unsafe { SerialPort::new(0x3F8) }; + serial_port.init(); + Mutex::new(serial_port) + }; +} +``` + +Como com o [buffer de texto VGA][vga lazy-static], usamos `lazy_static` e um spinlock para criar uma instância writer `static`. Ao usar `lazy_static` podemos garantir que o método `init` seja chamado exatamente uma vez em seu primeiro uso. + +Como o dispositivo `isa-debug-exit`, o UART é programado usando I/O de porta. Como o UART é mais complexo, ele usa múltiplas portas I/O para programar diferentes registradores do dispositivo. A função unsafe `SerialPort::new` espera o endereço da primeira porta I/O do UART como argumento, a partir do qual ela pode calcular os endereços de todas as portas necessárias. Estamos passando o endereço de porta `0x3F8`, que é o número de porta padrão para a primeira interface serial. + +[vga lazy-static]: @/edition-2/posts/03-vga-text-buffer/index.pt-BR.md#lazy-statics + +Para tornar a porta serial facilmente utilizável, adicionamos macros `serial_print!` e `serial_println!`: + +```rust +// em src/serial.rs + +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + SERIAL1.lock().write_fmt(args).expect("Printing to serial failed"); +} + +/// Imprime no host através da interface serial. +#[macro_export] +macro_rules! serial_print { + ($($arg:tt)*) => { + $crate::serial::_print(format_args!($($arg)*)); + }; +} + +/// Imprime no host através da interface serial, anexando uma newline. +#[macro_export] +macro_rules! serial_println { + () => ($crate::serial_print!("\n")); + ($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n"))); + ($fmt:expr, $($arg:tt)*) => ($crate::serial_print!( + concat!($fmt, "\n"), $($arg)*)); +} +``` + +A implementação é muito similar à implementação das nossas macros `print` e `println`. Como o tipo `SerialPort` já implementa a trait [`fmt::Write`], não precisamos fornecer nossa própria implementação. + +[`fmt::Write`]: https://doc.rust-lang.org/nightly/core/fmt/trait.Write.html + +Agora podemos imprimir na interface serial em vez do buffer de texto VGA no nosso código de teste: + +```rust +// em src/main.rs + +#[cfg(test)] +fn test_runner(tests: &[&dyn Fn()]) { + serial_println!("Running {} tests", tests.len()); + […] +} + +#[test_case] +fn trivial_assertion() { + serial_print!("trivial assertion... "); + assert_eq!(1, 1); + serial_println!("[ok]"); +} +``` + +Note que a macro `serial_println` vive diretamente sob o namespace raiz porque usamos o atributo `#[macro_export]`, então importá-la através de `use crate::serial::serial_println` não funcionará. + +### Argumentos do QEMU + +Para ver a saída serial do QEMU, precisamos usar o argumento `-serial` para redirecionar a saída para stdout: + +```toml +# em Cargo.toml + +[package.metadata.bootimage] +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio" +] +``` + +Quando executamos `cargo test` agora, vemos a saída do teste diretamente no console: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a +Building bootloader + Finished release [optimized + debuginfo] target(s) in 0.02s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device + isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio` +Running 1 tests +trivial assertion... [ok] +``` + +No entanto, quando um teste falha, ainda vemos a saída dentro do QEMU porque nosso handler de panic ainda usa `println`. Para simular isso, podemos mudar a assertion no nosso teste `trivial_assertion` para `assert_eq!(0, 1)`: + +![QEMU imprimindo "Hello World!" e "panicked at 'assertion failed: `(left == right)` + left: `0`, right: `1`', src/main.rs:55:5](qemu-failed-test.png) + +Vemos que a mensagem de panic ainda é impressa no buffer VGA, enquanto a outra saída de teste é impressa na porta serial. A mensagem de panic é bastante útil, então seria útil vê-la no console também. + +### Imprimir uma Mensagem de Erro no Panic + +Para sair do QEMU com uma mensagem de erro em um panic, podemos usar [compilação condicional] para usar um handler de panic diferente no modo de teste: + +[compilação condicional]: https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html + +```rust +// em src/main.rs + +// nosso handler de panic existente +#[cfg(not(test))] // novo atributo +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} + +// nosso handler de panic em modo de teste +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + loop {} +} +``` + +Para nosso handler de panic de teste, usamos `serial_println` em vez de `println` e então saímos do QEMU com um código de saída de falha. Note que ainda precisamos de um `loop` infinito após a chamada `exit_qemu` porque o compilador não sabe que o dispositivo `isa-debug-exit` causa uma saída do programa. + +Agora o QEMU também sai para testes falhados e imprime uma mensagem de erro útil no console: + +``` +> cargo test + Finished dev [unoptimized + debuginfo] target(s) in 0.02s + Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a +Building bootloader + Finished release [optimized + debuginfo] target(s) in 0.02s +Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/ + deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device + isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio` +Running 1 tests +trivial assertion... [failed] + +Error: panicked at 'assertion failed: `(left == right)` + left: `0`, + right: `1`', src/main.rs:65:5 +``` + +Como agora vemos toda a saída do teste no console, não precisamos mais da janela do QEMU que aparece por um curto tempo. Então podemos ocultá-la completamente. + +### Ocultando o QEMU + +Como reportamos os resultados completos do teste usando o dispositivo `isa-debug-exit` e a porta serial, não precisamos mais da janela do QEMU. Podemos ocultá-la passando o argumento `-display none` ao QEMU: + +```toml +# em Cargo.toml + +[package.metadata.bootimage] +test-args = [ + "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-serial", "stdio", + "-display", "none" +] +``` + +Agora o QEMU executa completamente em segundo plano e nenhuma janela é mais aberta. Isso não é apenas menos irritante, mas também permite que nosso framework de testes execute em ambientes sem interface gráfica do usuário, como serviços de CI ou conexões [SSH]. + +[SSH]: https://en.wikipedia.org/wiki/Secure_Shell + +### Timeouts + +Como `cargo test` espera até que o test runner saia, um teste que nunca retorna pode bloquear o test runner para sempre. Isso é infeliz, mas não é um grande problema na prática, pois geralmente é fácil evitar loops infinitos. No nosso caso, no entanto, loops infinitos podem ocorrer em várias situações: + +- O bootloader falha ao carregar nosso kernel, o que causa o sistema reiniciar infinitamente. +- O firmware BIOS/UEFI falha ao carregar o bootloader, o que causa a mesma reinicialização infinita. +- A CPU entra em uma declaração `loop {}` no final de algumas das nossas funções, por exemplo porque o dispositivo de saída do QEMU não funciona corretamente. +- O hardware causa um reset do sistema, por exemplo quando uma exceção de CPU não é capturada (explicado em um post futuro). + +Como loops infinitos podem ocorrer em tantas situações, a ferramenta `bootimage` define um timeout de 5 minutos para cada executável de teste por padrão. Se o teste não terminar dentro deste tempo, ele é marcado como falhado e um erro "Timed Out" é impresso no console. Este recurso garante que testes que estão presos em um loop infinito não bloqueiem `cargo test` para sempre. + +Você pode tentar você mesmo adicionando uma declaração `loop {}` no teste `trivial_assertion`. Quando você executa `cargo test`, vê que o teste é marcado como timed out após 5 minutos. A duração do timeout é [configurável][bootimage config] através de uma chave `test-timeout` no Cargo.toml: + +[bootimage config]: https://github.com/rust-osdev/bootimage#configuration + +```toml +# em Cargo.toml + +[package.metadata.bootimage] +test-timeout = 300 # (em segundos) +``` + +Se você não quiser esperar 5 minutos para o teste `trivial_assertion` dar timeout, pode diminuir temporariamente o valor acima. + +### Inserir Impressão Automaticamente + +Nosso teste `trivial_assertion` atualmente precisa imprimir suas próprias informações de status usando `serial_print!`/`serial_println!`: + +```rust +#[test_case] +fn trivial_assertion() { + serial_print!("trivial assertion... "); + assert_eq!(1, 1); + serial_println!("[ok]"); +} +``` + +Adicionar manualmente essas declarações de impressão para cada teste que escrevemos é trabalhoso, então vamos atualizar nosso `test_runner` para imprimir essas mensagens automaticamente. Para fazer isso, precisamos criar uma nova trait `Testable`: + +```rust +// em src/main.rs + +pub trait Testable { + fn run(&self) -> (); +} +``` + +O truque agora é implementar esta trait para todos os tipos `T` que implementam a [trait `Fn()`]: + +[trait `Fn()`]: https://doc.rust-lang.org/stable/core/ops/trait.Fn.html + +```rust +// em src/main.rs + +impl Testable for T +where + T: Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} +``` + +Implementamos a função `run` primeiro imprimindo o nome da função usando a função [`any::type_name`]. Esta função é implementada diretamente no compilador e retorna uma descrição em string de cada tipo. Para funções, o tipo é seu nome, então isso é exatamente o que queremos neste caso. O caractere `\t` é o [caractere tab], que adiciona algum alinhamento às mensagens `[ok]`. + +[`any::type_name`]: https://doc.rust-lang.org/stable/core/any/fn.type_name.html +[caractere tab]: https://en.wikipedia.org/wiki/Tab_key#Tab_characters + +Após imprimir o nome da função, invocamos a função de teste através de `self()`. Isso só funciona porque exigimos que `self` implemente a trait `Fn()`. Após a função de teste retornar, imprimimos `[ok]` para indicar que a função não entrou em panic. + +O último passo é atualizar nosso `test_runner` para usar a nova trait `Testable`: + +```rust +// em src/main.rs + +#[cfg(test)] +pub fn test_runner(tests: &[&dyn Testable]) { // novo + serial_println!("Running {} tests", tests.len()); + for test in tests { + test.run(); // novo + } + exit_qemu(QemuExitCode::Success); +} +``` + +As únicas duas mudanças são o tipo do argumento `tests` de `&[&dyn Fn()]` para `&[&dyn Testable]` e o fato de que agora chamamos `test.run()` em vez de `test()`. + +Agora podemos remover as declarações de impressão do nosso teste `trivial_assertion` já que elas são impressas automaticamente: + +```rust +// em src/main.rs + +#[test_case] +fn trivial_assertion() { + assert_eq!(1, 1); +} +``` + +A saída de `cargo test` agora se parece com isto: + +``` +Running 1 tests +blog_os::trivial_assertion... [ok] +``` + +O nome da função agora inclui o caminho completo para a função, o que é útil quando funções de teste em diferentes módulos têm o mesmo nome. Caso contrário, a saída parece igual a antes, mas não precisamos mais adicionar declarações de impressão aos nossos testes manualmente. + +## Testando o Buffer VGA + +Agora que temos um framework de testes funcionando, podemos criar alguns testes para nossa implementação de buffer VGA. Primeiro, criamos um teste muito simples para verificar que `println` funciona sem entrar em panic: + +```rust +// em src/vga_buffer.rs + +#[test_case] +fn test_println_simple() { + println!("test_println_simple output"); +} +``` + +O teste apenas imprime algo no buffer VGA. Se ele terminar sem entrar em panic, significa que a invocação de `println` também não entrou em panic. + +Para garantir que nenhum panic ocorra mesmo se muitas linhas forem impressas e as linhas forem deslocadas para fora da tela, podemos criar outro teste: + +```rust +// em src/vga_buffer.rs + +#[test_case] +fn test_println_many() { + for _ in 0..200 { + println!("test_println_many output"); + } +} +``` + +Também podemos criar uma função de teste para verificar que as linhas impressas realmente aparecem na tela: + +```rust +// em src/vga_buffer.rs + +#[test_case] +fn test_println_output() { + let s = "Some test string that fits on a single line"; + println!("{}", s); + for (i, c) in s.chars().enumerate() { + let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } +} +``` + +A função define uma string de teste, a imprime usando `println`, e então itera sobre os caracteres da tela do `WRITER` static, que representa o buffer de texto VGA. Como `println` imprime na última linha da tela e então anexa imediatamente uma newline, a string deve aparecer na linha `BUFFER_HEIGHT - 2`. + +Ao usar [`enumerate`], contamos o número de iterações na variável `i`, que então usamos para carregar o caractere da tela correspondente a `c`. Ao comparar o `ascii_character` do caractere da tela com `c`, garantimos que cada caractere da string realmente aparece no buffer de texto VGA. + +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate + +Como você pode imaginar, poderíamos criar muitas mais funções de teste. Por exemplo, uma função que testa que nenhum panic ocorre ao imprimir linhas muito longas e que elas são quebradas corretamente, ou uma função para testar que newlines, caracteres não imprimíveis e caracteres não-unicode são tratados corretamente. + +Para o resto deste post, no entanto, explicaremos como criar _testes de integração_ para testar a interação de diferentes componentes juntos. + +## Testes de Integração + +A convenção para [testes de integração] em Rust é colocá-los em um diretório `tests` na raiz do projeto (ou seja, ao lado do diretório `src`). Tanto o framework de testes padrão quanto frameworks de testes customizados detectarão e executarão automaticamente todos os testes naquele diretório. + +[testes de integração]: https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests + +Todos os testes de integração são seus próprios executáveis e completamente separados do nosso `main.rs`. Isso significa que cada teste precisa definir sua própria função de ponto de entrada. Vamos criar um teste de integração de exemplo chamado `basic_boot` para ver como funciona em detalhes: + +```rust +// em tests/basic_boot.rs + +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; + +#[unsafe(no_mangle)] // não altere (mangle) o nome desta função +pub extern "C" fn _start() -> ! { + test_main(); + + loop {} +} + +fn test_runner(tests: &[&dyn Fn()]) { + unimplemented!(); +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + loop {} +} +``` + +Como testes de integração são executáveis separados, precisamos fornecer todos os atributos da crate (`no_std`, `no_main`, `test_runner`, etc.) novamente. Também precisamos criar uma nova função de ponto de entrada `_start`, que chama a função de ponto de entrada de teste `test_main`. Não precisamos de nenhum atributo `cfg(test)` porque executáveis de teste de integração nunca são construídos em modo não-teste. + +Usamos a macro [`unimplemented`] que sempre entra em panic como placeholder para a função `test_runner` e apenas fazemos `loop` no handler de `panic` por enquanto. Idealmente, queremos implementar essas funções exatamente como fizemos no nosso `main.rs` usando a macro `serial_println` e a função `exit_qemu`. O problema é que não temos acesso a essas funções porque os testes são construídos completamente separados do nosso executável `main.rs`. + +[`unimplemented`]: https://doc.rust-lang.org/core/macro.unimplemented.html + +Se você executar `cargo test` neste estágio, entrará em um loop infinito porque o handler de panic faz loop infinitamente. Você precisa usar o atalho de teclado `ctrl+c` para sair do QEMU. + +### Criar uma Biblioteca + +Para tornar as funções necessárias disponíveis para nosso teste de integração, precisamos separar uma biblioteca do nosso `main.rs`, que pode ser incluída por outras crates e executáveis de teste de integração. Para fazer isso, criamos um novo arquivo `src/lib.rs`: + +```rust +// src/lib.rs + +#![no_std] + +``` + +Como o `main.rs`, o `lib.rs` é um arquivo especial que é automaticamente reconhecido pelo cargo. A biblioteca é uma unidade de compilação separada, então precisamos especificar o atributo `#![no_std]` novamente. + +Para fazer nossa biblioteca funcionar com `cargo test`, precisamos também mover as funções de teste e atributos de `main.rs` para `lib.rs`: + +```rust +// em src/lib.rs + +#![cfg_attr(test, no_main)] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; + +pub trait Testable { + fn run(&self) -> (); +} + +impl Testable for T +where + T: Fn(), +{ + fn run(&self) { + serial_print!("{}...\t", core::any::type_name::()); + self(); + serial_println!("[ok]"); + } +} + +pub fn test_runner(tests: &[&dyn Testable]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test.run(); + } + exit_qemu(QemuExitCode::Success); +} + +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + loop {} +} + +/// Ponto de entrada para `cargo test` +#[cfg(test)] +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + test_main(); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + test_panic_handler(info) +} +``` + +Para tornar nosso `test_runner` disponível para executáveis e testes de integração, o tornamos público e não aplicamos o atributo `cfg(test)` a ele. Também fatoramos a implementação do nosso handler de panic em uma função pública `test_panic_handler`, para que ela esteja disponível para executáveis também. + +Como nosso `lib.rs` é testado independentemente do nosso `main.rs`, precisamos adicionar um ponto de entrada `_start` e um handler de panic quando a biblioteca é compilada em modo de teste. Ao usar o atributo de crate [`cfg_attr`], habilitamos condicionalmente o atributo `no_main` neste caso. + +[`cfg_attr`]: https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute + +Também movemos o enum `QemuExitCode` e a função `exit_qemu` e os tornamos públicos: + +```rust +// em src/lib.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum QemuExitCode { + Success = 0x10, + Failed = 0x11, +} + +pub fn exit_qemu(exit_code: QemuExitCode) { + use x86_64::instructions::port::Port; + + unsafe { + let mut port = Port::new(0xf4); + port.write(exit_code as u32); + } +} +``` + +Agora executáveis e testes de integração podem importar essas funções da biblioteca e não precisam definir suas próprias implementações. Para também tornar `println` e `serial_println` disponíveis, movemos as declarações de módulo também: + +```rust +// em src/lib.rs + +pub mod serial; +pub mod vga_buffer; +``` + +Tornamos os módulos públicos para torná-los utilizáveis fora da nossa biblioteca. Isso também é necessário para tornar nossas macros `println` e `serial_println` utilizáveis, já que elas usam as funções `_print` dos módulos. + +Agora podemos atualizar nosso `main.rs` para usar a biblioteca: + +```rust +// em src/main.rs + +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(blog_os::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; +use blog_os::println; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + println!("Hello World{}", "!"); + + #[cfg(test)] + test_main(); + + loop {} +} + +/// Esta função é chamada em caso de pânico. +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +A biblioteca é utilizável como uma crate externa normal. É chamada `blog_os`, como nossa crate. O código acima usa a função `blog_os::test_runner` no atributo `test_runner` e a função `blog_os::test_panic_handler` no nosso handler de `panic` `cfg(test)`. Também importa a macro `println` para torná-la disponível para nossas funções `_start` e `panic`. + +Neste ponto, `cargo run` e `cargo test` devem funcionar novamente. É claro que `cargo test` ainda faz loop infinitamente (você pode sair com `ctrl+c`). Vamos corrigir isso usando as funções necessárias da biblioteca no nosso teste de integração. + +### Completando o Teste de Integração + +Como nosso `src/main.rs`, nosso executável `tests/basic_boot.rs` pode importar tipos da nossa nova biblioteca. Isso nos permite importar os componentes faltantes para completar nosso teste: + +```rust +// em tests/basic_boot.rs + +#![test_runner(blog_os::test_runner)] + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +Em vez de reimplementar o test runner, usamos a função `test_runner` da nossa biblioteca mudando o atributo `#![test_runner(crate::test_runner)]` para `#![test_runner(blog_os::test_runner)]`. Então não precisamos mais da função stub `test_runner` em `basic_boot.rs`, então podemos removê-la. Para nosso handler de `panic`, chamamos a função `blog_os::test_panic_handler` como fizemos no nosso `main.rs`. + +Agora `cargo test` sai normalmente novamente. Quando você o executa, verá que ele constrói e executa os testes para nosso `lib.rs`, `main.rs` e `basic_boot.rs` separadamente um após o outro. Para o `main.rs` e os testes de integração `basic_boot`, ele reporta "Running 0 tests" já que esses arquivos não têm nenhuma função anotada com `#[test_case]`. + +Agora podemos adicionar testes ao nosso `basic_boot.rs`. Por exemplo, podemos testar que `println` funciona sem entrar em panic, como fizemos nos testes do buffer VGA: + +```rust +// em tests/basic_boot.rs + +use blog_os::println; + +#[test_case] +fn test_println() { + println!("test_println output"); +} +``` + +Quando executamos `cargo test` agora, vemos que ele encontra e executa a função de teste. + +O teste pode parecer um pouco inútil agora já que é quase idêntico a um dos testes do buffer VGA. No entanto, no futuro, as funções `_start` do nosso `main.rs` e `lib.rs` podem crescer e chamar várias rotinas de inicialização antes de executar a função `test_main`, então os dois testes são executados em ambientes muito diferentes. + +Ao testar `println` em um ambiente `basic_boot` sem chamar nenhuma rotina de inicialização em `_start`, podemos garantir que `println` funciona logo após o boot. Isso é importante porque dependemos dele, por exemplo, para imprimir mensagens de panic. + +### Testes Futuros + +O poder dos testes de integração é que eles são tratados como executáveis completamente separados. Isso lhes dá controle completo sobre o ambiente, o que torna possível testar que o código interage corretamente com a CPU ou dispositivos de hardware. + +Nosso teste `basic_boot` é um exemplo muito simples de um teste de integração. No futuro, nosso kernel se tornará muito mais cheio de recursos e interagirá com o hardware de várias maneiras. Ao adicionar testes de integração, podemos garantir que essas interações funcionem (e continuem funcionando) como esperado. Algumas ideias para possíveis testes futuros são: + +- **Exceções de CPU**: Quando o código executa operações inválidas (por exemplo, divide por zero), a CPU lança uma exceção. O kernel pode registrar funções handler para tais exceções. Um teste de integração poderia verificar que o handler de exceção correto é chamado quando uma exceção de CPU ocorre ou que a execução continua corretamente após uma exceção resolvível. +- **Tabelas de Página**: Tabelas de página definem quais regiões de memória são válidas e acessíveis. Ao modificar as tabelas de página, é possível alocar novas regiões de memória, por exemplo ao lançar programas. Um teste de integração poderia modificar as tabelas de página na função `_start` e verificar que as modificações têm os efeitos desejados nas funções `#[test_case]`. +- **Programas Userspace**: Programas userspace são programas com acesso limitado aos recursos do sistema. Por exemplo, eles não têm acesso a estruturas de dados do kernel ou à memória de outros programas. Um teste de integração poderia lançar programas userspace que executam operações proibidas e verificar que o kernel as impede todas. + +Como você pode imaginar, muitos mais testes são possíveis. Ao adicionar tais testes, podemos garantir que não os quebramos acidentalmente quando adicionamos novos recursos ao nosso kernel ou refatoramos nosso código. Isso é especialmente importante quando nosso kernel se torna maior e mais complexo. + +### Testes que Devem Entrar em Panic + +O framework de testes da biblioteca padrão suporta um [atributo `#[should_panic]`][should_panic] que permite construir testes que devem falhar. Isso é útil, por exemplo, para verificar que uma função falha quando um argumento inválido é passado. Infelizmente, este atributo não é suportado em crates `#[no_std]` porque requer suporte da biblioteca padrão. + +[should_panic]: https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics + +Embora não possamos usar o atributo `#[should_panic]` no nosso kernel, podemos obter comportamento similar criando um teste de integração que sai com um código de erro de sucesso do handler de panic. Vamos começar a criar tal teste com o nome `should_panic`: + +```rust +// em tests/should_panic.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use blog_os::{QemuExitCode, exit_qemu, serial_println}; + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +Este teste ainda está incompleto, pois não define uma função `_start` ou nenhum dos atributos customizados de test runner ainda. Vamos adicionar as partes faltantes: + +```rust +// em tests/should_panic.rs + +#![feature(custom_test_frameworks)] +#![test_runner(test_runner)] +#![reexport_test_harness_main = "test_main"] + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + test_main(); + + loop {} +} + +pub fn test_runner(tests: &[&dyn Fn()]) { + serial_println!("Running {} tests", tests.len()); + for test in tests { + test(); + serial_println!("[test did not panic]"); + exit_qemu(QemuExitCode::Failed); + } + exit_qemu(QemuExitCode::Success); +} +``` + +Em vez de reutilizar o `test_runner` do nosso `lib.rs`, o teste define sua própria função `test_runner` que sai com um código de saída de falha quando um teste retorna sem entrar em panic (queremos que nossos testes entrem em panic). Se nenhuma função de teste for definida, o runner sai com um código de erro de sucesso. Como o runner sempre sai após executar um único teste, não faz sentido definir mais de uma função `#[test_case]`. + +Agora podemos criar um teste que deveria falhar: + +```rust +// em tests/should_panic.rs + +use blog_os::serial_print; + +#[test_case] +fn should_fail() { + serial_print!("should_panic::should_fail...\t"); + assert_eq!(0, 1); +} +``` + +O teste usa `assert_eq` para afirmar que `0` e `1` são iguais. É claro que isso falha, então nosso teste entra em panic como desejado. Note que precisamos imprimir manualmente o nome da função usando `serial_print!` aqui porque não usamos a trait `Testable`. + +Quando executamos o teste através de `cargo test --test should_panic` vemos que ele é bem-sucedido porque o teste entrou em panic como esperado. Quando comentamos a assertion e executamos o teste novamente, vemos que ele de fato falha com a mensagem _"test did not panic"_. + +Uma desvantagem significativa desta abordagem é que ela só funciona para uma única função de teste. Com múltiplas funções `#[test_case]`, apenas a primeira função é executada porque a execução não pode continuar após o handler de panic ter sido chamado. Atualmente não conheço uma boa maneira de resolver este problema, então me avise se você tiver uma ideia! + +### Testes Sem Harness + +Para testes de integração que têm apenas uma única função de teste (como nosso teste `should_panic`), o test runner realmente não é necessário. Para casos como este, podemos desabilitar o test runner completamente e executar nosso teste diretamente na função `_start`. + +A chave para isso é desabilitar a flag `harness` para o teste no `Cargo.toml`, que define se um test runner é usado para um teste de integração. Quando está definido como `false`, tanto o test runner padrão quanto o recurso de test runner customizado são desabilitados, de modo que o teste é tratado como um executável normal. + +Vamos desabilitar a flag `harness` para nosso teste `should_panic`: + +```toml +# em Cargo.toml + +[[test]] +name = "should_panic" +harness = false +``` + +Agora simplificamos vastamente nosso teste `should_panic` removendo o código relacionado ao `test_runner`. O resultado se parece com isto: + +```rust +// em tests/should_panic.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode}; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + should_fail(); + serial_println!("[test did not panic]"); + exit_qemu(QemuExitCode::Failed); + loop{} +} + +fn should_fail() { + serial_print!("should_panic::should_fail...\t"); + assert_eq!(0, 1); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +Agora chamamos a função `should_fail` diretamente da nossa função `_start` e saímos com um código de saída de falha se ela retornar. Quando executamos `cargo test --test should_panic` agora, vemos que o teste se comporta exatamente como antes. + +Além de criar testes `should_panic`, desabilitar o atributo `harness` também pode ser útil para testes de integração complexos, por exemplo, quando as funções de teste individuais têm efeitos colaterais e precisam ser executadas em uma ordem especificada. + +## Resumo + +Testes são uma técnica muito útil para garantir que certos componentes tenham o comportamento desejado. Mesmo que não possam mostrar a ausência de bugs, ainda são uma ferramenta útil para encontrá-los e especialmente para evitar regressões. + +Este post explicou como configurar um framework de testes para nosso kernel Rust. Usamos o recurso de frameworks de teste customizados do Rust para implementar suporte para um atributo `#[test_case]` simples no nosso ambiente bare metal. Usando o dispositivo `isa-debug-exit` do QEMU, nosso test runner pode sair do QEMU após executar os testes e reportar o status do teste. Para imprimir mensagens de erro no console em vez do buffer VGA, criamos um driver básico para a porta serial. + +Após criar alguns testes para nossa macro `println`, exploramos testes de integração na segunda metade do post. Aprendemos que eles vivem no diretório `tests` e são tratados como executáveis completamente separados. Para dar a eles acesso à função `exit_qemu` e à macro `serial_println`, movemos a maior parte do nosso código para uma biblioteca que pode ser importada por todos os executáveis e testes de integração. Como testes de integração são executados em seu próprio ambiente separado, eles tornam possível testar interações com o hardware ou criar testes que devem entrar em panic. + +Agora temos um framework de testes que executa em um ambiente realista dentro do QEMU. Ao criar mais testes em posts futuros, podemos manter nosso kernel sustentável quando ele se tornar mais complexo. + +## O que vem a seguir? + +No próximo post, exploraremos _exceções de CPU_. Essas exceções são lançadas pela CPU quando algo ilegal acontece, como uma divisão por zero ou um acesso a uma página de memória não mapeada (um chamado "page fault"). Ser capaz de capturar e examinar essas exceções é muito importante para depuração de erros futuros. O tratamento de exceções também é muito similar ao tratamento de interrupções de hardware, que é necessário para suporte a teclado. \ No newline at end of file diff --git a/blog/content/edition-2/posts/05-cpu-exceptions/index.pt-BR.md b/blog/content/edition-2/posts/05-cpu-exceptions/index.pt-BR.md new file mode 100644 index 00000000..a9035393 --- /dev/null +++ b/blog/content/edition-2/posts/05-cpu-exceptions/index.pt-BR.md @@ -0,0 +1,474 @@ ++++ +title = "Exceções de CPU" +weight = 5 +path = "pt-BR/cpu-exceptions" +date = 2018-06-17 + +[extra] +chapter = "Interrupções" +# Please update this when updating the translation +translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0" + +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Exceções de CPU ocorrem em várias situações errôneas, por exemplo, ao acessar um endereço de memória inválido ou ao dividir por zero. Para reagir a elas, precisamos configurar uma _tabela de descritores de interrupção_ que fornece funções manipuladoras. Ao final desta postagem, nosso kernel será capaz de capturar [exceções de breakpoint] e retomar a execução normal posteriormente. + +[exceções de breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-05`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-05 + + + +## Visão Geral +Uma exceção sinaliza que algo está errado com a instrução atual. Por exemplo, a CPU emite uma exceção se a instrução atual tenta dividir por 0. Quando uma exceção ocorre, a CPU interrompe seu trabalho atual e imediatamente chama uma função manipuladora de exceção específica, dependendo do tipo de exceção. + +No x86, existem cerca de 20 tipos diferentes de exceções de CPU. As mais importantes são: + +- **Page Fault**: Um page fault ocorre em acessos ilegais à memória. Por exemplo, se a instrução atual tenta ler de uma página não mapeada ou tenta escrever em uma página somente leitura. +- **Invalid Opcode**: Esta exceção ocorre quando a instrução atual é inválida, por exemplo, quando tentamos usar novas [instruções SSE] em uma CPU antiga que não as suporta. +- **General Protection Fault**: Esta é a exceção com a gama mais ampla de causas. Ela ocorre em vários tipos de violações de acesso, como tentar executar uma instrução privilegiada em código de nível de usuário ou escrever em campos reservados de registradores de configuração. +- **Double Fault**: Quando uma exceção ocorre, a CPU tenta chamar a função manipuladora correspondente. Se outra exceção ocorre _enquanto chama o manipulador de exceção_, a CPU levanta uma exceção de double fault. Esta exceção também ocorre quando não há função manipuladora registrada para uma exceção. +- **Triple Fault**: Se uma exceção ocorre enquanto a CPU tenta chamar a função manipuladora de double fault, ela emite um _triple fault_ fatal. Não podemos capturar ou manipular um triple fault. A maioria dos processadores reage redefinindo-se e reinicializando o sistema operacional. + +[instruções SSE]: https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions + +Para a lista completa de exceções, consulte a [wiki do OSDev][exceptions]. + +[exceptions]: https://wiki.osdev.org/Exceptions + +### A Tabela de Descritores de Interrupção +Para capturar e manipular exceções, precisamos configurar uma chamada _Tabela de Descritores de Interrupção_ (IDT - Interrupt Descriptor Table). Nesta tabela, podemos especificar uma função manipuladora para cada exceção de CPU. O hardware usa esta tabela diretamente, então precisamos seguir um formato predefinido. Cada entrada deve ter a seguinte estrutura de 16 bytes: + +Tipo| Nome | Descrição +----|--------------------------|----------------------------------- +u16 | Function Pointer [0:15] | Os bits inferiores do ponteiro para a função manipuladora. +u16 | GDT selector | Seletor de um segmento de código na [tabela de descritores globais]. +u16 | Options | (veja abaixo) +u16 | Function Pointer [16:31] | Os bits do meio do ponteiro para a função manipuladora. +u32 | Function Pointer [32:63] | Os bits restantes do ponteiro para a função manipuladora. +u32 | Reserved | + +[tabela de descritores globais]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +O campo options tem o seguinte formato: + +Bits | Nome | Descrição +------|-----------------------------------|----------------------------------- +0-2 | Interrupt Stack Table Index | 0: Não troca stacks, 1-7: Troca para a n-ésima stack na Interrupt Stack Table quando este manipulador é chamado. +3-7 | Reserved | +8 | 0: Interrupt Gate, 1: Trap Gate | Se este bit é 0, as interrupções são desativadas quando este manipulador é chamado. +9-11 | must be one | +12 | must be zero | +13‑14 | Descriptor Privilege Level (DPL) | O nível mínimo de privilégio necessário para chamar este manipulador. +15 | Present | + +Cada exceção tem um índice predefinido na IDT. Por exemplo, a exceção invalid opcode tem índice de tabela 6 e a exceção page fault tem índice de tabela 14. Assim, o hardware pode automaticamente carregar a entrada IDT correspondente para cada exceção. A [Tabela de Exceções][exceptions] na wiki do OSDev mostra os índices IDT de todas as exceções na coluna "Vector nr.". + +Quando uma exceção ocorre, a CPU aproximadamente faz o seguinte: + +1. Empurra alguns registradores na pilha, incluindo o ponteiro de instrução e o registrador [RFLAGS]. (Usaremos esses valores mais tarde nesta postagem.) +2. Lê a entrada correspondente da Tabela de Descritores de Interrupção (IDT). Por exemplo, a CPU lê a 14ª entrada quando ocorre um page fault. +3. Verifica se a entrada está presente e, se não estiver, levanta um double fault. +4. Desativa interrupções de hardware se a entrada é um interrupt gate (bit 40 não está definido). +5. Carrega o seletor [GDT] especificado no CS (segmento de código). +6. Pula para a função manipuladora especificada. + +[RFLAGS]: https://en.wikipedia.org/wiki/FLAGS_register +[GDT]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +Não se preocupe com os passos 4 e 5 por enquanto; aprenderemos sobre a tabela de descritores globais e interrupções de hardware em postagens futuras. + +## Um Tipo IDT +Em vez de criar nosso próprio tipo IDT, usaremos a [struct `InterruptDescriptorTable`] da crate `x86_64`, que se parece com isto: + +[struct `InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html + +``` rust +#[repr(C)] +pub struct InterruptDescriptorTable { + pub divide_by_zero: Entry, + pub debug: Entry, + pub non_maskable_interrupt: Entry, + pub breakpoint: Entry, + pub overflow: Entry, + pub bound_range_exceeded: Entry, + pub invalid_opcode: Entry, + pub device_not_available: Entry, + pub double_fault: Entry, + pub invalid_tss: Entry, + pub segment_not_present: Entry, + pub stack_segment_fault: Entry, + pub general_protection_fault: Entry, + pub page_fault: Entry, + pub x87_floating_point: Entry, + pub alignment_check: Entry, + pub machine_check: Entry, + pub simd_floating_point: Entry, + pub virtualization: Entry, + pub security_exception: Entry, + // alguns campos omitidos +} +``` + +Os campos têm o tipo [`idt::Entry`], que é uma struct que representa os campos de uma entrada IDT (veja a tabela acima). O parâmetro de tipo `F` define o tipo de função manipuladora esperado. Vemos que algumas entradas requerem uma [`HandlerFunc`] e algumas entradas requerem uma [`HandlerFuncWithErrCode`]. O page fault tem até seu próprio tipo especial: [`PageFaultHandlerFunc`]. + +[`idt::Entry`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.Entry.html +[`HandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFunc.html +[`HandlerFuncWithErrCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html +[`PageFaultHandlerFunc`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/type.PageFaultHandlerFunc.html + +Vamos olhar primeiro para o tipo `HandlerFunc`: + +```rust +type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame); +``` + +É um [type alias] para um tipo `extern "x86-interrupt" fn`. A palavra-chave `extern` define uma função com uma [convenção de chamada estrangeira] e é frequentemente usada para se comunicar com código C (`extern "C" fn`). Mas o que é a convenção de chamada `x86-interrupt`? + +[type alias]: https://doc.rust-lang.org/book/ch19-04-advanced-types.html#creating-type-synonyms-with-type-aliases +[convenção de chamada estrangeira]: https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions + +## A Convenção de Chamada de Interrupção +Exceções são bastante similares a chamadas de função: A CPU pula para a primeira instrução da função chamada e a executa. Depois, a CPU pula para o endereço de retorno e continua a execução da função pai. + +No entanto, há uma diferença importante entre exceções e chamadas de função: Uma chamada de função é invocada voluntariamente por uma instrução `call` inserida pelo compilador, enquanto uma exceção pode ocorrer em _qualquer_ instrução. Para entender as consequências desta diferença, precisamos examinar as chamadas de função em mais detalhes. + +[Convenções de chamada] especificam os detalhes de uma chamada de função. Por exemplo, elas especificam onde os parâmetros da função são colocados (por exemplo, em registradores ou na pilha) e como os resultados são retornados. No x86_64 Linux, as seguintes regras se aplicam para funções C (especificadas no [System V ABI]): + +[Convenções de chamada]: https://en.wikipedia.org/wiki/Calling_convention +[System V ABI]: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf + +- os primeiros seis argumentos inteiros são passados nos registradores `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` +- argumentos adicionais são passados na pilha +- resultados são retornados em `rax` e `rdx` + +Note que Rust não segue a ABI do C (na verdade, [nem existe uma ABI Rust ainda][rust abi]), então essas regras se aplicam apenas a funções declaradas como `extern "C" fn`. + +[rust abi]: https://github.com/rust-lang/rfcs/issues/600 + +### Registradores Preservados e Scratch +A convenção de chamada divide os registradores em duas partes: registradores _preservados_ e _scratch_. + +Os valores dos registradores _preservados_ devem permanecer inalterados entre chamadas de função. Portanto, uma função chamada (a _"callee"_) só tem permissão para sobrescrever esses registradores se restaurar seus valores originais antes de retornar. Portanto, esses registradores são chamados de _"callee-saved"_. Um padrão comum é salvar esses registradores na pilha no início da função e restaurá-los logo antes de retornar. + +Em contraste, uma função chamada tem permissão para sobrescrever registradores _scratch_ sem restrições. Se o chamador quiser preservar o valor de um registrador scratch entre uma chamada de função, ele precisa fazer backup e restaurá-lo antes da chamada de função (por exemplo, empurrando-o para a pilha). Portanto, os registradores scratch são _caller-saved_. + +No x86_64, a convenção de chamada C especifica os seguintes registradores preservados e scratch: + +registradores preservados | registradores scratch +---|--- +`rbp`, `rbx`, `rsp`, `r12`, `r13`, `r14`, `r15` | `rax`, `rcx`, `rdx`, `rsi`, `rdi`, `r8`, `r9`, `r10`, `r11` +_callee-saved_ | _caller-saved_ + +O compilador conhece essas regras, então gera o código de acordo. Por exemplo, a maioria das funções começa com um `push rbp`, que faz backup de `rbp` na pilha (porque é um registrador callee-saved). + +### Preservando Todos os Registradores +Em contraste com chamadas de função, exceções podem ocorrer em _qualquer_ instrução. Na maioria dos casos, não sabemos nem em tempo de compilação se o código gerado causará uma exceção. Por exemplo, o compilador não pode saber se uma instrução causa um stack overflow ou um page fault. + +Como não sabemos quando uma exceção ocorre, não podemos fazer backup de nenhum registrador antes. Isso significa que não podemos usar uma convenção de chamada que depende de registradores caller-saved para manipuladores de exceção. Em vez disso, precisamos de uma convenção de chamada que preserva _todos os registradores_. A convenção de chamada `x86-interrupt` é tal convenção de chamada, então garante que todos os valores de registrador são restaurados para seus valores originais no retorno da função. + +Note que isso não significa que todos os registradores são salvos na pilha na entrada da função. Em vez disso, o compilador apenas faz backup dos registradores que são sobrescritos pela função. Desta forma, código muito eficiente pode ser gerado para funções curtas que usam apenas alguns registradores. + +### O Stack Frame de Interrupção +Em uma chamada de função normal (usando a instrução `call`), a CPU empurra o endereço de retorno antes de pular para a função alvo. No retorno da função (usando a instrução `ret`), a CPU retira este endereço de retorno e pula para ele. Então o stack frame de uma chamada de função normal se parece com isto: + +![function stack frame](function-stack-frame.svg) + +Para manipuladores de exceção e interrupção, no entanto, empurrar um endereço de retorno não seria suficiente, já que manipuladores de interrupção frequentemente executam em um contexto diferente (ponteiro de pilha, flags da CPU, etc.). Em vez disso, a CPU executa os seguintes passos quando uma interrupção ocorre: + +0. **Salvando o antigo ponteiro de pilha**: A CPU lê os valores dos registradores ponteiro de pilha (`rsp`) e segmento de pilha (`ss`) e os lembra em um buffer interno. +1. **Alinhando o ponteiro de pilha**: Uma interrupção pode ocorrer em qualquer instrução, então o ponteiro de pilha pode ter qualquer valor também. No entanto, algumas instruções de CPU (por exemplo, algumas instruções SSE) requerem que o ponteiro de pilha esteja alinhado em um limite de 16 bytes, então a CPU realiza tal alinhamento logo após a interrupção. +2. **Trocando pilhas** (em alguns casos): Uma troca de pilha ocorre quando o nível de privilégio da CPU muda, por exemplo, quando uma exceção de CPU ocorre em um programa em modo usuário. Também é possível configurar trocas de pilha para interrupções específicas usando a chamada _Interrupt Stack Table_ (descrita na próxima postagem). +3. **Empurrando o antigo ponteiro de pilha**: A CPU empurra os valores `rsp` e `ss` do passo 0 para a pilha. Isso torna possível restaurar o ponteiro de pilha original ao retornar de um manipulador de interrupção. +4. **Empurrando e atualizando o registrador `RFLAGS`**: O registrador [`RFLAGS`] contém vários bits de controle e status. Na entrada de interrupção, a CPU muda alguns bits e empurra o valor antigo. +5. **Empurrando o ponteiro de instrução**: Antes de pular para a função manipuladora de interrupção, a CPU empurra o ponteiro de instrução (`rip`) e o segmento de código (`cs`). Isso é comparável ao push de endereço de retorno de uma chamada de função normal. +6. **Empurrando um código de erro** (para algumas exceções): Para algumas exceções específicas, como page faults, a CPU empurra um código de erro, que descreve a causa da exceção. +7. **Invocando o manipulador de interrupção**: A CPU lê o endereço e o descritor de segmento da função manipuladora de interrupção do campo correspondente na IDT. Ela então invoca este manipulador carregando os valores nos registradores `rip` e `cs`. + +[`RFLAGS`]: https://en.wikipedia.org/wiki/FLAGS_register + +Então o _interrupt stack frame_ se parece com isto: + +![interrupt stack frame](exception-stack-frame.svg) + +Na crate `x86_64`, o interrupt stack frame é representado pela struct [`InterruptStackFrame`]. Ela é passada para manipuladores de interrupção como `&mut` e pode ser usada para recuperar informações adicionais sobre a causa da exceção. A struct não contém campo de código de erro, já que apenas algumas exceções empurram um código de erro. Essas exceções usam o tipo de função [`HandlerFuncWithErrCode`] separado, que tem um argumento adicional `error_code`. + +[`InterruptStackFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptStackFrame.html + +### Por Trás das Cortinas +A convenção de chamada `x86-interrupt` é uma abstração poderosa que esconde quase todos os detalhes confusos do processo de manipulação de exceção. No entanto, às vezes é útil saber o que está acontecendo por trás das cortinas. Aqui está uma breve visão geral das coisas das quais a convenção de chamada `x86-interrupt` cuida: + +- **Recuperando os argumentos**: A maioria das convenções de chamada espera que os argumentos sejam passados em registradores. Isso não é possível para manipuladores de exceção, já que não devemos sobrescrever nenhum valor de registrador antes de fazer backup deles na pilha. Em vez disso, a convenção de chamada `x86-interrupt` está ciente de que os argumentos já estão na pilha em um deslocamento específico. +- **Retornando usando `iretq`**: Como o interrupt stack frame difere completamente dos stack frames de chamadas de função normais, não podemos retornar de funções manipuladoras através da instrução `ret` normal. Então, em vez disso, a instrução `iretq` deve ser usada. +- **Manipulando o código de erro**: O código de erro, que é empurrado para algumas exceções, torna as coisas muito mais complexas. Ele muda o alinhamento da pilha (veja o próximo ponto) e precisa ser retirado da pilha antes de retornar. A convenção de chamada `x86-interrupt` manipula toda essa complexidade. No entanto, ela não sabe qual função manipuladora é usada para qual exceção, então precisa deduzir essa informação do número de argumentos da função. Isso significa que o programador ainda é responsável por usar o tipo de função correto para cada exceção. Felizmente, o tipo `InterruptDescriptorTable` definido pela crate `x86_64` garante que os tipos de função corretos são usados. +- **Alinhando a pilha**: Algumas instruções (especialmente instruções SSE) requerem um alinhamento de pilha de 16 bytes. A CPU garante esse alinhamento sempre que uma exceção ocorre, mas para algumas exceções ela o destrói novamente mais tarde quando empurra um código de erro. A convenção de chamada `x86-interrupt` cuida disso realinhando a pilha neste caso. + +Se você estiver interessado em mais detalhes, também temos uma série de postagens que explica a manipulação de exceção usando [funções nuas] vinculadas [no final desta postagem][too-much-magic]. + +[funções nuas]: https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md +[too-much-magic]: #muita-magica + +## Implementação +Agora que entendemos a teoria, é hora de manipular exceções de CPU em nosso kernel. Começaremos criando um novo módulo interrupts em `src/interrupts.rs`, que primeiro cria uma função `init_idt` que cria uma nova `InterruptDescriptorTable`: + +``` rust +// em src/lib.rs + +pub mod interrupts; + +// em src/interrupts.rs + +use x86_64::structures::idt::InterruptDescriptorTable; + +pub fn init_idt() { + let mut idt = InterruptDescriptorTable::new(); +} +``` + +Agora podemos adicionar funções manipuladoras. Começamos adicionando um manipulador para a [exceção de breakpoint]. A exceção de breakpoint é a exceção perfeita para testar a manipulação de exceção. Seu único propósito é pausar temporariamente um programa quando a instrução de breakpoint `int3` é executada. + +[exceção de breakpoint]: https://wiki.osdev.org/Exceptions#Breakpoint + +A exceção de breakpoint é comumente usada em debuggers: Quando o usuário define um breakpoint, o debugger sobrescreve a instrução correspondente com a instrução `int3` para que a CPU lance a exceção de breakpoint quando atinge aquela linha. Quando o usuário quer continuar o programa, o debugger substitui a instrução `int3` pela instrução original novamente e continua o programa. Para mais detalhes, veja a série ["_How debuggers work_"]. + +["_How debuggers work_"]: https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints + +Para nosso caso de uso, não precisamos sobrescrever nenhuma instrução. Em vez disso, queremos apenas imprimir uma mensagem quando a instrução de breakpoint é executada e então continuar o programa. Então vamos criar uma função `breakpoint_handler` simples e adicioná-la à nossa IDT: + +```rust +// em src/interrupts.rs + +use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame}; +use crate::println; + +pub fn init_idt() { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); +} + +extern "x86-interrupt" fn breakpoint_handler( + stack_frame: InterruptStackFrame) +{ + println!("EXCEÇÃO: BREAKPOINT\n{:#?}", stack_frame); +} +``` + +Nosso manipulador apenas produz uma mensagem e imprime de forma bonita o interrupt stack frame. + +Quando tentamos compilá-lo, o seguinte erro ocorre: + +``` +error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180) + --> src/main.rs:53:1 + | +53 | / extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) { +54 | | println!("EXCEÇÃO: BREAKPOINT\n{:#?}", stack_frame); +55 | | } + | |_^ + | + = help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable +``` + +Este erro ocorre porque a convenção de chamada `x86-interrupt` ainda é instável. Para usá-la de qualquer forma, temos que habilitá-la explicitamente adicionando `#![feature(abi_x86_interrupt)]` no topo do nosso `lib.rs`. + +### Carregando a IDT +Para que a CPU use nossa nova tabela de descritores de interrupção, precisamos carregá-la usando a instrução [`lidt`]. A struct `InterruptDescriptorTable` da crate `x86_64` fornece um método [`load`][InterruptDescriptorTable::load] para isso. Vamos tentar usá-lo: + +[`lidt`]: https://www.felixcloutier.com/x86/lgdt:lidt +[InterruptDescriptorTable::load]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load + +```rust +// em src/interrupts.rs + +pub fn init_idt() { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt.load(); +} +``` + +Quando tentamos compilar agora, o seguinte erro ocorre: + +``` +error: `idt` does not live long enough + --> src/interrupts/mod.rs:43:5 + | +43 | idt.load(); + | ^^^ does not live long enough +44 | } + | - borrowed value only lives until here + | + = note: borrowed value must be valid for the static lifetime... +``` + +Então o método `load` espera um `&'static self`, isto é, uma referência válida para o tempo de execução completo do programa. A razão é que a CPU acessará esta tabela em cada interrupção até carregarmos uma IDT diferente. Então usar um tempo de vida menor que `'static` poderia levar a bugs de use-after-free. + +De fato, isso é exatamente o que acontece aqui. Nossa `idt` é criada na pilha, então ela é válida apenas dentro da função `init`. Depois, a memória da pilha é reutilizada para outras funções, então a CPU interpretaria memória aleatória da pilha como IDT. Felizmente, o método `InterruptDescriptorTable::load` codifica este requisito de tempo de vida em sua definição de função, para que o compilador Rust seja capaz de prevenir este possível bug em tempo de compilação. + +Para corrigir este problema, precisamos armazenar nossa `idt` em um lugar onde ela tenha um tempo de vida `'static`. Para conseguir isso, poderíamos alocar nossa IDT no heap usando [`Box`] e então convertê-la para uma referência `'static`, mas estamos escrevendo um kernel de SO e, portanto, não temos um heap (ainda). + +[`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html + + +Como alternativa, poderíamos tentar armazenar a IDT como uma `static`: + +```rust +static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new(); + +pub fn init_idt() { + IDT.breakpoint.set_handler_fn(breakpoint_handler); + IDT.load(); +} +``` + +No entanto, há um problema: Statics são imutáveis, então não podemos modificar a entrada de breakpoint da nossa função `init`. Poderíamos resolver este problema usando uma [`static mut`]: + +[`static mut`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable + +```rust +static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new(); + +pub fn init_idt() { + unsafe { + IDT.breakpoint.set_handler_fn(breakpoint_handler); + IDT.load(); + } +} +``` + +Esta variante compila sem erros, mas está longe de ser idiomática. `static mut`s são muito propensas a data races, então precisamos de um [bloco `unsafe`] em cada acesso. + +[bloco `unsafe`]: https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers + +#### Lazy Statics ao Resgate +Felizmente, a macro `lazy_static` existe. Em vez de avaliar uma `static` em tempo de compilação, a macro realiza a inicialização quando a `static` é referenciada pela primeira vez. Assim, podemos fazer quase tudo no bloco de inicialização e somos até capazes de ler valores de tempo de execução. + +Já importamos a crate `lazy_static` quando [criamos uma abstração para o buffer de texto VGA][vga text buffer lazy static]. Então podemos usar diretamente a macro `lazy_static!` para criar nossa IDT estática: + +[vga text buffer lazy static]: @/edition-2/posts/03-vga-text-buffer/index.md#lazy-statics + +```rust +// em src/interrupts.rs + +use lazy_static::lazy_static; + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt + }; +} + +pub fn init_idt() { + IDT.load(); +} +``` + +Note como esta solução não requer blocos `unsafe`. A macro `lazy_static!` usa `unsafe` por trás dos panos, mas é abstraída em uma interface segura. + +### Executando + +O último passo para fazer exceções funcionarem em nosso kernel é chamar a função `init_idt` do nosso `main.rs`. Em vez de chamá-la diretamente, introduzimos uma função geral `init` em nosso `lib.rs`: + +```rust +// em src/lib.rs + +pub fn init() { + interrupts::init_idt(); +} +``` + +Com esta função, agora temos um lugar central para rotinas de inicialização que podem ser compartilhadas entre as diferentes funções `_start` em nosso `main.rs`, `lib.rs` e testes de integração. + +Agora podemos atualizar a função `_start` do nosso `main.rs` para chamar `init` e então disparar uma exceção de breakpoint: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + println!("Olá Mundo{}", "!"); + + blog_os::init(); // novo + + // invoca uma exceção de breakpoint + x86_64::instructions::interrupts::int3(); // novo + + // como antes + #[cfg(test)] + test_main(); + + println!("Não crashou!"); + loop {} +} +``` + +Quando executamos agora no QEMU (usando `cargo run`), vemos o seguinte: + +![QEMU printing `EXCEÇÃO: BREAKPOINT` and the interrupt stack frame](qemu-breakpoint-exception.png) + +Funciona! A CPU invoca com sucesso nosso manipulador de breakpoint, que imprime a mensagem, e então retorna de volta para a função `_start`, onde a mensagem `Não crashou!` é impressa. + +Vemos que o interrupt stack frame nos diz os ponteiros de instrução e pilha no momento em que a exceção ocorreu. Esta informação é muito útil ao depurar exceções inesperadas. + +### Adicionando um Teste + +Vamos criar um teste que garante que o acima continue funcionando. Primeiro, atualizamos a função `_start` para também chamar `init`: + +```rust +// em src/lib.rs + +/// Ponto de entrada para `cargo test` +#[cfg(test)] +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + init(); // novo + test_main(); + loop {} +} +``` + +Lembre-se, esta função `_start` é usada quando executamos `cargo test --lib`, já que Rust testa o `lib.rs` completamente independente do `main.rs`. Precisamos chamar `init` aqui para configurar uma IDT antes de executar os testes. + +Agora podemos criar um teste `test_breakpoint_exception`: + +```rust +// em src/interrupts.rs + +#[test_case] +fn test_breakpoint_exception() { + // invoca uma exceção de breakpoint + x86_64::instructions::interrupts::int3(); +} +``` + +O teste invoca a função `int3` para disparar uma exceção de breakpoint. Ao verificar que a execução continua depois, verificamos que nosso manipulador de breakpoint está funcionando corretamente. + +Você pode tentar este novo teste executando `cargo test` (todos os testes) ou `cargo test --lib` (apenas testes de `lib.rs` e seus módulos). Você deve ver o seguinte na saída: + +``` +blog_os::interrupts::test_breakpoint_exception... [ok] +``` + +## Muita Mágica? +A convenção de chamada `x86-interrupt` e o tipo [`InterruptDescriptorTable`] tornaram o processo de manipulação de exceção relativamente simples e indolor. Se isso foi muita mágica para você e você gostaria de aprender todos os detalhes sórdidos da manipulação de exceção, nós temos você coberto: Nossa série ["Manipulando Exceções com Funções Nuas"] mostra como manipular exceções sem a convenção de chamada `x86-interrupt` e também cria seu próprio tipo IDT. Historicamente, essas postagens eram as principais postagens de manipulação de exceção antes que a convenção de chamada `x86-interrupt` e a crate `x86_64` existissem. Note que essas postagens são baseadas na [primeira edição] deste blog e podem estar desatualizadas. + +["Manipulando Exceções com Funções Nuas"]: @/edition-1/extra/naked-exceptions/_index.md +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[primeira edição]: @/edition-1/_index.md + +## O Que Vem a Seguir? +Capturamos com sucesso nossa primeira exceção e retornamos dela! O próximo passo é garantir que capturemos todas as exceções porque uma exceção não capturada causa um [triple fault] fatal, que leva a uma redefinição do sistema. A próxima postagem explica como podemos evitar isso capturando corretamente [double faults]. + +[triple fault]: https://wiki.osdev.org/Triple_Fault +[double faults]: https://wiki.osdev.org/Double_Fault#Double_Fault \ No newline at end of file diff --git a/blog/content/edition-2/posts/06-double-faults/index.pt-BR.md b/blog/content/edition-2/posts/06-double-faults/index.pt-BR.md new file mode 100644 index 00000000..e9d2a90d --- /dev/null +++ b/blog/content/edition-2/posts/06-double-faults/index.pt-BR.md @@ -0,0 +1,556 @@ ++++ +title = "Double Faults" +weight = 6 +path = "pt-BR/double-fault-exceptions" +date = 2018-06-18 + +[extra] +chapter = "Interrupções" +# Please update this when updating the translation +translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0" + +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Esta postagem explora a exceção de double fault em detalhes, que ocorre quando a CPU falha ao invocar um manipulador de exceção. Ao manipular esta exceção, evitamos _triple faults_ fatais que causam uma redefinição do sistema. Para prevenir triple faults em todos os casos, também configuramos uma _Interrupt Stack Table_ para capturar double faults em uma pilha de kernel separada. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-06`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-06 + + + +## O Que é um Double Fault? +Em termos simplificados, um double fault é uma exceção especial que ocorre quando a CPU falha ao invocar um manipulador de exceção. Por exemplo, ele ocorre quando um page fault é disparado mas não há manipulador de page fault registrado na [Tabela de Descritores de Interrupção][IDT] (IDT). Então é meio similar aos blocos catch-all em linguagens de programação com exceções, por exemplo, `catch(...)` em C++ ou `catch(Exception e)` em Java ou C#. + +[IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table + +Um double fault se comporta como uma exceção normal. Ele tem o número de vetor `8` e podemos definir uma função manipuladora normal para ele na IDT. É realmente importante fornecer um manipulador de double fault, porque se um double fault não é manipulado, ocorre um _triple fault_ fatal. Triple faults não podem ser capturados, e a maioria do hardware reage com uma redefinição do sistema. + +### Disparando um Double Fault +Vamos provocar um double fault disparando uma exceção para a qual não definimos uma função manipuladora: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + println!("Olá Mundo{}", "!"); + + blog_os::init(); + + // dispara um page fault + unsafe { + *(0xdeadbeef as *mut u8) = 42; + }; + + // como antes + #[cfg(test)] + test_main(); + + println!("Não crashou!"); + loop {} +} +``` + +Usamos `unsafe` para escrever no endereço inválido `0xdeadbeef`. O endereço virtual não está mapeado para um endereço físico nas tabelas de página, então ocorre um page fault. Não registramos um manipulador de page fault em nossa [IDT], então ocorre um double fault. + +Quando iniciamos nosso kernel agora, vemos que ele entra em um loop de boot infinito. A razão para o loop de boot é a seguinte: + +1. A CPU tenta escrever em `0xdeadbeef`, o que causa um page fault. +2. A CPU olha para a entrada correspondente na IDT e vê que nenhuma função manipuladora está especificada. Assim, ela não pode chamar o manipulador de page fault e ocorre um double fault. +3. A CPU olha para a entrada IDT do manipulador de double fault, mas esta entrada também não especifica uma função manipuladora. Assim, ocorre um _triple_ fault. +4. Um triple fault é fatal. QEMU reage a ele como a maioria do hardware real e emite uma redefinição do sistema. + +Então, para prevenir este triple fault, precisamos fornecer uma função manipuladora para page faults ou um manipulador de double fault. Queremos evitar triple faults em todos os casos, então vamos começar com um manipulador de double fault que é invocado para todos os tipos de exceção não manipulados. + +## Um Manipulador de Double Fault +Um double fault é uma exceção normal com um código de erro, então podemos especificar uma função manipuladora similar ao nosso manipulador de breakpoint: + +```rust +// em src/interrupts.rs + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + idt.double_fault.set_handler_fn(double_fault_handler); // novo + idt + }; +} + +// novo +extern "x86-interrupt" fn double_fault_handler( + stack_frame: InterruptStackFrame, _error_code: u64) -> ! +{ + panic!("EXCEÇÃO: DOUBLE FAULT\n{:#?}", stack_frame); +} +``` + +Nosso manipulador imprime uma mensagem de erro curta e despeja o exception stack frame. O código de erro do manipulador de double fault é sempre zero, então não há razão para imprimi-lo. Uma diferença para o manipulador de breakpoint é que o manipulador de double fault é [_divergente_]. A razão é que a arquitetura `x86_64` não permite retornar de uma exceção de double fault. + +[_divergente_]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html + +Quando iniciamos nosso kernel agora, devemos ver que o manipulador de double fault é invocado: + +![QEMU printing `EXCEÇÃO: DOUBLE FAULT` and the exception stack frame](qemu-catch-double-fault.png) + +Funcionou! Aqui está o que aconteceu desta vez: + +1. A CPU tenta escrever em `0xdeadbeef`, o que causa um page fault. +2. Como antes, a CPU olha para a entrada correspondente na IDT e vê que nenhuma função manipuladora está definida. Assim, ocorre um double fault. +3. A CPU pula para o manipulador de double fault – agora presente. + +O triple fault (e o loop de boot) não ocorre mais, já que a CPU agora pode chamar o manipulador de double fault. + +Isso foi bem direto! Então por que precisamos de uma postagem inteira para este tópico? Bem, agora somos capazes de capturar a _maioria_ dos double faults, mas há alguns casos onde nossa abordagem atual não é suficiente. + +## Causas de Double Faults +Antes de olharmos para os casos especiais, precisamos conhecer as causas exatas de double faults. Acima, usamos uma definição bem vaga: + +> Um double fault é uma exceção especial que ocorre quando a CPU falha ao invocar um manipulador de exceção. + +O que _"falha ao invocar"_ significa exatamente? O manipulador não está presente? O manipulador está [trocado para fora]? E o que acontece se um manipulador causa exceções ele mesmo? + +[trocado para fora]: http://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf + +Por exemplo, o que acontece se: + +1. uma exceção de breakpoint ocorre, mas a função manipuladora correspondente está trocada para fora? +2. um page fault ocorre, mas o manipulador de page fault está trocado para fora? +3. um manipulador de divide-by-zero causa uma exceção de breakpoint, mas o manipulador de breakpoint está trocado para fora? +4. nosso kernel estoura sua pilha e a _guard page_ é atingida? + +Felizmente, o manual AMD64 ([PDF][AMD64 manual]) tem uma definição exata (na Seção 8.2.9). De acordo com ele, uma "exceção de double fault _pode_ ocorrer quando uma segunda exceção ocorre durante a manipulação de um manipulador de exceção anterior (primeira)". O _"pode"_ é importante: Apenas combinações muito específicas de exceções levam a um double fault. Essas combinações são: + +| Primeira Exceção | Segunda Exceção | +| --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| [Divide-by-zero],
[Invalid TSS],
[Segment Not Present],
[Stack-Segment Fault],
[General Protection Fault] | [Invalid TSS],
[Segment Not Present],
[Stack-Segment Fault],
[General Protection Fault] | +| [Page Fault] | [Page Fault],
[Invalid TSS],
[Segment Not Present],
[Stack-Segment Fault],
[General Protection Fault] | + +[Divide-by-zero]: https://wiki.osdev.org/Exceptions#Division_Error +[Invalid TSS]: https://wiki.osdev.org/Exceptions#Invalid_TSS +[Segment Not Present]: https://wiki.osdev.org/Exceptions#Segment_Not_Present +[Stack-Segment Fault]: https://wiki.osdev.org/Exceptions#Stack-Segment_Fault +[General Protection Fault]: https://wiki.osdev.org/Exceptions#General_Protection_Fault +[Page Fault]: https://wiki.osdev.org/Exceptions#Page_Fault + + +[AMD64 manual]: https://www.amd.com/system/files/TechDocs/24593.pdf + +Então, por exemplo, uma falha de divide-by-zero seguida de um page fault está ok (o manipulador de page fault é invocado), mas uma falha de divide-by-zero seguida de um general-protection fault leva a um double fault. + +Com a ajuda desta tabela, podemos responder às primeiras três das questões acima: + +1. Se uma exceção de breakpoint ocorre e a função manipuladora correspondente está trocada para fora, ocorre um _page fault_ e o _manipulador de page fault_ é invocado. +2. Se um page fault ocorre e o manipulador de page fault está trocado para fora, ocorre um _double fault_ e o _manipulador de double fault_ é invocado. +3. Se um manipulador de divide-by-zero causa uma exceção de breakpoint, a CPU tenta invocar o manipulador de breakpoint. Se o manipulador de breakpoint está trocado para fora, ocorre um _page fault_ e o _manipulador de page fault_ é invocado. + +De fato, até o caso de uma exceção sem função manipuladora na IDT segue este esquema: Quando a exceção ocorre, a CPU tenta ler a entrada IDT correspondente. Como a entrada é 0, que não é uma entrada IDT válida, ocorre um _general protection fault_. Não definimos uma função manipuladora para o general protection fault também, então outro general protection fault ocorre. De acordo com a tabela, isso leva a um double fault. + +### Kernel Stack Overflow +Vamos olhar para a quarta questão: + +> O que acontece se nosso kernel estoura sua pilha e a guard page é atingida? + +Uma guard page é uma página de memória especial na parte inferior de uma pilha que torna possível detectar estouros de pilha. A página não está mapeada para nenhum frame físico, então acessá-la causa um page fault em vez de silenciosamente corromper outra memória. O bootloader configura uma guard page para nossa pilha de kernel, então um stack overflow causa um _page fault_. + +Quando um page fault ocorre, a CPU olha para o manipulador de page fault na IDT e tenta empurrar o [interrupt stack frame] na pilha. No entanto, o ponteiro de pilha atual ainda aponta para a guard page não presente. Assim, ocorre um segundo page fault, que causa um double fault (de acordo com a tabela acima). + +[interrupt stack frame]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-stack-frame + +Então a CPU tenta chamar o _manipulador de double fault_ agora. No entanto, em um double fault, a CPU tenta empurrar o exception stack frame também. O ponteiro de pilha ainda aponta para a guard page, então ocorre um _terceiro_ page fault, que causa um _triple fault_ e uma reinicialização do sistema. Então nosso manipulador de double fault atual não pode evitar um triple fault neste caso. + +Vamos tentar nós mesmos! Podemos facilmente provocar um kernel stack overflow chamando uma função que recursa infinitamente: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] // não altere (mangle) o nome desta função +pub extern "C" fn _start() -> ! { + println!("Olá Mundo{}", "!"); + + blog_os::init(); + + fn stack_overflow() { + stack_overflow(); // para cada recursão, o endereço de retorno é empurrado + } + + // dispara um stack overflow + stack_overflow(); + + […] // test_main(), println(…), e loop {} +} +``` + +Quando tentamos este código no QEMU, vemos que o sistema entra em um bootloop novamente. + +Então como podemos evitar este problema? Não podemos omitir o empurrar do exception stack frame, já que a própria CPU faz isso. Então precisamos garantir de alguma forma que a pilha esteja sempre válida quando uma exceção de double fault ocorre. Felizmente, a arquitetura x86_64 tem uma solução para este problema. + +## Trocando Pilhas +A arquitetura x86_64 é capaz de trocar para uma pilha predefinida e conhecida como boa quando uma exceção ocorre. Esta troca acontece em nível de hardware, então pode ser realizada antes que a CPU empurre o exception stack frame. + +O mecanismo de troca é implementado como uma _Interrupt Stack Table_ (IST). A IST é uma tabela de 7 ponteiros para pilhas conhecidas como boas. Em pseudocódigo similar a Rust: + +```rust +struct InterruptStackTable { + stack_pointers: [Option; 7], +} +``` + +Para cada manipulador de exceção, podemos escolher uma pilha da IST através do campo `stack_pointers` na [entrada IDT] correspondente. Por exemplo, nosso manipulador de double fault poderia usar a primeira pilha na IST. Então a CPU automaticamente troca para esta pilha sempre que ocorre um double fault. Esta troca aconteceria antes de qualquer coisa ser empurrada, prevenindo o triple fault. + +[entrada IDT]: @/edition-2/posts/05-cpu-exceptions/index.md#the-interrupt-descriptor-table + +### A IST e TSS +A Interrupt Stack Table (IST) é parte de uma antiga estrutura legada chamada _[Task State Segment]_ \(TSS). A TSS costumava armazenar várias informações (por exemplo, estado de registradores do processador) sobre uma tarefa no modo de 32 bits e era, por exemplo, usada para [troca de contexto de hardware]. No entanto, a troca de contexto de hardware não é mais suportada no modo de 64 bits e o formato da TSS mudou completamente. + +[Task State Segment]: https://en.wikipedia.org/wiki/Task_state_segment +[troca de contexto de hardware]: https://wiki.osdev.org/Context_Switching#Hardware_Context_Switching + +No x86_64, a TSS não armazena mais nenhuma informação específica de tarefa. Em vez disso, ela armazena duas tabelas de pilha (a IST é uma delas). O único campo comum entre a TSS de 32 bits e 64 bits é o ponteiro para o [bitmap de permissões de porta I/O]. + +[bitmap de permissões de porta I/O]: https://en.wikipedia.org/wiki/Task_state_segment#I.2FO_port_permissions + +A TSS de 64 bits tem o seguinte formato: + +| Campo | Tipo | +| -------------------------------------------- | ---------- | +| (reservado) | `u32` | +| Privilege Stack Table | `[u64; 3]` | +| (reservado) | `u64` | +| Interrupt Stack Table | `[u64; 7]` | +| (reservado) | `u64` | +| (reservado) | `u16` | +| I/O Map Base Address | `u16` | + +A _Privilege Stack Table_ é usada pela CPU quando o nível de privilégio muda. Por exemplo, se uma exceção ocorre enquanto a CPU está em modo usuário (nível de privilégio 3), a CPU normalmente troca para o modo kernel (nível de privilégio 0) antes de invocar o manipulador de exceção. Nesse caso, a CPU trocaria para a 0ª pilha na Privilege Stack Table (já que 0 é o nível de privilégio alvo). Ainda não temos nenhum programa em modo usuário, então ignoraremos esta tabela por enquanto. + +### Criando uma TSS +Vamos criar uma nova TSS que contém uma pilha de double fault separada em sua interrupt stack table. Para isso, precisamos de uma struct TSS. Felizmente, a crate `x86_64` já contém uma [struct `TaskStateSegment`] que podemos usar. + +[struct `TaskStateSegment`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/tss/struct.TaskStateSegment.html + +Criamos a TSS em um novo módulo `gdt` (o nome fará sentido mais tarde): + +```rust +// em src/lib.rs + +pub mod gdt; + +// em src/gdt.rs + +use x86_64::VirtAddr; +use x86_64::structures::tss::TaskStateSegment; +use lazy_static::lazy_static; + +pub const DOUBLE_FAULT_IST_INDEX: u16 = 0; + +lazy_static! { + static ref TSS: TaskStateSegment = { + let mut tss = TaskStateSegment::new(); + tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = { + const STACK_SIZE: usize = 4096 * 5; + static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE]; + + let stack_start = VirtAddr::from_ptr(&raw const STACK); + let stack_end = stack_start + STACK_SIZE; + stack_end + }; + tss + }; +} +``` + +Usamos `lazy_static` porque o avaliador const de Rust ainda não é poderoso o suficiente para fazer esta inicialização em tempo de compilação. Definimos que a 0ª entrada IST é a pilha de double fault (qualquer outro índice IST funcionaria também). Então escrevemos o endereço superior de uma pilha de double fault na 0ª entrada. Escrevemos o endereço superior porque pilhas no x86 crescem para baixo, isto é, de endereços altos para endereços baixos. + +Ainda não implementamos gerenciamento de memória, então não temos uma forma apropriada de alocar uma nova pilha. Em vez disso, usamos um array `static mut` como armazenamento de pilha por enquanto. É importante que seja uma `static mut` e não uma `static` imutável, porque caso contrário o bootloader a mapearia para uma página somente leitura. Substituiremos isto por uma alocação de pilha apropriada em uma postagem posterior. + +Note que esta pilha de double fault não tem guard page que proteja contra stack overflow. Isso significa que não devemos fazer nada intensivo em pilha em nosso manipulador de double fault porque um stack overflow poderia corromper a memória abaixo da pilha. + +#### Carregando a TSS +Agora que criamos uma nova TSS, precisamos de uma forma de dizer à CPU que ela deve usá-la. Infelizmente, isto é um pouco trabalhoso, já que a TSS usa o sistema de segmentação (por razões históricas). Em vez de carregar a tabela diretamente, precisamos adicionar um novo descritor de segmento à [Tabela de Descritores Globais] \(GDT). Então podemos carregar nossa TSS invocando a [instrução `ltr`] com o respectivo índice GDT. (Esta é a razão pela qual nomeamos nosso módulo `gdt`.) + +[Tabela de Descritores Globais]: https://web.archive.org/web/20190217233448/https://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/ +[instrução `ltr`]: https://www.felixcloutier.com/x86/ltr + +### A Tabela de Descritores Globais +A Tabela de Descritores Globais (GDT - Global Descriptor Table) é uma relíquia que foi usada para [segmentação de memória] antes de paginação se tornar o padrão de fato. No entanto, ela ainda é necessária no modo de 64 bits para várias coisas, como configuração de modo kernel/usuário ou carregamento de TSS. + +[segmentação de memória]: https://en.wikipedia.org/wiki/X86_memory_segmentation + +A GDT é uma estrutura que contém os _segmentos_ do programa. Ela foi usada em arquiteturas mais antigas para isolar programas uns dos outros antes de paginação se tornar o padrão. Para mais informações sobre segmentação, confira o capítulo de mesmo nome do livro gratuito ["Three Easy Pieces"]. Embora a segmentação não seja mais suportada no modo de 64 bits, a GDT ainda existe. Ela é usada principalmente para duas coisas: Trocar entre espaço de kernel e espaço de usuário, e carregar uma estrutura TSS. + +["Three Easy Pieces"]: http://pages.cs.wisc.edu/~remzi/OSTEP/ + +#### Criando uma GDT +Vamos criar uma `GDT` estática que inclui um segmento para nossa `TSS` estática: + +```rust +// em src/gdt.rs + +use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor}; + +lazy_static! { + static ref GDT: GlobalDescriptorTable = { + let mut gdt = GlobalDescriptorTable::new(); + gdt.add_entry(Descriptor::kernel_code_segment()); + gdt.add_entry(Descriptor::tss_segment(&TSS)); + gdt + }; +} +``` + +Como antes, usamos `lazy_static` novamente. Criamos uma nova GDT com um segmento de código e um segmento TSS. + +#### Carregando a GDT + +Para carregar nossa GDT, criamos uma nova função `gdt::init` que chamamos de nossa função `init`: + +```rust +// em src/gdt.rs + +pub fn init() { + GDT.load(); +} + +// em src/lib.rs + +pub fn init() { + gdt::init(); + interrupts::init_idt(); +} +``` + +Agora nossa GDT está carregada (já que a função `_start` chama `init`), mas ainda vemos o loop de boot no stack overflow. + +### Os Passos Finais + +O problema é que os segmentos GDT ainda não estão ativos porque os registradores de segmento e TSS ainda contêm os valores da GDT antiga. Também precisamos modificar a entrada IDT de double fault para que ela use a nova pilha. + +Em resumo, precisamos fazer o seguinte: + +1. **Recarregar registrador de segmento de código**: Mudamos nossa GDT, então devemos recarregar `cs`, o registrador de segmento de código. Isso é necessário já que o antigo seletor de segmento poderia agora apontar para um descritor GDT diferente (por exemplo, um descritor TSS). +2. **Carregar a TSS**: Carregamos uma GDT que contém um seletor TSS, mas ainda precisamos dizer à CPU que ela deve usar essa TSS. +3. **Atualizar a entrada IDT**: Assim que nossa TSS é carregada, a CPU tem acesso a uma interrupt stack table (IST) válida. Então podemos dizer à CPU que ela deve usar nossa nova pilha de double fault modificando nossa entrada IDT de double fault. + +Para os dois primeiros passos, precisamos de acesso às variáveis `code_selector` e `tss_selector` em nossa função `gdt::init`. Podemos conseguir isso tornando-as parte da static através de uma nova struct `Selectors`: + +```rust +// em src/gdt.rs + +use x86_64::structures::gdt::SegmentSelector; + +lazy_static! { + static ref GDT: (GlobalDescriptorTable, Selectors) = { + let mut gdt = GlobalDescriptorTable::new(); + let code_selector = gdt.add_entry(Descriptor::kernel_code_segment()); + let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS)); + (gdt, Selectors { code_selector, tss_selector }) + }; +} + +struct Selectors { + code_selector: SegmentSelector, + tss_selector: SegmentSelector, +} +``` + +Agora podemos usar os seletores para recarregar o registrador `cs` e carregar nossa `TSS`: + +```rust +// em src/gdt.rs + +pub fn init() { + use x86_64::instructions::tables::load_tss; + use x86_64::instructions::segmentation::{CS, Segment}; + + GDT.0.load(); + unsafe { + CS::set_reg(GDT.1.code_selector); + load_tss(GDT.1.tss_selector); + } +} +``` + +Recarregamos o registrador de segmento de código usando [`CS::set_reg`] e carregamos a TSS usando [`load_tss`]. As funções são marcadas como `unsafe`, então precisamos de um bloco `unsafe` para invocá-las. A razão é que pode ser possível quebrar a segurança de memória carregando seletores inválidos. + +[`CS::set_reg`]: https://docs.rs/x86_64/0.14.5/x86_64/instructions/segmentation/struct.CS.html#method.set_reg +[`load_tss`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tables/fn.load_tss.html + +Agora que carregamos uma TSS e interrupt stack table válidas, podemos definir o índice de pilha para nosso manipulador de double fault na IDT: + +```rust +// em src/interrupts.rs + +use crate::gdt; + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + unsafe { + idt.double_fault.set_handler_fn(double_fault_handler) + .set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // novo + } + + idt + }; +} +``` + +O método `set_stack_index` é unsafe porque o chamador deve garantir que o índice usado é válido e não está já usado para outra exceção. + +É isso! Agora a CPU deve trocar para a pilha de double fault sempre que ocorre um double fault. Assim, somos capazes de capturar _todos_ os double faults, incluindo kernel stack overflows: + +![QEMU printing `EXCEÇÃO: DOUBLE FAULT` and a dump of the exception stack frame](qemu-double-fault-on-stack-overflow.png) + +De agora em diante, nunca devemos ver um triple fault novamente! Para garantir que não quebramos acidentalmente o acima, devemos adicionar um teste para isso. + +## Um Teste de Stack Overflow + +Para testar nosso novo módulo `gdt` e garantir que o manipulador de double fault é corretamente chamado em um stack overflow, podemos adicionar um teste de integração. A ideia é provocar um double fault na função de teste e verificar que o manipulador de double fault é chamado. + +Vamos começar com um esqueleto mínimo: + +```rust +// em tests/stack_overflow.rs + +#![no_std] +#![no_main] + +use core::panic::PanicInfo; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + unimplemented!(); +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +Como nosso teste `panic_handler`, o teste executará [sem um test harness]. A razão é que não podemos continuar a execução após um double fault, então mais de um teste não faz sentido. Para desativar o test harness para o teste, adicionamos o seguinte ao nosso `Cargo.toml`: + +```toml +# em Cargo.toml + +[[test]] +name = "stack_overflow" +harness = false +``` + +[sem um test harness]: @/edition-2/posts/04-testing/index.md#no-harness-tests + +Agora `cargo test --test stack_overflow` deve compilar com sucesso. O teste falha, é claro, já que a macro `unimplemented` entra em panic. + +### Implementando `_start` + +A implementação da função `_start` se parece com isto: + +```rust +// em tests/stack_overflow.rs + +use blog_os::serial_print; + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + serial_print!("stack_overflow::stack_overflow...\t"); + + blog_os::gdt::init(); + init_test_idt(); + + // dispara um stack overflow + stack_overflow(); + + panic!("Execução continuou após stack overflow"); +} + +#[allow(unconditional_recursion)] +fn stack_overflow() { + stack_overflow(); // para cada recursão, o endereço de retorno é empurrado + volatile::Volatile::new(0).read(); // previne otimizações de tail recursion +} +``` + +Chamamos nossa função `gdt::init` para inicializar uma nova GDT. Em vez de chamar nossa função `interrupts::init_idt`, chamamos uma função `init_test_idt` que será explicada em um momento. A razão é que queremos registrar um manipulador de double fault customizado que faz um `exit_qemu(QemuExitCode::Success)` em vez de entrar em panic. + +A função `stack_overflow` é quase idêntica à função em nosso `main.rs`. A única diferença é que no final da função, realizamos uma leitura [volátil] adicional usando o tipo [`Volatile`] para prevenir uma otimização do compilador chamada [_tail call elimination_]. Entre outras coisas, esta otimização permite ao compilador transformar uma função cuja última instrução é uma chamada de função recursiva em um loop normal. Assim, nenhum stack frame adicional é criado para a chamada de função, então o uso de pilha permanece constante. + +[volátil]: https://en.wikipedia.org/wiki/Volatile_(computer_programming) +[`Volatile`]: https://docs.rs/volatile/0.2.6/volatile/struct.Volatile.html +[_tail call elimination_]: https://en.wikipedia.org/wiki/Tail_call + +No nosso caso, no entanto, queremos que o stack overflow aconteça, então adicionamos uma instrução de leitura volátil fictícia no final da função, que o compilador não tem permissão para remover. Assim, a função não é mais _tail recursive_, e a transformação em um loop é prevenida. Também adicionamos o atributo `allow(unconditional_recursion)` para silenciar o aviso do compilador de que a função recursa infinitamente. + +### A IDT de Teste + +Como notado acima, o teste precisa de sua própria IDT com um manipulador de double fault customizado. A implementação se parece com isto: + +```rust +// em tests/stack_overflow.rs + +use lazy_static::lazy_static; +use x86_64::structures::idt::InterruptDescriptorTable; + +lazy_static! { + static ref TEST_IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + unsafe { + idt.double_fault + .set_handler_fn(test_double_fault_handler) + .set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX); + } + + idt + }; +} + +pub fn init_test_idt() { + TEST_IDT.load(); +} +``` + +A implementação é muito similar à nossa IDT normal em `interrupts.rs`. Como na IDT normal, definimos um índice de pilha na IST para o manipulador de double fault para trocar para uma pilha separada. A função `init_test_idt` carrega a IDT na CPU através do método `load`. + +### O Manipulador de Double Fault + +A única peça que falta é nosso manipulador de double fault. Ele se parece com isto: + +```rust +// em tests/stack_overflow.rs + +use blog_os::{exit_qemu, QemuExitCode, serial_println}; +use x86_64::structures::idt::InterruptStackFrame; + +extern "x86-interrupt" fn test_double_fault_handler( + _stack_frame: InterruptStackFrame, + _error_code: u64, +) -> ! { + serial_println!("[ok]"); + exit_qemu(QemuExitCode::Success); + loop {} +} +``` + +Quando o manipulador de double fault é chamado, saímos do QEMU com um código de saída de sucesso, o que marca o teste como aprovado. Como testes de integração são executáveis completamente separados, precisamos definir o atributo `#![feature(abi_x86_interrupt)]` novamente no topo do nosso arquivo de teste. + +Agora podemos executar nosso teste através de `cargo test --test stack_overflow` (ou `cargo test` para executar todos os testes). Como esperado, vemos a saída `stack_overflow... [ok]` no console. Tente comentar a linha `set_stack_index`; isso deve fazer o teste falhar. + +## Resumo +Nesta postagem, aprendemos o que é um double fault e sob quais condições ele ocorre. Adicionamos um manipulador de double fault básico que imprime uma mensagem de erro e adicionamos um teste de integração para ele. + +Também habilitamos a troca de pilha suportada por hardware em exceções de double fault para que também funcione em stack overflow. Enquanto implementávamos isso, aprendemos sobre o segmento de estado de tarefa (TSS), a interrupt stack table (IST) contida nele, e a tabela de descritores globais (GDT), que foi usada para segmentação em arquiteturas mais antigas. + +## O Que Vem a Seguir? +A próxima postagem explica como manipular interrupções de dispositivos externos como temporizadores, teclados ou controladores de rede. Essas interrupções de hardware são muito similares a exceções, por exemplo, elas também são despachadas através da IDT. No entanto, ao contrário de exceções, elas não surgem diretamente na CPU. Em vez disso, um _controlador de interrupção_ agrega essas interrupções e as encaminha para a CPU dependendo de sua prioridade. Na próxima postagem, exploraremos o controlador de interrupções [Intel 8259] \("PIC") e aprenderemos como implementar suporte a teclado. + +[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259 \ No newline at end of file diff --git a/blog/content/edition-2/posts/07-hardware-interrupts/index.pt-BR.md b/blog/content/edition-2/posts/07-hardware-interrupts/index.pt-BR.md new file mode 100644 index 00000000..69377eb5 --- /dev/null +++ b/blog/content/edition-2/posts/07-hardware-interrupts/index.pt-BR.md @@ -0,0 +1,740 @@ ++++ +title = "Interrupções de Hardware" +weight = 7 +path = "pt-BR/hardware-interrupts" +date = 2018-10-22 + +[extra] +chapter = "Interrupções" +# Please update this when updating the translation +translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0" + +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Nesta postagem, configuramos o controlador de interrupção programável para encaminhar corretamente interrupções de hardware para a CPU. Para manipular essas interrupções, adicionamos novas entradas à nossa tabela de descritores de interrupção, assim como fizemos para nossos manipuladores de exceção. Aprenderemos como obter interrupções periódicas de timer e como obter entrada do teclado. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-07`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-07 + + + +## Visão Geral + +Interrupções fornecem uma forma de notificar a CPU de dispositivos de hardware conectados. Então, em vez de deixar o kernel verificar periodicamente o teclado por novos caracteres (um processo chamado [_polling_]), o teclado pode notificar o kernel de cada pressionamento de tecla. Isso é muito mais eficiente porque o kernel só precisa agir quando algo aconteceu. Também permite tempos de reação mais rápidos, já que o kernel pode reagir imediatamente e não apenas na próxima verificação. + +[_polling_]: https://en.wikipedia.org/wiki/Polling_(computer_science) + +Conectar todos os dispositivos de hardware diretamente à CPU não é possível. Em vez disso, um _controlador de interrupção_ separado agrega as interrupções de todos os dispositivos e então notifica a CPU: + +``` + ____________ _____ + Timer ------------> | | | | + Teclado ----------> | Controlador|---------> | CPU | + Outro Hardware ---> | de | |_____| + Etc. -------------> | Interrupção| + |____________| + +``` + +A maioria dos controladores de interrupção são programáveis, o que significa que suportam diferentes níveis de prioridade para interrupções. Por exemplo, isso permite dar às interrupções de timer uma prioridade mais alta que as interrupções de teclado para garantir cronometragem precisa. + +Ao contrário de exceções, interrupções de hardware ocorrem _assincronamente_. Isso significa que são completamente independentes do código executado e podem ocorrer a qualquer momento. Assim, temos repentinamente uma forma de concorrência em nosso kernel com todos os potenciais bugs relacionados à concorrência. O modelo estrito de ownership de Rust nos ajuda aqui porque proíbe estado global mutável. No entanto, deadlocks ainda são possíveis, como veremos mais tarde nesta postagem. + +## O 8259 PIC + +O [Intel 8259] é um controlador de interrupção programável (PIC) introduzido em 1976. Ele foi há muito tempo substituído pelo mais novo [APIC], mas sua interface ainda é suportada em sistemas atuais por razões de compatibilidade retroativa. O 8259 PIC é significativamente mais fácil de configurar que o APIC, então o usaremos para nos introduzir a interrupções antes de mudarmos para o APIC em uma postagem posterior. + +[APIC]: https://en.wikipedia.org/wiki/Intel_APIC_Architecture + +O 8259 tem oito linhas de interrupção e várias linhas para se comunicar com a CPU. Os sistemas típicos daquela época eram equipados com duas instâncias do 8259 PIC, um PIC primário e um secundário, conectado a uma das linhas de interrupção do primário: + +[Intel 8259]: https://en.wikipedia.org/wiki/Intel_8259 + +``` + ____________ ____________ +Real Time Clock --> | | Timer -------------> | | +ACPI -------------> | | Teclado-----------> | | _____ +Disponível -------> | Controlador|----------------------> | Controlador| | | +Disponível -------> | de | Porta Serial 2 ----> | de |---> | CPU | +Mouse ------------> | Interrupção| Porta Serial 1 ----> | Interrupção| |_____| +Co-Processador ---> | Secundário | Porta Paralela 2/3 > | Primário | +ATA Primário -----> | | Disquete ---------> | | +ATA Secundário ---> |____________| Porta Paralela 1---> |____________| + +``` + +Este gráfico mostra a atribuição típica de linhas de interrupção. Vemos que a maioria das 15 linhas têm um mapeamento fixo, por exemplo, a linha 4 do PIC secundário é atribuída ao mouse. + +Cada controlador pode ser configurado através de duas [portas I/O], uma porta "comando" e uma porta "dados". Para o controlador primário, essas portas são `0x20` (comando) e `0x21` (dados). Para o controlador secundário, elas são `0xa0` (comando) e `0xa1` (dados). Para mais informações sobre como os PICs podem ser configurados, veja o [artigo em osdev.org]. + +[portas I/O]: @/edition-2/posts/04-testing/index.md#i-o-ports +[artigo em osdev.org]: https://wiki.osdev.org/8259_PIC + +### Implementação + +A configuração padrão dos PICs não é utilizável porque envia números de vetor de interrupção no intervalo de 0–15 para a CPU. Esses números já estão ocupados por exceções de CPU. Por exemplo, o número 8 corresponde a um double fault. Para corrigir esse problema de sobreposição, precisamos remapear as interrupções PIC para números diferentes. O intervalo real não importa desde que não se sobreponha às exceções, mas tipicamente o intervalo de 32–47 é escolhido, porque esses são os primeiros números livres após os 32 slots de exceção. + +A configuração acontece escrevendo valores especiais nas portas de comando e dados dos PICs. Felizmente, já existe uma crate chamada [`pic8259`], então não precisamos escrever a sequência de inicialização nós mesmos. No entanto, se você estiver interessado em como funciona, confira [seu código-fonte][pic crate source]. Ele é bastante pequeno e bem documentado. + +[pic crate source]: https://docs.rs/crate/pic8259/0.10.1/source/src/lib.rs + +Para adicionar a crate como dependência, adicionamos o seguinte ao nosso projeto: + +[`pic8259`]: https://docs.rs/pic8259/0.10.1/pic8259/ + +```toml +# em Cargo.toml + +[dependencies] +pic8259 = "0.10.1" +``` + +A principal abstração fornecida pela crate é a struct [`ChainedPics`] que representa o layout primário/secundário de PIC que vimos acima. Ela é projetada para ser usada da seguinte forma: + +[`ChainedPics`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html + +```rust +// em src/interrupts.rs + +use pic8259::ChainedPics; +use spin; + +pub const PIC_1_OFFSET: u8 = 32; +pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8; + +pub static PICS: spin::Mutex = + spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) }); +``` + +Como notado acima, estamos definindo os offsets para os PICs no intervalo 32–47. Ao envolver a struct `ChainedPics` em um `Mutex`, obtemos acesso mutável seguro (através do [método `lock`][spin mutex lock]), que precisamos no próximo passo. A função `ChainedPics::new` é unsafe porque offsets errados poderiam causar comportamento indefinido. + +[spin mutex lock]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html#method.lock + +Agora podemos inicializar o 8259 PIC em nossa função `init`: + +```rust +// em src/lib.rs + +pub fn init() { + gdt::init(); + interrupts::init_idt(); + unsafe { interrupts::PICS.lock().initialize() }; // novo +} +``` + +Usamos a função [`initialize`] para realizar a inicialização do PIC. Como a função `ChainedPics::new`, esta função também é unsafe porque pode causar comportamento indefinido se o PIC estiver mal configurado. + +[`initialize`]: https://docs.rs/pic8259/0.10.1/pic8259/struct.ChainedPics.html#method.initialize + +Se tudo correr bem, devemos continuar a ver a mensagem "Não crashou!" ao executar `cargo run`. + +## Habilitando Interrupções + +Até agora, nada aconteceu porque as interrupções ainda estão desativadas na configuração da CPU. Isso significa que a CPU não escuta o controlador de interrupção de forma alguma, então nenhuma interrupção pode chegar à CPU. Vamos mudar isso: + +```rust +// em src/lib.rs + +pub fn init() { + gdt::init(); + interrupts::init_idt(); + unsafe { interrupts::PICS.lock().initialize() }; + x86_64::instructions::interrupts::enable(); // novo +} +``` + +A função `interrupts::enable` da crate `x86_64` executa a instrução especial `sti` ("set interrupts") para habilitar interrupções externas. Quando tentamos `cargo run` agora, vemos que ocorre um double fault: + +![QEMU printing `EXCEÇÃO: DOUBLE FAULT` because of hardware timer](qemu-hardware-timer-double-fault.png) + +A razão para este double fault é que o timer de hardware (o [Intel 8253], para ser exato) é habilitado por padrão, então começamos a receber interrupções de timer assim que habilitamos interrupções. Como ainda não definimos uma função manipuladora para ele, nosso manipulador de double fault é invocado. + +[Intel 8253]: https://en.wikipedia.org/wiki/Intel_8253 + +## Manipulando Interrupções de Timer + +Como vemos do gráfico [acima](#o-8259-pic), o timer usa a linha 0 do PIC primário. Isso significa que ele chega à CPU como interrupção 32 (0 + offset 32). Em vez de codificar rigidamente o índice 32, o armazenamos em um enum `InterruptIndex`: + +```rust +// em src/interrupts.rs + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum InterruptIndex { + Timer = PIC_1_OFFSET, +} + +impl InterruptIndex { + fn as_u8(self) -> u8 { + self as u8 + } + + fn as_usize(self) -> usize { + usize::from(self.as_u8()) + } +} +``` + +O enum é um [enum similar a C] para que possamos especificar diretamente o índice para cada variante. O atributo `repr(u8)` especifica que cada variante é representada como um `u8`. Adicionaremos mais variantes para outras interrupções no futuro. + +[enum similar a C]: https://doc.rust-lang.org/reference/items/enumerations.html#custom-discriminant-values-for-fieldless-enumerations + +Agora podemos adicionar uma função manipuladora para a interrupção de timer: + +```rust +// em src/interrupts.rs + +use crate::print; + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + […] + idt[InterruptIndex::Timer.as_usize()] + .set_handler_fn(timer_interrupt_handler); // novo + + idt + }; +} + +extern "x86-interrupt" fn timer_interrupt_handler( + _stack_frame: InterruptStackFrame) +{ + print!("."); +} +``` + +Nosso `timer_interrupt_handler` tem a mesma assinatura que nossos manipuladores de exceção, porque a CPU reage identicamente a exceções e interrupções externas (a única diferença é que algumas exceções empurram um código de erro). A struct [`InterruptDescriptorTable`] implementa a trait [`IndexMut`], então podemos acessar entradas individuais através da sintaxe de indexação de array. + +[`InterruptDescriptorTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html +[`IndexMut`]: https://doc.rust-lang.org/core/ops/trait.IndexMut.html + +Em nosso manipulador de interrupção de timer, imprimimos um ponto na tela. Como a interrupção de timer acontece periodicamente, esperaríamos ver um ponto aparecendo a cada tick do timer. No entanto, quando o executamos, vemos que apenas um único ponto é impresso: + +![QEMU printing only a single dot for hardware timer](qemu-single-dot-printed.png) + +### End of Interrupt + +A razão é que o PIC espera um sinal explícito de "end of interrupt" (EOI) do nosso manipulador de interrupção. Este sinal diz ao controlador que a interrupção foi processada e que o sistema está pronto para receber a próxima interrupção. Então o PIC pensa que ainda estamos ocupados processando a primeira interrupção de timer e espera pacientemente pelo sinal EOI antes de enviar a próxima. + +Para enviar o EOI, usamos nossa struct `PICS` estática novamente: + +```rust +// em src/interrupts.rs + +extern "x86-interrupt" fn timer_interrupt_handler( + _stack_frame: InterruptStackFrame) +{ + print!("."); + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Timer.as_u8()); + } +} +``` + +O `notify_end_of_interrupt` descobre se o PIC primário ou secundário enviou a interrupção e então usa as portas `command` e `data` para enviar um sinal EOI aos respectivos controladores. Se o PIC secundário enviou a interrupção, ambos os PICs precisam ser notificados porque o PIC secundário está conectado a uma linha de entrada do PIC primário. + +Precisamos ter cuidado para usar o número de vetor de interrupção correto, caso contrário poderíamos acidentalmente deletar uma importante interrupção não enviada ou fazer nosso sistema travar. Esta é a razão pela qual a função é unsafe. + +Quando agora executamos `cargo run` vemos pontos aparecendo periodicamente na tela: + +![QEMU printing consecutive dots showing the hardware timer](qemu-hardware-timer-dots.gif) + +### Configurando o Timer + +O timer de hardware que usamos é chamado de _Programmable Interval Timer_, ou PIT, resumidamente. Como o nome diz, é possível configurar o intervalo entre duas interrupções. Não entraremos em detalhes aqui porque mudaremos em breve para o [APIC timer], mas a wiki do OSDev tem um artigo extenso sobre [configurando o PIT]. + +[APIC timer]: https://wiki.osdev.org/APIC_timer +[configurando o PIT]: https://wiki.osdev.org/Programmable_Interval_Timer + +## Deadlocks + +Agora temos uma forma de concorrência em nosso kernel: As interrupções de timer ocorrem assincronamente, então podem interromper nossa função `_start` a qualquer momento. Felizmente, o sistema de ownership de Rust previne muitos tipos de bugs relacionados à concorrência em tempo de compilação. Uma exceção notável são deadlocks. Deadlocks ocorrem se uma thread tenta adquirir um lock que nunca se tornará livre. Assim, a thread trava indefinidamente. + +Já podemos provocar um deadlock em nosso kernel. Lembre-se, nossa macro `println` chama a função `vga_buffer::_print`, que [trava um `WRITER` global][vga spinlock] usando um spinlock: + +[vga spinlock]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks + +```rust +// em src/vga_buffer.rs + +[…] + +#[doc(hidden)] +pub fn _print(args: fmt::Arguments) { + use core::fmt::Write; + WRITER.lock().write_fmt(args).unwrap(); +} +``` + +Ela trava o `WRITER`, chama `write_fmt` nele, e implicitamente o destrava no final da função. Agora imagine que uma interrupção ocorre enquanto o `WRITER` está travado e o manipulador de interrupção tenta imprimir algo também: + +Passo de Tempo | _start | interrupt_handler +---------|------|------------------ +0 | chama `println!` |   +1 | `print` trava `WRITER` |   +2 | | **interrupção ocorre**, manipulador começa a executar +3 | | chama `println!` | +4 | | `print` tenta travar `WRITER` (já travado) +5 | | `print` tenta travar `WRITER` (já travado) +… | | … +_nunca_ | _destravar `WRITER`_ | + +O `WRITER` está travado, então o manipulador de interrupção espera até que se torne livre. Mas isso nunca acontece, porque a função `_start` só continua a executar após o manipulador de interrupção retornar. Assim, o sistema inteiro trava. + +### Provocando um Deadlock + +Podemos facilmente provocar tal deadlock em nosso kernel imprimindo algo no loop no final de nossa função `_start`: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + […] + loop { + use blog_os::print; + print!("-"); // novo + } +} +``` + +Quando o executamos no QEMU, obtemos uma saída da forma: + +![QEMU output with many rows of hyphens and no dots](./qemu-deadlock.png) + +Vemos que apenas um número limitado de hífens são impressos até que a primeira interrupção de timer ocorre. Então o sistema trava porque o manipulador de interrupção de timer entra em deadlock quando tenta imprimir um ponto. Esta é a razão pela qual não vemos pontos na saída acima. + +O número real de hífens varia entre execuções porque a interrupção de timer ocorre assincronamente. Este não-determinismo é o que torna bugs relacionados à concorrência tão difíceis de depurar. + +### Corrigindo o Deadlock + +Para evitar este deadlock, podemos desativar interrupções enquanto o `Mutex` está travado: + +```rust +// em src/vga_buffer.rs + +/// Imprime a string formatada dada no buffer de texto VGA +/// através da instância global `WRITER`. +#[doc(hidden)] +pub fn _print(args: fmt::Arguments) { + use core::fmt::Write; + use x86_64::instructions::interrupts; // novo + + interrupts::without_interrupts(|| { // novo + WRITER.lock().write_fmt(args).unwrap(); + }); +} +``` + +A função [`without_interrupts`] recebe um [closure] e o executa em um ambiente livre de interrupções. Usamos isso para garantir que nenhuma interrupção pode ocorrer enquanto o `Mutex` está travado. Quando executamos nosso kernel agora, vemos que ele continua executando sem travar. (Ainda não notamos nenhum ponto, mas isso é porque eles estão rolando rápido demais. Tente diminuir a velocidade da impressão, por exemplo, colocando um `for _ in 0..10000 {}` dentro do loop.) + +[`without_interrupts`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.without_interrupts.html +[closure]: https://doc.rust-lang.org/book/ch13-01-closures.html + +Podemos aplicar a mesma mudança à nossa função de impressão serial para garantir que nenhum deadlock ocorra com ela também: + +```rust +// em src/serial.rs + +#[doc(hidden)] +pub fn _print(args: ::core::fmt::Arguments) { + use core::fmt::Write; + use x86_64::instructions::interrupts; // novo + + interrupts::without_interrupts(|| { // novo + SERIAL1 + .lock() + .write_fmt(args) + .expect("Impressão para serial falhou"); + }); +} +``` + +Note que desativar interrupções não deve ser uma solução geral. O problema é que isso aumenta a latência de interrupção no pior caso, isto é, o tempo até o sistema reagir a uma interrupção. Portanto, interrupções devem ser desativadas apenas por um tempo muito curto. + +## Corrigindo uma Race Condition + +Se você executar `cargo test`, pode ver o teste `test_println_output` falhar: + +``` +> cargo test --lib +[…] +Running 4 tests +test_breakpoint_exception...[ok] +test_println... [ok] +test_println_many... [ok] +test_println_output... [failed] + +Error: panicked at 'assertion failed: `(left == right)` + left: `'.'`, + right: `'S'`', src/vga_buffer.rs:205:9 +``` + +A razão é uma _race condition_ entre o teste e nosso manipulador de timer. Lembre-se, o teste se parece com isto: + +```rust +// em src/vga_buffer.rs + +#[test_case] +fn test_println_output() { + let s = "Uma string de teste que cabe em uma única linha"; + println!("{}", s); + for (i, c) in s.chars().enumerate() { + let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } +} +``` + +O teste imprime uma string no buffer VGA e então verifica a saída iterando manualmente pelo array `buffer_chars`. A race condition ocorre porque o manipulador de interrupção de timer pode executar entre o `println` e a leitura dos caracteres de tela. Note que isso não é uma _data race_ perigosa, que Rust previne completamente em tempo de compilação. Veja o [_Rustonomicon_][nomicon-races] para detalhes. + +[nomicon-races]: https://doc.rust-lang.org/nomicon/races.html + +Para corrigir isso, precisamos manter o `WRITER` travado pela duração completa do teste, para que o manipulador de timer não possa escrever um `.` na tela no meio. O teste corrigido se parece com isto: + +```rust +// em src/vga_buffer.rs + +#[test_case] +fn test_println_output() { + use core::fmt::Write; + use x86_64::instructions::interrupts; + + let s = "Uma string de teste que cabe em uma única linha"; + interrupts::without_interrupts(|| { + let mut writer = WRITER.lock(); + writeln!(writer, "\n{}", s).expect("writeln falhou"); + for (i, c) in s.chars().enumerate() { + let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read(); + assert_eq!(char::from(screen_char.ascii_character), c); + } + }); +} +``` + +Realizamos as seguintes mudanças: + +- Mantemos o writer travado pelo teste completo usando o método `lock()` explicitamente. Em vez de `println`, usamos a macro [`writeln`] que permite imprimir em um writer já travado. +- Para evitar outro deadlock, desativamos interrupções pela duração do teste. Caso contrário, o teste poderia ser interrompido enquanto o writer ainda está travado. +- Como o manipulador de interrupção de timer ainda pode executar antes do teste, imprimimos uma nova linha adicional `\n` antes de imprimir a string `s`. Desta forma, evitamos falha do teste quando o manipulador de timer já imprimiu alguns caracteres `.` na linha atual. + +[`writeln`]: https://doc.rust-lang.org/core/macro.writeln.html + +Com as mudanças acima, `cargo test` agora tem sucesso deterministicamente novamente. + +Esta foi uma race condition muito inofensiva que causou apenas uma falha de teste. Como você pode imaginar, outras race conditions podem ser muito mais difíceis de depurar devido à sua natureza não-determinística. Felizmente, Rust nos previne de data races, que são a classe mais séria de race conditions, já que podem causar todo tipo de comportamento indefinido, incluindo crashes de sistema e corrupções silenciosas de memória. + +## A Instrução `hlt` + +Até agora, usamos uma simples instrução de loop vazio no final de nossas funções `_start` e `panic`. Isso faz a CPU girar infinitamente, e assim funciona como esperado. Mas também é muito ineficiente, porque a CPU continua executando a velocidade máxima mesmo que não haja trabalho a fazer. Você pode ver este problema em seu gerenciador de tarefas quando executa seu kernel: O processo QEMU precisa de perto de 100% de CPU o tempo todo. + +O que realmente queremos fazer é parar a CPU até a próxima interrupção chegar. Isso permite que a CPU entre em um estado de sono no qual consome muito menos energia. A [instrução `hlt`] faz exatamente isso. Vamos usar esta instrução para criar um loop infinito eficiente em energia: + +[instrução `hlt`]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) + +```rust +// em src/lib.rs + +pub fn hlt_loop() -> ! { + loop { + x86_64::instructions::hlt(); + } +} +``` + +A função `instructions::hlt` é apenas um [wrapper fino] em torno da instrução assembly. Ela é segura porque não há forma de comprometer a segurança de memória. + +[wrapper fino]: https://github.com/rust-osdev/x86_64/blob/5e8e218381c5205f5777cb50da3ecac5d7e3b1ab/src/instructions/mod.rs#L16-L22 + +Agora podemos usar este `hlt_loop` em vez dos loops infinitos em nossas funções `_start` e `panic`: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + […] + + println!("Não crashou!"); + blog_os::hlt_loop(); // novo +} + + +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{}", info); + blog_os::hlt_loop(); // novo +} + +``` + +Vamos atualizar nosso `lib.rs` também: + +```rust +// em src/lib.rs + +/// Ponto de entrada para `cargo test` +#[cfg(test)] +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + init(); + test_main(); + hlt_loop(); // novo +} + +pub fn test_panic_handler(info: &PanicInfo) -> ! { + serial_println!("[failed]\n"); + serial_println!("Error: {}\n", info); + exit_qemu(QemuExitCode::Failed); + hlt_loop(); // novo +} +``` + +Quando executamos nosso kernel agora no QEMU, vemos um uso de CPU muito menor. + +## Entrada de Teclado + +Agora que somos capazes de manipular interrupções de dispositivos externos, finalmente podemos adicionar suporte para entrada de teclado. Isso nos permitirá interagir com nosso kernel pela primeira vez. + + + +[PS/2]: https://en.wikipedia.org/wiki/PS/2_port + +Como o timer de hardware, o controlador de teclado já está habilitado por padrão. Então quando você pressiona uma tecla, o controlador de teclado envia uma interrupção para o PIC, que a encaminha para a CPU. A CPU procura por uma função manipuladora na IDT, mas a entrada correspondente está vazia. Portanto, ocorre um double fault. + +Então vamos adicionar uma função manipuladora para a interrupção de teclado. É bem similar a como definimos o manipulador para a interrupção de timer; apenas usa um número de interrupção diferente: + +```rust +// em src/interrupts.rs + +#[derive(Debug, Clone, Copy)] +#[repr(u8)] +pub enum InterruptIndex { + Timer = PIC_1_OFFSET, + Keyboard, // novo +} + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + idt.breakpoint.set_handler_fn(breakpoint_handler); + […] + // novo + idt[InterruptIndex::Keyboard.as_usize()] + .set_handler_fn(keyboard_interrupt_handler); + + idt + }; +} + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: InterruptStackFrame) +{ + print!("k"); + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +Como vemos do gráfico [acima](#o-8259-pic), o teclado usa a linha 1 do PIC primário. Isso significa que ele chega à CPU como interrupção 33 (1 + offset 32). Adicionamos este índice como uma nova variante `Keyboard` ao enum `InterruptIndex`. Não precisamos especificar o valor explicitamente, já que ele assume o valor anterior mais um por padrão, que também é 33. No manipulador de interrupção, imprimimos um `k` e enviamos o sinal end of interrupt para o controlador de interrupção. + +Agora vemos que um `k` aparece na tela quando pressionamos uma tecla. No entanto, isso só funciona para a primeira tecla que pressionamos. Mesmo se continuarmos a pressionar teclas, nenhum `k` adicional aparece na tela. Isso ocorre porque o controlador de teclado não enviará outra interrupção até lermos o chamado _scancode_ da tecla pressionada. + +### Lendo os Scancodes + +Para descobrir _qual_ tecla foi pressionada, precisamos consultar o controlador de teclado. Fazemos isso lendo da porta de dados do controlador PS/2, que é a [porta I/O] com o número `0x60`: + +[porta I/O]: @/edition-2/posts/04-testing/index.md#i-o-ports + +```rust +// em src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: InterruptStackFrame) +{ + use x86_64::instructions::port::Port; + + let mut port = Port::new(0x60); + let scancode: u8 = unsafe { port.read() }; + print!("{}", scancode); + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +Usamos o tipo [`Port`] da crate `x86_64` para ler um byte da porta de dados do teclado. Este byte é chamado de [_scancode_] e representa o pressionamento/liberação de tecla. Ainda não fazemos nada com o scancode, apenas o imprimimos na tela: + +[`Port`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/port/struct.Port.html +[_scancode_]: https://en.wikipedia.org/wiki/Scancode + +![QEMU printing scancodes to the screen when keys are pressed](qemu-printing-scancodes.gif) + +A imagem acima me mostra digitando lentamente "123". Vemos que teclas adjacentes têm scancodes adjacentes e que pressionar uma tecla causa um scancode diferente de liberá-la. Mas como traduzimos exatamente os scancodes para as ações reais de tecla? + +### Interpretando os Scancodes +Existem três padrões diferentes para o mapeamento entre scancodes e teclas, os chamados _conjuntos de scancode_. Todos os três remontam aos teclados de computadores IBM antigos: o [IBM XT], o [IBM 3270 PC], e o [IBM AT]. Felizmente, computadores posteriores não continuaram a tendência de definir novos conjuntos de scancode, mas em vez disso emularam os conjuntos existentes e os estenderam. Hoje, a maioria dos teclados pode ser configurada para emular qualquer um dos três conjuntos. + +[IBM XT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer_XT +[IBM 3270 PC]: https://en.wikipedia.org/wiki/IBM_3270_PC +[IBM AT]: https://en.wikipedia.org/wiki/IBM_Personal_Computer/AT + +Por padrão, teclados PS/2 emulam o conjunto de scancode 1 ("XT"). Neste conjunto, os 7 bits inferiores de um byte de scancode definem a tecla, e o bit mais significativo define se é um pressionamento ("0") ou uma liberação ("1"). Teclas que não estavam presentes no [IBM XT] original, como a tecla enter no teclado numérico, geram dois scancodes em sucessão: um byte de escape `0xe0` e então um byte representando a tecla. Para uma lista de todos os scancodes do conjunto 1 e suas teclas correspondentes, confira a [Wiki OSDev][scancode set 1]. + +[scancode set 1]: https://wiki.osdev.org/Keyboard#Scan_Code_Set_1 + +Para traduzir os scancodes para teclas, podemos usar uma instrução `match`: + +```rust +// em src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: InterruptStackFrame) +{ + use x86_64::instructions::port::Port; + + let mut port = Port::new(0x60); + let scancode: u8 = unsafe { port.read() }; + + // novo + let key = match scancode { + 0x02 => Some('1'), + 0x03 => Some('2'), + 0x04 => Some('3'), + 0x05 => Some('4'), + 0x06 => Some('5'), + 0x07 => Some('6'), + 0x08 => Some('7'), + 0x09 => Some('8'), + 0x0a => Some('9'), + 0x0b => Some('0'), + _ => None, + }; + if let Some(key) = key { + print!("{}", key); + } + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +O código acima traduz pressionamentos das teclas numéricas 0-9 e ignora todas as outras teclas. Ele usa uma instrução [match] para atribuir um caractere ou `None` a cada scancode. Então usa [`if let`] para desestruturar o `key` opcional. Ao usar o mesmo nome de variável `key` no padrão, [sombreamos] a declaração anterior, que é um padrão comum para desestruturar tipos `Option` em Rust. + +[match]: https://doc.rust-lang.org/book/ch06-02-match.html +[`if let`]: https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#conditional-if-let-expressions +[sombreamos]: https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing + +Agora podemos escrever números: + +![QEMU printing numbers to the screen](qemu-printing-numbers.gif) + +Traduzir as outras teclas funciona da mesma forma. Felizmente, existe uma crate chamada [`pc-keyboard`] para traduzir scancodes dos conjuntos de scancode 1 e 2, então não precisamos implementar isso nós mesmos. Para usar a crate, a adicionamos ao nosso `Cargo.toml` e a importamos em nosso `lib.rs`: + +[`pc-keyboard`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/ + +```toml +# em Cargo.toml + +[dependencies] +pc-keyboard = "0.7.0" +``` + +Agora podemos usar esta crate para reescrever nosso `keyboard_interrupt_handler`: + +```rust +// em/src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: InterruptStackFrame) +{ + use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; + use spin::Mutex; + use x86_64::instructions::port::Port; + + lazy_static! { + static ref KEYBOARD: Mutex> = + Mutex::new(Keyboard::new(ScancodeSet1::new(), + layouts::Us104Key, HandleControl::Ignore) + ); + } + + let mut keyboard = KEYBOARD.lock(); + let mut port = Port::new(0x60); + + let scancode: u8 = unsafe { port.read() }; + if let Ok(Some(key_event)) = keyboard.add_byte(scancode) { + if let Some(key) = keyboard.process_keyevent(key_event) { + match key { + DecodedKey::Unicode(character) => print!("{}", character), + DecodedKey::RawKey(key) => print!("{:?}", key), + } + } + } + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +Usamos a macro `lazy_static` para criar um objeto [`Keyboard`] estático protegido por um Mutex. Inicializamos o `Keyboard` com um layout de teclado americano e o conjunto de scancode 1. O parâmetro [`HandleControl`] permite mapear `ctrl+[a-z]` aos caracteres Unicode `U+0001` através de `U+001A`. Não queremos fazer isso, então usamos a opção `Ignore` para manipular o `ctrl` como teclas normais. + +[`HandleControl`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/enum.HandleControl.html + +Em cada interrupção, travamos o Mutex, lemos o scancode do controlador de teclado, e o passamos para o método [`add_byte`], que traduz o scancode em um `Option`. O [`KeyEvent`] contém a tecla que causou o evento e se foi um evento de pressionamento ou liberação. + +[`Keyboard`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.Keyboard.html +[`add_byte`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.Keyboard.html#method.add_byte +[`KeyEvent`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.KeyEvent.html + +Para interpretar este evento de tecla, o passamos para o método [`process_keyevent`], que traduz o evento de tecla em um caractere, se possível. Por exemplo, ele traduz um evento de pressionamento da tecla `A` em um caractere `a` minúsculo ou um caractere `A` maiúsculo, dependendo se a tecla shift foi pressionada. + +[`process_keyevent`]: https://docs.rs/pc-keyboard/0.7.0/pc_keyboard/struct.Keyboard.html#method.process_keyevent + +Com este manipulador de interrupção modificado, agora podemos escrever texto: + +![Typing "Hello World" in QEMU](qemu-typing.gif) + +### Configurando o Teclado + +É possível configurar alguns aspectos de um teclado PS/2, por exemplo, qual conjunto de scancode ele deve usar. Não cobriremos isso aqui porque esta postagem já está longa o suficiente, mas a Wiki do OSDev tem uma visão geral dos possíveis [comandos de configuração]. + +[comandos de configuração]: https://wiki.osdev.org/PS/2_Keyboard#Commands + +## Resumo + +Esta postagem explicou como habilitar e manipular interrupções externas. Aprendemos sobre o 8259 PIC e seu layout primário/secundário, o remapeamento dos números de interrupção, e o sinal "end of interrupt". Implementamos manipuladores para o timer de hardware e o teclado e aprendemos sobre a instrução `hlt`, que para a CPU até a próxima interrupção. + +Agora somos capazes de interagir com nosso kernel e temos alguns blocos fundamentais para criar um pequeno shell ou jogos simples. + +## O Que Vem a Seguir? + +Interrupções de timer são essenciais para um sistema operacional porque fornecem uma forma de interromper periodicamente o processo em execução e deixar o kernel retomar o controle. O kernel pode então mudar para um processo diferente e criar a ilusão de múltiplos processos executando em paralelo. + +Mas antes de podermos criar processos ou threads, precisamos de uma forma de alocar memória para eles. As próximas postagens explorarão gerenciamento de memória para fornecer este bloco fundamental. \ No newline at end of file diff --git a/blog/content/edition-2/posts/08-paging-introduction/index.pt-BR.md b/blog/content/edition-2/posts/08-paging-introduction/index.pt-BR.md new file mode 100644 index 00000000..7253681a --- /dev/null +++ b/blog/content/edition-2/posts/08-paging-introduction/index.pt-BR.md @@ -0,0 +1,419 @@ ++++ +title = "Introdução à Paginação" +weight = 8 +path = "pt-BR/paging-introduction" +date = 2019-01-14 + +[extra] +chapter = "Gerenciamento de Memória" +# Please update this when updating the translation +translation_based_on_commit = "9753695744854686a6b80012c89b0d850a44b4b0" + +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Esta postagem introduz _paginação_, um esquema de gerenciamento de memória muito comum que também usaremos para nosso sistema operacional. Ela explica por que o isolamento de memória é necessário, como _segmentação_ funciona, o que é _memória virtual_, e como paginação resolve problemas de fragmentação de memória. Também explora o layout de tabelas de página multinível na arquitetura x86_64. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-08`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-08 + + + +## Proteção de Memória + +Uma tarefa principal de um sistema operacional é isolar programas uns dos outros. Seu navegador web não deveria ser capaz de interferir com seu editor de texto, por exemplo. Para alcançar este objetivo, sistemas operacionais utilizam funcionalidade de hardware para garantir que áreas de memória de um processo não sejam acessíveis por outros processos. Existem diferentes abordagens dependendo do hardware e da implementação do SO. + +Como exemplo, alguns processadores ARM Cortex-M (usados para sistemas embarcados) têm uma [_Memory Protection Unit_] (MPU), que permite definir um pequeno número (por exemplo, 8) de regiões de memória com diferentes permissões de acesso (por exemplo, sem acesso, somente leitura, leitura-escrita). Em cada acesso à memória, a MPU garante que o endereço está em uma região com permissões de acesso corretas e lança uma exceção caso contrário. Ao mudar as regiões e permissões de acesso em cada troca de processo, o sistema operacional pode garantir que cada processo acesse apenas sua própria memória e assim isole processos uns dos outros. + +[_Memory Protection Unit_]: https://developer.arm.com/docs/ddi0337/e/memory-protection-unit/about-the-mpu + +No x86, o hardware suporta duas abordagens diferentes para proteção de memória: [segmentação] e [paginação]. + +[segmentação]: https://en.wikipedia.org/wiki/X86_memory_segmentation +[paginação]: https://en.wikipedia.org/wiki/Virtual_memory#Paged_virtual_memory + +## Segmentação + +Segmentação já foi introduzida em 1978, originalmente para aumentar a quantidade de memória endereçável. A situação naquela época era que CPUs usavam apenas endereços de 16 bits, o que limitava a quantidade de memória endereçável a 64 KiB. Para tornar mais que esses 64 KiB acessíveis, registradores de segmento adicionais foram introduzidos, cada um contendo um endereço de deslocamento. A CPU automaticamente adicionava este deslocamento em cada acesso à memória, então até 1 MiB de memória era acessível. + +O registrador de segmento é escolhido automaticamente pela CPU dependendo do tipo de acesso à memória: Para buscar instruções, o segmento de código `CS` é usado, e para operações de pilha (push/pop), o segmento de pilha `SS` é usado. Outras instruções usam o segmento de dados `DS` ou o segmento extra `ES`. Posteriormente, dois registradores de segmento adicionais, `FS` e `GS`, foram adicionados, que podem ser usados livremente. + +Na primeira versão de segmentação, os registradores de segmento continham diretamente o deslocamento e nenhum controle de acesso era realizado. Isso mudou posteriormente com a introdução do [_modo protegido_]. Quando a CPU executa neste modo, os descritores de segmento contêm um índice em uma [_tabela de descritores_] local ou global, que contém – além de um endereço de deslocamento – o tamanho do segmento e permissões de acesso. Ao carregar tabelas de descritores globais/locais separadas para cada processo, que confinam acessos à memória às próprias áreas de memória do processo, o SO pode isolar processos uns dos outros. + +[_modo protegido_]: https://en.wikipedia.org/wiki/X86_memory_segmentation#Protected_mode +[_tabela de descritores_]: https://en.wikipedia.org/wiki/Global_Descriptor_Table + +Ao modificar os endereços de memória antes do acesso real, segmentação já empregava uma técnica que agora é usada em quase todo lugar: _memória virtual_. + +### Memória Virtual + +A ideia por trás da memória virtual é abstrair os endereços de memória do dispositivo de armazenamento físico subjacente. Em vez de acessar diretamente o dispositivo de armazenamento, um passo de tradução é realizado primeiro. Para segmentação, o passo de tradução é adicionar o endereço de deslocamento do segmento ativo. Imagine um programa acessando o endereço de memória `0x1234000` em um segmento com deslocamento de `0x1111000`: O endereço que é realmente acessado é `0x2345000`. + +Para diferenciar os dois tipos de endereço, endereços antes da tradução são chamados _virtuais_, e endereços após a tradução são chamados _físicos_. Uma diferença importante entre esses dois tipos de endereços é que endereços físicos são únicos e sempre se referem à mesma localização de memória distinta. Endereços virtuais, por outro lado, dependem da função de tradução. É inteiramente possível que dois endereços virtuais diferentes se refiram ao mesmo endereço físico. Além disso, endereços virtuais idênticos podem se referir a endereços físicos diferentes quando usam funções de tradução diferentes. + +Um exemplo onde esta propriedade é útil é executar o mesmo programa duas vezes em paralelo: + +![Two virtual address spaces with address 0–150, one translated to 100–250, the other to 300–450](segmentation-same-program-twice.svg) + +Aqui o mesmo programa executa duas vezes, mas com funções de tradução diferentes. A primeira instância tem um deslocamento de segmento de 100, então seus endereços virtuais 0–150 são traduzidos para os endereços físicos 100–250. A segunda instância tem um deslocamento de 300, que traduz seus endereços virtuais 0–150 para endereços físicos 300–450. Isso permite que ambos os programas executem o mesmo código e usem os mesmos endereços virtuais sem interferir uns com os outros. + +Outra vantagem é que programas agora podem ser colocados em localizações arbitrárias de memória física, mesmo se usarem endereços virtuais completamente diferentes. Assim, o SO pode utilizar a quantidade total de memória disponível sem precisar recompilar programas. + +### Fragmentação + +A diferenciação entre endereços virtuais e físicos torna a segmentação realmente poderosa. No entanto, ela tem o problema de fragmentação. Como exemplo, imagine que queremos executar uma terceira cópia do programa que vimos acima: + +![Three virtual address spaces, but there is not enough continuous space for the third](segmentation-fragmentation.svg) + +Não há forma de mapear a terceira instância do programa para memória virtual sem sobreposição, mesmo que haja mais que memória livre suficiente disponível. O problema é que precisamos de memória _contínua_ e não podemos usar os pequenos pedaços livres. + +Uma forma de combater esta fragmentação é pausar a execução, mover as partes usadas da memória mais próximas, atualizar a tradução, e então retomar a execução: + +![Three virtual address spaces after defragmentation](segmentation-fragmentation-compacted.svg) + +Agora há espaço contínuo suficiente para iniciar a terceira instância do nosso programa. + +A desvantagem deste processo de desfragmentação é que ele precisa copiar grandes quantidades de memória, o que diminui o desempenho. Também precisa ser feito regularmente antes que a memória se torne muito fragmentada. Isso torna o desempenho imprevisível já que programas são pausados em momentos aleatórios e podem se tornar não responsivos. + +O problema de fragmentação é uma das razões pelas quais segmentação não é mais usada pela maioria dos sistemas. Na verdade, segmentação nem é mais suportada no modo de 64 bits no x86. Em vez disso, _paginação_ é usada, que evita completamente o problema de fragmentação. + +## Paginação + +A ideia é dividir tanto o espaço de memória virtual quanto o físico em pequenos blocos de tamanho fixo. Os blocos do espaço de memória virtual são chamados _páginas_, e os blocos do espaço de endereço físico são chamados _frames_. Cada página pode ser individualmente mapeada para um frame, o que torna possível dividir regiões de memória maiores em frames físicos não contínuos. + +A vantagem disso se torna visível se recapitularmos o exemplo do espaço de memória fragmentado, mas usando paginação em vez de segmentação desta vez: + +![With paging, the third program instance can be split across many smaller physical areas.](paging-fragmentation.svg) + +Neste exemplo, temos um tamanho de página de 50 bytes, o que significa que cada uma de nossas regiões de memória é dividida em três páginas. Cada página é mapeada para um frame individualmente, então uma região de memória virtual contínua pode ser mapeada para frames físicos não contínuos. Isso nos permite iniciar a terceira instância do programa sem realizar nenhuma desfragmentação antes. + +### Fragmentação Escondida + +Comparado à segmentação, paginação usa muitas regiões de memória pequenas de tamanho fixo em vez de algumas regiões grandes de tamanho variável. Como cada frame tem o mesmo tamanho, não há frames que são muito pequenos para serem usados, então nenhuma fragmentação ocorre. + +Ou _parece_ que nenhuma fragmentação ocorre. Ainda há algum tipo escondido de fragmentação, a chamada _fragmentação interna_. Fragmentação interna ocorre porque nem toda região de memória é um múltiplo exato do tamanho da página. Imagine um programa de tamanho 101 no exemplo acima: Ele ainda precisaria de três páginas de tamanho 50, então ocuparia 49 bytes a mais que o necessário. Para diferenciar os dois tipos de fragmentação, o tipo de fragmentação que acontece ao usar segmentação é chamado _fragmentação externa_. + +Fragmentação interna é infeliz mas frequentemente melhor que a fragmentação externa que ocorre com segmentação. Ela ainda desperdiça memória, mas não requer desfragmentação e torna a quantidade de fragmentação previsível (em média metade de uma página por região de memória). + +### Tabelas de Página + +Vimos que cada uma das potencialmente milhões de páginas é individualmente mapeada para um frame. Esta informação de mapeamento precisa ser armazenada em algum lugar. Segmentação usa um registrador seletor de segmento individual para cada região de memória ativa, o que não é possível para paginação já que há muito mais páginas que registradores. Em vez disso, paginação usa uma estrutura de tabela chamada _tabela de página_ para armazenar a informação de mapeamento. + +Para nosso exemplo acima, as tabelas de página pareceriam com isto: + +![Three page tables, one for each program instance. For instance 1, the mapping is 0->100, 50->150, 100->200. For instance 2, it is 0->300, 50->350, 100->400. For instance 3, it is 0->250, 50->450, 100->500.](paging-page-tables.svg) + +Vemos que cada instância de programa tem sua própria tabela de página. Um ponteiro para a tabela atualmente ativa é armazenado em um registrador especial da CPU. No `x86`, este registrador é chamado `CR3`. É trabalho do sistema operacional carregar este registrador com o ponteiro para a tabela de página correta antes de executar cada instância de programa. + +Em cada acesso à memória, a CPU lê o ponteiro da tabela do registrador e procura o frame mapeado para a página acessada na tabela. Isso é inteiramente feito em hardware e completamente invisível para o programa em execução. Para acelerar o processo de tradução, muitas arquiteturas de CPU têm um cache especial que lembra os resultados das últimas traduções. + +Dependendo da arquitetura, entradas da tabela de página também podem armazenar atributos como permissões de acesso em um campo de flags. No exemplo acima, a flag "r/w" torna a página tanto legível quanto gravável. + +### Tabelas de Página Multinível + +As tabelas de página simples que acabamos de ver têm um problema em espaços de endereço maiores: elas desperdiçam memória. Por exemplo, imagine um programa que usa as quatro páginas virtuais `0`, `1_000_000`, `1_000_050`, e `1_000_100` (usamos `_` como separador de milhares): + +![Page 0 mapped to frame 0 and pages `1_000_000`–`1_000_150` mapped to frames 100–250](single-level-page-table.svg) + +Ele precisa apenas de 4 frames físicos, mas a tabela de página tem mais de um milhão de entradas. Não podemos omitir as entradas vazias porque então a CPU não seria mais capaz de pular diretamente para a entrada correta no processo de tradução (por exemplo, não é mais garantido que a quarta página use a quarta entrada). + +Para reduzir a memória desperdiçada, podemos usar uma **tabela de página de dois níveis**. A ideia é que usamos tabelas de página diferentes para regiões de endereço diferentes. Uma tabela adicional chamada tabela de página de _nível 2_ contém o mapeamento entre regiões de endereço e tabelas de página (nível 1). + +Isso é melhor explicado por um exemplo. Vamos definir que cada tabela de página de nível 1 é responsável por uma região de tamanho `10_000`. Então as seguintes tabelas existiriam para o exemplo de mapeamento acima: + +![Page 0 points to entry 0 of the level 2 page table, which points to the level 1 page table T1. The first entry of T1 points to frame 0; the other entries are empty. Pages `1_000_000`–`1_000_150` point to the 100th entry of the level 2 page table, which points to a different level 1 page table T2. The first three entries of T2 point to frames 100–250; the other entries are empty.](multilevel-page-table.svg) + +A página 0 cai na primeira região de `10_000` bytes, então usa a primeira entrada da tabela de página de nível 2. Esta entrada aponta para a tabela de página de nível 1 T1, que especifica que a página `0` aponta para o frame `0`. + +As páginas `1_000_000`, `1_000_050`, e `1_000_100` todas caem na 100ª região de `10_000` bytes, então usam a 100ª entrada da tabela de página de nível 2. Esta entrada aponta para uma tabela de página de nível 1 diferente T2, que mapeia as três páginas para frames `100`, `150`, e `200`. Note que o endereço da página em tabelas de nível 1 não inclui o deslocamento da região. Por exemplo, a entrada para a página `1_000_050` é apenas `50`. + +Ainda temos 100 entradas vazias na tabela de nível 2, mas muito menos que o milhão de entradas vazias antes. A razão para essas economias é que não precisamos criar tabelas de página de nível 1 para as regiões de memória não mapeadas entre `10_000` e `1_000_000`. + +O princípio de tabelas de página de dois níveis pode ser estendido para três, quatro, ou mais níveis. Então o registrador de tabela de página aponta para a tabela de nível mais alto, que aponta para a tabela de próximo nível mais baixo, que aponta para o próximo nível mais baixo, e assim por diante. A tabela de página de nível 1 então aponta para o frame mapeado. O princípio em geral é chamado de tabela de página _multinível_ ou _hierárquica_. + +Agora que sabemos como paginação e tabelas de página multinível funcionam, podemos olhar como paginação é implementada na arquitetura x86_64 (assumimos no seguinte que a CPU executa no modo de 64 bits). + +## Paginação no x86_64 + +A arquitetura x86_64 usa uma tabela de página de 4 níveis e um tamanho de página de 4 KiB. Cada tabela de página, independente do nível, tem um tamanho fixo de 512 entradas. Cada entrada tem um tamanho de 8 bytes, então cada tabela tem 512 * 8 B = 4 KiB de tamanho e assim cabe exatamente em uma página. + +O índice da tabela de página para cada nível é derivado diretamente do endereço virtual: + +![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](x86_64-table-indices-from-address.svg) + +Vemos que cada índice de tabela consiste de 9 bits, o que faz sentido porque cada tabela tem 2^9 = 512 entradas. Os 12 bits mais baixos são o deslocamento na página de 4 KiB (2^12 bytes = 4 KiB). Os bits 48 a 64 são descartados, o que significa que x86_64 não é realmente 64 bits já que suporta apenas endereços de 48 bits. + +Mesmo que os bits 48 a 64 sejam descartados, eles não podem ser definidos para valores arbitrários. Em vez disso, todos os bits nesta faixa devem ser cópias do bit 47 para manter endereços únicos e permitir extensões futuras como a tabela de página de 5 níveis. Isso é chamado _extensão de sinal_ porque é muito similar à [extensão de sinal em complemento de dois]. Quando um endereço não é corretamente estendido com sinal, a CPU lança uma exceção. + +[extensão de sinal em complemento de dois]: https://en.wikipedia.org/wiki/Two's_complement#Sign_extension + +Vale notar que as CPUs Intel "Ice Lake" recentes opcionalmente suportam [tabelas de página de 5 níveis] para estender endereços virtuais de 48 bits para 57 bits. Dado que otimizar nosso kernel para uma CPU específica não faz sentido neste estágio, trabalharemos apenas com tabelas de página padrão de 4 níveis nesta postagem. + +[tabelas de página de 5 níveis]: https://en.wikipedia.org/wiki/Intel_5-level_paging + +### Exemplo de Tradução + +Vamos passar por um exemplo para entender como o processo de tradução funciona em detalhes: + +![An example of a 4-level page hierarchy with each page table shown in physical memory](x86_64-page-table-translation.svg) + +O endereço físico da tabela de página de nível 4 atualmente ativa, que é a raiz da tabela de página de 4 níveis, é armazenado no registrador `CR3`. Cada entrada da tabela de página então aponta para o frame físico da tabela de próximo nível. A entrada da tabela de nível 1 então aponta para o frame mapeado. Note que todos os endereços nas tabelas de página são físicos em vez de virtuais, porque caso contrário a CPU precisaria traduzi-los também (o que poderia causar uma recursão sem fim). + +A hierarquia de tabela de página acima mapeia duas páginas (em azul). Dos índices da tabela de página, podemos deduzir que os endereços virtuais dessas duas páginas são `0x803FE7F000` e `0x803FE00000`. Vamos ver o que acontece quando o programa tenta ler do endereço `0x803FE7F5CE`. Primeiro, convertemos o endereço para binário e determinamos os índices da tabela de página e o deslocamento de página para o endereço: + +![The sign extension bits are all 0, the level 4 index is 1, the level 3 index is 0, the level 2 index is 511, the level 1 index is 127, and the page offset is 0x5ce](x86_64-page-table-translation-addresses.png) + +Com esses índices, agora podemos percorrer a hierarquia da tabela de página para determinar o frame mapeado para o endereço: + +- Começamos lendo o endereço da tabela de nível 4 do registrador `CR3`. +- O índice de nível 4 é 1, então olhamos para a entrada com índice 1 daquela tabela, que nos diz que a tabela de nível 3 está armazenada no endereço 16 KiB. +- Carregamos a tabela de nível 3 daquele endereço e olhamos para a entrada com índice 0, que nos aponta para a tabela de nível 2 em 24 KiB. +- O índice de nível 2 é 511, então olhamos para a última entrada daquela página para descobrir o endereço da tabela de nível 1. +- Através da entrada com índice 127 da tabela de nível 1, finalmente descobrimos que a página está mapeada para o frame 12 KiB, ou 0x3000 em hexadecimal. +- O passo final é adicionar o deslocamento de página ao endereço do frame para obter o endereço físico 0x3000 + 0x5ce = 0x35ce. + +![The same example 4-level page hierarchy with 5 additional arrows: "Step 0" from the CR3 register to the level 4 table, "Step 1" from the level 4 entry to the level 3 table, "Step 2" from the level 3 entry to the level 2 table, "Step 3" from the level 2 entry to the level 1 table, and "Step 4" from the level 1 table to the mapped frames.](x86_64-page-table-translation-steps.svg) + +As permissões para a página na tabela de nível 1 são `r`, o que significa somente leitura. O hardware reforça essas permissões e lançaria uma exceção se tentássemos escrever naquela página. Permissões em páginas de nível mais alto restringem as permissões possíveis em níveis mais baixos, então se definirmos a entrada de nível 3 como somente leitura, nenhuma página que use esta entrada pode ser gravável, mesmo se níveis mais baixos especificarem permissões de leitura/escrita. + +É importante notar que mesmo que este exemplo usasse apenas uma única instância de cada tabela, tipicamente há múltiplas instâncias de cada nível em cada espaço de endereço. No máximo, há: + +- uma tabela de nível 4, +- 512 tabelas de nível 3 (porque a tabela de nível 4 tem 512 entradas), +- 512 * 512 tabelas de nível 2 (porque cada uma das 512 tabelas de nível 3 tem 512 entradas), e +- 512 * 512 * 512 tabelas de nível 1 (512 entradas para cada tabela de nível 2). + +### Formato da Tabela de Página + +Tabelas de página na arquitetura x86_64 são basicamente um array de 512 entradas. Na sintaxe Rust: + +```rust +#[repr(align(4096))] +pub struct PageTable { + entries: [PageTableEntry; 512], +} +``` + +Como indicado pelo atributo `repr`, tabelas de página precisam ser alinhadas por página, isto é, alinhadas em um limite de 4 KiB. Este requisito garante que uma tabela de página sempre preenche uma página completa e permite uma otimização que torna as entradas muito compactas. + +Cada entrada tem 8 bytes (64 bits) de tamanho e tem o seguinte formato: + +Bit(s) | Nome | Significado +------ | ---- | ------- +0 | present | a página está atualmente na memória +1 | writable | é permitido escrever nesta página +2 | user accessible | se não definido, apenas código em modo kernel pode acessar esta página +3 | write-through caching | escritas vão diretamente para a memória +4 | disable cache | nenhum cache é usado para esta página +5 | accessed | a CPU define este bit quando esta página é usada +6 | dirty | a CPU define este bit quando uma escrita nesta página ocorre +7 | huge page/null | deve ser 0 em P1 e P4, cria uma página de 1 GiB em P3, cria uma página de 2 MiB em P2 +8 | global | página não é removida dos caches em troca de espaço de endereço (bit PGE do registrador CR4 deve estar definido) +9-11 | available | pode ser usado livremente pelo SO +12-51 | physical address | o endereço físico de 52 bits alinhado por página do frame ou da próxima tabela de página +52-62 | available | pode ser usado livremente pelo SO +63 | no execute | proíbe executar código nesta página (o bit NXE no registrador EFER deve estar definido) + +Vemos que apenas os bits 12–51 são usados para armazenar o endereço físico do frame. Os bits restantes são usados como flags ou podem ser usados livremente pelo sistema operacional. Isso é possível porque sempre apontamos para um endereço alinhado em 4096 bytes, seja para uma tabela de página alinhada por página ou para o início de um frame mapeado. Isso significa que os bits 0–11 são sempre zero, então não há razão para armazenar esses bits porque o hardware pode simplesmente defini-los para zero antes de usar o endereço. O mesmo é verdade para os bits 52–63, porque a arquitetura x86_64 suporta apenas endereços físicos de 52 bits (similar a como suporta apenas endereços virtuais de 48 bits). + +Vamos olhar mais de perto as flags disponíveis: + +- A flag `present` diferencia páginas mapeadas de não mapeadas. Ela pode ser usada para temporariamente trocar páginas para o disco quando a memória principal fica cheia. Quando a página é acessada subsequentemente, uma exceção especial chamada _page fault_ ocorre, à qual o sistema operacional pode reagir recarregando a página faltante do disco e então continuando o programa. +- As flags `writable` e `no execute` controlam se o conteúdo da página é gravável ou contém instruções executáveis, respectivamente. +- As flags `accessed` e `dirty` são automaticamente definidas pela CPU quando uma leitura ou escrita na página ocorre. Esta informação pode ser aproveitada pelo sistema operacional, por exemplo, para decidir quais páginas trocar ou se o conteúdo da página foi modificado desde o último salvamento no disco. +- As flags `write-through caching` e `disable cache` permitem o controle de caches para cada página individualmente. +- A flag `user accessible` torna uma página disponível para código em espaço de usuário, caso contrário, é acessível apenas quando a CPU está em modo kernel. Este recurso pode ser usado para tornar [chamadas de sistema] mais rápidas mantendo o kernel mapeado enquanto um programa em espaço de usuário está executando. No entanto, a vulnerabilidade [Spectre] pode permitir que programas em espaço de usuário leiam essas páginas de qualquer forma. +- A flag `global` sinaliza ao hardware que uma página está disponível em todos os espaços de endereço e assim não precisa ser removida do cache de tradução (veja a seção sobre o TLB abaixo) em trocas de espaço de endereço. Esta flag é comumente usada junto com uma flag `user accessible` desmarcada para mapear o código do kernel para todos os espaços de endereço. +- A flag `huge page` permite a criação de páginas de tamanhos maiores permitindo que as entradas das tabelas de página de nível 2 ou nível 3 apontem diretamente para um frame mapeado. Com este bit definido, o tamanho da página aumenta por fator 512 para 2 MiB = 512 * 4 KiB para entradas de nível 2 ou até 1 GiB = 512 * 2 MiB para entradas de nível 3. A vantagem de usar páginas maiores é que menos linhas do cache de tradução e menos tabelas de página são necessárias. + +[chamadas de sistema]: https://en.wikipedia.org/wiki/System_call +[Spectre]: https://en.wikipedia.org/wiki/Spectre_(security_vulnerability) + +A crate `x86_64` fornece tipos para [tabelas de página] e suas [entradas], então não precisamos criar essas estruturas nós mesmos. + +[tabelas de página]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTable.html +[entradas]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html + +### O Translation Lookaside Buffer + +Uma tabela de página de 4 níveis torna a tradução de endereços virtuais cara porque cada tradução requer quatro acessos à memória. Para melhorar o desempenho, a arquitetura x86_64 armazena em cache as últimas traduções no chamado _translation lookaside buffer_ (TLB). Isso permite pular a tradução quando ela ainda está em cache. + +Ao contrário dos outros caches da CPU, o TLB não é totalmente transparente e não atualiza ou remove traduções quando o conteúdo das tabelas de página muda. Isso significa que o kernel deve atualizar manualmente o TLB sempre que modifica uma tabela de página. Para fazer isso, há uma instrução especial da CPU chamada [`invlpg`] ("invalidate page") que remove a tradução para a página especificada do TLB, para que seja carregada novamente da tabela de página no próximo acesso. O TLB também pode ser completamente esvaziado recarregando o registrador `CR3`, que simula uma troca de espaço de endereço. A crate `x86_64` fornece funções Rust para ambas as variantes no [módulo `tlb`]. + +[`invlpg`]: https://www.felixcloutier.com/x86/INVLPG.html +[módulo `tlb`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/tlb/index.html + +É importante lembrar de esvaziar o TLB em cada modificação de tabela de página porque caso contrário a CPU pode continuar usando a tradução antiga, o que pode levar a bugs não-determinísticos que são muito difíceis de depurar. + +## Implementação + +Uma coisa que ainda não mencionamos: **Nosso kernel já executa em paginação**. O bootloader que adicionamos na postagem ["Um Kernel Rust Mínimo"] já configurou uma hierarquia de paginação de 4 níveis que mapeia cada página do nosso kernel para um frame físico. O bootloader faz isso porque paginação é obrigatória no modo de 64 bits no x86_64. + +["Um Kernel Rust Mínimo"]: @/edition-2/posts/02-minimal-rust-kernel/index.md#creating-a-bootimage + +Isso significa que cada endereço de memória que usamos em nosso kernel era um endereço virtual. Acessar o buffer VGA no endereço `0xb8000` só funcionou porque o bootloader fez _identity mapping_ daquela página de memória, o que significa que ele mapeou a página virtual `0xb8000` para o frame físico `0xb8000`. + +Paginação já torna nosso kernel relativamente seguro, já que cada acesso à memória que está fora dos limites causa uma exceção de page fault em vez de escrever em memória física aleatória. O bootloader até define as permissões de acesso corretas para cada página, o que significa que apenas as páginas contendo código são executáveis e apenas páginas de dados são graváveis. + +### Page Faults + +Vamos tentar causar um page fault acessando alguma memória fora do nosso kernel. Primeiro, criamos um manipulador de page fault e o registramos em nossa IDT, para que vejamos uma exceção de page fault em vez de um [double fault] genérico: + +[double fault]: @/edition-2/posts/06-double-faults/index.md + +```rust +// em src/interrupts.rs + +lazy_static! { + static ref IDT: InterruptDescriptorTable = { + let mut idt = InterruptDescriptorTable::new(); + + […] + + idt.page_fault.set_handler_fn(page_fault_handler); // novo + + idt + }; +} + +use x86_64::structures::idt::PageFaultErrorCode; +use crate::hlt_loop; + +extern "x86-interrupt" fn page_fault_handler( + stack_frame: InterruptStackFrame, + error_code: PageFaultErrorCode, +) { + use x86_64::registers::control::Cr2; + + println!("EXCEÇÃO: PAGE FAULT"); + println!("Endereço Acessado: {:?}", Cr2::read()); + println!("Código de Erro: {:?}", error_code); + println!("{:#?}", stack_frame); + hlt_loop(); +} +``` + +O registrador [`CR2`] é automaticamente definido pela CPU em um page fault e contém o endereço virtual acessado que causou o page fault. Usamos a função [`Cr2::read`] da crate `x86_64` para lê-lo e imprimi-lo. O tipo [`PageFaultErrorCode`] fornece mais informações sobre o tipo de acesso à memória que causou o page fault, por exemplo, se foi causado por uma operação de leitura ou escrita. Por esta razão, também o imprimimos. Não podemos continuar a execução sem resolver o page fault, então entramos em um [`hlt_loop`] no final. + +[`CR2`]: https://en.wikipedia.org/wiki/Control_register#CR2 +[`Cr2::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr2.html#method.read +[`PageFaultErrorCode`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html +[LLVM bug]: https://github.com/rust-lang/rust/issues/57270 +[`hlt_loop`]: @/edition-2/posts/07-hardware-interrupts/index.md#the-hlt-instruction + +Agora podemos tentar acessar alguma memória fora do nosso kernel: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + println!("Olá Mundo{}", "!"); + + blog_os::init(); + + // novo + let ptr = 0xdeadbeaf as *mut u8; + unsafe { *ptr = 42; } + + // como antes + #[cfg(test)] + test_main(); + + println!("Não crashou!"); + blog_os::hlt_loop(); +} +``` + +Quando o executamos, vemos que nosso manipulador de page fault é chamado: + +![EXCEPTION: Page Fault, Accessed Address: VirtAddr(0xdeadbeaf), Error Code: CAUSED_BY_WRITE, InterruptStackFrame: {…}](qemu-page-fault.png) + +O registrador `CR2` de fato contém `0xdeadbeaf`, o endereço que tentamos acessar. O código de erro nos diz através do [`CAUSED_BY_WRITE`] que a falha ocorreu ao tentar realizar uma operação de escrita. Ele nos diz ainda mais através dos [bits que _não_ estão definidos][`PageFaultErrorCode`]. Por exemplo, o fato de que a flag `PROTECTION_VIOLATION` não está definida significa que o page fault ocorreu porque a página alvo não estava presente. + +[`CAUSED_BY_WRITE`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE + +Vemos que o ponteiro de instrução atual é `0x2031b2`, então sabemos que este endereço aponta para uma página de código. Páginas de código são mapeadas como somente leitura pelo bootloader, então ler deste endereço funciona mas escrever causa um page fault. Você pode tentar isso mudando o ponteiro `0xdeadbeaf` para `0x2031b2`: + +```rust +// Note: O endereço real pode ser diferente para você. Use o endereço que +// seu manipulador de page fault reporta. +let ptr = 0x2031b2 as *mut u8; + +// lê de uma página de código +unsafe { let x = *ptr; } +println!("leitura funcionou"); + +// escreve em uma página de código +unsafe { *ptr = 42; } +println!("escrita funcionou"); +``` + +Ao comentar a última linha, vemos que o acesso de leitura funciona, mas o acesso de escrita causa um page fault: + +![QEMU with output: "leitura funcionou, EXCEÇÃO: Page Fault, Accessed Address: VirtAddr(0x2031b2), Error Code: PROTECTION_VIOLATION | CAUSED_BY_WRITE, InterruptStackFrame: {…}"](qemu-page-fault-protection.png) + +Vemos que a mensagem _"leitura funcionou"_ é impressa, o que indica que a operação de leitura não causou nenhum erro. No entanto, em vez da mensagem _"escrita funcionou"_, ocorre um page fault. Desta vez a flag [`PROTECTION_VIOLATION`] está definida além da flag [`CAUSED_BY_WRITE`], o que indica que a página estava presente, mas a operação não era permitida nela. Neste caso, escritas na página não são permitidas já que páginas de código são mapeadas como somente leitura. + +[`PROTECTION_VIOLATION`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION + +### Acessando as Tabelas de Página + +Vamos tentar dar uma olhada nas tabelas de página que definem como nosso kernel é mapeado: + +```rust +// em src/main.rs + +#[unsafe(no_mangle)] +pub extern "C" fn _start() -> ! { + println!("Olá Mundo{}", "!"); + + blog_os::init(); + + use x86_64::registers::control::Cr3; + + let (level_4_page_table, _) = Cr3::read(); + println!("Tabela de página de nível 4 em: {:?}", level_4_page_table.start_address()); + + […] // test_main(), println(…), e hlt_loop() +} +``` + +A função [`Cr3::read`] da crate `x86_64` retorna a tabela de página de nível 4 atualmente ativa do registrador `CR3`. Ela retorna uma tupla de um tipo [`PhysFrame`] e um tipo [`Cr3Flags`]. Estamos interessados apenas no frame, então ignoramos o segundo elemento da tupla. + +[`Cr3::read`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3.html#method.read +[`PhysFrame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/frame/struct.PhysFrame.html +[`Cr3Flags`]: https://docs.rs/x86_64/0.14.2/x86_64/registers/control/struct.Cr3Flags.html + +Quando o executamos, vemos a seguinte saída: + +``` +Tabela de página de nível 4 em: PhysAddr(0x1000) +``` + +Então a tabela de página de nível 4 atualmente ativa está armazenada no endereço `0x1000` na memória _física_, como indicado pelo tipo wrapper [`PhysAddr`]. A questão agora é: como podemos acessar esta tabela do nosso kernel? + +[`PhysAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.PhysAddr.html + +Acessar memória física diretamente não é possível quando paginação está ativa, já que programas poderiam facilmente contornar a proteção de memória e acessar a memória de outros programas caso contrário. Então a única forma de acessar a tabela é através de alguma página virtual que está mapeada para o frame físico no endereço `0x1000`. Este problema de criar mapeamentos para frames de tabela de página é um problema geral, já que o kernel precisa acessar as tabelas de página regularmente, por exemplo, ao alocar uma pilha para uma nova thread. + +Soluções para este problema são explicadas em detalhes na próxima postagem. + +## Resumo + +Esta postagem introduziu duas técnicas de proteção de memória: segmentação e paginação. Enquanto a primeira usa regiões de memória de tamanho variável e sofre de fragmentação externa, a última usa páginas de tamanho fixo e permite controle muito mais refinado sobre permissões de acesso. + +Paginação armazena a informação de mapeamento para páginas em tabelas de página com um ou mais níveis. A arquitetura x86_64 usa tabelas de página de 4 níveis e um tamanho de página de 4 KiB. O hardware automaticamente percorre as tabelas de página e armazena em cache as traduções resultantes no translation lookaside buffer (TLB). Este buffer não é atualizado transparentemente e precisa ser esvaziado manualmente em mudanças de tabela de página. + +Aprendemos que nosso kernel já executa em cima de paginação e que acessos ilegais à memória causam exceções de page fault. Tentamos acessar as tabelas de página atualmente ativas, mas não conseguimos fazê-lo porque o registrador CR3 armazena um endereço físico que não podemos acessar diretamente do nosso kernel. + +## O Que Vem a Seguir? + +A próxima postagem explica como implementar suporte para paginação em nosso kernel. Ela apresenta diferentes formas de acessar memória física do nosso kernel, o que torna possível acessar as tabelas de página nas quais nosso kernel executa. Neste ponto, seremos capazes de implementar funções para traduzir endereços virtuais para físicos e para criar novos mapeamentos nas tabelas de página. \ No newline at end of file diff --git a/blog/content/edition-2/posts/09-paging-implementation/index.pt-BR.md b/blog/content/edition-2/posts/09-paging-implementation/index.pt-BR.md new file mode 100644 index 00000000..8334c7c8 --- /dev/null +++ b/blog/content/edition-2/posts/09-paging-implementation/index.pt-BR.md @@ -0,0 +1,1012 @@ ++++ +title = "Implementação de Paginação" +weight = 9 +path = "pt-BR/paging-implementation" +date = 2019-03-14 + +[extra] +chapter = "Gerenciamento de Memória" +# Please update this when updating the translation +translation_based_on_commit = "32f629fb2dc193db0dc0657338bd0ddec5914f05" + +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Esta postagem mostra como implementar suporte a paginação em nosso kernel. Ela primeiro explora diferentes técnicas para tornar os frames físicos da tabela de página acessíveis ao kernel e discute suas respectivas vantagens e desvantagens. Em seguida, implementa uma função de tradução de endereços e uma função para criar um novo mapeamento. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-09`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-09 + + + +## Introdução + +A [postagem anterior] deu uma introdução ao conceito de paginação. Ela motivou paginação comparando-a com segmentação, explicou como paginação e tabelas de página funcionam, e então introduziu o design de tabela de página de 4 níveis do `x86_64`. Descobrimos que o bootloader já configurou uma hierarquia de tabela de página para nosso kernel, o que significa que nosso kernel já executa em endereços virtuais. Isso melhora a segurança, já que acessos ilegais à memória causam exceções de page fault em vez de modificar memória física arbitrária. + +[postagem anterior]: @/edition-2/posts/08-paging-introduction/index.md + +A postagem terminou com o problema de que [não podemos acessar as tabelas de página do nosso kernel][end of previous post] porque estão armazenadas na memória física e nosso kernel já executa em endereços virtuais. Esta postagem explora diferentes abordagens para tornar os frames da tabela de página acessíveis ao nosso kernel. Discutiremos as vantagens e desvantagens de cada abordagem e então decidiremos sobre uma abordagem para nosso kernel. + +[end of previous post]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables + +Para implementar a abordagem, precisaremos de suporte do bootloader, então o configuraremos primeiro. Depois, implementaremos uma função que percorre a hierarquia de tabela de página para traduzir endereços virtuais em físicos. Finalmente, aprenderemos como criar novos mapeamentos nas tabelas de página e como encontrar frames de memória não usados para criar novas tabelas de página. + +## Acessando Tabelas de Página + +Acessar as tabelas de página do nosso kernel não é tão fácil quanto pode parecer. Para entender o problema, vamos dar uma olhada na hierarquia de tabela de página de 4 níveis de exemplo da postagem anterior novamente: + +![An example 4-level page hierarchy with each page table shown in physical memory](../paging-introduction/x86_64-page-table-translation.svg) + +A coisa importante aqui é que cada entrada de página armazena o endereço _físico_ da próxima tabela. Isso evita a necessidade de executar uma tradução para esses endereços também, o que seria ruim para o desempenho e poderia facilmente causar loops de tradução infinitos. + +O problema para nós é que não podemos acessar diretamente endereços físicos do nosso kernel, já que nosso kernel também executa em cima de endereços virtuais. Por exemplo, quando acessamos o endereço `4 KiB`, acessamos o endereço _virtual_ `4 KiB`, não o endereço _físico_ `4 KiB` onde a tabela de página de nível 4 está armazenada. Quando queremos acessar o endereço físico `4 KiB`, só podemos fazê-lo através de algum endereço virtual que mapeia para ele. + +Então, para acessar frames de tabela de página, precisamos mapear algumas páginas virtuais para eles. Existem diferentes formas de criar esses mapeamentos que todos nos permitem acessar frames de tabela de página arbitrários. + +### Identity Mapping + +Uma solução simples é fazer **identity map de todas as tabelas de página**: + +![A virtual and a physical address space with various virtual pages mapped to the physical frame with the same address](identity-mapped-page-tables.svg) + +Neste exemplo, vemos vários frames de tabela de página com identity mapping. Desta forma, os endereços físicos das tabelas de página também são endereços virtuais válidos, então podemos facilmente acessar as tabelas de página de todos os níveis começando do registrador CR3. + +No entanto, isso confunde o espaço de endereço virtual e torna mais difícil encontrar regiões contínuas de memória de tamanhos maiores. Por exemplo, imagine que queremos criar uma região de memória virtual de tamanho 1000 KiB no gráfico acima, por exemplo, para [mapear um arquivo na memória]. Não podemos iniciar a região em `28 KiB` porque colidia com a página já mapeada em `1004 KiB`. Então temos que procurar mais até encontrarmos uma área não mapeada grande o suficiente, por exemplo em `1008 KiB`. Este é um problema de fragmentação similar ao da [segmentação]. + +[mapear um arquivo na memória]: https://en.wikipedia.org/wiki/Memory-mapped_file +[segmentação]: @/edition-2/posts/08-paging-introduction/index.md#fragmentation + +Igualmente, torna muito mais difícil criar novas tabelas de página porque precisamos encontrar frames físicos cujas páginas correspondentes já não estão em uso. Por exemplo, vamos assumir que reservamos a região de memória _virtual_ de 1000 KiB começando em `1008 KiB` para nosso arquivo mapeado na memória. Agora não podemos mais usar nenhum frame com endereço _físico_ entre `1000 KiB` e `2008 KiB`, porque não podemos fazer identity mapping dele. + +### Mapear em um Deslocamento Fixo + +Para evitar o problema de confundir o espaço de endereço virtual, podemos **usar uma região de memória separada para mapeamentos de tabela de página**. Então, em vez de fazer identity mapping dos frames de tabela de página, os mapeamos em um deslocamento fixo no espaço de endereço virtual. Por exemplo, o deslocamento poderia ser 10 TiB: + +![The same figure as for the identity mapping, but each mapped virtual page is offset by 10 TiB.](page-tables-mapped-at-offset.svg) + +Ao usar a memória virtual no intervalo `10 TiB..(10 TiB + tamanho da memória física)` exclusivamente para mapeamentos de tabela de página, evitamos os problemas de colisão do identity mapping. Reservar uma região 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. Isso não é um problema no x86_64, já que o espaço de endereço de 48 bits tem 256 TiB de tamanho. + +Esta abordagem ainda tem a desvantagem de que precisamos criar um novo mapeamento sempre que criamos uma nova tabela de página. Além disso, não permite acessar tabelas de página de outros espaços de endereço, o que seria útil ao criar um novo processo. + +### Mapear a Memória Física Completa + +Podemos resolver esses problemas **mapeando a memória física completa** em vez de apenas frames de tabela de página: + +![The same figure as for the offset mapping, but every physical frame has a mapping (at 10 TiB + X) instead of only page table frames.](map-complete-physical-memory.svg) + +Esta abordagem permite que nosso kernel acesse memória física arbitrária, incluindo frames de tabela de página de outros espaços de endereço. O intervalo de memória virtual reservado tem o mesmo tamanho de antes, com a diferença de que não contém mais páginas não mapeadas. + +A desvantagem desta abordagem é que tabelas de página adicionais são necessárias para armazenar o mapeamento da memória física. Essas tabelas de página precisam ser armazenadas em algum lugar, então usam uma parte da memória física, o que pode ser um problema em dispositivos com uma pequena quantidade de memória. + +No x86_64, no entanto, podemos usar [huge pages] com tamanho de 2 MiB para o mapeamento, em vez das páginas padrão de 4 KiB. Desta forma, mapear 32 GiB de memória física requer apenas 132 KiB para tabelas de página, já que apenas uma tabela de nível 3 e 32 tabelas de nível 2 são necessárias. Huge pages também são mais eficientes em cache, já que usam menos entradas no translation lookaside buffer (TLB). + +[huge pages]: https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes + +### Mapeamento Temporário + +Para dispositivos com quantidades muito pequenas de memória física, poderíamos **mapear os frames de tabela de página apenas temporariamente** quando precisamos acessá-los. Para poder criar os mapeamentos temporários, precisamos apenas de uma única tabela de nível 1 com identity mapping: + +![A virtual and a physical address space with an identity mapped level 1 table, which maps its 0th entry to the level 2 table frame, thereby mapping that frame to the page with address 0](temporarily-mapped-page-tables.svg) + +A tabela de nível 1 neste gráfico controla os primeiros 2 MiB do espaço de endereço virtual. Isso ocorre porque ela é alcançável começando no registrador CR3 e seguindo a 0ª entrada nas tabelas de página de nível 4, nível 3 e nível 2. A entrada com índice `8` mapeia a página virtual no endereço `32 KiB` para o frame físico no endereço `32 KiB`, fazendo assim identity mapping da própria tabela de nível 1. O gráfico mostra este identity mapping pela seta horizontal em `32 KiB`. + +Ao escrever na tabela de nível 1 com identity mapping, nosso kernel pode criar até 511 mapeamentos temporários (512 menos a entrada necessária para o identity mapping). No exemplo acima, o kernel criou dois mapeamentos temporários: + +- Ao mapear a 0ª entrada da tabela de nível 1 para o frame com endereço `24 KiB`, ele criou um mapeamento temporário da página virtual em `0 KiB` para o frame físico da tabela de página de nível 2, indicado pela seta tracejada. +- Ao mapear a 9ª entrada da tabela de nível 1 para o frame com endereço `4 KiB`, ele criou um mapeamento temporário da página virtual em `36 KiB` para o frame físico da tabela de página de nível 4, indicado pela seta tracejada. + +Agora o kernel pode acessar a tabela de página de nível 2 escrevendo na página `0 KiB` e a tabela de página de nível 4 escrevendo na página `36 KiB`. + +O processo para acessar um frame de tabela de página arbitrário com mapeamentos temporários seria: + +- Procurar uma entrada livre na tabela de nível 1 com identity mapping. +- Mapear essa entrada para o frame físico da tabela de página que queremos acessar. +- Acessar o frame alvo através da página virtual que mapeia para a entrada. +- Definir a entrada de volta para não usada, removendo assim o mapeamento temporário novamente. + +Esta abordagem reutiliza as mesmas 512 páginas virtuais para criar os mapeamentos e assim requer apenas 4 KiB de memória física. A desvantagem é que é um pouco trabalhosa, especialmente já que um novo mapeamento pode requerer modificações a múltiplos níveis de tabela, o que significa que precisaríamos repetir o processo acima múltiplas vezes. + +### Tabelas de Página Recursivas + +Outra abordagem interessante, que não requer nenhuma tabela de página adicional, é **mapear a tabela de página recursivamente**. A ideia por trás desta abordagem é mapear uma entrada da tabela de página de nível 4 para a própria tabela de nível 4. Ao fazer isso, efetivamente reservamos uma parte do espaço de endereço virtual e mapeamos todos os frames de tabela de página atuais e futuros para esse espaço. + +Vamos passar por um exemplo para entender como isso tudo funciona: + +![An example 4-level page hierarchy with each page table shown in physical memory. Entry 511 of the level 4 page is mapped to frame 4KiB, the frame of the level 4 table itself.](recursive-page-table.png) + +A única diferença para o [exemplo no início desta postagem] é a entrada adicional no índice `511` na tabela de nível 4, que está mapeada para o frame físico `4 KiB`, o frame da própria tabela de nível 4. + +[exemplo no início desta postagem]: #acessando-tabelas-de-pagina + +Ao deixar a CPU seguir esta entrada em uma tradução, ela não alcança uma tabela de nível 3, mas a mesma tabela de nível 4 novamente. Isso é similar a uma função recursiva que se chama, portanto esta tabela é chamada de _tabela de página recursiva_. A coisa importante é que a CPU assume que cada entrada na tabela de nível 4 aponta para uma tabela de nível 3, então agora trata a tabela de nível 4 como uma tabela de nível 3. Isso funciona porque tabelas de todos os níveis têm exatamente o mesmo layout no x86_64. + +Ao seguir a entrada recursiva uma ou múltiplas vezes antes de começarmos a tradução real, podemos efetivamente encurtar o número de níveis que a CPU percorre. Por exemplo, se seguirmos a entrada recursiva uma vez e então prosseguirmos para a tabela de nível 3, a CPU pensa que a tabela de nível 3 é uma tabela de nível 2. Indo mais longe, ela trata a tabela de nível 2 como uma tabela de nível 1 e a tabela de nível 1 como o frame mapeado. Isso significa que agora podemos ler e escrever a tabela de página de nível 1 porque a CPU pensa que é o frame mapeado. O gráfico abaixo ilustra os cinco passos de tradução: + +![The above example 4-level page hierarchy with 5 arrows: "Step 0" from CR4 to level 4 table, "Step 1" from level 4 table to level 4 table, "Step 2" from level 4 table to level 3 table, "Step 3" from level 3 table to level 2 table, and "Step 4" from level 2 table to level 1 table.](recursive-page-table-access-level-1.png) + +Similarmente, podemos seguir a entrada recursiva duas vezes antes de iniciar a tradução para reduzir o número de níveis percorridos para dois: + +![The same 4-level page hierarchy with the following 4 arrows: "Step 0" from CR4 to level 4 table, "Steps 1&2" from level 4 table to level 4 table, "Step 3" from level 4 table to level 3 table, and "Step 4" from level 3 table to level 2 table.](recursive-page-table-access-level-2.png) + +Vamos passar por isso passo a passo: Primeiro, a CPU segue a entrada recursiva na tabela de nível 4 e pensa que alcança uma tabela de nível 3. Então ela segue a entrada recursiva novamente e pensa que alcança uma tabela de nível 2. Mas na realidade, ela ainda está na tabela de nível 4. Quando a CPU agora segue uma entrada diferente, ela aterrissa em uma tabela de nível 3, mas pensa que já está em uma tabela de nível 1. Então, enquanto a próxima entrada aponta para uma tabela de nível 2, a CPU pensa que aponta para o frame mapeado, o que nos permite ler e escrever a tabela de nível 2. + +Acessar as tabelas de níveis 3 e 4 funciona da mesma forma. Para acessar a tabela de nível 3, seguimos a entrada recursiva três vezes, enganando a CPU a pensar que já está em uma tabela de nível 1. Então seguimos outra entrada e alcançamos uma tabela de nível 3, que a CPU trata como um frame mapeado. Para acessar a própria tabela de nível 4, apenas seguimos a entrada recursiva quatro vezes até a CPU tratar a própria tabela de nível 4 como o frame mapeado (em azul no gráfico abaixo). + +![The same 4-level page hierarchy with the following 3 arrows: "Step 0" from CR4 to level 4 table, "Steps 1,2,3" from level 4 table to level 4 table, and "Step 4" from level 4 table to level 3 table. In blue, the alternative "Steps 1,2,3,4" arrow from level 4 table to level 4 table.](recursive-page-table-access-level-3.png) + +Pode levar algum tempo para entender o conceito, mas funciona muito bem na prática. + +Na seção abaixo, explicamos como construir endereços virtuais para seguir a entrada recursiva uma ou múltiplas vezes. Não usaremos paginação recursiva para nossa implementação, então você não precisa ler para continuar com a postagem. Se isso te interessa, apenas clique em _"Cálculo de Endereço"_ para expandir. + +--- + +
+

Cálculo de Endereço

+ +Vimos que podemos acessar tabelas de todos os níveis seguindo a entrada recursiva uma ou múltiplas vezes antes da tradução real. Como os índices nas tabelas dos quatro níveis são derivados diretamente do endereço virtual, precisamos construir endereços virtuais especiais para esta técnica. Lembre-se, os índices da tabela de página são derivados do endereço da seguinte forma: + +![Bits 0–12 are the page offset, bits 12–21 the level 1 index, bits 21–30 the level 2 index, bits 30–39 the level 3 index, and bits 39–48 the level 4 index](../paging-introduction/x86_64-table-indices-from-address.svg) + +Vamos assumir que queremos acessar a tabela de página de nível 1 que mapeia uma página específica. Como aprendemos acima, isso significa que temos que seguir a entrada recursiva uma vez antes de continuar com os índices de nível 4, nível 3 e nível 2. Para fazer isso, movemos cada bloco do endereço um bloco para a direita e definimos o índice de nível 4 original para o índice da entrada recursiva: + +![Bits 0–12 are the offset into the level 1 table frame, bits 12–21 the level 2 index, bits 21–30 the level 3 index, bits 30–39 the level 4 index, and bits 39–48 the index of the recursive entry](table-indices-from-address-recursive-level-1.svg) + +Para acessar a tabela de nível 2 daquela página, movemos cada bloco de índice dois blocos para a direita e definimos tanto os blocos do índice de nível 4 original quanto do índice de nível 3 original para o índice da entrada recursiva: + +![Bits 0–12 are the offset into the level 2 table frame, bits 12–21 the level 3 index, bits 21–30 the level 4 index, and bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-2.svg) + +Acessar a tabela de nível 3 funciona movendo cada bloco três blocos para a direita e usando o índice recursivo para os blocos de endereço originais de nível 4, nível 3 e nível 2: + +![Bits 0–12 are the offset into the level 3 table frame, bits 12–21 the level 4 index, and bits 21–30, bits 30–39 and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-3.svg) + +Finalmente, podemos acessar a tabela de nível 4 movendo cada bloco quatro blocos para a direita e usando o índice recursivo para todos os blocos de endereço exceto o deslocamento: + +![Bits 0–12 are the offset into the level l table frame and bits 12–21, bits 21–30, bits 30–39, and bits 39–48 are the index of the recursive entry](table-indices-from-address-recursive-level-4.svg) + +Agora podemos calcular endereços virtuais para as tabelas de página de todos os quatro níveis. Podemos até calcular um endereço que aponta exatamente para uma entrada de tabela de página específica multiplicando seu índice por 8, o tamanho de uma entrada de tabela de página. + +A tabela abaixo resume a estrutura de endereço para acessar os diferentes tipos de frames: + +Endereço Virtual para | Estrutura de Endereço ([octal]) +------------------- | ------------------------------- +Página | `0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE` +Entrada da Tabela de Nível 1 | `0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD` +Entrada da Tabela de Nível 2 | `0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC` +Entrada da Tabela de Nível 3 | `0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB` +Entrada da Tabela de Nível 4 | `0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA` + +[octal]: https://en.wikipedia.org/wiki/Octal + +Onde `AAA` é o índice de nível 4, `BBB` o índice de nível 3, `CCC` o índice de nível 2, e `DDD` o índice de nível 1 do frame mapeado, e `EEEE` o deslocamento nele. `RRR` é o índice da entrada recursiva. Quando um índice (três dígitos) é transformado em um deslocamento (quatro dígitos), é feito multiplicando-o por 8 (o tamanho de uma entrada de tabela de página). Com este deslocamento, o endereço resultante aponta diretamente para a respectiva entrada de tabela de página. + +`SSSSSS` são bits de extensão de sinal, o que significa que são todas cópias do bit 47. Este é um requisito especial para endereços válidos na arquitetura x86_64. Explicamos isso na [postagem anterior][sign extension]. + +[sign extension]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 + +Usamos números [octais] para representar os endereços, já que cada caractere octal representa três bits, o que nos permite separar claramente os índices de 9 bits dos diferentes níveis de tabela de página. Isso não é possível com o sistema hexadecimal, onde cada caractere representa quatro bits. + +##### Em Código Rust + +Para construir tais endereços em código Rust, você pode usar operações bitwise: + +```rust +// o endereço virtual cujas tabelas de página correspondentes você deseja acessar +let addr: usize = […]; + +let r = 0o777; // índice recursivo +let sign = 0o177777 << 48; // extensão de sinal + +// recupera os índices da tabela de página do endereço que queremos traduzir +let l4_idx = (addr >> 39) & 0o777; // índice de nível 4 +let l3_idx = (addr >> 30) & 0o777; // índice de nível 3 +let l2_idx = (addr >> 21) & 0o777; // índice de nível 2 +let l1_idx = (addr >> 12) & 0o777; // índice de nível 1 +let page_offset = addr & 0o7777; + +// calcula os endereços da tabela +let level_4_table_addr = + sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); +let level_3_table_addr = + sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); +let level_2_table_addr = + sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); +let level_1_table_addr = + sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); +``` + +O código acima assume que a última entrada de nível 4 com índice `0o777` (511) está mapeada recursivamente. Isso não é o caso atualmente, então o código ainda não funcionará. Veja abaixo sobre como dizer ao bootloader para configurar o mapeamento recursivo. + +Alternativamente a realizar as operações bitwise manualmente, você pode usar o tipo [`RecursivePageTable`] da crate `x86_64`, que fornece abstrações seguras para várias operações de tabela de página. Por exemplo, o código abaixo mostra como traduzir um endereço virtual para seu endereço físico mapeado: + +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html + +```rust +// em src/memory.rs + +use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; +use x86_64::{VirtAddr, PhysAddr}; + +/// Cria uma instância RecursivePageTable do endereço de nível 4. +let level_4_table_addr = […]; +let level_4_table_ptr = level_4_table_addr as *mut PageTable; +let recursive_page_table = unsafe { + let level_4_table = &mut *level_4_table_ptr; + RecursivePageTable::new(level_4_table).unwrap(); +} + + +/// Recupera o endereço físico para o endereço virtual dado +let addr: u64 = […] +let addr = VirtAddr::new(addr); +let page: Page = Page::containing_address(addr); + +// realiza a tradução +let frame = recursive_page_table.translate_page(page); +frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) +``` + +Novamente, um mapeamento recursivo válido é necessário para este código. Com tal mapeamento, o `level_4_table_addr` faltante pode ser calculado como no primeiro exemplo de código. + +
+ +--- + +Paginação Recursiva é uma técnica interessante que mostra quão poderoso um único mapeamento em uma tabela de página pode ser. É relativamente fácil de implementar e requer apenas uma quantidade mínima de configuração (apenas uma única entrada recursiva), então é uma boa escolha para primeiros experimentos com paginação. + +No entanto, também tem algumas desvantagens: + +- Ela ocupa uma grande quantidade de memória virtual (512 GiB). Isso não é um grande problema no grande espaço de endereço de 48 bits, mas pode levar a comportamento de cache subótimo. +- Ela só permite acessar facilmente o espaço de endereço atualmente ativo. Acessar outros espaços de endereço ainda é possível mudando a entrada recursiva, mas um mapeamento temporário é necessário para mudar de volta. Descrevemos como fazer isso na postagem (desatualizada) [_Remap The Kernel_]. +- Ela depende fortemente do formato de tabela de página do x86 e pode não funcionar em outras arquiteturas. + +[_Remap The Kernel_]: https://os.phil-opp.com/remap-the-kernel/#overview + +## Suporte do Bootloader + +Todas essas abordagens requerem modificações de tabela de página para sua configuração. Por exemplo, mapeamentos para a memória física precisam ser criados ou uma entrada da tabela de nível 4 precisa ser mapeada recursivamente. O problema é que não podemos criar esses mapeamentos necessários sem uma forma existente de acessar as tabelas de página. + +Isso significa que precisamos da ajuda do bootloader, que cria as tabelas de página nas quais nosso kernel executa. O bootloader tem acesso às tabelas de página, então pode criar quaisquer mapeamentos que precisamos. Em sua implementação atual, a crate `bootloader` tem suporte para duas das abordagens acima, controladas através de [cargo features]: + +[cargo features]: https://doc.rust-lang.org/cargo/reference/features.html#the-features-section + +- A feature `map_physical_memory` mapeia a memória física completa em algum lugar no espaço de endereço virtual. Assim, o kernel tem acesso a toda a memória física e pode seguir a abordagem [_Mapear a Memória Física Completa_](#mapear-a-memoria-fisica-completa). +- Com a feature `recursive_page_table`, o bootloader mapeia uma entrada da tabela de página de nível 4 recursivamente. Isso permite que o kernel acesse as tabelas de página como descrito na seção [_Tabelas de Página Recursivas_](#tabelas-de-pagina-recursivas). + +Escolhemos a primeira abordagem para nosso kernel, já que é simples, independente de plataforma, e mais poderosa (também permite acesso a frames que não são de tabela de página). Para habilitar o suporte de bootloader necessário, adicionamos a feature `map_physical_memory` à nossa dependência `bootloader`: + +```toml +[dependencies] +bootloader = { version = "0.9", features = ["map_physical_memory"]} +``` + +Com esta feature habilitada, o bootloader mapeia a memória física completa para algum intervalo de endereço virtual não usado. Para comunicar o intervalo de endereço virtual ao nosso kernel, o bootloader passa uma estrutura de _boot information_. + +### Boot Information + +A crate `bootloader` define uma struct [`BootInfo`] que contém todas as informações que ela passa para nosso kernel. A struct ainda está em um estágio inicial, então espere alguma quebra ao atualizar para versões [semver-incompatíveis] futuras do bootloader. Com a feature `map_physical_memory` habilitada, ela atualmente tem dois campos `memory_map` e `physical_memory_offset`: + +[`BootInfo`]: https://docs.rs/bootloader/0.9/bootloader/bootinfo/struct.BootInfo.html +[semver-incompatíveis]: https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements + +- O campo `memory_map` contém uma visão geral da memória física disponível. Isso diz ao nosso kernel quanta memória física está disponível no sistema e quais regiões de memória são reservadas para dispositivos como o hardware VGA. O mapa de memória pode ser consultado do firmware BIOS ou UEFI, mas apenas muito cedo no processo de boot. Por esta razão, deve ser fornecido pelo bootloader porque não há forma do kernel recuperá-lo mais tarde. Precisaremos do mapa de memória mais tarde nesta postagem. +- O `physical_memory_offset` nos diz o endereço inicial virtual do mapeamento de memória física. Ao adicionar este deslocamento a um endereço físico, obtemos o endereço virtual correspondente. Isso nos permite acessar memória física arbitrária do nosso kernel. +- Este deslocamento de memória física pode ser customizado adicionando uma tabela `[package.metadata.bootloader]` em Cargo.toml e definindo o campo `physical-memory-offset = "0x0000f00000000000"` (ou qualquer outro valor). No entanto, note que o bootloader pode entrar em panic se ele encontrar valores de endereço físico que começam a se sobrepor com o espaço além do deslocamento, isto é, áreas que ele teria previamente mapeado para alguns outros endereços físicos iniciais. Então, em geral, quanto maior o valor (> 1 TiB), melhor. + +O bootloader passa a struct `BootInfo` para nosso kernel na forma de um argumento `&'static BootInfo` para nossa função `_start`. Ainda não temos este argumento declarado em nossa função, então vamos adicioná-lo: + +```rust +// em src/main.rs + +use bootloader::BootInfo; + +#[unsafe(no_mangle)] +pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // novo argumento + […] +} +``` + +Não foi um problema deixar este argumento de fora antes porque a convenção de chamada x86_64 passa o primeiro argumento em um registrador da CPU. Assim, o argumento é simplesmente ignorado quando não é declarado. No entanto, seria um problema se usássemos acidentalmente um tipo de argumento errado, já que o compilador não conhece a assinatura de tipo correta da nossa função de ponto de entrada. + +### A Macro `entry_point` + +Como nossa função `_start` é chamada externamente pelo bootloader, nenhuma verificação da assinatura da nossa função ocorre. Isso significa que poderíamos deixá-la receber argumentos arbitrários sem nenhum erro de compilação, mas falharia ou causaria comportamento indefinido em tempo de execução. + +Para garantir que a função de ponto de entrada sempre tenha a assinatura correta que o bootloader espera, a crate `bootloader` fornece uma macro [`entry_point`] que fornece uma forma verificada por tipo de definir uma função Rust como ponto de entrada. Vamos reescrever nossa função de ponto de entrada para usar esta macro: + +[`entry_point`]: https://docs.rs/bootloader/0.6.4/bootloader/macro.entry_point.html + +```rust +// em src/main.rs + +use bootloader::{BootInfo, entry_point}; + +entry_point!(kernel_main); + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + […] +} +``` + +Não precisamos mais usar `extern "C"` ou `no_mangle` para nosso ponto de entrada, já que a macro define o verdadeiro ponto de entrada `_start` de nível mais baixo para nós. A função `kernel_main` agora é uma função Rust completamente normal, então podemos escolher um nome arbitrário para ela. A coisa importante é que ela é verificada por tipo, então um erro de compilação ocorre quando usamos uma assinatura de função errada, por exemplo, adicionando um argumento ou mudando o tipo do argumento. + +Vamos realizar a mesma mudança em nosso `lib.rs`: + +```rust +// em src/lib.rs + +#[cfg(test)] +use bootloader::{entry_point, BootInfo}; + +#[cfg(test)] +entry_point!(test_kernel_main); + +/// Ponto de entrada para `cargo test` +#[cfg(test)] +fn test_kernel_main(_boot_info: &'static BootInfo) -> ! { + // como antes + init(); + test_main(); + hlt_loop(); +} +``` + +Como o ponto de entrada é usado apenas em modo de teste, adicionamos o atributo `#[cfg(test)]` a todos os itens. Damos ao nosso ponto de entrada de teste o nome distinto `test_kernel_main` para evitar confusão com o `kernel_main` do nosso `main.rs`. Não usamos o parâmetro `BootInfo` por enquanto, então prefixamos o nome do parâmetro com um `_` para silenciar o aviso de variável não usada. + +## Implementação + +Agora que temos acesso à memória física, podemos finalmente começar a implementar nosso código de tabela de página. Primeiro, daremos uma olhada nas tabelas de página atualmente ativas nas quais nosso kernel executa. No segundo passo, criaremos uma função de tradução que retorna o endereço físico para o qual um dado endereço virtual está mapeado. Como último passo, tentaremos modificar as tabelas de página para criar um novo mapeamento. + +Antes de começarmos, criamos um novo módulo `memory` para nosso código: + +```rust +// em src/lib.rs + +pub mod memory; +``` + +Para o módulo, criamos um arquivo vazio `src/memory.rs`. + +### Acessando as Tabelas de Página + +No [final da postagem anterior], tentamos dar uma olhada nas tabelas de página nas quais nosso kernel executa, mas falhamos, já que não conseguimos acessar o frame físico para o qual o registrador `CR3` aponta. Agora podemos continuar de lá criando uma função `active_level_4_table` que retorna uma referência à tabela de página de nível 4 ativa: + +[final da postagem anterior]: @/edition-2/posts/08-paging-introduction/index.md#accessing-the-page-tables + +```rust +// em src/memory.rs + +use x86_64::{ + structures::paging::PageTable, + VirtAddr, +}; + +/// Retorna uma referência mutável à tabela de nível 4 ativa. +/// +/// Esta função é unsafe porque o chamador deve garantir que a +/// memória física completa está mapeada para memória virtual no +/// `physical_memory_offset` passado. Além disso, esta função deve ser chamada apenas uma vez +/// para evitar referenciar `&mut` com aliasing (que é comportamento indefinido). +pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) + -> &'static mut PageTable +{ + use x86_64::registers::control::Cr3; + + let (level_4_table_frame, _) = Cr3::read(); + + let phys = level_4_table_frame.start_address(); + let virt = physical_memory_offset + phys.as_u64(); + let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); + + unsafe { &mut *page_table_ptr } +} +``` + +Primeiro, lemos o frame físico da tabela de nível 4 ativa do registrador `CR3`. Então pegamos seu endereço inicial físico, o convertemos para um `u64`, e o adicionamos ao `physical_memory_offset` para obter o endereço virtual onde o frame da tabela de página está mapeado. Finalmente, convertemos o endereço virtual para um ponteiro bruto `*mut PageTable` através do método `as_mut_ptr` e então criamos unsafely uma referência `&mut PageTable` dele. Criamos uma referência `&mut` em vez de uma referência `&` porque mudaremos as tabelas de página mais tarde nesta postagem. + +Não precisávamos especificar o nome da nossa função de ponto de entrada explicitamente, já que o linker procura por uma função com o nome `_start` por padrão. + +Agora podemos usar esta função para imprimir as entradas da tabela de nível 4: + +```rust +// em src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory::active_level_4_table; + use x86_64::VirtAddr; + + println!("Olá Mundo{}", "!"); + blog_os::init(); + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let l4_table = unsafe { active_level_4_table(phys_mem_offset) }; + + for (i, entry) in l4_table.iter().enumerate() { + if !entry.is_unused() { + println!("Entrada L4 {}: {:?}", i, entry); + } + } + + // como antes + #[cfg(test)] + test_main(); + + println!("Não crashou!"); + blog_os::hlt_loop(); +} +``` + +Primeiro, convertemos o `physical_memory_offset` da struct `BootInfo` para um [`VirtAddr`] e o passamos para a função `active_level_4_table`. Então usamos a função `iter` para iterar sobre as entradas da tabela de página e o combinador [`enumerate`] para adicionar adicionalmente um índice `i` a cada elemento. Imprimimos apenas entradas não vazias porque todas as 512 entradas não caberiam na tela. + +[`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html +[`enumerate`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate + +Quando o executamos, vemos a seguinte saída: + +![QEMU printing entry 0 (0x2000, PRESENT, WRITABLE, ACCESSED), entry 1 (0x894000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 31 (0x88e000, PRESENT, WRITABLE, ACCESSED, DIRTY), entry 175 (0x891000, PRESENT, WRITABLE, ACCESSED, DIRTY), and entry 504 (0x897000, PRESENT, WRITABLE, ACCESSED, DIRTY)](qemu-print-level-4-table.png) + +Vemos que existem várias entradas não vazias, que todas mapeiam para diferentes tabelas de nível 3. Há tantas regiões porque código do kernel, pilha do kernel, mapeamento de memória física, e informação de boot todos usam áreas de memória separadas. + +Para percorrer as tabelas de página mais e dar uma olhada em uma tabela de nível 3, podemos pegar o frame mapeado de uma entrada e convertê-lo para um endereço virtual novamente: + +```rust +// em no loop `for` em src/main.rs + +use x86_64::structures::paging::PageTable; + +if !entry.is_unused() { + println!("Entrada L4 {}: {:?}", i, entry); + + // obtém o endereço físico da entrada e o converte + let phys = entry.frame().unwrap().start_address(); + let virt = phys.as_u64() + boot_info.physical_memory_offset; + let ptr = VirtAddr::new(virt).as_mut_ptr(); + let l3_table: &PageTable = unsafe { &*ptr }; + + // imprime entradas não vazias da tabela de nível 3 + for (i, entry) in l3_table.iter().enumerate() { + if !entry.is_unused() { + println!(" Entrada L3 {}: {:?}", i, entry); + } + } +} +``` + +Para olhar as tabelas de nível 2 e nível 1, repetimos esse processo para as entradas de nível 3 e nível 2. Como você pode imaginar, isso se torna muito verboso muito rapidamente, então não mostramos o código completo aqui. + +Percorrer as tabelas de página manualmente é interessante porque ajuda a entender como a CPU realiza a tradução. No entanto, na maioria das vezes, estamos interessados apenas no endereço físico mapeado para um dado endereço virtual, então vamos criar uma função para isso. + +### Traduzindo Endereços + +Para traduzir um endereço virtual para físico, temos que percorrer a tabela de página de quatro níveis até alcançarmos o frame mapeado. Vamos criar uma função que realiza esta tradução: + +```rust +// em src/memory.rs + +use x86_64::PhysAddr; + +/// Traduz o endereço virtual dado para o endereço físico mapeado, ou +/// `None` se o endereço não está mapeado. +/// +/// Esta função é unsafe porque o chamador deve garantir que a +/// memória física completa está mapeada para memória virtual no +/// `physical_memory_offset` passado. +pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr) + -> Option +{ + translate_addr_inner(addr, physical_memory_offset) +} +``` + +Encaminhamos a função para uma função `translate_addr_inner` segura para limitar o escopo de `unsafe`. Como notamos acima, Rust trata o corpo completo de uma `unsafe fn` como um grande bloco unsafe. Ao chamar uma função privada segura, tornamos cada operação `unsafe` explícita novamente. + +A função privada interna contém a implementação real: + +```rust +// em src/memory.rs + +/// Função privada que é chamada por `translate_addr`. +/// +/// Esta função é segura para limitar o escopo de `unsafe` porque Rust trata +/// todo o corpo de funções unsafe como um bloco unsafe. Esta função deve +/// ser alcançável apenas através de `unsafe fn` de fora deste módulo. +fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr) + -> Option +{ + use x86_64::structures::paging::page_table::FrameError; + use x86_64::registers::control::Cr3; + + // lê o frame da tabela de nível 4 ativa do registrador CR3 + let (level_4_table_frame, _) = Cr3::read(); + + let table_indexes = [ + addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() + ]; + let mut frame = level_4_table_frame; + + // percorre a tabela de página multinível + for &index in &table_indexes { + // converte o frame em uma referência de tabela de página + let virt = physical_memory_offset + frame.start_address().as_u64(); + let table_ptr: *const PageTable = virt.as_ptr(); + let table = unsafe {&*table_ptr}; + + // lê a entrada da tabela de página e atualiza `frame` + let entry = &table[index]; + frame = match entry.frame() { + Ok(frame) => frame, + Err(FrameError::FrameNotPresent) => return None, + Err(FrameError::HugeFrame) => panic!("huge pages não suportadas"), + }; + } + + // calcula o endereço físico adicionando o deslocamento de página + Some(frame.start_address() + u64::from(addr.page_offset())) +} +``` + +Em vez de reutilizar nossa função `active_level_4_table`, lemos o frame de nível 4 do registrador `CR3` novamente. Fazemos isso porque isso simplifica esta implementação de protótipo. Não se preocupe, criaremos uma solução melhor em um momento. + +A struct `VirtAddr` já fornece métodos para computar os índices nas tabelas de página dos quatro níveis. Armazenamos esses índices em um pequeno array porque isso nos permite percorrer as tabelas de página usando um loop `for`. Fora do loop, lembramos do último `frame` visitado para calcular o endereço físico mais tarde. O `frame` aponta para frames de tabela de página enquanto itera e para o frame mapeado após a última iteração, isto é, após seguir a entrada de nível 1. + +Dentro do loop, novamente usamos o `physical_memory_offset` para converter o frame em uma referência de tabela de página. Então lemos a entrada da tabela de página atual e usamos a função [`PageTableEntry::frame`] para recuperar o frame mapeado. Se a entrada não está mapeada para um frame, retornamos `None`. Se a entrada mapeia uma huge page de 2 MiB ou 1 GiB, entramos em panic por enquanto. + +[`PageTableEntry::frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame + +Vamos testar nossa função de tradução traduzindo alguns endereços: + +```rust +// em src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // nova importação + use blog_os::memory::translate_addr; + + […] // hello world e blog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + + let addresses = [ + // a página do buffer vga com identity mapping + 0xb8000, + // alguma página de código + 0x201008, + // alguma página de pilha + 0x0100_0020_1a10, + // endereço virtual mapeado para endereço físico 0 + boot_info.physical_memory_offset, + ]; + + for &address in &addresses { + let virt = VirtAddr::new(address); + let phys = unsafe { translate_addr(virt, phys_mem_offset) }; + println!("{:?} -> {:?}", virt, phys); + } + + […] // test_main(), impressão "não crashou", e hlt_loop() +} +``` + +Quando o executamos, vemos a seguinte saída: + +![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, "panicked at 'huge pages não suportadas'](qemu-translate-addr.png) + +Como esperado, o endereço com identity mapping `0xb8000` traduz para o mesmo endereço físico. A página de código e a página de pilha traduzem para alguns endereços físicos arbitrários, que dependem de como o bootloader criou o mapeamento inicial para nosso kernel. Vale notar que os últimos 12 bits sempre permanecem os mesmos após a tradução, o que faz sentido porque esses bits são o [_deslocamento de página_] e não fazem parte da tradução. + +[_deslocamento de página_]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 + +Como cada endereço físico pode ser acessado adicionando o `physical_memory_offset`, a tradução do próprio endereço `physical_memory_offset` deveria apontar para o endereço físico `0`. No entanto, a tradução falha porque o mapeamento usa huge pages para eficiência, o que não é suportado em nossa implementação ainda. + +### Usando `OffsetPageTable` + +Traduzir endereços virtuais para físicos é uma tarefa comum em um kernel de SO, portanto a crate `x86_64` fornece uma abstração para isso. A implementação já suporta huge pages e várias outras funções de tabela de página além de `translate_addr`, então a usaremos no seguinte em vez de adicionar suporte a huge pages à nossa própria implementação. + +Na base da abstração estão duas traits que definem várias funções de mapeamento de tabela de página: + +- A trait [`Mapper`] é genérica sobre o tamanho da página e fornece funções que operam em páginas. Exemplos são [`translate_page`], que traduz uma dada página para um frame do mesmo tamanho, e [`map_to`], que cria um novo mapeamento na tabela de página. +- A trait [`Translate`] fornece funções que trabalham com múltiplos tamanhos de página, como [`translate_addr`] ou a [`translate`] geral. + +[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`translate_page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to +[`Translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html +[`translate_addr`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr +[`translate`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Translate.html#tymethod.translate + +As traits apenas definem a interface, elas não fornecem nenhuma implementação. A crate `x86_64` atualmente fornece três tipos que implementam as traits com diferentes requisitos. O tipo [`OffsetPageTable`] assume que a memória física completa está mapeada para o espaço de endereço virtual em algum deslocamento. O [`MappedPageTable`] é um pouco mais flexível: Ele apenas requer que cada frame de tabela de página esteja mapeado para o espaço de endereço virtual em um endereço calculável. Finalmente, o tipo [`RecursivePageTable`] pode ser usado para acessar frames de tabela de página através de [tabelas de página recursivas](#tabelas-de-pagina-recursivas). + +[`OffsetPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html +[`MappedPageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html +[`RecursivePageTable`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html + +No nosso caso, o bootloader mapeia a memória física completa em um endereço virtual especificado pela variável `physical_memory_offset`, então podemos usar o tipo `OffsetPageTable`. Para inicializá-lo, criamos uma nova função `init` em nosso módulo `memory`: + +```rust +use x86_64::structures::paging::OffsetPageTable; + +/// Inicializa um novo OffsetPageTable. +/// +/// Esta função é unsafe porque o chamador deve garantir que a +/// memória física completa está mapeada para memória virtual no +/// `physical_memory_offset` passado. Além disso, esta função deve ser chamada apenas uma vez +/// para evitar referenciar `&mut` com aliasing (que é comportamento indefinido). +pub unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static> { + unsafe { + let level_4_table = active_level_4_table(physical_memory_offset); + OffsetPageTable::new(level_4_table, physical_memory_offset) + } +} + +// torna privada +unsafe fn active_level_4_table(physical_memory_offset: VirtAddr) + -> &'static mut PageTable +{…} +``` + +A função recebe o `physical_memory_offset` como argumento e retorna uma nova instância `OffsetPageTable` com um tempo de vida `'static`. Isso significa que a instância permanece válida pela execução completa do nosso kernel. No corpo da função, primeiro chamamos a função `active_level_4_table` para recuperar uma referência mutável à tabela de página de nível 4. Então invocamos a função [`OffsetPageTable::new`] com esta referência. Como segundo parâmetro, a função `new` espera o endereço virtual no qual o mapeamento da memória física começa, que é dado na variável `physical_memory_offset`. + +[`OffsetPageTable::new`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new + +A função `active_level_4_table` deve ser chamada apenas da função `init` a partir de agora porque pode facilmente levar a referências mutáveis com aliasing quando chamada múltiplas vezes, o que pode causar comportamento indefinido. Por esta razão, tornamos a função privada removendo o especificador `pub`. + +Agora podemos usar o método `Translate::translate_addr` em vez de nossa própria função `memory::translate_addr`. Precisamos mudar apenas algumas linhas em nosso `kernel_main`: + +```rust +// em src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // novo: importações diferentes + use blog_os::memory; + use x86_64::{structures::paging::Translate, VirtAddr}; + + […] // hello world e blog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + // novo: inicializa um mapper + let mapper = unsafe { memory::init(phys_mem_offset) }; + + let addresses = […]; // mesmo de antes + + for &address in &addresses { + let virt = VirtAddr::new(address); + // novo: use o método `mapper.translate_addr` + let phys = mapper.translate_addr(virt); + println!("{:?} -> {:?}", virt, phys); + } + + […] // test_main(), impressão "não crashou", e hlt_loop() +} +``` + +Precisamos importar a trait `Translate` para usar o método [`translate_addr`] que ela fornece. + +Quando o executamos agora, vemos os mesmos resultados de tradução de antes, com a diferença de que a tradução de huge page agora também funciona: + +![0xb8000 -> 0xb8000, 0x201008 -> 0x401008, 0x10000201a10 -> 0x279a10, 0x18000000000 -> 0x0](qemu-mapper-translate-addr.png) + +Como esperado, as traduções de `0xb8000` e dos endereços de código e pilha permanecem as mesmas da nossa própria função de tradução. Adicionalmente, agora vemos que o endereço virtual `physical_memory_offset` está mapeado para o endereço físico `0x0`. + +Ao usar a função de tradução do tipo `MappedPageTable`, podemos nos poupar o trabalho de implementar suporte a huge pages. Também temos acesso a outras funções de página, como `map_to`, que usaremos na próxima seção. + +Neste ponto, não precisamos mais de nossas funções `memory::translate_addr` e `memory::translate_addr_inner`, então podemos deletá-las. + +### Criando um Novo Mapeamento + +Até agora, apenas olhamos para as tabelas de página sem modificar nada. Vamos mudar isso criando um novo mapeamento para uma página previamente não mapeada. + +Usaremos a função [`map_to`] da trait [`Mapper`] para nossa implementação, então vamos olhar para essa função primeiro. A documentação nos diz que ela recebe quatro argumentos: a página que queremos mapear, o frame para o qual a página deve ser mapeada, um conjunto de flags para a entrada da tabela de página, e um `frame_allocator`. O frame allocator é necessário porque mapear a página dada pode requerer criar tabelas de página adicionais, que precisam de frames não usados como armazenamento de respaldo. + +[`map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html#tymethod.map_to +[`Mapper`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.Mapper.html + +#### Uma Função `create_example_mapping` + +O primeiro passo de nossa implementação é criar uma nova função `create_example_mapping` que mapeia uma dada página virtual para `0xb8000`, o frame físico do buffer de texto VGA. Escolhemos esse frame porque nos permite facilmente testar se o mapeamento foi criado corretamente: Apenas precisamos escrever na página recém-mapeada e ver se vemos a escrita aparecer na tela. + +A função `create_example_mapping` se parece com isto: + +```rust +// em src/memory.rs + +use x86_64::{ + PhysAddr, + structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator} +}; + +/// Cria um mapeamento de exemplo para a página dada para o frame `0xb8000`. +pub fn create_example_mapping( + page: Page, + mapper: &mut OffsetPageTable, + frame_allocator: &mut impl FrameAllocator, +) { + use x86_64::structures::paging::PageTableFlags as Flags; + + let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); + let flags = Flags::PRESENT | Flags::WRITABLE; + + let map_to_result = unsafe { + // FIXME: isso não é seguro, fazemos apenas para testes + mapper.map_to(page, frame, flags, frame_allocator) + }; + map_to_result.expect("map_to falhou").flush(); +} +``` + +Além da `page` que deve ser mapeada, a função espera uma referência mutável para uma instância `OffsetPageTable` e um `frame_allocator`. O parâmetro `frame_allocator` usa a sintaxe [`impl Trait`][impl-trait-arg] para ser [genérico] sobre todos os tipos que implementam a trait [`FrameAllocator`]. A trait é genérica sobre a trait [`PageSize`] para trabalhar com páginas padrão de 4 KiB e huge pages de 2 MiB/1 GiB. Queremos criar apenas um mapeamento de 4 KiB, então definimos o parâmetro genérico para `Size4KiB`. + +[impl-trait-arg]: https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters +[genérico]: https://doc.rust-lang.org/book/ch10-00-generics.html +[`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html +[`PageSize`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/trait.PageSize.html + +O método [`map_to`] é unsafe porque o chamador deve garantir que o frame ainda não está em uso. A razão para isso é que mapear o mesmo frame duas vezes poderia resultar em comportamento indefinido, por exemplo, quando duas diferentes referências `&mut` apontam para a mesma localização de memória física. No nosso caso, reutilizamos o frame do buffer de texto VGA, que já está mapeado, então quebramos a condição necessária. No entanto, a função `create_example_mapping` é apenas uma função de teste temporária e será removida após esta postagem, então está ok. Para nos lembrar da insegurança, colocamos um comentário `FIXME` na linha. + +Além da `page` e do `unused_frame`, o método `map_to` recebe um conjunto de flags para o mapeamento e uma referência ao `frame_allocator`, que será explicado em um momento. Para as flags, definimos a flag `PRESENT` porque ela é necessária para todas as entradas válidas e a flag `WRITABLE` para tornar a página mapeada gravável. Para uma lista de todas as flags possíveis, veja a seção [_Formato da Tabela de Página_] da postagem anterior. + +[_Formato da Tabela de Página_]: @/edition-2/posts/08-paging-introduction/index.md#page-table-format + +O método [`map_to`] pode falhar, então retorna um [`Result`]. Como este é apenas algum código de exemplo que não precisa ser robusto, apenas usamos [`expect`] para entrar em panic quando ocorre um erro. Em sucesso, a função retorna um tipo [`MapperFlush`] que fornece uma forma fácil de esvaziar a página recém-mapeada do translation lookaside buffer (TLB) com seu método [`flush`]. Como `Result`, o tipo usa o atributo [`#[must_use]`][must_use] para emitir um aviso se acidentalmente esquecermos de usá-lo. + +[`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html +[`expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect +[`MapperFlush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[`flush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush +[must_use]: https://doc.rust-lang.org/std/result/#results-must-be-used + +#### Um `FrameAllocator` Fictício + +Para poder chamar `create_example_mapping`, precisamos criar um tipo que implemente a trait `FrameAllocator` primeiro. Como notado acima, a trait é responsável por alocar frames para novas tabelas de página se elas são necessárias pelo `map_to`. + +Vamos começar com o caso simples e assumir que não precisamos criar novas tabelas de página. Para este caso, um frame allocator que sempre retorna `None` é suficiente. Criamos tal `EmptyFrameAllocator` para testar nossa função de mapeamento: + +```rust +// em src/memory.rs + +/// Um FrameAllocator que sempre retorna `None`. +pub struct EmptyFrameAllocator; + +unsafe impl FrameAllocator for EmptyFrameAllocator { + fn allocate_frame(&mut self) -> Option { + None + } +} +``` + +Implementar o `FrameAllocator` é unsafe porque o implementador deve garantir que o allocator retorna apenas frames não usados. Caso contrário, comportamento indefinido pode ocorrer, por exemplo, quando duas páginas virtuais são mapeadas para o mesmo frame físico. Nosso `EmptyFrameAllocator` apenas retorna `None`, então isso não é um problema neste caso. + +#### Escolhendo uma Página Virtual + +Agora temos um frame allocator simples que podemos passar para nossa função `create_example_mapping`. No entanto, o allocator sempre retorna `None`, então isso só funcionará se nenhum frame de tabela de página adicional for necessário para criar o mapeamento. Para entender quando frames de tabela de página adicionais são necessários e quando não, vamos considerar um exemplo: + +![A virtual and a physical address space with a single mapped page and the page tables of all four levels](required-page-frames-example.svg) + +O gráfico mostra o espaço de endereço virtual à esquerda, o espaço de endereço físico à direita, e as tabelas de página entre eles. As tabelas de página são armazenadas em frames de memória física, indicados pelas linhas tracejadas. O espaço de endereço virtual contém uma única página mapeada no endereço `0x803fe00000`, marcada em azul. Para traduzir esta página para seu frame, a CPU percorre a tabela de página de 4 níveis até alcançar o frame no endereço 36 KiB. + +Adicionalmente, o gráfico mostra o frame físico do buffer de texto VGA em vermelho. Nosso objetivo é mapear uma página virtual previamente não mapeada para este frame usando nossa função `create_example_mapping`. Como nosso `EmptyFrameAllocator` sempre retorna `None`, queremos criar o mapeamento de forma que nenhum frame adicional seja necessário do allocator. Isso depende da página virtual que selecionamos para o mapeamento. + +O gráfico mostra duas páginas candidatas no espaço de endereço virtual, ambas marcadas em amarelo. Uma página está no endereço `0x803fdfd000`, que é 3 páginas antes da página mapeada (em azul). Enquanto os índices de tabela de página de nível 4 e nível 3 são os mesmos da página azul, os índices de nível 2 e nível 1 são diferentes (veja a [postagem anterior][page-table-indices]). O índice diferente na tabela de nível 2 significa que uma tabela de nível 1 diferente é usada para esta página. Como esta tabela de nível 1 ainda não existe, precisaríamos criá-la se escolhêssemos aquela página para nosso mapeamento de exemplo, o que requereria um frame físico não usado adicional. Em contraste, a segunda página candidata no endereço `0x803fe02000` não tem este problema porque usa a mesma tabela de página de nível 1 que a página azul. Assim, todas as tabelas de página necessárias já existem. + +[page-table-indices]: @/edition-2/posts/08-paging-introduction/index.md#paging-on-x86-64 + +Em resumo, a dificuldade de criar um novo mapeamento depende da página virtual que queremos mapear. No caso mais fácil, a tabela de página de nível 1 para a página já existe e apenas precisamos escrever uma única entrada. No caso mais difícil, a página está em uma região de memória para a qual ainda não existe nível 3, então precisamos criar novas tabelas de página de nível 3, nível 2 e nível 1 primeiro. + +Para chamar nossa função `create_example_mapping` com o `EmptyFrameAllocator`, precisamos escolher uma página para a qual todas as tabelas de página já existem. Para encontrar tal página, podemos utilizar o fato de que o bootloader se carrega no primeiro megabyte do espaço de endereço virtual. Isso significa que uma tabela de nível 1 válida existe para todas as páginas nesta região. Assim, podemos escolher qualquer página não usada nesta região de memória para nosso mapeamento de exemplo, como a página no endereço `0`. Normalmente, esta página deveria permanecer não usada para garantir que desreferenciar um ponteiro nulo cause um page fault, então sabemos que o bootloader a deixa não mapeada. + +#### Criando o Mapeamento + +Agora temos todos os parâmetros necessários para chamar nossa função `create_example_mapping`, então vamos modificar nossa função `kernel_main` para mapear a página no endereço virtual `0`. Como mapeamos a página para o frame do buffer de texto VGA, deveríamos ser capazes de escrever na tela através dela depois. A implementação se parece com isto: + +```rust +// em src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory; + use x86_64::{structures::paging::Page, VirtAddr}; // nova importação + + […] // hello world e blog_os::init + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let mut mapper = unsafe { memory::init(phys_mem_offset) }; + let mut frame_allocator = memory::EmptyFrameAllocator; + + // mapeia uma página não usada + let page = Page::containing_address(VirtAddr::new(0)); + memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); + + // escreve a string `New!` na tela através do novo mapeamento + let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); + unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; + + […] // test_main(), impressão "não crashou", e hlt_loop() +} +``` + +Primeiro, criamos o mapeamento para a página no endereço `0` chamando nossa função `create_example_mapping` com referências mutáveis às instâncias `mapper` e `frame_allocator`. Isso mapeia a página para o frame do buffer de texto VGA, então deveríamos ver qualquer escrita a ela na tela. + +Então, convertemos a página para um ponteiro bruto e escrevemos um valor no deslocamento `400`. Não escrevemos no início da página porque a linha superior do buffer VGA é diretamente deslocada para fora da tela pelo próximo `println`. Escrevemos o valor `0x_f021_f077_f065_f04e`, que representa a string _"New!"_ em um fundo branco. Como aprendemos [na postagem _"Modo de Texto VGA"_], escritas no buffer VGA devem ser voláteis, então usamos o método [`write_volatile`]. + +[na postagem _"Modo de Texto VGA"_]: @/edition-2/posts/03-vga-text-buffer/index.md#volatile +[`write_volatile`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile + +Quando o executamos no QEMU, vemos a seguinte saída: + +![QEMU printing "Não crashou!" with four completely white cells in the middle of the screen](qemu-new-mapping.png) + +O _"New!"_ na tela é causado por nossa escrita na página `0`, o que significa que criamos com sucesso um novo mapeamento nas tabelas de página. + +Criar aquele mapeamento só funcionou porque a tabela de nível 1 responsável pela página no endereço `0` já existe. Quando tentamos mapear uma página para a qual não existe tabela de nível 1 ainda, a função `map_to` falha porque tenta criar novas tabelas de página alocando frames com o `EmptyFrameAllocator`. Podemos ver isso acontecer quando tentamos mapear a página `0xdeadbeaf000` em vez de `0`: + +```rust +// em src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + […] + let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); + […] +} +``` + +Quando o executamos, um panic com a seguinte mensagem de erro ocorre: + +``` +panicked at 'map_to falhou: FrameAllocationFailed', /…/result.rs:999:5 +``` + +Para mapear páginas que ainda não têm uma tabela de página de nível 1, precisamos criar um `FrameAllocator` apropriado. Mas como sabemos quais frames não estão usados e quanta memória física está disponível? + +### Alocando Frames + +Para criar novas tabelas de página, precisamos criar um frame allocator apropriado. Para fazer isso, usamos o `memory_map` que é passado pelo bootloader como parte da struct `BootInfo`: + +```rust +// em src/memory.rs + +use bootloader::bootinfo::MemoryMap; + +/// Um FrameAllocator que retorna frames utilizáveis do mapa de memória do bootloader. +pub struct BootInfoFrameAllocator { + memory_map: &'static MemoryMap, + next: usize, +} + +impl BootInfoFrameAllocator { + /// Cria um FrameAllocator do mapa de memória passado. + /// + /// Esta função é unsafe porque o chamador deve garantir que o mapa de memória + /// passado é válido. O requisito principal é que todos os frames que são marcados + /// como `USABLE` nele estejam realmente não usados. + pub unsafe fn init(memory_map: &'static MemoryMap) -> Self { + BootInfoFrameAllocator { + memory_map, + next: 0, + } + } +} +``` + +A struct tem dois campos: Uma referência `'static` ao mapa de memória passado pelo bootloader e um campo `next` que mantém rastro do número do próximo frame que o allocator deve retornar. + +Como explicamos na seção [_Boot Information_](#boot-information), o mapa de memória é fornecido pelo firmware BIOS/UEFI. Ele pode ser consultado apenas muito cedo no processo de boot, então o bootloader já chama as respectivas funções para nós. O mapa de memória consiste de uma lista de structs [`MemoryRegion`], que contêm o endereço inicial, o comprimento, e o tipo (por exemplo, não usado, reservado, etc.) de cada região de memória. + +A função `init` inicializa um `BootInfoFrameAllocator` com um dado mapa de memória. O campo `next` é inicializado com `0` e será aumentado para cada alocação de frame para evitar retornar o mesmo frame duas vezes. Como não sabemos se os frames utilizáveis do mapa de memória já foram usados em outro lugar, nossa função `init` deve ser `unsafe` para requerer garantias adicionais do chamador. + +#### Um Método `usable_frames` + +Antes de implementarmos a trait `FrameAllocator`, adicionamos um método auxiliar que converte o mapa de memória em um iterador de frames utilizáveis: + +```rust +// em src/memory.rs + +use bootloader::bootinfo::MemoryRegionType; + +impl BootInfoFrameAllocator { + /// Retorna um iterador sobre os frames utilizáveis especificados no mapa de memória. + fn usable_frames(&self) -> impl Iterator { + // obtém regiões utilizáveis do mapa de memória + let regions = self.memory_map.iter(); + let usable_regions = regions + .filter(|r| r.region_type == MemoryRegionType::Usable); + // mapeia cada região para seu intervalo de endereços + let addr_ranges = usable_regions + .map(|r| r.range.start_addr()..r.range.end_addr()); + // transforma em um iterador de endereços iniciais de frame + let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096)); + // cria tipos `PhysFrame` dos endereços iniciais + frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr))) + } +} +``` + +Esta função usa métodos combinadores de iterador para transformar o `MemoryMap` inicial em um iterador de frames físicos utilizáveis: + +- Primeiro, chamamos o método `iter` para converter o mapa de memória em um iterador de [`MemoryRegion`]s. +- Então usamos o método [`filter`] para pular qualquer região reservada ou de outra forma indisponível. O bootloader atualiza o mapa de memória para todos os mapeamentos que cria, então frames que são usados por nosso kernel (código, dados, ou pilha) ou para armazenar a boot information já estão marcados como `InUse` ou similar. Assim, podemos ter certeza de que frames `Usable` não são usados em outro lugar. +- Depois, usamos o combinador [`map`] e a [sintaxe de range] do Rust para transformar nosso iterador de regiões de memória em um iterador de intervalos de endereços. +- Em seguida, usamos [`flat_map`] para transformar os intervalos de endereços em um iterador de endereços iniciais de frame, escolhendo cada 4096º endereço usando [`step_by`]. Como 4096 bytes (= 4 KiB) é o tamanho da página, obtemos o endereço inicial de cada frame. O bootloader alinha todas as áreas de memória utilizáveis por página, então não precisamos de nenhum código de alinhamento ou arredondamento aqui. Ao usar [`flat_map`] em vez de `map`, obtemos um `Iterator` em vez de um `Iterator>`. +- Finalmente, convertemos os endereços iniciais para tipos `PhysFrame` para construir um `Iterator`. + +[`MemoryRegion`]: https://docs.rs/bootloader/0.6.4/bootloader/bootinfo/struct.MemoryRegion.html +[`filter`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.filter +[`map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.map +[sintaxe de range]: https://doc.rust-lang.org/core/ops/struct.Range.html +[`step_by`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.step_by +[`flat_map`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.flat_map + +O tipo de retorno da função usa a feature [`impl Trait`]. Desta forma, podemos especificar que retornamos algum tipo que implementa a trait [`Iterator`] com tipo de item `PhysFrame` mas não precisamos nomear o tipo de retorno concreto. Isso é importante aqui porque não _podemos_ nomear o tipo concreto já que ele depende de tipos de closure não nomeáveis. + +[`impl Trait`]: https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits +[`Iterator`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html + +#### Implementando a Trait `FrameAllocator` + +Agora podemos implementar a trait `FrameAllocator`: + +```rust +// em src/memory.rs + +unsafe impl FrameAllocator for BootInfoFrameAllocator { + fn allocate_frame(&mut self) -> Option { + let frame = self.usable_frames().nth(self.next); + self.next += 1; + frame + } +} +``` + +Primeiro usamos o método `usable_frames` para obter um iterador de frames utilizáveis do mapa de memória. Então, usamos a função [`Iterator::nth`] para obter o frame com índice `self.next` (pulando assim `(self.next - 1)` frames). Antes de retornar aquele frame, aumentamos `self.next` em um para que retornemos o frame seguinte na próxima chamada. + +[`Iterator::nth`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.nth + +Esta implementação não é totalmente ideal, já que ela recria o allocator `usable_frame` em cada alocação. Seria melhor armazenar diretamente o iterador como um campo de struct em vez disso. Então não precisaríamos do método `nth` e poderíamos apenas chamar [`next`] em cada alocação. O problema com esta abordagem é que não é possível armazenar um tipo `impl Trait` em um campo de struct atualmente. Pode funcionar algum dia quando [_named existential types_] estiverem totalmente implementados. + +[`next`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#tymethod.next +[_named existential types_]: https://github.com/rust-lang/rfcs/pull/2071 + +#### Usando o `BootInfoFrameAllocator` + +Agora podemos modificar nossa função `kernel_main` para passar uma instância `BootInfoFrameAllocator` em vez de um `EmptyFrameAllocator`: + +```rust +// em src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::memory::BootInfoFrameAllocator; + […] + let mut frame_allocator = unsafe { + BootInfoFrameAllocator::init(&boot_info.memory_map) + }; + […] +} +``` + +Com o boot info frame allocator, o mapeamento tem sucesso e vemos o _"New!"_ preto-sobre-branco na tela novamente. Por trás das cortinas, o método `map_to` cria as tabelas de página faltantes da seguinte forma: + +- Use o `frame_allocator` passado para alocar um frame não usado. +- Zera o frame para criar uma nova tabela de página vazia. +- Mapeia a entrada da tabela de nível mais alto para aquele frame. +- Continua com o próximo nível de tabela. + +Embora nossa função `create_example_mapping` seja apenas algum código de exemplo, agora somos capazes de criar novos mapeamentos para páginas arbitrárias. Isso será essencial para alocar memória ou implementar multithreading em postagens futuras. + +Neste ponto, devemos deletar a função `create_example_mapping` novamente para evitar acidentalmente invocar comportamento indefinido, como explicado [acima](#uma-funcao-create-example-mapping). + +## Resumo + +Nesta postagem, aprendemos sobre diferentes técnicas para acessar os frames físicos das tabelas de página, incluindo identity mapping, mapeamento da memória física completa, mapeamento temporário, e tabelas de página recursivas. Escolhemos mapear a memória física completa, já que é simples, portável e poderosa. + +Não podemos mapear a memória física do nosso kernel sem acesso à tabela de página, então precisamos de suporte do bootloader. A crate `bootloader` suporta criar o mapeamento necessário através de cargo crate features opcionais. Ela passa a informação necessária para nosso kernel na forma de um argumento `&BootInfo` para nossa função de ponto de entrada. + +Para nossa implementação, primeiro percorremos manualmente as tabelas de página para implementar uma função de tradução, e então usamos o tipo `MappedPageTable` da crate `x86_64`. Também aprendemos como criar novos mapeamentos na tabela de página e como criar o `FrameAllocator` necessário em cima do mapa de memória passado pelo bootloader. + +## O Que Vem a Seguir? + +A próxima postagem criará uma região de memória heap para nosso kernel, o que nos permitirá [alocar memória] e usar vários [tipos de coleção]. + +[alocar memória]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html +[tipos de coleção]: https://doc.rust-lang.org/alloc/collections/index.html \ No newline at end of file diff --git a/blog/content/edition-2/posts/10-heap-allocation/index.pt-BR.md b/blog/content/edition-2/posts/10-heap-allocation/index.pt-BR.md new file mode 100644 index 00000000..7c9ea490 --- /dev/null +++ b/blog/content/edition-2/posts/10-heap-allocation/index.pt-BR.md @@ -0,0 +1,769 @@ ++++ +title = "Alocação no Heap" +weight = 10 +path = "pt-BR/heap-allocation" +date = 2019-06-26 + +[extra] +chapter = "Gerenciamento de Memória" +# Please update this when updating the translation +translation_based_on_commit = "1ba06fe61c39c1379bd768060c21040b62ff3f0b" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Este post adiciona suporte para alocação no heap ao nosso kernel. Primeiro, ele fornece uma introdução à memória dinâmica e mostra como o verificador de empréstimos previne erros comuns de alocação. Em seguida, implementa a interface básica de alocação do Rust, cria uma região de memória heap e configura uma crate de alocador. Ao final deste post, todos os tipos de alocação e coleção da crate embutida `alloc` estarão disponíveis para o nosso kernel. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou pergunta, por favor abra uma issue lá. Você também pode deixar comentários [no final]. O código-fonte completo para este post pode ser encontrado no branch [`post-10`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[at the bottom]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-10 + + + +## Variáveis Locais e Estáticas + +Atualmente usamos dois tipos de variáveis em nosso kernel: variáveis locais e variáveis `static`. Variáveis locais são armazenadas na [pilha de chamadas] e são válidas apenas até que a função circundante retorne. Variáveis estáticas são armazenadas em um local de memória fixo e sempre vivem pela duração completa do programa. + +### Variáveis Locais + +Variáveis locais são armazenadas na [pilha de chamadas], que é uma [estrutura de dados de pilha] que suporta operações de `push` e `pop`. Em cada entrada de função, os parâmetros, o endereço de retorno e as variáveis locais da função chamada são colocados na pilha pelo compilador: + +[pilha de chamadas]: https://en.wikipedia.org/wiki/Call_stack +[estrutura de dados de pilha]: https://en.wikipedia.org/wiki/Stack_(abstract_data_type) + +![Uma função `outer()` e uma função `inner(i: usize)`, onde `outer` chama `inner(1)`. Ambas têm algumas variáveis locais. A pilha de chamadas contém os seguintes slots: as variáveis locais de outer, então o argumento `i = 1`, então o endereço de retorno, então as variáveis locais de inner.](call-stack.svg) + +O exemplo acima mostra a pilha de chamadas depois que a função `outer` chamou a função `inner`. Vemos que a pilha de chamadas contém as variáveis locais de `outer` primeiro. Na chamada de `inner`, o parâmetro `1` e o endereço de retorno da função foram colocados na pilha. Então o controle foi transferido para `inner`, que colocou suas variáveis locais na pilha. + +Depois que a função `inner` retorna, sua parte da pilha de chamadas é removida novamente e apenas as variáveis locais de `outer` permanecem: + +![A pilha de chamadas contendo apenas as variáveis locais de `outer`](call-stack-return.svg) + +Vemos que as variáveis locais de `inner` vivem apenas até a função retornar. O compilador Rust impõe esses tempos de vida e gera um erro quando usamos um valor por muito tempo, por exemplo, quando tentamos retornar uma referência a uma variável local: + +```rust +fn inner(i: usize) -> &'static u32 { + let z = [1, 2, 3]; + &z[i] +} +``` + +([execute o exemplo no playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=6186a0f3a54f468e1de8894996d12819)) + +Embora retornar uma referência não faça sentido neste exemplo, há casos em que queremos que uma variável viva mais tempo do que a função. Já vimos tal caso em nosso kernel quando tentamos [carregar uma tabela de descritores de interrupção] e tivemos que usar uma variável `static` para estender o tempo de vida. + +[carregar uma tabela de descritores de interrupção]: @/edition-2/posts/05-cpu-exceptions/index.md#loading-the-idt + +### Variáveis Estáticas + +Variáveis estáticas são armazenadas em um local de memória fixo separado da pilha. Este local de memória é atribuído em tempo de compilação pelo linker e codificado no executável. Variáveis estáticas vivem pela duração completa de execução do programa, então têm o tempo de vida `'static` e sempre podem ser referenciadas de variáveis locais: + +![O mesmo exemplo outer/inner, exceto que inner tem um `static Z: [u32; 3] = [1,2,3];` e retorna uma referência `&Z[i]`](call-stack-static.svg) + +Quando a função `inner` retorna no exemplo acima, sua parte da pilha de chamadas é destruída. As variáveis estáticas vivem em um intervalo de memória separado que nunca é destruído, então a referência `&Z[1]` ainda é válida após o retorno. + +Além do tempo de vida `'static`, variáveis estáticas também têm a propriedade útil de que sua localização é conhecida em tempo de compilação, de modo que nenhuma referência é necessária para acessá-las. Utilizamos essa propriedade para nossa macro `println`: Ao usar um [`Writer` estático] internamente, nenhuma referência `&mut Writer` é necessária para invocar a macro, o que é muito útil em [manipuladores de exceção], onde não temos acesso a variáveis adicionais. + +[`Writer` estático]: @/edition-2/posts/03-vga-text-buffer/index.md#a-global-interface +[manipuladores de exceção]: @/edition-2/posts/05-cpu-exceptions/index.md#implementation + +No entanto, essa propriedade de variáveis estáticas traz uma desvantagem crucial: elas são somente leitura por padrão. Rust impõe isso porque uma [corrida de dados] ocorreria se, por exemplo, duas threads modificassem uma variável estática ao mesmo tempo. A única maneira de modificar uma variável estática é encapsulá-la em um tipo [`Mutex`], que garante que apenas uma referência `&mut` exista em qualquer momento. Já usamos um `Mutex` para nosso [`Writer` estático do buffer VGA][vga mutex]. + +[corrida de dados]: https://doc.rust-lang.org/nomicon/races.html +[`Mutex`]: https://docs.rs/spin/0.5.2/spin/struct.Mutex.html +[vga mutex]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks + +## Memória Dinâmica + +Variáveis locais e estáticas já são muito poderosas juntas e permitem a maioria dos casos de uso. No entanto, vimos que ambas têm suas limitações: + +- Variáveis locais vivem apenas até o final da função ou bloco circundante. Isso ocorre porque elas vivem na pilha de chamadas e são destruídas depois que a função circundante retorna. +- Variáveis estáticas sempre vivem pela duração completa de execução do programa, então não há maneira de recuperar e reutilizar sua memória quando não são mais necessárias. Além disso, elas têm semântica de propriedade pouco clara e são acessíveis de todas as funções, então precisam ser protegidas por um [`Mutex`] quando queremos modificá-las. + +Outra limitação de variáveis locais e estáticas é que elas têm um tamanho fixo. Então elas não podem armazenar uma coleção que cresce dinamicamente quando mais elementos são adicionados. (Existem propostas para [rvalues não dimensionados] em Rust que permitiriam variáveis locais de tamanho dinâmico, mas eles só funcionam em alguns casos específicos.) + +[rvalues não dimensionados]: https://github.com/rust-lang/rust/issues/48055 + +Para contornar essas desvantagens, linguagens de programação frequentemente suportam uma terceira região de memória para armazenar variáveis chamada **heap**. O heap suporta _alocação de memória dinâmica_ em tempo de execução através de duas funções chamadas `allocate` e `deallocate`. Funciona da seguinte maneira: A função `allocate` retorna um pedaço livre de memória do tamanho especificado que pode ser usado para armazenar uma variável. Esta variável então vive até ser liberada chamando a função `deallocate` com uma referência à variável. + +Vamos passar por um exemplo: + +![A função inner chama `allocate(size_of([u32; 3]))`, escreve `z.write([1,2,3]);`, e retorna `(z as *mut u32).offset(i)`. No valor retornado `y`, a função outer realiza `deallocate(y, size_of(u32))`.](call-stack-heap.svg) + +Aqui a função `inner` usa memória heap em vez de variáveis estáticas para armazenar `z`. Primeiro ela aloca um bloco de memória do tamanho necessário, que retorna um [ponteiro bruto] `*mut u32`. Em seguida, usa o método [`ptr::write`] para escrever o array `[1,2,3]` nele. No último passo, usa a função [`offset`] para calcular um ponteiro para o `i`-ésimo elemento e então o retorna. (Note que omitimos alguns casts e blocos unsafe necessários nesta função de exemplo por brevidade.) + +[ponteiro bruto]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer +[`ptr::write`]: https://doc.rust-lang.org/core/ptr/fn.write.html +[`offset`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset + +A memória alocada vive até ser explicitamente liberada através de uma chamada para `deallocate`. Assim, o ponteiro retornado ainda é válido mesmo depois que `inner` retornou e sua parte da pilha de chamadas foi destruída. A vantagem de usar memória heap comparada à memória estática é que a memória pode ser reutilizada depois de ser liberada, o que fazemos através da chamada `deallocate` em `outer`. Depois dessa chamada, a situação se parece com isso: + +![A pilha de chamadas contém as variáveis locais de `outer`, o heap contém `z[0]` e `z[2]`, mas não mais `z[1]`.](call-stack-heap-freed.svg) + +Vemos que o slot `z[1]` está livre novamente e pode ser reutilizado para a próxima chamada `allocate`. No entanto, também vemos que `z[0]` e `z[2]` nunca são liberados porque nunca os desalocamos. Tal bug é chamado de _vazamento de memória_ e é frequentemente a causa do consumo excessivo de memória de programas (imagine apenas o que acontece quando chamamos `inner` repetidamente em um loop). Isso pode parecer ruim, mas existem tipos muito mais perigosos de bugs que podem acontecer com alocação dinâmica. + +### Erros Comuns + +Além de vazamentos de memória, que são lamentáveis mas não tornam o programa vulnerável a atacantes, existem dois tipos comuns de bugs com consequências mais graves: + +- Quando acidentalmente continuamos a usar uma variável depois de chamar `deallocate` nela, temos uma chamada vulnerabilidade **use-after-free**. Tal bug causa comportamento indefinido e pode frequentemente ser explorado por atacantes para executar código arbitrário. +- Quando acidentalmente liberamos uma variável duas vezes, temos uma vulnerabilidade **double-free**. Isso é problemático porque pode liberar uma alocação diferente que foi alocada no mesmo local após a primeira chamada `deallocate`. Assim, pode levar a uma vulnerabilidade use-after-free novamente. + +Esses tipos de vulnerabilidades são comumente conhecidos, então pode-se esperar que as pessoas tenham aprendido como evitá-los até agora. Mas não, tais vulnerabilidades ainda são encontradas regularmente, por exemplo esta [vulnerabilidade use-after-free no Linux][linux vulnerability] (2019), que permitiu execução de código arbitrário. Uma busca na web como `use-after-free linux {ano atual}` provavelmente sempre produzirá resultados. Isso mostra que mesmo os melhores programadores nem sempre são capazes de lidar corretamente com memória dinâmica em projetos complexos. + +[linux vulnerability]: https://securityboulevard.com/2019/02/linux-use-after-free-vulnerability-found-in-linux-2-6-through-4-20-11/ + +Para evitar esses problemas, muitas linguagens, como Java ou Python, gerenciam memória dinâmica automaticamente usando uma técnica chamada [_coleta de lixo_]. A ideia é que o programador nunca invoca `deallocate` manualmente. Em vez disso, o programa é regularmente pausado e escaneado em busca de variáveis heap não utilizadas, que são então automaticamente desalocadas. Assim, as vulnerabilidades acima nunca podem ocorrer. As desvantagens são a sobrecarga de desempenho do escaneamento regular e os tempos de pausa provavelmente longos. + +[_coleta de lixo_]: https://en.wikipedia.org/wiki/Garbage_collection_(computer_science) + +Rust adota uma abordagem diferente para o problema: Ele usa um conceito chamado [_propriedade_] que é capaz de verificar a correção das operações de memória dinâmica em tempo de compilação. Assim, nenhuma coleta de lixo é necessária para evitar as vulnerabilidades mencionadas, o que significa que não há sobrecarga de desempenho. Outra vantagem dessa abordagem é que o programador ainda tem controle refinado sobre o uso de memória dinâmica, assim como com C ou C++. + +[_propriedade_]: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html + +### Alocações em Rust + +Em vez de deixar o programador chamar `allocate` e `deallocate` manualmente, a biblioteca padrão do Rust fornece tipos de abstração que chamam essas funções implicitamente. O tipo mais importante é [**`Box`**], que é uma abstração para um valor alocado no heap. Ele fornece uma função construtora [`Box::new`] que recebe um valor, chama `allocate` com o tamanho do valor e então move o valor para o slot recém-alocado no heap. Para liberar a memória heap novamente, o tipo `Box` implementa a [trait `Drop`] para chamar `deallocate` quando sai do escopo: + +[**`Box`**]: https://doc.rust-lang.org/std/boxed/index.html +[`Box::new`]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html#method.new +[trait `Drop`]: https://doc.rust-lang.org/book/ch15-03-drop.html + +```rust +{ + let z = Box::new([1,2,3]); + […] +} // z sai do escopo e `deallocate` é chamado +``` + +Esse padrão tem o nome estranho [_aquisição de recurso é inicialização_] (ou _RAII_ abreviado). Ele se originou em C++, onde é usado para implementar um tipo de abstração similar chamado [`std::unique_ptr`]. + +[_aquisição de recurso é inicialização_]: https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization +[`std::unique_ptr`]: https://en.cppreference.com/w/cpp/memory/unique_ptr + +Tal tipo sozinho não é suficiente para prevenir todos os bugs use-after-free, já que programadores ainda podem manter referências depois que o `Box` sai do escopo e o slot de memória heap correspondente é desalocado: + +```rust +let x = { + let z = Box::new([1,2,3]); + &z[1] +}; // z sai do escopo e `deallocate` é chamado +println!("{}", x); +``` + +É aqui que a propriedade do Rust entra. Ela atribui um [tempo de vida] abstrato a cada referência, que é o escopo no qual a referência é válida. No exemplo acima, a referência `x` é retirada do array `z`, então ela se torna inválida depois que `z` sai do escopo. Quando você [executa o exemplo acima no playground][playground-2], você vê que o compilador Rust de fato gera um erro: + +[tempo de vida]: https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html +[playground-2]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=28180d8de7b62c6b4a681a7b1f745a48 + +``` +error[E0597]: `z[_]` does not live long enough + --> src/main.rs:4:9 + | +2 | let x = { + | - borrow later stored here +3 | let z = Box::new([1,2,3]); + | - binding `z` declared here +4 | &z[1] + | ^^^^^ borrowed value does not live long enough +5 | }; // z sai do escopo e `deallocate` é chamado + | - `z[_]` dropped here while still borrowed +``` + +A terminologia pode ser um pouco confusa no início. Pegar uma referência a um valor é chamado de _emprestar_ o valor, já que é similar a um empréstimo na vida real: Você tem acesso temporário a um objeto mas precisa devolvê-lo em algum momento, e você não deve destruí-lo. Ao verificar que todos os empréstimos terminam antes que um objeto seja destruído, o compilador Rust pode garantir que nenhuma situação use-after-free pode ocorrer. + +O sistema de propriedade do Rust vai ainda mais longe, prevenindo não apenas bugs use-after-free mas também fornecendo [_segurança de memória_] completa, como linguagens com coleta de lixo como Java ou Python fazem. Adicionalmente, ele garante [_segurança de thread_] e assim é ainda mais seguro que essas linguagens em código multi-thread. E mais importante, todas essas verificações acontecem em tempo de compilação, então não há sobrecarga em tempo de execução comparado ao gerenciamento de memória manual em C. + +[_segurança de memória_]: https://en.wikipedia.org/wiki/Memory_safety +[_segurança de thread_]: https://en.wikipedia.org/wiki/Thread_safety + +### Casos de Uso + +Agora sabemos o básico de alocação de memória dinâmica em Rust, mas quando devemos usá-la? Chegamos muito longe com nosso kernel sem alocação de memória dinâmica, então por que precisamos dela agora? + +Primeiro, alocação de memória dinâmica sempre vem com um pouco de sobrecarga de desempenho, já que precisamos encontrar um slot livre no heap para cada alocação. Por essa razão, variáveis locais geralmente são preferíveis, especialmente em código kernel sensível ao desempenho. No entanto, existem casos em que alocação de memória dinâmica é a melhor escolha. + +Como regra básica, memória dinâmica é necessária para variáveis que têm um tempo de vida dinâmico ou um tamanho variável. O tipo mais importante com tempo de vida dinâmico é [**`Rc`**], que conta as referências ao seu valor encapsulado e o desaloca depois que todas as referências saíram do escopo. Exemplos de tipos com tamanho variável são [**`Vec`**], [**`String`**] e outros [tipos de coleção] que crescem dinamicamente quando mais elementos são adicionados. Esses tipos funcionam alocando uma quantidade maior de memória quando ficam cheios, copiando todos os elementos e então desalocando a alocação antiga. + +[**`Rc`**]: https://doc.rust-lang.org/alloc/rc/index.html +[**`Vec`**]: https://doc.rust-lang.org/alloc/vec/index.html +[**`String`**]: https://doc.rust-lang.org/alloc/string/index.html +[tipos de coleção]: https://doc.rust-lang.org/alloc/collections/index.html + +Para o nosso kernel, precisaremos principalmente dos tipos de coleção, por exemplo, para armazenar uma lista de tarefas ativas ao implementar multitarefa em posts futuros. + +## A Interface do Alocador + +O primeiro passo na implementação de um alocador heap é adicionar uma dependência na crate embutida [`alloc`]. Como a crate [`core`], ela é um subconjunto da biblioteca padrão que adicionalmente contém os tipos de alocação e coleção. Para adicionar a dependência em `alloc`, adicionamos o seguinte ao nosso `lib.rs`: + +[`alloc`]: https://doc.rust-lang.org/alloc/ +[`core`]: https://doc.rust-lang.org/core/ + +```rust +// em src/lib.rs + +extern crate alloc; +``` + +Ao contrário de dependências normais, não precisamos modificar o `Cargo.toml`. A razão é que a crate `alloc` vem com o compilador Rust como parte da biblioteca padrão, então o compilador já conhece a crate. Ao adicionar esta declaração `extern crate`, especificamos que o compilador deve tentar incluí-la. (Historicamente, todas as dependências precisavam de uma declaração `extern crate`, que agora é opcional). + +Como estamos compilando para um alvo personalizado, não podemos usar a versão pré-compilada de `alloc` que vem com a instalação do Rust. Em vez disso, temos que dizer ao cargo para recompilar a crate a partir do código-fonte. Podemos fazer isso adicionando-a ao array `unstable.build-std` em nosso arquivo `.cargo/config.toml`: + +```toml +# em .cargo/config.toml + +[unstable] +build-std = ["core", "compiler_builtins", "alloc"] +``` + +Agora o compilador irá recompilar e incluir a crate `alloc` em nosso kernel. + +A razão pela qual a crate `alloc` é desabilitada por padrão em crates `#[no_std]` é que ela tem requisitos adicionais. Quando tentamos compilar nosso projeto agora, veremos esses requisitos como erros: + +``` +error: no global memory allocator found but one is required; link to std or add + #[global_allocator] to a static item that implements the GlobalAlloc trait. +``` + +O erro ocorre porque a crate `alloc` requer um alocador heap, que é um objeto que fornece as funções `allocate` e `deallocate`. Em Rust, alocadores heap são descritos pela trait [`GlobalAlloc`], que é mencionada na mensagem de erro. Para definir o alocador heap para a crate, o atributo `#[global_allocator]` deve ser aplicado a uma variável `static` que implementa a trait `GlobalAlloc`. + +[`GlobalAlloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html + +### A Trait `GlobalAlloc` + +A trait [`GlobalAlloc`] define as funções que um alocador heap deve fornecer. A trait é especial porque quase nunca é usada diretamente pelo programador. Em vez disso, o compilador irá automaticamente inserir as chamadas apropriadas aos métodos da trait ao usar os tipos de alocação e coleção de `alloc`. + +Como precisaremos implementar a trait para todos os nossos tipos de alocador, vale a pena dar uma olhada mais de perto em sua declaração: + +```rust +pub unsafe trait GlobalAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8; + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout); + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { ... } + unsafe fn realloc( + &self, + ptr: *mut u8, + layout: Layout, + new_size: usize + ) -> *mut u8 { ... } +} +``` + +Ela define os dois métodos obrigatórios [`alloc`] e [`dealloc`], que correspondem às funções `allocate` e `deallocate` que usamos em nossos exemplos: +- O método [`alloc`] recebe uma instância [`Layout`] como argumento, que descreve o tamanho e alinhamento desejados que a memória alocada deve ter. Ele retorna um [ponteiro bruto] para o primeiro byte do bloco de memória alocado. Em vez de um valor de erro explícito, o método `alloc` retorna um ponteiro nulo para sinalizar um erro de alocação. Isso é um pouco não idiomático, mas tem a vantagem de que envolver alocadores de sistema existentes é fácil, já que eles usam a mesma convenção. +- O método [`dealloc`] é a contraparte e é responsável por liberar um bloco de memória novamente. Ele recebe dois argumentos: o ponteiro retornado por `alloc` e o `Layout` que foi usado para a alocação. + +[`alloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.alloc +[`dealloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.dealloc +[`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html + +A trait adicionalmente define os dois métodos [`alloc_zeroed`] e [`realloc`] com implementações padrão: + +- O método [`alloc_zeroed`] é equivalente a chamar `alloc` e então definir o bloco de memória alocado para zero, que é exatamente o que a implementação padrão fornecida faz. Uma implementação de alocador pode substituir as implementações padrão com uma implementação personalizada mais eficiente se possível. +- O método [`realloc`] permite aumentar ou diminuir uma alocação. A implementação padrão aloca um novo bloco de memória com o tamanho desejado e copia todo o conteúdo da alocação anterior. Novamente, uma implementação de alocador pode provavelmente fornecer uma implementação mais eficiente deste método, por exemplo, aumentando/diminuindo a alocação no lugar, se possível. + +[`alloc_zeroed`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.alloc_zeroed +[`realloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.realloc + +#### Insegurança + +Uma coisa a notar é que tanto a trait em si quanto todos os métodos da trait são declarados como `unsafe`: + +- A razão para declarar a trait como `unsafe` é que o programador deve garantir que a implementação da trait para um tipo de alocador esteja correta. Por exemplo, o método `alloc` nunca deve retornar um bloco de memória que já está sendo usado em outro lugar porque isso causaria comportamento indefinido. +- Similarmente, a razão pela qual os métodos são `unsafe` é que o chamador deve garantir várias invariantes ao chamar os métodos, por exemplo, que o `Layout` passado para `alloc` especifica um tamanho diferente de zero. Isso não é realmente relevante na prática, já que os métodos normalmente são chamados diretamente pelo compilador, que garante que os requisitos sejam atendidos. + +### Um `DummyAllocator` + +Agora que sabemos o que um tipo de alocador deve fornecer, podemos criar um alocador dummy simples. Para isso, criamos um novo módulo `allocator`: + +```rust +// em src/lib.rs + +pub mod allocator; +``` + +Nosso alocador dummy faz o mínimo absoluto para implementar a trait e sempre retorna um erro quando `alloc` é chamado. Ele se parece com isso: + +```rust +// em src/allocator.rs + +use alloc::alloc::{GlobalAlloc, Layout}; +use core::ptr::null_mut; + +pub struct Dummy; + +unsafe impl GlobalAlloc for Dummy { + unsafe fn alloc(&self, _layout: Layout) -> *mut u8 { + null_mut() + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + panic!("dealloc should be never called") + } +} +``` + +A struct não precisa de nenhum campo, então a criamos como um [tipo de tamanho zero]. Como mencionado acima, sempre retornamos o ponteiro nulo de `alloc`, que corresponde a um erro de alocação. Como o alocador nunca retorna nenhuma memória, uma chamada para `dealloc` nunca deve ocorrer. Por essa razão, simplesmente entramos em pânico no método `dealloc`. Os métodos `alloc_zeroed` e `realloc` têm implementações padrão, então não precisamos fornecer implementações para eles. + +[tipo de tamanho zero]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts + +Agora temos um alocador simples, mas ainda temos que dizer ao compilador Rust que ele deve usar este alocador. É aqui que o atributo `#[global_allocator]` entra. + +### O Atributo `#[global_allocator]` + +O atributo `#[global_allocator]` diz ao compilador Rust qual instância de alocador ele deve usar como alocador heap global. O atributo só é aplicável a um `static` que implementa a trait `GlobalAlloc`. Vamos registrar uma instância de nosso alocador `Dummy` como o alocador global: + +```rust +// em src/allocator.rs + +#[global_allocator] +static ALLOCATOR: Dummy = Dummy; +``` + +Como o alocador `Dummy` é um [tipo de tamanho zero], não precisamos especificar nenhum campo na expressão de inicialização. + +Com este static, os erros de compilação devem ser corrigidos. Agora podemos usar os tipos de alocação e coleção de `alloc`. Por exemplo, podemos usar um [`Box`] para alocar um valor no heap: + +[`Box`]: https://doc.rust-lang.org/alloc/boxed/struct.Box.html + +```rust +// em src/main.rs + +extern crate alloc; + +use alloc::boxed::Box; + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // […] imprimir "Hello World!", chamar `init`, criar `mapper` e `frame_allocator` + + let x = Box::new(41); + + // […] chamar `test_main` no modo de teste + + println!("It did not crash!"); + blog_os::hlt_loop(); +} + +``` + +Note que precisamos especificar a declaração `extern crate alloc` em nosso `main.rs` também. Isso é necessário porque as partes `lib.rs` e `main.rs` são tratadas como crates separadas. No entanto, não precisamos criar outro `#[global_allocator]` static porque o alocador global se aplica a todas as crates do projeto. Na verdade, especificar um alocador adicional em outra crate seria um erro. + +Quando executamos o código acima, vemos que um pânico ocorre: + +![QEMU imprimindo "panicked at `allocation error: Layout { size_: 4, align_: 4 }, src/lib.rs:89:5"](qemu-dummy-output.png) + +O pânico ocorre porque a função `Box::new` chama implicitamente a função `alloc` do alocador global. Nosso alocador dummy sempre retorna um ponteiro nulo, então toda alocação falha. Para corrigir isso, precisamos criar um alocador que realmente retorna memória utilizável. + +## Criando um Heap do Kernel + +Antes de podermos criar um alocador apropriado, primeiro precisamos criar uma região de memória heap da qual o alocador pode alocar memória. Para fazer isso, precisamos definir um intervalo de memória virtual para a região heap e então mapear esta região para frames físicos. Veja o post [_"Introdução ao Paging"_] para uma visão geral de memória virtual e tabelas de página. + +[_"Introdução ao Paging"_]: @/edition-2/posts/08-paging-introduction/index.md + +O primeiro passo é definir uma região de memória virtual para o heap. Podemos escolher qualquer intervalo de endereço virtual que quisermos, desde que não esteja já sendo usado para uma região de memória diferente. Vamos defini-la como a memória começando no endereço `0x_4444_4444_0000` para que possamos facilmente reconhecer um ponteiro heap mais tarde: + +```rust +// em src/allocator.rs + +pub const HEAP_START: usize = 0x_4444_4444_0000; +pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB +``` + +Definimos o tamanho do heap para 100 KiB por enquanto. Se precisarmos de mais espaço no futuro, podemos simplesmente aumentá-lo. + +Se tentássemos usar esta região heap agora, uma falha de página ocorreria, já que a região de memória virtual ainda não está mapeada para memória física. Para resolver isso, criamos uma função `init_heap` que mapeia as páginas heap usando a [API `Mapper`] que introduzimos no post [_"Implementação de Paging"_]: + +[API `Mapper`]: @/edition-2/posts/09-paging-implementation/index.md#using-offsetpagetable +[_"Implementação de Paging"_]: @/edition-2/posts/09-paging-implementation/index.md + +```rust +// em src/allocator.rs + +use x86_64::{ + structures::paging::{ + mapper::MapToError, FrameAllocator, Mapper, Page, PageTableFlags, Size4KiB, + }, + VirtAddr, +}; + +pub fn init_heap( + mapper: &mut impl Mapper, + frame_allocator: &mut impl FrameAllocator, +) -> Result<(), MapToError> { + let page_range = { + let heap_start = VirtAddr::new(HEAP_START as u64); + let heap_end = heap_start + HEAP_SIZE - 1u64; + let heap_start_page = Page::containing_address(heap_start); + let heap_end_page = Page::containing_address(heap_end); + Page::range_inclusive(heap_start_page, heap_end_page) + }; + + for page in page_range { + let frame = frame_allocator + .allocate_frame() + .ok_or(MapToError::FrameAllocationFailed)?; + let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE; + unsafe { + mapper.map_to(page, frame, flags, frame_allocator)?.flush() + }; + } + + Ok(()) +} +``` + +A função recebe referências mutáveis para uma instância [`Mapper`] e uma instância [`FrameAllocator`], ambas limitadas a páginas de 4 KiB usando [`Size4KiB`] como o parâmetro genérico. O valor de retorno da função é um [`Result`] com o tipo unitário `()` como a variante de sucesso e um [`MapToError`] como a variante de erro, que é o tipo de erro retornado pelo método [`Mapper::map_to`]. Reutilizar o tipo de erro faz sentido aqui porque o método `map_to` é a principal fonte de erros nesta função. + +[`Mapper`]:https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html +[`FrameAllocator`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html +[`Size4KiB`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/enum.Size4KiB.html +[`Result`]: https://doc.rust-lang.org/core/result/enum.Result.html +[`MapToError`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/enum.MapToError.html +[`Mapper::map_to`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to + +A implementação pode ser dividida em duas partes: + +- **Criando o intervalo de páginas:**: Para criar um intervalo das páginas que queremos mapear, convertemos o ponteiro `HEAP_START` para um tipo [`VirtAddr`]. Então calculamos o endereço final do heap a partir dele adicionando o `HEAP_SIZE`. Queremos um limite inclusivo (o endereço do último byte do heap), então subtraímos 1. Em seguida, convertemos os endereços em tipos [`Page`] usando a função [`containing_address`]. Finalmente, criamos um intervalo de páginas das páginas inicial e final usando a função [`Page::range_inclusive`]. + +- **Mapeando as páginas:** O segundo passo é mapear todas as páginas do intervalo de páginas que acabamos de criar. Para isso, iteramos sobre essas páginas usando um loop `for`. Para cada página, fazemos o seguinte: + + - Alocamos um frame físico para o qual a página deve ser mapeada usando o método [`FrameAllocator::allocate_frame`]. Este método retorna [`None`] quando não há mais frames disponíveis. Lidamos com esse caso mapeando-o para um erro [`MapToError::FrameAllocationFailed`] através do método [`Option::ok_or`] e então aplicando o [operador de ponto de interrogação] para retornar cedo em caso de erro. + + - Definimos a flag `PRESENT` obrigatória e a flag `WRITABLE` para a página. Com essas flags, tanto acessos de leitura quanto de escrita são permitidos, o que faz sentido para memória heap. + + - Usamos o método [`Mapper::map_to`] para criar o mapeamento na tabela de páginas ativa. O método pode falhar, então usamos o [operador de ponto de interrogação] novamente para encaminhar o erro ao chamador. Em caso de sucesso, o método retorna uma instância [`MapperFlush`] que podemos usar para atualizar o [_buffer de tradução lookaside_] usando o método [`flush`]. + +[`VirtAddr`]: https://docs.rs/x86_64/0.14.2/x86_64/addr/struct.VirtAddr.html +[`Page`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html +[`containing_address`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html#method.containing_address +[`Page::range_inclusive`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/page/struct.Page.html#method.range_inclusive +[`FrameAllocator::allocate_frame`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/trait.FrameAllocator.html#tymethod.allocate_frame +[`None`]: https://doc.rust-lang.org/core/option/enum.Option.html#variant.None +[`MapToError::FrameAllocationFailed`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/enum.MapToError.html#variant.FrameAllocationFailed +[`Option::ok_or`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.ok_or +[operador de ponto de interrogação]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html +[`MapperFlush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html +[_buffer de tradução lookaside_]: @/edition-2/posts/08-paging-introduction/index.md#the-translation-lookaside-buffer +[`flush`]: https://docs.rs/x86_64/0.14.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush + +O passo final é chamar esta função de nossa `kernel_main`: + +```rust +// em src/main.rs + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + use blog_os::allocator; // nova importação + use blog_os::memory::{self, BootInfoFrameAllocator}; + + println!("Hello World{}", "!"); + blog_os::init(); + + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let mut mapper = unsafe { memory::init(phys_mem_offset) }; + let mut frame_allocator = unsafe { + BootInfoFrameAllocator::init(&boot_info.memory_map) + }; + + // novo + allocator::init_heap(&mut mapper, &mut frame_allocator) + .expect("heap initialization failed"); + + let x = Box::new(41); + + // […] chamar `test_main` no modo de teste + + println!("It did not crash!"); + blog_os::hlt_loop(); +} +``` + +Mostramos a função completa para contexto aqui. As únicas linhas novas são a importação `blog_os::allocator` e a chamada para a função `allocator::init_heap`. No caso de a função `init_heap` retornar um erro, entramos em pânico usando o método [`Result::expect`], já que atualmente não há maneira sensata de lidarmos com este erro. + +[`Result::expect`]: https://doc.rust-lang.org/core/result/enum.Result.html#method.expect + +Agora temos uma região de memória heap mapeada que está pronta para ser usada. A chamada `Box::new` ainda usa nosso alocador `Dummy` antigo, então você ainda verá o erro "out of memory" quando executá-lo. Vamos corrigir isso usando um alocador apropriado. + +## Usando uma Crate de Alocador + +Como implementar um alocador é um tanto complexo, começamos usando uma crate de alocador externa. Aprenderemos como implementar nosso próprio alocador no próximo post. + +Uma crate de alocador simples para aplicações `no_std` é a crate [`linked_list_allocator`]. Seu nome vem do fato de que ela usa uma estrutura de dados de lista encadeada para acompanhar as regiões de memória desalocadas. Veja o próximo post para uma explicação mais detalhada dessa abordagem. + +Para usar a crate, primeiro precisamos adicionar uma dependência nela em nosso `Cargo.toml`: + +[`linked_list_allocator`]: https://github.com/phil-opp/linked-list-allocator/ + +```toml +# em Cargo.toml + +[dependencies] +linked_list_allocator = "0.9.0" +``` + +Então podemos substituir nosso alocador dummy pelo alocador fornecido pela crate: + +```rust +// em src/allocator.rs + +use linked_list_allocator::LockedHeap; + +#[global_allocator] +static ALLOCATOR: LockedHeap = LockedHeap::empty(); +``` + +A struct é chamada `LockedHeap` porque usa o tipo [`spinning_top::Spinlock`] para sincronização. Isso é necessário porque múltiplas threads podem acessar o static `ALLOCATOR` ao mesmo tempo. Como sempre, ao usar um spinlock ou um mutex, precisamos ter cuidado para não causar acidentalmente um deadlock. Isso significa que não devemos realizar nenhuma alocação em manipuladores de interrupção, já que eles podem executar em um momento arbitrário e podem interromper uma alocação em andamento. + +[`spinning_top::Spinlock`]: https://docs.rs/spinning_top/0.1.0/spinning_top/type.Spinlock.html + +Definir o `LockedHeap` como alocador global não é suficiente. A razão é que usamos a função construtora [`empty`], que cria um alocador sem nenhuma memória de suporte. Como nosso alocador dummy, ele sempre retorna um erro em `alloc`. Para corrigir isso, precisamos inicializar o alocador após criar o heap: + +[`empty`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.LockedHeap.html#method.empty + +```rust +// em src/allocator.rs + +pub fn init_heap( + mapper: &mut impl Mapper, + frame_allocator: &mut impl FrameAllocator, +) -> Result<(), MapToError> { + // […] mapear todas as páginas heap para frames físicos + + // novo + unsafe { + ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE); + } + + Ok(()) +} +``` + +Usamos o método [`lock`] no spinlock interno do tipo `LockedHeap` para obter uma referência exclusiva à instância [`Heap`] encapsulada, na qual então chamamos o método [`init`] com os limites do heap como argumentos. Como a função [`init`] já tenta escrever na memória heap, devemos inicializar o heap somente _depois_ de mapear as páginas heap. + +[`lock`]: https://docs.rs/lock_api/0.3.3/lock_api/struct.Mutex.html#method.lock +[`Heap`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html +[`init`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.init + +Depois de inicializar o heap, agora podemos usar todos os tipos de alocação e coleção da crate embutida [`alloc`] sem erro: + +```rust +// em src/main.rs + +use alloc::{boxed::Box, vec, vec::Vec, rc::Rc}; + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // […] inicializar interrupções, mapper, frame_allocator, heap + + // alocar um número no heap + let heap_value = Box::new(41); + println!("heap_value at {:p}", heap_value); + + // criar um vetor de tamanho dinâmico + let mut vec = Vec::new(); + for i in 0..500 { + vec.push(i); + } + println!("vec at {:p}", vec.as_slice()); + + // criar um vetor com contagem de referências -> será liberado quando a contagem chegar a 0 + let reference_counted = Rc::new(vec![1, 2, 3]); + let cloned_reference = reference_counted.clone(); + println!("current reference count is {}", Rc::strong_count(&cloned_reference)); + core::mem::drop(reference_counted); + println!("reference count is {} now", Rc::strong_count(&cloned_reference)); + + // […] chamar `test_main` no contexto de teste + println!("It did not crash!"); + blog_os::hlt_loop(); +} +``` + +Este exemplo de código mostra alguns usos dos tipos [`Box`], [`Vec`] e [`Rc`]. Para os tipos `Box` e `Vec`, imprimimos os ponteiros heap subjacentes usando o [especificador de formatação `{:p}`]. Para mostrar `Rc`, criamos um valor heap com contagem de referências e usamos a função [`Rc::strong_count`] para imprimir a contagem de referências atual antes e depois de descartar uma instância (usando [`core::mem::drop`]). + +[`Vec`]: https://doc.rust-lang.org/alloc/vec/ +[`Rc`]: https://doc.rust-lang.org/alloc/rc/ +[especificador de formatação `{:p}`]: https://doc.rust-lang.org/core/fmt/trait.Pointer.html +[`Rc::strong_count`]: https://doc.rust-lang.org/alloc/rc/struct.Rc.html#method.strong_count +[`core::mem::drop`]: https://doc.rust-lang.org/core/mem/fn.drop.html + +Quando o executamos, vemos o seguinte: + +![QEMU imprimindo ` +heap_value at 0x444444440000 +vec at 0x4444444408000 +current reference count is 2 +reference count is 1 now +](qemu-alloc-showcase.png) + +Como esperado, vemos que os valores `Box` e `Vec` vivem no heap, como indicado pelo ponteiro começando com o prefixo `0x_4444_4444_*`. O valor com contagem de referências também se comporta como esperado, com a contagem de referências sendo 2 após a chamada `clone`, e 1 novamente depois que uma das instâncias foi descartada. + +A razão pela qual o vetor começa no offset `0x800` não é que o valor encaixotado seja `0x800` bytes grande, mas as [realocações] que ocorrem quando o vetor precisa aumentar sua capacidade. Por exemplo, quando a capacidade do vetor é 32 e tentamos adicionar o próximo elemento, o vetor aloca um novo array de suporte com capacidade de 64 nos bastidores e copia todos os elementos. Então ele libera a alocação antiga. + +[realocações]: https://doc.rust-lang.org/alloc/vec/struct.Vec.html#capacity-and-reallocation + +É claro que existem muitos mais tipos de alocação e coleção na crate `alloc` que agora podemos usar todos em nosso kernel, incluindo: + +- o ponteiro com contagem de referências thread-safe [`Arc`] +- o tipo de string proprietária [`String`] e a macro [`format!`] +- [`LinkedList`] +- o buffer circular crescente [`VecDeque`] +- a fila de prioridade [`BinaryHeap`] +- [`BTreeMap`] e [`BTreeSet`] + +[`Arc`]: https://doc.rust-lang.org/alloc/sync/struct.Arc.html +[`String`]: https://doc.rust-lang.org/alloc/string/struct.String.html +[`format!`]: https://doc.rust-lang.org/alloc/macro.format.html +[`LinkedList`]: https://doc.rust-lang.org/alloc/collections/linked_list/struct.LinkedList.html +[`VecDeque`]: https://doc.rust-lang.org/alloc/collections/vec_deque/struct.VecDeque.html +[`BinaryHeap`]: https://doc.rust-lang.org/alloc/collections/binary_heap/struct.BinaryHeap.html +[`BTreeMap`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html +[`BTreeSet`]: https://doc.rust-lang.org/alloc/collections/btree_set/struct.BTreeSet.html + +Esses tipos se tornarão muito úteis quando quisermos implementar listas de threads, filas de escalonamento ou suporte para async/await. + +## Adicionando um Teste + +Para garantir que não quebremos acidentalmente nosso novo código de alocação, devemos adicionar um teste de integração para ele. Começamos criando um novo arquivo `tests/heap_allocation.rs` com o seguinte conteúdo: + +```rust +// em tests/heap_allocation.rs + +#![no_std] +#![no_main] +#![feature(custom_test_frameworks)] +#![test_runner(blog_os::test_runner)] +#![reexport_test_harness_main = "test_main"] + +extern crate alloc; + +use bootloader::{entry_point, BootInfo}; +use core::panic::PanicInfo; + +entry_point!(main); + +fn main(boot_info: &'static BootInfo) -> ! { + unimplemented!(); +} + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + blog_os::test_panic_handler(info) +} +``` + +Reutilizamos as funções `test_runner` e `test_panic_handler` de nosso `lib.rs`. Como queremos testar alocações, habilitamos a crate `alloc` através da declaração `extern crate alloc`. Para mais informações sobre o boilerplate de teste, confira o post [_Testing_]. + +[_Testing_]: @/edition-2/posts/04-testing/index.md + +A implementação da função `main` se parece com isso: + +```rust +// em tests/heap_allocation.rs + +fn main(boot_info: &'static BootInfo) -> ! { + use blog_os::allocator; + use blog_os::memory::{self, BootInfoFrameAllocator}; + use x86_64::VirtAddr; + + blog_os::init(); + let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset); + let mut mapper = unsafe { memory::init(phys_mem_offset) }; + let mut frame_allocator = unsafe { + BootInfoFrameAllocator::init(&boot_info.memory_map) + }; + allocator::init_heap(&mut mapper, &mut frame_allocator) + .expect("heap initialization failed"); + + test_main(); + loop {} +} +``` + +Ela é muito similar à função `kernel_main` em nosso `main.rs`, com as diferenças de que não invocamos `println`, não incluímos nenhuma alocação de exemplo, e chamamos `test_main` incondicionalmente. + +Agora estamos prontos para adicionar alguns casos de teste. Primeiro, adicionamos um teste que realiza algumas alocações simples usando [`Box`] e verifica os valores alocados para garantir que as alocações básicas funcionam: + +```rust +// em tests/heap_allocation.rs +use alloc::boxed::Box; + +#[test_case] +fn simple_allocation() { + let heap_value_1 = Box::new(41); + let heap_value_2 = Box::new(13); + assert_eq!(*heap_value_1, 41); + assert_eq!(*heap_value_2, 13); +} +``` + +Mais importante, este teste verifica que nenhum erro de alocação ocorre. + +Em seguida, construímos iterativamente um vetor grande, para testar tanto alocações grandes quanto múltiplas alocações (devido a realocações): + +```rust +// em tests/heap_allocation.rs + +use alloc::vec::Vec; + +#[test_case] +fn large_vec() { + let n = 1000; + let mut vec = Vec::new(); + for i in 0..n { + vec.push(i); + } + assert_eq!(vec.iter().sum::(), (n - 1) * n / 2); +} +``` + +Verificamos a soma comparando-a com a fórmula para a [soma parcial n-ésima]. Isso nos dá alguma confiança de que os valores alocados estão todos corretos. + +[soma parcial n-ésima]: https://en.wikipedia.org/wiki/1_%2B_2_%2B_3_%2B_4_%2B_%E2%8B%AF#Partial_sums + +Como terceiro teste, criamos dez mil alocações uma após a outra: + +```rust +// em tests/heap_allocation.rs + +use blog_os::allocator::HEAP_SIZE; + +#[test_case] +fn many_boxes() { + for i in 0..HEAP_SIZE { + let x = Box::new(i); + assert_eq!(*x, i); + } +} +``` + +Este teste garante que o alocador reutiliza memória liberada para alocações subsequentes, já que ficaria sem memória caso contrário. Isso pode parecer um requisito óbvio para um alocador, mas existem designs de alocador que não fazem isso. Um exemplo é o design de alocador bump que será explicado no próximo post. + +Vamos executar nosso novo teste de integração: + +``` +> cargo test --test heap_allocation +[…] +Running 3 tests +simple_allocation... [ok] +large_vec... [ok] +many_boxes... [ok] +``` + +Todos os três testes foram bem-sucedidos! Você também pode invocar `cargo test` (sem o argumento `--test`) para executar todos os testes unitários e de integração. + +## Resumo + +Este post deu uma introdução à memória dinâmica e explicou por que e onde ela é necessária. Vimos como o verificador de empréstimos do Rust previne vulnerabilidades comuns e aprendemos como a API de alocação do Rust funciona. + +Depois de criar uma implementação mínima da interface de alocador do Rust usando um alocador dummy, criamos uma região de memória heap apropriada para o nosso kernel. Para isso, definimos um intervalo de endereço virtual para o heap e então mapeamos todas as páginas desse intervalo para frames físicos usando o `Mapper` e `FrameAllocator` do post anterior. + +Finalmente, adicionamos uma dependência na crate `linked_list_allocator` para adicionar um alocador apropriado ao nosso kernel. Com este alocador, pudemos usar `Box`, `Vec` e outros tipos de alocação e coleção da crate `alloc`. + +## O que vem a seguir? + +Embora já tenhamos adicionado suporte para alocação heap neste post, deixamos a maior parte do trabalho para a crate `linked_list_allocator`. O próximo post mostrará em detalhes como um alocador pode ser implementado do zero. Ele apresentará múltiplos designs de alocador possíveis, mostrará como implementar versões simples deles e explicará suas vantagens e desvantagens. \ No newline at end of file diff --git a/blog/content/edition-2/posts/11-allocator-designs/index.pt-BR.md b/blog/content/edition-2/posts/11-allocator-designs/index.pt-BR.md new file mode 100644 index 00000000..a6ea9bab --- /dev/null +++ b/blog/content/edition-2/posts/11-allocator-designs/index.pt-BR.md @@ -0,0 +1,1254 @@ ++++ +title = "Designs de Alocadores" +weight = 11 +path = "pt-BR/allocator-designs" +date = 2020-01-20 + +[extra] +chapter = "Gerenciamento de Memória" +# Please update this when updating the translation +translation_based_on_commit = "c0fc0bed9e8b8459dde80a71f4f89f578cb5ddfb" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Este post explica como implementar alocadores heap do zero. Ele apresenta e discute diferentes designs de alocadores, incluindo alocação bump, alocação de lista encadeada e alocação de bloco de tamanho fixo. Para cada um dos três designs, criaremos uma implementação básica que pode ser usada para o nosso kernel. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou pergunta, por favor abra uma issue lá. Você também pode deixar comentários [no final]. O código-fonte completo para este post pode ser encontrado no branch [`post-11`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[at the bottom]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-11 + + + +## Introdução + +No [post anterior], adicionamos suporte básico para alocações heap ao nosso kernel. Para isso, [criamos uma nova região de memória][map-heap] nas tabelas de página e [usamos a crate `linked_list_allocator`][use-alloc-crate] para gerenciar essa memória. Embora agora tenhamos um heap funcional, deixamos a maior parte do trabalho para a crate do alocador sem tentar entender como ela funciona. + +[post anterior]: @/edition-2/posts/10-heap-allocation/index.md +[map-heap]: @/edition-2/posts/10-heap-allocation/index.md#creating-a-kernel-heap +[use-alloc-crate]: @/edition-2/posts/10-heap-allocation/index.md#using-an-allocator-crate + +Neste post, mostraremos como criar nosso próprio alocador heap do zero em vez de depender de uma crate de alocador existente. Discutiremos diferentes designs de alocadores, incluindo um _alocador bump_ simplista e um _alocador de bloco de tamanho fixo_ básico, e usaremos esse conhecimento para implementar um alocador com desempenho aprimorado (comparado à crate `linked_list_allocator`). + +### Objetivos de Design + +A responsabilidade de um alocador é gerenciar a memória heap disponível. Ele precisa retornar memória não utilizada em chamadas `alloc` e acompanhar a memória liberada por `dealloc` para que possa ser reutilizada novamente. Mais importante, ele nunca deve entregar memória que já está em uso em outro lugar porque isso causaria comportamento indefinido. + +Além da correção, existem muitos objetivos de design secundários. Por exemplo, o alocador deve utilizar efetivamente a memória disponível e manter a [_fragmentação_] baixa. Além disso, ele deve funcionar bem para aplicações concorrentes e escalar para qualquer número de processadores. Para desempenho máximo, ele poderia até otimizar o layout da memória em relação aos caches da CPU para melhorar a [localidade de cache] e evitar [compartilhamento falso]. + +[localidade de cache]: https://www.geeksforgeeks.org/locality-of-reference-and-cache-operation-in-cache-memory/ +[_fragmentação_]: https://en.wikipedia.org/wiki/Fragmentation_(computing) +[compartilhamento falso]: https://mechanical-sympathy.blogspot.de/2011/07/false-sharing.html + +Esses requisitos podem tornar bons alocadores muito complexos. Por exemplo, [jemalloc] tem mais de 30.000 linhas de código. Essa complexidade é frequentemente indesejada no código do kernel, onde um único bug pode levar a vulnerabilidades de segurança graves. Felizmente, os padrões de alocação do código do kernel são frequentemente muito mais simples comparados ao código do espaço do usuário, de modo que designs de alocadores relativamente simples frequentemente são suficientes. + +[jemalloc]: http://jemalloc.net/ + +A seguir, apresentamos três possíveis designs de alocadores de kernel e explicamos suas vantagens e desvantagens. + +## Alocador Bump + +O design de alocador mais simples é um _alocador bump_ (também conhecido como _alocador de pilha_). Ele aloca memória linearmente e só mantém o controle do número de bytes alocados e do número de alocações. Ele só é útil em casos de uso muito específicos porque tem uma limitação severa: ele só pode liberar toda a memória de uma vez. + +### Ideia + +A ideia por trás de um alocador bump é alocar memória linearmente aumentando (_"bumping"_) uma variável `next`, que aponta para o início da memória não utilizada. No início, `next` é igual ao endereço inicial do heap. Em cada alocação, `next` é aumentado pelo tamanho da alocação para que sempre aponte para a fronteira entre memória usada e não utilizada: + +![A área de memória heap em três pontos no tempo: + 1: Uma única alocação existe no início do heap; o ponteiro `next` aponta para seu final. + 2: Uma segunda alocação foi adicionada logo após a primeira; o ponteiro `next` aponta para o final da segunda alocação. + 3: Uma terceira alocação foi adicionada logo após a segunda; o ponteiro `next` aponta para o final da terceira alocação.](bump-allocation.svg) + +O ponteiro `next` só se move em uma única direção e, portanto, nunca entrega a mesma região de memória duas vezes. Quando ele alcança o final do heap, nenhuma memória adicional pode ser alocada, resultando em um erro de falta de memória na próxima alocação. + +Um alocador bump é frequentemente implementado com um contador de alocações, que é aumentado em 1 em cada chamada `alloc` e diminuído em 1 em cada chamada `dealloc`. Quando o contador de alocações atinge zero, significa que todas as alocações no heap foram desalocadas. Nesse caso, o ponteiro `next` pode ser redefinido para o endereço inicial do heap, de modo que a memória heap completa esteja disponível para alocações novamente. + +### Implementação + +Começamos nossa implementação declarando um novo submódulo `allocator::bump`: + +```rust +// em src/allocator.rs + +pub mod bump; +``` + +O conteúdo do submódulo vive em um novo arquivo `src/allocator/bump.rs`, que criamos com o seguinte conteúdo: + +```rust +// em src/allocator/bump.rs + +pub struct BumpAllocator { + heap_start: usize, + heap_end: usize, + next: usize, + allocations: usize, +} + +impl BumpAllocator { + /// Cria um novo alocador bump vazio. + pub const fn new() -> Self { + BumpAllocator { + heap_start: 0, + heap_end: 0, + next: 0, + allocations: 0, + } + } + + /// Inicializa o alocador bump com os limites de heap fornecidos. + /// + /// Este método é unsafe porque o chamador deve garantir que o intervalo + /// de memória fornecido esteja não utilizado. Além disso, este método deve ser chamado apenas uma vez. + pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) { + self.heap_start = heap_start; + self.heap_end = heap_start + heap_size; + self.next = heap_start; + } +} +``` + +Os campos `heap_start` e `heap_end` mantêm o controle dos limites inferior e superior da região de memória heap. O chamador precisa garantir que esses endereços sejam válidos, caso contrário o alocador retornaria memória inválida. Por essa razão, a função `init` precisa ser `unsafe` para chamar. + +O propósito do campo `next` é sempre apontar para o primeiro byte não utilizado do heap, ou seja, o endereço inicial da próxima alocação. Ele é definido como `heap_start` na função `init` porque no início, o heap inteiro está não utilizado. Em cada alocação, este campo será aumentado pelo tamanho da alocação (_"bumped"_) para garantir que não retornemos a mesma região de memória duas vezes. + +O campo `allocations` é um simples contador para as alocações ativas com o objetivo de redefinir o alocador após a última alocação ter sido liberada. Ele é inicializado com 0. + +Escolhemos criar uma função `init` separada em vez de realizar a inicialização diretamente em `new` para manter a interface idêntica ao alocador fornecido pela crate `linked_list_allocator`. Dessa forma, os alocadores podem ser trocados sem mudanças adicionais no código. + +### Implementando `GlobalAlloc` + +Como [explicado no post anterior][global-alloc], todos os alocadores heap precisam implementar a trait [`GlobalAlloc`], que é definida assim: + +[global-alloc]: @/edition-2/posts/10-heap-allocation/index.pt-BR.md#a-interface-do-alocador +[`GlobalAlloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html + +```rust +pub unsafe trait GlobalAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8; + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout); + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { ... } + unsafe fn realloc( + &self, + ptr: *mut u8, + layout: Layout, + new_size: usize + ) -> *mut u8 { ... } +} +``` + +Apenas os métodos `alloc` e `dealloc` são obrigatórios; os outros dois métodos têm implementações padrão e podem ser omitidos. + +#### Primeira Tentativa de Implementação + +Vamos tentar implementar o método `alloc` para nosso `BumpAllocator`: + +```rust +// em src/allocator/bump.rs + +use alloc::alloc::{GlobalAlloc, Layout}; + +unsafe impl GlobalAlloc for BumpAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + // TODO verificação de alinhamento e limites + let alloc_start = self.next; + self.next = alloc_start + layout.size(); + self.allocations += 1; + alloc_start as *mut u8 + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + todo!(); + } +} +``` + +Primeiro, usamos o campo `next` como o endereço inicial para nossa alocação. Então atualizamos o campo `next` para apontar para o endereço final da alocação, que é o próximo endereço não utilizado no heap. Antes de retornar o endereço inicial da alocação como um ponteiro `*mut u8`, aumentamos o contador `allocations` em 1. + +Note que não realizamos nenhuma verificação de limites ou ajustes de alinhamento, então esta implementação ainda não é segura. Isso não importa muito porque ela falha ao compilar de qualquer forma com o seguinte erro: + +``` +error[E0594]: cannot assign to `self.next` which is behind a `&` reference + --> src/allocator/bump.rs:29:9 + | +29 | self.next = alloc_start + layout.size(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be written +``` + +(O mesmo erro também ocorre para a linha `self.allocations += 1`. Omitimos aqui por brevidade.) + +O erro ocorre porque os métodos [`alloc`] e [`dealloc`] da trait `GlobalAlloc` operam apenas em uma referência imutável `&self`, então atualizar os campos `next` e `allocations` não é possível. Isso é problemático porque atualizar `next` em cada alocação é o princípio essencial de um alocador bump. + +[`alloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.alloc +[`dealloc`]: https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.dealloc + +#### `GlobalAlloc` e Mutabilidade + +Antes de olharmos para uma possível solução para este problema de mutabilidade, vamos tentar entender por que os métodos da trait `GlobalAlloc` são definidos com argumentos `&self`: Como vimos [no post anterior][global-allocator], o alocador heap global é definido adicionando o atributo `#[global_allocator]` a um `static` que implementa a trait `GlobalAlloc`. Variáveis estáticas são imutáveis em Rust, então não há maneira de chamar um método que recebe `&mut self` no alocador estático. Por essa razão, todos os métodos de `GlobalAlloc` recebem apenas uma referência imutável `&self`. + +[global-allocator]: @/edition-2/posts/10-heap-allocation/index.md#the-global-allocator-attribute + +Felizmente, há uma maneira de obter uma referência `&mut self` de uma referência `&self`: Podemos usar [mutabilidade interior] sincronizada envolvendo o alocador em um spinlock [`spin::Mutex`]. Este tipo fornece um método `lock` que realiza [exclusão mútua] e, portanto, transforma com segurança uma referência `&self` em uma referência `&mut self`. Já usamos o tipo wrapper várias vezes em nosso kernel, por exemplo, para o [buffer de texto VGA][vga-mutex]. + +[mutabilidade interior]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html +[vga-mutex]: @/edition-2/posts/03-vga-text-buffer/index.md#spinlocks +[`spin::Mutex`]: https://docs.rs/spin/0.5.0/spin/struct.Mutex.html +[exclusão mútua]: https://en.wikipedia.org/wiki/Mutual_exclusion + +#### Um Tipo Wrapper `Locked` + +Com a ajuda do tipo wrapper `spin::Mutex`, podemos implementar a trait `GlobalAlloc` para nosso alocador bump. O truque é implementar a trait não para o `BumpAllocator` diretamente, mas para o tipo envolvido `spin::Mutex`: + +```rust +unsafe impl GlobalAlloc for spin::Mutex {…} +``` + +Infelizmente, isso ainda não funciona porque o compilador Rust não permite implementações de traits para tipos definidos em outras crates: + +``` +error[E0117]: only traits defined in the current crate can be implemented for arbitrary types + --> src/allocator/bump.rs:28:1 + | +28 | unsafe impl GlobalAlloc for spin::Mutex { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^-------------------------- + | | | + | | `spin::mutex::Mutex` is not defined in the current crate + | impl doesn't use only types from inside the current crate + | + = note: define and implement a trait or new type instead +``` + +Para corrigir isso, precisamos criar nosso próprio tipo wrapper em torno de `spin::Mutex`: + +```rust +// em src/allocator.rs + +/// Um wrapper em torno de spin::Mutex para permitir implementações de traits. +pub struct Locked { + inner: spin::Mutex, +} + +impl Locked { + pub const fn new(inner: A) -> Self { + Locked { + inner: spin::Mutex::new(inner), + } + } + + pub fn lock(&self) -> spin::MutexGuard { + self.inner.lock() + } +} +``` + +O tipo é um wrapper genérico em torno de um `spin::Mutex`. Ele não impõe restrições no tipo envolvido `A`, então pode ser usado para envolver todos os tipos, não apenas alocadores. Ele fornece uma simples função construtora `new` que envolve um valor dado. Para conveniência, ele também fornece uma função `lock` que chama `lock` no `Mutex` envolvido. Como o tipo `Locked` é geral o suficiente para ser útil para outras implementações de alocadores também, o colocamos no módulo `allocator` pai. + +#### Implementação para `Locked` + +O tipo `Locked` é definido em nossa própria crate (em contraste com `spin::Mutex`), então podemos usá-lo para implementar `GlobalAlloc` para nosso alocador bump. A implementação completa se parece com isso: + +```rust +// em src/allocator/bump.rs + +use super::{align_up, Locked}; +use alloc::alloc::{GlobalAlloc, Layout}; +use core::ptr; + +unsafe impl GlobalAlloc for Locked { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let mut bump = self.lock(); // obter uma referência mutável + + let alloc_start = align_up(bump.next, layout.align()); + let alloc_end = match alloc_start.checked_add(layout.size()) { + Some(end) => end, + None => return ptr::null_mut(), + }; + + if alloc_end > bump.heap_end { + ptr::null_mut() // fora de memória + } else { + bump.next = alloc_end; + bump.allocations += 1; + alloc_start as *mut u8 + } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + let mut bump = self.lock(); // obter uma referência mutável + + bump.allocations -= 1; + if bump.allocations == 0 { + bump.next = bump.heap_start; + } + } +} +``` + +O primeiro passo para tanto `alloc` quanto `dealloc` é chamar o método [`Mutex::lock`] através do campo `inner` para obter uma referência mutável ao tipo alocador envolvido. A instância permanece bloqueada até o final do método, para que nenhuma corrida de dados possa ocorrer em contextos multi-thread (adicionaremos suporte a threading em breve). + +[`Mutex::lock`]: https://docs.rs/spin/0.5.0/spin/struct.Mutex.html#method.lock + +Comparado ao protótipo anterior, a implementação de `alloc` agora respeita requisitos de alinhamento e realiza uma verificação de limites para garantir que as alocações permaneçam dentro da região de memória heap. O primeiro passo é arredondar o endereço `next` para cima até o alinhamento especificado pelo argumento `Layout`. O código para a função `align_up` é mostrado em um momento. Então adicionamos o tamanho de alocação solicitado a `alloc_start` para obter o endereço final da alocação. Para prevenir overflow de inteiro em alocações grandes, usamos o método [`checked_add`]. Se ocorrer um overflow ou se o endereço final resultante da alocação for maior que o endereço final do heap, retornamos um ponteiro nulo para sinalizar uma situação de falta de memória. Caso contrário, atualizamos o endereço `next` e aumentamos o contador `allocations` em 1 como antes. Finalmente, retornamos o endereço `alloc_start` convertido para um ponteiro `*mut u8`. + +[`checked_add`]: https://doc.rust-lang.org/std/primitive.usize.html#method.checked_add +[`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html + +A função `dealloc` ignora o ponteiro e os argumentos `Layout` fornecidos. Em vez disso, ela apenas diminui o contador `allocations`. Se o contador atingir `0` novamente, significa que todas as alocações foram liberadas novamente. Nesse caso, ela redefine o endereço `next` para o endereço `heap_start` para tornar a memória heap completa disponível novamente. + +#### Alinhamento de Endereço + +A função `align_up` é geral o suficiente para que possamos colocá-la no módulo `allocator` pai. Uma implementação básica se parece com isso: + +```rust +// em src/allocator.rs + +/// Alinha o endereço fornecido `addr` para cima até o alinhamento `align`. +fn align_up(addr: usize, align: usize) -> usize { + let remainder = addr % align; + if remainder == 0 { + addr // addr já está alinhado + } else { + addr - remainder + align + } +} +``` + +A função primeiro calcula o [resto] da divisão de `addr` por `align`. Se o resto for `0`, o endereço já está alinhado com o alinhamento fornecido. Caso contrário, alinhamos o endereço subtraindo o resto (para que o novo resto seja 0) e então adicionando o alinhamento (para que o endereço não se torne menor que o endereço original). + +[resto]: https://en.wikipedia.org/wiki/Euclidean_division + +Note que esta não é a maneira mais eficiente de implementar esta função. Uma implementação muito mais rápida se parece com isso: + +```rust +/// Alinha o endereço fornecido `addr` para cima até o alinhamento `align`. +/// +/// Requer que `align` seja uma potência de dois. +fn align_up(addr: usize, align: usize) -> usize { + (addr + align - 1) & !(align - 1) +} +``` + +Este método requer que `align` seja uma potência de dois, o que pode ser garantido utilizando a trait `GlobalAlloc` (e seu parâmetro [`Layout`]). Isso torna possível criar uma [máscara de bits] para alinhar o endereço de uma maneira muito eficiente. Para entender como funciona, vamos passar por isso passo a passo, começando no lado direito: + +[`Layout`]: https://doc.rust-lang.org/alloc/alloc/struct.Layout.html +[máscara de bits]: https://en.wikipedia.org/wiki/Mask_(computing) + +- Como `align` é uma potência de dois, sua [representação binária] tem apenas um único bit definido (por exemplo, `0b000100000`). Isso significa que `align - 1` tem todos os bits inferiores definidos (por exemplo, `0b00011111`). +- Ao criar o [`NOT` bit a bit] através do operador `!`, obtemos um número que tem todos os bits definidos exceto os bits inferiores a `align` (por exemplo, `0b…111111111100000`). +- Ao realizar um [`AND` bit a bit] em um endereço e `!(align - 1)`, alinhamos o endereço _para baixo_. Isso funciona limpando todos os bits que são inferiores a `align`. +- Como queremos alinhar para cima em vez de para baixo, aumentamos o `addr` por `align - 1` antes de realizar o `AND` bit a bit. Dessa forma, endereços já alinhados permanecem os mesmos enquanto endereços não alinhados são arredondados para o próximo limite de alinhamento. + +[representação binária]: https://en.wikipedia.org/wiki/Binary_number#Representation +[`NOT` bit a bit]: https://en.wikipedia.org/wiki/Bitwise_operation#NOT +[`AND` bit a bit]: https://en.wikipedia.org/wiki/Bitwise_operation#AND + +Qual variante você escolher fica a seu critério. Ambas calculam o mesmo resultado, apenas usando métodos diferentes. + +### Usando-o + +Para usar o alocador bump em vez da crate `linked_list_allocator`, precisamos atualizar o static `ALLOCATOR` em `allocator.rs`: + +```rust +// em src/allocator.rs + +use bump::BumpAllocator; + +#[global_allocator] +static ALLOCATOR: Locked = Locked::new(BumpAllocator::new()); +``` + +Aqui se torna importante que declaramos `BumpAllocator::new` e `Locked::new` como [funções `const`]. Se fossem funções normais, ocorreria um erro de compilação porque a expressão de inicialização de um `static` deve ser avaliável em tempo de compilação. + +[funções `const`]: https://doc.rust-lang.org/reference/items/functions.html#const-functions + +Não precisamos modificar a chamada `ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE)` em nossa função `init_heap` porque o alocador bump fornece a mesma interface que o alocador fornecido pela `linked_list_allocator`. + +Agora nosso kernel usa nosso alocador bump! Tudo ainda deve funcionar, incluindo os [testes `heap_allocation`] que criamos no post anterior: + +[testes `heap_allocation`]: @/edition-2/posts/10-heap-allocation/index.md#adding-a-test + +``` +> cargo test --test heap_allocation +[…] +Running 3 tests +simple_allocation... [ok] +large_vec... [ok] +many_boxes... [ok] +``` + +Nosso novo alocador parece funcionar! + +### Discussão + +A grande vantagem da alocação bump é que ela é muito rápida. Comparado a outros designs de alocadores (veja abaixo) que precisam procurar ativamente por um bloco de memória adequado e realizar várias tarefas de contabilidade em `alloc` e `dealloc`, um alocador bump [pode ser otimizado][bump downwards] para apenas algumas instruções assembly. Isso torna os alocadores bump úteis para otimizar o desempenho de alocação, por exemplo, ao criar uma [biblioteca DOM virtual]. + +[bump downwards]: https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html +[biblioteca DOM virtual]: https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/ + +Embora um alocador bump raramente seja usado como o alocador global, o princípio de alocação bump é frequentemente aplicado na forma de [alocação arena], que basicamente agrupa alocações individuais juntas para melhorar o desempenho. Um exemplo de um alocador arena para Rust está contido na crate [`toolshed`]. + +[alocação arena]: https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas.html +[`toolshed`]: https://docs.rs/toolshed/0.8.1/toolshed/index.html + +#### A Desvantagem de um Alocador Bump + +A principal limitação de um alocador bump é que ele só pode reutilizar memória desalocada depois que todas as alocações foram liberadas. Isso significa que uma única alocação de longa duração é suficiente para prevenir a reutilização de memória. Podemos ver isso quando adicionamos uma variação do teste `many_boxes`: + +```rust +// em tests/heap_allocation.rs + +#[test_case] +fn many_boxes_long_lived() { + let long_lived = Box::new(1); // novo + for i in 0..HEAP_SIZE { + let x = Box::new(i); + assert_eq!(*x, i); + } + assert_eq!(*long_lived, 1); // novo +} +``` + +Como o teste `many_boxes`, este teste cria um grande número de alocações para provocar uma falha de falta de memória se o alocador não reutilizar memória liberada. Adicionalmente, o teste cria uma alocação `long_lived`, que vive pela execução completa do loop. + +Quando tentamos executar nosso novo teste, vemos que ele de fato falha: + +``` +> cargo test --test heap_allocation +Running 4 tests +simple_allocation... [ok] +large_vec... [ok] +many_boxes... [ok] +many_boxes_long_lived... [failed] + +Error: panicked at 'allocation error: Layout { size_: 8, align_: 8 }', src/lib.rs:86:5 +``` + +Vamos tentar entender por que essa falha ocorre em detalhe: Primeiro, a alocação `long_lived` é criada no início do heap, aumentando assim o contador `allocations` em 1. Para cada iteração do loop, uma alocação de curta duração é criada e diretamente liberada novamente antes da próxima iteração começar. Isso significa que o contador `allocations` é temporariamente aumentado para 2 no início de uma iteração e diminuído para 1 no final dela. O problema agora é que o alocador bump só pode reutilizar memória depois que _todas_ as alocações foram liberadas, ou seja, quando o contador `allocations` cai para 0. Como isso não acontece antes do final do loop, cada iteração do loop aloca uma nova região de memória, levando a um erro de falta de memória após um número de iterações. + +#### Corrigindo o Teste? + +Existem dois truques potenciais que poderíamos utilizar para corrigir o teste para nosso alocador bump: + +- Poderíamos atualizar `dealloc` para verificar se a alocação liberada foi a última alocação retornada por `alloc` comparando seu endereço final com o ponteiro `next`. No caso de serem iguais, podemos com segurança redefinir `next` de volta ao endereço inicial da alocação liberada. Dessa forma, cada iteração do loop reutiliza o mesmo bloco de memória. +- Poderíamos adicionar um método `alloc_back` que aloca memória do _final_ do heap usando um campo `next_back` adicional. Então poderíamos usar manualmente este método de alocação para todas as alocações de longa duração, separando assim alocações de curta e longa duração no heap. Note que esta separação só funciona se estiver claro de antemão quanto tempo cada alocação viverá. Outra desvantagem desta abordagem é que realizar alocações manualmente é trabalhoso e potencialmente inseguro. + +Embora ambas essas abordagens funcionem para corrigir o teste, elas não são uma solução geral, já que são capazes apenas de reutilizar memória em casos muito específicos. A questão é: Existe uma solução geral que reutiliza _toda_ memória liberada? + +#### Reutilizando Toda Memória Liberada? + +Como aprendemos [no post anterior][heap-intro], alocações podem viver arbitrariamente por muito tempo e podem ser liberadas em uma ordem arbitrária. Isso significa que precisamos acompanhar um número potencialmente ilimitado de regiões de memória não contínuas e não utilizadas, conforme ilustrado pelo seguinte exemplo: + +[heap-intro]: @/edition-2/posts/10-heap-allocation/index.md#dynamic-memory + +![](allocation-fragmentation.svg) + +O gráfico mostra o heap ao longo do tempo. No início, o heap completo está não utilizado, e o endereço `next` é igual a `heap_start` (linha 1). Então a primeira alocação ocorre (linha 2). Na linha 3, um segundo bloco de memória é alocado e a primeira alocação é liberada. Muitas mais alocações são adicionadas na linha 4. Metade delas tem vida muito curta e já são liberadas na linha 5, onde outra nova alocação também é adicionada. + +A linha 5 mostra o problema fundamental: Temos cinco regiões de memória não utilizadas com tamanhos diferentes, mas o ponteiro `next` só pode apontar para o início da última região. Embora pudéssemos armazenar os endereços iniciais e tamanhos das outras regiões de memória não utilizadas em um array de tamanho 4 para este exemplo, isso não é uma solução geral, já que poderíamos facilmente criar um exemplo com 8, 16 ou 1000 regiões de memória não utilizadas. + +Normalmente, quando temos um número potencialmente ilimitado de itens, podemos simplesmente usar uma coleção alocada no heap. Isso não é realmente possível no nosso caso, já que o alocador heap não pode depender de si mesmo (isso causaria recursão infinita ou deadlocks). Então precisamos encontrar uma solução diferente. + +## Alocador de Lista Encadeada + +Um truque comum para acompanhar um número arbitrário de áreas de memória livres ao implementar alocadores é usar essas áreas em si como armazenamento de suporte. Isso utiliza o fato de que as regiões ainda estão mapeadas para um endereço virtual e apoiadas por um frame físico, mas a informação armazenada não é mais necessária. Ao armazenar a informação sobre a região liberada na própria região, podemos acompanhar um número ilimitado de regiões liberadas sem precisar de memória adicional. + +A abordagem de implementação mais comum é construir uma lista encadeada única na memória liberada, com cada nó sendo uma região de memória liberada: + +![](linked-list-allocation.svg) + +Cada nó da lista contém dois campos: o tamanho da região de memória e um ponteiro para a próxima região de memória não utilizada. Com esta abordagem, só precisamos de um ponteiro para a primeira região não utilizada (chamada `head`) para acompanhar todas as regiões não utilizadas, independentemente de seu número. A estrutura de dados resultante é frequentemente chamada de [_lista livre_]. + +[_lista livre_]: https://en.wikipedia.org/wiki/Free_list + +Como você pode adivinhar pelo nome, esta é a técnica que a crate `linked_list_allocator` usa. Alocadores que usam esta técnica também são frequentemente chamados de _alocadores de pool_. + +### Implementação + +A seguir, criaremos nosso próprio tipo simples `LinkedListAllocator` que usa a abordagem acima para acompanhar regiões de memória liberadas. Esta parte do post não é necessária para posts futuros, então você pode pular os detalhes de implementação se quiser. + +#### O Tipo Alocador + +Começamos criando uma struct privada `ListNode` em um novo submódulo `allocator::linked_list`: + +```rust +// em src/allocator.rs + +pub mod linked_list; +``` + +```rust +// em src/allocator/linked_list.rs + +struct ListNode { + size: usize, + next: Option<&'static mut ListNode>, +} +``` + +Como no gráfico, um nó da lista tem um campo `size` e um ponteiro opcional para o próximo nó, representado pelo tipo `Option<&'static mut ListNode>`. O tipo `&'static mut` descreve semanticamente um objeto [possuído] por trás de um ponteiro. Basicamente, é um [`Box`] sem um destruidor que libera o objeto no final do escopo. + +[possuído]: https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html +[`Box`]: https://doc.rust-lang.org/alloc/boxed/index.html + +Implementamos o seguinte conjunto de métodos para `ListNode`: + +```rust +// em src/allocator/linked_list.rs + +impl ListNode { + const fn new(size: usize) -> Self { + ListNode { size, next: None } + } + + fn start_addr(&self) -> usize { + self as *const Self as usize + } + + fn end_addr(&self) -> usize { + self.start_addr() + self.size + } +} +``` + +O tipo tem uma simples função construtora chamada `new` e métodos para calcular os endereços inicial e final da região representada. Tornamos a função `new` uma [função const], que será necessária mais tarde ao construir um alocador de lista encadeada estático. + +[função const]: https://doc.rust-lang.org/reference/items/functions.html#const-functions + +Com a struct `ListNode` como um bloco de construção, agora podemos criar a struct `LinkedListAllocator`: + +```rust +// em src/allocator/linked_list.rs + +pub struct LinkedListAllocator { + head: ListNode, +} + +impl LinkedListAllocator { + /// Cria um LinkedListAllocator vazio. + pub const fn new() -> Self { + Self { + head: ListNode::new(0), + } + } + + /// Inicializa o alocador com os limites de heap fornecidos. + /// + /// Esta função é unsafe porque o chamador deve garantir que os + /// limites de heap fornecidos sejam válidos e que o heap esteja não utilizado. Este método deve ser + /// chamado apenas uma vez. + pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) { + unsafe { + self.add_free_region(heap_start, heap_size); + } + } + + /// Adiciona a região de memória fornecida à frente da lista. + unsafe fn add_free_region(&mut self, addr: usize, size: usize) { + todo!(); + } +} +``` + +A struct contém um nó `head` que aponta para a primeira região heap. Estamos interessados apenas no valor do ponteiro `next`, então definimos o `size` como 0 na função `ListNode::new`. Tornar `head` um `ListNode` em vez de apenas um `&'static mut ListNode` tem a vantagem de que a implementação do método `alloc` será mais simples. + +Como para o alocador bump, a função `new` não inicializa o alocador com os limites do heap. Além de manter compatibilidade com a API, a razão é que a rotina de inicialização requer escrever um nó na memória heap, o que só pode acontecer em tempo de execução. A função `new`, no entanto, precisa ser uma [função `const`] que pode ser avaliada em tempo de compilação porque será usada para inicializar o static `ALLOCATOR`. Por essa razão, fornecemos novamente um método `init` separado e não constante. + +[função `const`]: https://doc.rust-lang.org/reference/items/functions.html#const-functions + +O método `init` usa um método `add_free_region`, cuja implementação será mostrada em um momento. Por enquanto, usamos a macro [`todo!`] para fornecer uma implementação placeholder que sempre entra em pânico. + +[`todo!`]: https://doc.rust-lang.org/core/macro.todo.html + +#### O Método `add_free_region` + +O método `add_free_region` fornece a operação fundamental de _push_ na lista encadeada. Atualmente só chamamos este método de `init`, mas ele também será o método central em nossa implementação de `dealloc`. Lembre-se, o método `dealloc` é chamado quando uma região de memória alocada é liberada novamente. Para acompanhar esta região de memória liberada, queremos empurrá-la para a lista encadeada. + +A implementação do método `add_free_region` se parece com isso: + +```rust +// em src/allocator/linked_list.rs + +use super::align_up; +use core::mem; + +impl LinkedListAllocator { + /// Adiciona a região de memória fornecida à frente da lista. + unsafe fn add_free_region(&mut self, addr: usize, size: usize) { + // garantir que a região liberada seja capaz de conter ListNode + assert_eq!(align_up(addr, mem::align_of::()), addr); + assert!(size >= mem::size_of::()); + + // criar um novo nó da lista e anexá-lo no início da lista + let mut node = ListNode::new(size); + node.next = self.head.next.take(); + let node_ptr = addr as *mut ListNode; + unsafe { + node_ptr.write(node); + self.head.next = Some(&mut *node_ptr) + } + } +} +``` + +O método recebe o endereço e tamanho de uma região de memória como argumento e a adiciona à frente da lista. Primeiro, ele garante que a região fornecida tenha o tamanho e alinhamento necessários para armazenar um `ListNode`. Então ele cria o nó e o insere na lista através dos seguintes passos: + +![](linked-list-allocator-push.svg) + +O passo 0 mostra o estado do heap antes de `add_free_region` ser chamado. No passo 1, o método é chamado com a região de memória marcada como `freed` no gráfico. Após as verificações iniciais, o método cria um novo `node` em sua pilha com o tamanho da região liberada. Então ele usa o método [`Option::take`] para definir o ponteiro `next` do nó para o ponteiro `head` atual, redefinindo assim o ponteiro `head` para `None`. + +[`Option::take`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.take + +No passo 2, o método escreve o `node` recém-criado no início da região de memória liberada através do método [`write`]. Então ele aponta o ponteiro `head` para o novo nó. A estrutura de ponteiros resultante parece um pouco caótica porque a região liberada é sempre inserida no início da lista, mas se seguirmos os ponteiros, vemos que cada região livre ainda é alcançável a partir do ponteiro `head`. + +[`write`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write + +#### O Método `find_region` + +A segunda operação fundamental em uma lista encadeada é encontrar uma entrada e removê-la da lista. Esta é a operação central necessária para implementar o método `alloc`. Implementamos a operação como um método `find_region` da seguinte maneira: + +```rust +// em src/allocator/linked_list.rs + +impl LinkedListAllocator { + /// Procura por uma região livre com o tamanho e alinhamento fornecidos e a remove + /// da lista. + /// + /// Retorna uma tupla do nó da lista e o endereço inicial da alocação. + fn find_region(&mut self, size: usize, align: usize) + -> Option<(&'static mut ListNode, usize)> + { + // referência ao nó atual da lista, atualizada para cada iteração + let mut current = &mut self.head; + // procurar uma região de memória grande o suficiente na lista encadeada + while let Some(ref mut region) = current.next { + if let Ok(alloc_start) = Self::alloc_from_region(®ion, size, align) { + // região adequada para alocação -> remover nó da lista + let next = region.next.take(); + let ret = Some((current.next.take().unwrap(), alloc_start)); + current.next = next; + return ret; + } else { + // região não adequada -> continuar com a próxima região + current = current.next.as_mut().unwrap(); + } + } + + // nenhuma região adequada encontrada + None + } +} +``` + +O método usa uma variável `current` e um [loop `while let`] para iterar sobre os elementos da lista. No início, `current` é definido como o nó `head` (dummy). Em cada iteração, ele é então atualizado para o campo `next` do nó atual (no bloco `else`). Se a região for adequada para uma alocação com o tamanho e alinhamento fornecidos, a região é removida da lista e retornada junto com o endereço `alloc_start`. + +[loop `while let`]: https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops + +Quando o ponteiro `current.next` se torna `None`, o loop sai. Isso significa que iteramos sobre toda a lista mas não encontramos nenhuma região adequada para uma alocação. Nesse caso, retornamos `None`. Se uma região é adequada é verificado pela função `alloc_from_region`, cuja implementação será mostrada em um momento. + +Vamos dar uma olhada mais detalhada em como uma região adequada é removida da lista: + +![](linked-list-allocator-remove-region.svg) + +O passo 0 mostra a situação antes de quaisquer ajustes de ponteiros. As regiões `region` e `current` e os ponteiros `region.next` e `current.next` estão marcados no gráfico. No passo 1, tanto o ponteiro `region.next` quanto `current.next` são redefinidos para `None` usando o método [`Option::take`]. Os ponteiros originais são armazenados em variáveis locais chamadas `next` e `ret`. + +No passo 2, o ponteiro `current.next` é definido para o ponteiro local `next`, que é o ponteiro original `region.next`. O efeito é que `current` agora aponta diretamente para a região depois de `region`, de modo que `region` não é mais um elemento da lista encadeada. A função então retorna o ponteiro para `region` armazenado na variável local `ret`. + +##### A Função `alloc_from_region` + +A função `alloc_from_region` retorna se uma região é adequada para uma alocação com um dado tamanho e alinhamento. Ela é definida assim: + +```rust +// em src/allocator/linked_list.rs + +impl LinkedListAllocator { + /// Tenta usar a região fornecida para uma alocação com tamanho e + /// alinhamento dados. + /// + /// Retorna o endereço inicial da alocação em caso de sucesso. + fn alloc_from_region(region: &ListNode, size: usize, align: usize) + -> Result + { + let alloc_start = align_up(region.start_addr(), align); + let alloc_end = alloc_start.checked_add(size).ok_or(())?; + + if alloc_end > region.end_addr() { + // região muito pequena + return Err(()); + } + + let excess_size = region.end_addr() - alloc_end; + if excess_size > 0 && excess_size < mem::size_of::() { + // resto da região muito pequeno para conter um ListNode (necessário porque a + // alocação divide a região em uma parte usada e uma parte livre) + return Err(()); + } + + // região adequada para alocação + Ok(alloc_start) + } +} +``` + +Primeiro, a função calcula os endereços inicial e final de uma alocação potencial, usando a função `align_up` que definimos anteriormente e o método [`checked_add`]. Se ocorrer um overflow ou se o endereço final estiver além do endereço final da região, a alocação não cabe na região e retornamos um erro. + +A função realiza uma verificação menos óbvia depois disso. Esta verificação é necessária porque na maioria das vezes uma alocação não se encaixa perfeitamente em uma região adequada, de modo que uma parte da região permanece utilizável após a alocação. Esta parte da região deve armazenar seu próprio `ListNode` após a alocação, então deve ser grande o suficiente para fazê-lo. A verificação verifica exatamente isso: ou a alocação se encaixa perfeitamente (`excess_size == 0`) ou o tamanho excedente é grande o suficiente para armazenar um `ListNode`. + +#### Implementando `GlobalAlloc` + +Com as operações fundamentais fornecidas pelos métodos `add_free_region` e `find_region`, agora podemos finalmente implementar a trait `GlobalAlloc`. Como com o alocador bump, não implementamos a trait diretamente para o `LinkedListAllocator`, mas apenas para um `Locked` envolvido. O [wrapper `Locked`] adiciona mutabilidade interior através de um spinlock, que nos permite modificar a instância do alocador mesmo que os métodos `alloc` e `dealloc` recebam apenas referências `&self`. + +[wrapper `Locked`]: @/edition-2/posts/11-allocator-designs/index.md#a-locked-wrapper-type + +A implementação se parece com isso: + +```rust +// em src/allocator/linked_list.rs + +use super::Locked; +use alloc::alloc::{GlobalAlloc, Layout}; +use core::ptr; + +unsafe impl GlobalAlloc for Locked { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + // realizar ajustes de layout + let (size, align) = LinkedListAllocator::size_align(layout); + let mut allocator = self.lock(); + + if let Some((region, alloc_start)) = allocator.find_region(size, align) { + let alloc_end = alloc_start.checked_add(size).expect("overflow"); + let excess_size = region.end_addr() - alloc_end; + if excess_size > 0 { + unsafe { + allocator.add_free_region(alloc_end, excess_size); + } + } + alloc_start as *mut u8 + } else { + ptr::null_mut() + } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + // realizar ajustes de layout + let (size, _) = LinkedListAllocator::size_align(layout); + + unsafe { self.lock().add_free_region(ptr as usize, size) } + } +} +``` + +Vamos começar com o método `dealloc` porque ele é mais simples: Primeiro, ele realiza alguns ajustes de layout, que explicaremos em um momento. Então, ele recupera uma referência `&mut LinkedListAllocator` chamando a função [`Mutex::lock`] no [wrapper `Locked`]. Por último, ele chama a função `add_free_region` para adicionar a região desalocada à lista livre. + +O método `alloc` é um pouco mais complexo. Ele começa com os mesmos ajustes de layout e também chama a função [`Mutex::lock`] para receber uma referência mutável do alocador. Então ele usa o método `find_region` para encontrar uma região de memória adequada para a alocação e removê-la da lista. Se isso não tiver sucesso e `None` for retornado, ele retorna `null_mut` para sinalizar um erro, já que não há nenhuma região de memória adequada. + +No caso de sucesso, o método `find_region` retorna uma tupla da região adequada (não mais na lista) e do endereço inicial da alocação. Usando `alloc_start`, o tamanho da alocação e o endereço final da região, ele calcula o endereço final da alocação e o tamanho excedente novamente. Se o tamanho excedente não for nulo, ele chama `add_free_region` para adicionar o tamanho excedente da região de memória de volta à lista livre. Finalmente, ele retorna o endereço `alloc_start` convertido como um ponteiro `*mut u8`. + +#### Ajustes de Layout + +Então, o que são esses ajustes de layout que fazemos no início de tanto `alloc` quanto `dealloc`? Eles garantem que cada bloco alocado é capaz de armazenar um `ListNode`. Isso é importante porque o bloco de memória vai ser desalocado em algum ponto, onde queremos escrever um `ListNode` nele. Se o bloco for menor que um `ListNode` ou não tiver o alinhamento correto, comportamento indefinido pode ocorrer. + +Os ajustes de layout são realizados pela função `size_align`, que é definida assim: + +```rust +// em src/allocator/linked_list.rs + +impl LinkedListAllocator { + /// Ajusta o layout fornecido para que a região de memória alocada resultante + /// também seja capaz de armazenar um `ListNode`. + /// + /// Retorna o tamanho e alinhamento ajustados como uma tupla (size, align). + fn size_align(layout: Layout) -> (usize, usize) { + let layout = layout + .align_to(mem::align_of::()) + .expect("adjusting alignment failed") + .pad_to_align(); + let size = layout.size().max(mem::size_of::()); + (size, layout.align()) + } +} +``` + +Primeiro, a função usa o método [`align_to`] no [`Layout`] passado para aumentar o alinhamento para o alinhamento de um `ListNode` se necessário. Então ela usa o método [`pad_to_align`] para arredondar o tamanho para um múltiplo do alinhamento para garantir que o endereço inicial do próximo bloco de memória também terá o alinhamento correto para armazenar um `ListNode`. +No segundo passo, ela usa o método [`max`] para impor um tamanho mínimo de alocação de `mem::size_of::`. Dessa forma, a função `dealloc` pode com segurança escrever um `ListNode` no bloco de memória liberado. + +[`align_to`]: https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.align_to +[`pad_to_align`]: https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.pad_to_align +[`max`]: https://doc.rust-lang.org/std/cmp/trait.Ord.html#method.max + +### Usando-o + +Agora podemos atualizar o static `ALLOCATOR` no módulo `allocator` para usar nosso novo `LinkedListAllocator`: + +```rust +// em src/allocator.rs + +use linked_list::LinkedListAllocator; + +#[global_allocator] +static ALLOCATOR: Locked = + Locked::new(LinkedListAllocator::new()); +``` + +Como a função `init` se comporta da mesma forma para os alocadores bump e de lista encadeada, não precisamos modificar a chamada `init` em `init_heap`. + +Quando agora executamos nossos testes `heap_allocation` novamente, vemos que todos os testes passam agora, incluindo o teste `many_boxes_long_lived` que falhou com o alocador bump: + +``` +> cargo test --test heap_allocation +simple_allocation... [ok] +large_vec... [ok] +many_boxes... [ok] +many_boxes_long_lived... [ok] +``` + +Isso mostra que nosso alocador de lista encadeada é capaz de reutilizar memória liberada para alocações subsequentes. + +### Discussão + +Em contraste com o alocador bump, o alocador de lista encadeada é muito mais adequado como um alocador de propósito geral, principalmente porque é capaz de reutilizar diretamente memória liberada. No entanto, ele também tem algumas desvantagens. Algumas delas são causadas apenas pela nossa implementação básica, mas também existem desvantagens fundamentais do próprio design do alocador. + +#### Mesclando Blocos Liberados + +O principal problema com nossa implementação é que ela apenas divide o heap em blocos menores, mas nunca os mescla de volta juntos. Considere este exemplo: + +![](linked-list-allocator-fragmentation-on-dealloc.svg) + +Na primeira linha, três alocações são criadas no heap. Duas delas são liberadas novamente na linha 2 e a terceira é liberada na linha 3. Agora o heap completo está não utilizado novamente, mas ainda está dividido em quatro blocos individuais. Neste ponto, uma alocação grande pode não ser mais possível porque nenhum dos quatro blocos é grande o suficiente. Ao longo do tempo, o processo continua, e o heap é dividido em blocos cada vez menores. Em algum ponto, o heap fica tão fragmentado que até alocações de tamanho normal falharão. + +Para corrigir este problema, precisamos mesclar blocos adjacentes liberados de volta juntos. Para o exemplo acima, isso significaria o seguinte: + +![](linked-list-allocator-merge-on-dealloc.svg) + +Como antes, duas das três alocações são liberadas na linha `2`. Em vez de manter o heap fragmentado, agora realizamos um passo adicional na linha `2a` para mesclar os dois blocos mais à direita de volta juntos. Na linha `3`, a terceira alocação é liberada (como antes), resultando em um heap completamente não utilizado representado por três blocos distintos. Em um passo de mesclagem adicional na linha `3a`, então mesclamos os três blocos adjacentes de volta juntos. + +A crate `linked_list_allocator` implementa esta estratégia de mesclagem da seguinte maneira: Em vez de inserir blocos de memória liberados no início da lista encadeada em `deallocate`, ela sempre mantém a lista ordenada por endereço inicial. Dessa forma, a mesclagem pode ser realizada diretamente na chamada `deallocate` examinando os endereços e tamanhos dos dois blocos vizinhos na lista. É claro que a operação de desalocação é mais lenta dessa forma, mas previne a fragmentação heap que vimos acima. + +#### Desempenho + +Como aprendemos acima, o alocador bump é extremamente rápido e pode ser otimizado para apenas algumas operações assembly. O alocador de lista encadeada tem um desempenho muito pior nesta categoria. O problema é que uma requisição de alocação pode precisar percorrer a lista encadeada completa até encontrar um bloco adequado. + +Como o comprimento da lista depende do número de blocos de memória não utilizados, o desempenho pode variar extremamente para diferentes programas. Um programa que cria apenas algumas alocações experimentará um desempenho de alocação relativamente rápido. Para um programa que fragmenta o heap com muitas alocações, no entanto, o desempenho de alocação será muito ruim porque a lista encadeada será muito longa e conterá principalmente blocos muito pequenos. + +Vale a pena notar que este problema de desempenho não é um problema causado pela nossa implementação básica, mas um problema fundamental da abordagem de lista encadeada. Como o desempenho de alocação pode ser muito importante para código a nível de kernel, exploramos um terceiro design de alocador a seguir que troca utilização de memória melhorada por desempenho reduzido. + +## Alocador de Bloco de Tamanho Fixo + +A seguir, apresentamos um design de alocador que usa blocos de memória de tamanho fixo para atender requisições de alocação. Dessa forma, o alocador frequentemente retorna blocos que são maiores do que necessário para alocações, o que resulta em memória desperdiçada devido à [fragmentação interna]. Por outro lado, ele reduz drasticamente o tempo necessário para encontrar um bloco adequado (comparado ao alocador de lista encadeada), resultando em muito melhor desempenho de alocação. + +### Introdução + +A ideia por trás de um _alocador de bloco de tamanho fixo_ é a seguinte: Em vez de alocar exatamente a quantidade de memória solicitada, definimos um pequeno número de tamanhos de bloco e arredondamos cada alocação para cima até o próximo tamanho de bloco. Por exemplo, com tamanhos de bloco de 16, 64 e 512 bytes, uma alocação de 4 bytes retornaria um bloco de 16 bytes, uma alocação de 48 bytes um bloco de 64 bytes, e uma alocação de 128 bytes um bloco de 512 bytes. + +Como o alocador de lista encadeada, mantemos o controle da memória não utilizada criando uma lista encadeada na memória não utilizada. No entanto, em vez de usar uma única lista com diferentes tamanhos de bloco, criamos uma lista separada para cada classe de tamanho. Cada lista então armazena apenas blocos de um único tamanho. Por exemplo, com tamanhos de bloco de 16, 64 e 512, haveria três listas encadeadas separadas na memória: + +![](fixed-size-block-example.svg). + +Em vez de um único ponteiro `head`, temos os três ponteiros head `head_16`, `head_64` e `head_512` que cada um aponta para o primeiro bloco não utilizado do tamanho correspondente. Todos os nós em uma única lista têm o mesmo tamanho. Por exemplo, a lista iniciada pelo ponteiro `head_16` contém apenas blocos de 16 bytes. Isso significa que não precisamos mais armazenar o tamanho em cada nó da lista, já que ele já está especificado pelo nome do ponteiro head. + +Como cada elemento em uma lista tem o mesmo tamanho, cada elemento da lista é igualmente adequado para uma requisição de alocação. Isso significa que podemos realizar uma alocação de forma muito eficiente usando os seguintes passos: + +- Arredondar o tamanho de alocação solicitado para cima até o próximo tamanho de bloco. Por exemplo, quando uma alocação de 12 bytes é solicitada, escolheríamos o tamanho de bloco de 16 no exemplo acima. +- Recuperar o ponteiro head para a lista, por exemplo, para tamanho de bloco 16, precisamos usar `head_16`. +- Remover o primeiro bloco da lista e retorná-lo. + +Mais notavelmente, sempre podemos retornar o primeiro elemento da lista e não precisamos mais percorrer a lista completa. Assim, alocações são muito mais rápidas do que com o alocador de lista encadeada. + +#### Tamanhos de Bloco e Memória Desperdiçada + +Dependendo dos tamanhos de bloco, perdemos muita memória ao arredondar para cima. Por exemplo, quando um bloco de 512 bytes é retornado para uma alocação de 128 bytes, três quartos da memória alocada estão não utilizados. Ao definir tamanhos de bloco razoáveis, é possível limitar a quantidade de memória desperdiçada até certo ponto. Por exemplo, ao usar as potências de 2 (4, 8, 16, 32, 64, 128, …) como tamanhos de bloco, podemos limitar o desperdício de memória a metade do tamanho de alocação no pior caso e um quarto do tamanho de alocação no caso médio. + +Também é comum otimizar tamanhos de bloco com base em tamanhos de alocação comuns em um programa. Por exemplo, poderíamos adicionar adicionalmente o tamanho de bloco 24 para melhorar o uso de memória para programas que frequentemente realizam alocações de 24 bytes. Dessa forma, a quantidade de memória desperdiçada frequentemente pode ser reduzida sem perder os benefícios de desempenho. + +#### Desalocação + +Assim como a alocação, a desalocação também é muito performática. Ela envolve os seguintes passos: + +- Arredondar o tamanho de alocação liberado para cima até o próximo tamanho de bloco. Isso é necessário já que o compilador passa apenas o tamanho de alocação solicitado para `dealloc`, não o tamanho do bloco que foi retornado por `alloc`. Ao usar a mesma função de ajuste de tamanho em tanto `alloc` quanto `dealloc`, podemos garantir que sempre liberamos a quantidade correta de memória. +- Recuperar o ponteiro head para a lista. +- Adicionar o bloco liberado à frente da lista atualizando o ponteiro head. + +Mais notavelmente, nenhum percurso da lista é necessário para desalocação também. Isso significa que o tempo necessário para uma chamada `dealloc` permanece o mesmo independentemente do comprimento da lista. + +#### Alocador de Fallback + +Dado que alocações grandes (>2 KB) são frequentemente raras, especialmente em kernels de sistemas operacionais, pode fazer sentido recorrer a um alocador diferente para essas alocações. Por exemplo, poderíamos recorrer a um alocador de lista encadeada para alocações maiores que 2048 bytes a fim de reduzir o desperdício de memória. Como apenas muito poucas alocações desse tamanho são esperadas, a lista encadeada permaneceria pequena e as (des)alocações ainda seriam razoavelmente rápidas. + +#### Criando Novos Blocos + +Acima, sempre assumimos que há blocos suficientes de um tamanho específico na lista para atender todas as requisições de alocação. No entanto, em algum ponto, a lista encadeada para um determinado tamanho de bloco fica vazia. Neste ponto, existem duas maneiras pelas quais podemos criar novos blocos não utilizados de um tamanho específico para atender uma requisição de alocação: + +- Alocar um novo bloco do alocador de fallback (se houver um). +- Dividir um bloco maior de uma lista diferente. Isso funciona melhor se os tamanhos de bloco forem potências de dois. Por exemplo, um bloco de 32 bytes pode ser dividido em dois blocos de 16 bytes. + +Para nossa implementação, alocaremos novos blocos do alocador de fallback, já que a implementação é muito mais simples. + +### Implementação + +Agora que sabemos como um alocador de bloco de tamanho fixo funciona, podemos começar nossa implementação. Não dependeremos da implementação do alocador de lista encadeada criado na seção anterior, então você pode seguir esta parte mesmo se pulou a implementação do alocador de lista encadeada. + +#### Nó da Lista + +Começamos nossa implementação criando um tipo `ListNode` em um novo módulo `allocator::fixed_size_block`: + +```rust +// em src/allocator.rs + +pub mod fixed_size_block; +``` + +```rust +// em src/allocator/fixed_size_block.rs + +struct ListNode { + next: Option<&'static mut ListNode>, +} +``` + +Este tipo é similar ao tipo `ListNode` de nossa [implementação de alocador de lista encadeada], com a diferença de que não temos um campo `size`. Ele não é necessário porque cada bloco em uma lista tem o mesmo tamanho com o design de alocador de bloco de tamanho fixo. + +[implementação de alocador de lista encadeada]: #o-tipo-alocador + +#### Tamanhos de Bloco + +Em seguida, definimos uma slice constante `BLOCK_SIZES` com os tamanhos de bloco usados para nossa implementação: + +```rust +// em src/allocator/fixed_size_block.rs + +/// Os tamanhos de bloco a usar. +/// +/// Os tamanhos devem cada um ser potência de 2 porque também são usados como +/// o alinhamento de bloco (alinhamentos devem ser sempre potências de 2). +const BLOCK_SIZES: &[usize] = &[8, 16, 32, 64, 128, 256, 512, 1024, 2048]; +``` + +Como tamanhos de bloco, usamos potências de 2, começando de 8 até 2048. Não definimos tamanhos de bloco menores que 8 porque cada bloco deve ser capaz de armazenar um ponteiro de 64 bits para o próximo bloco quando liberado. Para alocações maiores que 2048 bytes, recorreremos a um alocador de lista encadeada. + +Para simplificar a implementação, definimos o tamanho de um bloco como seu alinhamento necessário na memória. Então um bloco de 16 bytes sempre está alinhado em um limite de 16 bytes e um bloco de 512 bytes está alinhado em um limite de 512 bytes. Como alinhamentos sempre precisam ser potências de 2, isso exclui quaisquer outros tamanhos de bloco. Se precisarmos de tamanhos de bloco que não são potências de 2 no futuro, ainda podemos ajustar nossa implementação para isso (por exemplo, definindo um segundo array `BLOCK_ALIGNMENTS`). + +#### O Tipo Alocador + +Usando o tipo `ListNode` e a slice `BLOCK_SIZES`, agora podemos definir nosso tipo alocador: + +```rust +// em src/allocator/fixed_size_block.rs + +pub struct FixedSizeBlockAllocator { + list_heads: [Option<&'static mut ListNode>; BLOCK_SIZES.len()], + fallback_allocator: linked_list_allocator::Heap, +} +``` + +O campo `list_heads` é um array de ponteiros `head`, um para cada tamanho de bloco. Isso é implementado usando o `len()` da slice `BLOCK_SIZES` como o comprimento do array. Como um alocador de fallback para alocações maiores que o maior tamanho de bloco, usamos o alocador fornecido pela crate `linked_list_allocator`. Também poderíamos usar o `LinkedListAllocator` que implementamos nós mesmos em vez disso, mas ele tem a desvantagem de que não [mescla blocos liberados]. + +[mescla blocos liberados]: #mesclando-blocos-liberados + +Para construir um `FixedSizeBlockAllocator`, fornecemos as mesmas funções `new` e `init` que implementamos para os outros tipos de alocadores também: + +```rust +// em src/allocator/fixed_size_block.rs + +impl FixedSizeBlockAllocator { + /// Cria um FixedSizeBlockAllocator vazio. + pub const fn new() -> Self { + const EMPTY: Option<&'static mut ListNode> = None; + FixedSizeBlockAllocator { + list_heads: [EMPTY; BLOCK_SIZES.len()], + fallback_allocator: linked_list_allocator::Heap::empty(), + } + } + + /// Inicializa o alocador com os limites de heap fornecidos. + /// + /// Esta função é unsafe porque o chamador deve garantir que os + /// limites de heap fornecidos sejam válidos e que o heap esteja não utilizado. Este método deve ser + /// chamado apenas uma vez. + pub unsafe fn init(&mut self, heap_start: usize, heap_size: usize) { + unsafe { self.fallback_allocator.init(heap_start, heap_size); } + } +} +``` + +A função `new` apenas inicializa o array `list_heads` com nós vazios e cria um alocador de lista encadeada [`empty`] como `fallback_allocator`. A constante `EMPTY` é necessária para dizer ao compilador Rust que queremos inicializar o array com um valor constante. Inicializar o array diretamente como `[None; BLOCK_SIZES.len()]` não funciona, porque então o compilador exigiria que `Option<&'static mut ListNode>` implementasse a trait `Copy`, o que ele não faz. Esta é uma limitação atual do compilador Rust, que pode desaparecer no futuro. + +[`empty`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.empty + +A função `init` unsafe apenas chama a função [`init`] do `fallback_allocator` sem fazer nenhuma inicialização adicional do array `list_heads`. Em vez disso, inicializaremos as listas preguiçosamente em chamadas `alloc` e `dealloc`. + +[`init`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.init + +Por conveniência, também criamos um método privado `fallback_alloc` que aloca usando o `fallback_allocator`: + +```rust +// em src/allocator/fixed_size_block.rs + +use alloc::alloc::Layout; +use core::ptr; + +impl FixedSizeBlockAllocator { + /// Aloca usando o alocador de fallback. + fn fallback_alloc(&mut self, layout: Layout) -> *mut u8 { + match self.fallback_allocator.allocate_first_fit(layout) { + Ok(ptr) => ptr.as_ptr(), + Err(_) => ptr::null_mut(), + } + } +} +``` + +O tipo [`Heap`] da crate `linked_list_allocator` não implementa [`GlobalAlloc`] (já que [não é possível sem bloqueio]). Em vez disso, ele fornece um método [`allocate_first_fit`] que tem uma interface ligeiramente diferente. Em vez de retornar um `*mut u8` e usar um ponteiro nulo para sinalizar um erro, ele retorna um `Result, ()>`. O tipo [`NonNull`] é uma abstração para um ponteiro bruto que é garantido de não ser um ponteiro nulo. Ao mapear o caso `Ok` para o método [`NonNull::as_ptr`] e o caso `Err` para um ponteiro nulo, podemos facilmente traduzir isso de volta para um tipo `*mut u8`. + +[`Heap`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html +[não é possível sem bloqueio]: #globalalloc-e-mutabilidade +[`allocate_first_fit`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.allocate_first_fit +[`NonNull`]: https://doc.rust-lang.org/nightly/core/ptr/struct.NonNull.html +[`NonNull::as_ptr`]: https://doc.rust-lang.org/nightly/core/ptr/struct.NonNull.html#method.as_ptr + +#### Calculando o Índice da Lista + +Antes de implementarmos a trait `GlobalAlloc`, definimos uma função auxiliar `list_index` que retorna o menor tamanho de bloco possível para um dado [`Layout`]: + +```rust +// em src/allocator/fixed_size_block.rs + +/// Escolhe um tamanho de bloco apropriado para o layout fornecido. +/// +/// Retorna um índice no array `BLOCK_SIZES`. +fn list_index(layout: &Layout) -> Option { + let required_block_size = layout.size().max(layout.align()); + BLOCK_SIZES.iter().position(|&s| s >= required_block_size) +} +``` + +O bloco deve ter pelo menos o tamanho e alinhamento exigidos pelo `Layout` fornecido. Como definimos que o tamanho do bloco também é seu alinhamento, isso significa que o `required_block_size` é o [máximo] dos atributos [`size()`] e [`align()`] do layout. Para encontrar o próximo bloco maior na slice `BLOCK_SIZES`, primeiro usamos o método [`iter()`] para obter um iterador e então o método [`position()`] para encontrar o índice do primeiro bloco que é pelo menos tão grande quanto o `required_block_size`. + +[máximo]: https://doc.rust-lang.org/core/cmp/trait.Ord.html#method.max +[`size()`]: https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.size +[`align()`]: https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.align +[`iter()`]: https://doc.rust-lang.org/std/primitive.slice.html#method.iter +[`position()`]: https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.position + +Note que não retornamos o próprio tamanho de bloco, mas o índice na slice `BLOCK_SIZES`. A razão é que queremos usar o índice retornado como um índice no array `list_heads`. + +#### Implementando `GlobalAlloc` + +O último passo é implementar a trait `GlobalAlloc`: + +```rust +// em src/allocator/fixed_size_block.rs + +use super::Locked; +use alloc::alloc::GlobalAlloc; + +unsafe impl GlobalAlloc for Locked { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + todo!(); + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + todo!(); + } +} +``` + +Como para os outros alocadores, não implementamos a trait `GlobalAlloc` diretamente para nosso tipo alocador, mas usamos o [wrapper `Locked`] para adicionar mutabilidade interior sincronizada. Como as implementações de `alloc` e `dealloc` são relativamente grandes, as introduzimos uma por uma a seguir. + +##### `alloc` + +A implementação do método `alloc` se parece com isso: + +```rust +// no bloco `impl` em src/allocator/fixed_size_block.rs + +unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let mut allocator = self.lock(); + match list_index(&layout) { + Some(index) => { + match allocator.list_heads[index].take() { + Some(node) => { + allocator.list_heads[index] = node.next.take(); + node as *mut ListNode as *mut u8 + } + None => { + // nenhum bloco existe na lista => alocar novo bloco + let block_size = BLOCK_SIZES[index]; + // só funciona se todos os tamanhos de bloco forem uma potência de 2 + let block_align = block_size; + let layout = Layout::from_size_align(block_size, block_align) + .unwrap(); + allocator.fallback_alloc(layout) + } + } + } + None => allocator.fallback_alloc(layout), + } +} +``` + +Vamos passar por isso passo a passo: + +Primeiro, usamos o método `Locked::lock` para obter uma referência mutável à instância do alocador envolvido. Em seguida, chamamos a função `list_index` que acabamos de definir para calcular o tamanho de bloco apropriado para o layout fornecido e obter o índice correspondente no array `list_heads`. Se este índice for `None`, nenhum tamanho de bloco se encaixa para a alocação, portanto usamos o `fallback_allocator` usando a função `fallback_alloc`. + +Se o índice da lista for `Some`, tentamos remover o primeiro nó na lista correspondente iniciada por `list_heads[index]` usando o método [`Option::take`]. Se a lista não estiver vazia, entramos no branch `Some(node)` da instrução `match`, onde apontamos o ponteiro head da lista para o sucessor do `node` removido (usando [`take`][`Option::take`] novamente). Finalmente, retornamos o ponteiro `node` removido como um `*mut u8`. + +[`Option::take`]: https://doc.rust-lang.org/core/option/enum.Option.html#method.take + +Se o head da lista for `None`, indica que a lista de blocos está vazia. Isso significa que precisamos construir um novo bloco como [descrito acima](#criando-novos-blocos). Para isso, primeiro obtemos o tamanho do bloco atual da slice `BLOCK_SIZES` e o usamos como tanto o tamanho quanto o alinhamento para o novo bloco. Então criamos um novo `Layout` a partir dele e chamamos o método `fallback_alloc` para realizar a alocação. A razão para ajustar o layout e alinhamento é que o bloco será adicionado à lista de blocos na desalocação. + +#### `dealloc` + +A implementação do método `dealloc` se parece com isso: + +```rust +// em src/allocator/fixed_size_block.rs + +use core::{mem, ptr::NonNull}; + +// dentro do bloco `unsafe impl GlobalAlloc` + +unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + let mut allocator = self.lock(); + match list_index(&layout) { + Some(index) => { + let new_node = ListNode { + next: allocator.list_heads[index].take(), + }; + // verificar que o bloco tem tamanho e alinhamento necessários para armazenar nó + assert!(mem::size_of::() <= BLOCK_SIZES[index]); + assert!(mem::align_of::() <= BLOCK_SIZES[index]); + let new_node_ptr = ptr as *mut ListNode; + unsafe { + new_node_ptr.write(new_node); + allocator.list_heads[index] = Some(&mut *new_node_ptr); + } + } + None => { + let ptr = NonNull::new(ptr).unwrap(); + unsafe { + allocator.fallback_allocator.deallocate(ptr, layout); + } + } + } +} +``` + +Como em `alloc`, primeiro usamos o método `lock` para obter uma referência mutável do alocador e então a função `list_index` para obter a lista de blocos correspondente ao `Layout` fornecido. Se o índice for `None`, nenhum tamanho de bloco adequado existe em `BLOCK_SIZES`, o que indica que a alocação foi criada pelo alocador de fallback. Portanto, usamos seu método [`deallocate`][`Heap::deallocate`] para liberar a memória novamente. O método espera um [`NonNull`] em vez de um `*mut u8`, então precisamos converter o ponteiro primeiro. (A chamada `unwrap` só falha quando o ponteiro é nulo, o que nunca deve acontecer quando o compilador chama `dealloc`.) + +[`Heap::deallocate`]: https://docs.rs/linked_list_allocator/0.9.0/linked_list_allocator/struct.Heap.html#method.deallocate + +Se `list_index` retorna um índice de bloco, precisamos adicionar o bloco de memória liberado à lista. Para isso, primeiro criamos um novo `ListNode` que aponta para o head atual da lista (usando [`Option::take`] novamente). Antes de escrevermos o novo nó no bloco de memória liberado, primeiro afirmamos que o tamanho do bloco atual especificado por `index` tem o tamanho e alinhamento necessários para armazenar um `ListNode`. Então realizamos a escrita convertendo o ponteiro `*mut u8` fornecido para um ponteiro `*mut ListNode` e então chamando o método [`write`][`pointer::write`] unsafe nele. O último passo é definir o ponteiro head da lista, que atualmente é `None` já que chamamos `take` nele, para nosso `ListNode` recém-escrito. Para isso, convertemos o ponteiro bruto `new_node_ptr` para uma referência mutável. + +[`pointer::write`]: https://doc.rust-lang.org/std/primitive.pointer.html#method.write + +Há algumas coisas que vale a pena notar: + +- Não diferenciamos entre blocos alocados de uma lista de blocos e blocos alocados do alocador de fallback. Isso significa que novos blocos criados em `alloc` são adicionados à lista de blocos em `dealloc`, aumentando assim o número de blocos daquele tamanho. +- O método `alloc` é o único lugar onde novos blocos são criados em nossa implementação. Isso significa que inicialmente começamos com listas de blocos vazias e só preenchemos essas listas preguiçosamente quando alocações de seu tamanho de bloco são realizadas. +- Não precisamos de blocos `unsafe` em `alloc` e `dealloc`, mesmo que realizemos algumas operações `unsafe`. A razão é que Rust atualmente trata o corpo completo de funções unsafe como um grande bloco `unsafe`. Como usar blocos `unsafe` explícitos tem a vantagem de que é óbvio quais operações são unsafe e quais não são, há uma [RFC proposta](https://github.com/rust-lang/rfcs/pull/2585) para mudar este comportamento. + +### Usando-o + +Para usar nosso novo `FixedSizeBlockAllocator`, precisamos atualizar o static `ALLOCATOR` no módulo `allocator`: + +```rust +// em src/allocator.rs + +use fixed_size_block::FixedSizeBlockAllocator; + +#[global_allocator] +static ALLOCATOR: Locked = Locked::new( + FixedSizeBlockAllocator::new()); +``` + +Como a função `init` se comporta da mesma forma para todos os alocadores que implementamos, não precisamos modificar a chamada `init` em `init_heap`. + +Quando agora executamos nossos testes `heap_allocation` novamente, todos os testes ainda devem passar: + +``` +> cargo test --test heap_allocation +simple_allocation... [ok] +large_vec... [ok] +many_boxes... [ok] +many_boxes_long_lived... [ok] +``` + +Nosso novo alocador parece funcionar! + +### Discussão + +Embora a abordagem de bloco de tamanho fixo tenha um desempenho muito melhor do que a abordagem de lista encadeada, ela desperdiça até metade da memória ao usar potências de 2 como tamanhos de bloco. Se este trade-off vale a pena depende muito do tipo de aplicação. Para um kernel de sistema operacional, onde o desempenho é crítico, a abordagem de bloco de tamanho fixo parece ser a melhor escolha. + +No lado da implementação, existem várias coisas que poderíamos melhorar em nossa implementação atual: + +- Em vez de alocar blocos preguiçosamente apenas usando o alocador de fallback, pode ser melhor pré-preencher as listas para melhorar o desempenho das alocações iniciais. +- Para simplificar a implementação, permitimos apenas tamanhos de bloco que são potências de 2 para que também possamos usá-los como o alinhamento do bloco. Ao armazenar (ou calcular) o alinhamento de uma maneira diferente, também poderíamos permitir outros tamanhos de bloco arbitrários. Dessa forma, poderíamos adicionar mais tamanhos de bloco, por exemplo, para tamanhos de alocação comuns, a fim de minimizar a memória desperdiçada. +- Atualmente apenas criamos novos blocos, mas nunca os liberamos novamente. Isso resulta em fragmentação e pode eventualmente resultar em falha de alocação para alocações grandes. Pode fazer sentido impor um comprimento máximo de lista para cada tamanho de bloco. Quando o comprimento máximo é atingido, desalocações subsequentes são liberadas usando o alocador de fallback em vez de serem adicionadas à lista. +- Em vez de recorrer a um alocador de lista encadeada, poderíamos ter um alocador especial para alocações maiores que 4 KiB. A ideia é utilizar [paginação], que opera em páginas de 4 KiB, para mapear um bloco contínuo de memória virtual a frames físicos não contínuos. Dessa forma, fragmentação de memória não utilizada não é mais um problema para alocações grandes. +- Com tal alocador de página, pode fazer sentido adicionar tamanhos de bloco até 4 KiB e descartar o alocador de lista encadeada completamente. As principais vantagens disso seriam fragmentação reduzida e melhor previsibilidade de desempenho, ou seja, melhor desempenho de pior caso. + +[paginação]: @/edition-2/posts/08-paging-introduction/index.md + +É importante notar que as melhorias de implementação descritas acima são apenas sugestões. Alocadores usados em kernels de sistemas operacionais são tipicamente altamente otimizados para a carga de trabalho específica do kernel, o que só é possível através de profiling extensivo. + +### Variações + +Também existem muitas variações do design de alocador de bloco de tamanho fixo. Dois exemplos populares são o _alocador slab_ e o _alocador buddy_, que também são usados em kernels populares como o Linux. A seguir, damos uma breve introdução a esses dois designs. + +#### Alocador Slab + +A ideia por trás de um [alocador slab] é usar tamanhos de bloco que correspondem diretamente a tipos selecionados no kernel. Dessa forma, alocações desses tipos se encaixam em um tamanho de bloco exatamente e nenhuma memória é desperdiçada. Às vezes, pode até ser possível pré-inicializar instâncias de tipo em blocos não utilizados para melhorar ainda mais o desempenho. + +[alocador slab]: https://en.wikipedia.org/wiki/Slab_allocation + +Alocação slab é frequentemente combinada com outros alocadores. Por exemplo, ela pode ser usada junto com um alocador de bloco de tamanho fixo para dividir ainda mais um bloco alocado a fim de reduzir o desperdício de memória. Também é frequentemente usada para implementar um [padrão de pool de objetos] em cima de uma única grande alocação. + +[padrão de pool de objetos]: https://en.wikipedia.org/wiki/Object_pool_pattern + +#### Alocador Buddy + +Em vez de usar uma lista encadeada para gerenciar blocos liberados, o design [alocador buddy] usa uma estrutura de dados de [árvore binária] junto com tamanhos de bloco que são potências de 2. Quando um novo bloco de um certo tamanho é necessário, ele divide um bloco de tamanho maior em duas metades, criando assim dois nós filhos na árvore. Sempre que um bloco é liberado novamente, seu bloco vizinho na árvore é analisado. Se o vizinho também estiver livre, os dois blocos são unidos de volta para formar um bloco de duas vezes o tamanho. + +A vantagem deste processo de mesclagem é que a [fragmentação externa] é reduzida para que pequenos blocos liberados possam ser reutilizados para uma alocação grande. Também não usa um alocador de fallback, então o desempenho é mais previsível. A maior desvantagem é que apenas tamanhos de bloco que são potências de 2 são possíveis, o que pode resultar em uma grande quantidade de memória desperdiçada devido à [fragmentação interna]. Por essa razão, alocadores buddy são frequentemente combinados com um alocador slab para dividir ainda mais um bloco alocado em múltiplos blocos menores. + +[alocador buddy]: https://en.wikipedia.org/wiki/Buddy_memory_allocation +[árvore binária]: https://en.wikipedia.org/wiki/Binary_tree +[fragmentação externa]: https://en.wikipedia.org/wiki/Fragmentation_(computing)#External_fragmentation +[fragmentação interna]: https://en.wikipedia.org/wiki/Fragmentation_(computing)#Internal_fragmentation + + +## Resumo + +Este post deu uma visão geral de diferentes designs de alocadores. Aprendemos como implementar um [alocador bump] básico, que distribui memória linearmente aumentando um único ponteiro `next`. Embora a alocação bump seja muito rápida, ela só pode reutilizar memória depois que todas as alocações foram liberadas. Por essa razão, raramente é usada como um alocador global. + +[alocador bump]: @/edition-2/posts/11-allocator-designs/index.md#bump-allocator + +Em seguida, criamos um [alocador de lista encadeada] que usa os próprios blocos de memória liberados para criar uma lista encadeada, a chamada [lista livre]. Esta lista torna possível armazenar um número arbitrário de blocos liberados de diferentes tamanhos. Embora nenhum desperdício de memória ocorra, a abordagem sofre de desempenho pobre porque uma requisição de alocação pode requerer um percurso completo da lista. Nossa implementação também sofre de [fragmentação externa] porque não mescla blocos adjacentes liberados de volta juntos. + +[alocador de lista encadeada]: @/edition-2/posts/11-allocator-designs/index.md#linked-list-allocator +[lista livre]: https://en.wikipedia.org/wiki/Free_list + +Para corrigir os problemas de desempenho da abordagem de lista encadeada, criamos um [alocador de bloco de tamanho fixo] que predefine um conjunto fixo de tamanhos de bloco. Para cada tamanho de bloco, uma [lista livre] separada existe, de modo que alocações e desalocações só precisam inserir/remover na frente da lista e são assim muito rápidas. Como cada alocação é arredondada para cima até o próximo tamanho de bloco maior, alguma memória é desperdiçada devido à [fragmentação interna]. + +[alocador de bloco de tamanho fixo]: @/edition-2/posts/11-allocator-designs/index.md#fixed-size-block-allocator + +Existem muitos outros designs de alocadores com diferentes trade-offs. [Alocação slab] funciona bem para otimizar a alocação de estruturas comuns de tamanho fixo, mas não é aplicável em todas as situações. [Alocação buddy] usa uma árvore binária para mesclar blocos liberados de volta juntos, mas desperdiça uma grande quantidade de memória porque só suporta tamanhos de bloco que são potências de 2. Também é importante lembrar que cada implementação de kernel tem uma carga de trabalho única, então não há design de alocador "melhor" que se encaixe em todos os casos. + +[Alocação slab]: @/edition-2/posts/11-allocator-designs/index.md#slab-allocator +[Alocação buddy]: @/edition-2/posts/11-allocator-designs/index.md#buddy-allocator + + +## O que vem a seguir? + +Com este post, concluímos nossa implementação de gerenciamento de memória por enquanto. Em seguida, começaremos a explorar [_multitarefa_], começando com multitarefa cooperativa na forma de [_async/await_]. Em posts subsequentes, então exploraremos [_threads_], [_multiprocessamento_] e [_processos_]. + +[_multitarefa_]: https://en.wikipedia.org/wiki/Computer_multitasking +[_threads_]: https://en.wikipedia.org/wiki/Thread_(computing) +[_processos_]: https://en.wikipedia.org/wiki/Process_(computing) +[_multiprocessamento_]: https://en.wikipedia.org/wiki/Multiprocessing +[_async/await_]: https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html \ No newline at end of file diff --git a/blog/content/edition-2/posts/12-async-await/index.pt-BR.md b/blog/content/edition-2/posts/12-async-await/index.pt-BR.md new file mode 100644 index 00000000..562d267b --- /dev/null +++ b/blog/content/edition-2/posts/12-async-await/index.pt-BR.md @@ -0,0 +1,1827 @@ ++++ +title = "Async/Await" +weight = 12 +path = "pt-BR/async-await" +date = 2020-03-27 + +[extra] +chapter = "Multitasking" +# Please update this when updating the translation +translation_based_on_commit = "1ba06fe61c39c1379bd768060c21040b62ff3f0b" +# GitHub usernames of the people that translated this post +translators = ["richarddalves"] ++++ + +Neste post, exploramos _multitarefa cooperativa_ e a funcionalidade _async/await_ do Rust. Fazemos uma análise detalhada de como async/await funciona em Rust, incluindo o design da trait `Future`, a transformação em máquina de estados e _pinning_. Então adicionamos suporte básico para async/await ao nosso kernel criando uma tarefa assíncrona de teclado e um executor básico. + + + +Este blog é desenvolvido abertamente no [GitHub]. Se você tiver algum problema ou dúvida, abra um issue lá. Você também pode deixar comentários [na parte inferior]. O código-fonte completo desta publicação pode ser encontrado na branch [`post-12`][post branch]. + +[GitHub]: https://github.com/phil-opp/blog_os +[na parte inferior]: #comments + +[post branch]: https://github.com/phil-opp/blog_os/tree/post-12 + + + +## Multitarefa + +Uma das funcionalidades fundamentais da maioria dos sistemas operacionais é [_multitarefa_], que é a capacidade de executar múltiplas tarefas concorrentemente. Por exemplo, você provavelmente tem outros programas abertos enquanto olha este post, como um editor de texto ou uma janela de terminal. Mesmo se você tiver apenas uma janela de navegador aberta, provavelmente existem várias tarefas em segundo plano gerenciando suas janelas da área de trabalho, verificando atualizações ou indexando arquivos. + +[_multitarefa_]: https://en.wikipedia.org/wiki/Computer_multitasking + +Embora pareça que todas as tarefas estão sendo executadas em paralelo, apenas uma única tarefa pode ser executada em um núcleo de CPU por vez. Para criar a ilusão de que as tarefas estão sendo executadas em paralelo, o sistema operacional alterna rapidamente entre as tarefas ativas para que cada uma possa fazer um pouco de progresso. Como os computadores são rápidos, não notamos essas alternâncias na maior parte do tempo. + +Enquanto CPUs de núcleo único podem executar apenas uma tarefa por vez, CPUs multi-core podem executar múltiplas tarefas de forma verdadeiramente paralela. Por exemplo, uma CPU com 8 núcleos pode executar 8 tarefas ao mesmo tempo. Explicaremos como configurar CPUs multi-core em um post futuro. Para este post, focaremos em CPUs de núcleo único por simplicidade. (Vale notar que todas as CPUs multi-core começam com apenas um único núcleo ativo, então podemos tratá-las como CPUs de núcleo único por enquanto.) + +Existem duas formas de multitarefa: Multitarefa _cooperativa_ requer que as tarefas regularmente cedam o controle da CPU para que outras tarefas possam progredir. Multitarefa _preemptiva_ usa funcionalidades do sistema operacional para alternar threads em pontos arbitrários no tempo, pausando-as forçadamente. A seguir, exploraremos as duas formas de multitarefa em mais detalhes e discutiremos suas respectivas vantagens e desvantagens. + +### Multitarefa Preemptiva + +A ideia por trás da multitarefa preemptiva é que o sistema operacional controla quando alternar tarefas. Para isso, ele utiliza o fato de que recupera o controle da CPU em cada interrupção. Isso torna possível alternar tarefas sempre que uma nova entrada está disponível para o sistema. Por exemplo, seria possível alternar tarefas quando o mouse é movido ou um pacote de rede chega. O sistema operacional também pode determinar o tempo exato que uma tarefa tem permissão para executar configurando um temporizador de hardware para enviar uma interrupção após esse tempo. + +O gráfico seguinte ilustra o processo de alternância de tarefas em uma interrupção de hardware: + +![](regain-control-on-interrupt.svg) + +Na primeira linha, a CPU está executando a tarefa `A1` do programa `A`. Todas as outras tarefas estão pausadas. Na segunda linha, uma interrupção de hardware chega na CPU. Como descrito no post [_Interrupções de Hardware_], a CPU imediatamente para a execução da tarefa `A1` e salta para o manipulador de interrupção definido na tabela de descritores de interrupção (IDT). Através deste manipulador de interrupção, o sistema operacional agora tem controle da CPU novamente, o que permite alternar para a tarefa `B1` em vez de continuar a tarefa `A1`. + +[_Interrupções de Hardware_]: @/edition-2/posts/07-hardware-interrupts/index.md + +#### Salvando o Estado + +Como as tarefas são interrompidas em pontos arbitrários no tempo, elas podem estar no meio de alguns cálculos. Para poder retomá-las mais tarde, o sistema operacional deve fazer backup do estado completo da tarefa, incluindo sua [pilha de chamadas] e os valores de todos os registradores da CPU. Este processo é chamado de [_troca de contexto_]. + +[pilha de chamadas]: https://en.wikipedia.org/wiki/Call_stack +[_troca de contexto_]: https://en.wikipedia.org/wiki/Context_switch + +Como a pilha de chamadas pode ser muito grande, o sistema operacional normalmente configura uma pilha de chamadas separada para cada tarefa em vez de fazer backup do conteúdo da pilha de chamadas em cada alternância de tarefa. Tal tarefa com sua própria pilha é chamada de [_thread de execução_] ou _thread_ para abreviar. Ao usar uma pilha separada para cada tarefa, apenas o conteúdo dos registradores precisa ser salvo em uma troca de contexto (incluindo o contador de programa e o ponteiro de pilha). Esta abordagem minimiza a sobrecarga de desempenho de uma troca de contexto, o que é muito importante já que trocas de contexto geralmente ocorrem até 100 vezes por segundo. + +[_thread de execução_]: https://en.wikipedia.org/wiki/Thread_(computing) + +#### Discussão + +A principal vantagem da multitarefa preemptiva é que o sistema operacional pode controlar totalmente o tempo de execução permitido de uma tarefa. Desta forma, ele pode garantir que cada tarefa receba uma parcela justa do tempo de CPU, sem a necessidade de confiar que as tarefas cooperarão. Isto é especialmente importante ao executar tarefas de terceiros ou quando múltiplos usuários compartilham um sistema. + +A desvantagem da preempção é que cada tarefa requer sua própria pilha. Comparado a uma pilha compartilhada, isso resulta em maior uso de memória por tarefa e frequentemente limita o número de tarefas no sistema. Outra desvantagem é que o sistema operacional sempre tem que salvar o estado completo dos registradores da CPU em cada troca de tarefa, mesmo que a tarefa tenha usado apenas um pequeno subconjunto dos registradores. + +Multitarefa preemptiva e threads são componentes fundamentais de um sistema operacional porque tornam possível executar programas de espaço de usuário não confiáveis. Discutiremos esses conceitos em detalhes completos em posts futuros. Para este post, no entanto, focaremos na multitarefa cooperativa, que também fornece capacidades úteis para o nosso kernel. + +### Multitarefa Cooperativa + +Em vez de pausar forçadamente as tarefas em execução em pontos arbitrários no tempo, a multitarefa cooperativa permite que cada tarefa execute até que ela voluntariamente ceda o controle da CPU. Isso permite que as tarefas se pausem em pontos convenientes no tempo, por exemplo, quando precisam esperar por uma operação de E/S de qualquer forma. + +Multitarefa cooperativa é frequentemente usada no nível da linguagem, como na forma de [corrotinas] ou [async/await]. A ideia é que o programador ou o compilador insira operações de [_yield_] no programa, que cedem o controle da CPU e permitem que outras tarefas executem. Por exemplo, um yield poderia ser inserido após cada iteração de um loop complexo. + +[corrotinas]: https://en.wikipedia.org/wiki/Coroutine +[async/await]: https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html +[_yield_]: https://en.wikipedia.org/wiki/Yield_(multithreading) + +É comum combinar multitarefa cooperativa com [operações assíncronas]. Em vez de esperar até que uma operação seja finalizada e impedir outras tarefas de executar durante esse tempo, operações assíncronas retornam um status "não pronto" se a operação ainda não foi finalizada. Neste caso, a tarefa em espera pode executar uma operação de yield para permitir que outras tarefas executem. + +[operações assíncronas]: https://en.wikipedia.org/wiki/Asynchronous_I/O + +#### Salvando o Estado + +Como as tarefas definem seus próprios pontos de pausa, elas não precisam que o sistema operacional salve seu estado. Em vez disso, elas podem salvar exatamente o estado de que precisam para continuar antes de se pausarem, o que frequentemente resulta em melhor desempenho. Por exemplo, uma tarefa que acabou de finalizar um cálculo complexo pode precisar fazer backup apenas do resultado final do cálculo, já que não precisa mais dos resultados intermediários. + +Implementações de tarefas cooperativas com suporte da linguagem são frequentemente até capazes de fazer backup das partes necessárias da pilha de chamadas antes de pausar. Como exemplo, a implementação async/await do Rust armazena todas as variáveis locais que ainda são necessárias em uma struct gerada automaticamente (veja abaixo). Ao fazer backup das partes relevantes da pilha de chamadas antes de pausar, todas as tarefas podem compartilhar uma única pilha de chamadas, o que resulta em consumo de memória muito menor por tarefa. Isso torna possível criar um número quase arbitrário de tarefas cooperativas sem ficar sem memória. + +#### Discussão + +A desvantagem da multitarefa cooperativa é que uma tarefa não cooperativa pode potencialmente executar por um tempo ilimitado. Assim, uma tarefa maliciosa ou com bugs pode impedir outras tarefas de executar e desacelerar ou até bloquear todo o sistema. Por esta razão, multitarefa cooperativa deve ser usada apenas quando todas as tarefas são conhecidas por cooperar. Como contraexemplo, não é uma boa ideia fazer o sistema operacional depender da cooperação de programas arbitrários de nível de usuário. + +No entanto, os fortes benefícios de desempenho e memória da multitarefa cooperativa tornam-na uma boa abordagem para uso _dentro_ de um programa, especialmente em combinação com operações assíncronas. Como um kernel de sistema operacional é um programa crítico em termos de desempenho que interage com hardware assíncrono, multitarefa cooperativa parece uma boa abordagem para implementar concorrência. + +## Async/Await em Rust + +A linguagem Rust fornece suporte de primeira classe para multitarefa cooperativa na forma de async/await. Antes que possamos explorar o que é async/await e como funciona, precisamos entender como _futures_ e programação assíncrona funcionam em Rust. + +### Futures + +Uma _future_ representa um valor que pode ainda não estar disponível. Isso poderia ser, por exemplo, um inteiro que é computado por outra tarefa ou um arquivo que está sendo baixado da rede. Em vez de esperar até que o valor esteja disponível, futures tornam possível continuar a execução até que o valor seja necessário. + +#### Exemplo + +O conceito de futures é melhor ilustrado com um pequeno exemplo: + +![Diagrama de sequência: main chama `read_file` e é bloqueado até que retorne; então chama `foo()` e também é bloqueado até que retorne. O mesmo processo é repetido, mas desta vez `async_read_file` é chamado, que retorna diretamente uma future; então `foo()` é chamado novamente, que agora executa concorrentemente com o carregamento do arquivo. O arquivo está disponível antes que `foo()` retorne.](async-example.svg) + +Este diagrama de sequência mostra uma função `main` que lê um arquivo do sistema de arquivos e então chama uma função `foo`. Este processo é repetido duas vezes: uma vez com uma chamada `read_file` síncrona e uma vez com uma chamada `async_read_file` assíncrona. + +Com a chamada síncrona, a função `main` precisa esperar até que o arquivo seja carregado do sistema de arquivos. Somente então ela pode chamar a função `foo`, o que requer que ela espere novamente pelo resultado. + +Com a chamada `async_read_file` assíncrona, o sistema de arquivos retorna diretamente uma future e carrega o arquivo assincronamente em segundo plano. Isso permite que a função `main` chame `foo` muito mais cedo, que então executa em paralelo com o carregamento do arquivo. Neste exemplo, o carregamento do arquivo até termina antes que `foo` retorne, então `main` pode trabalhar diretamente com o arquivo sem mais espera após `foo` retornar. + +#### Futures em Rust + +Em Rust, futures são representadas pela trait [`Future`], que se parece com isto: + +[`Future`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html + +```rust +pub trait Future { + type Output; + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll; +} +``` + +O [tipo associado] `Output` especifica o tipo do valor assíncrono. Por exemplo, a função `async_read_file` no diagrama acima retornaria uma instância `Future` com `Output` definido como `File`. + +[tipo associado]: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#specifying-placeholder-types-in-trait-definitions-with-associated-types + +O método [`poll`] permite verificar se o valor já está disponível. Ele retorna um enum [`Poll`], que se parece com isto: + +[`poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll +[`Poll`]: https://doc.rust-lang.org/nightly/core/task/enum.Poll.html + +```rust +pub enum Poll { + Ready(T), + Pending, +} +``` + +Quando o valor já está disponível (por exemplo, o arquivo foi totalmente lido do disco), ele é retornado encapsulado na variante `Ready`. Caso contrário, a variante `Pending` é retornada, que sinaliza ao chamador que o valor ainda não está disponível. + +O método `poll` recebe dois argumentos: `self: Pin<&mut Self>` e `cx: &mut Context`. O primeiro se comporta de forma similar a uma referência normal `&mut self`, exceto que o valor `Self` é [_fixado_] em sua localização na memória. Entender `Pin` e por que é necessário é difícil sem entender como async/await funciona primeiro. Portanto, explicaremos isso mais tarde neste post. + +[_fixado_]: https://doc.rust-lang.org/nightly/core/pin/index.html + +O propósito do parâmetro `cx: &mut Context` é passar uma instância [`Waker`] para a tarefa assíncrona, por exemplo, o carregamento do arquivo do sistema de arquivos. Este `Waker` permite que a tarefa assíncrona sinalize que ela (ou uma parte dela) foi finalizada, por exemplo, que o arquivo foi carregado do disco. Como a tarefa principal sabe que será notificada quando a `Future` estiver pronta, ela não precisa chamar `poll` repetidamente. Explicaremos este processo em mais detalhes mais tarde neste post quando implementarmos nosso próprio tipo waker. + +[`Waker`]: https://doc.rust-lang.org/nightly/core/task/struct.Waker.html + +### Trabalhando com Futures + +Agora sabemos como futures são definidas e entendemos a ideia básica por trás do método `poll`. No entanto, ainda não sabemos como trabalhar efetivamente com futures. O problema é que futures representam os resultados de tarefas assíncronas, que podem ainda não estar disponíveis. Na prática, no entanto, frequentemente precisamos desses valores diretamente para cálculos adicionais. Então a questão é: Como podemos recuperar eficientemente o valor de uma future quando precisamos dele? + +#### Esperando por Futures + +Uma resposta possível é esperar até que uma future se torne pronta. Isso poderia se parecer com algo assim: + +```rust +let future = async_read_file("foo.txt"); +let file_content = loop { + match future.poll(…) { + Poll::Ready(value) => break value, + Poll::Pending => {}, // não faz nada + } +} +``` + +Aqui nós _ativamente_ esperamos pela future chamando `poll` repetidamente em um loop. Os argumentos para `poll` não importam aqui, então os omitimos. Embora esta solução funcione, ela é muito ineficiente porque mantemos a CPU ocupada até que o valor se torne disponível. + +Uma abordagem mais eficiente poderia ser _bloquear_ a thread atual até que a future se torne disponível. Isso é, claro, possível apenas se você tiver threads, então esta solução não funciona para o nosso kernel, pelo menos ainda não. Mesmo em sistemas onde o bloqueio é suportado, frequentemente não é desejado porque transforma uma tarefa assíncrona em uma tarefa síncrona novamente, inibindo assim os benefícios de desempenho potenciais de tarefas paralelas. + +#### Combinadores de Future + +Uma alternativa a esperar é usar combinadores de future. Combinadores de future são métodos como `map` que permitem encadear e combinar futures juntas, similar aos métodos da trait [`Iterator`]. Em vez de esperar pela future, esses combinadores retornam uma future eles mesmos, que aplica a operação de mapeamento em `poll`. + +[`Iterator`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html + +Como exemplo, um simples combinador `string_len` para converter uma `Future` em uma `Future` poderia se parecer com isto: + +```rust +struct StringLen { + inner_future: F, +} + +impl Future for StringLen where F: Future { + type Output = usize; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.inner_future.poll(cx) { + Poll::Ready(s) => Poll::Ready(s.len()), + Poll::Pending => Poll::Pending, + } + } +} + +fn string_len(string: impl Future) + -> impl Future +{ + StringLen { + inner_future: string, + } +} + +// Uso +fn file_len() -> impl Future { + let file_content_future = async_read_file("foo.txt"); + string_len(file_content_future) +} +``` + +Este código não funciona perfeitamente porque não lida com [_pinning_], mas é suficiente como exemplo. A ideia básica é que a função `string_len` encapsula uma determinada instância `Future` em uma nova struct `StringLen`, que também implementa `Future`. Quando a future encapsulada é consultada, ela consulta a future interna. Se o valor ainda não está pronto, `Poll::Pending` é retornado da future encapsulada também. Se o valor está pronto, a string é extraída da variante `Poll::Ready` e seu comprimento é calculado. Depois, é encapsulado em `Poll::Ready` novamente e retornado. + +[_pinning_]: https://doc.rust-lang.org/stable/core/pin/index.html + +Com esta função `string_len`, podemos calcular o comprimento de uma string assíncrona sem esperar por ela. Como a função retorna uma `Future` novamente, o chamador não pode trabalhar diretamente no valor retornado, mas precisa usar funções combinadoras novamente. Desta forma, todo o grafo de chamadas se torna assíncrono e podemos esperar por múltiplas futures eficientemente de uma vez em algum ponto, por exemplo, na função main. + +Como escrever funções combinadoras manualmente é difícil, elas são frequentemente fornecidas por bibliotecas. Embora a biblioteca padrão do Rust ainda não forneça métodos combinadores, a crate semi-oficial (e compatível com `no_std`) [`futures`] fornece. Sua trait [`FutureExt`] fornece métodos combinadores de alto nível como [`map`] ou [`then`], que podem ser usados para manipular o resultado com closures arbitrárias. + +[`futures`]: https://docs.rs/futures/0.3.4/futures/ +[`FutureExt`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html +[`map`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.map +[`then`]: https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.then + +##### Vantagens + +A grande vantagem dos combinadores de future é que eles mantêm as operações assíncronas. Em combinação com interfaces de E/S assíncronas, esta abordagem pode levar a desempenho muito alto. O fato de que combinadores de future são implementados como structs normais com implementações de trait permite que o compilador os otimize excessivamente. Para mais detalhes, veja o post [_Zero-cost futures in Rust_], que anunciou a adição de futures ao ecossistema do Rust. + +[_Zero-cost futures in Rust_]: https://aturon.github.io/blog/2016/08/11/futures/ + +##### Desvantagens + +Embora combinadores de future tornem possível escrever código muito eficiente, eles podem ser difíceis de usar em algumas situações por causa do sistema de tipos e da interface baseada em closures. Por exemplo, considere código como este: + +```rust +fn example(min_len: usize) -> impl Future { + async_read_file("foo.txt").then(move |content| { + if content.len() < min_len { + Either::Left(async_read_file("bar.txt").map(|s| content + &s)) + } else { + Either::Right(future::ready(content)) + } + }) +} +``` + +([Tente no playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=91fc09024eecb2448a85a7ef6a97b8d8)) + +Aqui lemos o arquivo `foo.txt` e então usamos o combinador [`then`] para encadear uma segunda future baseada no conteúdo do arquivo. Se o comprimento do conteúdo é menor que o `min_len` dado, lemos um arquivo diferente `bar.txt` e o anexamos a `content` usando o combinador [`map`]. Caso contrário, retornamos apenas o conteúdo de `foo.txt`. + +Precisamos usar a [palavra-chave `move`] para a closure passada a `then` porque caso contrário haveria um erro de tempo de vida para `min_len`. A razão para o wrapper [`Either`] é que os blocos `if` e `else` devem sempre ter o mesmo tipo. Como retornamos diferentes tipos de future nos blocos, devemos usar o tipo wrapper para unificá-los em um único tipo. A função [`ready`] encapsula um valor em uma future que está imediatamente pronta. A função é necessária aqui porque o wrapper `Either` espera que o valor encapsulado implemente `Future`. + +[palavra-chave `move`]: https://doc.rust-lang.org/std/keyword.move.html +[`Either`]: https://docs.rs/futures/0.3.4/futures/future/enum.Either.html +[`ready`]: https://docs.rs/futures/0.3.4/futures/future/fn.ready.html + +Como você pode imaginar, isso pode rapidamente levar a código muito complexo para projetos maiores. Fica especialmente complicado se empréstimos e diferentes tempos de vida estiverem envolvidos. Por esta razão, muito trabalho foi investido em adicionar suporte para async/await ao Rust, com o objetivo de tornar o código assíncrono radicalmente mais simples de escrever. + +### O Padrão Async/Await + +A ideia por trás de async/await é permitir que o programador escreva código que _parece_ com código síncrono normal, mas é transformado em código assíncrono pelo compilador. Funciona baseado em duas palavras-chave `async` e `await`. A palavra-chave `async` pode ser usada em uma assinatura de função para transformar uma função síncrona em uma função assíncrona que retorna uma future: + +```rust +async fn foo() -> u32 { + 0 +} + +// o código acima é aproximadamente traduzido pelo compilador para: +fn foo() -> impl Future { + future::ready(0) +} +``` + +Esta palavra-chave sozinha não seria tão útil. No entanto, dentro de funções `async`, a palavra-chave `await` pode ser usada para recuperar o valor assíncrono de uma future: + +```rust +async fn example(min_len: usize) -> String { + let content = async_read_file("foo.txt").await; + if content.len() < min_len { + content + &async_read_file("bar.txt").await + } else { + content + } +} +``` + +([Tente no playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=d93c28509a1c67661f31ff820281d434)) + +Esta função é uma tradução direta da função `example` de [acima](#desvantagens) que usava funções combinadoras. Usando o operador `.await`, podemos recuperar o valor de uma future sem precisar de closures ou tipos `Either`. Como resultado, podemos escrever nosso código como escrevemos código síncrono normal, com a diferença de que _este ainda é código assíncrono_. + +#### Transformação em Máquina de Estados + +Nos bastidores, o compilador converte o corpo da função `async` em uma [_máquina de estados_], com cada chamada `.await` representando um estado diferente. Para a função `example` acima, o compilador cria uma máquina de estados com os seguintes quatro estados: + +[_máquina de estados_]: https://en.wikipedia.org/wiki/Finite-state_machine + +![Quatro estados: início, esperando por foo.txt, esperando por bar.txt, fim](async-state-machine-states.svg) + +Cada estado representa um ponto de pausa diferente na função. Os estados _"Início"_ e _"Fim"_ representam a função no começo e no fim de sua execução. O estado _"Esperando por foo.txt"_ representa que a função está atualmente esperando pelo primeiro resultado de `async_read_file`. Similarmente, o estado _"Esperando por bar.txt"_ representa o ponto de pausa onde a função está esperando pelo segundo resultado de `async_read_file`. + +A máquina de estados implementa a trait `Future` fazendo cada chamada `poll` uma possível transição de estado: + +![Quatro estados e suas transições: início, esperando por foo.txt, esperando por bar.txt, fim](async-state-machine-basic.svg) + +O diagrama usa setas para representar mudanças de estado e formas de diamante para representar formas alternativas. Por exemplo, se o arquivo `foo.txt` não está pronto, o caminho marcado com _"não"_ é tomado e o estado _"Esperando por foo.txt"_ é alcançado. Caso contrário, o caminho _"sim"_ é tomado. O pequeno diamante vermelho sem legenda representa a branch `if content.len() < 100` da função `example`. + +Vemos que a primeira chamada `poll` inicia a função e a deixa executar até alcançar uma future que ainda não está pronta. Se todas as futures no caminho estão prontas, a função pode executar até o estado _"Fim"_, onde retorna seu resultado encapsulado em `Poll::Ready`. Caso contrário, a máquina de estados entra em um estado de espera e retorna `Poll::Pending`. Na próxima chamada `poll`, a máquina de estados então começa do último estado de espera e tenta novamente a última operação. + +#### Salvando o Estado + +Para poder continuar do último estado de espera, a máquina de estados deve acompanhar internamente o estado atual. Além disso, ela deve salvar todas as variáveis de que precisa para continuar a execução na próxima chamada `poll`. É aqui que o compilador pode realmente brilhar: Como ele sabe quais variáveis são usadas quando, ele pode gerar automaticamente structs com exatamente as variáveis que são necessárias. + +Como exemplo, o compilador gera structs como as seguintes para a função `example` acima: + +```rust +// A função `example` novamente para que você não precise rolar para cima +async fn example(min_len: usize) -> String { + let content = async_read_file("foo.txt").await; + if content.len() < min_len { + content + &async_read_file("bar.txt").await + } else { + content + } +} + +// As structs de estado geradas pelo compilador: + +struct StartState { + min_len: usize, +} + +struct WaitingOnFooTxtState { + min_len: usize, + foo_txt_future: impl Future, +} + +struct WaitingOnBarTxtState { + content: String, + bar_txt_future: impl Future, +} + +struct EndState {} +``` + +Nos estados "início" e _"Esperando por foo.txt"_, o parâmetro `min_len` precisa ser armazenado para a comparação posterior com `content.len()`. O estado _"Esperando por foo.txt"_ armazena adicionalmente uma `foo_txt_future`, que representa a future retornada pela chamada `async_read_file`. Esta future precisa ser consultada novamente quando a máquina de estados continua, então ela precisa ser salva. + +O estado _"Esperando por bar.txt"_ contém a variável `content` para a concatenação de string posterior quando `bar.txt` estiver pronto. Ele também armazena uma `bar_txt_future` que representa o carregamento em progresso de `bar.txt`. A struct não contém a variável `min_len` porque não é mais necessária após a comparação `content.len()`. No estado _"fim"_, nenhuma variável é armazenada porque a função já executou até completar. + +Lembre-se que este é apenas um exemplo do código que o compilador poderia gerar. Os nomes das structs e o layout dos campos são detalhes de implementação e podem ser diferentes. + +#### O Tipo Completo da Máquina de Estados + +Embora o código exato gerado pelo compilador seja um detalhe de implementação, ajuda no entendimento imaginar como a máquina de estados gerada _poderia_ parecer para a função `example`. Já definimos as structs representando os diferentes estados e contendo as variáveis necessárias. Para criar uma máquina de estados em cima delas, podemos combiná-las em um [`enum`]: + +[`enum`]: https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html + +```rust +enum ExampleStateMachine { + Start(StartState), + WaitingOnFooTxt(WaitingOnFooTxtState), + WaitingOnBarTxt(WaitingOnBarTxtState), + End(EndState), +} +``` + +Definimos uma variante de enum separada para cada estado e adicionamos a struct de estado correspondente a cada variante como um campo. Para implementar as transições de estado, o compilador gera uma implementação da trait `Future` baseada na função `example`: + +```rust +impl Future for ExampleStateMachine { + type Output = String; // tipo de retorno de `example` + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + loop { + match self { // TODO: lidar com pinning + ExampleStateMachine::Start(state) => {…} + ExampleStateMachine::WaitingOnFooTxt(state) => {…} + ExampleStateMachine::WaitingOnBarTxt(state) => {…} + ExampleStateMachine::End(state) => {…} + } + } + } +} +``` + +O tipo `Output` da future é `String` porque é o tipo de retorno da função `example`. Para implementar a função `poll`, usamos uma expressão `match` no estado atual dentro de um `loop`. A ideia é que mudamos para o próximo estado enquanto possível e usamos um `return Poll::Pending` explícito quando não podemos continuar. + +Para simplicidade, mostramos apenas código simplificado e não lidamos com [pinning][_pinning_], propriedade, tempos de vida, etc. Então este e o código seguinte devem ser tratados como pseudocódigo e não usados diretamente. Claro, o código real gerado pelo compilador lida com tudo corretamente, embora possivelmente de uma forma diferente. + +Para manter os trechos de código pequenos, apresentamos o código para cada braço `match` separadamente. Vamos começar com o estado `Start`: + +```rust +ExampleStateMachine::Start(state) => { + // do corpo de `example` + let foo_txt_future = async_read_file("foo.txt"); + // operação `.await` + let state = WaitingOnFooTxtState { + min_len: state.min_len, + foo_txt_future, + }; + *self = ExampleStateMachine::WaitingOnFooTxt(state); +} +``` + +A máquina de estados está no estado `Start` quando está bem no início da função. Neste caso, executamos todo o código do corpo da função `example` até o primeiro `.await`. Para lidar com a operação `.await`, mudamos o estado da máquina de estados `self` para `WaitingOnFooTxt`, que inclui a construção da struct `WaitingOnFooTxtState`. + +Como a expressão `match self {…}` é executada em um loop, a execução salta para o braço `WaitingOnFooTxt` em seguida: + +```rust +ExampleStateMachine::WaitingOnFooTxt(state) => { + match state.foo_txt_future.poll(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(content) => { + // do corpo de `example` + if content.len() < state.min_len { + let bar_txt_future = async_read_file("bar.txt"); + // operação `.await` + let state = WaitingOnBarTxtState { + content, + bar_txt_future, + }; + *self = ExampleStateMachine::WaitingOnBarTxt(state); + } else { + *self = ExampleStateMachine::End(EndState); + return Poll::Ready(content); + } + } + } +} +``` + +Neste braço `match`, primeiro chamamos a função `poll` da `foo_txt_future`. Se não está pronta, saímos do loop e retornamos `Poll::Pending`. Como `self` permanece no estado `WaitingOnFooTxt` neste caso, a próxima chamada `poll` na máquina de estados entrará no mesmo braço `match` e tentará consultar a `foo_txt_future` novamente. + +Quando a `foo_txt_future` está pronta, atribuímos o resultado à variável `content` e continuamos a executar o código da função `example`: Se `content.len()` é menor que o `min_len` salvo na struct de estado, o arquivo `bar.txt` é lido assincronamente. Novamente traduzimos a operação `.await` em uma mudança de estado, desta vez para o estado `WaitingOnBarTxt`. Como estamos executando o `match` dentro de um loop, a execução salta diretamente para o braço `match` para o novo estado depois, onde a `bar_txt_future` é consultada. + +Caso entremos no braço `else`, nenhuma operação `.await` adicional ocorre. Alcançamos o fim da função e retornamos `content` encapsulado em `Poll::Ready`. Também mudamos o estado atual para o estado `End`. + +O código para o estado `WaitingOnBarTxt` parece com isto: + +```rust +ExampleStateMachine::WaitingOnBarTxt(state) => { + match state.bar_txt_future.poll(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(bar_txt) => { + *self = ExampleStateMachine::End(EndState); + // do corpo de `example` + return Poll::Ready(state.content + &bar_txt); + } + } +} +``` + +Similar ao estado `WaitingOnFooTxt`, começamos consultando a `bar_txt_future`. Se ainda está pendente, saímos do loop e retornamos `Poll::Pending`. Caso contrário, podemos executar a última operação da função `example`: concatenar a variável `content` com o resultado da future. Atualizamos a máquina de estados para o estado `End` e então retornamos o resultado encapsulado em `Poll::Ready`. + +Finalmente, o código para o estado `End` parece com isto: + +```rust +ExampleStateMachine::End(_) => { + panic!("poll chamado após Poll::Ready ter sido retornado"); +} +``` + +Futures não devem ser consultadas novamente após retornarem `Poll::Ready`, então entramos em pânico se `poll` é chamado enquanto já estamos no estado `End`. + +Agora sabemos como a máquina de estados gerada pelo compilador e sua implementação da trait `Future` _poderiam_ parecer. Na prática, o compilador gera código de forma diferente. (Caso esteja interessado, a implementação é atualmente baseada em [_corrotinas_], mas isso é apenas um detalhe de implementação.) + +[_corrotinas_]: https://doc.rust-lang.org/stable/unstable-book/language-features/coroutines.html + +A última peça do quebra-cabeça é o código gerado para a própria função `example`. Lembre-se, o cabeçalho da função foi definido assim: + +```rust +async fn example(min_len: usize) -> String +``` + +Como o corpo completo da função agora é implementado pela máquina de estados, a única coisa que a função precisa fazer é inicializar a máquina de estados e retorná-la. O código gerado para isso poderia parecer com isto: + +```rust +fn example(min_len: usize) -> ExampleStateMachine { + ExampleStateMachine::Start(StartState { + min_len, + }) +} +``` + +A função não tem mais um modificador `async` porque agora retorna explicitamente um tipo `ExampleStateMachine`, que implementa a trait `Future`. Como esperado, a máquina de estados é construída no estado `Start` e a struct de estado correspondente é inicializada com o parâmetro `min_len`. + +Note que esta função não inicia a execução da máquina de estados. Esta é uma decisão de design fundamental de futures em Rust: elas não fazem nada até serem consultadas pela primeira vez. + +### Pinning + +Já tropeçamos em _pinning_ múltiplas vezes neste post. Agora é finalmente a hora de explorar o que é pinning e por que é necessário. + +#### Structs Auto-Referenciais + +Como explicado acima, a transformação da máquina de estados armazena as variáveis locais de cada ponto de pausa em uma struct. Para exemplos pequenos como nossa função `example`, isso foi direto e não levou a problemas. No entanto, as coisas se tornam mais difíceis quando variáveis referenciam umas às outras. Por exemplo, considere esta função: + +```rust +async fn pin_example() -> i32 { + let array = [1, 2, 3]; + let element = &array[2]; + async_write_file("foo.txt", element.to_string()).await; + *element +} +``` + +Esta função cria um pequeno `array` com o conteúdo `1`, `2` e `3`. Ela então cria uma referência ao último elemento do array e a armazena em uma variável `element`. Em seguida, ela escreve assincronamente o número convertido em string para um arquivo `foo.txt`. Finalmente, ela retorna o número referenciado por `element`. + +Como a função usa uma única operação `await`, a máquina de estados resultante tem três estados: início, fim e "esperando por escrita". A função não recebe argumentos, então a struct para o estado de início está vazia. Como antes, a struct para o estado de fim está vazia porque a função está finalizada neste ponto. A struct para o estado "esperando por escrita" é mais interessante: + +```rust +struct WaitingOnWriteState { + array: [1, 2, 3], + element: 0x1001c, // endereço do último elemento do array +} +``` + +Precisamos armazenar tanto as variáveis `array` quanto `element` porque `element` é necessária para o valor de retorno e `array` é referenciado por `element`. Como `element` é uma referência, ela armazena um _ponteiro_ (ou seja, um endereço de memória) para o elemento referenciado. Usamos `0x1001c` como um endereço de memória de exemplo aqui. Na realidade, precisa ser o endereço do último elemento do campo `array`, então depende de onde a struct vive na memória. Structs com tais ponteiros internos são chamadas _structs auto-referenciais_ porque referenciam a si mesmas de um de seus campos. + +#### O Problema com Structs Auto-Referenciais + +O ponteiro interno de nossa struct auto-referencial leva a um problema fundamental, que se torna aparente quando olhamos para seu layout de memória: + +![array em 0x10014 com campos 1, 2 e 3; element em endereço 0x10020, apontando para o último elemento do array em 0x1001c](self-referential-struct.svg) + +O campo `array` começa no endereço 0x10014 e o campo `element` no endereço 0x10020. Ele aponta para o endereço 0x1001c porque o último elemento do array vive neste endereço. Neste ponto, tudo ainda está bem. No entanto, um problema ocorre quando movemos esta struct para um endereço de memória diferente: + +![array em 0x10024 com campos 1, 2 e 3; element em endereço 0x10030, ainda apontando para 0x1001c, mesmo que o último elemento do array agora viva em 0x1002c](self-referential-struct-moved.svg) + +Movemos a struct um pouco então ela agora começa no endereço `0x10024`. Isso poderia, por exemplo, acontecer quando passamos a struct como um argumento de função ou a atribuímos a uma variável de pilha diferente. O problema é que o campo `element` ainda aponta para o endereço `0x1001c` mesmo que o último elemento `array` agora viva no endereço `0x1002c`. Assim, o ponteiro está pendente, com o resultado de que comportamento indefinido ocorre na próxima chamada `poll`. + +#### Soluções Possíveis + +Existem três abordagens fundamentais para resolver o problema do ponteiro pendente: + +- **Atualizar o ponteiro no movimento:** A ideia é atualizar o ponteiro interno sempre que a struct é movida na memória para que ainda seja válido após o movimento. Infelizmente, esta abordagem exigiria mudanças extensas ao Rust que resultariam em potencialmente enormes perdas de desempenho. A razão é que algum tipo de runtime precisaria acompanhar o tipo de todos os campos da struct e verificar em cada operação de movimento se uma atualização de ponteiro é necessária. +- **Armazenar um offset em vez de auto-referências:** Para evitar a necessidade de atualizar ponteiros, o compilador poderia tentar armazenar auto-referências como offsets do início da struct. Por exemplo, o campo `element` da struct `WaitingOnWriteState` acima poderia ser armazenado na forma de um campo `element_offset` com um valor de 8 porque o elemento do array para o qual a referência aponta começa 8 bytes após o início da struct. Como o offset permanece o mesmo quando a struct é movida, nenhuma atualização de campo é necessária. + + O problema com esta abordagem é que requer que o compilador detecte todas as auto-referências. Isso não é possível em tempo de compilação porque o valor de uma referência pode depender da entrada do usuário, então precisaríamos de um sistema de runtime novamente para analisar referências e criar corretamente as structs de estado. Isso não apenas resultaria em custos de runtime, mas também impediria certas otimizações do compilador, de modo que causaria grandes perdas de desempenho novamente. +- **Proibir mover a struct:** Como vimos acima, o ponteiro pendente só ocorre quando movemos a struct na memória. Ao proibir completamente operações de movimento em structs auto-referenciais, o problema também pode ser evitado. A grande vantagem desta abordagem é que pode ser implementada no nível do sistema de tipos sem custos de runtime adicionais. A desvantagem é que coloca o ônus de lidar com operações de movimento em structs possivelmente auto-referenciais no programador. + +Rust escolheu a terceira solução por causa de seu princípio de fornecer _abstrações de custo zero_, o que significa que abstrações não devem impor custos de runtime adicionais. A API de [_pinning_] foi proposta para este propósito na [RFC 2349](https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md). No que segue, daremos uma breve visão geral desta API e explicaremos como funciona com async/await e futures. + +#### Valores de Heap + +A primeira observação é que valores [alocados em heap] já têm um endereço de memória fixo na maior parte do tempo. Eles são criados usando uma chamada para `allocate` e então referenciados por um tipo de ponteiro como `Box`. Embora mover o tipo de ponteiro seja possível, o valor de heap para o qual o ponteiro aponta permanece no mesmo endereço de memória até ser liberado através de uma chamada `deallocate` novamente. + +[alocados em heap]: @/edition-2/posts/10-heap-allocation/index.md + +Usando alocação de heap, podemos tentar criar uma struct auto-referencial: + +```rust +fn main() { + let mut heap_value = Box::new(SelfReferential { + self_ptr: 0 as *const _, + }); + let ptr = &*heap_value as *const SelfReferential; + heap_value.self_ptr = ptr; + println!("heap value at: {:p}", heap_value); + println!("internal reference: {:p}", heap_value.self_ptr); +} + +struct SelfReferential { + self_ptr: *const Self, +} +``` + +([Tente no playground][playground-self-ref]) + +[playground-self-ref]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=ce1aff3a37fcc1c8188eeaf0f39c97e8 + +Criamos uma struct simples chamada `SelfReferential` que contém um único campo de ponteiro. Primeiro, inicializamos esta struct com um ponteiro nulo e então a alocamos no heap usando `Box::new`. Então determinamos o endereço de memória da struct alocada em heap e o armazenamos em uma variável `ptr`. Finalmente, tornamos a struct auto-referencial atribuindo a variável `ptr` ao campo `self_ptr`. + +Quando executamos este código [no playground][playground-self-ref], vemos que o endereço do valor de heap e seu ponteiro interno são iguais, o que significa que o campo `self_ptr` é uma auto-referência válida. Como a variável `heap_value` é apenas um ponteiro, movê-la (por exemplo, passando-a para uma função) não muda o endereço da própria struct, então o `self_ptr` permanece válido mesmo se o ponteiro é movido. + +No entanto, ainda há uma forma de quebrar este exemplo: Podemos mover para fora de um `Box` ou substituir seu conteúdo: + +```rust +let stack_value = mem::replace(&mut *heap_value, SelfReferential { + self_ptr: 0 as *const _, +}); +println!("value at: {:p}", &stack_value); +println!("internal reference: {:p}", stack_value.self_ptr); +``` + +([Tente no playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=e160ee8a64cba4cebc1c0473dcecb7c8)) + +Aqui usamos a função [`mem::replace`] para substituir o valor alocado em heap por uma nova instância da struct. Isso nos permite mover o `heap_value` original para a pilha, enquanto o campo `self_ptr` da struct agora é um ponteiro pendente que ainda aponta para o endereço de heap antigo. Quando você tenta executar o exemplo no playground, vê que as linhas impressas _"value at:"_ e _"internal reference:"_ de fato mostram ponteiros diferentes. Então alocar um valor em heap não é suficiente para tornar auto-referências seguras. + +[`mem::replace`]: https://doc.rust-lang.org/nightly/core/mem/fn.replace.html + +O problema fundamental que permitiu a quebra acima é que `Box` nos permite obter uma referência `&mut T` para o valor alocado em heap. Esta referência `&mut` torna possível usar métodos como [`mem::replace`] ou [`mem::swap`] para invalidar o valor alocado em heap. Para resolver este problema, devemos evitar que referências `&mut` para structs auto-referenciais sejam criadas. + +[`mem::swap`]: https://doc.rust-lang.org/nightly/core/mem/fn.swap.html + +#### `Pin>` e `Unpin` + +A API de pinning fornece uma solução para o problema `&mut T` na forma do tipo wrapper [`Pin`] e da trait marcadora [`Unpin`]. A ideia por trás desses tipos é controlar todos os métodos de `Pin` que podem ser usados para obter referências `&mut` ao valor encapsulado (por exemplo, [`get_mut`][pin-get-mut] ou [`deref_mut`][pin-deref-mut]) na trait `Unpin`. A trait `Unpin` é uma [_auto trait_], que é automaticamente implementada para todos os tipos exceto aqueles que explicitamente desistem dela. Ao fazer structs auto-referenciais desistirem de `Unpin`, não há forma (segura) de obter uma `&mut T` de um tipo `Pin>` para elas. Como resultado, suas auto-referências internas têm garantia de permanecer válidas. + +[`Pin`]: https://doc.rust-lang.org/stable/core/pin/struct.Pin.html +[`Unpin`]: https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html +[pin-get-mut]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_mut +[pin-deref-mut]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.deref_mut +[_auto trait_]: https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits + +Como exemplo, vamos atualizar o tipo `SelfReferential` de acima para desistir de `Unpin`: + +```rust +use core::marker::PhantomPinned; + +struct SelfReferential { + self_ptr: *const Self, + _pin: PhantomPinned, +} +``` + +Desistimos adicionando um segundo campo `_pin` do tipo [`PhantomPinned`]. Este tipo é um tipo marcador de tamanho zero cujo único propósito é _não_ implementar a trait `Unpin`. Por causa da forma como [auto traits][_auto trait_] funcionam, um único campo que não é `Unpin` é suficiente para fazer a struct completa desistir de `Unpin`. + +[`PhantomPinned`]: https://doc.rust-lang.org/nightly/core/marker/struct.PhantomPinned.html + +O segundo passo é mudar o tipo `Box` no exemplo para um tipo `Pin>`. A maneira mais fácil de fazer isso é usar a função [`Box::pin`] em vez de [`Box::new`] para criar o valor alocado em heap: + +[`Box::pin`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.pin +[`Box::new`]: https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.new + +```rust +let mut heap_value = Box::pin(SelfReferential { + self_ptr: 0 as *const _, + _pin: PhantomPinned, +}); +``` + +Além de mudar `Box::new` para `Box::pin`, também precisamos adicionar o novo campo `_pin` no inicializador da struct. Como `PhantomPinned` é um tipo de tamanho zero, só precisamos de seu nome de tipo para inicializá-lo. + +Quando [tentamos executar nosso exemplo ajustado](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=961b0db194bbe851ff4d0ed08d3bd98a) agora, vemos que ele não funciona mais: + +``` +error[E0594]: cannot assign to data in dereference of `Pin>` + --> src/main.rs:10:5 + | +10 | heap_value.self_ptr = ptr; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign + | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Pin>` + +error[E0596]: cannot borrow data in dereference of `Pin>` as mutable + --> src/main.rs:16:36 + | +16 | let stack_value = mem::replace(&mut *heap_value, SelfReferential { + | ^^^^^^^^^^^^^^^^ cannot borrow as mutable + | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Pin>` +``` + +Ambos os erros ocorrem porque o tipo `Pin>` não implementa mais a trait `DerefMut`. Isso é exatamente o que queríamos porque a trait `DerefMut` retornaria uma referência `&mut`, que queríamos evitar. Isso só acontece porque tanto desistimos de `Unpin` quanto mudamos `Box::new` para `Box::pin`. + +O problema agora é que o compilador não apenas evita mover o tipo na linha 16, mas também proíbe inicializar o campo `self_ptr` na linha 10. Isso acontece porque o compilador não pode diferenciar entre usos válidos e inválidos de referências `&mut`. Para fazer a inicialização funcionar novamente, temos que usar o método unsafe [`get_unchecked_mut`]: + +[`get_unchecked_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_unchecked_mut + +```rust +// seguro porque modificar um campo não move a struct inteira +unsafe { + let mut_ref = Pin::as_mut(&mut heap_value); + Pin::get_unchecked_mut(mut_ref).self_ptr = ptr; +} +``` + +([Tente no playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b9ebbb11429d9d79b3f9fffe819e2018)) + +A função [`get_unchecked_mut`] funciona em um `Pin<&mut T>` em vez de um `Pin>`, então temos que usar [`Pin::as_mut`] para converter o valor primeiro. Então podemos definir o campo `self_ptr` usando a referência `&mut` retornada por `get_unchecked_mut`. + +[`Pin::as_mut`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut + +Agora o único erro restante é o erro desejado em `mem::replace`. Lembre-se, esta operação tenta mover o valor alocado em heap para a pilha, o que quebraria a auto-referência armazenada no campo `self_ptr`. Ao desistir de `Unpin` e usar `Pin>`, podemos evitar esta operação em tempo de compilação e assim trabalhar com segurança com structs auto-referenciais. Como vimos, o compilador não é capaz de provar que a criação da auto-referência é segura (ainda), então precisamos usar um bloco unsafe e verificar a correção nós mesmos. + +#### Pinning de Pilha e `Pin<&mut T>` + +Na seção anterior, aprendemos como usar `Pin>` para criar com segurança um valor auto-referencial alocado em heap. Embora esta abordagem funcione bem e seja relativamente segura (além da construção unsafe), a alocação de heap necessária vem com um custo de desempenho. Como Rust se esforça para fornecer _abstrações de custo zero_ sempre que possível, a API de pinning também permite criar instâncias `Pin<&mut T>` que apontam para valores alocados em pilha. + +Diferente de instâncias `Pin>`, que têm _propriedade_ do valor encapsulado, instâncias `Pin<&mut T>` apenas emprestam temporariamente o valor encapsulado. Isso torna as coisas mais complicadas, pois requer que o programador garanta garantias adicionais por si mesmo. Mais importante, um `Pin<&mut T>` deve permanecer fixado por todo o tempo de vida do `T` referenciado, o que pode ser difícil de verificar para variáveis baseadas em pilha. Para ajudar com isso, crates como [`pin-utils`] existem, mas eu ainda não recomendaria fixar na pilha a menos que você realmente saiba o que está fazendo. + +[`pin-utils`]: https://docs.rs/pin-utils/0.1.0-alpha.4/pin_utils/ + +Para leitura adicional, confira a documentação do [módulo `pin`] e do método [`Pin::new_unchecked`]. + +[módulo `pin`]: https://doc.rust-lang.org/nightly/core/pin/index.html +[`Pin::new_unchecked`]: https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.new_unchecked + +#### Pinning e Futures + +Como já vimos neste post, o método [`Future::poll`] usa pinning na forma de um parâmetro `Pin<&mut Self>`: + +[`Future::poll`]: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll + +```rust +fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll +``` + +A razão pela qual este método recebe `self: Pin<&mut Self>` em vez do `&mut self` normal é que instâncias de future criadas a partir de async/await são frequentemente auto-referenciais, como vimos [acima][self-ref-async-await]. Ao encapsular `Self` em `Pin` e deixar o compilador desistir de `Unpin` para futures auto-referenciais gerados de async/await, é garantido que as futures não sejam movidas na memória entre chamadas `poll`. Isso garante que todas as referências internas ainda são válidas. + +[self-ref-async-await]: @/edition-2/posts/12-async-await/index.pt-BR.md#o-problema-com-structs-auto-referenciais + + +Vale notar que mover futures antes da primeira chamada `poll` é aceitável. Isso é resultado do fato de que futures são preguiçosas e não fazem nada até serem consultadas pela primeira vez. O estado `start` das máquinas de estados geradas, portanto, contém apenas os argumentos da função mas nenhuma referência interna. Para chamar `poll`, o chamador deve encapsular a future em `Pin` primeiro, o que garante que a future não pode ser movida na memória mais. Como fixar em pilha é mais difícil de acertar, recomendo sempre usar [`Box::pin`] combinado com [`Pin::as_mut`] para isso. + +[`futures`]: https://docs.rs/futures/0.3.4/futures/ + +Caso esteja interessado em entender como implementar com segurança uma função combinadora de future usando fixação em pilha você mesmo, dê uma olhada no [código-fonte relativamente curto do método combinador `map`][map-src] da crate `futures` e na seção sobre [projeções e fixação estrutural] da documentação de pin. + +[map-src]: https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html +[projeções e fixação estrutural]: https://doc.rust-lang.org/stable/std/pin/index.html#projections-and-structural-pinning + +### Executores e Wakers + +Usando async/await, é possível trabalhar com futures de forma completamente assíncrona e ergonômica. No entanto, como aprendemos acima, futures não fazem nada até serem consultadas. Isso significa que temos que chamar `poll` nelas em algum ponto, caso contrário o código assíncrono nunca é executado. + +Com uma única future, podemos sempre esperar por cada future manualmente usando um loop [como descrito acima](#esperando-por-futures). No entanto, esta abordagem é muito ineficiente e não prática para programas que criam um grande número de futures. A solução mais comum para este problema é definir um _executor_ global que é responsável por consultar todas as futures no sistema até serem finalizadas. + +#### Executores + +O propósito de um executor é permitir spawnar futures como tarefas independentes, tipicamente através de algum tipo de método `spawn`. O executor é então responsável por consultar todas as futures até serem completadas. A grande vantagem de gerenciar todas as futures em um lugar central é que o executor pode alternar para uma future diferente sempre que uma future retorna `Poll::Pending`. Assim, operações assíncronas são executadas em paralelo e a CPU é mantida ocupada. + +Muitas implementações de executor também podem aproveitar sistemas com múltiplos núcleos de CPU. Eles criam um [thread pool] que é capaz de utilizar todos os núcleos se há trabalho suficiente disponível e usam técnicas como [work stealing] para equilibrar a carga entre núcleos. Também existem implementações especiais de executor para sistemas embarcados que otimizam para baixa latência e sobrecarga de memória. + +[thread pool]: https://en.wikipedia.org/wiki/Thread_pool +[work stealing]: https://en.wikipedia.org/wiki/Work_stealing + +Para evitar a sobrecarga de consultar futures repetidamente, executores tipicamente aproveitam a API de _waker_ suportada pelas futures do Rust. + +#### Wakers + +A ideia por trás da API de waker é que um tipo especial [`Waker`] é passado para cada invocação de `poll`, encapsulado no tipo [`Context`]. Este tipo `Waker` é criado pelo executor e pode ser usado pela tarefa assíncrona para sinalizar sua conclusão (parcial). Como resultado, o executor não precisa chamar `poll` em uma future que previamente retornou `Poll::Pending` até ser notificado pelo waker correspondente. + +[`Context`]: https://doc.rust-lang.org/nightly/core/task/struct.Context.html + +Isso é melhor ilustrado por um pequeno exemplo: + +```rust +async fn write_file() { + async_write_file("foo.txt", "Hello").await; +} +``` + +Esta função escreve assincronamente a string "Hello" em um arquivo `foo.txt`. Como escritas em disco demoram algum tempo, a primeira chamada `poll` nesta future provavelmente retornará `Poll::Pending`. No entanto, o driver de disco rígido armazenará internamente o `Waker` passado para a chamada `poll` e o usará para notificar o executor quando o arquivo for escrito no disco. Desta forma, o executor não precisa desperdiçar tempo tentando fazer `poll` da future novamente antes de receber a notificação do waker. + +Veremos como o tipo `Waker` funciona em detalhes quando criarmos nosso próprio executor com suporte a waker na seção de implementação deste post. + +### Multitarefa Cooperativa? + +No início deste post, falamos sobre multitarefa preemptiva e cooperativa. Enquanto multitarefa preemptiva depende do sistema operacional para pausar forçadamente tarefas em execução, multitarefa cooperativa requer que as tarefas cedam voluntariamente o controle da CPU através de uma operação _yield_ regularmente. A grande vantagem da abordagem cooperativa é que as tarefas podem salvar seu próprio estado, o que resulta em trocas de contexto mais eficientes e torna possível compartilhar a mesma pilha de chamadas entre tarefas. + +Pode não ser imediatamente aparente, mas futures e async/await são uma implementação do padrão de multitarefa cooperativa: + +- Cada future que é adicionada ao executor é basicamente uma tarefa cooperativa. +- Em vez de usar uma operação de yield explícita, futures cedem o controle do núcleo da CPU retornando `Poll::Pending` (ou `Poll::Ready` no final). + - Não há nada que force futures a ceder a CPU. Se quiserem, podem nunca retornar de `poll`, por exemplo, girando indefinidamente em um loop. + - Como cada future pode bloquear a execução das outras futures no executor, precisamos confiar que elas não sejam maliciosas. +- Futures armazenam internamente todo o estado de que precisam para continuar a execução na próxima chamada `poll`. Com async/await, o compilador detecta automaticamente todas as variáveis necessárias e as armazena dentro da máquina de estados gerada. + - Apenas o estado mínimo necessário para continuação é salvo. + - Como o método `poll` cede a pilha de chamadas quando retorna, a mesma pilha pode ser usada para consultar outras futures. + +Vemos que futures e async/await se encaixam perfeitamente no padrão de multitarefa cooperativa; eles apenas usam terminologia diferente. No que segue, portanto, usaremos os termos "tarefa" e "future" de forma intercambiável. + +## Implementação + +Agora que entendemos como multitarefa cooperativa baseada em futures e async/await funciona em Rust, é hora de adicionar suporte para ela ao nosso kernel. Como a trait [`Future`] é parte da biblioteca `core` e async/await é uma funcionalidade da própria linguagem, não há nada especial que precisamos fazer para usá-la em nosso kernel `#![no_std]`. O único requisito é que usemos pelo menos o nightly `2020-03-25` do Rust porque async/await não era compatível com `no_std` antes. + +Com um nightly recente o suficiente, podemos começar a usar async/await em nosso `main.rs`: + +```rust +// em src/main.rs + +async fn async_number() -> u32 { + 42 +} + +async fn example_task() { + let number = async_number().await; + println!("async number: {}", number); +} +``` + +A função `async_number` é uma `async fn`, então o compilador a transforma em uma máquina de estados que implementa `Future`. Como a função retorna apenas `42`, a future resultante retornará diretamente `Poll::Ready(42)` na primeira chamada `poll`. Como `async_number`, a função `example_task` também é uma `async fn`. Ela aguarda o número retornado por `async_number` e então o imprime usando a macro `println`. + +Para executar a future retornada por `example_task`, precisamos chamar `poll` nela até sinalizar sua conclusão retornando `Poll::Ready`. Para fazer isso, precisamos criar um tipo executor simples. + +### Tarefa + +Antes de começarmos a implementação do executor, criamos um novo módulo `task` com um tipo `Task`: + +```rust +// em src/lib.rs + +pub mod task; +``` + +```rust +// em src/task/mod.rs + +use core::{future::Future, pin::Pin}; +use alloc::boxed::Box; + +pub struct Task { + future: Pin>>, +} +``` + +A struct `Task` é um tipo newtype wrapper em torno de uma future fixada, alocada em heap e dinamicamente despachada com o tipo vazio `()` como saída. Vamos passar por ela em detalhes: + +- Requeremos que a future associada a uma tarefa retorne `()`. Isso significa que tarefas não retornam nenhum resultado, elas são apenas executadas por seus efeitos colaterais. Por exemplo, a função `example_task` que definimos acima não tem valor de retorno, mas ela imprime algo na tela como efeito colateral. +- A palavra-chave `dyn` indica que armazenamos um [_trait object_] no `Box`. Isso significa que os métodos na future são [_dinamicamente despachados_], permitindo que diferentes tipos de futures sejam armazenados no tipo `Task`. Isso é importante porque cada `async fn` tem seu próprio tipo e queremos poder criar múltiplas tarefas diferentes. +- Como aprendemos na [seção sobre pinning], o tipo `Pin` garante que um valor não pode ser movido na memória colocando-o no heap e impedindo a criação de referências `&mut` a ele. Isso é importante porque futures gerados por async/await podem ser auto-referenciais, ou seja, conter ponteiros para si mesmos que seriam invalidados quando a future é movida. + +[_trait object_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html +[_dinamicamente despachados_]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch +[seção sobre pinning]: #pinning + +Para permitir a criação de novas structs `Task` a partir de futures, criamos uma função `new`: + +```rust +// em src/task/mod.rs + +impl Task { + pub fn new(future: impl Future + 'static) -> Task { + Task { + future: Box::pin(future), + } + } +} +``` + +A função recebe uma future arbitrária com um tipo de saída de `()` e a fixa na memória através da função [`Box::pin`]. Então encapsula a future em caixa na struct `Task` e a retorna. O tempo de vida `'static` é necessário aqui porque a `Task` retornada pode viver por um tempo arbitrário, então a future precisa ser válida por esse tempo também. + +Também adicionamos um método `poll` para permitir que o executor consulte a future armazenada: + +```rust +// em src/task/mod.rs + +use core::task::{Context, Poll}; + +impl Task { + fn poll(&mut self, context: &mut Context) -> Poll<()> { + self.future.as_mut().poll(context) + } +} +``` + +Como o método [`poll`] da trait `Future` espera ser chamado em um tipo `Pin<&mut T>`, usamos o método [`Pin::as_mut`] para converter o campo `self.future` do tipo `Pin>` primeiro. Então chamamos `poll` no campo `self.future` convertido e retornamos o resultado. Como o método `Task::poll` deve ser chamado apenas pelo executor que criaremos em um momento, mantemos a função privada ao módulo `task`. + +### Executor Simples + +Como executores podem ser bem complexos, deliberadamente começamos criando um executor muito básico antes de implementar um executor com mais funcionalidades mais tarde. Para isso, primeiro criamos um novo submódulo `task::simple_executor`: + +```rust +// em src/task/mod.rs + +pub mod simple_executor; +``` + +```rust +// em src/task/simple_executor.rs + +use super::Task; +use alloc::collections::VecDeque; + +pub struct SimpleExecutor { + task_queue: VecDeque, +} + +impl SimpleExecutor { + pub fn new() -> SimpleExecutor { + SimpleExecutor { + task_queue: VecDeque::new(), + } + } + + pub fn spawn(&mut self, task: Task) { + self.task_queue.push_back(task) + } +} +``` + +A struct contém um único campo `task_queue` do tipo [`VecDeque`], que é basicamente um vetor que permite operações de push e pop em ambas as extremidades. A ideia por trás de usar este tipo é que inserimos novas tarefas através do método `spawn` no final e retiramos a próxima tarefa para execução do início. Desta forma, obtemos uma simples [fila FIFO] (_"first in, first out"_). + +[`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html +[fila FIFO]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) + +#### Waker Dummy + +Para chamar o método `poll`, precisamos criar um tipo [`Context`], que encapsula um tipo [`Waker`]. Para começar simples, primeiro criaremos um waker dummy que não faz nada. Para isso, criamos uma instância [`RawWaker`], que define a implementação dos diferentes métodos `Waker`, e então usamos a função [`Waker::from_raw`] para transformá-lo em um `Waker`: + +[`RawWaker`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html +[`Waker::from_raw`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.from_raw + +```rust +// em src/task/simple_executor.rs + +use core::task::{Waker, RawWaker}; + +fn dummy_raw_waker() -> RawWaker { + todo!(); +} + +fn dummy_waker() -> Waker { + unsafe { Waker::from_raw(dummy_raw_waker()) } +} +``` + +A função `from_raw` é unsafe porque comportamento indefinido pode ocorrer se o programador não respeitar os requisitos documentados de `RawWaker`. Antes de olharmos para a implementação da função `dummy_raw_waker`, primeiro tentamos entender como o tipo `RawWaker` funciona. + +##### `RawWaker` + +O tipo [`RawWaker`] requer que o programador defina explicitamente uma [_tabela de métodos virtuais_] (_vtable_) que especifica as funções que devem ser chamadas quando o `RawWaker` é clonado, acordado ou descartado. O layout desta vtable é definido pelo tipo [`RawWakerVTable`]. Cada função recebe um argumento `*const ()`, que é um ponteiro _type-erased_ para algum valor. A razão para usar um ponteiro `*const ()` em vez de uma referência apropriada é que o tipo `RawWaker` deve ser não genérico mas ainda suportar tipos arbitrários. O ponteiro é fornecido colocando-o no argumento `data` de [`RawWaker::new`], que apenas inicializa um `RawWaker`. O `Waker` então usa este `RawWaker` para chamar as funções da vtable com `data`. + +[_tabela de métodos virtuais_]: https://en.wikipedia.org/wiki/Virtual_method_table +[`RawWakerVTable`]: https://doc.rust-lang.org/stable/core/task/struct.RawWakerVTable.html +[`RawWaker::new`]: https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html#method.new + +Tipicamente, o `RawWaker` é criado para alguma struct alocada em heap que é encapsulada no tipo [`Box`] ou [`Arc`]. Para tais tipos, métodos como [`Box::into_raw`] podem ser usados para converter o `Box` em um ponteiro `*const T`. Este ponteiro pode então ser convertido em um ponteiro anônimo `*const ()` e passado para `RawWaker::new`. Como cada função da vtable recebe o mesmo `*const ()` como argumento, as funções podem com segurança converter o ponteiro de volta para um `Box` ou um `&T` para operar nele. Como você pode imaginar, este processo é altamente perigoso e pode facilmente levar a comportamento indefinido em erros. Por esta razão, criar manualmente um `RawWaker` não é recomendado a menos que seja necessário. + +[`Box`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[`Box::into_raw`]: https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html#method.into_raw + +##### Um `RawWaker` Dummy + +Embora criar manualmente um `RawWaker` não seja recomendado, atualmente não há outra forma de criar um `Waker` dummy que não faz nada. Felizmente, o fato de que queremos não fazer nada torna relativamente seguro implementar a função `dummy_raw_waker`: + +```rust +// em src/task/simple_executor.rs + +use core::task::RawWakerVTable; + +fn dummy_raw_waker() -> RawWaker { + fn no_op(_: *const ()) {} + fn clone(_: *const ()) -> RawWaker { + dummy_raw_waker() + } + + let vtable = &RawWakerVTable::new(clone, no_op, no_op, no_op); + RawWaker::new(0 as *const (), vtable) +} +``` + +Primeiro, definimos duas funções internas chamadas `no_op` e `clone`. A função `no_op` recebe um ponteiro `*const ()` e não faz nada. A função `clone` também recebe um ponteiro `*const ()` e retorna um novo `RawWaker` chamando `dummy_raw_waker` novamente. Usamos estas duas funções para criar uma `RawWakerVTable` mínima: A função `clone` é usada para as operações de clonagem, e a função `no_op` é usada para todas as outras operações. Como o `RawWaker` não faz nada, não importa que retornamos um novo `RawWaker` de `clone` em vez de cloná-lo. + +Após criar a `vtable`, usamos a função [`RawWaker::new`] para criar o `RawWaker`. O `*const ()` passado não importa já que nenhuma das funções da vtable o usa. Por esta razão, simplesmente passamos um ponteiro nulo. + +#### Um Método `run` + +Agora temos uma forma de criar uma instância `Waker`, podemos usá-la para implementar um método `run` em nosso executor. O método `run` mais simples é consultar repetidamente todas as tarefas enfileiradas em um loop até todas estarem prontas. Isso não é muito eficiente já que não utiliza as notificações do tipo `Waker`, mas é uma forma fácil de fazer as coisas funcionarem: + +```rust +// em src/task/simple_executor.rs + +use core::task::{Context, Poll}; + +impl SimpleExecutor { + pub fn run(&mut self) { + while let Some(mut task) = self.task_queue.pop_front() { + let waker = dummy_waker(); + let mut context = Context::from_waker(&waker); + match task.poll(&mut context) { + Poll::Ready(()) => {} // tarefa concluída + Poll::Pending => self.task_queue.push_back(task), + } + } + } +} +``` + +A função usa um loop `while let` para lidar com todas as tarefas na `task_queue`. Para cada tarefa, primeiro cria um tipo `Context` encapsulando uma instância `Waker` retornada por nossa função `dummy_waker`. Então invoca o método `Task::poll` com este `context`. Se o método `poll` retorna `Poll::Ready`, a tarefa está finalizada e podemos continuar com a próxima tarefa. Se a tarefa ainda está `Poll::Pending`, nós a adicionamos de volta ao final da fila para que seja consultada novamente em uma iteração de loop subsequente. + +#### Experimentando + +Com nosso tipo `SimpleExecutor`, agora podemos tentar executar a tarefa retornada pela função `example_task` em nosso `main.rs`: + +```rust +// em src/main.rs + +use blog_os::task::{Task, simple_executor::SimpleExecutor}; + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + + // […] rotinas de inicialização, incluindo init_heap, test_main + + let mut executor = SimpleExecutor::new(); + executor.spawn(Task::new(example_task())); + executor.run(); + + // […] mensagem "it did not crash", hlt_loop +} + + +// Abaixo está a função example_task novamente para que você não precise rolar para cima + +async fn async_number() -> u32 { + 42 +} + +async fn example_task() { + let number = async_number().await; + println!("async number: {}", number); +} +``` + +Quando executamos, vemos que a mensagem esperada _"async number: 42"_ é impressa na tela: + +![QEMU imprimindo "Hello World", "async number: 42", e "It did not crash!"](qemu-simple-executor.png) + +Vamos resumir os vários passos que acontecem neste exemplo: + +- Primeiro, uma nova instância do nosso tipo `SimpleExecutor` é criada com uma `task_queue` vazia. +- Em seguida, chamamos a função assíncrona `example_task`, que retorna uma future. Encapsulamos esta future no tipo `Task`, que a move para o heap e a fixa, e então adicionamos a tarefa à `task_queue` do executor através do método `spawn`. +- Então chamamos o método `run` para iniciar a execução da única tarefa na fila. Isso envolve: + - Retirar a tarefa do início da `task_queue`. + - Criar um `RawWaker` para a tarefa, convertê-lo em uma instância [`Waker`], e então criar uma instância [`Context`] a partir dele. + - Chamar o método [`poll`] na future da tarefa, usando o `Context` que acabamos de criar. + - Como a `example_task` não espera por nada, pode executar diretamente até seu fim na primeira chamada `poll`. É aqui que a linha _"async number: 42"_ é impressa. + - Como a `example_task` retorna diretamente `Poll::Ready`, ela não é adicionada de volta à fila de tarefas. +- O método `run` retorna após a `task_queue` se tornar vazia. A execução de nossa função `kernel_main` continua e a mensagem _"It did not crash!"_ é impressa. + +### Entrada de Teclado Assíncrona + +Nosso executor simples não utiliza as notificações `Waker` e simplesmente faz loop sobre todas as tarefas até estarem prontas. Isso não foi um problema para nosso exemplo já que nossa `example_task` pode executar diretamente até finalizar na primeira chamada `poll`. Para ver as vantagens de desempenho de uma implementação `Waker` apropriada, primeiro precisamos criar uma tarefa que é verdadeiramente assíncrona, ou seja, uma tarefa que provavelmente retornará `Poll::Pending` na primeira chamada `poll`. + +Já temos algum tipo de assincronia em nosso sistema que podemos usar para isso: interrupções de hardware. Como aprendemos no post [_Interrupções_], interrupções de hardware podem ocorrer em pontos arbitrários no tempo, determinados por algum dispositivo externo. Por exemplo, um temporizador de hardware envia uma interrupção para a CPU após algum tempo predefinido ter decorrido. Quando a CPU recebe uma interrupção, ela transfere imediatamente o controle para a função manipuladora correspondente definida na tabela de descritores de interrupção (IDT). + +[_Interrupções_]: @/edition-2/posts/07-hardware-interrupts/index.md + +No que segue, criaremos uma tarefa assíncrona baseada na interrupção de teclado. A interrupção de teclado é uma boa candidata para isso porque é tanto não determinística quanto crítica em latência. Não determinística significa que não há forma de prever quando a próxima tecla será pressionada porque depende inteiramente do usuário. Crítica em latência significa que queremos lidar com a entrada de teclado de forma oportuna, caso contrário o usuário sentirá um atraso. Para suportar tal tarefa de forma eficiente, será essencial que o executor tenha suporte apropriado para notificações `Waker`. + +#### Fila de Scancode + +Atualmente, lidamos com a entrada de teclado diretamente no manipulador de interrupção. Isso não é uma boa ideia a longo prazo porque manipuladores de interrupção devem permanecer o mais curtos possível já que podem interromper trabalho importante. Em vez disso, manipuladores de interrupção devem executar apenas a quantidade mínima de trabalho necessária (por exemplo, ler o scancode do teclado) e deixar o resto do trabalho (por exemplo, interpretar o scancode) para uma tarefa em segundo plano. + +Um padrão comum para delegar trabalho para uma tarefa em segundo plano é criar algum tipo de fila. O manipulador de interrupção empurra unidades de trabalho para a fila, e a tarefa em segundo plano lida com o trabalho na fila. Aplicado à nossa interrupção de teclado, isso significa que o manipulador de interrupção lê apenas o scancode do teclado, o empurra para a fila e então retorna. A tarefa de teclado fica no outro extremo da fila e interpreta e lida com cada scancode que é empurrado para ela: + +![Fila de scancode com 8 slots no topo. Manipulador de interrupção de teclado na parte inferior esquerda com uma seta "push scancode" para a esquerda da fila. Tarefa de teclado na parte inferior direita com uma seta "pop scancode" vindo do lado direito da fila.](scancode-queue.svg) + +Uma implementação simples dessa fila poderia ser um [`VecDeque`] protegido por mutex. No entanto, usar mutexes em manipuladores de interrupção não é uma boa ideia porque pode facilmente levar a deadlocks. Por exemplo, quando o usuário pressiona uma tecla enquanto a tarefa de teclado bloqueou a fila, o manipulador de interrupção tenta adquirir o bloqueio novamente e trava indefinidamente. Outro problema com esta abordagem é que `VecDeque` aumenta automaticamente sua capacidade realizando uma nova alocação de heap quando fica cheio. Isso pode levar a deadlocks novamente porque nosso alocador também usa um mutex internamente. Problemas adicionais são que alocações de heap podem falhar ou demorar um tempo considerável quando o heap está fragmentado. + +Para evitar esses problemas, precisamos de uma implementação de fila que não requer mutexes ou alocações para sua operação `push`. Tais filas podem ser implementadas usando [operações atômicas] sem bloqueio para empurrar e retirar elementos. Desta forma, é possível criar operações `push` e `pop` que requerem apenas uma referência `&self` e são, portanto, utilizáveis sem um mutex. Para evitar alocações em `push`, a fila pode ser apoiada por um buffer pré-alocado de tamanho fixo. Embora isso torne a fila _limitada_ (ou seja, tem um comprimento máximo), frequentemente é possível definir limites superiores razoáveis para o comprimento da fila na prática, então isso não é um grande problema. + +[operações atômicas]: https://doc.rust-lang.org/core/sync/atomic/index.html + +##### A Crate `crossbeam` + +Implementar tal fila de forma correta e eficiente é muito difícil, então recomendo aderir a implementações existentes e bem testadas. Um projeto popular de Rust que implementa vários tipos sem mutex para programação concorrente é [`crossbeam`]. Ele fornece um tipo chamado [`ArrayQueue`] que é exatamente o que precisamos neste caso. E temos sorte: o tipo é totalmente compatível com crates `no_std` com suporte a alocação. + +[`crossbeam`]: https://github.com/crossbeam-rs/crossbeam +[`ArrayQueue`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html + +Para usar o tipo, precisamos adicionar uma dependência na crate `crossbeam-queue`: + +```toml +# em Cargo.toml + +[dependencies.crossbeam-queue] +version = "0.3.11" +default-features = false +features = ["alloc"] +``` + +Por padrão, a crate depende da biblioteca padrão. Para torná-la compatível com `no_std`, precisamos desabilitar suas funcionalidades padrão e em vez disso habilitar a funcionalidade `alloc`. (Note que também poderíamos adicionar uma dependência na crate `crossbeam` principal, que reexporta a crate `crossbeam-queue`, mas isso resultaria em um número maior de dependências e tempos de compilação mais longos.) + +##### Implementação da Fila + +Usando o tipo `ArrayQueue`, agora podemos criar uma fila de scancode global em um novo módulo `task::keyboard`: + +```rust +// em src/task/mod.rs + +pub mod keyboard; +``` + +```rust +// em src/task/keyboard.rs + +use conquer_once::spin::OnceCell; +use crossbeam_queue::ArrayQueue; + +static SCANCODE_QUEUE: OnceCell> = OnceCell::uninit(); +``` + +Como [`ArrayQueue::new`] realiza uma alocação de heap, que não é possível em tempo de compilação ([ainda][const-heap-alloc]), não podemos inicializar a variável estática diretamente. Em vez disso, usamos o tipo [`OnceCell`] da crate [`conquer_once`], que torna possível realizar uma inicialização única segura de valores estáticos. Para incluir a crate, precisamos adicioná-la como uma dependência em nosso `Cargo.toml`: + +[`ArrayQueue::new`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.new +[const-heap-alloc]: https://github.com/rust-lang/const-eval/issues/20 +[`OnceCell`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html +[`conquer_once`]: https://docs.rs/conquer-once/0.2.0/conquer_once/index.html + +```toml +# em Cargo.toml + +[dependencies.conquer-once] +version = "0.2.0" +default-features = false +``` + +Em vez do primitivo [`OnceCell`], também poderíamos usar a macro [`lazy_static`] aqui. No entanto, o tipo `OnceCell` tem a vantagem de que podemos garantir que a inicialização não acontece no manipulador de interrupção, evitando assim que o manipulador de interrupção realize uma alocação de heap. + +[`lazy_static`]: https://docs.rs/lazy_static/1.4.0/lazy_static/index.html + +#### Preenchendo a Fila + +Para preencher a fila de scancode, criamos uma nova função `add_scancode` que chamaremos do manipulador de interrupção: + +```rust +// em src/task/keyboard.rs + +use crate::println; + +/// Chamada pelo manipulador de interrupção de teclado +/// +/// Não deve bloquear ou alocar. +pub(crate) fn add_scancode(scancode: u8) { + if let Ok(queue) = SCANCODE_QUEUE.try_get() { + if let Err(_) = queue.push(scancode) { + println!("AVISO: fila de scancode cheia; descartando entrada de teclado"); + } + } else { + println!("AVISO: fila de scancode não inicializada"); + } +} +``` + +Usamos [`OnceCell::try_get`] para obter uma referência à fila inicializada. Se a fila ainda não está inicializada, ignoramos o scancode do teclado e imprimimos um aviso. É importante que não tentemos inicializar a fila nesta função porque ela será chamada pelo manipulador de interrupção, que não deve realizar alocações de heap. Como esta função não deve ser chamável de nosso `main.rs`, usamos a visibilidade `pub(crate)` para torná-la disponível apenas para nosso `lib.rs`. + +[`OnceCell::try_get`]: https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get + +O fato de que o método [`ArrayQueue::push`] requer apenas uma referência `&self` torna muito simples chamar o método na fila estática. O tipo `ArrayQueue` realiza toda a sincronização necessária por si mesmo, então não precisamos de um wrapper mutex aqui. Caso a fila esteja cheia, também imprimimos um aviso. + +[`ArrayQueue::push`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.push + +Para chamar a função `add_scancode` em interrupções de teclado, atualizamos nossa função `keyboard_interrupt_handler` no módulo `interrupts`: + +```rust +// em src/interrupts.rs + +extern "x86-interrupt" fn keyboard_interrupt_handler( + _stack_frame: InterruptStackFrame +) { + use x86_64::instructions::port::Port; + + let mut port = Port::new(0x60); + let scancode: u8 = unsafe { port.read() }; + crate::task::keyboard::add_scancode(scancode); // novo + + unsafe { + PICS.lock() + .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()); + } +} +``` + +Removemos todo o código de manipulação de teclado desta função e em vez disso adicionamos uma chamada para a função `add_scancode`. O resto da função permanece o mesmo de antes. + +Como esperado, pressionamentos de tecla não são mais impressos na tela quando executamos nosso projeto usando `cargo run` agora. Em vez disso, vemos o aviso de que a fila de scancode está não inicializada para cada pressionamento de tecla. + +#### Scancode Stream + +Para inicializar a `SCANCODE_QUEUE` e ler os scancodes da fila de forma assíncrona, criamos um novo tipo `ScancodeStream`: + +```rust +// em src/task/keyboard.rs + +pub struct ScancodeStream { + _private: (), +} + +impl ScancodeStream { + pub fn new() -> Self { + SCANCODE_QUEUE.try_init_once(|| ArrayQueue::new(100)) + .expect("ScancodeStream::new deve ser chamado apenas uma vez"); + ScancodeStream { _private: () } + } +} +``` + +O propósito do campo `_private` é evitar a construção da struct de fora do módulo. Isso torna a função `new` a única forma de construir o tipo. Na função, primeiro tentamos inicializar a estática `SCANCODE_QUEUE`. Entramos em pânico se ela já estiver inicializada para garantir que apenas uma única instância `ScancodeStream` pode ser criada. + +Para disponibilizar os scancodes para tarefas assíncronas, o próximo passo é implementar um método tipo `poll` que tenta retirar o próximo scancode da fila. Embora isso soe como deveríamos implementar a trait [`Future`] para nosso tipo, isso não se encaixa perfeitamente aqui. O problema é que a trait `Future` abstrai apenas sobre um único valor assíncrono e espera que o método `poll` não seja chamado novamente após retornar `Poll::Ready`. Nossa fila de scancode, no entanto, contém múltiplos valores assíncronos, então está ok continuar consultando-a. + +##### A Trait `Stream` + +Como tipos que produzem múltiplos valores assíncronos são comuns, a crate [`futures`] fornece uma abstração útil para tais tipos: a trait [`Stream`]. A trait é definida assim: + +[`Stream`]: https://rust-lang.github.io/async-book/05_streams/01_chapter.html + +```rust +pub trait Stream { + type Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) + -> Poll>; +} +``` + +Esta definição é bem similar à trait [`Future`], com as seguintes diferenças: + +- O tipo associado é chamado `Item` em vez de `Output`. +- Em vez de um método `poll` que retorna `Poll`, a trait `Stream` define um método `poll_next` que retorna um `Poll>` (note o `Option` adicional). + +Há também uma diferença semântica: O `poll_next` pode ser chamado repetidamente, até retornar `Poll::Ready(None)` para sinalizar que o stream está finalizado. Neste aspecto, o método é similar ao método [`Iterator::next`], que também retorna `None` após o último valor. + +[`Iterator::next`]: https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html#tymethod.next + +##### Implementando `Stream` + +Vamos implementar a trait `Stream` para nosso `ScancodeStream` para fornecer os valores da `SCANCODE_QUEUE` de forma assíncrona. Para isso, primeiro precisamos adicionar uma dependência na crate `futures-util`, que contém o tipo `Stream`: + +```toml +# em Cargo.toml + +[dependencies.futures-util] +version = "0.3.4" +default-features = false +features = ["alloc"] +``` + +Desabilitamos as funcionalidades padrão para tornar a crate compatível com `no_std` e habilitamos a funcionalidade `alloc` para disponibilizar seus tipos baseados em alocação (precisaremos disso mais tarde). (Note que também poderíamos adicionar uma dependência na crate `futures` principal, que reexporta a crate `futures-util`, mas isso resultaria em um número maior de dependências e tempos de compilação mais longos.) + +Agora podemos importar e implementar a trait `Stream`: + +```rust +// em src/task/keyboard.rs + +use core::{pin::Pin, task::{Poll, Context}}; +use futures_util::stream::Stream; + +impl Stream for ScancodeStream { + type Item = u8; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let queue = SCANCODE_QUEUE.try_get().expect("não inicializada"); + match queue.pop() { + Some(scancode) => Poll::Ready(Some(scancode)), + None => Poll::Pending, + } + } +} +``` + +Primeiro usamos o método [`OnceCell::try_get`] para obter uma referência à fila de scancode inicializada. Isso nunca deve falhar já que inicializamos a fila na função `new`, então podemos usar com segurança o método `expect` para entrar em pânico se não estiver inicializada. Em seguida, usamos o método [`ArrayQueue::pop`] para tentar obter o próximo elemento da fila. Se tiver sucesso, retornamos o scancode encapsulado em `Poll::Ready(Some(…))`. Se falhar, significa que a fila está vazia. Nesse caso, retornamos `Poll::Pending`. + +[`ArrayQueue::pop`]: https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.pop + +#### Suporte a Waker + +Como o método `Futures::poll`, o método `Stream::poll_next` requer que a tarefa assíncrona notifique o executor quando se torna pronta após `Poll::Pending` ser retornado. Desta forma, o executor não precisa consultar a mesma tarefa novamente até ser notificado, o que reduz grandemente a sobrecarga de desempenho de tarefas em espera. + +Para enviar esta notificação, a tarefa deve extrair o [`Waker`] da referência [`Context`] passada e armazená-lo em algum lugar. Quando a tarefa se torna pronta, ela deve invocar o método [`wake`] no `Waker` armazenado para notificar o executor que a tarefa deve ser consultada novamente. + +##### AtomicWaker + +Para implementar a notificação `Waker` para nosso `ScancodeStream`, precisamos de um lugar onde possamos armazenar o `Waker` entre chamadas poll. Não podemos armazená-lo como um campo no próprio `ScancodeStream` porque ele precisa ser acessível da função `add_scancode`. A solução para isso é usar uma variável estática do tipo [`AtomicWaker`] fornecido pela crate `futures-util`. Como o tipo `ArrayQueue`, este tipo é baseado em instruções atômicas e pode ser armazenado com segurança em um `static` e modificado concorrentemente. + +[`AtomicWaker`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html + +Vamos usar o tipo [`AtomicWaker`] para definir um `WAKER` estático: + +```rust +// em src/task/keyboard.rs + +use futures_util::task::AtomicWaker; + +static WAKER: AtomicWaker = AtomicWaker::new(); +``` + +A ideia é que a implementação `poll_next` armazena o waker atual nesta estática, e a função `add_scancode` chama a função `wake` nele quando um novo scancode é adicionado à fila. + +##### Armazenando um Waker + +O contrato definido por `poll`/`poll_next` requer que a tarefa registre um acordar para o `Waker` passado quando retorna `Poll::Pending`. Vamos modificar nossa implementação `poll_next` para satisfazer este requisito: + +```rust +// em src/task/keyboard.rs + +impl Stream for ScancodeStream { + type Item = u8; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let queue = SCANCODE_QUEUE + .try_get() + .expect("fila de scancode não inicializada"); + + // caminho rápido + if let Some(scancode) = queue.pop() { + return Poll::Ready(Some(scancode)); + } + + WAKER.register(&cx.waker()); + match queue.pop() { + Some(scancode) => { + WAKER.take(); + Poll::Ready(Some(scancode)) + } + None => Poll::Pending, + } + } +} +``` + +Como antes, primeiro usamos a função [`OnceCell::try_get`] para obter uma referência à fila de scancode inicializada. Então otimisticamente tentamos `pop` da fila e retornamos `Poll::Ready` quando tiver sucesso. Desta forma, podemos evitar a sobrecarga de desempenho de registrar um waker quando a fila não está vazia. + +Se a primeira chamada para `queue.pop()` não tiver sucesso, a fila está potencialmente vazia. Apenas potencialmente porque o manipulador de interrupção pode ter preenchido a fila assincronamente imediatamente após a verificação. Como esta condição de corrida pode ocorrer novamente para a próxima verificação, precisamos registrar o `Waker` no `WAKER` estático antes da segunda verificação. Desta forma, um acordar pode acontecer antes de retornarmos `Poll::Pending`, mas é garantido que recebemos um acordar para qualquer scancode empurrado após a verificação. + +Após registrar o `Waker` contido no [`Context`] passado através da função [`AtomicWaker::register`], tentamos retirar da fila uma segunda vez. Se agora tiver sucesso, retornamos `Poll::Ready`. Também removemos o waker registrado novamente usando [`AtomicWaker::take`] porque uma notificação de waker não é mais necessária. Caso `queue.pop()` falhe pela segunda vez, retornamos `Poll::Pending` como antes, mas desta vez com um acordar registrado. + +[`AtomicWaker::register`]: https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html#method.register +[`AtomicWaker::take`]: https://docs.rs/futures/0.3.4/futures/task/struct.AtomicWaker.html#method.take + +Note que há duas formas de um acordar acontecer para uma tarefa que não retornou `Poll::Pending` (ainda). Uma forma é a condição de corrida mencionada quando o acordar acontece imediatamente antes de retornar `Poll::Pending`. A outra forma é quando a fila não está mais vazia após registrar o waker, de modo que `Poll::Ready` é retornado. Como esses acordares espúrios não são evitáveis, o executor precisa ser capaz de lidar com eles corretamente. + +##### Acordando o Waker Armazenado + +Para acordar o `Waker` armazenado, adicionamos uma chamada para `WAKER.wake()` na função `add_scancode`: + +```rust +// em src/task/keyboard.rs + +pub(crate) fn add_scancode(scancode: u8) { + if let Ok(queue) = SCANCODE_QUEUE.try_get() { + if let Err(_) = queue.push(scancode) { + println!("AVISO: fila de scancode cheia; descartando entrada de teclado"); + } else { + WAKER.wake(); // novo + } + } else { + println!("AVISO: fila de scancode não inicializada"); + } +} +``` + +A única mudança que fizemos é adicionar uma chamada para `WAKER.wake()` se o push para a fila de scancode tiver sucesso. Se um waker está registrado no `WAKER` estático, este método chamará o método [`wake`] igualmente nomeado nele, que notifica o executor. Caso contrário, a operação é uma no-op, ou seja, nada acontece. + +[`wake`]: https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.wake + +É importante que chamemos `wake` apenas após empurrar para a fila porque caso contrário a tarefa pode ser acordada muito cedo enquanto a fila ainda está vazia. Isso pode, por exemplo, acontecer ao usar um executor multi-threaded que inicia a tarefa acordada concorrentemente em um núcleo de CPU diferente. Embora ainda não tenhamos suporte a threads, adicionaremos isso em breve e não queremos que as coisas quebrem então. + +#### Tarefa de Teclado + +Agora que implementamos a trait `Stream` para nosso `ScancodeStream`, podemos usá-la para criar uma tarefa de teclado assíncrona: + +```rust +// em src/task/keyboard.rs + +use futures_util::stream::StreamExt; +use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1}; +use crate::print; + +pub async fn print_keypresses() { + let mut scancodes = ScancodeStream::new(); + let mut keyboard = Keyboard::new(ScancodeSet1::new(), + layouts::Us104Key, HandleControl::Ignore); + + while let Some(scancode) = scancodes.next().await { + if let Ok(Some(key_event)) = keyboard.add_byte(scancode) { + if let Some(key) = keyboard.process_keyevent(key_event) { + match key { + DecodedKey::Unicode(character) => print!("{}", character), + DecodedKey::RawKey(key) => print!("{:?}", key), + } + } + } + } +} +``` + +O código é muito similar ao código que tínhamos em nosso [manipulador de interrupção de teclado] antes de modificá-lo neste post. A única diferença é que, em vez de ler o scancode de uma porta de E/S, nós o pegamos do `ScancodeStream`. Para isso, primeiro criamos um novo `Scancode` stream e então usamos repetidamente o método [`next`] fornecido pela trait [`StreamExt`] para obter uma `Future` que resolve para o próximo elemento no stream. Usando o operador `await` nele, aguardamos assincronamente o resultado da future. + +[manipulador de interrupção de teclado]: @/edition-2/posts/07-hardware-interrupts/index.pt-BR.md#interpretando-os-scancodes +[`next`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html#method.next +[`StreamExt`]: https://docs.rs/futures-util/0.3.4/futures_util/stream/trait.StreamExt.html + +Usamos `while let` para fazer loop até o stream retornar `None` para sinalizar seu fim. Como nosso método `poll_next` nunca retorna `None`, este é efetivamente um loop infinito, então a tarefa `print_keypresses` nunca termina. + +Vamos adicionar a tarefa `print_keypresses` ao nosso executor em nosso `main.rs` para obter entrada de teclado funcionando novamente: + +```rust +// em src/main.rs + +use blog_os::task::keyboard; // novo + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + + // […] rotinas de inicialização, incluindo init_heap, test_main + + let mut executor = SimpleExecutor::new(); + executor.spawn(Task::new(example_task())); + executor.spawn(Task::new(keyboard::print_keypresses())); // novo + executor.run(); + + // […] mensagem "it did not crash", hlt_loop +} +``` + +Quando executamos `cargo run` agora, vemos que a entrada de teclado funciona novamente: + +![QEMU imprimindo ".....H...e...l...l..o..... ...W..o..r....l...d...!"](qemu-keyboard-output.gif) + +Se você ficar de olho na utilização de CPU do seu computador, verá que o processo `QEMU` agora mantém continuamente a CPU ocupada. Isso acontece porque nosso `SimpleExecutor` consulta tarefas repetidamente em um loop. Então mesmo se não pressionarmos nenhuma tecla no teclado, o executor chama repetidamente `poll` em nossa tarefa `print_keypresses`, mesmo que a tarefa não possa fazer progresso e retornará `Poll::Pending` cada vez. + +### Executor com Suporte a Waker + +Para corrigir o problema de desempenho, precisamos criar um executor que utilize adequadamente as notificações `Waker`. Desta forma, o executor é notificado quando a próxima interrupção de teclado ocorre, então não precisa continuar consultando a tarefa `print_keypresses` repetidamente. + +#### Task Id + +O primeiro passo na criação de um executor com suporte adequado para notificações de waker é dar a cada tarefa um ID único. Isso é necessário porque precisamos de uma forma de especificar qual tarefa deve ser acordada. Começamos criando um novo tipo wrapper `TaskId`: + +```rust +// em src/task/mod.rs + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct TaskId(u64); +``` + +A struct `TaskId` é um tipo wrapper simples em torno de `u64`. Derivamos várias traits para ela para torná-la imprimível, copiável, comparável e ordenável. Esta última é importante porque queremos usar `TaskId` como o tipo de chave de um [`BTreeMap`] daqui a pouco. + +[`BTreeMap`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html + +Para criar um novo ID único, criamos uma função `TaskId::new`: + +```rust +use core::sync::atomic::{AtomicU64, Ordering}; + +impl TaskId { + fn new() -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(0); + TaskId(NEXT_ID.fetch_add(1, Ordering::Relaxed)) + } +} +``` + +A função usa uma variável estática `NEXT_ID` do tipo [`AtomicU64`] para garantir que cada ID seja atribuído apenas uma vez. O método [`fetch_add`] incrementa atomicamente o valor e retorna o valor anterior em uma operação atômica. Isso significa que mesmo quando o método `TaskId::new` é chamado em paralelo, cada ID é retornado exatamente uma vez. O parâmetro [`Ordering`] define se o compilador tem permissão para reordenar a operação `fetch_add` no fluxo de instruções. Como apenas requeremos que o ID seja único, a ordenação `Relaxed` com os requisitos mais fracos é suficiente neste caso. + +[`AtomicU64`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html +[`fetch_add`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicU64.html#method.fetch_add +[`Ordering`]: https://doc.rust-lang.org/core/sync/atomic/enum.Ordering.html + +Agora podemos estender nosso tipo `Task` com um campo `id` adicional: + +```rust +// em src/task/mod.rs + +pub struct Task { + id: TaskId, // novo + future: Pin>>, +} + +impl Task { + pub fn new(future: impl Future + 'static) -> Task { + Task { + id: TaskId::new(), // novo + future: Box::pin(future), + } + } +} +``` + +O novo campo `id` torna possível nomear exclusivamente uma tarefa, o que é necessário para acordar uma tarefa específica. + +#### O Tipo `Executor` + +Criamos nosso novo tipo `Executor` em um módulo `task::executor`: + +```rust +// em src/task/mod.rs + +pub mod executor; +``` + +```rust +// em src/task/executor.rs + +use super::{Task, TaskId}; +use alloc::{collections::BTreeMap, sync::Arc}; +use core::task::Waker; +use crossbeam_queue::ArrayQueue; + +pub struct Executor { + tasks: BTreeMap, + task_queue: Arc>, + waker_cache: BTreeMap, +} + +impl Executor { + pub fn new() -> Self { + Executor { + tasks: BTreeMap::new(), + task_queue: Arc::new(ArrayQueue::new(100)), + waker_cache: BTreeMap::new(), + } + } +} +``` + +Em vez de armazenar tarefas em um [`VecDeque`] como fizemos para nosso `SimpleExecutor`, usamos uma `task_queue` de IDs de tarefa e um [`BTreeMap`] chamado `tasks` que contém as instâncias `Task` reais. O mapa é indexado pelo `TaskId` para permitir continuação eficiente de uma tarefa específica. + +O campo `task_queue` é um [`ArrayQueue`] de IDs de tarefa, encapsulado no tipo [`Arc`] que implementa _contagem de referência_. Contagem de referência torna possível compartilhar propriedade do valor entre múltiplos proprietários. Funciona alocando o valor no heap e contando o número de referências ativas a ele. Quando o número de referências ativas chega a zero, o valor não é mais necessário e pode ser desalocado. + +Usamos este tipo `Arc` para a `task_queue` porque ela será compartilhada entre o executor e wakers. A ideia é que os wakers empurram o ID da tarefa acordada para a fila. O executor fica na extremidade receptora da fila, recupera as tarefas acordadas por seu ID do mapa `tasks`, e então as executa. A razão para usar uma fila de tamanho fixo em vez de uma fila ilimitada como [`SegQueue`] é que manipuladores de interrupção não devem alocar ao empurrar para esta fila. + +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[`SegQueue`]: https://docs.rs/crossbeam-queue/0.2.1/crossbeam_queue/struct.SegQueue.html + +Além da `task_queue` e do mapa `tasks`, o tipo `Executor` tem um campo `waker_cache` que também é um mapa. Este mapa armazena em cache o [`Waker`] de uma tarefa após sua criação. Isso tem duas razões: Primeiro, melhora o desempenho reutilizando o mesmo waker para múltiplos acordares da mesma tarefa em vez de criar um novo waker cada vez. Segundo, garante que wakers contados por referência não sejam desalocados dentro de manipuladores de interrupção porque isso poderia levar a deadlocks (há mais detalhes sobre isso abaixo). + +Para criar um `Executor`, fornecemos uma função `new` simples. Escolhemos uma capacidade de 100 para a `task_queue`, que deve ser mais que suficiente para o futuro previsível. Caso nosso sistema tenha mais de 100 tarefas concorrentes em algum ponto, podemos facilmente aumentar esse tamanho. + +#### Spawnando Tarefas + +Como para o `SimpleExecutor`, fornecemos um método `spawn` em nosso tipo `Executor` que adiciona uma determinada tarefa ao mapa `tasks` e imediatamente a acorda empurrando seu ID para a `task_queue`: + +```rust +// em src/task/executor.rs + +impl Executor { + pub fn spawn(&mut self, task: Task) { + let task_id = task.id; + if self.tasks.insert(task.id, task).is_some() { + panic!("tarefa com o mesmo ID já em tasks"); + } + self.task_queue.push(task_id).expect("fila cheia"); + } +} +``` + +Se já houver uma tarefa com o mesmo ID no mapa, o método [`BTreeMap::insert`] a retorna. Isso nunca deve acontecer já que cada tarefa tem um ID único, então entramos em pânico neste caso porque indica um bug em nosso código. Similarmente, entramos em pânico quando a `task_queue` está cheia já que isso nunca deve acontecer se escolhermos um tamanho de fila grande o suficiente. + +#### Executando Tarefas + +Para executar todas as tarefas na `task_queue`, criamos um método privado `run_ready_tasks`: + +```rust +// em src/task/executor.rs + +use core::task::{Context, Poll}; + +impl Executor { + fn run_ready_tasks(&mut self) { + // desestruturar `self` para evitar erros do borrow checker + let Self { + tasks, + task_queue, + waker_cache, + } = self; + + while let Some(task_id) = task_queue.pop() { + let task = match tasks.get_mut(&task_id) { + Some(task) => task, + None => continue, // tarefa não existe mais + }; + let waker = waker_cache + .entry(task_id) + .or_insert_with(|| TaskWaker::new(task_id, task_queue.clone())); + let mut context = Context::from_waker(waker); + match task.poll(&mut context) { + Poll::Ready(()) => { + // tarefa concluída -> removê-la e seu waker em cache + tasks.remove(&task_id); + waker_cache.remove(&task_id); + } + Poll::Pending => {} + } + } + } +} +``` + +A ideia básica desta função é similar ao nosso `SimpleExecutor`: Fazer loop sobre todas as tarefas na `task_queue`, criar um waker para cada tarefa e então consultá-las. No entanto, em vez de adicionar tarefas pendentes de volta ao final da `task_queue`, deixamos nossa implementação `TaskWaker` cuidar de adicionar tarefas acordadas de volta à fila. A implementação deste tipo waker será mostrada daqui a pouco. + +Vamos olhar alguns dos detalhes de implementação deste método `run_ready_tasks`: + +- Usamos [_desestruturação_] para dividir `self` em seus três campos para evitar alguns erros do borrow checker. Nomeadamente, nossa implementação precisa acessar o `self.task_queue` de dentro de uma closure, o que atualmente tenta emprestar `self` completamente. Este é um problema fundamental do borrow checker que será resolvido quando [RFC 2229] for [implementado][RFC 2229 impl]. + +Nota do tradutor ([Richard Alves](https://github.com/richarddalves)): Na data desta tradução (2025), verifiquei que o [RFC 2229] já foi implementado. + +- Para cada ID de tarefa retirado, recuperamos uma referência mutável à tarefa correspondente do mapa `tasks`. Como nossa implementação `ScancodeStream` registra wakers antes de verificar se uma tarefa precisa ser colocada para dormir, pode acontecer que um acordar ocorra para uma tarefa que não existe mais. Neste caso, simplesmente ignoramos o acordar e continuamos com o próximo ID da fila. + +- Para evitar a sobrecarga de desempenho de criar um waker em cada poll, usamos o mapa `waker_cache` para armazenar o waker para cada tarefa após ter sido criado. Para isso, usamos o método [`BTreeMap::entry`] em combinação com [`Entry::or_insert_with`] para criar um novo waker se ele ainda não existir e então obter uma referência mutável a ele. Para criar um novo waker, clonamos a `task_queue` e a passamos junto com o ID da tarefa para a função `TaskWaker::new` (implementação mostrada abaixo). Como a `task_queue` está encapsulada em um `Arc`, o `clone` apenas incrementa a contagem de referência do valor, mas ainda aponta para a mesma fila alocada em heap. Note que reutilizar wakers assim não é possível para todas as implementações de waker, mas nosso tipo `TaskWaker` permitirá isso. + +[_desestruturação_]: https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-to-break-apart-values +[RFC 2229]: https://github.com/rust-lang/rfcs/pull/2229 +[RFC 2229 impl]: https://github.com/rust-lang/rust/issues/53488 + +[`BTreeMap::entry`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.entry +[`Entry::or_insert_with`]: https://doc.rust-lang.org/alloc/collections/btree_map/enum.Entry.html#method.or_insert_with + +Uma tarefa está finalizada quando retorna `Poll::Ready`. Nesse caso, nós a removemos do mapa `tasks` usando o método [`BTreeMap::remove`]. Também removemos seu waker em cache, se existir. + +[`BTreeMap::remove`]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.remove + +#### Design do Waker + +O trabalho do waker é empurrar o ID da tarefa acordada para a `task_queue` do executor. Implementamos isso criando uma nova struct `TaskWaker` que armazena o ID da tarefa e uma referência à `task_queue`: + +```rust +// em src/task/executor.rs + +struct TaskWaker { + task_id: TaskId, + task_queue: Arc>, +} +``` + +Como a propriedade da `task_queue` é compartilhada entre o executor e wakers, usamos o tipo wrapper [`Arc`] para implementar propriedade compartilhada contada por referência. + +[`Arc`]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html + +A implementação da operação de acordar é bem simples: + +```rust +// em src/task/executor.rs + +impl TaskWaker { + fn wake_task(&self) { + self.task_queue.push(self.task_id).expect("task_queue cheia"); + } +} +``` + +Empurramos o `task_id` para a `task_queue` referenciada. Como modificações ao tipo [`ArrayQueue`] requerem apenas uma referência compartilhada, podemos implementar este método em `&self` em vez de `&mut self`. + +##### A Trait `Wake` + +Para usar nosso tipo `TaskWaker` para consultar futures, precisamos convertê-lo em uma instância [`Waker`] primeiro. Isso é necessário porque o método [`Future::poll`] recebe uma instância [`Context`] como argumento, que só pode ser construída a partir do tipo `Waker`. Embora pudéssemos fazer isso fornecendo uma implementação do tipo [`RawWaker`], é tanto mais simples quanto mais seguro em vez disso implementar a trait [`Wake`][wake-trait] baseada em `Arc` e então usar as implementações [`From`] fornecidas pela biblioteca padrão para construir o `Waker`. + +A implementação da trait parece com isto: + +[wake-trait]: https://doc.rust-lang.org/nightly/alloc/task/trait.Wake.html + +```rust +// em src/task/executor.rs + +use alloc::task::Wake; + +impl Wake for TaskWaker { + fn wake(self: Arc) { + self.wake_task(); + } + + fn wake_by_ref(self: &Arc) { + self.wake_task(); + } +} +``` + +Como wakers são comumente compartilhados entre o executor e as tarefas assíncronas, os métodos da trait requerem que a instância `Self` seja encapsulada no tipo [`Arc`], que implementa propriedade contada por referência. Isso significa que temos que mover nosso `TaskWaker` para um `Arc` para chamá-los. + +A diferença entre os métodos `wake` e `wake_by_ref` é que o último requer apenas uma referência ao `Arc`, enquanto o primeiro toma propriedade do `Arc` e, portanto, frequentemente requer um incremento da contagem de referência. Nem todos os tipos suportam acordar por referência, então implementar o método `wake_by_ref` é opcional. No entanto, pode levar a melhor desempenho porque evita modificações desnecessárias da contagem de referência. No nosso caso, podemos simplesmente encaminhar ambos os métodos da trait para nossa função `wake_task`, que requer apenas uma referência compartilhada `&self`. + +##### Criando Wakers + +Como o tipo `Waker` suporta conversões [`From`] para todos os valores encapsulados em `Arc` que implementam a trait `Wake`, agora podemos implementar a função `TaskWaker::new` que é requerida por nosso método `Executor::run_ready_tasks`: + +[`From`]: https://doc.rust-lang.org/nightly/core/convert/trait.From.html + +```rust +// em src/task/executor.rs + +impl TaskWaker { + fn new(task_id: TaskId, task_queue: Arc>) -> Waker { + Waker::from(Arc::new(TaskWaker { + task_id, + task_queue, + })) + } +} +``` + +Criamos o `TaskWaker` usando o `task_id` e `task_queue` passados. Então encapsulamos o `TaskWaker` em um `Arc` e usamos a implementação `Waker::from` para convertê-lo em um [`Waker`]. Este método `from` cuida de construir um [`RawWakerVTable`] e uma instância [`RawWaker`] para nosso tipo `TaskWaker`. Caso esteja interessado em como funciona em detalhes, confira a [implementação na crate `alloc`][waker-from-impl]. + +[waker-from-impl]: https://github.com/rust-lang/rust/blob/cdb50c6f2507319f29104a25765bfb79ad53395c/src/liballoc/task.rs#L58-L87 + +#### Um Método `run` + +Com nossa implementação de waker em vigor, finalmente podemos construir um método `run` para nosso executor: + +```rust +// em src/task/executor.rs + +impl Executor { + pub fn run(&mut self) -> ! { + loop { + self.run_ready_tasks(); + } + } +} +``` + +Este método apenas chama a função `run_ready_tasks` em um loop. Embora teoricamente pudéssemos retornar da função quando o mapa `tasks` se torna vazio, isso nunca aconteceria já que nossa `keyboard_task` nunca termina, então um simples `loop` deve ser suficiente. Como a função nunca retorna, usamos o tipo de retorno `!` para marcar a função como [divergente] para o compilador. + +[divergente]: https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html + +Agora podemos mudar nosso `kernel_main` para usar nosso novo `Executor` em vez do `SimpleExecutor`: + +```rust +// em src/main.rs + +use blog_os::task::executor::Executor; // novo + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // […] rotinas de inicialização, incluindo init_heap, test_main + + let mut executor = Executor::new(); // novo + executor.spawn(Task::new(example_task())); + executor.spawn(Task::new(keyboard::print_keypresses())); + executor.run(); +} +``` + +Só precisamos mudar a importação e o nome do tipo. Como nossa função `run` é marcada como divergente, o compilador sabe que nunca retorna, então não precisamos mais de uma chamada para `hlt_loop` no final de nossa função `kernel_main`. + +Quando executamos nosso kernel usando `cargo run` agora, vemos que a entrada de teclado ainda funciona: + +![QEMU imprimindo ".....H...e...l...l..o..... ...a..g..a....i...n...!"](qemu-keyboard-output-again.gif) + +No entanto, a utilização de CPU do QEMU não melhorou. A razão para isso é que ainda mantemos a CPU ocupada o tempo todo. Não consultamos mais tarefas até serem acordadas novamente, mas ainda verificamos a `task_queue` em um loop ocupado. Para corrigir isso, precisamos colocar a CPU para dormir se não há mais trabalho a fazer. + +#### Dormir se Inativo + +A ideia básica é executar a [instrução `hlt`] quando a `task_queue` está vazia. Esta instrução coloca a CPU para dormir até a próxima interrupção chegar. O fato de que a CPU imediatamente se torna ativa novamente em interrupções garante que ainda podemos reagir diretamente quando um manipulador de interrupção empurra para a `task_queue`. + +[instrução `hlt`]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) + +Para implementar isso, criamos um novo método `sleep_if_idle` em nosso executor e o chamamos de nosso método `run`: + +```rust +// em src/task/executor.rs + +impl Executor { + pub fn run(&mut self) -> ! { + loop { + self.run_ready_tasks(); + self.sleep_if_idle(); // novo + } + } + + fn sleep_if_idle(&self) { + if self.task_queue.is_empty() { + x86_64::instructions::hlt(); + } + } +} +``` + +Como chamamos `sleep_if_idle` diretamente após `run_ready_tasks`, que faz loop até a `task_queue` se tornar vazia, verificar a fila novamente pode parecer desnecessário. No entanto, uma interrupção de hardware pode ocorrer diretamente após `run_ready_tasks` retornar, então pode haver uma nova tarefa na fila no momento em que a função `sleep_if_idle` é chamada. Apenas se a fila ainda estiver vazia, colocamos a CPU para dormir executando a instrução `hlt` através da função wrapper [`instructions::hlt`] fornecida pela crate [`x86_64`]. + +[`instructions::hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/fn.hlt.html +[`x86_64`]: https://docs.rs/x86_64/0.14.2/x86_64/index.html + +Infelizmente, ainda há uma condição de corrida sutil nesta implementação. Como interrupções são assíncronas e podem acontecer a qualquer momento, é possível que uma interrupção aconteça logo entre a verificação `is_empty` e a chamada para `hlt`: + +```rust +if self.task_queue.is_empty() { + /// <--- interrupção pode acontecer aqui + x86_64::instructions::hlt(); +} +``` + +Caso esta interrupção empurre para a `task_queue`, colocamos a CPU para dormir mesmo que agora haja uma tarefa pronta. No pior caso, isso poderia atrasar o tratamento de uma interrupção de teclado até o próximo pressionamento de tecla ou a próxima interrupção de temporizador. Então como evitamos isso? + +A resposta é desabilitar interrupções na CPU antes da verificação e atomicamente habilitá-las novamente junto com a instrução `hlt`. Desta forma, todas as interrupções que acontecem no meio são atrasadas após a instrução `hlt` para que nenhum acordar seja perdido. Para implementar esta abordagem, podemos usar a função [`interrupts::enable_and_hlt`][`enable_and_hlt`] fornecida pela crate [`x86_64`]. + +[`enable_and_hlt`]: https://docs.rs/x86_64/0.14.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html + +A implementação atualizada de nossa função `sleep_if_idle` parece com isto: + +```rust +// em src/task/executor.rs + +impl Executor { + fn sleep_if_idle(&self) { + use x86_64::instructions::interrupts::{self, enable_and_hlt}; + + interrupts::disable(); + if self.task_queue.is_empty() { + enable_and_hlt(); + } else { + interrupts::enable(); + } + } +} +``` + +Para evitar condições de corrida, desabilitamos interrupções antes de verificar se a `task_queue` está vazia. Se estiver, usamos a função [`enable_and_hlt`] para habilitar interrupções e colocar a CPU para dormir como uma única operação atômica. Caso a fila não esteja mais vazia, significa que uma interrupção acordou uma tarefa após `run_ready_tasks` retornar. Nesse caso, habilitamos interrupções novamente e continuamos a execução diretamente sem executar `hlt`. + +Agora nosso executor coloca adequadamente a CPU para dormir quando não há trabalho a fazer. Podemos ver que o processo QEMU tem uma utilização de CPU muito menor quando executamos nosso kernel usando `cargo run` novamente. + +#### Extensões Possíveis + +Nosso executor agora é capaz de executar tarefas de forma eficiente. Ele utiliza notificações de waker para evitar consultar tarefas em espera e coloca a CPU para dormir quando atualmente não há trabalho a fazer. No entanto, nosso executor ainda é bem básico e há muitas formas possíveis de estender sua funcionalidade: + +- **Agendamento**: Para nossa `task_queue`, atualmente usamos o tipo [`VecDeque`] para implementar uma estratégia _first in first out_ (FIFO), que também é frequentemente chamada de agendamento _round robin_. Esta estratégia pode não ser a mais eficiente para todas as cargas de trabalho. Por exemplo, pode fazer sentido priorizar tarefas críticas em latência ou tarefas que fazem muita E/S. Veja o [capítulo de agendamento] do livro [_Operating Systems: Three Easy Pieces_] ou o [artigo da Wikipedia sobre agendamento][scheduling-wiki] para mais informações. +- **Spawning de Tarefa**: Nosso método `Executor::spawn` atualmente requer uma referência `&mut self` e, portanto, não está mais disponível após invocar o método `run`. Para corrigir isso, poderíamos criar um tipo `Spawner` adicional que compartilha algum tipo de fila com o executor e permite criação de tarefas de dentro das próprias tarefas. A fila poderia ser a própria `task_queue` diretamente ou uma fila separada que o executor verifica em seu loop de execução. +- **Utilizando Threads**: Ainda não temos suporte para threads, mas o adicionaremos no próximo post. Isso tornará possível lançar múltiplas instâncias do executor em threads diferentes. A vantagem desta abordagem é que o atraso imposto por tarefas de longa execução pode ser reduzido porque outras tarefas podem executar concorrentemente. Esta abordagem também permite utilizar múltiplos núcleos de CPU. +- **Balanceamento de Carga**: Ao adicionar suporte a threading, torna-se importante saber como distribuir as tarefas entre os executores para garantir que todos os núcleos de CPU sejam utilizados. Uma técnica comum para isso é [_work stealing_]. + +[capítulo de agendamento]: http://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched.pdf +[_Operating Systems: Three Easy Pieces_]: http://pages.cs.wisc.edu/~remzi/OSTEP/ +[scheduling-wiki]: https://en.wikipedia.org/wiki/Scheduling_(computing) +[_work stealing_]: https://en.wikipedia.org/wiki/Work_stealing + +## Resumo + +Começamos este post introduzindo **multitarefa** e diferenciando entre multitarefa _preemptiva_, que interrompe forçadamente tarefas em execução regularmente, e multitarefa _cooperativa_, que permite que tarefas executem até voluntariamente cederem o controle da CPU. + +Então exploramos como o suporte do Rust para **async/await** fornece uma implementação no nível da linguagem de multitarefa cooperativa. Rust baseia sua implementação em cima da trait `Future` baseada em polling, que abstrai tarefas assíncronas. Usando async/await, é possível trabalhar com futures quase como com código síncrono normal. A diferença é que funções assíncronas retornam uma `Future` novamente, que precisa ser adicionada a um executor em algum ponto para executá-la. + +Por trás dos bastidores, o compilador transforma código async/await em _máquinas de estados_, com cada operação `.await` correspondendo a um possível ponto de pausa. Ao utilizar seu conhecimento sobre o programa, o compilador é capaz de salvar apenas o estado mínimo para cada ponto de pausa, resultando em um consumo de memória muito pequeno por tarefa. Um desafio é que as máquinas de estados geradas podem conter _structs auto-referenciais_, por exemplo quando variáveis locais da função assíncrona se referenciam. Para evitar invalidação de ponteiro, Rust usa o tipo `Pin` para garantir que futures não possam mais ser movidas na memória após serem consultadas pela primeira vez. + +Para nossa **implementação**, primeiro criamos um executor muito básico que consulta todas as tarefas spawnadas em um loop ocupado sem usar o tipo `Waker` de forma alguma. Então mostramos a vantagem das notificações de waker implementando uma tarefa de teclado assíncrona. A tarefa define uma `SCANCODE_QUEUE` estática usando o tipo `ArrayQueue` sem mutex fornecido pela crate `crossbeam`. Em vez de lidar com pressionamentos de tecla diretamente, o manipulador de interrupção de teclado agora coloca todos os scancodes recebidos na fila e então acorda o `Waker` registrado para sinalizar que nova entrada está disponível. Na extremidade receptora, criamos um tipo `ScancodeStream` para fornecer uma `Future` resolvendo para o próximo scancode na fila. Isso tornou possível criar uma tarefa `print_keypresses` assíncrona que usa async/await para interpretar e imprimir os scancodes na fila. + +Para utilizar as notificações de waker da tarefa de teclado, criamos um novo tipo `Executor` que usa uma `task_queue` compartilhada com `Arc` para tarefas prontas. Implementamos um tipo `TaskWaker` que empurra o ID de tarefas acordadas diretamente para esta `task_queue`, que então são consultadas novamente pelo executor. Para economizar energia quando nenhuma tarefa é executável, adicionamos suporte para colocar a CPU para dormir usando a instrução `hlt`. Finalmente, discutimos algumas extensões potenciais ao nosso executor, por exemplo, fornecer suporte multi-core. + +## O Que Vem a Seguir? + +Usando async/await, agora temos suporte básico para multitarefa cooperativa em nosso kernel. Embora multitarefa cooperativa seja muito eficiente, ela leva a problemas de latência quando tarefas individuais continuam executando por muito tempo, impedindo assim outras tarefas de executar. Por esta razão, faz sentido também adicionar suporte para multitarefa preemptiva ao nosso kernel. + +No próximo post, introduziremos _threads_ como a forma mais comum de multitarefa preemptiva. Além de resolver o problema de tarefas de longa execução, threads também nos prepararão para utilizar múltiplos núcleos de CPU e executar programas de usuário não confiáveis no futuro. \ No newline at end of file diff --git a/blog/content/edition-2/posts/_index.pt-BR.md b/blog/content/edition-2/posts/_index.pt-BR.md new file mode 100644 index 00000000..c7079c40 --- /dev/null +++ b/blog/content/edition-2/posts/_index.pt-BR.md @@ -0,0 +1,7 @@ ++++ +title = "Posts" +sort_by = "weight" +insert_anchor_links = "left" +render = false +page_template = "edition-2/page.html" ++++ diff --git a/blog/typos.toml b/blog/typos.toml index a2f89fbf..fa6f781d 100644 --- a/blog/typos.toml +++ b/blog/typos.toml @@ -3,6 +3,7 @@ extend-exclude = [ "*.svg", "*.fr.md", "*.es.md", + "*.pt-BR.md", "blog/config.toml", ]