RTLinux to rygorystyczny (twardy) system operacyjny czasu rzeczywistego, którego cechą charakterystyczną jest to, że współistnieją w nim: jądro czasu rzeczywistego RTCore i jądro Linuksa.
Obecnie istnieją dwie odmiany RTLinuksa: RTLinux/GPL, dostępna na licencji GPL i RTLinuxPro -- komercyjna.
Twórcy RTLinuksa, zamiast wyszukiwać i poprawiać w kodzie Linuksa fragmenty, które mogą stwarzać problemy przy wykonywaniu zadań krytycznych (zadań czasu rzeczywistego), zdecydowali się użyć dwóch różnych jąder do różnych zadań. Ma to na celu wykorzystanie zaawansowanych usług i dużej wydajności w przypadku średnim oferowanej przez system operacyjny ogólnego przeznaczenia z zachowaniem gwarancji terminowego wykonania zadań krytycznych.
W systemach operacyjnych ogólnego przeznaczenia dąży się do optymalizacji przypadku średniego kosztem mniejszej efektywności w przypadku pesymistycznym, który właśnie jest przedmiotem zainteresowania systemów czasu rzeczywistego. Udogodnienia takie jak stronicowanie na żądanie i wszelkie pamięci podręczne powodują nieprzewidywalne odchylenia czasu wykonania. Również szeregowanie w czasie rzeczywistym stawia inne wymagania niż w zwykłym trybie przetwarzania.
Standard POSIX (Portable Operating System Interface) definiuje interfejs funkcji systemowych, by umożliwić przenoszenie aplikacji między różnymi systemami operacyjnymi. Istnieją rozszerzenia POSIX dotyczące różnych aspektów jądra m.in. przetwarzania w czasie rzeczywistym. RTLinux implementuje standard POSIX 1003.13 PSE51 (minimal realtime system profile), by móc korzystać z dorobku oprogramowania i ułatwić programistom tworzenie nowych narzędzi za pomocą znanych mechanizmów.
Pod kontrolą małego jądra czasu rzeczywistego RTCore uruchomione jest jądro Linuksa jako wątek o najniższym priorytecie (idle thread).
Wirtualny mechanizm przerwań jest kluczowym elementem architektury RTLinuksa. Wszystkie przerwania sprzętowe są przechwytywane przez jądro RTCore. Jednocześnie emuluje ono kontroler przerwań na potrzeby Linuksa i Linux nadal działa tak, jakby docierały do niego przerwania sprzętowe.
Każde nadchodzące przerwanie jest przechwytywane przez jądro RTCore. Jeśli jest dla niego zainstalowana procedura obsługi czasu rzeczywistego, to jest ona wywoływana. W przeciwnym razie, o ile nie wykonuje się żadne zadanie krytyczne, przerwanie jest przekazywane do jądra Linuksa. Samodzielne jądro Linuksa czasem blokuje przerwania (np. przy synchronizacji). RTCore pamięta ustawiony przez nie stan bitu zezwolenia na przerwania. Jeśli jądro Linuksa wyłączy przerwania, to nadchodzące dla niego przerwania są oznaczane jako oczekujące i zostaną emulowane, gdy tylko Linux je odblokuje. Dzieje się tak dzięki zamianie w kodzie jądra Linuksa instrukcji cli, sti i iret odpowiednio na makra S_CLI, S_STI, S_IRET.
S_CLI: movl $0, SFIFMakro S_CLI zamiast zerować znacznik IF zezwolenia na przerwania w rejestrze stanu procesora opuszcza flagę SFIF należącą do wirtualnego mechanizmu przerwań.
S_STI: sti pushfl pushl $KERNEL_CS pushl $1f S_IRETMakro S_STI rzeczywiście włącza przerwania i odkłada na stos flagi procesora, rejestr segmentowy jądra i adres powrotu tak jakby przygotowywało obsługę przerwania.
S_IRET: push %ds pushl %eax pushl %edx movl $KERNEL_DS, %edx mov %dx, %ds cli movl SFREQ, %edx andl SFMASK, %edx bsfl %edx, %eax jz 1f S_CLI sti jmp SFIDT(,%eax,4) 1: movl $1, SFIF popl %edx popl %eax pop %ds iretMakro S_IRET najpierw zachowuje używane rejestry (odkłada ich zawartość na stos, którą na koniec zdejmie). Ustawia rejestr segmentu danych na segment jądra, aby mieć dostęp do zmiennych globalnych. Następnie maska bitowa SFREQ jest przeglądana w poszukiwaniu ustawionego bitu reprezentującego oczekujące przerwanie. Jeśli żadne nie zostanie znalezione, to następuje prawdziwy powrót, a w przeciwnym razie skok do procedury obsługi przerwania. Jako że procedurę obsługi przerwania kończy S_IRET, wszystkie oczekujące przerwania zostaną kolejno obsłużone.
Zadania krytyczne to programy użytkownika wykonywane w przestrzeni adresowej jądra RTCore, szeregowane przez planistę tego jądra. Umieszczenie zadań we wspólnej przestrzeni adresowej bez ochrony pamięci powoduje, że błędy w tych zadaniach mogą doprowadzić do załamania systemu. Taka decyzja przynosi jednak następujące korzyści istotne dla zadań czasu rzeczywistego:
Zadania czasu rzeczywistego umieszczane są w ładowalnych modułach jądra. W trakcie inicjacji modułu zadanie są tworzone za pomocą standardowej funkcji pthread_create. Zadania mogą być wykonywane okresowo, jak również mogą być zawieszane i budzone z poziomu procedur obsługi przerwań.
Program szeregujący jest zaimplementowany jako ładowalny moduł jądra RTCore, więc można napisać własny. Algorytmy szeregowania stosowane w systemach czasu rzeczywistego opierają się zwykle na priorytetowym wywłaszczaniu tzn. zadanie krytyczne jest wykonywane dopóki dobrowolnie nie odda procesora albo w kolejce zadań gotowych nie pojawi się zadanie o wyższym priorytecie.
Na potrzeby omawiania algorytmów szeregowania posłużymy się przykładem dwóch zadań okresowych. Okres pierwszego wynosi T1 = 50 ms, a najgorszy czas wykonania (podczas jednego okresu) to C1 = 25 ms. Dla drugiego zadania wartości te to odpowiednio T2 = 100 ms i C2 = 40 ms. Dla obu zadań nieprzekraczalny termin zakończenia (deadline) przypada na początek ich następnego okresu. Niewłaściwy dobór priorytetów może prowadzić do niedotrzymania terminu. Ilustruje to rysunek, na którym zadanie drugie ma pierwszeństwo przed pierwszym:
Wykorzystanie procesora przez zadanie wyraża się wzorem U = C/T. Dla zbioru zadań rozważa się sumaryczne wykorzystanie procesora. Ograniczenie szeregowalności (schedulable bound) to maksymalne wykorzystanie procesora, jakie zapewnia, że zbiór zadań jest szeregowalny.
Domyślny planista używa statycznego doboru priorytetów algorytmem RMS, zgodnie z którym im krótszy okres zadania, tym wyższy jego priorytet. Jest to algorytm optymalny w tym sensie, że jeśli zadanie nie jest szeregowalne (nie może być wypełnione w terminie) przez ten algorytm, to nie jest szeregowalne przez żaden algorytm używający statycznych priorytetów. Wadą tego algorytmu jest niskie ograniczenie szeregowalności - 69.3%, które oznacza, że jeśli zbiór zadań wykorzystuje procesor w 70%, to być może nie wszystkie zadania mogą być wypełnione w terminie. W naszym przykładzie zadanie pierwsze powinno mieć wedle tego schematu pierwszeństwo przed zadaniem drugim:
Obecnie RTLinux jest dystrybuowany razem z dodatkowym modułem implementującym planistę z dynamicznym przydziałem priorytetów. Im bliższy nieprzekraczalny termin wykonania, tym wyższy priorytet. Zaletą tego algorytmu jest 100% ograniczenie szeregowalności, ale wadę stanowi narzut na oblicznie priorytetów.
Ani RMS ani EDF nie gwarantują terminowego wypełnienia zadań nieokresowych zwanych sporadycznymi tzn. pojawiających się w dowolnym czasie. Algorytmy o nazwach Slot Shifting i Stack Stealing mają na celu lepszą obsługę zadań nieokresowych poprzez wykorzystanie wolnych cykli procesora pomiędzy zadaniami okresowymi.
Sprzęt do pomiaru upływającego czasu to czasomierz programowalny (programmable interval timer). W komputerach klasy PC jest to zwykle Intel 8253/8254. Można go nastawić na okresowe generowanie przerwań lub jednorazowe wygenerowanie przerwania po upływie pewnego czasu. Domyślny planista jądra RTCore korzystał w pierwotnej wersji tylko z tego drugiego trybu (RTL_CLOCK_MODE_ONESHOT). Na przykład dla dwóch zadań okresowych o okresach A i B czasomierz generował najpierw przerwanie po A jednostkach czasu, a potem po (B-A-x) jednostkach, gdzie x jest czasem potrzebnym na przeprogramowanie czasomierza. Zaletą tego podejścia jest rzadkie obsługiwanie przerwań, a wadą przeprogramowywanie czasomierza, które na jednoprocesorowych architekturach x86 jest stosunkowo powolne (platformy wieloprocesorowe mają bardziej zaawansowane czasomierze). Poza tym zdarza się, że dla okresów zadań istnieje duży wspólny dzielnik, który można przyjąć za podstawę generowania przerwań okresowych. Dlatego obecnie domyślny planista może pracować i w tym trybie (RTL_CLOCK_MODE_PERIODIC).
Tylko zadania, na które nałożono twarde ograniczenia czasowe, działają jako zadania krytyczne. Pozostała praca wykonywana jest przez procesy Linuksa m.in. rozruch systemu, większość obsługi urządzeń, sieć, systemy plików itp. Niezbędna jest zatem komunikacja. Jednak jako że wątek jądra Linuksa jest wywłaszczalny, zadania krytyczne nie mogą wywoływać jego funkcji systemowych.
Kolejki czasu rzeczywistego RT-FIFO dostępne są w postaci ładowalnego modułu jądra. Przypominają znane z Linuksa łącza nazwane. Są zaalokowane w przestrzeni jądra RTCore. Zadania krytyczne mogą je tworzyć, niszczyć, czytać i pisać. Operacje odczytu i zapisu są po stronie zadań krytycznych niepodzielne i nieblokujące, co rozwiązuje problem odwróconych priorytetów. Procesy Linuksa widzą te kolejki jako zwykłe urządzenia znakowe (/dev/rtf0 itd.), do których mają dostęp poprzez standardowe funkcje POSIXowe.
Niesekwencyjna wymiana danych między procesami Linuksa a zadaniami krytycznymi może przebiegać za pomocą pamięci dzielonej. Różne implementacje RTLinuksa używają różnych funkcji dostępowych do pamięci dzielonej (mmap() w wersji niekomercyjnej i shm_open(), shm_unlink() w wersji komercyjnej), ale zawsze są one zgodne ze standardem POSIX.
Przy współdzieleniu zasobów istnieje potrzeba synchronizacji i wzajemnego wykluczania. RTLinux dostarcza w tym celu rodzinę funkcji pthread_mutex. Przy synchronizacji i wzajemnym wykluczaniu w systemach czasu rzeczywistego pojawiają się jednak pewne problemy.
Odwrócenie priorytetów (priority inversion) to sytuacja, w której zadanie wysokopriorytetowe nie dostaje procesora, choć powinno. Scenariusz ten może przybierać różne oblicza. W najprostszym przypadku odwrócenie priorytetów spowodowane jest czekaniem przez wysokopriorytetowe zadanie, aż zadanie o niższym priorytecie zwolni zasób. Szczęśliwie opóźnienie stąd wynikające może być wyliczone wcześniej. Gorzej jest, gdy dochodzi do tego proces średniego priorytetu, który nie potrzebuje zasobu trzymanego przez proces niskopriorytetowy i go wywłaszcza. Zdarzyło się to podczas misji Pathfindera na Marsie w lipcu 1997. Opracowano dwa obejścia tego problemu. Dziedziczenie priorytetów (priority inheritance) polega na tym, że zadanie niskopriorytetowe otrzymuje priorytet zadania wysokopriorytetowego w momencie, gdy to drugie rozpoczęło czekanie na zasób będący w posiadaniu pierwszego zadania i utrzymuje ten priorytet do chwili, aż zwolni zasób. Uniemożliwia to wkradnięcie się zadań średniopriorytetowych. Może jednak prowadzić do zakleszczenia, gdy zadanie dziedziczące zażąda drugiego zasobu będącego akurat w posiadaniu czekającego zadania wysokopriorytetowego. Dlatego RTLinux obrał inne metody.
Pułap zasobu (inaczej semafora, który broni wyłącznego dostępu do tego zasobu) to maksimum po priorytetach zadań, które mogą objąć ten zasób plus jeden. Zgodnie z protokołem CSP zadanie obejmujące zasób przyjmuje priorytet równy pułapowi tego zasobu do momentu, aż zwolni zasób. Jest to rozwinięcie pomysłu dziedziczenia priorytetów eliminujące ryzyko zakleszczenia.
Według zasad szeregowania SRP zadanie nie może zacząć się wykonywać, dopóki jego priorytet nie jest najwyższy spośród zadań aktywnych lub jego poziom wywłaszczenia nie jest większy od pułapu systemowego. Poziom wywłaszczenia zadania Ti definiuje się jako Pi = 1/Di, gdzie Di jest terminem zakończenia zadania. Używając SRP zadanie, które zaczyna się wykonywać, nie zostanie zablokowane do momentu zakończenia. Może być co najwyżej wywłaszczone przez zadania o wyższych priorytetach. Nazwa SRP pochodzi stąd, że wszystkie zadania mogą używać jednego stosu do zapamiętywania parametrów wywoływanych funkcji i adresów powrotu.
RTLinux powstał jako projekt akademicki w Institute for Mining and Technology of New Mexico. Jego głównym autorem jest profesor tej uczelni Victor Yodaiken uśmiechający się do nas: Jest on także założycielem FSMLabs, które rozwijają komercyjną odmianę RTLinuxPro.
Do zainstalowania RTLinuksa potrzebne są źródła jądra RTCore, odpowiadająca im łata na jądro Linuksa i owo jądro. Najnowsze źródła jądra RTCore oraz różne łaty autorstwa wielu osób zebrane razem w pakiet kontrybucji rtlinux_contrib można pozyskać ze strony FSMLabs po wypełnieniu krótkiego formularza. Można tam też znaleźć informacje o tym, jak otrzymać wersję demonstracyjną komercyjnego RTLinkusa. Godnym polecenia serwerem, z którego można ściągnąć jedną wybraną łatę zamiast całego pakietu, a także starsze wersje jądra RTCore jest OpenTech.
Najnowsza w chwili pisania tego dokumentu wersja RTCore wypuszczona w kwietniu 2003 to 3.2-pre2.
Źródła Linuksa oraz jądra RTCore należy rozpakować, a następnie nałożyć na Linuksa łatę znajdującą się wśród źródeł RTCore w katalogu patches. W tym celu w katalogu ze źródłami Linuksa należy wykonać komendę
patch -p1 <katalog_rtlinuksa/patches/kernel_patch-2.4.19-rtl3.2-pre2Należy oczywiście podać ścieżkę dostępu do łaty odpowiadającej wersji Linuksa i RTCore. Następnie jądro Linuksa należy skonfigurować i skompilować w standardowy sposób.
RTCore należy skonfigurować i skompilować wydając w katalogu z jego źródłami polecenia:
make RTLINUX=katalog_ze_źródłami_linuksa menuconfig make RTLINUX=katalog_ze_źródłami_linuksaJeśli parametr RTLINUX nie zostanie podany, plik Makefile spróbuje sam znaleźć źródła Linuksa. Następnie warto stworzyć pliki urządzeń:
make RTLINUX=katalog_ze_źródłami_linuksa devicesW tym momencie nowo stworzone jądro jest gotowe do pracy. Moduły RTLinuksa można załadować dostarczonym z nim skryptem
scripts/insrtl