Systemy operacyjne czasu rzeczywistego - charakterystyka i kryteria wyboru
| TechnikaPrzeciętny wielozadaniowy system operacyjny przydziela czas procesora wszystkim zadaniom po kolei według pewnego algorytmu zwanego algorytmem szeregowania. Odpowiada za to tzw. planista (scheduler), program znajdujący się wewnątrz jądra systemu, uruchamiany przerwaniem co określony interwał czasowy. Planista decyduje, czy bieżące zadanie należy kontynuować, a jeśli nie, to któremu z innych procesów przydzielić teraz czas CPU. Kiedy cała kolejka procesów zostanie w ten sposób obsłużona, planista zaczyna całą procedurę od początku.
Wynika z tego, że program użytkownika działający w takim systemie nie jest w stanie przewidzieć dwóch rzeczy: ani kiedy zostanie mu odebrany czas procesora (zdarzenie takie nazywamy wywłaszczeniem, preemption), ani też - co ważniejsze - na jak długo. Tego drugiego natomiast nie da się przewidzieć, gdyż czas obsługi całej kolejki procesów przez planistę jest mało deterministyczny: zależy od zbyt wielu czynników, na które pojedyncze zadanie nie ma wpływu. Można tu zaliczyć częstotliwość przerwania uruchamiającego planistę (może to być kilka lub kilkaset razy na sekundę), długość kolejki procesów w systemie, przy czym liczą się tu też procesy jądra, które mogą być na oko niewidoczne oraz wzajemny stosunek ustawionych dla nich priorytetów.
Takie działanie systemu zaczyna być problemem, kiedy jeden z procesów ma obsługiwać komunikację z urządzeniem zewnętrznym, a protokół wymiany danych jest krytyczny czasowo - czyli nakłada na obie strony obowiązek reakcji w ściśle określonym i na ogół krótkim czasie. O ile odbiór danych przez program zwykle nie nastręcza problemów - proces czekający na nadejście danych jest natychmiast "budzony" w chwili, kiedy się pojawią - o tyle wywłaszczenie go w trakcie nadawania spowoduje oczywiste kłopoty: po drugiej stronie wystąpi tzw. timeout i komunikacja ulegnie zerwaniu.
Problemowi można próbować zaradzić, podnosząc radykalnie częstotliwość pracy zegara sterującego procedurą planisty. Ustawienie tej częstotliwości np. na 1 kHz lub więcej nie zapobiegnie wprawdzie wywłaszczeniom, ale może sprawić, że obsłużenie całej kolejki zadań zajmie planiście czas rzędu jednej lub dwóch setnych sekundy, a tak krótka przerwa w działaniu naszego przykładowego programu może już nie kolidować z zaspokojeniem obiektywnie całkiem wysokich wymagań czasowych.
Jest to jednak sposób jakby żywcem wyjęty z repertuaru gospodyni domowej: po pierwsze, wysoka częstotliwość przerwania zegarowego spowoduje, że wrośnie tzw. narzut systemowy - znaczna część mocy maszyny pójdzie na realizację samego algorytmu szeregowania - a to zniesie bez zauważalnych skutków jedynie całkiem mocny komputer.
Po drugie, skrócenie czasu wykonywania kolejki procesów nie sprawi, że stanie się on przewidywalny - zawsze może zajść coś, co go niespodziewanie wydłuży. System może od czasu do czasu, np. raz na dobę, raz na kilka dni, uruchamiać dodatkowe, intensywnie wykorzystujące zasoby zadanie, a to może wystarczyć do zrujnowania wszystkich kalkulacji dotyczących długości przerw wynikających z wywłaszczania.
Po trzecie, większość systemów operacyjnych (np. Linux) nie pozwala wywłaszczać procesów jądra: start takiego procesu może znacznie opóźnić wykonywanie całej kolejki. Po czwarte wreszcie, samo podniesienie częstotliwości zegara sterującego planistą, w niektórych systemach (FreeBSD) bardzo łatwe, w innych może się okazać niemożliwe (Windows) bądź kłopotliwe (Linux). Nie ma też gwarancji, że szeregowanie będzie wykonywane z zadaną częstotliwością: wybieramy w ten sposób wartość maksymalną, ale z powodu blokowania przerwań wewnątrz jądra będą one obsługiwane z większym lub mniejszym opóźnieniem.
Innym sposobem jest obsługa krytycznego czasowo zdarzenia przez specjalnie napisany moduł jądra systemu operacyjnego. To rozwiązanie, jakkolwiek mniej chałupnicze niż poprzednie, jest obarczone pewnymi wadami: samo zadanie zaimplementowania takiego modułu może się okazać nietrywialne, a na dodatek wszelkie popełnione przy tym błędy wpłyną ujemnie na stabilność całego systemu. By już nie wspomnieć o tym, że taki moduł, w przeciwieństwie do zwykłej aplikacji, jest nieprzenośny na inne środowisko.
W celu uniknięcia takich trudności stworzono pojęcie systemu operacyjnego czasu rzeczywistego (Real-Time Operating System, RTOS). Takie systemy zapewniają środowisko pracy programom, które muszą działać w ścisłym reżimie czasowym. Technicznie rzecz biorąc, RTOS-em jest też każdy system operacyjny, który nie zapewnia wcale wielozadaniowości z wywłaszczaniem (np. MS-DOS). Jednak współcześnie to pojęcie oznacza wielozadaniowy system operacyjny, w którym jednak można mieć pewność, że dany proces nie zostanie wywłaszczony w "niewłaściwym" momencie, a nawet jeśli się tak stanie, to czas procesora zostanie mu zwrócony po upływie interwału o gwarantowanej wielkości maksymalnej.
Specyfika RTOS
Algorytmów szeregowania jest bardzo wiele. Przeciętny system wielozadaniowy dokonuje wywłaszczenia procesu, kiedy zajdzie jeden z następujących warunków:
- proces wywołuje funkcję jądra - jest to na ogół świetna okazja, żeby odebrać mu sterowanie, gdyż program i tak nie może oczekiwać, że np. odczyt danych z pliku znajdującego się na dysku dokona się w jakimś z góry wiadomym czasie,
- proces oddaje czas CPU przez wywołanie funkcji sleep(), yield() lub podobnej,
- upłynął czas, przez który proces może zajmować czas procesora. Ten czas zależy od priorytetu procesu.
W systemach czasu rzeczywistego stosuje się różne strategie podziału czasu CPU. Najprostsze jest szeregowanie według algorytmu FIFO: proces dostaje czas procesora do chwili, kiedy się zakończy i w tej (dopiero) chwili następuje przełączenie zadań. Jest to wzmiankowany wyżej przypadek systemu bez wywłaszczania.
W wielozadaniowym systemie czasu rzeczywistego istnieją trzy główne strategie podziału czasu, z których pierwsza, podstawowa, została opisana powyżej. Drugi algorytm wykazuje pewne analogie z opisanym powyżej mechanizmem podziału czasu w systemie bez wywłaszczania, dlatego również określa się go jako algorytm FIFO. Tu wywłaszczenie następuje po zajściu jednego z następujących warunków:
- proces wywołuje funkcję jądra,
- proces oddaje czas CPU przez wywołanie funkcji sleep(), yield() lub podobnej,
- proces zostaje wywłaszczony w momencie zakończenia się lub w momencie pojawienia się procesu o wyższym priorytecie.
Trzeci algorytm, zwany planowaniem rotacyjnym (round-robin scheduling), często używany w RTOS-ach, stanowi kombinację dwóch wyżej wymienionych. Przy zastosowaniu tego algorytmu proces zostanie wywłaszczony w następujących warunkach:
- proces wywołuje funkcję jądra lub
- proces oddaje czas CPU przez wywołanie funkcji sleep(), yield() lub podobnej lub,
- proces zostaje wywłaszczony w momencie zakończenia się albo w momencie pojawienia się procesu o wyższym priorytecie lub
- upłynął przydzielony mu czas.
Z punktu widzenia programu aplikacyjnego jedną z podstawowych różnic pomiędzy "zwykłym" systemem wielozadaniowym a wielozadaniowym RTOS-em jest działanie priorytetów. W zwykłym systemie zwiększenie priorytetu procesu może wydłużać czas pomiędzy jego wywłaszczeniami, ale nie chroni przed nimi.
Co więcej, wartość priorytetu jest względna: działa ona w zależności od wartości priorytetów nadanych wszystkim innym procesom w systemie. Innymi słowy, proces dostaje (nieco) więcej czasu wtedy, kiedy jego priorytet jest wyższy niż przeciętny priorytet wszystkich procesów w tym systemie. Jeśli jest taki sam - choćby była to wartość najwyższa z możliwych - proces dostanie tyle samo czasu CPU co wszystkie inne aktywne procesy tego systemu. Na dodatek wartość priorytetu nie wpływa w ogóle na to, kiedy wywłaszczony proces zostanie wznowiony.
W niektórych systemach czasu rzeczywistego wysoki priorytet zapewnia procesowi ochronę przed wywłaszczeniem, dopóki w kolejce nie pojawi się zadanie o priorytecie wyższym. Na ogół jednak działa to nieco inaczej: priorytet wyznacza liczbę "gwarantowanych kwantów czasu", jakie dostaje proces, a na dodatek liczba procesów, które mogą mieć określony priorytet, jest ograniczona (w szczególnym przypadku: do jednego). To gwarantuje, że proces co prawda zostanie wywłaszczony, ale w dającym się przewidzieć czasie dostanie procesor z powrotem. Dzięki takim mechanizmom interwały wynikające z wywłaszczania robią się deterministyczne, i o to właśnie chodzi.
Dlaczego nie OS?
Istnieją specjalne odmiany "zwykłych" systemów operacyjnych działające jako systemy czasu rzeczywistego. Taki system na ogół oferuje specjalną klasę procesów czasu rzeczywistego, w której można uruchamiać programy krytyczne czasowo i liczyć na ich deterministyczne funkcjonowanie. Jednak np. w systemie RTLinux działają one jako moduły jądra, co pociąga za sobą, oprócz ograniczeń, także niebezpieczeństwa związane z narażeniem na szwank działania całego systemu. Reszta systemu działa w ramach "normalnego" jądra Linuxa, które jest jednym z procesów czasu rzeczywistego (i ma najniższy priorytet).
Procesy czasu rzeczywistego można też od jakiegoś czasu uruchamiać na niektórych "zwykłych" dystrybucjach systemów operacyjnych. Przykładem może tu być FreeBSD od wersji 5.0, podobne własności ma też Linux. Właśnie Linux używany jest jako system operacyjny w wielu większych urządzeniach mikroprocesorowych.
Mikrokontrolery jednoukładowe osiągnęły taką fazę rozwoju, że większość z nich może już pracować pod kontrolą jakiegoś systemu operacyjnego, nie może to być jednak Linux, wariant Linuksa ani Windows z powodu ogromu zasobów, jakich potrzebują te systemy. W związku z tym projektanci takich małych systemów mikrokontrolerowych nie mają innego wyjścia jak albo obsługiwać sprzęt bezpośrednio, albo użyć systemu operacyjnego czasu rzeczywistego. Poniższe rozważania dotyczą wyboru RTOS-u oraz kryteriów, według których można takiego wyboru dokonać.
Kryteria wyboru RTOS
Przede wszystkim istnieją trzy rodzaje RTOS. Pierwszy rodzaj, tzw. miękki RTOS (soft RTOS), jest zbliżony cechami do zwykłego systemu operacyjnego: zadania wykonywane są tak szybko jak to możliwe, ale bez konieczności mieszczenia się w ustalonym reżimie czasowym. Drugi - solidny RTOS (firm RTOS) - musi zapewnić spełnianie dużych wymagań czasowych w sytuacjach, gdy niespełnienie ich spowoduje wymierne straty. Trzeci - twardy RTOS (hard RTOS) stosujemy, gdy zachodzi absolutna konieczność zmieszczenia się w ostrym reżimie czasowym pod groźbą wystąpienia jakiejś katastrofy.
Można się spotkać z opinią, iż system niespełniający wymagań ostrych ograniczeń czasowych - hard real time performance - w ogóle nie jest systemem czasu rzeczywistego. W każdym razie systemów operacyjnych czasu rzeczywistego istotnie używa się na ogół wtedy, kiedy mamy do czynienia z ostrymi ograniczeniami czasowymi. Pokutuje związany z tym mit, że "RTOS musi być szybki". W rzeczywistości wcale nie musi, gdyż kluczową cechą jest deterministyczne zachowanie i spełnianie nałożonych restrykcji czasowych: a zatem liczy się nie tyle szybkość przetwarzania i zajmowanie minimalnej części czasu CPU, ile zapewnianie zadaniom odpowiedniego czasu reakcji.
Jedną z pierwszych kwestii do rozstrzygnięcia jest zazwyczaj zagadnienie, czy nasz projekt wymaga całego systemu operacyjnego czasu rzeczywistego, czy może wystarczy samo jądro. Jądro nie jest tożsame z RTOS-em, z czego nie wszyscy muszą sobie zdawać sprawę, zwłaszcza w obliczu niezbyt szczęśliwych określeń w rodzaju freeRTOS - wbrew nazwie, nie jest to cały system czasu rzeczywistego, a jedynie jądro takowego.
Typowy RTOS składa się z jądra, które zapewnia szeregowanie procesów, wymianę komunikatów i synchronizację między nimi, mechanizmy pomiaru czasu i obsługę przerwań. Dodatkowo RTOS zawiera mechanizmy obsługi wejścia/wyjścia dające zunifikowany dostęp do urządzeń zewnętrznych, moduły komunikacyjne, sterowniki systemów plików, obsługę wyświetlania obrazu, przykładowe aplikacje, dokumentację oraz zestaw narzędzi do uruchamiania własnych programów (np. debugger). Niektóre większe RTOS-y (jak Integrity, QNX i VxWorks) mają też mechanizm pamięci wirtualnej.
Generalna zasada jest taka, że systemy RTOS działające na mikrokontrolerach jednoukładowych (MCU), takie jak Nucleus, ThreadX, Unison OS, µC/OSII i µC/OS-III, będą działać też w systemach mikroprocesorowych (MPU), ale bez wsparcia dla pamięci wirtualnej. Natomiast wszystkie oferują mechanizmy ochrony pamięci i tym samym mogą użyć jednostki zarządzania pamięcią (MMU) do włączenia tejże ochrony także na większych mikroprocesorach.
Pisać czy kupić?
Jednym z problemów wielkich przedsiębiorstw jest marnotrawienie środków zainwestowanych w tworzenie oprogramowania. We wszystkich większych systemach operacyjnych unika się tego przez stosowanie norm POSIX jako standardowego interfejsu aplikacji (API). Pojawienie się takich produktów jak różne dystrybucje Embedded Linux oraz RTOS-y ze wsparciem dla pamięci wirtualnej, które stosują ten standard, oznacza, że należy pójść za tym trendem i także stosować normy POSIX we własnym oprogramowaniu.
Sprawi to, że środki raz zainwestowane w napisanie danego programu nie pójdą zupełnie na marne przy zmianie platformy sprzętowej. Używanie standardu POSIX zapewnia łatwość ponownego użycia raz napisanych programów, zmniejszenie ryzyka ponownego ponoszenia pewnych kosztów, większą elastyczność i przenośność samego programowania, a także pozwala programistom dużych systemów na łatwe przestawienie się na pracę przy oprogramowaniu przeznaczonym dla mikrokontrolerów jednoukładowych, gdyż większe systemy operacyjne dla komputerów osobistych, a nawet serwerów (takie jak Windows, Linux, różne odmiany BSD), także przestrzegają tego standardu.
Jeśli chodzi o możliwości oferowane przez systemy operacyjne dla mikrokomputerów jednoukładowych i mniejszych mikroprocesorów, stosowanie norm POSIX może przynieść korzyść w postaci możności użycia zarówno samego oprogramowania, jak i nabytej wiedzy na obszarach dotyczących też samych układów mikroprocesorowych oraz układów FPGA.
Modułowość jest kluczowa w środowiskach o ograniczonych zasobach. Dlatego bardzo ważna jest możliwość łatwego włączania i wyłączania poszczególnych modułów. Również bardzo ważna jest możliwość włączania i wyłączania gotowych modułów systemowych podczas inicjowania systemu, tak że sekwencja startowa może zostać łatwo dostosowana do potrzeb użytkownika, a system ma możność zaoferowania pewnych usług szybciej niż innych, podczas gdy proces startu jeszcze trwa w tle.
Następnym ważnym czynnikiem jest czas wprowadzenia gotowego produktu na rynek (time-to-market). Ten czas można znacznie skrócić, a ryzyko ponoszenia dodatkowych kosztów znacznie zmniejszyć przez zakup odpowiednich półproduktów. Nierzadko dostępne są również darmowe odpowiedniki, ale trzeba pamiętać, że rzecz "darmowa" nie zawsze jest rzeczywiście darmowa.
Integrowanie i testowanie wszystkich komponentów zajmuje zwykle około połowy czasu przeznaczonego na typowy projekt związany z tworzeniem oprogramowania, a co za tym idzie, proces integracji i testowania może być dość kosztowny sam w sobie, nawet jeśli same komponenty są dostępne za darmo.
Z tego względu lepiej jest na początek wziąć na warsztat albo standardowe komponenty, albo takie, które dają się bardzo łatwo adaptować do bieżących potrzeb. Nie należy przy tym brać pod uwagę wyłącznie tego, co jest dostępne obecnie, ale należy zwrócić uwagę na możliwe drogi przyszłego rozwoju wszystkich użytych elementów, tak by móc rozwijać własny produkt bez ryzyka ponoszenia nadmiernych kosztów dostosowawczych.
Kolejny ważny aspekt to wybór platformy - a to ma też bliski związek z wyżej wspomnianym czasem wprowadzenia produktu na rynek. Zgodność z POSIX daje tu wachlarz korzyści, które przynoszą zbiorczy efekt w postaci znacznego skrócenia czasu wprowadzenia produktu na rynek, a także zmniejszają całkowity koszt dalszego rozwoju produktu. POSIX polepsza też jakość programowania, gdyż istnieje bardzo wiele narzędzi testujących zgodność z jego normami, na dodatek przygotowanych przez wielu różnych ludzi, co zmniejsza prawdopodobieństwo "przechyłu" w jakąś konkretną stronę - inaczej niż to często bywa, gdy zestawy testujące przygotuje pojedynczy programista.
Ograniczenia w interfejsach wejścia/wyjścia są dość powszechną cechą RTOS-ów. Standard POSIX jest najlepszy również w tym wypadku, gdyż oferuje przenośność i uniwersalny model funkcji I/O. Modele, które próbują naśladować jakiś standard, ale nie są zunifikowane, nastręczają problemów w postaci np. nieoczekiwanych zachowań programu, a to prowadzi do opóźnień w przygotowaniu produktu oraz może powodować wystąpienie ukrytych wad.
Przyjmując za konieczny zakup komponentów w celu zmniejszenia czasu wprowadzenia produktu na rynek oraz zakładając, że potrzebne są standardowe funkcje i usługi, kluczowe jest, żeby wybrane środowisko dysponowało sprawdzonym zestawem modułów obsługujących połączenie z Internetem. Modne dziś terminy to Internet Rzeczy (Internet of Things) oraz komunikacja Machine-to-Machine (M2M). Wkraczając na to pole, należy się upewnić, że wybrany system spełnia nasze wymagania nie tylko tu i teraz, ale również podąża za głównymi trendami rozwojowymi oraz, przede wszystkim, że w podstawowej konfiguracji zawiera moduły obsługujące podstawową i zaawansowaną komunikację z Internetem.
Bezpieczeństwo
Skoro już jesteśmy przy tym: bezpieczeństwo połączeń sieciowych może nie być ważne dziś, ale w dłuższej perspektywie szyfrowanie zapewne okaże się konieczne, a zatem dobrze jest być na to przygotowanym. W chwili obecnej połączenia sieciowe zabezpieczane są przy użyciu protokołu TLS (SSL 3 jest już przestarzały). Czasem używa się sieci VPN, ale są one dość trudne do konfiguracji i wymagają wsparcia protokołów IPsec. Zdalny dostęp przez SSH to norma, tak samo jak bezpieczne przesyłanie plików (SFTP). SNMP może się okazać konieczne przy większych projektach internetowych. Bardzo wysoki stopień podatności na ataki wykazuje zdalne serwisowanie, takie połączenia również trzeba odpowiednio zabezpieczyć: będzie tu potrzebna usługa Secure Boot.
W ciągu ostatnich kilku lat najbardziej powszechnym protokołem komunikacji lokalnej dla mikrokontrolerów jednoukładowych stał się protokół USB. Projektowane dziś urządzenie może go nawet w tej chwili nie potrzebować, ale wobec tych trendów nie jest wykluczone, że obsługa USB stanie się koniecznością w przyszłości.
Z wielu powodów rzeczą godną rozważenia przy wyborze RTOS-u dla mikrokomputera jednoukładowego lub układu z małym mikroprocesorem jest wielkość i "ciężar" systemu. Po pierwsze, wszystko, tj. system i program, musi się zmieścić wewnątrz układu. Jeśli się nie mieści, konieczne jest dodanie zewnętrznej pamięci, a ta jest zazwyczaj wolna i droga.
Trzeba wziąć pod uwagę wielkość dostępnej pamięci w ogóle, obszar zmiennych inicjowanych (w pamięci Flash lub RAM) oraz wielkość obszaru pamięci programu zawierającej stos i inne tego typu struktury systemowe. Jeśli są do wyboru dwa systemy o bardzo podobnych cechach, ale różniące się wielkością, należy wybrać ten mniejszy, ponieważ zmniejsza to koszty poniesione na zakup części i daje większe pole do manewru. Z doświadczenia wynika, że różne implementacje tych samych funkcji mogą się drastycznie różnić co do wielkości wymaganych zasobów.
Konrad Kokoszkiewicz