- Compartilha a experiência de implementar um kernel protótipo de sistema operacional com time-sharing na arquitetura RISC-V
- Explica, com foco em prática, o conceito e o funcionamento de um kernel de time-sharing, implementado em Zig em vez de C para melhorar a reprodutibilidade
- Adota a abordagem de unikernel, empacotando kernel e código de usuário em um único binário, e usa OpenSBI como camada para saída no console e controle do temporizador
- As threads executam em modo usuário (U-mode), enquanto o kernel realiza troca de contexto em modo supervisor (S-mode) por meio de interrupções de temporizador, atravessando a fronteira com chamadas de sistema
- O ponto central é a técnica de trocar o stack frame acumulado pelo prólogo/epílogo da interrupção para restaurar o conjunto de registradores e CSR de outra thread, mudando assim o fluxo de execução
- Com base em uma máquina virtual QEMU e no OpenSBI mais recente, oferece um ambiente de aprendizado reproduzível por qualquer pessoa e serve como material introdutório valioso para ensino e prática ao conectar conceitualmente o espectro de virtualização entre threads, processos e contêineres
Visão geral
- Apresenta o processo de implementar diretamente um kernel de sistema operacional com time-sharing na arquitetura RISC-V
- O público principal são iniciantes em software de sistemas e arquitetura de computadores, estudantes e engenheiros interessados em entender princípios de funcionamento de baixo nível
- Este experimento usa a linguagem Zig em vez da tradicional linguagem C para aumentar a reprodutibilidade prática, além de simplificar a instalação
- O código final está publicado no repositório popovicu/zig-time-sharing-kernel, embora possa haver alguma diferença em relação ao texto
- Recomenda-se tratar a versão do repositório como a fonte única de verdade em vez dos trechos de código do artigo
- Isso também facilita alinhar o ambiente prático com base no script do linker e nas opções de build do repositório
Leitura recomendada
- O texto pressupõe noções básicas de arquitetura de computadores, como registradores, endereçamento de memória e interrupções
- Como material prévio, recomenda Bare metal on RISC-V, processo de boot do SBI e exemplo de interrupção de temporizador
- O texto sobre micro distribuição Linux também pode ser útil opcionalmente para entender a filosofia de separação entre kernel e espaço de usuário
Unikernel
- Adota uma configuração de unikernel que vincula a aplicação e o kernel do SO em um único executável
- Evita a complexidade de loader e linker em tempo de execução e simplifica o carregamento do código de usuário na memória junto com o kernel
- Para fins didáticos e de reprodutibilidade, isso traz vantagens em simplicidade de distribuição e consistência de ambiente
Camada SBI
- O RISC-V usa um modelo de privilégios com modos M/S/U, e neste experimento o OpenSBI fica no modo M enquanto o kernel roda em modo S
- A saída no console e o controle do dispositivo de temporizador são delegados ao SBI para garantir portabilidade
- Quando o SBI não está disponível, usa-se UART MMIO como fallback, embora o uso do OpenSBI mais recente seja recomendado para a prática
Objetivo do kernel
- Para simplificar, oferece suporte apenas a threads estáticas, compostas por funções que nunca terminam
- As threads executam em modo U e enviam chamadas de sistema ao kernel em modo S
- Implementa escalonamento por time-sharing em núcleo único para permitir a troca para outra thread a cada tick do temporizador
Virtualização e o que exatamente é uma thread
- O threading com time-sharing é uma forma de virtualização que permite executar várias tarefas em paralelo em um único núcleo sem mudar o modelo de programação
- Diferentemente do escalonamento cooperativo, a troca ocorre por interrupção de temporizador, sem
yield explícito
- Cada thread possui seu próprio conjunto de registradores inacessível às demais e sua própria pilha, enquanto o restante da memória pode ser compartilhado
A pilha e a virtualização de memória
- Threads precisam de pilhas separadas, essenciais para manter o contexto de execução, incluindo variáveis locais e preservação de
ra conforme a convenção de chamada
- O espectro de virtualização segue thread < processo < contêiner < VM, variando em nível de isolamento e visão (view) do ambiente
- No Linux, contêineres são implementados por meio da combinação de mecanismos do kernel como chroot e cgroups
Virtualizando uma thread
- O objetivo mínimo de virtualização neste experimento é manter o modelo de programação inalterado, proteger registradores e alguns CSRs e atribuir pilhas individuais
- Destaca por que cálculos significativos se tornam impossíveis se a visão dos registradores não for protegida
- Valores iniciais como a0 são semeados na pilha para simplificar a passagem de argumentos no início da thread
Contexto de interrupção
- Interrupções podem ser entendidas como um modelo semelhante a chamada de função, no qual prólogo/epílogo preservam e restauram registradores na pilha
- Para que uma interrupção assíncrona de temporizador não corrompa registradores, é obrigatório seguir a convenção de preservação
- O assembly de exemplo preserva x0–x31 e também salva/restaura CSRs como sstatus, sepc, scause, stval
Implementação (alto nível)
Aproveitando a convenção da pilha de interrupção
- O corpo da rotina de interrupção fica entre o prólogo e o epílogo; ao trocar
sp por outra região de memória, passa-se a restaurar o conjunto de registradores de outro contexto
- Isso corresponde diretamente a uma troca de contexto e é a ideia central da implementação de time-sharing neste experimento
- A interrupção de temporizador entra periodicamente para alternar a execução entre o fluxo principal e o fluxo de interrupção
Separação entre kernel e espaço de usuário
- Mantém a fronteira entre kernel em modo S e usuário em modo U, com interrupções e chamadas de sistema tratadas por um trap handler em modo S
- O boot segue a sequência OpenSBI em modo M → inicialização do kernel em modo S → início das threads em modo U
- Interrupções periódicas de temporizador viabilizam escalonamento e troca de contexto
Implementação (código)
Inicialização em assembly
- Em
startup.S, monta uma sequência mínima que faz a inicialização da BSS, define o stack pointer inicial e então salta para o main em Zig
- O ponto de entrada do kernel usa a convenção
export para se integrar ao ABI de C
Arquivo principal do kernel e drivers de I/O
- O
main de kernel.zig primeiro verifica a função de console do OpenSBI e, se falhar, usa UART MMIO como fallback
sbi.debug_print faz a chamada ajustando os registradores a0/a1/a6/a7 conforme o protocolo ECALL
- Depois de configurar o temporizador, registra o handler de interrupção em modo S e ativa os ticks
Handler em modo S e a troca de contexto
- O handler é escrito com a convenção
naked do Zig para montar manualmente um prólogo/epílogo completo, incluindo a preservação de CSR
- No corpo, chama
handle_kernel(sp) e troca para o sp retornado para decidir se haverá mudança de contexto
- Usa
scause para distinguir entre ECALL em modo U e interrupção de temporizador, tratando cada caso por um caminho diferente
As threads em espaço de usuário
- O código de usuário é incluído no mesmo binário único que o kernel, e as threads de exemplo repetem impressão de strings → loop de espera
syscall.debug_print coloca o número da chamada de sistema 64 em a7 e buffer/tamanho em a0/a1, depois executa ECALL
- Na inicialização da thread, a pilha é semeada com endereço de retorno e valores iniciais de registradores, permitindo o uso imediato de argumentos já no primeiro retorno
Executando o kernel
- O build é feito com
zig build, e a execução acontece no QEMU especificando máquina virt + nographic + OpenSBI fw_dynamic
- No boot, após o banner do OpenSBI, aparecem saídas periódicas intercaladas por ID de thread
- Se compilado com
-Ddebug-logs=true, são exibidos em detalhes a origem da interrupção, a pilha atual e logs de enqueue/dequeue
Conclusão
- Este experimento moderniza um kernel educacional com a combinação RISC-V + OpenSBI + Zig, aumentando a reprodutibilidade e a legibilidade
- Há simplificações como tratamento mínimo de erros e pilhas superdimensionadas, mas o foco está na aprendizagem da essência da troca de contexto e da separação de privilégios
- A portabilidade para hardware real é possível desde que se ajustem constantes de linker e drivers e se garanta a disponibilidade do SBI
Nota adicional: resumo do espectro de virtualização
- Threads: virtualização focada em registradores e pilha, com alta possibilidade de memória compartilhada
- Processo: virtualização do espaço de endereçamento para isolamento de memória, com possibilidade de conter várias threads internamente
- Contêiner: unidade de isolamento composta pela combinação de visões de ambiente, como filesystem e network namespaces
- VM: busca a virtualização completa do hardware como um todo
Resumo dos principais pontos de implementação
- Troca da pilha de interrupção para realizar a troca de contexto
- Trap handler em modo S que preserva/restaura o estado completo, incluindo CSRs
- Caminho de saída duplicado com SBI como prioridade e UART MMIO como fallback
- Escalonamento simples centrado em threads estáticas, núcleo único e time slice
- Chamadas de sistema baseadas em ECALL para deixar clara a fronteira entre modo U e modo S
Ainda não há comentários.