Manual

do

Maker

.

com

Introdução a sistemas embarcados - Parte 1

Introdução a sistemas embarcados - Parte 1

O lider dos sistemas embarcados sem dúvida é o Linux; Linux embedded ou Linux embarcado. apesar de não ser a forma mais natural de um sistema Linux, o Android é um exemplo de sistema operacional embarcado baseado no kernel do Linux. Entre as aplicações, centrais multimidias (domésticas ou automotivas), automação residencial, equipamentos hospitalares e muitos outros também utilizam sistemas baseados no Kernel linux, utilizando (ou não) a tradicional glibc.
Basicamente, um sistema embarcado pode possuir muitas (ou todas) das características de um sistema operacional desktop comum, como Ubuntu. Por ser um sistema para um hardware minimalístico, toda sua composição deve ser feita pensando na economia dos recursos que são normalmente muito limitados.
Esse é um assunto extenso e do qual não sou especialista, mas tenho alguma experiência baseado no Phantom e em tarefas atuais com compilação de sistema para a arquitetura ARM. Então, farei algumas introduções em assuntos específicos como a composição de um sistema operacional, seguindo por cross-compiling, arquiteturas e algumas curiosidades como tipos binários, tipos de compilação de kernel e mais. Também farei referências a artigos de especialistas na área para introduzir diversas fases do desenvolvimento.

Composição de um sistema operacional

Todos os assuntos abordados a partir desse artigo são de extrema complexidade e dependendo do seu nível de conhecimento ou sua linha de raciocínio, os artigos relacionados a embedded podem ser deficientes para você, leitor. De qualquer forma, vou tentar seguir um caminho que torne possível compor um (ou talvez dois) sistema(s) nos moldes do embarcado.
Um sistema operacional é basicamente um kernel, um sistema raiz e um interpretador de comandos. Agora vamos entrando nos detalhes;

Processo de boot

O processo de boot é semelhante para a maioria dos hardwares, assim como na arquitetura ARM que será exemplifica nos futuros posts. Agora baseando-se em uma arquitetura desktop, normalmente x86 ou x86_64 para 64bits.

  • Ao energizar o hardware, um código inicial faz analise da estrutura física; os canais de comunicação do hardware e sua composição. Estando tudo Ok nesse ponto, é iniciada a sequência de boot, buscando pela ordem pré-definida na BIOS (no caso da plataforma x86).

  • Chegando ao boot do disco rígido, é iniciada uma comunicação com o setor 0, onde encontram-se nos primeiros 512 bytes as informações necessárias para iniciar um sistema operacional (utilizando MBR, não GPT). É como se fosse uma estrada e em uma bifurcação a placa indica cada caminho a seguir para um determinado destino; nos primeiros 446 bytes do setor 0 do disco encontra-se o gerenciador de boot. Do byte 447 ao 512 encontra-se a tabela de partições. Normalmente quando se tem apenas 1 sistema operacional instalado, o gerenciador de boot menospreza um timer, uma vez que não há porque o usuário interagir. Quando se tem mais do que 1 sistema operacional instalado, então cada gerenciador de boot tem sua pré-definição de timeout, podendo ser alterada nos mais populares boot managers do mercado.

A partir do momento que um sistema é escolhido (seja pelo usuário ou boot automático) então é lido (no caso do Linux para a plataforma de exemplo) o endereço físico do kernel e do respectivo ramdisk inicial. O kernel é carregado para a memória e o endereço físico do ramdisk é passado para ele. Quando o kernel é descomprimido, ele assume todas as tarefas de gerenciamento do hardware e passa a descomprimir o ramdisk inicial.

Quando o ramdisk é descomprimido, passa a executar scripts de inicialização com a chamada do init. Ainda nesse ponto o sistema operacional trabalha em modo protegido, ou seja, não há como o usuário interagir até que todo o processo de pré-inicialização seja finalizado. Nessa fase são criados os dispositivos de sistema, são carregados módulos para reconhecimento do hardware (como por exemplo o próprio disco, que até esse ponto poderia não ser reconhecido pelo sistema operacional em carga). Normalmente a última fase desse ramdisk é montar a partição de disco e transferir a raiz do sistema operacional para lá, de forma que o ramdisk inicial não é mais utilizado.

Quando a partição do disco é montada, mais uma vez é chamado o init, só que dessa vez o do disco rígido, não o do ramdisk. Então, todos os serviços configurados nesse sistema nativo são carregados para uma área de memória que é uma imagem do sistema operacional em disco, ou seja, o sistema operacional que está em uso não é lido diretamente do disco rígido e sim de uma área de memória mapeada. Quando é necessário carregar alguma referência que não esteja em memória, então esses dados, programas e suas dependências são carregadas a partir da referência no disco, e é por isso que se abrirmos um programa "pesado" e o fecharmos, ao abrí-lo pela segunda vez sua execução é mais rápida; suas referências ainda se encontrarão em memória.

Terminada a carga do sistema em disco, então é disponibilizado um interpretador de comandos, a partir de onde o usuário poderá interagir com o sistema operacional através de comandos do sistema, sejam eles programas binários de linha de comando ou eventos de gerenciadores de janelas. Vamos descer mais um nível (quanto mais baixo for ficando, maior será a complexidade)

Kernel

O kernel é o núcleo de um sistema operacional; é o cérebro, pois ele interage diretamente com o hardware e também tem um nível de interação com o usuário (por exemplo, através do iptables, que manipula/gera regras para o netfilter - o firewall nativo do linux que trabalha no kernel space).
O Linux é puramente isso - um kernel. O sistema operacional composto por ferramentas opensource é chamado GNU/Linux. No caso do Android, não creio que possa ser chamado assim, mas sistemas como o Meego, TSLinux e (em)Debian são exemplos de GNU/Linux.

Falando ainda do Kernel, uma informação importante que pouco é notada é a nomeação do kernel. Sabe qual a diferença entre vmlinuz e vmlinux? - Não vale falar que é Z e X!

As primeiras duas letras (VM) são um padrão para "Virtual Memory". Linux tem suporte a memória virtual, diferentemente de velhos sistemas operacionals como DOS, que tem um limite de 640KB. O Linux pode utilizar áreas do disco rígido como memória virtual.

O vmlinuz é o executável do kernel do Linux, normalmente localizado em /boot/vmlinuz, podendo ser um link simbólico para um nome mais elaborado contendo versão e distribuição que o compilou (e normalmente o é). Ele normalmente é criado ao invocar make bzImage de dentro do diretório da extração de seu código fonte e ele é gerado em ./arch/<arquitetura>/zImage. Pronto, copie-o para /boot/. O vmlinuz é um arquivo comprimido do vmlinux.Esse arquivo comprimido possui também um micro-extrator, permitindo a descompressão do kernel para a área de memória em tempo de boot. De nenhuma forma você poderá extraí-lo com gzip manualmente.
Além disso, há também o bzImage, que ao contrário do que parece, não é bzip2 na compressão. Enquanto o zImage descomprime o kernel em memória baixa (nos primeiros 640KB), o bzImage o descomprime na memória alta (acima de 1MB). O 'B' inicial é de BIG. Normalmente o vmlinux é um passo intermediário e não há interação até esse momento, mas se ver escrito dessa forma por aí, já sabe que não é um erro ortográfico.

Se um kernel for pequeno, ele funcionará bem tanto com zImage como com bzImage. Um kernel grande não funciona com o zImage. Até aqui já é possível notar que há uma vantagem do bzImage sobre o zImage e superficialmente falando, a questão de existirem os dois é a compatibilidade.

O kernel pode ser construído de duas maneiras; estático ou modular.
Um kernel estático possui todas as características, suporte e drivers compilados intrinsicamente, de forma a não depender de estímulos externos para algo funcionar.
Um kernel modular tem a capacidade de reconhecer ou manipular algum recurso através de cargas externas de códigos, chamados módulos, ou drivers. Desse segundo modo, o kernel possui um tamanho reduzido e seu tempo de carga é menor. Além disso, os drivers podem ser carregados posteriormente pelo sistema operacional ou pelo usuário, o que reduz o tempo de boot desde a descompressão do kernel para a memória.

No post referente à compilação de kernel veremos como recolher e como instalar os modulos.
Tendo comprovado o funcionamento do kernel com suas respectivas configurações de compilação, deve-se então guardar suas pré-definições de forma que seja possível gerar outras compilações sem ter que refazer todo o processo de seleção de drivers, etc. Para isso, basta copiar o arquivo oculto '.config' que se encontrará na raiz do código-fonte do kernel extraido.

Quando compilarmos um kernel, veremos system map, oops e informações do kernel no espaço do usuário.

initrd

O initrd ou, "initial ramdisk" é uma estrutura de sistema mínima que define a carga de um sistema operacional. O initrd possui um sistema de arquivos, um sistema raiz e um interpretador de comandos, além da libC. No caso do Phantom, a carga do sistema termina após a descompressão do initrd em memória, que finaliza mudando a raiz de read-only para read-write. Assim sendo, o próprio ramdisk se torna o snapshot do sistema. Há um custo para isso; todos os recursos necessários do sistema devem estar contidos na ramdisk, uma vez que não se esteja carregando um sistema de um disco rígido. Nesse caso o ramdisk pode se tornar um tanto grande e é necessário tomar muito cuidado para não exceder os limites de memória.

O Linux tem 2 modos de trabalho com a memória; o ramdisk, que é um espaço de memória de tamanho pré-definido para ser utilizado pelo sistema e o ramfs, que é a capacidade de manipular o sistema de arquivos em memória tornando-o expansível quando necessário. Esse segundo modo de trabalho é o melhor, pois se for necessário gerar algum tipo de dado no pseudo-disco, o sistema não entrará em read-only por esgotamento da ramdisk.O ramfs é o modo de operação do Phantom.

libC

Sendo o kernel o "cérebro" do sistema operacional, a libC é a "alma" dele. Isso porque tudo que for executado sobre a raiz do sistema se comunica com o kernel através de instruções compreendidas pela libC. Assim sendo, é fundamental que toda a raiz seja composta por:

  • binários do mesmo tipo (ELF)
  • binários da mesma arquitetura (ARM)
  • libC da mesma versão (uClibc)

Esse conjunto deve ser o básico fundamental para a composição do sistema. Porém, para produzir binários ELF para a arquitetura ARM utilizando a uClibc é necessário fazer o chamado cross-compiling. Não só isso, mas outras informações sobre ARM são importantes nesse momento, como por exemplo o tipo de interface binária da aplicação, ou ABI (Application Binary Interface). Em antigos sistemas como os velhos macs utiliza-se OABI ('O' de old) e para plataformas embarcadas utiliza-se o EABI ('E' de embedded). Existe também a plataforma, sendo que escolhe-se 'generic ARM' no menu 'Target Architecture Variant' quando utilizando o toolchain buildroot para ARM inferior a 7. Esses detalhes serão abordados no momento certo, não se preocupe.

A uClibc ou micro-c-libc é uma libc 'capada' para utilização em plataformas embarcadas, reduzindo drasticamente o tamanho, simbolos e processamento gerado pela tradicional libc, porém diversas plataformas embarcadas utilizam a tradicional glibc por necessidades específicas.

Interpretador de comandos

Por exemplo, o bash. O Bash (Bourn Again Shell) é o interpretador de comandos mais comum nas plataformas Linux, inclusive podendo ser utilizado no busybox. Através dele que se pode interagir com com outras partes do sistema, inclusive com o kernel.

BusyBox

A libC é uma camada de reconhecimento das instruções passadas por programas e de comunicação com o Kernel. Quando executa-se um simples 'ls', a libC entra em ação. Um sistema mínimo é composto por diversos comandos, cada um criado por uma pessoa ou por um grupo, contendo muitas das vezes instruções que são comuns por exemplo à chamada da libC. O BusyBox é um binário único que tem a capacidade de executar as instruções desses binários de sistema tradicionais, porém toda a camada de código que se repete em cada um deles é eliminada e há uma camada única de instruções em tudo que é possível (como includes, rotinas, etc). O BusyBox é completamente customizável através de menu, da mesma forma que o kernel, uClibc, BuidRoot, e outros mais. Sendo assim, pode-se habilitar apenas um conjunto de instruções reduzidas para que o sistema tenha apenas o essencial em comandos, podendo-se ao fim gerar pela própria compilação links simbólicos com os nomes dos comandos para que a mágica aconteça. O BusyBox é a camada de aplicação utilizada também no Phantom.

Essa é apenas uma introdução às ferramentas iniciais que serão utilizadas daqui para frente, nos demais posts sobre sistemas embarcados. No próximo post falaremos sobre toolchains, meios de comunicação para dissolução de problemas e preparação inicial de um ambiente para cross-compiling.
Espero que esteja sendo agradável a leitura!

Inscreva-se no nosso canal Manual do Maker Brasil no YouTube.

Próximo post a caminho!

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.