Linux embedded – toolchain i biblioteka standardowa

Przy budowie, rozwoju modyfikacji systemów wbudowanych opartych o linux embedded konieczny jest wybór tzw. toolchian’a. Powinien on być dopasowany do architektury procesora.

Czym właściwie jest toolchain?

Toolchain to zestaw narzędzi, w skład którego wchodzi m.in kompilator, linker, biblioteki, debugger. Wykorzystywany jest on do budowy bootloader’a, jądra systemu oraz systemu plików, a później też do budowy aplikacji użytkownika. Mówiąc o budowaniu mam na myśli możliwość skompilowania programów napisanych w językach: asembler, C oraz C++. Ponieważ w tych językach dostarczane są podstawowe elementy systemu. Toolchain zazwyczaj basuje na projekcie GNU GCC. Toolchain może pracować w dwojaki sposób:

  • Natywnie: toolchain jest uruchamiany na tym samym typie maszyny co budowana aplikacja. Jest to znane rozwiązanie ze świata komputerów PC. 
  • Cross: toolchain uruchamiany jest na innej maszynie niż zbudowana aplikacja. Czyli kompilacja aplikacji wykonywana jest na innej architekturze niż docelowa.

Zazwyczaj używane jest podejście cross. W polskiej terminologii funkcjonuje pojecie kompilacji skrośnej na to zagadnienie.  Maszyna na której uruchamiany jest toolchain do budowy aplikacji to HOST a maszyna docelowa to TARGET. Podejście takie stosowane jest głównie z takiego powodu, że maszyna host posiada znaczenie większą moc obliczeniową, oraz łatwiejszy dostęp i użytkowanie. Minusem takiego podejścia jest wymóg kompilacji skrośnej oprócz budowanej aplikacji również używanych bibliotek. Co czasem może utrudniać prace. Aby ułatwić pracę programistom powstały do tego celu specjalne narzędzia. Zazwyczaj używa się Buildroot lub Yocto. 

Toolchain powinien uwzględniać: architekturę procesora (np. x86, arm, mips), kolejność bajtów (big lub little-endian), wsparcie obliczeń zmienne-przecinkowych, ABI – Application Binary Interface – reguły współpracy między programami, bibliotekami a system operacyjnym i sprzętem.

Nazwa toolchain’a zawiera w sobie informacje o: CPU (np. Arm, mips x86_64), dostawcy toolchiain’a (np. Buildroot, poky), kernel, systemie operacyjnym(np gnu, może być dołączona też informacji o ABI). Informacje te rozdzielone są myślnikiem. Przykładowe nazwy toolchain’ów: 

  • x86_64-linux-gnu
  • arm-linux-gnueabihf
  • arm-buildroot-linux-gnueabi

Biblioteka standardowa język C

Język C jest jeżykiem systemów operacyjnych z rodziny Unix. Biblioteka języka C jest wykorzystywana przez każdy program (nawet napisany w innym języku niż C) do komunikacji z jądrem systemu operacyjnego. Używa ona wywołania systemowe do komunikacji z kernelem. Jest interfejsem pomiędzy przestrzenią użytkownika a przestrzenią jądra. Możliwe jest ominięcie biblioteki C poprzez bezpośrednie wywołanie systemowe, ale bywa to utrudnione i zazwyczaj niekonieczne.

Dostępnych jest kilka implementacji bibilliteki C. Najbardziej popularne to:

  • Glibc – biblioteka stworzona w ramach projektu GNU. Najbardziej kompletna implementacja POSIX API. 
  • eglibc – wersja glibc dla embedded. Dodano opcje konfguracji, wsparcie kilku architektur. Od 2014 nie jest nadal rozwijana. Została złączona z glibc.
  • uClibc – implementacja przystosowana pod systemy wbudowane. Jest ona o wiele lżejsza niż glibc. Nie implementuje ona w pełni standardu POSIX, ale można uruchomić z nią większość aplikacji współpracujących z glic. Pierwotnie powstała ona dla systemu uClinux – linux dla procesory bez jednostki zarządzania pamięci.
  • musl libc – lekka implementacja biblioteki jezka C dla systemów wbudowanych
  • dietlibc – podobnie jak musl libc, odchudzona implementacja biblioteki C. Zawiera najważniejsze i najcześciej używane funkcje. 

Wielkość pamięci masowej i RAM determinuje wybór odpowiedniej biblioteki języka C. Użycie dystrybucji uClinux implikuje użycie biblioteki uClibc.

Linux w systemach embedded

Wstęp

Zastosowanie Linuxa w urządzeniach embedded jest i staje się coraz bardziej popularne. Przykładowo występuje on w urządzeniach IoT (pełniących role gateway), routerach, telewizorach, rejestratorach video oraz jądro Linuxa używa Android. Warto również wspomnieć o zastosowaniach w branży samochodowej, medycznej i różnych gałęziach przemysłu.

Dlaczego w tego typu urządzeniach używa się systemów operacyjnych? A nie na przykład oprogramowania typu bare-metal lub z wykorzystaniem systemu czasu rzeczywistego – RTOS? Po części jest to związane ze wzrostem mocy obliczeniowej procesorów. Zarówno tych stosowanych w komputerach klasy PC jak i w systemach wbudowanych. Urządzenia embedded często wykorzystują układy scalone które mają w sobie zintegrowany procesor, pamięć oraz całą gamę układów peryferyjnych tj. JTAG, regulatory zasilania, interfejsy komunikacyjne, przetworniki analogowe cyfrowe, timery itp. Są to układy typu SoC(System on Chip).

Linux posiada dobre wsparcie dla obsługi komunikacji sieciowej, Wi-FI, Bluetooth, USB, pamięci masowej oraz sterowników do urządzeń peryferyjnych. Przyspiesza to pracę w budowie urządzeń opartych o te elementy. Linux jest przesortowany, dostosowanych do różnego rodzaju architektur MIPS, x86 ARM – tak popularnej w świecie embedded.  Większość producentów układów SoC dostarcza przygotowaną dystrybucję linuxa, która zawiera obsługę peryferii danego układu. Dodatkowo dostarczają opisy jak samemu takie lub podobne modyfikacji wykonać.

Warto też wspomnieć, że linux jest open source. Dostęp do źródeł, modyfikacji,  umożliwia dopasowania systemu do konkretnego sprzętu (np pod dany SoC) oraz pod konkretne zastosowanie. W celu zwiększenia wydajności wycina się z systemu nieużywane funkcjonalności (np. nie zawsze konieczny jest interfejs graficzny – GUI). Dostęp do kodu źródłowego jądra linuxa nie jest uzależniony od dostawcy sprzętu. 

Niestety złożoność systemu, różnorodność konfiguracji, dostępnych narzędzi, paczek czy mnogość końcowych dystrybucji może wprowadzać w zakłopotanie i trudności w zrozumieniu działania. 

Linux a RTOS

Nie w każdych urządzeniu embedded linux będzie właściwym wyborem. W bardzo małych urządzenia, z małymi procesorami będzie to niemożliwe. W takich systemach wystarczy oprogramowania typu bare-metal (czyli firmware uruchomiony bez pośrednio na procesorze bez sytemu operacyjnego) lub użycie systemu czasu rzeczywistego RTOS(real-time operation system). Zastosowanie systemu operacyjnego Linux jest ograniczone w urządzenia mocno energooszczędnych czy z restrykcjami czasowymi wykonania danych funkcji.  W takim wypadku lepiej sprawdzi się RTOS.

Warto wspomnieć o różnicach pomiędzy system operacyjnym typu Linux a  RTOS. Podstawowa różnica to  przewidywalność i zachowanie reżimów czasowych. Scheduler (planista) w tym systemach przydziela czas procesora na innych zasadach. 

Linux ma Fair Scheduling 

  • Dba o to by każdy wątek dostał odrobinę czasu procesora
  • Czas procesora przydzielany i odbierany jest w niedeterministycznych chwilach czasu

RTOS

  • W danej chwili wykonuje się jedynie najważniejszy wątek

Linux embedded części składowe

Linux embedded składa się z czterech podstawowych elementów. Są to:

  • Toolchain – zawiera zestaw narzędzi (m.in kompilator, debuger) potrzebnych do tworzenia oprogramowania dla danego urządzenia (target divice). W głównej mierze zależny od architektury procesora.
  • Bootloader – program inicjalizujący wstępną konfigurację procesora i niezbędnych peryferii oraz ładuje i startuje kernel Linuksa
  • Kernel – serce, główna część systemu, zarządza zasobami systemowymi i dostępem do sprzętu
  • Root filesystem – zawiera programy i biblioteki, które są wykorzystywane po inicjalizacji jądra

Procesy i wątki

Proces

Proces to swojego rodzaju logiczny kontener, który zawiera m.in.:

  • Listę wątków
  • Mapę pamięci procesu
  • Uchwyty do otwartych plików, połączeń sieciowych, urządzeń itp.
  • Dodatkowe informacje potrzebne systemowi do zarządzania danym procesem
  • Obsługę sygnałów, procesów potomnych

Wydzielenie takie – w postaci procesu, daje systemowi operacyjnemu możliwość zarządzania uruchomionymi programami. Scheduler systemowy (planista) przydziela czas procesora poszczególnym procesom uruchomionym w systemie. Konieczność przełączania pomiędzy procesami spowodowana jest tym, że danej chwili rdzeń procesora może wykonywać jeden wątek. Z punktu widzenia procesora na komputerze wykonywany jest jeden program, a z punktu widzenia systemu jednocześnie wykonywanych jest wiele programów. 

Współczesne procesory mogą wykonywać kilka wątków równocześnie, w zależności od liczby rdzeni. W tym momencie warto zauważyć, że proces po uruchomieniu posiada przynajmniej jeden wątek, z poziomu którego programowo można utworzyć kolejne. Wątek jest odpowidzialny za wykonywanie kodu programu.

Uruchomienie aplikacji powoduje utworzenie w systemie nowego procesu. Wyjątkiem może być np. Skrypt Java Script uruchomiony w przeglądarce, gdzie interpreter jest współdzielony przez wiele skryptów. Każdy proces ma wydzieloną przestrzeń adresową, która jest zapełniania i zwalniana podczas działania programu. Dostęp do tej pamięci przez inne procesy jest ograniczony. Proces zawiera także informacje zarządzane przez system operacyjny np.: identyfikator PID; czas życia uptime; ścieżka do katalogu roboczego; lista otwartych uchwytów; priorytet; lista argumentów.

Tworzenie procesu

Poniżej przedstawiam prosty przykład tworzenia nowego procesu z wykorzystaniem niskopoziomowego API systemów unixowych. Użyta została funkcja fork, która wykonuje kopię obecnego procesu.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>


int main(void)
{
    pid_t pid = fork();
    if(pid == 0)
    {
        //Kod procesu dziecka
        printf("Child Process\n");
    }
    else if(pid > 0)
    {
        //Kod procesu rodzica
        printf("Parent Process\n");
    }
    else
    {
        //Błąd
        perror("Failed to create process\n");
    }

    return 0;
}

Kompilacja i uruchomienie:

gcc -Wall -Wextra fork.c
./a.out
Parent Process
Child Process

Wątek

Wątek w praktyce jest jednostką wykonującą kod programu. Programiści tworzą wiele wątków działających w obrębie jednego procesu w celu uproszczenia struktury/architektury programu, wydzielenia logicznych części programu oraz zwiększenie wydajności przez zrównokeglenie obliczeń. 


Różnica pomiędzy wątkiem a procesem polega na tym, że wątki w obrębie tego samego procesu współdzielą między sobą pamięć adresową, uchwyty do urządzeń, plików, połączeń (proces ma przydzielone niezależne zasoby).Dzięki temu wymagają mniej zasobów do działania, krótszy jest ich czas tworzenia/zakończenia, komunikacja/współdzielenie zasobów może odbywać się bez pomocy jądra systemu oraz możliwość szybkiego przełączania kontekstu pomiędzy wątkami tego samego procesu. Możliwość jednoczesnego wykonywania kodu korzystającego z tych samych zasobów wymaga zapewnienia zsynchronizowanego dostępu do danych w celu uniknięcia błędów w obliczeniach, zakleszczenia wątków czy błędów bezpieczeństwa.

Tworzenie wątków

Poniżej przykład utworzenia nowych wątków z wykorzystaniem API zgodnego z POSIX.

#include <stdio.h>
#include <pthread.h>

void *thread_print(void *data);

int main(void)
{
    pthread_t th1, th2;
    int ret1, ret2;

    ret1 = pthread_create(&th1, NULL, thread_print, (void *)"Message 1");
    if(ret1 != 0)
    {
        fprintf(stderr, "Error: creating thread\n");
        return 1;
    }

    ret2 = pthread_create(&th2, NULL, thread_print, (void *)"Message 2");
    if(ret2 != 0)
    {
        fprintf(stderr, "Error: creating thread\n");
        return 1;
    }

    pthread_join(th1, NULL);
    pthread_join(th2, NULL);

    return 0;
}

void *thread_print(void *data)
{
    char *msg = (char *)data;
    printf("%s\n", msg);
    return NULL;
}

Kompilacja i uruchomienie: 

gcc -Wall -Wextra thread.c -lpthread
./a.out
Message 1
Message 2

UDP vs TCP

UDP

UDP(User Datagram Protocol) jest szybkim i prostym protokołem. Działa w oparciu o protokół IP. Wprowadza kilka dodatkowych paramentów na protokół IP, dzięki którym system może  przekazać dany pakiet do konkretnej aplikacji oraz sprawdzić czy nie uległ uszkodzeniu. Narzut ze strony nagłówka jest minimalny, co z kolej przekłada się na mały narzut wydajności.

Rysunek poniżej przedstawia strukturę nagłówka UDP.

Port UDP działa w oparciu o mechanizm portów (który również występuje w TCP). Port to 16-bitowa liczba, wybrana przez aplikację wysyłającą lub odbierającą dane. W niektórych systemach operacyjnych wybranie portów z przedziału 0-1023 wymaga uprawnień administratora. Mechanizm portów pozwala kierować dane (lub informację o połączeniu) do konkretnej aplikacji lub usługi. Umożliwia to jednoczesną wymianę danych pomiędzy wieloma programami. Nadawca wiadomości musi podać numer portu docelowego, aby system mógł przekazać pakiet do właściwej aplikacji. 

TCP

Do zapewnienia niezawodnej komunikacji wykorzystuje się protokół TCP(Transmission Control Protocol). Podobnie jak UDP działa w warstwie transportowej (czwarta warstwa). TCP stara się zagwarantować jak najwyższą niezawodność połączenia oraz integralność przesyłanych strumieniowo danych. W przeciwieństwie do UDP podczas transmisji TCP można mieć pewność, że dane dotrą w takim stanie jakim zostały wysłane. Jeśli korekcja błędów nie będzie możliwa, to rozmowa zostanie porzucona. Często obarczone jest ponownym wysyłaniem pakietów.

Struktura nagłówka TCP przedstawiona jest na rysunku poniżej.

Warto zwrócić uwagę na:

Bity sterujące m.in. ACK i SYN – wykorzystywane podczas nawiązywania połączenia.Suma kontrolna – 16-bitowa, umożliwia wykrycie uszkodzonych danych.Numer sekwencyjny oraz numer potwierdzający – służą do zapewnienia właściwej kolejności odbioru danych zgodnej z kolejnością ich wysyłania oraz do kontroli, czy wszystkie pakiety dotarły do odbiorcy.

TCP jest protokołem połączeniowym. W celu nawiązania połączenia jedna ze stron tworzy gniazdo nasłuchujące i wiąże je z określonym portem TCP oraz lokalnym adresem IP interfejsu sieciowego (lub może być powiązane z portem na wszystkich dostępnych interfejsach sieciowych – wówczas za adres IP przypisuje się 0.0.0.0). 

Gdy gniazdo nasłuchujące jest gotowe, druga strona komunikacji może zainicjajizować połączenie. Odbywa się to to za pomocą mechanizmu trójetapowego negocjowania połączenia (three-way handshake). Przesyłane są specjalne pakiety (bez danych) do ustalenia celu komunikacji, potwierdzenia tożsamości i ustalenia początkowych numerów sekwencyjnych i potwierdzających. W uproszczeniu proces ten wygląda następująco:

  • Klient wysyła do serwera pusty pakiet z ustawionym bitem SYN oraz początkowym numerem sekwencyjnym
  • Serwer wysyła do klienta pakiet z ustawionymi bitami SYN i ACK
  • Klient wysyła do serwera pakiet z ustawionym bitem ACK

Negocjowanie połączenia eliminuje możliwość prostego podszycia się pod jednego z rozmówców (istotne jest wybranie trudnych do przewidzenia początkowych numerów sekwencyjnych). Po udanej negocjacji połączenie jest nawiązane. Po stronie serwera zostaje utworzone drugie gniazdo, reprezentujące nawiązane połączenie, które służyć będzie do wymiany danych pomiędzy stronami. Gniazdo nasłuchujące nadal pozostaje aktywne i może służyć do nawiązania kolejnych połączeń.Zakończenie sesji następuje, gdy jedna ze stron wyśle pakiet z ustawionym bitem FIN oraz odpowiednim numerem sekwencyjnym. Jest możliwe również zakończenie połączenia w dowolnym momencie poprzez wysłanie pakietu RST.

Podsumowanie

TCPUDP
AkronimTransmission Control ProtocolUser Datagram Protocol
StanowośćPołączeniowyBezpołączeniowy
Kontrola danych16-bitowa suma kontrolna16-bitowa suma kontrolna
Reakcja na uszkodzone danePonowienie transmisji
Odrzucenie pakietu
Kolejność danychKolejność odebranych danych zgodna z kolejnością ich wysyłaniaBrak
PotwierdzeniaPotwierdzenia po otrzymaniu segmentu danychBrak
Rozpoczęcie transmisjiObie stony negocjują/potwierdzają chęć połączeniaBrak
Zakończenie transmisjiJedna ze stron może zażądać zakończenia połączenia lub po określonym czasie braku aktywności którejkolwiek ze stronBrak
Prędkość transmisjiWolniejszy niż UDPSzybszy niż TCP
Używany przez
HTTP, HTTPS, FTP, SMTP, Telnet
DNS, DHCP, TFTP, VOIP 

Mode OSI

Opisów modelu OSI w sieci nie brakuje. Niniejszy wpis ma bardziej formę notatki zawierającej najważniejsze informację niż dokładnej analizy tego zagadnienia. Zapraszam do lektury. 


Model OSI (Open Systems Interconnection) jest modelem warstwowym w którym system sieciowy podzielony jest na 7 warstw. Podział na warstwy ułatwia określenie reguł i zasad komunikacji – protokołów komunikacyjnych. Umożliwia to współdziałanie urządzeń i oprogramowania różnych producentów oraz przede wszystkim ułatwia zarządzanie procesem komunikacji. Model dzieli komunikację na mniejsze łatwiejsze do zarządzania etapy oraz określa zadania jakie muszą być realizowane w poszczególnych warstwach. Model OSI umiejscawia określony protokół w odpowiedniej warstwie abstrakcji.


Rysunek poniżej przedstawia warstwy modelu OSI ISO oraz najważniejsze protokoły pracujące w danej warstwie.

Rola poszczególnych warstw:

Warstwa fizyczna: 

  • definiuje parametry elektryczne nośnika danych (poziomy napięć, długość fali, modulacja) 
  • transmisja surowych danych za pomocą medium fizycznego
  • jednostka danych – bit

Warstwa łącza danych:

  • obsługa medium transmisyjnego
  • kodowanie (niezalecane jest wysyłanie długich sekwencji samych zer lub jedynek)
  • synchronizacja odbiornika do nadajnika
  • jednostka danych – ramka

Warstwa sieciowa:

  • adresowania
  • trasowanie czyli routing – znajdowanie drogi do odbiorcy
  • jednostka danych – pakiet

Warstwa transportowa:

  • zapewnienie jak najwyższej niezawodności i odporności na błędy
  • sposób w jaki sposób dane przesyłane są przez sieć, podstawowe protokoły to TCP, UDP
  • nie musi znać topologi sieci, gdzie znajduje się odbiorca
  • jednostka danych – segment (TCP), datagram (UDP)

Warstwa sesji:

  • utrzymanie połączenia pomiędzy wysokopoziomowymi stronami
  • multipleksowanie danych różnych aplikacji
  • może odbywać się szyfrowanie 
  • jednostka danych – dane

Warstwa prezentacji:

  • ustalenie jednolitego formatu danych
  • porządek bajtów little/big-endian
  • kodowanie liczb np. uzupełnienie do 2
  • kodowanie napisów np. ASCII, UTF-8
  • szyfrowanie(SSH, TLS/SSL), kompresja przesyłanych danych
  • jednostka danych – dane

Warstwa aplikacji:

  • pozwala korzystać z sieci jej użytkownikom
  • jednostka danych – dane

Dane wysłane przez nadawce przechodzą przez poszczególne warstwy od najwyższej do najniższej (w dół stosu). Są one dzielone na odpowiednie fragmenty oraz dodawane są informacje sterujące. Proces ten nazywamy enkapsulacją.  Odbiorca w celu otrzymania poprawnych danych przeprowadza proces odwrotny – dekapsulację. Przepływ danych występuje w górę stosu.

Ethernet

Warstwa fizyczna – PHY, odpowiada za współpracę z właściwym medium transmisyjnym. Natomiast MAC (Media Access Control) za obsługę protokołu Ethernet.MAC z PHY połączone są za pomocą MII, RMII, GMII


Rysunek poniżej przedstawia budowę rakmi ethernet

Aby istniała możliwość detekcji zajętości łącza, to pierwszy bit wysyłanj ramki musi dotrzeć do najdalszego zakątka sieci zanim zostanie wysłany ostatni bit ramki. Stąd minimalna długość pola danych.

Protokół IPv4

Działa w warstwie trzeciej i wprowadza:

  • adresacje IP – 32-bitowe adresy, które zazwyczaj zapisane są w postaci 1-bajtowych wartości oddzielonych kropkami
  • routing, czyli przekazywanie pakietów pomiędzy sieciami

Adres IP składa się z prefiksu i sufiksu, te dwie części wyróżnione są za pomocą maski.Prefiks wraz z maską wyznaczają pulę adresów do której należą wszystkie adresy mające ten sam prefiks.192.168.51.0/24 oznacza przedział 192.168.51.0 do 192.168.51.255

Pule adresów prywatnych:

10.0.0.0 do 10.255.255.255
172.16.0.0 do 172.31.255.255
192.168.0.0 do 192.168.255.255

Podsieci łączy się za pomocą routerów (ma wile interfejsów sieciowych).Rysunek poniżej przedstawia budowę datagramu IP

Jako pierwszy przesyłany jest najbardziej znaczący oktet – big-endian. Jest to tzw. porządek sieciowy oktetów.

ARP – tłumaczenie adresów sieciowych na adresy sprzętowe.

ARP – Address Resolution Protocol

Ethernet używa adresów MAC (sprzętowych). Komunikat ARP podróżuje w danych ramki ethernetowej, która w polu typ ma wartość – 0x0806. Ramka zawiera m.in. pola: adres sieciowy odbiorcy, adres sieciowy nadawcy, adres sprzętowy odbiorcy, adres sprzętowy nadawcy, pole operacja. Węzeł chcący wysłać datagram IP do odbiorcy którego adresy ethernetowego nie zna, rozgłasza do wszystkich węzłów (na adres ff:ff:ff:ff:ff:ff) w swojej podsieci zapytanie ARP (wartość – 1 w polu operacja). Nadawca pole – adres sprzętowy nadawcy oraz adres sieciowy nadawcy uzupełnia swoimi adresami. Natomiast w polu – adres sieciowy odbiorcy umieszcza IP węzła, którego adres ethernetowy chce otrzymać.
Węzeł który ma poszukiwany adres IP odpowiada bezpośrednio pytającemu, gdyż zna jego adres sprzętowy (otrzymał go w zapytaniu). W odpowiedzi pole operacja ma wartość – 2. W odpowiedzi nadawca pola –  adres sieciowy nadawcy i adres ethernetowy nadawcy uzupełnia swoimi adresami.Aby poprawić efektywność komunikacji węzły utrzymują w pamięci podręcznej (cache) tablice ARP, która zawiera poznane dotychczas przyporządkowania adresu ethernetowego do IP. Adresy są aktualizowane przez wysłanie zapytania oraz gdy zapytania otrzymują nawet jeśli nie były one do danego węzła bezpośrednio kierowane.