Embedded

Docker w świecie systemów wbudowanych

Lipiec 29, 2020 0
Podziel się:

W jaki sposób wykorzystać Dockera w embedded? Poniżej przedstawiam przykłady użycia kontenerów.

Czym jest Docker?

Ze słowem Docker ściśle powiązane jest pojęcie kontener, a kontener w ujęciu programistycznym, to pojedyncza jednostka zawierająca w sobie wyizolowane środowisko systemowe. Innymi słowami kontner to standardowa jednostka oprogramowania, która jest w stanie upakować w sobie pliki z kodem źródłowym programu wraz ze wszystkimi potrzebnymi zależnościami wymaganymi do przekształcenia tego kodu w program wynikowy. Kontener działa szybko i pracuje niezależnie od innych środowisk systemowych [1].

Przygotowany kontener może posiadać wszelkie potrzebne komponenty do wykonania zamierzonego zadania, np. uruchomienia bazy danych czy brokera MQTT. Docker zaś jest programem umożliwiającym realizację idei kontenerów. Można za jego pomocą uruchamiać, definiować kontenery oraz nimi zarządzać.

Zalety Dockera:

  • Zapewnienie izolacji między kontenerem a systemem operacyjnym oraz innymi uruchomionymi kontenerami wraz z możliwością zdefiniowania mediów pośredniczących.
  • Kontenery potrzebują mniej zasobów niż wirtualne maszyny, które również można wykorzystać do przygotowania wyizolowanego środowiska. Nie jest wymagane ścisłe przypisanie dostępnych zasobów sprzętowych. Kontenery korzystają z jądra systemu, na którym są uruchamiane.
  • Przenośność. Do wystartowania kontenera wymagane jest jedynie zainstalowanie Dockera na urządzeniu przeznaczonym do uruchomienia. Przygotowany kontener można uruchomić zarówna na Linuksie jak i Windowsie czy MAC OS.
  • Docker pozwala na ujednolicenie narzędzi porzez umieszczenie we wnętrzu kontenera konkretych ich wersji. Może okazać się to pomocne przy pracy nad projektem, a zwłaszcza w sytuacji jego długoterminowego utrzymania.
  • Zwykle Docker pozwala osiągnąć większą wydajność, w granicznym przypadku taką samą jak ta oferowana przez wirtualne maszyny.

Wady Dockera:

  • Trzeba przynajmniej w minimalnym stopniu zaprzyjaźnic się z linią komend (ang. commmand line). Nie jest to jednak takie straszne, a bywa przydatne 😉

Czy Dockera można zastosować w embedded? Czemu nie! W tym artykule postaram przedstawić się jedno z zastosowań kontenerów w świecie systemów wbudowanych. Jakie będzie to zastosowanie okaże się już niebawem. Mam nadzieję, że pomoże ono przekonać do Dockera przynajmniej jedną osobę ze świata systemów wbudowanych.

Docker w embedded: środowisko do kompilacji firmware’u dla mikrokontrolerów

Jednym z zastosowań Dockera w embedded może być przygotowanie kompletnego środowiska do kompilacji firmware’u dla mikrokontrolera. Wszelkie potrzebne narzędzia potrzebne aby przekształcić kod programu do pliku binarnego można zawrzeć wewnątrz kontenera. Po przygotowaniu takiego obrazu kontenera kompilacja na wielu komputerach powinna wyglądać tak samo, czyli:

  • zainstalowanie Dockera (tylko jeśli nie zrobiono tego wcześniej),
  • uruchomienie oraz ewentualnie wcześniejsze zbudowanie obrazu kontenera,
  • skompilowanie programu. Myślę, że nie wygląda to na skomplikowany proces 🙂

Przykładowy program

W celu przedstawienia idei środowiska kompilacyjnego zamkniętego w kontenerze przygotowałem przykład. Jest to program napisany w języku C dla Arduino Nano opartego o mikrokontroler ATmega328P. Realizuje on prostą funkcjonalność. Jest to wysyłanie przez interfejs UART rosnącej wartości licznika co 1 sekundę. Aktualna wartość licznika konwertowana jest do formy znakowej w celu wyświetlania jej w postaci czytelnej dla człowieka.

Kod napisanego programu znajduje się poniżej:

#include <stdint.h>
#include <stdio.h>
#include <avr/io.h>
#include <util/delay.h>

#define CALC_UBRR(baudrate)  (F_CPU/(baudrate*16UL)-1)
#define UART_BAUDRATE        9600U

static void init_uart(uint16_t baudrate);
static void uart_send(uint8_t data, uint8_t data_len);

int main()
{
    uint16_t counter = 0;
    uint8_t data_len = 0;
    uint8_t data_buffer[10] = { 0 };

    init_uart(CALC_UBRR(UART_BAUDRATE));

    while (1) {
        data_len = sprintf((char)data_buffer, „%d\n\r”, counter++);
        uart_send(data_buffer, data_len);

        _delay_ms(1000);
    }
}

void init_uart(uint16_t ubrr)
{
    UBRR0H = ubrr >> 8;
    UBRR0L = ubrr;
    UCSR0B |= (1 << TXEN0); // Enable only TX with default 8N1 configuration
}

void uart_send(uint8_t *data, uint8_t data_len)
{
    while (data_len--) {
        while (!(UCSR0A & (1<<UDRE0)));
        UDR0 = *data++;
    }
}

Kompilacja bez kontenera

Cross kompilacja kodu w poniższym przykładzie została wykonana na Ubuntu 18.04 LTS z użyciem cross kompilatora avr-gcc. Komponenty potrzebne do wykonania kompilacji zostały zainstalowane wcześniej. Jako terminal do obsługi portu szeregowego wykorzystano program minicom.

Objaśnienie komendy wykorzystanej do cross kompilacji:

  • avr-gcc – wywołanie programu będącego cross kompilatorem umożliwiającym cross kompilację kodu na architekturę AVR
  • –std=c11 – wybór wykorzystywanego standardu języka C
  • -Os – włączenie optymalizacji pod kątem jak najmniejszego rozmiaru programu wynikowego
  • -Wall -Wextra -pedantic – włączenie oraz zwiększenie poziomu ostrzeżeń kompilatora
  • -mmcu=atmega328p – ustawienie informacji na jaki mikroprocesor ma zostać skompilakowany kod
  • -DF_CPU=16000000 – dodanie dyrektywy z informacją o częstotliwości pracy mikrokontrolera potrzebnej do poprawnego działania funkcji opóźniających
  • -o main.o – wybór nazwy dla pliku zawierającego wynik procesu kompilacji
  • c – wskazanie pliku z kodem źródłowym przeznaczonym do skompilowania

Objaśnienie komendy wykorzystanej do wgrania programu do pamięci Arduino:

  • sudo – komenda sprawiająca, że wywoływany program zostanie uruchomiony przez użytkownika root, często wymagane w przypadku wykorzystania zasobów sprzętowych
  • avrdude – wywołanie programu umożliwiającego wgranie programu do pamięci Arduino
  • -p atmega328p – wybór modelu mikrokontrolera, który ma zostać zaprogramowany
  • -c arduino – wskazanie jaki programator ma zostać użyty
  • -b 57600 – prędkość transmisji szeregowej wykorzystanej jako medium między komputerem a Arduino
  • -P /dev/ttyUSB0 – ścieżka w systemie do urządzenia umożliwiające transmisję szeregową między komputerem a Arduino
  • -U flash:w:main.o – zdefiniowanie operacji na pamięci jaka ma zostać wykonana w ramach procesu programowania

Kompilacja z użyciem kontenera

Instalacja Dockera

Instrukcja opisuje instalację Dockera na dystrybucji Ubuntu, jednak na końcu sekcji znajduje się link do poradnika, który opisuje proces instalacji na innych systemach operacyjnych.

Przestawię tu instalację Dockera na Ubuntu 18.04 LTS. Wystarczy użyć komend przestawionych poniżej w ukazanej kolejności. Informacje pochodzą z tej instrukcji.

sudo apt-get update
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
    „deb [arch=amd64] https://download.docker.com/linux/ubuntu \
    $(lsb_release -cs) \
    stable”
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

Polecam dodać użytkownika do grupy docker, nie trzeba potem używać sudo przed prawie każdym wywołaniem. Wymagane jest ponowne zalogowanie się na konto użytkownika po użyciu poniższej komendy.

sudo usermod -aG docker $USER

Oczywiście możliwa jest instalacja Dockera na innych systemach operacyjnych. W tym celu odsyłam do oficjalnej instrukcji instalacji Dockera.

Przygotowanie kontenera

To jak ma wyglądać kontener określamy pisząc plik Dockerfile. Poniżej znajduje się kod zawarty w takim pliku, który pozwoli na przygotowanie kontenera zawierającego wszystkie potrzebne narzędzia do skompilowania wcześniej przedstawionego programu dla Arduino Nano.

FROM ubuntu:18.04

RUN apt-get update && apt-get install -y \
avr-libc \
gcc-avr \
avrdude

Patrząc na pierwszą linię powyższego przykładu można zauważyć, że występuje tam słowo kluczowe FROM. Służy ono do wyboru obrazu bazowego dla przygotowywanego kontenera.

W przypadku kontenera, obrazem bazowym możemy nazwać otagowany zestaw programów, plików oraz ustawień środowiskowych w obrębie przestrzeni użytkownika (ang. user space). Zestaw ten może być charakterystyczny dla danej dystybucji, tak jak w przykładzie powyżej. Najbardziej widoczną różnicą w porównaniu do maszyny wirtualnej jest to, że w przypadku kontenera w obrazie bazowym nie jest zawarte jądro (ang. kernel) systemu. Także nie jest obecny kompletny system operacyjny, a jedynie jego część znajdująca się powyżej jądra i potrzebuje ona do działania zapewnienia niższych warst.

Dockerfile pełniący rolę przykładu korzysta z obrazu systemu Ubuntu otagowanego jako 18.04, co odpowiada numerowi wersji wspomnianej dystrybucji. Tu można zadać pytanie, po co używać obrazu Ubuntu 18.04 jednocześnie używając tego samego systemu operacyjnego? Pamiętajmy jednak, że kontener będziemy mogli później uruchomić na wielu innych systemach. Zaś wybór Ubuntu to tylko moja prywatna preferencja. Jest to dystrybucja przeze mnie sprawdzona i wygodna w użytkowaniu.

Kolejnym użytym słowem kluczowym Dockera jest RUN. Można je wykorzystać do wywołania polecenia w sposób przypominający wywołania z użyciem powłoki systemowej. Wykonanie polecenia dzieje się w trakcie budowania kontenera. Powoduje, to że wynik działania wywołanej komendy będzie zawarty w obrazie wynikowym.

FROM i RUN nie są to jedyne słowa kluczowe dostępne dla użytkownika Dockera. Poleceń jest więcej, jednak nie jest to bardzo rozbudowana pula. Jest to atut, ponieważ nauka poleceń Dockera nie trwa długo, a i tak nie trzeba znać wszystkich słów kluczowych aby zaprojektować użyteczny kontener.

Poniżej znajduje się demo z procesu budowy kontenera.

Najistotniejszą komendą z powyższego dema, która uruchamia proces budowy obrazu kontenera jest:

docker build -t avr-build .

gdzie:

docker build -t <WYJŚCIOWA_NAZWA_OBRAZU_KONTENERA> <ŚCIEŻKA_DO_DOCKERFILE>

Obraz z powyższego dema może jednak budować się dłużej niż przedstawiono w tym artykule. Wynika to z tego, że w zaprezentowanym przypadku części obrazu, z poprzednich operacji budowania, znajdowały się w pamięci i zostały wykorzystywane do przyspieszenia omawianego procesu. Jest to bardzo przydatne rozwiązanie w przypadku przebudowy dużych kontenerów. Pozwala to na zaoszczędzenie czasu. Dodatkowo obraz bazowy Ubuntu 18.04 znajdował już się na komputerze, dlatego pominięty został krok związany z pobraniem owego obrazu z ogólnodostępnego zdalnego repozytorium z obrazami.

Kompilacja wewnątrz kontenera

Demo umieszczone poniżej przedstawia proces kompilacji programu dla Arduino Nano wewnątrz kontenera.

Uruchomienie kontenera wykonano komendą:

docker run -it -v $(pwd):/root avr-build bash

gdzie:

docker run -it -v <ŚCIEŻKA_DO_ZAMONTOWANIA_W_KONTENERZE>:<MIEJSCE_DOCELOWE_W_KONTENERZE> <NAZWA_OBRAZU_KONTENERA> <KOMENDA_DO_WYWOŁANIA_W_KONTENERZE>

Istotne w wyżej przedstawionym przypadku było zamontowanie we wnętrzu kontenera folderu z kodem źródłowym programu dla Arduino Nano. Dokonano tego korzystając z flagi -v, po której podano ścieżkę do folderu, który ma być zamontowany oraz miejsce docelowe, w którym ma się znaleźć owy folder. Parametry te oddzielone są znakiem :.

Zastosowano także sztuczkę z wywołaniem powłoki bash. W ogólnym przypadku po wykonaniu zadania kontener jest zatrzymywany, jednak program, który pełni roli powłoki systemowej nie kończy swojego działania samoczynnie, lecz czeka na polecenia od użytkownika. Dlatego możliwe było eksportowanie zawartości uruchomionego wyizolowanego środowiska oraz podanie poleceń prowadzących do otrzymania binarnego pliku wynikowego dla mikrokontrolera.

Jak można było zauważyć w samym kontenerze wykonano tylko kompilację. Wgranie programu na Arduino wykonano już po wyjściu z niego. Mikrokontroler nie był widoczny we wnętrzu kontenera. Stąd potrzeba uruchomienia programatora z poziomu powłoki systemu operacyjnego. Jest to wspominana wcześniej w artykule izolacja na jaką pozwala Docker. Istnieje jednak możliwość przekazania dostępu do urządzenia do wnętrza kontenera. Wtedy możliwe jest wykonanie operacji takich jak wykorzystanie programatora. Rezygnuje się wtedy niestety z części izolacji. Wzrasta wtedy poziom skomplikowania obsługi Dockera oraz utrudnia się przenośność obrazu kontenera. Niemniej istnieje taka możliwość, a użytkownik musi sam zdecydować co jest dla niego ważniejsze.

Porównanie sum kontrolnych

Czy pliki zbudowane wewnątrz jak i poza kontenerem są identyczne? Do sprawdzenia tego można wykorzystać sumy kontrolne. Jeśli wyliczone wartości sum będą sobie równe to można przyjąć, że porównywane pliki są takie same. Dla pewności sprawdzone zostały 2 rodzaje sum kontrolnych, mianowicie MD5 oraz SHA256. W takiej sytuacji prawdopodobieństwo, że pliki z tą samą wartością sumy kontrolnej będą mimo to od siebie różne, zostaje silnie zredukowane. Uzyskane rezultaty można obejrzeć w poniższym demie.

Zarówno wyliczenie sumy kontrolnej korzystając z MD5 jak i SHA256 daje te same wyniki dla pliku binarnego wygenerowanego w kontenerze jak i poza nim. To pozwala na postawienie tezy, że oba produkty kompilacji programu dla Arduino są identyczne. Oznacza to, że użycie kontenera do zbudowania pliku binarnego nie wpłynęło w żadnym stopniu na sam plik.

Informacja opisana powyżej jest dowodem na to, że można przygotować środowisko z ujednolicenowymi narzędziami nie wpływające negatywnie na program wynikowy. Do tego będzie ono przenośne i zachowywać się będzie tak samo na różnych platformach. Nie zmieni również zachowania wraz z upływem czasu. Wersje narzędzi umieszczonych w kontenerze zostaną zachowane.

Kontener a wirtualna maszyna

Jednym z najczęściej przytaczanych punktów w przypadku artykułów związanych z Dockerem jest róznica między kontenerem a wirtualną maszyną. Tu będzie podobnie. Co prawda we wcześniejszych akapitach pojawiły się już przykłady odróżniające od siebie bohaterów porównania, ale chciałbym jeszcze przytoczyć ogólne róznice.

Realizacja idei kontenerów jest abstrakcją wykonaną w warstwie aplikacji. W przypadku Dockera potrzebny jest jego daemon pracujący w tle systemu operacyjnego, który pełni nadzór nad kontenerami. Potrzebny również jest system operacyjny hosta. Może być wiele kontenerów w obrębie jednego sytemu operacyjnego, bądź wirtualnej maszyny. Kontenery wykorzystują jądro systemu, na którym są uruchamiane i zapewniają separację  jedynie w warstwie przestrzeni użytkownika.

Wirtualne maszyny posiadają natomiast w sobie cały, kompletny system operacyjny. Są realizacją idei separacji już w warstwie fizycznej. Rolę zarządcy wirtualnych maszyn, czyli odpowiednika daemona Dockera, pełni hypervisor. Decyduje on o dostępie od zasobów fizycznych lub podejmuję decyzję o ich emulacji. System operacyjny znajduje się wewnątrz wirtualnej maszyny, wraz z jądrem.  Umożliwia to uzyskanie separacji już na poziomie jądra systemu. Na jednej maszynie sprzętowej możliwe jest uruchomienie wielu maszyn wirtualnych.

Oczywiście różnic między kontenerami a wirtualnymi maszynami można wymienić znacznie więcej. Nie było jednak moim celem wymienianie ich wszystkich możliwych, a jedynie nakreślenie podstawowych różnic w koncepcjach. Mimo że obie omawiane idee umożliwiają uzyskanie separacji, czyli tego samego celu, to są one jednak czymś odmiennym. Na to właśnie chciałbym szczególnie zwrócić uwagę. Kontener nie jest synonimem wirtualnej maszyny, choć w mowie potocznej można spotkać się z takim stwierdzeniem.

Krótkie podsumowanie

Jak można zobaczyć na podstawie tego artykułu, Docker może być z powodzeniem wykorzystany i w embedded. Mam nadzieję, że przedstawiony przeze mnie przykład użycia kontenerów okaże się dla kogoś przydatny. Może nawet przekona do wprowadzenia Dockera do własnych projektów czy to komercyjnych, czy prywatnych. O ile już tego nie robi 🙂 Docker jest to bardzo użyteczne narzędzie, także sposobów jego wykorzystania jest wiele.

W ostatnim akapicie, chciałbym bardzo podziękować za uwagę i poświęcony czas 🙂

4.8 / 5
Kategorie: Embedded
Arkadiusz Cichocki
Autor: Arkadiusz Cichocki
Inżynier ds. oprogramowania w Centrum Kompetencyjnym Embedded

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

Zostaw komentarz