Politechnika Częstochowska
Wydział Inżynierii Mechanicznej i Informatyki
Katedra Inżynierii Komputerowej
PRACA MAGISTERSKA
Systemy Czasu Rzeczywistego
Marcin Rojek
nr albumu: 29999
kierunek: Informatyka
specjalność: Inżynieria Oprogramowania i Systemy Informatyczne
promotor: dr inż. Jarosław Bilski
Częstochowa 2004
Autor pracy serdecznie dziękuje dr inż. Jarosławowi Bilskiemu, za pomoc przy jej tworzeniu, wszelkie cenne uwagi oraz cierpliwość i poświęcony czas.
Spis treści
Cel i zakres pracy
Praca ta ma na celu odsłonić przed Czytelnikiem wszelkie tajemnice związane z systemami czasu rzeczywistego, a zwłaszcza systemami operacyjnymi potrafiącymi przetwarzać w czasie rzeczywistym. Z całą pewnością dostarczy ona solidnej wiedzy, szczegółowo omówionych i praktycznych przykładów, metod wykorzystania dostępnych narzędzi oraz gotowych rozwiązań niezbędnych przy tworzeniu nowych projektów dla tego typu środowisk. Kolejne rozdziały pozwolą poznać całkowicie odrębny świat systemów wbudowanych. Zaprezentowane zostaną prawa nim rządzące oraz wynikające z nich bezpośrednie konsekwencje. Dzięki tej wiedzy każdy może przejść od tworzenia dotąd prostych aplikacji dla tradycyjnych systemów do zaawansowanych projektów, dla których bardzo ważny jest czas obliczeń oraz ich wiarygodność. Praca stanowi swego rodzaju przewodnik pozwalający uniknąć mogących pojawić się problemów (np.: zakleszczenie wątków), zminimalizować skutki niekorzystnych sytuacji (np.: inwersji priorytetów) oraz lepiej zrozumieć procesy zachodzące w najniższych warstwach systemu (np.: sposób podziału czasu procesora lub zarządzanie pamięcią).
Dodatkowo zostaną przedstawione najbardziej popularne systemy jak: Windows Embedded, RT Linux czy QNX. Autor szczególną uwagę stara się skupić na różnice w ich budowie. Gruntownej analizie zostanie poddana ich architektura. Zaprezentowane będą implementowane w nich mechanizmy, sposoby radzenia sobie z różnymi niekorzystnymi sytuacjami oraz dostępne środowiska programistyczne. Ponadto Czytelnik pozna przykłady ich zastosowań oraz sposoby ich rozpowszechniania. Podjęta również zostanie próba odpowiedzi na bardzo istotne pytania jak np.: czy jest jeden najlepszy system operacyjny czasu rzeczywistego?
W ramach pracy zostanie także wykonana prezentacja w postaci strony internetowej. Dzięki niej możliwe będzie szybkie odnalezienie kompletnych informacji na temat interesujących Czytelnika zagadnień jak np.: opisu konkretnego algorytmu lub rozwiązanie dla zaistniałego problemu. Poza tym, pozwoli ona w sposób przejrzysty i spójny zgłębić wiedzę oraz poszerzyć swoje spojrzenie na współczesną informatykę.
Z punktu widzenia celów pracy, ważne są również przyczyny jej powstania. Od dłuższego czasu zainteresowania autora skupiały się na urządzeniach przenośnych oraz wszelkich nowych i bardzo ciekawych technologiach kreujących styl życia człowieka przyszłości. Zaliczyć do nich można zarówno te powszechnie znane, usprawniające komunikację czy transport, ale również te najbardziej śmiałe projekty, do których zaliczają się np.: inteligentne domy czy rzeczywistość wirtualna. Wszystkie one wręcz nierozerwalnie są związane z przetwarzaniem w czasie rzeczywistym. Już teraz można pokusić się o wniosek, że systemy wbudowane w niedługim czasie staną się tymi najczęściej używanymi. To właśnie ten fakt skłonił autora do bardziej gruntownego zgłębienia prezentowanej tematyki. Kolejną sprawą jest to, że podczas poszukiwania materiałów, autor nie natrafił na jakikolwiek serwis internetowy, który w sposób kompleksowy oraz w oczekiwanej przez niego formie prezentowałby świat systemów czasu rzeczywistego (oczywiście sytuacja taka dotyczy chwili powstawania pracy, rok 2004). W różnych częściach Internetu znajdują się pewne szczątkowe informacje, jakieś dane statystyczne oraz tematy różnych publikacji. Nie ma natomiast miejsca, w którym dociekliwy Czytelnik mógłby bliżej zapoznać się z omawianymi zagadnieniami.
Wprowadzenie
Systemy czasu rzeczywistego to dość specyficzna, jednakże bardzo ważna dziedzina współczesnej informatyki. Często przeciętny użytkownik praktycznie nic o nich nie wie. Przyczyną takiego stanu rzeczy jest fakt, że oprogramowanie czasu rzeczywistego zazwyczaj implementuje się w niewidocznych, najniższych warstwach systemów informatycznych. Wiedza na ich temat jest bardzo często ograniczona.
Przetwarzanie w czasie rzeczywistym jest wszechobecne i odgrywa coraz istotniejszą rolę w życiu codziennym. Systemy wykorzystujące jego możliwości spotyka się obecnie prawie na każdym kroku, często nie zdając sobie z tego sprawy. Są one towarzyszami w czasie zabawy, dbają o bezpieczeństwo, pomagają lepiej poznać świat oraz dostarczają coraz to nowszych usług. Sprawiają, że życie zarówno poszczególnych jednostek, jak i całych grup społecznych staje się wygodniejsze oraz bardziej beztroskie. Nie ma wątpliwości, że to właśnie dzięki rozwojowi tej dziedziny informatyki, życie codzienne nabiera barw oraz bardzo często wkracza w nowy, może i lepszy wymiar. Przykłady zastosowań systemów czasu rzeczywistego można mnożyć. Wraz z każdym wschodem Słońca osadzają się one w nowym miejscu a ich twórcy starają się przy ich pomocy pokazać często bardzo nowatorskie rozwiązania znanych problemów. Powszechnie używane środki transportu, komunikacji czy choćby te odpowiedzialne za dostarczanie energii elektrycznej przestałyby działać, gdyby systemy wbudowane w tak powszechnie znane obiekty jak samochody, telefony komórkowe, routery, elektrownie nagle zostały wyłączone.
Specyfika i złożoność samego przetwarzania w czasie rzeczywistym zmuszają twórców oprogramowania do ciągłego poszerzania swojej wiedzy o różnych, czasami bardzo rozległych technologiach. Zakres wymaganych przez nich umiejętności rozciąga się od tych najbardziej skomplikowanych i wymagających największych nakładów czasu, czyli związanych z warstwą sprzętową, kompilatorami oraz debuggerami. Kolejne etapy nauki zmuszają do poznania struktury systemów operacyjnych czasu rzeczywistego, ich możliwości i ograniczenia oraz charakterystyczne dla nich rozwiązania. Większość pracy zostanie poświęcona właśnie tym zagadnieniom. Na samym końcu, znajdują się wszelkie zagadnienia dotyczące projektowania wielowątkowego, nowych mechanizmów oraz bibliotek.
W kierunku rozwoju technologii związanych z przetwarzaniem w czasie rzeczywistym zostało poczynione wiele kroków. Przede wszystkim powstał nowy, lepszy i jednocześnie bardziej złożony sprzęt. Dzisiejsze procesory, przeznaczone dla tego typu zastosowań, znacznie się różnią od tych, znanych z komputerów klasy PC (ang. personal computer). Charakteryzują się one ogromną wydajnością oraz często integrują w sobie wiele innych urządzeń jak np.: obsługujących łącza USB czy pozwalające na bezpośrednie podłączenie monitora. Należy jednak pamiętać, że są one dedykowane zazwyczaj dość konkretnym urządzeniom. Z całą pewnością bardzo istotne, było także dokonanie jakiegoś ujednolicenia interfejsów programistycznych. Owocem takich starań jest np.: standard POSIX. Poza tym powstała cała masa systemów operacyjnych czasu rzeczywistego. Obecnie na rynku istnieje dość dużo firm, których produkty potrafią sprostać różnym ograniczeniom czasowym. Pomimo tego, że stawianym wymaganiom potrafią sprostać z różnymi rezultatami, wcale to nie upraszcza dokonania wyboru najlepszego. Systemy te są bardzo różne. Praktycznie wybór jest uwarunkowany potrzebami oraz oczekiwaniami mających z nich korzystać osób.
Świat systemów wbudowanych i czasu rzeczywistego
Historia systemów operacyjnych obfituje w ważne wydarzenia, które wyznaczają etapy rozwoju tej dziedziny informatyki. Z prostego monitora, nadzorującego wykonanie zadań obliczeniowych, system operacyjny przeistoczył się w zaawansowane, rozproszone środowisko programowe, nadzorujące pracę wielu komputerów. Systemy operacyjne dla komputerów osobistych i rozwój graficznych interfejsów użytkownika wpłynęły na popularyzację komputerów oraz ich ekspansję na wszystkie dziedziny życia. Systemy operacyjne czasu rzeczywistego stały się podstawą systemów sterowania i systemów wbudowanych. Prace nad rozproszonymi systemami operacyjnymi wskazały, w jaki sposób tworzyć modularne i skalowalne rozwiązania oraz systemy obliczeniowe, oparte na zwykłych komputerach PC. Rozwój Open Source przyczynił się do podniesienia poziomu wiedzy na temat systemów operacyjnych i spowodował, że przestała ona nosić znamiona wiedzy tajemnej.
Czym jest system czasu rzeczywistego
Czym jest więc przetwarzanie w czasie rzeczywistym i sam system czasu rzeczywistego? Zapewne jedną z najbardziej zrozumiałych definicji jest następujące stwierdzenie: „System czasu rzeczywistego to taki, w którym wynik przetwarzania nie zależy tylko i wyłącznie od jego logicznej poprawności, ale również od czasu, w jakim został osiągnięty. Jeśli nie są spełnione ograniczenia czasowe, mówi się, że nastąpił błąd systemu.” (patrz np. [1]). W literaturze można spotkać również inne definicje, które w mniej lub bardziej obrazowy sposób oddają istotę tego typu systemów. Niektóre z nich brzmią następująco:
Tryb przetwarzania w czasie rzeczywistym jest takim trybem, w którym programy przetwarzające dane napływające z zewnątrz są zawsze gotowe, a wynik ich działania jest dostępny nie później niż po zadanym czasie. Moment nadejścia kolejnych danych może być losowy (asynchroniczny) lub ściśle określony (synchroniczny) [2].
System czasu rzeczywistego jest systemem interaktywnym, który utrzymuje ciągły związek z asynchronicznym środowiskiem, np. środowiskiem, które zmienia się bez względu na system, w sposób niezależny [2].
Oprogramowanie czasu rzeczywistego odnosi się do systemu lub trybu działania, w którym przetwarzanie jest przeprowadzane na bieżąco, w czasie wystąpienia zewnętrznego zdarzenia, w celu użycia rezultatów przetwarzania do kontrolowania lub monitorowania zewnętrznego procesu [2].
System czasu rzeczywistego odpowiada w sposób przewidywalny (w określonym czasie) na bodźce zewnętrzne napływające w sposób nieprzewidywalny [2].
System mikrokomputerowy działa w czasie rzeczywistym, jeżeli wypracowane przez ten system decyzje są realizowane w tempie obsługiwanego procesu. Inaczej mówiąc, system działa w czasie rzeczywistym, jeżeli czas reakcji systemu jest niezauważalny przez proces (decyzja jest wypracowana we właściwym czasie) [3].
Powyższe definicje pozwalają wysnuć wniosek, że samo określenie „czasu rzeczywistego” jest pojęciem w pewnym sensie względnym. Niech będą dane dwa systemy działające w czasie rzeczywistym:
odtwarzacz DVD;
system naprowadzający obrony przeciwrakietowej;
W pierwszym przypadku, strumienie obrazu i dźwięku są dekodowane przez odtwarzacz jako wynik odpowiedzi na rozkazy wydane przez użytkownika. Jednakże rozkazy mogą zostać tak gwałtownie wydawane, że dekoder nie będzie w stanie ich natychmiast obsłużyć. Przekroczy swoje ograniczenia czasowe. Jako rezultat lub kara za takie postępowanie, pojawią się chwilowe, lecz niestety widoczne, zniekształcenia obrazu i/lub dźwięku. Odtwarzacz DVD oczywiście nie przestanie funkcjonować. Dalej będzie poprawnie spełniał swoje zadania.
W drugim przypadku, rzeczywistość nie wygląda już tak różowo. Tutaj bardzo istotny jest tzw. czas reakcji systemu, czyli przedział czasu potrzebny systemowi na wypracowanie decyzji (sygnału wyjściowego) w odpowiedzi na zewnętrzny bodziec (sygnał wejściowy). System naprowadzający dla obrony przeciwrakietowej, aby mógł poprawnie spełniać swoje zadania, musi bezwarunkowo spełnić bardzo rygorystyczne ograniczenia czasowe. Jeśli np. nowe współrzędne z jakiś powodów nie mogą zostać wyliczone odpowiednio szybko, system stara się jak najlepiej uśrednić swoje obliczenia i wyprowadzić ich wyniki na zewnątrz.
Czytelnik może dojść do wniosku, że zależnie od roli i przeznaczenia danej grupy aplikacji czasu rzeczywistego, ograniczenia czasowe są koniecznością, której niespełnienie prowadzi w najgorszym przypadku do nieodwracalnych i tragicznych skutków oraz tych, w których czas wykonania nie jest tak krytyczny i dopuszcza się pewne odstępstwa. Najczęściej systemy czasu rzeczywistego dzieli się na dwie grupy:
Rygorystyczne (twarde, ang. Hard real - time systems) - gwarantują terminowe wypełnianie krytycznych zadań. Osiągnięcie tego celu wymaga ograniczenia wszystkich opóźnień w systemie, poczynając od odzyskiwania przechowywanych danych, a kończąc na czasie zużywanym przez system na wypełnienie dowolnego zamówienia. Takie ograniczenia czasu wpływają na dobór środków, w które są wyposażane rygorystyczne systemy czasu rzeczywistego. Wszelkiego rodzaju pamięć pomocnicza jest na ogół bardzo mała albo nie występuje wcale. Wszystkie dane są przechowywane w pamięci o krótkim czasie dostępu lub w pamięci, z której można je tylko pobierać (ang. read-only memory- ROM). Pamięć ROM jest nieulotna, tzn. zachowuje zawartość również po wyłączeniu dopływu prądu elektrycznego; większość innych rodzajów pamięci jest nietrwała. Systemy te nie mają również większości cech nowoczesnych systemów operacyjnych, które oddalają użytkownika od sprzętu, zwiększając niepewność odnośnie do ilości czasu zużywanego przez operacje. Na przykład prawie nie spotyka się w systemach czasu rzeczywistego pamięci wirtualnej. Dlatego rygorystyczne systemy czasu rzeczywistego pozostają w konflikcie z działaniem systemów z podziałem czasu i nie wolno ich ze sobą mieszać. Przykładem może być system kontroli lotu [9].
Łagodne (miękkie, ang. Soft real - time systems) - są mniej wymagające. W nich krytyczne zadanie do obsługi w czasie rzeczywistym otrzymuje pierwszeństwo przed innymi zadaniami i zachowuje je aż do swojego zakończenia. Podobnie jak w rygorystycznym systemie czasu rzeczywistego opóźnienia muszą być ograniczone - zadanie czasu rzeczywistego nie może w nieskończoność czekać na usługi jądra. Łagodne traktowanie wymagań dotyczących czasu rzeczywistego umożliwia godzenie ich z systemami innych rodzajów. Jednak użyteczność łagodnych systemów czasu rzeczywistego jest bardziej ograniczona niż systemów rygorystycznych. Ponieważ nie zapewniają one nieprzekraczalnych terminów, zastosowanie ich w przemyśle i robotyce jest ryzykowne. Niemniej jednak istnieje kilka dziedzin, w których są one przydatne. Są to np. techniki multimedialne, kreowanie sztucznej rzeczywistości, zaawansowane projekty badawcze w rodzaju eksploracji podmorskich lub wypraw planetarnych. Znajdują one swoje miejsce wszędzie tam, gdzie istnieje potrzeba systemów o bardziej rozbudowanych możliwościach [9].
Oczywiście w literaturze można spotkać również pewną pośrednią grupę, tzw. firm real - time systems (patrz np. [4]). Tutaj nie ma żadnej korzyści jeśli nastąpi spóźnienie w dostarczaniu usług. Nie ma też żadnej groźby związanej z takim przypadkiem.
Bardzo prostym i jednocześnie oddającym istotę systemów czasu rzeczywistego, jest poniższy schemat:
Rysunek 1. Prosty schemat systemu czasu rzeczywistego.
W dalszej części pracy pojawia się określenie systemów wbudowanych (ang. Embedded). Najprościej można je zdefiniować jako te systemy lub urządzenia, które nie posiadają typowej klawiatury, myszki, czy wyświetlacza. Oczywiście taka definicja jest bardzo obrazowa i mówi, że są to systemy, które nie wyglądają jak typowy komputer, a raczej jak cyfrowa kamera czy sprytny toster. Fachowa literatura opisuje to zagadnienie w sposób bardziej oficjalny i spójny twierdząc, że: „Systemy wbudowane to te, w których istnieje bardzo silna więź między oprogramowaniem i sprzętem. Są projektowane do wykonywania ściśle określonych zadań. Słowo wbudowane oznacza, że są one częścią większych i bardziej złożonych systemów określanych wbudowującymi. Taki system nadrzędny może składać się z wielu prostszych systemów wbudowanych” (patrz np. [5]).
Jak bardzo bliskie są systemy wbudowane systemom czasu rzeczywistego przedstawiono na rys. 2. Zależność tą można interpretować następująco: nie wszystkie systemy wbudowanego zawierają cechy systemów czasu rzeczywistego, jak i również nie wszystkie systemy czasu rzeczywistego są wbudowane. Jednakże oba te typy systemów nie stanowią zbiorów rozłącznych. Ich część wspólna, to wbudowane systemy czasu rzeczywistego.
Rysunek 2. Systemy wbudowane i czasu rzeczywistego.
Dla uproszczenia, w dalszej części pracy pojęcia system wbudowany i system czasu rzeczywistego będą stosowane wymiennie i oba będą odnosić się do ogólnie rozumianych systemów czasu rzeczywistego. W innych przypadkach, zostanie wyraźnie zaznaczone, o jaki rodzaj systemu chodzi.
Rys historyczny
Jeszcze kilka dekad temu, nikomu nawet się nie śniło, jak bardzo drastycznie systemy wbudowane zrewolucjonizują styl życia, pracy a nawet zabawy współczesnych ludzi. W początkowych latach ery komputerów, projektanci tworzyli oprogramowanie, które zawierało niskopoziomowy kod maszynowy, niezbędny do inicjalizacji i interakcji ze sprzętem. Była to komunikacja bezpośrednia. Tak silna więź między oprogramowaniem a sprzętem owocowała w powstaniu nieprzenośnego kodu. Często znikoma zmiana w sprzęcie powodowała konieczność przepisywania, nierzadko całych aplikacji od początku. Oczywistą sprawą jest, że takie podejście dawało w wyniku system bardzo trudny i przede wszystkim drogi w utrzymaniu.
Wraz z rozwojem przemysłu softwerowego, systemy operacyjne, które dostarczały podstawowej bazy programowej, ewoluowały i umożliwiły stworzenie tzw. abstrakcyjnej warstwy sprzętowej. Dodatkowo, postęp sprawił, że ogromne, monolityczne aplikacje zaczęły przybierać formy modularne, działające na szczycie danej platformy.
Przez całe lata powstawało wiele wersji i typów systemów operacyjnych. Ich spektrum rozciąga się od systemów ogólnego przeznaczenia, jak np. UNIX i Microsoft Windows do prostszych, mniej złożonych systemów czasu rzeczywistego jak np. VxWorks.
Lata 60 i 70 to czasy, kiedy główny prym wiodły systemy komputerowe średnich i dużych rozmiarów (ang. Mainframe). Powstał system UNIX. Jego celem było umożliwienie dostępu do drogich systemów komputerowych wielu użytkownikom jednocześnie oraz współdzielenie zasobów. To przyczyniło się do jego sukcesu i wkrótce UNIX pojawił się na wielu maszynach, począwszy od mikrokomputerów, a skończywszy na superkomputerach.
W latach 80 Microsoft zaprezentował swoje „okienka”. Windows od samego początku były ukierunkowane na komputery osobiste. Niewątpliwie ich ogromną zaletą był graficzny interfejs użytkownika, co bardzo przyspieszyło pęd rozwoju całego przemysłu komputerowego.
Pod koniec lat 80, nastąpiła era tzw. post - PC, czyli systemów wbudowanych (ang. Embedded - computing era). Era ta, to efekt wcześniejszych prac oraz coraz to nowszych miejsc, w których komputery znajdowały swoje zastosowanie Właśnie na ten moment czekały wszelkie koncepcje związane z przetwarzaniem w czasie rzeczywistym. Systemy operacyjne czasu rzeczywistego (RTOS), tak jak i te ogólnego przeznaczenia, zawierają pewien poziom wielozadaniowości, moduły odpowiedzialne za zarządzanie oprogramowaniem i sprzętem, usługi oraz tworzą abstrakcyjną warstwę sprzętową. To, co wyróżnia RTOS, to oczywiście większa wiarygodność w kontekście aplikacji wbudowanych, skalowalność, zarówno w górę, jak i w dół, zredukowane zapotrzebowanie na pamięć, scheduler zaprojektowany specjalnie dla zadań czasu rzeczywistego oraz lepsza przenośność na inne platformy sprzętowe.
Właściwy rozwój systemów wbudowanych rozpoczął się w roku 1992, kiedy powstało Konsorcjum PC/104. Grupa ta stworzyła standard dla intelowskiego mikroprocesora. Zamieszczony na płycie głównej, mógł stać się podstawą budowy urządzenia o boku nie przekraczającym czterech cali oraz wysokości poniżej jednego cala. PC/104 początkowo skupiała się na zastosowaniach militarnych i medycznych. Kiedy moc procesorów wzrosła, prace badawcze rozszerzono o zakres zastosowań multimedialnych.
W 1993 roku opracowano rozszerzenie POSIX 1003.1b, dotyczące systemów czasu rzeczywistego. Jest ono dosyć powszechnie implementowane, najczęściej przez systemy operacyjne wywodzące się z rodziny UNIX. Jeśli chodzi o inne (jak np. Windows), to bywa różnie. Standard 1003.1b wprowadza wiele ciekawych rzeczy, do których zaliczają się między innymi:
obsługa sygnałów czasu rzeczywistego
zegary (ang. timers)
szeregowanie w oparciu o priorytety
różne mechanizmy synchronizacyjne jak np. semafory, kolejki itp.
funkcje blokujące usuwanie stron z pamięci (mechanizm pamięci wirtualnej)
mechanizmy przekazywania komunikatów
asynchroniczne operacje wejścia - wyjścia oraz szeregowaniu ich w oparciu o priorytety
Dalsze rozszerzenia standardu POSIX, wprowadzają dodatkowo funkcje wspierające wielowątkowość w ramach jednego procesu, kontrolujące wątki i ich atrybuty, muteksy, zmienne warunkowe, funkcje umożliwiające monitorowanie czasu wykonywania procesów i wątków oraz określające limity czasów oczekiwania dla funkcji blokujących. Określają również normy dotyczące rozproszonej komunikacji czasu rzeczywistego, obsługi urządzeń z buforami, wysyłania bloków kontrolnych oraz co bardzo ważne, tzw. usługi dla niezawodności, dostępności oraz użyteczności.
Oczywiście POSIX daje pewną dowolność twórcom systemów czasu rzeczywistego. Nie muszą oni implementować całości, a tylko wybrane przez siebie funkcje. Wymagane jest jednak jasne określenie co jest, a czego nie ma w systemie. W praktyce, zgodność systemu ze standardem 1003.1b gwarantuje poprawne wykonywanie zadań czasu rzeczywistego o charakterze miękkim. Więcej o standardzie POSIX można znaleźć np. w [11].
Według analityków, systemy wbudowane, które na początku lat 90 stanowiły jedynie 5% rynku systemów komputerowych, najprawdopodobniej około roku 2005 przejmą już ponad 50% tego rynku.
Systemy czasu rzeczywistego w otaczającym świecie
Jak już wcześniej wspomniano, systemy czasu rzeczywistego są obecne niemalże wszędzie. Ich możliwości oraz zastosowania są ograniczone tylko i wyłącznie zdolnościami ludzkiego umysłu. Często są niezauważone oraz kiedy działają poprawnie, zapomina się o ich istnieniu. Są używane coraz liczniej w różnych gałęziach przemysłu.
Typowymi przykładami zastosowań, są systemy sterowania procesów przemysłowych. Zadaniem ich, jest przede wszystkim nadzorowanie linii technologicznych, obrabiarek, robotów, układów napędowych itp. Znajdują również zastosowanie w urządzeniach telekomunikacyjnych i sieciowych. Niejednokrotnie mają wpływ na ludzkie życie jak np. systemy sterowania reaktorami jądrowymi, czy wspomniany już wcześniej system ochrony przeciwrakietowej. W życiu codziennym, systemy czasu rzeczywistego są stosowane w nowoczesnych aparatach cyfrowych, w samochodach, komputerach przenośnych, a także w systemach kina domowego i sekretarkach telefonicznych. Można je znaleźć we współczesnych pralkach i zmywarkach, gdzie po zebraniu informacji z licznych czujników (jak stopień zabrudzenia wody), odpowiednio optymalizują przebieg programu. Calem jest oczywiście uzyskanie jak najlepszych rezultatów. W zastosowaniach biurowych spotyka się je najczęściej, jako funkcjonalna część urządzeń sieciowych jak np. switche, routery, drukarki, modemy itp. Ich wyspecjalizowane formy znajduje się w medycynie, fizyce, astronomii itp. Przykładowo wyprodukowany przez NASA Path Finder, czy system naprowadzający dla pocisków (Lockheed Martin), składają się z całych grup systemów czasu rzeczywistego.
Jak widać, systemy wbudowane coraz intensywniej „bombardują” cały współczesny świat. Zajmują coraz to nowsze obszary zastosowań oraz zadomawiają się w nich na stałe. W niedalekiej przyszłości, z pewnością wyprą tradycyjne rozwiązania. Człowiek przyszłości nie będzie się borykać z takimi problemami, jak robienie codziennie tych samych zakupów, zniknie wiele ograniczeń oraz pojawią się nowe perspektywy.
Zanim jednak to nastąpi, twórców oprogramowania czeka ogromna praca. Należy pamiętać, że programowanie wielozadaniowe nie jest tak proste, jak znane wszystkim podejście tradycyjne. Dodatkowo z góry narzucone ograniczenia zmuszają do ciągłych kompromisów oraz tworzenia nowych rozwiązań.
Kolejne rozdziały pracy przybliżą wewnętrzną strukturę środowisk wbudowanych. Czytelnik niemalże z precyzją chirurgiczną będzie miał możliwość przestudiowania mechanizmów, z których składa się większość systemów czasu rzeczywistego, podane zostaną przyczyny wielu mogących się pojawić problemów oraz metody ich rozwiązania.
Inicjalizacja systemu wbudowanego
Nim zostaną przedstawione wewnętrzna budowa oraz mechanizmy rządzące światem systemów wbudowanych, bardzo istotnym faktem jest dobre zrozumienie fazy ich startu.
Inicjalizacja systemu wbudowanego z pewnością stanowi zagadkę dla nowicjuszy w tym temacie. Nie jednego przeraża konieczność studiowania długich instrukcji, aby poznać docelową platformę. Analizowanie tabel pamięci, rejestrów i diagramów przedstawiających wewnętrzną strukturę sprzętową to niestety obowiązki. Wszystko to ma na celu uzyskać odpowiedź na pytania dotyczące m. in. sposobów ładowania obrazu systemu oraz obszarów pamięci, w których jego poszczególne części powinny się znaleźć. Poza tym, warto poznać metody inicjalizacji aplikacji oraz w jaki sposób generują one sygnały wyjściowe. Wszelkie te kwestie zostaną szczegółowo omówione w kolejnych podrozdziałach.
Narzędzia potrzebne do transferu obrazu systemu
Obraz jądra systemu przesyła się z komputera nadrzędnego, zwanego hostem (ang. host). Cały proces nazywa się ładowaniem obrazu oraz można go wykonać na wiele sposobów. Ich wybór jest uwarunkowany możliwościami konkretnej platformy. Wielu producentów udostępnia przynajmniej kilka interfejsów łączących ich platformę ze światem zewnętrznym. Najczęściej spotykane metody umieszczania obrazu systemu w urządzeniu docelowym opierają się albo o bezpośrednie zaprogramowanie zawartości pamięci EEPROM lub Flash , bądź (co jest o wiele wygodniejsze) wgranie całego obrazu przy pomocy łącza szeregowego lub jakiegoś połączenia sieciowego. Ta druga metoda wymaga, aby komputer hosta, z którego dokonuje się transferu, zawierał specjalne oprogramowanie. Urządzenie docelowe musi zawierać program ładujący, wbudowany monitor oraz dodatkowo może być wzbogacone o agenta dla debuggera.
Obraz jest zazwyczaj przechowywany w pamięci ROM lub flash. Jeśli obie te pamięci są obecne w systemie, to wówczas odpowiednie ustawienie zworek określa, z której procesor ma pobierać swoje pierwsze instrukcje po inicjalizacji. Jeśli np. ustawienia wskazują pamięć ROM, to wówczas procesor zacznie ładować instrukcje z obszarów pamięci mapowanych na startowy adres pamięci ROM.
Program ładujący
Program ładujący (ang. loader), znajduje się na docelowym urządzeniu i służy do pobrania z komputera hosta obrazu systemu. Zazwyczaj, ze względu na swoje małe rozmiary, znajduje się w pamięci ROM i działa w kooperacji z oprogramowaniem hosta. Po uruchomieniu urządzenia, przejmuje nad nim kontrolę tzw. boot image (również obecny w ROM) oraz inicjalizuje sprzęt. Kiedy pamięć oraz podstawowe urządzenia są już zainicjalizowane, zaczyna wykonywać się loader, czyli właściwy program ładujący.
Aby pobranie obrazu było możliwe, trzeba ustalić pewne protokoły transmisji, jak i również parametry komunikacji. W najprostszych przypadkach używa się łącza szeregowego. Bardziej wyrafinowane programy ładujące potrafią korzystać z połączeń sieciowych oraz dokonywać operacji na pamięci flash.
Loader pobiera obraz bezpośrednio do pamięci RAM. Musi rozumieć format przesyłanego pliku, ponieważ zawarte są w nim instrukcje określające pod jakimi adresami oraz w jakich częściach pamięci należy umieścić jego określone sekcje.
Po zakończeniu transmisji, program ładujący przekazuje kontrolę do świeżo zainstalowanego obrazu.
Monitor
Zamiast używać duetu boot image i program ładujący, maszyna docelowa może zawierać tzw. monitor. Jest to o wiele bardziej złożone oprogramowanie, które nie tylko inicjalizuje podstawowy sprzęt zaraz po uruchomieniu urządzenia, ale również urządzenia peryferyjne (np. czasomierze, interfejs szeregowy) i kontroler przerwań oraz instaluje ich podstawowe procedury obsługi.
Monitor posiada dodatkowo prosty interfejs użytkownika, który jest dostępny dla terminali podłączanych do łącza szeregowego. Dostarcza on szereg komend umożliwiających m. in. rozpoczęcie pobierania jądra systemu, zapis lub odczyt z konkretnych miejsc pamięci, rejestrów, resetowanie systemu oraz wykonanie instrukcji debugujących.
Istnienie takiego mechanizmu jest bardzo istotne dla większości systemów oraz niejednokrotnie może mieć kluczowe znaczenie przy zachowywaniu ich przydatności, nawet po wystąpieniu nieprzewidzianych problemów. Jak uczy historia, przy większości projektów, wręcz niemożliwe jest uniknięcie wszystkich błędów oraz przeanalizowanie wszelkich przyszłych okoliczności, w jakich dane urządzenie będzie uruchamiane. Systemy wbudowane, są jednak tak projektowane, aby maksymalnie uprościć ich ewentualne przeprogramowanie, umożliwiając jednocześnie zarządzanie całym takim procesem zdalnie. Taka sytuacja miała miejsce, kiedy po wysłaniu w przestrzeń kosmiczną pewnej sondy odkryto, że w jej oprogramowaniu tkwi poważny błąd, uniemożliwiający jej poprawne funkcjonowanie. Wówczas jedynym rozwiązaniem okazało się ponowne przesłanie instrukcji sterujących oraz powtórna inicjalizacja systemu. Z możliwości takiej bardzo często również korzystają producenci tunerów dla cyfrowej telewizji satelitarnej. Zdalnie instalują oni nowe wersje oprogramowania.
Scenariusz inicjalizacji
Poniższe rozumowanie ma na celu przedstawić inicjalizację systemu wbudowanego, widzianą z lotu ptaka. Szerzej na ten temat można przeczytać w literaturze, czego bardzo dobrym przykładem jest [5].
Proces ten, jest nie tyle istotny i potrzebny do zrozumienia wszelkich skomplikowanych zagadnień związanych z przetwarzaniem w czasie rzeczywistym, co rzuca światło na pewne realia oraz ukazuje świat, w którym takie przetwarzanie najczęściej jest spotykane.
Po uruchomieniu urządzenia, jego procesor zaczyna pobierać kolejne instrukcje z określonego na sztywno systemowego obszaru pamięci. Znajduje się tam tzw. wektor resetujący, który ze względu na swoje ograniczone rozmiary, zawiera tylko instrukcję skoku do miejsca zawierającego właściwy kod inicjalizujący. Jeśli urządzenie jest zaopatrzone w pamięć flash, w której został umieszczony program ładujący, natomiast wektor resetujący znajduje się w pamięci ROM mapowanej na adres 0x00h oraz zawiera skok do adresu 0x00040h, to przebieg inicjalizacji takiego systemu może być podobny do tego, przedstawionego na rys. 3.
Rysunek 3. Inicjalizacja systemu wbudowanego.
Zaraz po uruchomieniu urządzenia, rejestry jego procesora przyjmują wartości domyślne. Następnie, program ładujący wyłącza procedury obsługi przerwań, inicjalizuje pamięć RAM, pamięci podręczne procesora oraz dokonuje bardzo podstawowego sprawdzenia sprzętu. Przyczyną wyłączenia obsługi przerwań jest brak gotowości systemu do ich obsługi. Jak wiadomo, pamięć RAM jest o wiele szybsza niż ROM, czy flash oraz dodatkowo umożliwia prostsze wykonanie operacji zapisu, czy procedur związanych z debugowaniem. Dlatego też, jeśli tylko pozwalają na to jej rozmiary, program ładujący kopiuje całość lub tylko część obrazu systemu do pamięci RAM. Ewentualnie, może być konieczne wcześniejsze jego rozpakowanie. Niekiedy pamięć ta jest tak mała, że jądro systemu musi pozostać w pamięci stałej, natomiast RAM zawiera tylko niezbędne minimum, potrzebne do jego poprawnego funkcjonowania.
Źródła systemu mogą być pobrane zarówno z pamięci stałej, jak i odległego komputera, do którego istnieje tylko połączenie sieciowe. Później, następuje uruchomienie wymaganego sprzętu, jak np. urządzenia wejścia - wyjścia oraz w fazie końcowej, sterowanie przejmuje właściwy system operacyjny.
Inicjalizacja oprogramowania
Obraz, który jest ładowany do maszyny docelowej, to zbiór wielu pakietów oprogramowania. Są to między innymi moduły odpowiedzialne za obsługę płyty, liczne sterowniki, system czasu rzeczywistego, który dostarcza podstawowe usługi i umożliwia operacje wejścia - wyjścia oraz szereg innych, wzbogacających możliwości całej maszyny. Schematyczny obraz takiej struktury przedstawia rys. 4.
Rysunek 4. Budowa obrazu systemu.
Wszystkie te komponenty, są odpowiedzialne za pełną inicjalizację urządzenia. Składają się na nią przede wszystkim pełna inicjalizacja sprzętu, instalacja procedur obsługi przerwań i wyjątków, inicjalizacja samego systemu czasu rzeczywistego oraz poszczególnych jego aplikacji. Jak widać na rys. 4, najbliżej sprzętu jest zbiór sterowników dla płyty głównej (BSP). Są one zazwyczaj pisane w języku niskiego poziomu oraz związane są z konkretną architekturą. Pozostałe widoczne moduły, mogą być już nieco bardziej elastyczne oraz implementowane w językach jak np. C++.
Uruchomienie samego systemu czasu rzeczywistego, oznacza inicjalizację różnych jego obiektów i usług, jak np. zadania, semafory, kolejki; stworzenie stosu pamięci oraz wszelkich innych, dodatkowych modułów, jak np. obsługa komunikacji opartej na TCP/IP. Wszystkie te elementy zostaną bardziej szczegółowo omówione w dalszej części pracy.
Oczywiście tak szybko, jak zostanie uruchomiony moduł szeregujący procesy, użytkownik może uruchamiać wszelkie inne obecne w pamięci aplikacje. Wówczas system ma możliwość podziału dostępu do procesora oraz przekazywanie im sterowania.
Na uwagę zasługują widoczne na rys. 4 złącza BDM/JTAG, które stanowią interfejs umożliwiający debugowanie oraz przejęcie kontroli nad mikroprocesorem i zasobami bez pomocy specjalnego oprogramowania. Dzięki niemu, można odczytać stany rejestrów procesora, zmodyfikować zawartość pamięci czy wymusić na procesorze wykonanie jakiejś instrukcji, po czym zablokować go (ang. halt) lub odczytać, jaka instrukcja jest właśnie wykonywana. Dają one również możliwość wgrania całego obrazu systemu do urządzenia.
Budowa systemu czasu rzeczywistego
Czym jest owy system czasu rzeczywistego, zostało już bardzo dokładnie wyjaśnione. Wcześniejsze rozdziały, nie tylko przytaczały ich liczne definicje oraz obszary zastosowań, ale również zarysowały pewne ramy, tego bardzo specyficznego i jednocześnie nowego świata. Można je potraktować jako rodzaj przedłużanego wstępu, mającego na celu zapoznanie Czytelnika z klimatem oraz atmosferą, w jakich lubują się systemy wbudowane.
Dalsza część pracy, będzie zorientowana głównie na poszczególne elementy takich systemów. Ich działanie, przeznaczenie oraz wzajemne powiązania zostaną omówione z precyzją niemalże chirurgiczną. Dodatkowo uwaga czytelnika zostanie zwrócona na mogące się pojawić problemy, sposoby ich unikania oraz eliminowania lub minimalizowania skutków.
Na samym wstępie poznawania wewnętrznej budowy, należy zrozumieć, co odróżnia systemy czasu rzeczywistego od „zwykłych” systemów operacyjnych. Otóż podstawową sprawą jest sposób szeregowania zadań. Te najpowszechniejsze systemy jak np. Linux, Windows, stosują bardzo sprawiedliwą politykę podziału zasobów. Umożliwiają każdemu zadaniu wykonanie swojej pracy. Procesy czasu rzeczywistego nie są specjalnie uprzywilejowane, ani też nie są zachowywane ich priorytety. Zazwyczaj informacja o priorytetach jest gubiona, kiedy usługi systemowe są wykonywane w kontekście wątków użytkownika. Priorytety te, mogą być np. zmniejszane wraz z każdym impulsem zegarowym. Doprowadza to, do powstawania wszelkiego rodzaju opóźnień. Dodatkowo, ciężko tutaj przewidywać dokładnie kolejność wykonania czy zakończenia poszczególnych zadań. Kolejną sprawą jest, że zwykłe systemy operacyjne poniekąd faworyzują tzw. użytkowników terminalowych i programy interaktywne, jak np. edytory tekstu. Jeśli dany proces zostaje wstrzymany na operacji wejścia wyjścia, to często jego priorytet jest zwiększany. Poza tym, program szeregujący zadania bardzo często oddaje czas procesora zadaniom o niskim priorytecie, co sprawia, że wykonanie dowolnego programu jest uzależnione od obciążenia całego systemu oraz zachowania innych procesów. Bardzo często również stosowane są wszelkiego rodzaju optymalizacje podnoszące ogólną wydajność jak np. mające na celu zwiększenie transferu odczytu danych z dysku poprzez wybranie tego zadania, którego żądania wymagają najmniejszego przesunięcia głowic. Istotnym jest również fakt, że procesy wykonujące się w trybie systemowym nie mogą zostać wywłaszczone.
Systemy czasu rzeczywistego, są pod tymi względami o wiele bardziej restrykcyjne. Dla nich można powiedzieć, że czas to pieniądz. Priorytety są przypisane do określonych zadań i w oparciu o nie przydzielane są zasoby. Szukają wszelkich rozwiązań, które eliminują możliwe opóźnienia oraz niedeterministyczny przebieg wykonania. Nie jest dopuszczalna sytuacja, aby krytyczne zadanie musiało czekać ponieważ jakiś proces właśnie kopiuje olbrzymie porcje danych lub uruchamiany jest serwer XWindow. Dodatkowo, w takich systemach, praktycznie rezygnuje się z pamięci wirtualnej. Pomimo wszelkich dobrodziejstw jakie ona oferuje, jak np. możliwość uruchomienia programów, których sumaryczna ilość wymaganej pamięci przekracza ilość dostępnej fizycznie w systemie, niesie ona ze sobą poważne niebezpieczeństwo. Jeśli dany proces kilkakrotnie nie trafi w odpowiednią jej stronę, zostanie wygenerowane opóźnienie, którego skutki mogą być katastrofalne.
Każdy system czasu rzeczywistego składa się z jądra (ang. kernel) oraz grupy innych komponentów zwiększających jego możliwości oraz udostępniających dodatkowe usługi. Najważniejszą częścią jest program szeregujący zadania oraz zarządzający zasobami, który dodatkowo musi być tak zaprojektowany, aby było możliwe spełnienie ograniczeń czasowych. Prosty schemat przedstawiający bardzo ogólnie budowę takiego systemu został przedstawiony na rys. 5.
Rysunek 5. Prosty schemat systemu czasu rzeczywistego.
Jak widać na powyższym rysunku, najniższe dwie warstwy to sprzęt oraz zbiór sterowników (BSP). Później jest jądro systemu oraz szereg modułów jak np. system plików, podsystem wejścia - wyjścia, dodatkowe sterowniki, protokoły sieciowe oraz komponenty wspierające tworzenie aplikacji. Na samym szczycie powyższej hierarchii, uruchamiane są aplikacje użytkownika.
Jądro systemu
Każdy, dobrze zaprojektowany system czasu rzeczywistego, powinien być skalowalny, zarówno w górę, jak i w dół. Oznacza to, że dodawanie czy usuwanie dodatkowych komponentów dostosowujących jego funkcjonalność do wymagań powinno być proste oraz nie mieć wpływu na poprawne działanie całości. Takie podejście ma ogromne znaczenie ekonomiczne. Sprawia, że cały produkt jest tani oraz umożliwia implementowanie tego samego systemu w różnego rodzaju urządzeniach. Czy to będzie prosty telefon komórkowy, czy rozbudowany system sterowania lotem samolotów, dalej użytkownik korzysta z tego samego wyrobu. W miarę potrzeb, może zwiększać jego możliwości poprzez instalowanie kolejnych modułów. Z tego też względu, systemy tego typu, stosują tzw. architekturę mikrojądra (ang. microkernel).
Mikrojądro, to komponent dostarczający minimalną liczbę usług. jak np. tworzenie lub usuwanie zadania, alokowanie i dealokowanie potrzebnej pamięci, dostarczenie kolejek komunikatów itp. Klient ma dostęp do tych obiektów poprzez różne interfejsy, jak np. API. Najczęściej jest implementowane w języku niskiego poziomu (np. assembler). co sprawia, że jego przenośność jest ograniczona tylko i wyłącznie do konkretnego typu procesora. Nie jest to jednak ogromny problem, gdyż cała reszta systemu jest implementowana w językach jak np. C++ i przeniesienie całości z jednej platformy na inną, wiąże się tylko i wyłącznie z przepisaniem tych najbardziej podstawowych usług. Niskopoziomowy kod, to konieczność, która pomimo pozornego trudu, daje ogromny zysk w postaci zwiększonej wydajności oraz szybszego działania. Cały proces powstawania systemu oraz bardziej szczegółowe sposoby jego implementacji zainteresowany czytelnik znajdzie np. w [6]. Dalsza część pracy przedstawi najbardziej istotne elementy mikrojądra, jakimi niewątpliwie są program szeregujący oraz liczne obiekty i usługi.
Szeregowanie zadań
W systemach czasu rzeczywistego spotyka się najczęściej dwa sposoby szeregowania zadań. Jeśli właściwości kontrolowanego systemu mogą zostać określone z góry, to rodzaj i kolejność podejmowanych działań są określone przed rozpoczęciem wykonywania procesu. Wówczas stosuje się tzw. szeregowanie statyczne. Polityka taka, najczęściej znajduje swoje zastosowanie dla zadań występujących okresowo (np. analiza danych z czujnika). Jest to oczywiście podejście całkowicie nieelastyczne i sprawia, że system zostaje ściśle związany z konkretnym procesem. Jedyną zaletą, jest prostota implementacji, a co za tym idzie, mniejsze koszta. Niestety, kontrolowane procesy okazują się o wiele bardziej złożone, a środowiska w jakich najczęściej się wykonują, charakteryzuje niepewność. Istnienie elementów stochastycznych sprawia, że powstają także systemy z szeregowaniem dynamicznym. Dzięki takiemu podejściu, uzyskuje się o wiele bardziej uniwersalne systemy.
Najważniejszą częścią jądra każdego systemu czasu rzeczywistego, jest tzw. program szeregujący (ang. scheduler). Znajduje się on w samym jego sercu oraz dostarcza algorytmów określających, jakie zadanie czy proces ma przejąć procesor oraz kiedy ma to nastąpić. Pozwala to na stworzenie wielozadaniowości (ang. multitasking). Niezależnie od ilości procesorów w urządzeniu, użytkownik ma wrażenie, że system wykonuje wiele rzeczy jednocześnie. Analizując jednak dogłębniej takie zachowanie, można dojść do wniosku, że owa „jednoczesność” ma dwa oblicza. Otóż, jeśli liczba procesorów dostępnych dla konkretnego rozwiązania jest przynajmniej równa ilości obecnych w systemie zadań, to wówczas rzeczywiście, wiele procesów wykonuje się jednocześnie w dosłownym znaczeniu tego słowa. Każdy ma przydzielony odpowiedni procesor i zasoby oraz „okupuje” je do chwili, aż zakończy swoje działanie (zadania te muszą być także niezależne od siebie). Jeśli natomiast liczba obecnych zadań przewyższa liczbę dostępnych procesorów, to wtedy mówi się o tzw. pozornej wielozadaniowości. Z punktu widzenia algorytmów szeregujących, właśnie taka sytuacja jest zdecydowanie najciekawsza. W zależności od charakteru i przeznaczenia systemu, stosują one różne techniki aby jak najlepiej wykorzystać dostępną moc obliczeniową oraz jak najlepiej stworzyć pozory tej prawdziwej „jednoczesności”.
Dla każdego uruchomionego w systemie zadania, jądro przydziela tzw. blok kontrolny zadania (ang. Task Control Block, TCB), w którym pamiętane są jego charakterystyczne cechy jak np. kontekst (stan rejestrów procesora). Struktura ta umożliwia przerywanie wykonywania zadań, przekazywania sterowania innym oraz ich przywracanie.
Jeśli program szeregujący podejmie decyzję, że trzeba zatrzymać wykonywania zadania np. o numerze 1 oraz wznowić wykonywanie zadania numer 2, to wówczas mają miejsce następujące czynności:
stan zadania nr 1 jest „zamrażany” w jego bloku kontrolnym i zapisywany w celu późniejszego odtworzenia
ładowany jest blok kontrolny zadania nr 2 oraz staje się ono aktualnie wykonywanym
Cały ten proces najlepiej obrazuje poniższy rysunek (rys. 6):
Rysunek 6. Przełączanie kontekstu.
Jak widać, istnieje pewien okres czasu, potrzebny dla przełączenia kontekstu. Jest on znikomy w porównaniu do tego potrzebnego dla wykonania obliczeń. Jednakże nie można o nim zapominać, gdyż zbyt częste przekazywanie procesora pomiędzy zadaniami może prowadzić do spadku wydajności.
Jak wspomniano wcześniej, nadrzędnym zadaniem programu szeregującego jest określenie, które zadania należy wstrzymać, a które wznowić. Warto w tym momencie wspomnieć o dwóch, najczęściej stosowanych w tym celu algorytmach. W obu przypadkach, stosowana jest polityka z wywłaszczeniem. Oznacza to, że ważniejsze zadanie w danej chwili może przerwać wykonywanie mniej ważnego i przejąć procesor. Po jego zakończeniu lub gdy zaistnieją pewne okoliczności, wywłaszczone zadanie może kontynuować swoje obliczenia.
Pierwszy z tych algorytmów, opiera się tylko o zestaw priorytetów, określających poziom ważności. Ich interpretacja oraz zakres zależą od konkretnej implementacji. Ogólna zasada polega na tym, że jeśli w danej chwili jest jednocześnie kilka zadań gotowych do uruchomienia, to wybierane jest to o najwyższym priorytecie. Jeśli natomiast, w czasie obliczeń, pojawi się zadanie o wyższym priorytecie od aktualnie wykonywanego, to wówczas scheduler przerywa je, zapisuje jego stan oraz oddaje procesor zadaniu ważniejszemu.
Całość najlepiej obrazuje rys. 7:
Rysunek 7. Szeregowanie zadań z wywłaszczeniem w oparciu o priorytety.
Jak widać, wykonywanie zadania nr 1, a później także zadania nr 2 jest przerywane. Ich konteksty są zapisywane oraz procesor przejmuje zadanie o najwyższym priorytecie (nr 3). Kiedy ono się zakończy, stan zadania nr 2 jest przywracany oraz może się ono zakończyć. Później, również zadanie o najniższym priorytecie uzyskuje możliwość zakończenia swoich obliczeń.
Jak w dalszej części pracy zostanie zauważone, podejście takie nie jest idealne oraz bardzo często prowadzi do powstania tak niekorzystnych zjawisk, jakimi są zakleszczenie, czy inwersja priorytetów.
Drugi algorytm, oprócz priorytetów, stosuje również technikę przydzielania procesora na określony okres czasu. Oczywista jest płynąca z tego faktu bezpośrednia korzyść. Jeśli w systemie będzie wiele zadań o identycznym poziomie ważności, to wówczas nie będą one wykonywane sekwencyjnie, tylko okresowo przerywane i wznawiane. Pozwala to uniknąć opóźnień, na które nie może pozwolić sobie system czasu rzeczywistego oraz w pewnym sensie uniezależni czas zakończenia danego procesu od czasu jego uruchomienia. Niech będzie taka sytuacja, w której każde zadanie po przejęciu procesora wykonuje się aż do zakończenia, a przerwane może zostać tylko przez zadanie o wyższym priorytecie. Gdyby w takich warunkach pojawiło się kilka żądań o jednakowej ważności, to wówczas każde kolejne musiałoby czekać na zakończenie poprzedniego. Jest to niedopuszczalne, gdyż powoduje zbyt duże czasy oczekiwania. Ideę algorytmu wywłaszczeniowego z podziałem czasu procesora obrazuje poniższy rysunek:
Rysunek 8. Szeregowanie zadań z wywłaszczeniem w oparciu o priorytety i z podziałem czasu procesora.
Jak widać, każdemu zadaniu przydzielony został określony przedział czasowy. Zadanie nr 1, po przejęciu procesora, wykonuje się przez określony czas, po czym oddaje procesor kolejnemu. Oczywiście jeśli w dowolnej chwili pojawi się zadanie o wyższym priorytecie od aktualnego, to wówczas ono staje się bieżącym. Jeśli jest ono przez cały czas swoich obliczeń zadaniem najważniejszym, to wykonuje się aż do zakończenia i dopiero oddaje procesor, aby inne zadania mogły się dokończyć. Wartym uwagi jest również fakt, że wywłaszczone zadanie nr 1, po przywróceniu jego stanu nie otrzymuje dodatkowego czasu. Jest ono wykonywane dalej tak, jakby przełączenie kontekstu w ogóle nie miało miejsca.
Zadania
Zadania, w rozumieniu systemów czasu rzeczywistego, to niezależne „programy”, zdefiniowane przez użytkownika, które konkurują między sobą o czas procesora. Zazwyczaj znacznie różnią się od procesów, co przede wszystkim objawia się w prostocie i szybkości działania. Wykonują się we wspólnej przestrzeni adresowej (w przestrzeni jądra) z maksymalnym poziomem uprzywilejowania i bezpośrednim dostępem do urządzeń, co upraszcza przełączanie kontekstu i sprowadza się jedynie do zmiany zawartości rejestrów procesora. Czasem w literaturze spotyka się ich określenie jako „procesy lekkie” (ang. light-weight processes) lub „wątki” (ang. threads). Określenia te jednak nie we wszystkich implementacjach oznaczają zawsze to samo, dlatego warto pozostać przy nazwie „zadania”.
Każde zadanie zaraz po utworzeniu zawiera szereg parametrów, które je definiują. Są to m. in. unikalny numer, priorytet, blok kontrolny, stos oraz kod programu. W trakcie swojego życia, z różnych powodów, ich stan jest często zmieniany i może określać w najprostszym przypadku następujące fazy:
zadanie gotowe do wykonania (gotowe)
wykonywane
zablokowane
Są one ze sobą powiązane w sposób pokazany na rys. 9.
Rysunek 9. Podstawowe stany zadania.
Stan gotowości (ang. ready) - w takim stanie znajduje się zadanie zaraz po jego utworzeniu. Jest ono gotowe do uruchomienia ale z różnych przyczyn (np. wykonujące się właśnie zadanie o wyższym priorytecie) ciągle oczekuje na czas procesora. Które zadanie zostanie przełączone w stan „wykonywane” określa planista.
Jak widać na powyższym rysunku, zadanie nie może bezpośrednio przejść ze stanu „gotowe” do „zablokowane”.
Stan wykonywania (ang. running) - gdy zadanie przechodzi w ten stan, procesor ładuje odpowiednią zawartość rejestrów oraz zaczyna wykonywać jego kod. Jak wynika z powyższego schematu, zadanie może przejść ze stanu „wykonywane” zarówno do stanu „gotowe”, jak i „zablokowane”. W pierwszym przypadku, jest to możliwe, gdy zostanie wywłaszczone przez inne, oczekujące na czas procesora, jako rezultat działania programu szeregującego. Drugi przypadek, to klasyczny efekt współdzielenia zasobów. Zadanie może zostać zablokowane i oczekiwać na spełnienie jakiegoś warunku (np. zwolnienie danego obszaru pamięci) lub wystąpienie zdarzenia (np. upłynął określony okres czasu lub inne zadanie wykonało konkretne obliczenia).
Stan blokowania (ang. blocked) - to wbrew pozorom bardzo istotny stan dla środowisk przetwarzających w czasie rzeczywistym. Gdyby nie istniał, zadania o najniższych priorytetach nigdy lub w sposób bardzo ograniczony nie mogłyby uzyskać dostępu do procesora. Byłyby od razu pozbawione jakichkolwiek szans konkurowania. Proces taki nazywa się głodzeniem (ang. starvation). Jak widać na rys. 9, z takiego stanu zadanie może przejść bezpośrednio do „wykonywane” (jeśli po odblokowaniu dalej jest zadaniem o najwyższym priorytecie) lub „gotowe”.
Semafory
Wielozadaniowe systemy operacyjne nie mogłyby istnieć, gdyby nie dostarczały jakiś obiektów umożliwiających wzajemne wykluczanie oraz synchronizację zadań. Ich bogactwo oraz możliwości są zależne od konkretnej implementacji. Ogólna zasada działania oraz przeznaczenie pozostają jednakże takie same. Projektanci współczesnych systemów operacyjnych raczej unikają jakiś egzotycznych rozwiązań w tym zakresie, uciekając się do standardu jakim niewątpliwie jest POSIX.
Semafor, jest obiektem jądra, który może zostać zajęty przez jeden lub kilka wątków w celu sterowania dostępem do np. jakiegoś wspólnego zasobu. Każdy jest zdefiniowany przez swój unikalny numer, blok kontrolny zawierający dodatkowe dane, wartość (binarną lub wyliczeniową) oraz listę oczekujących zadań. Z punktu widzenia programisty, semafor jest postrzegany jako abstrakcyjny typ danych, na którym oprócz określenia jego stanu początkowego, można wykonać tylko dwie operacje, umożliwiające wstrzymanie i wznowienie procesów.
Są nimi:
czekaj - to operacja opuszczająca semafor. Zmniejsza jego wartość o jeden. Pamiętać należy, że sam semafor jest liczbą nieujemną. Dlatego też operacja ta jest wykonywana tylko wtedy, gdy jest możliwa.
sygnalizuj - jest operacją zwiększającą wartość semafora o jeden. Logicznie odpowiada operacji podniesienia semafora.
Obie te operacje są niepodzielne, co oznacza, że w danej chwili może być wykonana tylko i wyłącznie jedna z nich. Raz rozpoczęte, muszą się zakończyć. W tym czasie, żaden inny proces nie może ich wywołać. Są implementowane w jądrze systemu, co daje szereg zalet, jak np. dostępność dla wszystkich procedur działających w systemie, operacja czekaj może zablokować wykonywanie jakiegoś procesu, co powinno spowodować przejście do innego a sygnalizuj może uczynić proces wykonywalnym.
Synchronizacja w oparciu o semafory przypomina sytuację, kiedy wiele obiektów pobiera klucze dostępu. Oczywiście istnieje pewna skończona liczba takich kluczy i kiedy ilość osiągnie wartość zero, wszystkie kolejne żądania ich wydania są blokowane. Te nieszczęsne obiekty muszą czekać aż ktoś zwróci jakiś klucz i będzie można go pobrać. Do tego momentu są one przechowywane w różnego rodzaju kolejkach. Mogą być to struktury typu FIFO lub innych np. posortowanych wg priorytetów.
Kiedy w systemie pojawia się możliwy do przejęcia klucz, wówczas jądro przydziela go pierwszemu obiektowi z listy oczekujących oraz zmienia jego stan w „gotowy do wykonania” lub „wykonywany” w zależności od panującej w danej chwili sytuacji.
Jądro może udostępniać wiele rodzajów semaforów. Te najczęściej spotykane to semafor binarny, semafor ogólny oraz mutex.
Semafor binarny
Jest to semafor, który może przyjmować tylko jedną z dwu możliwych wartości: 1 lub 0. Operacja opuszczenia, polega na zmniejszeniu jego wartości o jeden, jeśli jest to możliwe. Tak więc, jeśli semafor ma wartość 1, to wówczas przyjmuje wartość 0. Jeśli natomiast jest już „wyzerowany”, to uważany jest jako „niedostępny” lub „pusty” a zadanie wykonujące taką operację jest blokowane.
Operacja podniesienia takiego semafora, kieruje się zasadą, że jeśli istnieją procesy wstrzymane w wyniku jego opuszczenia, to jeden z nich jest wznawiany. W przeciwnym wypadku, semafor przyjmuje wartość 1. Diagram stanów dla semafora binarnego został przedstawiony na rys. 10.
Rysunek 10. Diagram stanów semafora binarnego.
Semafor ogólny
Jest to semafor, który posiada więcej niż dwa stany. Może być zajmowany oraz zwalniany wiele razy. Mówiąc bardziej obrazowo, jego wartość określa liczbę dostępnych w systemie jego kopii. Każde zadanie może pobrać jakąś ich ilość i jeśli zacznie ich brakować, to wówczas zadania zgłaszające ich żądanie będą blokowane.
Operacja opuszczenia takiego semafora, tak jak w poprzednim przypadku, polega na zmniejszeniu jego wartości o jeden, pod warunkiem, że otrzymany wynik jest wciąż liczbą nieujemną. W przeciwnym wypadku dochodzi do blokowania procesu ją wykonującego.
Operacja podniesienia, polega na wznowieniu jakiegoś procesu oczekującego lub zwiększeniu jego wartości o jeden. Taka inkrementacja, w większości implementacji nie jest wykonywana bez końca. Zazwyczaj narzucana jest maksymalna wartość, która może wynikać z przyjętego typu danych lub też być narzucona z góry.
Rys. 11 przedstawia diagram stanów oraz przebieg operacji wykonywanych na semaforze ogólnym.
Rysunek 11. Diagram stanów semafora ogólnego.
Muteksy
Muteksy (ang. mutual exclusion semaphores), stanowią szczególny rodzaj semaforów binarnych. Ich stany określa się inaczej niż to było pokazane wcześniej dla semaforów binarnego i ogólnego. Muteks może być zablokowany (ma wartość 1) lub odblokowany (ma wartość 0 - nie zablokowany).
Najważniejszym problemem semaforów jest fakt, że są one globalne. Dowolny proces może je opuszczać albo podnosić. Umożliwia to powstanie sytuacji, w której jedno z zadań wykona operację czekaj aby synchronizować dostęp do jakiegoś zasobu w później inne, niezwiązane logicznie z tym zasobem, podniesie semafor. Może to prowadzić do uszkodzenia wspólnych danych a w najgorszym przypadku do załamania całego systemu. Dlatego też, wiele implementacji wprowadza pewne nowe cechy dla muteksów. Jedną z nich jest zasada posiadania. Otóż polega ona na tym, że jeśli jakieś zadanie zablokuje muteks (nada mu wartość 1), to tylko ono może ten muteks odblokować (nadać mu wartość 0). Takie podejście eliminuje całkowicie opisany wcześniej potencjalny problem niespójności danych.
Kolejnym udogodnieniem, jakie się często spotyka, jest możliwość blokowania muteksów w sposób rekurencyjny. Nie ma ona nic wspólnego z semaforem ogólnym. Określa tylko i wyłącznie, ile razy zadanie, które posiada dany muteks wykonało na nim operację blokowania. Jest to bardzo przydatna cecha, która umożliwia uniknięcia takich sytuacji jak np. zakleszczenie procesów (ang. deadlock). Niech będzie dana sytuacja, w której jakieś zadanie kilkakrotnie zgłasza żądanie dostępu do zasobu. Wówczas, po pierwszym jego wystąpieniu, wszystkie kolejne byłyby niemożliwe do zrealizowania. Jednakże, ponieważ są one zgłaszane przez tego samego właściciela, nie stanowią one zagrożenia dla systemu oraz mogą zostać obsłużone. Cały proces najlepiej oddaje rys. 12.
Rysunek 12. Diagram stanów dla muteksów.
Jak widać na powyższym rysunku, kolejne operacje blokujące, powodują zwiększenie wartości wewnętrznego licznika. Sytuacja taka może wystąpić jeśli zadanie składa się z szeregu podprogramów, również wymagających dostęp do części chronionej przez muteks. Istotną sprawą jest fakt, że zadanie musi tyle samo wykonać na danym muteksie operacji odblokowujących, ile wcześniej zostało wykonanych blokujących.
Zmienne warunkowe
Zmienne warunkowe również służą do synchronizacji oraz pozwalają wielokrotnie wstrzymywać wykonanie jakiegoś zadania, aż żądany warunek zostanie spełniony. Kiedy zadanie sprawdza daną zmienną warunkową, musi mieć do niej dostęp na wyłączność. Dlatego też, dostęp do nich musi być chroniony przez muteksy, uniemożliwiające zmianę ich stanu, podczas jego odczytu. Zasadę ich działania najlepiej oddaje rys. 13.
Rysunek 13. Zasada działania na zmiennych warunkowych.
Jak widać, w pierwszym kroku, zadanie nr 1 blokuje chroniący zmienną warunkową muteks oraz sprawdza ją. Niestety, stan zasobu powiązanego z tą zmienną nie spełnia wymagań i w kroku drugim oraz trzecim zadanie „wędruje” do listy zadań oczekujących, zwalniając jednocześnie muteks. W kolejnym kroku, zadanie nr 2 blokuje dostępny muteks, wykonuje jakieś operacje na zasobie współdzielonym oraz zwalnia go, zapisując ten fakt w zmiennej warunkowej. Następnie, w kroku piątym, wykonuje operację informującą zadania oczekujące, że warunek został zmieniony. Wówczas wybrane zadanie z kolejki (w tym przypadku zadanie nr 1), ponownie blokuje muteks, sprawdza warunek (ponowne sprawdzanie warunku powoduje uniknąć sytuacji, w której operacja sygnalizowania jest błędnie wykonana i najczęściej jest implementowane w postaci pętli while) oraz zajmuje zasób.
Komunikacja międzyprocesowa
Kolejnym, bardzo ciekawym zagadnieniem związanym ogólnie z systemami operacyjnymi, jest wymiana danych. W najprostszym przypadku, wystarczą zmienne globalne. Jednakże w środowiskach przetwarzających w czasie rzeczywistym, jak już zostało wcześniej wspomniane, ucieka się do wielu kompromisów, dzięki którym można „zdążyć na czas”. Bardzo często zadania dzieli się na dwie grupy. Najwyższy priorytet uzyskują te, tzw. czasu rzeczywistego. Zawierają one najbardziej krytyczne procedury. Są one maksymalnie upraszczane i charakteryzują się możliwie najmniejszym rozmiarem. Pozostałe, tzw. zwykłe procesy, zajmują się np. obsługą operacji wejścia - wyjścia, wizualizacją danych itp. Oczywistą konsekwencją takiej architektury, jest konieczność zastosowania dodatkowych kanałów komunikacyjnych. Ich liczba oraz możliwości są zależne od konkretnej implementacji. Każde jądro systemu czasu rzeczywistego udostępnia swój zbiór obiektów.
Lektura kolejnych podrozdziałów pozwoli dogłębniej poznać dostępne w większości współczesnych systemów mechanizmy przeznaczone do wymiany informacji. Czytelnik uzyska niezbędną wiedzę, która pozwoli mu wybrać najlepsze oraz najbardziej odpowiednie metody dla konkretnych przypadków.
Kolejki komunikatów
Kolejki komunikatów, to inteligentne bufory, które zazwyczaj działają w oparciu o algorytm „pierwszy przyszedł, pierwszy został obsłużony” (FIFO). Możliwe jest jednak, aby w konkretnych przypadkach przypominały swego rodzaju stos. Umożliwia to wysyłanie krytycznych informacji, które nie mogą czekać aż zostaną przekazane dalej. Wtedy taki komunikat od razu trafia na pierwszą pozycję a cała struktura działa zgodnie z algorytmem „ostatni przyszedł, pierwszy zostanie obsłużony” (LIFO). Względnie komunikaty mogą być również sortowane wg priorytetów.
Po utworzeniu, każda otrzymuje swój unikalny identyfikator, nazwę, blok kontrolny, przydział pamięci, rozmiar ogólny i rozmiar komunikatów oraz jedną bądź kilka list dla zadań oczekujących.
Strukturę tą najlepiej przedstawia poniższy rysunek (rys. 14).
Rysunek 14. Kolejka komunikatów i powiązane z nią obiekty.
Jak wynika z powyższego rysunku, przez rozmiar komunikatu rozumie się ilość bajtów, z ilu maksymalnie może się on składać. Natomiast długość kolejki, to ilość komunikatów, jakie może ona pomieścić.
Rozmiary poszczególnych buforów są podawane przez użytkownika. Jądro alokuje je albo w swojej prywatnej przestrzeni lub w przestrzeni systemowej. Jest to zależne od konkretnej implementacji. Pierwszy przypadek, wiąże się z większym wykorzystaniem pamięci. Zajmowana jest taka ilość, aby każda kolejka miała dostęp do żądanego buforu na wyłączność. Eliminuje to całkowicie problem nadpisywania danych. Natomiast w drugim przypadku, kiedy wykorzystywana jest przestrzeń systemowa, zakłada się, że w danej chwili nie są zapełnione wszystkie kolejki. Są one przechowywane we wspólnym, dużym obszarze pamięci. Tak więc, projektanci mają pewien wybór. Albo stworzyć struktury bardziej wiarygodne i pewniejsze, albo zaoszczędzić na pamięci, co zazwyczaj wiąże się z koniecznością stworzenia pewnych dodatkowych protokołów, eliminujących wszelkie nieprzyjemne sytuacje.
Dodatkowo jądro udostępnia podstawowy interfejs programistyczny, służący do komunikacji z kolejkami. Składają się na niego takie niepodzielne operacje, jak tworzenie, niszczenie, czytanie, zapis. Sprawia to, że dla zwykłego procesu, są one widziane jak pliki dyskowe, co bardzo upraszcza proces korzystania z nich.
Istotnym jest fakt, że z kolejką komunikatów są powiązane zazwyczaj dwie listy zadań oczekujących. Pierwsza z nich, lista zadań wysyłających, jest zapełniana, kiedy kolejka jest pełna i nie może przyjąć więcej danych. Druga, tzw. lista zadań odbierających, jest zapełniana w przeciwnym wypadku, kiedy kolejka jest pusta. Oczekują one na pojawiające się komunikaty. Innym przykładem zapełniania obu list, jest sytuacja, w której szybkości pojawiania się nowych danych i ich odbierania nie są ze sobą zgrane. Należy zauważyć pewną niedogodność związaną z taką sytuacją. Otóż, przekazywany komunikat jest kopiowany dwukrotnie. Pierwszy raz, z obszaru zadania wysyłającego do bufora kolejki, a drugi, z kolejki do pamięci zadania docelowego.
Rysunek 15. Proces przesyłania komunikatu przy pomocy kolejki.
Ponieważ takie postępowanie może mieć bardzo znaczący wpływ na wydajność, zaleca się przesyłanie wskaźnika do danych niż same dane. Pozwala to lepiej wykorzystać dostępną pamięć.
Kolejki komunikatów bardzo często są także wykorzystywane do przesyłania danych nie tylko pomiędzy zadaniami, ale również pomiędzy procedurami obsługi przerwań i zadaniami. Tutaj należy zwrócić uwagę na bardzo istotną różnicę pomiędzy tymi dwoma zastosowaniami. Kiedy zadanie zaczyna wysyłać dane do przepełnionej kolejki, to zazwyczaj dopuszczalne są pewne wynikające z tego faktu opóźnienia. Zadanie może być tak zaprojektowane, aby oczekiwało na wolny bufor aż zostanie zwolniony lub tylko przez pewien, z góry określony czas. Procedury obsługi przerwań nie mogą sobie jednak pozwolić na taki luksus. Ich natura jest bardziej niecierpliwa i pod żadnym pozorem nie mogą przejść do stanu blokowany (jak to było opisane już wcześniej w przypadku zadań, zob. rys. 9). Wtedy np. zadanie oczekuje na pustej kolejce, która jest zapełniana danymi po wystąpieniu przerwania sprzętowego.
Wiele współczesnych aplikacji, aby móc poprawnie funkcjonować, wymaga dwukierunkowego połączenia. Kolejki są niestety strukturami jednokierunkowymi. Tej samej nie można używać jednocześnie do zapisu i odczytu. Dlatego w takich przypadkach wykorzystuje się dwie kolejki. Architektura taka jest najczęściej spotykana w systemach typu klient/serwer.
Rysunek 16. Dwukierunkowa komunikacja z użyciem kolejek.
Niekiedy jednak istnieje potrzeba wysłania jakiegoś komunikatu do kilku zadań. Wtedy kilka zadań jest blokowanych na danej kolejce i oczekują na komunikat. Po umieszczeniu komunikatu przez nadawcę, każde z zadań oczekujących pobiera jego kopię, nie usuwając go z kolejki oraz kontynuuje swoje obliczenia. Jest to tzw. broadcast i odpowiada związkowi opisującemu zależność jeden do wielu.
Rysunek 17. Rozsyłanie wiadomości z jednego zadania do wielu.
W najprostszych przypadkach, zazwyczaj wystarcza wykorzystanie jednej kolejki. Zadania producenta i konsumenta można dodatkowo synchronizować w oparciu o semafor zainicjalizowany wartością 0. Nadawca umieszcza dane w kolejce i po próbie zajęcia semafora, jest na nim blokowany. Konsument w kolejnym kroku pobiera dane oraz inkrementuje semafor.
Potoki
Potoki (ang. pipes), służą do przechowywania danych jako strumień pozbawiony jakiejkolwiek struktury. Są one odczytywane wg algorytmu „pierwszy przyszedł, pierwszy zostanie obsłużony” (FIFO). Po utworzeniu potoku, otrzymuje się dwa wskaźniki (deskryptory). Jeden z nich jest ustawiony na jego początek, a drugi na koniec. Dzięki nim możliwe są operacje zapisu i odczytu.
Rysunek 18. Jednokierunkowy potok.
Zazwyczaj, potoki dostarczają prostych protokołów blokujących zadania oczekujące na dane jeśli potok jest pusty oraz próbujące dokonać do niego zapisu gdy jest przepełniony. Umożliwia to istnienie wielu tzw. konsumentów i producentów oraz podstawową ich synchronizację.
Istnieje podstawowa różnica pomiędzy potokami a kolejkami komunikatów. Potok może przechowywać tylko i wyłącznie jedną porcję danych i to nie w postaci strukturalnej, a jedynie jako ciąg bajtów. Niesie to za sobą następującą konsekwencję. Nie jest możliwe posortowanie danych w oparciu o ich priorytet. Potoki jednakże dostarczają bardzo potężną możliwość, jaką jest operacja selekcji. Zadanie może być zablokowane i oczekiwać na pojawienie się jakiegoś warunku na jednym lub kilku potokach. Warunkiem tym może być np.: pojawienie się nowych danych lub pobranie ich. Przykładem może być sytuacja, w której dany proces oczekuje na dane na jednym z dwóch potoków. Jeśli się one pojawią, to natychmiast przesyła wynik operacji na nich do trzeciego.
Rysunek 19. Operacja selekcji na potokach.
Jak widać na rys. 19, zadanie oczekuje na dane przesyłane od innych. Jednym z nich, może być np.: procedura obsługi przerwania. Jak już wcześniej wspomniano, obsługa przerwania nie może być blokowana w oczekiwaniu na zasób, gdyż produkowane przez nią sygnały byłyby gubione. Potoki bardzo dobrze się nadają do eliminacji takiego problemu, gdyż umożliwiają bardzo łatwe stworzenie osobnych kanałów komunikacyjnych. Jeśli któryś z potoków zostanie zapełniony, wówczas zadanie jest natychmiast aktywowane, wykonuje swoją procedurę oraz przesyła jej wyniki do innego potoku, którego ujście znajduje się w innym zadaniu. Taka sytuacja nie byłaby możliwa z wykorzystaniem kolejek. Nie jest możliwe blokowanie zadania na kilku kolejkach w oczekiwaniu np. na pojawienie się danych na którejś z nich.
Rejestry zdarzeń
Bardzo często występuje konieczność, aby zadanie miało możliwość śledzenia wystąpienia jakiś konkretnych zdarzeń oraz podjęcia odpowiednich kroków w celu wypracowania jakiejś odpowiedzi na nie. Obiektami umożliwiającymi takie zachowanie są właśnie rejestry zdarzeń. Są to obiekty, które składają się z szeregu bitów, interpretowanych jako flagi. Zależnie od implementacji mogą być np. 8, 16, 32 - bitowe. Każda aplikacja określa dla konkretnej pozycji odpowiednie zachowanie. Jest ono ustalone zarówno przez nadawcę, jak i odbiorcę. Jeśli dane zdarzenie pojawi się w systemie, to wówczas zadanie lub procedura obsługi przerwania ustawiają daną flagę. W ten sposób, inny proces jest informowany o jego wystąpieniu oraz wykonuje w odpowiedzi jakąś określoną procedurę.
Proces ten najlepiej przedstawia rys. 20.
Rysunek 20. Działanie rejestru zdarzeń.
Zadanie może okresowo sprawdzać oraz czekać na pojawienie się określonych zdarzeń. Dzięki rejestrowi zdarzeń, jądro systemu jest uczone, kiedy ma przebudzać dane zadania. Warunkiem „przebudzenia” może być skomplikowana formuła logiczna, jak np.: „informuj mnie gdy pojawi się zdarzenie typu 1 i typu 2”.
Na uwagę zasługuje fakt, że zdarzenia w rejestrze nie są kolejkowane. Nie implementuje on zdolności zliczania wystąpień tego samego zdarzenia. Każde kolejne są gubione.
Kolejną ważną cechą rejestrów zdarzeń jest to, że ten rodzaj komunikacji międzyprocesowej nie ma żadnego powiązania z przesyłaniem danych oraz nie zapewnia mechanizmów umożliwiających identyfikację źródła. Służy jedynie do zapewnienia bardzo podstawowej synchronizacji. Co więcej, dane zadanie jest informowane o wystąpieniu jakiegoś zdarzenia tylko i wyłącznie jeśli samo tego chce. W przeciwnym wypadku, jego wystąpienie nie ma na nie najmniejszego wpływu. Jest albo obsługiwane przez kogoś innego, albo zapominane.
Sygnały
Sygnał, to przerwanie programowe, generowane w odpowiedzi na zaistnienie jakiegoś zdarzenia. Przerywa normalny tok wykonywania u jego odbiorcy oraz wymusza wykonanie jakiegoś określonego zadania. Sygnały są asynchroniczne oraz nie posiadają charakterystycznych punktów występowania. Zadanie - odbiorca nie ma żadnego wpływu na to kiedy ma się pojawić sygnał. Toteż jego wystąpienie jest raczej losowe. Jest to główny powód, dla którego sygnałów nie powinno się używać w celach synchronizacji. Są one główną przyczyną do powstawania zadań niedeterministycznych, co w środowiskach przetwarzających w czasie rzeczywistym jest bardzo niepożądaną cechą. Ogólną ideę sygnałów najlepiej przedstawia rys. 21
.
Rysunek 21. Ogólna zasada działania sygnałów.
Jak widać, kiedy zadanie odbierze sygnał, którego źródłem może być zarówno jakieś inne zadanie, jak i procedura obsługi przerwania, jego wykonanie jest wstrzymywane oraz sterowanie jest przekazywane go procedury obsługi sygnału. Dopiero po jej zakończeniu, zadanie może być kontynuowane.
Sygnały są identyfikowane przez swój numer. Blok kontrolny każdego zadania zawiera w swojej strukturze dodatkową informację o tym, które sygnały ma dane zadanie ignorować (wówczas do ich obsługi jądro wywołuje domyślną procedurę; niektórych sygnałów nie można ignorować), na które ma reagować oraz szereg dodatkowych danych jak np. sygnały oczekujące czy zablokowane (nie są w ogóle dostarczane przez jądro systemu, np. by chronić sekcję krytyczną zadania). Należy zauważyć, że tylko niektóre systemy czasu rzeczywistego interpretują numer danego sygnału jako jego priorytet, co rozwiązuje problem kolejności jego dostarczenia oraz ma znaczący wpływ na stabilność całej aplikacji.
Procedura obsługi przerwania stanowi sekcję krytyczną. Przerwanie jej wykonania nie jest możliwe, a na czas jej trwania, wszystkie nadchodzące sygnały są kolejkowane.
Ciekawą sprawą jest fakt, że każde zadanie może zainstalować własną procedurę obsługi danego sygnału. Z tego względu, aby uniknąć mogących się pojawić problemów, zaleca się, aby każde zadanie przed jej zmianą, zapisało w swojej pamięci podręcznej adres już wcześniej zainstalowanej procedury. Na koniec jego obsługi, powinno przywrócić pierwotną procedurę obsługi.
Na rys. 21 widać jeszcze tzw. tablicę wektorów. Zawiera ona adresy wskazujące na procedury obsługi danego sygnału. Wartość 0 (NULL) oznacza, że dany sygnał nie ma zainstalowanej procedury obsługi.
Większość implementacji nie dostarcza mechanizmów zliczających kolejne wystąpienia tego samego sygnału. Dlatego też, jeśli procedura jego obsługi nie zostanie wywołana dostatecznie szybko, jego kolejne wystąpienia są zapominane.
Tak samo jak w przypadku rejestrów zdarzeń, sygnały nie są przeznaczone do przesyłania jakiejś konkretnej porcji danych.
Przerwania i wyjątki
Każdy współczesny system dostarcza mechanizmów umożliwiających natychmiastowe przerwanie normalnego toku obliczeniowego procesora oraz wymuszających wykonanie jakiejś procedury. Umożliwiają one lepsze panowanie nad wszelkimi błędami i nienaturalnymi sytuacjami. Pozwalają stworzyć architekturę czułą na wszelkie zewnętrzne impulsy jak np. pojawienie się przeszkody czy zmiana temperatury. Ten rozdział przedstawi dwa takie mechanizmy: przerwania i wyjątki. Czytelnik nie tylko dowie się czym one są oraz do jakich celów są przeznaczone, ale i również zrozumie w jaki sposób są one interpretowane przez system.
Czym są przerwania i wyjątki ?
Aby odpowiedzieć na to pytanie, najlepiej jest przytoczyć definicje, jakie można znaleźć w dostępnej literaturze. Poniższe zostały zaczerpnięte z [5].
Wyjątek (ang. exception) - jest to dowolne zdarzenie, które przerywa normalny tok obliczeń procesora i wymusza wykonanie określonego zbioru instrukcji w trybie uprzywilejowanym. Najogólniej można je podzielić na dwie grupy: synchroniczne i asynchroniczne.
Synchroniczne są generowane przez tzw. zdarzenia wewnętrzne jak np. efekt wykonania jakiejś instrukcji procesora. Przykładami mogą być dzielenie przez zero lub niepoprawny odczyt z pamięci.
Przerwanie (ang. interrupt, external interrupt) - to tzw. wyjątki asynchroniczne i nie są powiązane z instrukcjami wykonywanymi przez procesor. Ich źródłem są wszelkie zdarzenia zewnętrzne, czyli odnoszą się do różnych sygnałów generowanych przez sprzęt. Przykładami mogą być wciśnięcie przycisku reset na płycie głównej lub sygnał urządzenia komunikacyjnego, które właśnie otrzymało pakiet z danymi.
Można je podzielić na maskowalne, czyli takie, które można wyłączyć programowo oraz niemaskowalne, których nie da się zablokować. Te drugie, zazwyczaj są połączone z procesorem przy pomocy specjalnego kanału komunikacyjnego i są obsługiwane natychmiast po ich wystąpieniu.
Wewnętrzna interpretacja
Jednym z podstawowych elementów większości architektur, jest programowalny kontroler przerwań (ang. programmable interrupt controller, PIC). Jest on odpowiedzialny za komunikację ze światem zewnętrznym oraz umożliwia odbieranie z niego różnych sygnałów. Standardowo, spełnia dwie zasadnicze role:
sortuje nadchodzące przerwania wg priorytetów tak, aby w danej chwili procesor odebrał tylko to najważniejsze
odciąża procesor z konieczności określenia źródła przerwania
Kontroler przerwań składa się z wielu linii żądań, na które przyjmowane są przerwania. Każda ma swój przypisany priorytet oraz jest powiązana z konkretnym źródłem.
Przerwania i wyjątki są obsługiwane w kolejności od najważniejszego do najmniej istotnego. Zazwyczaj najwyższy priorytet mają te najbardziej krytyczne dla całego systemu jak np. reset. Później znajdują się wszelkie wyjątki informujące o różnego rodzaju błędach, które mogą być zgłaszane np. przy pomocy odpowiednich instrukcji. Wreszcie grupę przerwań o najniższym priorytecie stanowią asynchroniczne maskowalne.
Z punktu widzenia aplikacji, wszystkie przerwania i wyjątki mają wyższy priorytet niż obiekty systemu operacyjnego, jak np. semafory, kolejki, zadania itd.
Całą tą zależność najlepiej obrazuje rys. 22.
Rysunek 22. Priorytety przerwań w skali systemu.
Dodatkową sprawą jest fakt, że wszelka obsługa obiektów systemu operacyjnego jest sterowana przez jego jądro.
Czas i jego odmierzanie
Aby poprawnie mogły działać proces szeregujący zadania oraz same zadania czasu rzeczywistego, bardzo ważne jest precyzyjne odmierzanie czasu. Istnieją zadania, które wymagają aktywowania w określonych momentach czasu, oczekiwania na zdarzenie przez zadany czas, bądź to uruchamiania okresowego. Niedokładne odmierzanie czasu jest zjawiskiem bardzo niekorzystnym, gdyż powoduje odchylenia od zaplanowanych terminów.
Większość systemów wbudowanych dostarcza dwa rodzaje mechanizmów odmierzających czas (ang. timer). Pierwsze z nich, opierają się o rozwiązania sprzętowe. Składają się z chipów, które w sposób bezpośredni przerywają obliczenia procesora oraz sygnalizują, że upłynął jakiś okres. Są one stosowane wszędzie tam, gdzie bardzo istotna jest precyzja.
Współczesne architektury są zaopatrzone w specjalne układy, zwane programowalnymi kontrolerami czasu. Są one zazwyczaj połączone z innymi częściami urządzenia, jak np. chip DMA oraz stają się ich siłą napędową np. poprzez dyktowanie tempa odświeżania zawartości pamięci DRAM. Wszystkie programowalne kontrolery czasu określają częstotliwość impulsu wejściowego oraz zbiór programowalnych rejestrów. Częstotliwość generowanych przez nie przerwań jest funkcją odpowiedniego rejestru oraz sygnału wejściowego. Reszta rejestrów, może określać inne cechy, jak np. czy odmierzanie ma być cykliczne oraz są zależne od konkretnej implementacji. Podczas startu systemu następuje inicjalizacja takiego układu, na którą oprócz określenia jego stanu oraz ustawienia podstawowych parametrów, składa się również instalacja procedury obsługi przerwania czasowego. Główne zadania tej procedury to:
aktualizacja zegara systemowego - dla podtrzymania daty, godziny oraz licznika informującego jak długo system jest uruchomiony
wywołanie odpowiedniej funkcji jądra, która informuje o upłynięciu danego okresu. Ma to na celu synchronizację zegarów programowych oraz planisty zadań
ponowna inicjalizacja odpowiednich rejestrów programowalnych
Druga forma mechanizmów odpowiedzialnych za odmierzanie czasu, to typowe rozwiązania programowe. Pozwalają bardzo wydajnie szeregować zadania, dla których można założyć pewne niedokładności jeśli chodzi o impulsy czasowe. Ich użycie składa się zazwyczaj z trzech podstawowych etapów:
instalacja zegara (uruchomienie odliczania)
podtrzymywanie zegara(np. odliczanie)
usunięcie zegara (zatrzymanie odliczania).
Etapy te są możliwe do realizacji dzięki dostępnym w konkretnych implementacjach zestawom interfejsów (np. API zgodne ze standardem POSIX).
Bardzo ciekawym jest fakt, że zegary programowe składają się z dwóch komponentów. Pierwszy z nich, to maksymalnie uproszczona procedura obsługi przerwania. Drugi natomiast, może zawierać o wiele więcej instrukcji. Jest to część wykonywana w kontekście zadania. Takie podejście daje wiele korzyści. Gdyby cała praca, jaka jest związana z zadanym upływem czasu była wykonywana w procedurze obsługi przerwania, to prawie na pewno jej wykonanie zajęłoby kilka taktów zegarowych. To mogłoby, w najgorszym przypadku, doprowadzić do utraty zdarzeń informujących o upływie jakiegoś okresu czasu. Aby uniknąć tego typu sytuacji, procedura obsługi przerwania wykonuje tylko prostą operację na wewnętrznym liczniku oraz ewentualne przekazuje sterowania do określonego zadania, zwanego zadaniem podrzędnym. Użycie takiego mechanizmu odmierzania czasu najlepiej obrazuje poniższy przykład.
Przykład 1. Użycie zegara programowego [5]
Niech będzie dane urządzenie, w którym zegar sprzętowy generuje przerwanie co 10ms. Dla potrzeb projektowanego oprogramowania, konieczne są trzy zegary generujące impulsy po upływie odpowiednio 200ms, 300ms i 500ms. Największym wspólnym dzielnikiem podanych czasów jest 100ms, które odpowiada dziesięciu kolejnym impulsom zegara sprzętowego. Aby zaimplementować taką sytuację, najlepiej w chwili początkowej zablokować zadanie podrzędne na jakimś semaforze. Wówczas, procedura obsługi przerwania, powinna po każdym impulsie zegara sprzętowego zmniejszać wartość wewnętrznego licznika o jeden. Oczywiście jego wartością początkową powinno być 10. Kiedy osiągnie on wartość 0, procedura obsługi przerwania zwalnia semafor i w ten sposób informuje o upływie zadanego okresu czasu. Wtedy zadanie odpowiedzialne za dalsze operacje, jeśli ma najwyższy priorytet z zadań obecnych w danej chwili w systemie, przejmuje sterowanie i zajmie się dalszymi operacjami. Koncepcja ta jest zobrazowana na rys. 23.
Rysunek 23. Użycie zegara programowego.
Podsumowując to, co powiedziano do tej pory, zadanie podrzędne (ang. worker task) jest wywoływane przez procedurę obsługi przerwania co 100ms. Aby sprostać wymogom tej konkretnej aplikacji, implementuje ono 3 kolejne liczniki o wartościach początkowych, odpowiednio: 2, 3 i 5. Będą one wraz z każdym uruchomieniem tego zadania dekrementowane. Po wyzerowaniu którejkolwiek z wartości, natychmiast uruchamiana jest odpowiednia procedura. Proces ten obrazuje rys. 24.
Rysunek 24. Obsługa zegara programowego w kontekście zadania.
Jak widać na powyższym rysunku, kiedy pierwszy z lokalnych liczników osiąga wartość zero, uznaje się, że upłynął czas 200ms. Wówczas wywoływana jest odpowiednia procedura (w tym wypadku: timeout_fn_1).
Jeśli natomiast zaistniałaby konieczność implementowania zegarów o okresach, które nie mają wspólnego podzielnika, to wówczas po określonej liczbie taktów, można go przeprogramować. Oczywiście w takich przypadkach należy wziąć również pod uwagę liczbę taktów potrzebnych na taką operację.
Nasuwa się oczywiste pytanie: dlaczego używać tych mniej dokładnych mechanizmów programowych, skoro można bardziej precyzyjnie określać upływ czasu? Otóż, odpowiedź jest bardzo prosta. Faktem przemawiającym za rozwiązaniami programowymi jest zmniejszenie liczby generowanych przerwań sprzętowych i zwiększenie wydajności systemu. Gdyby uruchomić wiele zegarów sprzętowych, to wówczas, mogłaby powstać sytuacja, w której zakłócałyby się one wzajemnie, co powodowałoby znaczne opóźnienia. Eliminuje to również powstanie tzw. kosztów stałych, związanych z ich obsługą. Do tego należy dodać, że większość aplikacji, które wymagają obliczeń czasowych nawet na poziomie milisekund, zakładają pewną granicę błędu. Dlatego też, zegary programowe są w takich przypadkach wystarczające. Należy zwrócić uwagę na fakt, że zadania czasu rzeczywistego nie posiadają własnych zegarów. Zegar posiada jedynie jednostka szeregująca zadania.
Problemy związane z szeregowaniem zadań
W systemach wielozadaniowych, prawie zawsze istnieje pewien podzbiór procesów uruchomionych w danej chwili, które oddziałują na siebie. Źle zorganizowany współbieżny dostęp do tych samych zasobów, najczęściej prowadzi do powstawania niespójności danych, w efekcie których przydatność całego systemu może spaść do zera. Oczywiście aby uniknąć takich sytuacji, można skorzystać z mechanizmów omówionych już wcześniej, jak np. semafory czy muteksy. Niewątpliwie przy ich pomocy można rozwiązać problemy wzajemnego wykluczania i synchronizacji. Jednakże w środowiskach, w których program szeregujący zadania opiera swoje algorytmy o priorytety oraz możliwe jest wywłaszczenie, projektanci aplikacji muszą wykazać się dodatkową ostrożnością. Muszą przede wszystkim starać się nie dopuścić do powstania scenariusza, w którym prawo do wykonania otrzymują tylko procesy o najwyższych priorytetach a zasoby przekazują między sobą. Prowadzi to do tego, że te mniej istotne zadania z punktu widzenia całego systemu, mogą nigdy się nie doczekać na swój czas procesora co uniemożliwi ich zakończenie. Takie zjawisko nazywa się głodzeniem (ang. starvation) lub wykluczeniem. Innymi, o wiele bardziej groźnymi problemami, które mogą wywołać niestabilność lub co gorsze całkowitą blokadę systemu, są zakleszczenie (ang. impas lub deadlock) oraz inwersja priorytetów (ang. priority inversion). Należy jednak zwrócić uwagę, że w systemie istnieją dwie kategorie zasobów. Pierwszą stanowią te, których współdzielenie nie wymaga stosowania jakiś wyrafinowanych protokołów. Są nimi np. zbiory rejestrów, które przy przełączaniu kontekstu są zapamiętywane lub przywracane przez program szeregujący zadania. Mimo, że są wykorzystywane przez wiele zadań, każde tak naprawdę ma ich własną kopię. Drugą kategorię stanowią zasoby, do których dostęp musi być bezwzględnie synchronizowany. Nie można ich zabrać. Trzeba czekać, aż zadanie samo je zwolni. Dostęp do nich opiera się o regułę wyłączności. Są nimi przede wszystkim wspólne obszary pamięci. Niedopuszczalnym jest fakt, aby jakieś zadanie zaczynało czytać dane z jakiegoś bufora nim inne zakończy do niego zapisywać.
Dostępna literatura (patrz np. [5], [7], [8]) przedstawia wiele mechanizmów, dzięki którym można uniknąć opisanych wyżej sytuacji lub jeśli już się pojawią, zminimalizować ich skutki oraz przywrócić system do stanu „normalnego”. Kolejne podrozdziały przybliżą naturę takich problemów oraz przedstawią metody ich rozwiązywania. Zdobyta po ich lekturze wiedza, z całą pewnością pomoże projektować systemy, których działanie jest bardziej przewidywalne (tzw. systemy deterministyczne) oraz lepiej spełnić ograniczenia czasowe.
Zakleszczenie procesów
W poprawnym programie współbieżnym, tj. posiadającym własną żywotność, w każdym procesie powinno w końcu nastąpić oczekiwane zdarzenie.
Zbiór procesów znajduje się w stanie blokady (inne określenie to np. zastój, zakleszczenie, martwy punkt), jeśli każdy z nich jest wstrzymany w oczekiwaniu na zdarzenie, które może być wywołane przez jakiś inny proces z tego zbioru. Sytuację taką przedstawia rys. 25.
Rysunek 25. Zakleszczenie procesów (ang. deadlock).
Są tutaj trzy zadania ponumerowane jako T1, T2 i T3 oraz trzy zasoby: R1, R2, R3. Strzałka biegnąca od zadania do zasobu oznacza, że dane zadanie potrzebuje tego zasobu, aby móc kontynuować swoje obliczenia. Strzałka skierowana od zasobu do zadania oznacza, że dany zasób jest używany przez określone zadanie. Na rysunku przedstawiona jest klasyczna sytuacja cyklicznego oczekiwania. Zadanie T1 posiada zasób R1, lecz potrzebuje również dostępu do zasobu R2, który należy do zadania T2. Zadanie T2 również nie może być kontynuowane, gdyż potrzebuje dostępu do zasobu R3 ale jest on używany przez zadanie T3. Koło zamyka się ponieważ zadanie T3, aby mogło być kontynuowane, musi uzyskać dostęp do zasobu R1. Zakłada się w tym przykładzie, że w systemie istnieją tylko pojedyncze kopie zasobów R1, R2 i R3 oraz, że zadania nie oddają zasobów, jeśli w określonym czasie nie otrzymają pełnego, potrzebnego do dalszej pracy ich podzbioru.
Oczywiście system jakoś musi wykryć taką sytuację oraz podjąć próbę jej wyeliminowania. Systemy czasu rzeczywistego okresowo sprawdzają politykę podziału zasobów oraz zgłoszone żądania dostępu do nich. Algorytmy wykrywające zakleszczenie procesów opierają się o teorię grafów. Otóż okazuje się, że warunkiem koniecznym i wystarczającym na zaistnienie blokady, w sytuacji kiedy procesy, aby móc poprawnie funkcjonować, muszą uzyskać dostęp do wszystkich żądanych zasobów jednocześnie, jest istnienie cyklu w grafie opisującym alokację zasobów w systemie. W literaturze (patrz np. [5] - z tej pozycji został zaczerpnięty poniższy przykład) można doszukać się następującego algorytmu, mającego na celu wykrycie zastoju. Jego kolejne kroki wyglądają następująco:
Zrób listę N wszystkich wierzchołków grafu.
Wybierz dowolny wierzchołek z N oraz utwórz kolejną listę L, która początkowo jest pusta.
Wstaw poprzednio wybrany wierzchołek do listy L. Jeśli taki sam już się w niej znajduje, to graf zawiera cykl a w systemie istnieje blokada. Zakończ algorytm. W przeciwnym wypadku, tylko usuń ten wierzchołek z listy N.
Sprawdź czy istnieje jakiś wektor prowadzący z tego wierzchołka do innego. Jeśli tak, to go oznacz i idź wraz z jego zwrotem. Jeśli nie, to idź do kroku 6.
Podążając wraz ze zwrotem tego wektora, wybierz kolejny wierzchołek i powróć do kroku 3.
Jeśli lista L zawiera więcej niż jeden element, to usuń z niej ostatni. Jeśli dalej będzie zawierać więcej niż jeden, wybierz ostatni i wróć do kroku 4.
Jeśli lista N nie jest pusta, to wróć do kroku 2. W przeciwnym wypadku algorytm kończy swoje działanie, co oznacza, że w systemie nie ma blokady.
Niech będzie w systemie sytuacja, którą opisuje graf przedstawiony na rys. 26. Interpretacja strzałek i oznaczeń jest analogiczna do tej, przyjętej na rys. 25.
Rysunek 26. Przykładowa alokacja zasobów w systemie.
Krok 1: N = { R1, T1, R2, T2, R3, T3, R4, T4, T5, R5, T6 }
Krok 2: L = { <pusty> }; wybierz wierzchołek R1
Krok 3: L = { R1 }; L nie zawiera cykli; N = { T1, R2, T2, R3, T3, R4, T4, T5, R5, T6 }
Krok 4: Z R1 prowadzi wychodzi tylko jeden wektor.
Krok 5: Wybierz wierzchołek T1 i powróć do Krok 3.
Krok 3: L = { R1, T1 }; N = { R2, T2, R3, T3, R4, T4, T5, R5, T6 }; L nie zawiera cykli.
Kroki 3 do 5 są powtarzane, aż trafiony zostaje wierzchołek T3, w którym lista L = { R1, T1, R2, T2, R4, T3 }, natomiast lista N = { R3, T4, T5, R5, T6 }. Z wierzchołka T3 prowadzą dwa wektory. Jeśli zostanie wybrany dolny, prowadzący do R5, to L = { R1, T1, R2, T2, R4, T3, R5 }. Z R5 również wychodzą dwa wektory. Gdy zostanie wybrany prawy, prowadzący do T6, to L = { R1, T1, R2, T2, R4, T3, R5, T6 }.
Krok 4: Wierzchołek T6 nie ma dalszych połączeń (nie wychodzi z niego żadna strzałka). Kontynuuj do kroku 6.
Krok 6: Usuń T6 z listy L; L = { R1, T1, R2, T2, R4, T3, R5 }; wybierz ostatni i powróć do Krok 4.
Krok 4: Wybierz nieoznaczony wektor i podążaj za jego zwrotem. W tym przypadku w kierunku wierzchołka T5.
Krok 5: Wybierz wierzchołek T5 i wróć do Krok 3.
Krok 3: L = { R1, T1, R2, T2, R4, T3, R5, T5 }; N = { R3, T4 }; L nie zawiera cykli.
Krok 4: Z T5 wychodzi tylko jeden wektor.
Krok 5: Wybierz wierzchołek R3 i powróć do Krok 3.
Krok 3: L = { R1, T1, R2, T2, R4, T3, R5, T5, R3 }; N = { T4 }; L nie zawiera cykli.
Krok 4: Z R3 wychodzi tylko jeden wektor.
Krok 5: Wybierz wierzchołek T1 i powróć do Krok 3.
Krok 3: L = { R1, T1, R2, T2, R4, T3, R5, T5, R3, T1 }; Wierzchołek T1 już się znajduje na liście L. Graf zawiera cykl, czyli w systemie istnieje blokada. Zakończ działanie algorytmu.
Węzły biorące udział w blokadzie, są „otoczone” przez węzeł T1, czyli jest to następujący zbiór: { T1, R2, T2, R4, T3, R5, T5, R3}. Analizując rys. 26, można zauważyć, że istnieje na nim jeszcze jeden cykl opisany przez sekwencję: {R2, T2, R4, T3}. To, że nie została ona wykryta przez algorytm jako pierwsza, zależne jest od jego konkretnej implementacji, a przede wszystkim kolejności sprawdzania wierzchołków.
Jest to, jak widać dość prosty algorytm, w którym zakłada się, że każde zadanie zgłasza żądania dostępu do zasobów oraz nie może być kontynuowane, jeśli wszystkie potrzebne nie zostaną mu przydzielone. W systemach czasu rzeczywistego, niestety najczęściej takie żądania są bardziej skomplikowane. Czasami pojawiają się pewne alternatywy w postaci zdania: „chcę dostępu do zasobu (A lub B) i do zasobu (C lub D)”. Wówczas wykrycie zakleszczenia nie jest już takie trywialne.
Po wykryciu blokady, kolejnym etapem jest próba jej wyeliminowania. Nie ma jednego cudownego sposobu na rozwiązanie takiego problemu. W zależności od warunków, system może próbować „odebrać” części zadaniom zasoby, przydzielić je innym. Takie postępowanie może znacznie wpłynąć na opóźnienia czasowe co z kolei bardzo często prowadzi do powstania różnego rodzaju błędów bądź też przyczynić się do powstania niespójnych obszarów danych. Oczywiście te skutki można starać się minimalizować np. poprzez implementowanie procedur odzyskujących dane i czuwających nad poprawnym wykonywaniem zadań.
Dla sytuacji przedstawionej na rys. 26, algorytm usuwający blokadę, może próbować w pierwszych krokach odebrać zasób R2 od zadania T2 a później przydzielić go do T1. Kiedy T1 się zakończy, zwolni zasoby R1, R2 i R3 oraz umożliwi dalszą pracę zadaniu T5, które po zakończeniu zwolni R5. Kolejne procesy jak T6, T3, T4 i T5 również będą miały możliwość wykonania się.
Oczywiście, jak to w życiu bywa, lepiej zapobiegać niż leczyć. Nawet najlepsze metody niosą ze sobą pewne ryzyko opóźnień czy utraty danych lub spadek wydajności całego systemu. W czasie projektowania aplikacji, należy unikać rozwiązań typu: „zajmij wszystkie potrzebne i wolne zasoby oraz czekaj na resztę w nieskończoność”. Zadania powinny najpierw sprawdzać czy to, czego oczekują, jest w danej chwili dostępne oraz natychmiast, kiedy już nie korzystają z jakiś zasobów, zwolnić je. Aby uniknąć zakleszczenia cyklicznego, przedstawionego na rys. 25, można również zasoby zorganizować w hierarchię oraz zaimplementować pewien protokół ich przydziału. Mógłby on polegać np.: na tym, że zadanie, które uzyska dostęp do jakiegoś zasobu, w kolejnych krokach albo musi ten zasób zwolnić lub starać się tylko i wyłącznie o zasoby, stojące wyżej w hierarchii.
Inwersja priorytetów
Każdy program szeregujący procesy w systemie opiera swoje działanie o jakiś algorytm i np. dostarcza procesorowi zadanie, którego priorytet jest w danej chwili najwyższy. Zadania korzystają najczęściej z wielu zasobów. Jeśli te o różnych priorytetach mają jakiś wspólny mianownik w postaci zasobu dzielonego, to okazuje się, że może dojść do zjawiska, określanego jako inwersja priorytetów (ang. priority inversion). Ogólnie polega ono na tym, że zadanie o niskim priorytecie, zajmując zasób dzielony, zmusza zadanie o wyższym priorytecie do przejścia w stan „blokowane” i oczekiwania na zwolnienie tego zasobu. Taka sytuacja prowadzi do wielu niebezpieczeństw, które w najgorszym wypadku mogą prowadzić do powstania w systemie sporych anomalii czasowych.
Dwa przykładowe scenariusze powstania inwersji priorytetów są przedstawione na rys. 27 i rys. 28.
Rysunek 27. Inwersja priorytetów (dwa zadania).
Jak widać na powyższym rysunku, w systemie istnieją dwa zadania o różnych priorytetach, które mają wspólny mianownik. Jest nim zasób, do którego oba prędzej czy później żądają dostępu. W chwili t1, zadanie o niskim priorytecie zajmuje zasób i zaczyna wykonywać jakieś obliczenia. W chwili t2, w systemie pojawia się zadanie o wyższym priorytecie i natychmiast scheduler przydziela mu czas procesora. Jednakże, już w chwili t3, zadanie to, ponieważ żąda dostępu do zasobu już zajętego przez inne zadanie, przechodzi do stanu „blokowane” i oddaje procesor, co umożliwia dalsze wykonywanie się zadania o niższym priorytecie. Właśnie ten moment jest początkiem tzw. inwersji priorytetów, która kończy się wraz z nastaniem chwili t4, w której zasób dzielony jest zwalniany. Wówczas ponownie scheduler przydziela procesor do zadania najważniejszego, które kończy swoje działanie w chwili t5, zwalnia zasób oraz pozwala na dokończenie swoich prac wszystkim mniej istotnym zadaniom.
Oczywiście taka sytuacja może znacznie opóźnić czas wykonania zadań najbardziej krytycznych, jednakże w większości przypadków, nie są one ogromne i w zależności od systemu, mogą być tolerowane. Najważniejszym jest fakt, że opóźnienia te można wyliczyć.
O wiele groźniejsza jest sytuacja, w której do przedstawionego na rys. 27 scenariusza wkroczyłoby dodatkowe zadanie o pośrednim priorytecie, nie dzielące z pozostałymi żadnych zasobów. Jest ona ukazana na rys. 28.
Rysunek 28. Inwersja priorytetów (nieznany czas trwania).
Jak widać, do inwersji priorytetów dochodzi, tak jak poprzednio, w chwili t3. Wówczas procesor jest ponownie przyznawany do zadania o najniższym priorytecie. Niestety, w systemie pojawia się w chwili t4 zadanie o średnim priorytecie, które przejmuje procesor. Ponieważ nie dzieli ono z żadnym innym zadaniem zasobów, to będzie się wykonywać do chwili swojego zakończenia. Prowadzić to może w niektórych przypadkach do katastrofalnych skutków jak np. całkowite uniemożliwienie dokończenia się zadań najbardziej krytycznych. Bardzo istotnym jest fakt, że czas pomiędzy chwilami t4 i t5 jest nieznany. Może się wtedy w systemie pojawić wiele zadań o pośrednim priorytecie oraz każde wprowadzić dodatkowe opóźnienia. Do takiej właśnie sytuacji doszło podczas misji Pathfindera na Marsie w lipcu 1997 roku.
Powyższe przykłady jasno przedstawiają, jak bardzo niekorzystnym zjawiskiem jest inwersja priorytetów. Niestety całkowite jej wyeliminowanie jest praktycznie niemożliwe. Można jedynie starać się, by występowała jak najrzadziej. W literaturze (patrz np. [5], [7], [8]) można spotkać wiele rozwiązań, które całkowicie eliminują sytuację pokazaną na rys. 28 (nieznany czas trwania inwersji priorytetów) oraz podejmują próbę minimalizacji czasu trwania inwersji priorytetów. Wszystkie one proponują jakiś model sterowania dostępem do zasobów (protokoły).
Jednym z nich, może nie najdoskonalszym, bo jak się później okaże stwarzającym możliwość powstania zakleszczenia procesów, jest tzw. protokół z dziedziczeniem priorytetów (ang. priority inheritance protocol). Zasada jego działania polega na podnoszeniu priorytetu zadania, które zajmuje zasób dzielony, do poziomu priorytetu tego zadania, które zgłasza żądanie dostępu do tego zasobu. Kiedy zadanie zwalnia zasób, jego priorytet jest obniżany do swojej pierwotnej wartości. Takie podejście całkowicie eliminuje sytuację, w której zadanie o niskim priorytecie zostaje wywłaszczone przez zadanie o średnim priorytecie w swojej sekcji krytycznej. Wówczas sytuacja przedstawiona na rys. 28, wyglądałaby następująco (rys. 29):
Rysunek 29. Protokół dostępu do zasobu z wykorzystaniem dziedziczenia priorytetów.
Na początki, w chwili t1, zadanie o najniższym priorytecie, jako pierwsze zajmuje zasób współdzielony oraz wykonuje swoje operacje. Zostaje ono wywłaszczone w chwili t2 przez zadanie o średnim priorytecie, które z kolei przechodzi do stanu „blokowane” w chwili t3, ponieważ zgłasza żądanie dostępu do zajętego już zasobu. Wówczas priorytet zadania o najniższym priorytecie zostaje zwiększony oraz kontynuuje ono swoje obliczenia. Ponieważ algorytm procesu szeregującego opiera się o wartości priorytetów, istnieje możliwość ponownego wywłaszczenia przez zadanie o jeszcze wyższym priorytecie. Taka sytuacja ma miejsce w chwili t4. Oczywiście scenariusz się powtarza i kiedy zadanie o najwyższym priorytecie jest blokowane w chwili t5, zadanie pierwotnie o najniższym priorytecie znowu ma podwyższany priorytet oraz kontynuuje swoją sekcję krytyczną, po czym w chwili t6 zwalnia zasób. Jego priorytet jest automatycznie zmniejszany do wartości początkowej (tej z chwili t1) i procesor przejmuje zadanie, którego w tej chwili priorytet jest najwyższy.
Protokół ten, ma bardzo poważną wadę. W chwili t2 zadanie o średnim priorytecie może zająć zasoby wymagane przez mające się pojawić zadanie o najwyższym priorytecie. To z kolei, może w chwili t4 zaryglować dostęp do jakiś zasobów wymaganych przez zadanie o pośrednim priorytecie. Efekt jest oczywisty i objawia się w chwili t6 powstaniem zakleszczenia. Oba te zadania nie będą mogły być kontynuowane.
Inną metodą, minimalizującą zjawisko inwersji priorytetów, jest stosowanie protokołu wykorzystującego tzw. pułap priorytetów (ang. ceiling priority protocol). Jej podstawą, jest wcześniejsze określenie zasobów wymaganych przez konkretne zadania oraz priorytetów tych zadań. Idea jest następująca: każdy zasób ma przypisany tzw. pułap priorytetów. Jego wartość jest określana przez najwyższy priorytet spośród zadań, które mogą o niego konkurować. Wówczas, kiedy zadanie zarygluje dostęp do takiego zasobu, jego priorytet jest zwiększany do wartości pułapu tego zasobu. Po zwolnieniu zasobu, zadanie ma przywracany priorytet do wartości początkowej. Można powiedzieć, że wszystkie zadania, których wspólnym mianownikiem jest dany zasób, wykonują swoje sekcje krytyczne na tym samym poziomie ważności. Działanie tego protokołu najlepiej przedstawia rys. 30.
Rysunek 30. Pułap priorytetów.
Jak widać na powyższym rysunku, w chwili t1, działa proces o niskim priorytecie. Do czasu zajęcia zasobu współdzielonego, może zostać wywłaszczony przez zadanie o wyższym priorytecie. Wraz z zablokowaniem dostępu do zasobu, w chwili t2, jego priorytet zostaje zwiększony do wartości pułapu tego zasobu. To gwarantuje mu, że będzie mógł pomyślnie zakończyć swoją sekcję krytyczną. Jest zadaniem o najwyższym priorytecie spośród tych, które mogą zażądać dostępu do tego samego zasobu. Po zwolnieniu tego zasobu, jego priorytet jest przywracany i wówczas scheduler może odebrać mu procesor. Może to wystąpić nawet, jeśli zadanie jeszcze nie zakończyło swoich prac.
Protokół ten, ma szereg zalet. Do najważniejszych, z pewnością zalicza się całkowita eliminacja wystąpienia blokady. Jest on również przejrzysty oraz przewidywalny.
Jedynym niebezpieczeństwem jakie grozi, to zwiększony czas odpowiedzi. Można wyobrazić sobie sytuację, w której sekcja krytyczna zadania, które jako pierwsze zajmie zasób jest na tyle długa, że znacznie opóźni lub co gorsze uniemożliwi wykonanie innych zadań.
Inne protokoły dostępu do zasobów współdzielonych wprowadzają różnego rodzaju modyfikacje do tych przedstawionych powyżej. Mogą one polegać np.: na tym, że pułap zasobu nie jest określany tylko przez zadania żądające do niego dostępu, lecz również przez wartości pułapów dla innych zasobów.
RTLinux
RTLinux, to rygorystyczny system czasu rzeczywistego. Jego początki sięgają lat dziewięćdziesiątych, kiedy to Victor Yodaiken, profesor politechniki w Nowym Meksyku doszedł do wniosku, że największym problemem w weryfikacji i formalnym dowodzeniu poprawności programów działających w czasie rzeczywistym jest udowodnienie, że komponenty, które nie działają w czasie rzeczywistym, nie zakłócają działania tych czasu rzeczywistego. Swoje badania skupiał nad zagadnieniami współistnienia obu tych komponentów w ramach jednego systemu komputerowego. Tak oto zrodziła się idea dwóch systemów operacyjnych - miniaturowego, pracującego w czasie rzeczywistym i rozbudowanego, do wszystkich innych zadań. Oba miałyby ze sobą współpracować.
Pierwsza wersja RTLinuksa ujrzała światło dzienne w 1995 roku.. Wydano ją na licencji GPL (szerzej na temat jej treści np. [12]) i szybko zapewniła sobie uznanie, zarówno wśród amatorów, jak i profesjonalistów, stanowiąc darmową alternatywą dla kosztujących astronomiczne sumy komercyjnych systemów operacyjnych czasu rzeczywistego. Z czasem, twórca RTLinuksa założył firmę FSMLabs (ang. Finite State Machine Labs), odszedł z uczelni oraz prace nad rozwojem systemu przybrały dość imponującego tempa, czego owocem są jego dwie wersje: bezpłatna RTLinux (GPL) oraz komercyjna - RTLinux Pro
Aby ułatwić programistom tworzenie nowych aplikacji dla swojego środowiska oraz umożliwić korzystanie z istniejącego dorobku, RTLinux jest oczywiście zgodny z pewnymi normami, które określa profil POSIX 1003.13/PSE51 (ang. Minimal Realtime Environment). Profil ten, to dokument opisujący bardzo prosty system czasu rzeczywistego, który posiada interfejs POSIX dla programowania zadań czasu rzeczywistego i nie posiada systemu plików (np. ze względu na zbyt czasochłonne przetwarzanie związane z operacjami wejścia - wyjścia). RTLinux, dla zwiększenia swoich możliwości oraz lepszego wykorzystania różnych platform sprzętowych, wprowadza pewne nowości, jak np. możliwość określenia procesora, na którym ma działać zadanie czasu rzeczywistego, opcjonalna obsługa koprocesora oraz możliwość działania zadań w trybie periodycznym.
Kolejne podrozdziały przybliżą świat systemu RTLinux w wersji bezpłatnej (GPL). Zostanie przedstawiona jego architektura oraz implementowane w nim mechanizmy. Całość wzbogaci ogólny sposób instalacji pakietu RTLinux oraz przykłady jego użycia w przemyśle.
Instalacja i uruchomienie
Aby zainstalować pakiet RTLinux, potrzebne są jego źródła oraz odpowiadająca im łata na jądro Linuksa. Nie ma specjalnych wymagań sprzętowych, poza procesorem klasy Pentium, gdyż zawierają one zegar wysokiej rozdzielczości. Najnowsze źródła RTLinuksa oraz różne łaty autorstwa wielu osób można uzyskać z pakietu rtlinux_contrib, który w chwili pisania tej pracy (06.2004) jest dostępny w wersji: 3.2-pre2. Całość można pobrać ze strony FSMLabs (www.fsmlabs.com), po wypełnieniu krótkiego formularza. Można tam również znaleźć informacje o tym, jak otrzymać wersję demonstracyjną pakietu RTLinkus Pro. Godnym polecenia serwerem, z którego również można ściągnąć odpowiednie łaty lub cały pakiet (również w wersji mini, dyskietkowej) jest ftp://ftp.opentech.at/pub/rtlinux/.
Po rozpakowaniu RTLinuksa, nałożeniu na jądro łaty (znajdującej się w katalogu patches) i sprawdzeniu wymagań dla środowiska programistycznego (określonego przez plik documentation/changes), można przystąpić do jego instalacji. Ważną sprawą jest to, że jądro systemu Linuks musi być wcześniej skompilowane z włączoną opcją CONFIG_RTLINUX (jest ona standardowo włączana dla procesorów architektury i386) oraz włączenie obsługi ładowalnych modułów jądra (ang. enable loadable module support - loadable module support). Zaleca się również wyłączenie zaawansowanego zarządzania energią (ang. use real mode APM BIOS call to power off - general setup). Reszta ustawień jądra nie jest związana z pakietem RTLinux.
Pierwszym krokiem do uruchomienia RTLinuksa, jest jego odpowiednia konfiguracja. Przeprowadza się ją za pomocą specjalnego narzędzia, które jest uruchamiane z katalogu zawierające źródła pakietu przy pomocy jednego z następujących poleceń:
make config - wyświetli dialog
make menuconfig - wyświetli menu tekstowe
make xconfig - menu z interfejsem graficznym
Umożliwia ono takie rzeczy, jak np. włączenie POSIX-owego protokołu utrzymywania pułapu priorytetu, włączenie dostępu do urządzenia /dev/mem z poziomu zadań czasu rzeczywistego, włączenie kompilacji modułów pakietu z informacjami dla debuggera, umożliwienie używania operacji zmiennoprzecinkowych w zadaniach czasu rzeczywistego. Dodatkowo sekcja Drivers pozwala skonfigurować np. liczbę dostępnych kolejek RT_FIFO, czyli urządzeń /dev/rtf* (domyślnie jest ich 64), skonfigurować ich rozmiar (domyślnie jest to 2kB na kolejkę), liczbę prealokowanych kolejek (domyślnie 16) oraz takie sprawy jak np. umożliwienie korzystania ze sterownika czasu rzeczywistego obsługi portu szeregowego RS-232C, co jest wykorzystywane np. przez różnego rodzaju systemy pomiarowe.
Kolejnym krokiem, jest kompilacja pakietu poprzez wydanie polecenia make, skopiowanie go w odpowiednie miejsce przy pomocy polecenia make install oraz stworzenie plików urządzeń wykorzystywanych przez RTLinuksa. Za to ostatnie odpowiada polecenie: make devices. Stworzy ono kolejki RT-FIFO (dev/ftf*) oraz pamięć dzieloną (dev/mbuff).
Ostatnim wymogiem jest ponowne uruchomienie systemu i załadowanie do pamięci jądra wzbogaconego o odpowiednie patche. Oczywiście RTLinux nie „startuje” automatycznie. Trzeba załadować jego moduły. Można tego dokonać np. przy pomocy odpowiednich skryptów powłoki. Są one dostarczane razem z pakietem oraz ze względu na szereg zależności pomiędzy poszczególnymi jego częściami, pozwalają zaoszczędzić ogrom czasu. Pierwszym z nich, jest skrypt rtl-config, który udostępnia informacje dotyczące np. położenia takich elementów jak pliki nagłówkowe, wersji systemu czy flag wymaganych przy kompilacji modułów czasu rzeczywistego. Korzysta z niego inny skrypt o nazwie rtlinux (z parametrami stop, start, status), który jest odpowiedzialny za prawidłowe uruchomienie systemu RTLinux. Po jego uruchomieniu, na ekranie monitora pojawiają się informacje o załadowanych modułach systemowych oraz ewentualnie (jeśli są obecne) modułach użytkownika.
Poszczególne zadania w systemie RTLinux są realizowane przez odpowiednie moduły:
rtl - główny moduł systemu. Zawiera obsługę wirtualnego systemu przerwań;
rtl_time - zapewnia obsługę zegara i mierzenia czasu (zależny od sprzętu);
rtl_sched - implementuje domyślną jednostkę szeregującą;
rtl_posixio - dostarcza możliwości wykonywania na urządzeniach RTLinux prostych operacji jak np. open, close, read, write. Są one zgodne z POSIX;
rtl_fifo - obsługa kolejek czasu rzeczywistego;
mbuff - obsługa pamięci współdzielonej (opcja);
psc - (Programmable Signal Control) umożliwia wykorzystanie sygnałów czasu rzeczywistego (funkcji obsługi przerwań) w zwykłych procesach Linuksa (opcja);
rt_com - ułatwia korzystanie z portu szeregowego w czasie rzeczywistym.
Dopiero teraz, kiedy już działa w systemie jądro systemu RTLinux, można uruchamiać moduły zawierające zadania czasu rzeczywistego, zaprogramowane przez użytkownika. Można do tego celu użyć skryptu rtlinux przy pomocy składni: rtlinux start nazwa_modułu. Ich kompilacja natomiast, może zostać przeprowadzona przy pomocy pliku Makefile, którego treść może wyglądać jak ta, przedstawiona poniżej (listing 1):
Listing1. Makefile dla kompilacji modułów RTLinux.
# kod zrodlowy modulu zawarty w pliku: nazwa_modulu.c
all: nazwa_modulu.o
include /usr/rtlinux/rtl.mk
clean:
rm -f *.o
include $(RTL_DIR)/Rules.make
Architektura
Jak już we wstępie tego rozdziału wspomniano, RTLinux oddziela mechanizmy systemu operacyjnego czasu rzeczywistego od systemu operacyjnego ogólnego przeznaczenia. Jego twórcy, zamiast wyszukiwać i poprawiać w kodzie Linuksa fragmenty, które mogą stwarzać problemy przy wykonywaniu zadań czasu rzeczywistego, zdecydowali się użyć dwóch różnych jąder do różnych zadań. Oryginalne jądro Linuksa zaprojektowano w ten sposób, aby wszystkie aplikacje mogły się wykonać oraz aby ich przetwarzanie było w miarę wydajne - uśrednione. Takie podejście bardzo sprzyja wszelkim zastosowaniom biurowym czy przy serwerach. Oczywiście od wersji 2.0 tegoż jądra, wprowadzono dodatkowe kolejki, przeznaczone do szeregowania procesów czasu rzeczywistego, zgodne z POSIX 1003.1b. Procesy w trybach SCHED_FIFO i SCHED_RR mają wyższy priorytet niż zwykłe zadania, czyli te działające w trybie SCHED_OTHER. Niestety pomimo tych ulepszeń, zwykły Linux może zostać zaklasyfikowany co najwyżej do systemów czasu rzeczywistego o charakterze nierygorystycznym. Wynika to przede wszystkim ze zbyt dużych opóźnień obsługi przerwań, częstego blokowania ich obsługi w sekcjach krytycznych jądra oraz co najgorsze, nie dających się przewidzieć opóźnień związanych z obsługą pamięci wirtualnej. Cechą charakterystyczną „zwykłego” Linuksa jest jeszcze to, że nie można wywłaszczyć procesu znajdującego się w trybie jądra. Upraszcza to oczywiście implementację, gdyż jądro nie musi być tzw. wielowejściowe (ang. reentrant). Powstają jednak kolejne źródła opóźnień w systemie, czego zadania czasu rzeczywistego nie mogą zaakceptować.
RTLinux działa w ten sposób, że jądro „zwykłego” Linuksa jest traktowane jako zadanie i działa pod kontrolą niewielkiego i prostego systemu operacyjnego czasu rzeczywistego. Linux staje się praktycznie tzw. zadaniem tła (ang. idle task) dla RTLinuksa, które jest wykonywane tylko wtedy, kiedy nie istnieje jakiekolwiek zadanie czasu rzeczywistego ubiegające się o procesor. Założenie jest takie, że zadania Linuksa nie są w stanie zablokować przerwań ani też zapobiec wywłaszczeniu siebie. Taki cel osiągnięto dzięki implementacji programowej warstwy, która emuluje sprzętowy mechanizm kontroli przerwań. W efekcie pozbawiono „zwykłego” Linuksa możliwości blokady przerwań sprzętowych. Jeśli podejmie taką próbę, RTLinux natychmiast przechwytuje ten fakt, zaznacza odpowiednio oraz ponownie oddaje sterowanie do jądra Linuksa. Bez względu na tryb pracy, jak tryb użytkownika, systemowy czy nawet sekcja krytyczna jądra, Linux absolutnie nie jest w stanie zwiększyć czasu odpowiedzi na przerwanie czasu rzeczywistego. Jeśli ono się pojawi, jest przechwytywane przez RTLinuksa i to on decyduje co dalej z nim zrobić. Zależnie od jego natury, możliwe są dwa rozwiązania. Jeśli w systemie istnieje zarejestrowana procedura jego obsługi oraz pochodzi ona z zadania czasu rzeczywistego, to jest wywoływana. Jeśli natomiast przerwanie to ma być obsłużone przez „zwykłego” Linuksa, to wówczas jest kolejkowane i oznaczane jako oczekujące. Zostanie obsłużone dopiero, kiedy RTLinux wykryje, że jądro „dużego” systemu podjęło próbę ponownego włączenia obsługi przerwań.
Ogólna, schematyczna budowa systemu RTLinux została przedstawiona na rys. 31. Widać na nim wyraźny podział na część odpowiedzialną za prawidłowe przetwarzanie w czasie rzeczywistym oraz tą, odpowiedzialną za wszelkie dodatki jak np. system plików. Takie podejście umożliwiło stworzenie architektury w pełni funkcjonalnej i nadającej się do zastosowań bardziej powszechnych niż tylko czysto obliczeniowych. Dodatkowo widać na nim szereg modułów. Daje to możliwość optymalizowania każdej części systemu oddzielnie. Poza tym, każdy może zaimplementować np.: własny program szeregujący oraz podmienić z tym oryginalnym.
Rysunek 31. Schematyczna budowa systemu RTLinux.
RTLinux został tak zaprojektowany, aby nie musiał czekać na zwolnienie jakichkolwiek zasobów przez Linuksa. Do wymiany danych pomiędzy dwoma systemami są stosowane mechanizmy oparte o kolejki i ze strony RTLinuksa nie są one blokujące. Kluczową zasadą, jaką kierowali się twórcy tego systemu jest to, aby pozostawić go jak najprostszym i możliwie najmniejszym. Efektem jest to, że RTLinux dostarcza jedynie bezpośredniego dostępu do sprzętu dla wątków czasu rzeczywistego, czasomierzy, mechanizmów komunikacji międzyprocesowej (zwłaszcza pomiędzy zadaniami czasu rzeczywistego i zwykłymi) oraz jednostkę odpowiedzialną za szeregowanie zadań. Pozostałe obowiązki, jak np. inicjalizacja sprzętu czy nawet samego RTLinuksa, to już nie jego sprawa.
Emulacja przerwań
Główna idea tego mechanizmu została już poniekąd zaprezentowana. Jak wspomniano powyżej, jądro Linuksa chroni swoje sekcje krytyczne poprzez wyłączanie obsługi przerwań. Dokonuje tego przy pomocy instrukcji cli, która zeruje znacznik IF w rejestrze stanu procesora. Wówczas przerwania zostają zablokowane. Przywrócenie ich obsługi jest wykonywane poprzez wywołanie instrukcji sti, której działanie jest odwrotne do poprzedniej i odpowiada ustawieniu znacznika IF. Aby powrócić z procedury obsługi przerwania używana jest instrukcja iret. RTLinux, jako nadrzędny proces w całym systemie, implementuje dość ciekawy mechanizm, dzięki któremu ta „samowola” jądra Linuksa jest ograniczana. Otóż, wystąpienie w jego kodzie wszystkich wspomnianych wcześniej instrukcji zostało zastąpione przez makra, odpowiednio: S_CLI (listing 2), S_STI (listing 3) oraz S_IRET (listing 4). W ten oto bardzo prosty sposób osiągnięto główny cel - wykrycie każdej próby zarządzania przez jądro Linuksa obsługą przerwań i podjęcie odpowiedniego działania przez emulator.
Listing 2. Makro S_CLI.
:
movl $0, SFIF
Makro to, jak widać, nie ingeruje w wartość znacznika stanu procesora (IF), tylko zeruje odpowiednią zmienną emulatora (SFIF). Określa ona zachowanie emulatora w chwili wystąpienia przerwania. Wówczas, jeśli ma wartość zero (SFIF == 0) i pojawiające się przerwanie nie posiada procedury obsługi czasu rzeczywistego, to informacja o nim jest kolejkowana. W przeciwnym wypadku, gdy SFIF = 1, wywoływana jest odpowiednia procedura obsługi przerwania, implementowana przez jądro Linuksa.
Listing 3. Makro S_STI.
S_STI:
sti
pushfl
pushl $KERNEL_CS
pushl $1f
S_IRET
1:
Powyższy kod przedstawia zwykłą procedurę włączania przerwania. Oczywiście na początku odkładane są na stos flagi procesora, rejestr segmentowy jądra oraz adres powrotu. Później uruchomiona zostaje emulacja wszystkich zakolejkowanych przerwań (jeśli istnieją jakieś oczekujące), po czym zmienna SFIF przyjmuje wartość 1. Odpowiedzialne jest za to kolejne makro, przedstawione poniżej (listing 4).
Listing 4. Makro S_IRET.
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
S_IRET
Jest to zdecydowanie najistotniejsze makro. Można powiedzieć, że wykonuje całą „czarną robotę”. Na początku odkładane są na stos używane rejestry oraz rejestr segmentu danych ustawiany jest na segment jądra, aby uzyskać dostęp do zmiennych globalnych. Następnie, przy pomocy rejestru edx, przeglądana jest maska bitowa SFREQ w poszukiwaniu ustawionego bitu. Reprezentuje on oczekujące, niezamaskowane przerwanie. Jeśli żadne nie zostanie znalezione, to następuje bezpośredni powrót z przerwania. W przeciwnym razie, wykonywany jest skok do procedury obsługi przerwania. Ponieważ ostatnią instrukcją procedury jest wywołanie S_IRET, wszystkie oczekujące przerwania zostaną obsłużone sekwencyjnie. Należy zauważyć jednak fakt, że jeśli w chwili poszukiwania kolejnego zapamiętanego przerwania pojawi się w systemie nowe przerwanie, to jego obsługa zostanie nieznacznie opóźniona. Na ten dodatkowy czas składa się jedynie okres do wywołania makra S_STI lub S_IRET.
RTLinux potrafi rozróżnić dwa rodzaje przerwań: twarde i miękkie. Pierwsze, to tzw. przerwania sprzętowe (czasu rzeczywistego) i są skojarzone z odpowiednimi liniami IRQ programowalnego kontrolera przerwań. Mają ograniczone możliwości i nie wszystkie funkcje jądra mogą zostać wywołane z ich poziomu. Przerwania miękkie są wywoływane programowo i mogą wprowadzać do systemu dodatkowe opóźnienia. Ich niewątpliwą zaletą jest nieograniczony dostęp do standardowych funkcji jądra. Zadania czasu rzeczywistego mogą oprócz blokowania i włączania obsługi przerwań (co może stanowić źródło wielu niebezpieczeństw) także instalować procedury ich obsługi. Służy do tego odpowiednie API.
Dla przerwań twardych, aby zainstalować jego uchwyt, wykorzystywana jest funkcja: rtl_request_irq(), która przyjmuje dwa parametry. Pierwszy określa numer linii zgłoszeń przerwania (int irq), a drugi to wskaźnik do funkcji wywoływanej. Ma on postać:
( int (*handler)(unsigned int irq, struct pt_regs *regs) ).
Aby dezaktywować takie przerwanie, wywołuje się funkcję: rtl_free_irq() i jako parametr należy podać numer odpowiedniego przerwania.
Ponieważ na początku wywołania funkcji obsługującej dane przerwanie, aby zapewnić niezakłócony przebieg jej wykonania, standardowo blokowane są przerwania sprzętowe, w celu umożliwienia obsługi kolejnych, należy wywołać funkcję: rtl_hard_enable_irq(), podając jako parametr numer przerwania.
Dla rozważań czysto teoretycznych, przykładowy kod pokazujący powyższą zasadę może wyglądać następująco (zob. listing 5):
Listing 5. Pseudokod obsługi przerwań twardych dla RTLinux.
// inicjalizacja zadania czasu rzeczywistego:
{
...
rtl_request_irq(8, irq_handler);
…
// zaprogramowanie kontrolera przerwań, by generował // sygnał z określoną częstotliwością
...
rtl_hard_enable_irq(8);
…
}
// procedura obsługi przerwania
unsigned int irq_handler(unsigned int irq, struct pt_regs *regs)
{
...
rtl_hard_enable_irq(8);
return 0;
}
// końcowe porządki
{
…
rtl_free_irq(8);
}
Przerwania miękkie, obsługuje się poprzez przydzielenie wektora przerwania oraz zarejestrowania funkcji jego obsługi. Zadanie to realizuje funkcja:
rtl_get_soft_irq( void (*handler)(int, void*, struct pt_regs*), const char *devname ).
Drugi jej parametr określa nazwę, jaka pojawi się w pliku /proc/interrupts. Funkcja zwraca numer przydzielonego wektora. Aby odinstalować przydzielony wektor, używa się funkcji: rtl_free_soft_irq().
Zadania w RTLinuksie i ich szeregowanie.
Struktura RTLinuksa wymusza pewną strukturę aplikacji tworzonych dla tego systemu. Muszą się one składać z dwóch fragmentów. Pierwszy, to tzw. komponent czasu rzeczywistego. Zawiera on właściwe zadania, które powinny być jak najprostsze. Mają one bezpośredni dostęp do zasobów sprzętowych. Należy zwrócić uwagę na fakt, że zadania RTLinuksa uruchamiane są we wspólnej przestrzeni adresowej (przestrzeń jądra). Mają maksymalny poziom uprzywilejowania. Ma to ogromną zaletę, gdyż szeregowanie ich, praktycznie sprowadza się jedynie do odtworzenia rejestrów procesora. Odpada narzut czasowy jaki wiąże się ze zmianą trybu uprzywilejowania procesora (np. zmiana z trybu systemowego na tryb użytkownika zajmuje kilkadziesiąt taktów procesora). Poza tym, zadania aby się komunikować, mogą wykorzystywać np. zmienne globalne. Drugi fragment, to komponent, na który nie są narzucone jakieś specjalne ograniczenia czasowe. Uruchamia on zwykłe procesy, które mogą się zająć np. operacjami wejścia - wyjścia (zapis do pliku, przesyłanie przez sieć, wizualizacja danych itp.).
Od wersji 3.0 systemu, komponenty czasu rzeczywistego mogą zostać wywołane również z poziomu programów użytkowników.
Ważną sprawą jest fakt, że programy czasu rzeczywistego muszą być implementowane jako ładowalne moduły o następującej strukturze:
pliki nagłówkowe;
zmienne globalne i definicje;
kod zadania czasu rzeczywistego;
kod inicjalizujący ( funkcja: int init_module() )
kod finalizujący ( funkcja: void cleanup_module() )
Głównymi obowiązkami kodu inicjalizującego są między innymi: inicjalizacja wszelkich struktur danych i kolejek czasu rzeczywistego (szerzej o nich - dalej w tym rozdziale) służących do komunikacji pomiędzy zwykłymi procesami Linuksa a zadaniami czasu rzeczywistego. Oczywiście znaleźć się tam też musi kod tworzący właściwe zadanie „rzeczywiste”, które będzie odpowiedzią np. na przerwanie (asynchroniczne) lub upływ zadanego czasu (synchroniczne).
Kod finalizujący, dokonuje ostatecznego „sprzątania”. Zatrzymuje zadania czasu rzeczywistego, zwalnia pamięć, zamyka pliki itp.
Należy pamiętać o tym, że dzięki maksymalnemu uproszczeniu zadań czasu rzeczywistego, pojawia się zysk w postaci wydajności ale i również pewne niebezpieczeństwo. Z powodu braku mechanizmów pamięci wirtualnej oraz ogólnej ochrony pamięci, każdy błąd w programie może zawiesić cały system. Nie jest również możliwe wykonanie prostego śledzenia ich wykonania.
Zawsze, kiedy mówi się o jakimś zagadnieniu programistycznym, najlepiej zobrazować wszystko przykładami. Najprostszą i jednocześnie tradycyjną w takich sytuacjach aplikacją jest oczywiście „Hello World!” - przedstawia ją listing 6.
Listing 6. „Hello World!” dla RTLinux.
#include <rtl.h>
#include <time.h>
#include <pthread.h>
pthread_t thread;
void *run(void *arg)
{
pthread_make_periodic_np( pthread_self(), gethrtime(), 1000 000 000);
while(1)
{
pthread_wait_np();
rtl_printf( “ Hello World!” );
}
}
int init_module()
{
return pthread_create( &thread, 0, run, 0 );
}
void cleanup_module()
{
pthread_delete_np(thread);
}
W efekcie kompilacji powstanie moduł jądra, który można uruchomić np. przy pomocy skryptu rtlinux lub polecenia insmod.
Część inicjalizacyjna modułu tworzy zadanie czasu rzeczywistego przy pomocy funkcji: pthread_create(). Ma ona następujący prototyp:
int pthread_create( pthread_t thread, pthread_attr_t attr, void (*run)(void*), void *arg);
thread - wskaźnik do zmiennej, do której zostanie przekazany identyfikator wątku
attr - atrybuty wątku. Gdy NULL, to przyjmowane są domyślne
void (*run)(void*) - ciało wątku - wskaźnik do metody wykonywanej przez wątek
arg - parametry dla metody ciała wątku
Jak widać, nie są ustawiane żadne szczególne atrybuty zadania, jak np. tryb jądra czy tryb użytkownika, ani też nic nie jest przekazywane do funkcji run.
Procedura cleanup_module() jest wykonywana na chwile przed usunięciem modułu z pamięci, np. przy pomocy polecenia rmmod. Jedynym jej zadaniem jest zatrzymać uruchomiony wątek, czyli wykonanie ostatecznego sprzątania.
Zdecydowanie najciekawszą częścią prezentowanego wyżej kodu, jest zawartość funkcji run. Na samym początku, powoduje ona, że aktualne zadanie staje się okresowym, będzie aktywowane co 1000 000 000 nanosekund (1 sekunda) . Dokonuje tego przy pomocy wywołania funkcji: pthread_make_periodic_np(), której prototyp wygląda następująco:
int pthread_make_periodic_np(pthread_t thread, hrtime_t start_time, hrtime_t period);
thread - zadanie o tym identyfikatorze stanie się gotowe do wykonania
start_time - czas rozpoczęcia wykonywania zadania
period - okres, co jaki zadanie ma być aktywowane (w nanosekundach)
Jak widać, jako pierwszy parametr podawany jest bieżący wątek (jego identyfikator zwraca funkcja: pthread_self() - zwraca z poziomu wątku ID samego siebie), a czas rozpoczęcia jego wykonywania określa funkcja: gethrtime(), która zwraca czas podawany w nanosekundach od ostatniej inicjalizacji systemu.
Następnie, w nieskończonej pętli następuje uśpienie wątku (funkcja: pthread_wait_np.()) oraz po upływie określonego wcześniej okresu, wypisanie tekstu na ekran. Należy tutaj zwrócić uwagę na fakt, że określane przez programistę ograniczenia czasowe są pewnego rodzaju przybliżeniem rzeczywistej sytuacji. RTLinux stara się maksymalnie wiernie je odwzorować ale jest to również zależne od rozdzielczości zegara systemowego. Poza tym, które zadanie w danej chwili uzyska dostęp do procesora w ostateczności decyduje planista (ang. scheduler).
Planista w systemie RTLinux implementuje algorytm szeregowania, opierający się o stałe priorytety zadań. Wybierane jest zawsze zadanie gotowe do wykonania i o najwyższym priorytecie (ang. Priority Based Rate Monotonic Scheduling Algorithm - RMS). Jeśli zaistniałaby sytuacja, że jest kilka zadań o tym samym, najwyższym priorytecie i są one jednocześnie gotowe do wykonania, to wybierane jest to, które zostało odnalezione jako pierwsze. Nie można tutaj mówić o jakimś podziale czasu procesora. Z założenia, zadanie powinno samo oddać procesor lub zostanie wywłaszczone przez inne o wyższym priorytecie. Pseudokod tego algorytmu został przedstawiony na listingu 7.
Cechą charakterystyczną RTLinuksa jest również to, że nie jest w nim budowana oddzielna kolejka procesów gotowych, czekających na przydział procesora. Wszystkie zadania znajdują się w jednej kolejce, a ich stan jest określany przez przydzieloną każdemu strukturę.
Listing 7. Algorytm RMS (ang. Rate Monotonic Scheduling).
rt_schedule()
{
for (każdy gotowy proces realtime)
wybierz proces o najwyższym priorytecie;
if (wybrano proces)
{
for (każdy wstrzymany proces realtime o wyższym priorytecie niż priorytet wybranego procesu)
wybierz proces, który powinien być wznowiony
najwcześniej;
if (wybrano wstrzymany proces)
ustaw zegar na czas "budzenia" procesu
}
else
wybierz "proces" jądra;
wznów wybrany proces;
}
wake_up()
{
dodaj proces do kolejki procesów gotowych;
if (priorytet "budzonego" procesu > priorytet procesu bieżącego)
rt_schedule();
}
Jak widać, scheduler jest uruchamiany tylko wtedy, gdy pojawia się taka potrzeba, a nie co jakiś ustalony przedział czasu. Efektem są mniejsze narzuty związane z jego działaniem.
Jest to standardowy algorytm używany przez RTLinuksa. Zgodnie z nim, im krótszy okres działania danego zadania, tym powinien być wyższy jego priorytet. Może ono być „okresowe”, tzn. jeśli nie będzie kolidować z „ważniejszym” zadaniem, to będzie uruchamiane w równych odstępach czasowych. Wadą tego algorytmu jest to, że nie zawsze gwarantuje on wykonanie się wszystkim zadaniom na czas. W pesymistycznym przypadku procesor jest wykorzystywany tylko w 69%.
Obecnie, dystrybucja RTLinuksa została wzbogacona o dodatkowy moduł, który implementuje planistę z dynamicznym przydziałem priorytetów. Główna idea jego działania polega na tym, że im zadanie ma bliższy nieprzekraczalny termin wykonania, tym otrzymuje wyższy priorytet. Ogromną wadą jest ogromny narzut związany z wyliczaniem priorytetów. Zaletą natomiast jest to, że procesor jest wykorzystywany w 100%. Pseudokod przedstawiający algorytm działania tego modułu zawiera listing 8.
Listing 8. Algorytm EDF (ang. Earliest Deadline First)
rt_schedule()
{
for (każdy gotowy proces realtime)
wybierz proces o najmniejszej wartości absolute_deadline;
if (wybrano proces)
{
for (każdy wstrzymany proces
wybierz proces, który powinien być wznowiony najwcześniej;
if (wybrano wstrzymany proces)
ustaw zegar na czas "budzenia" procesu
}
else
wybierz "proces" jądra;
wznów wybrany proces;
}
Należy się słowo wyjaśnienia, czym jest owa wartość absolute_deadline. Otóż, z każdym zadaniem czasu rzeczywistego jest związana struktura: rt_task_struct. Do najistotniejszych jej pól, można zaliczyć przede wszystkim:
state - stan procesu. Możliwe wartości to:
RT_TASK_READY - zadanie gotowe do wykonania.
RT_TASK_DORMANT - zadanie wstrzymane. Wznowi je inne zadanie.
RT_TASK_DELAYED - pole resume_time określa czas ponownego wznowienia
priority - wartość priorytetu zadania. Obecnie dla RTLinux jest ona z przedziału domkniętego obustronnie: [0, 1 000 000]. Wyższa wartość liczbowa oznacza wyższy priorytet.
resume_time - dla zadań periodycznych oznacza czas budzenia.
period - dla zadań periodycznych oznacza odstęp czasu pomiędzy kolejnymi jego przebudzeniami.
absolute_deadline - dodatkowe pole wprowadzone specjalnie dla potrzeb algorytmu EDF (listing 8). Określa ostateczny czas wznowienia zadania.
Na uwagę zasługuje fakt, że ani RMS, ani EDF nie gwarantują terminowego wypełnienia zadań nieokresowych (sporadycznych), które mogą pojawić się w dowolnym czasie. Do tego celu, stosuje się algorytmy np. Slot Shifting oraz Stack Stealing. Koncentrują się one na tego typu zadaniach oraz lepiej wykorzystują wolne cykle procesora pomiędzy zadaniami periodycznymi. Oczywiście nic nie stoi na przeszkodzie, aby użytkownik napisał swój własny algorytm szeregowania. Jego uruchomienie sprowadza się jedynie do podmiany zawartości funkcji rt_schedule() oraz ewentualnych zmian struktury zadania. Można tego dokonać „w locie” bez ponownej kompilacji jądra „zwykłego” Linuksa. Wystarczy usunąć z pamięci odpowiedni moduł i załadować nowy.
RTLinux jest przystosowany również do obsługi symetrycznej wieloprocesorowości (SMP - Shared Memory Multiprocessor machine). Można określić, na którym procesorze ma się uruchomić zadanie czasu rzeczywistego. Do każdego procesora przydzielana jest struktura rtl_sched_cpu_struct, która zawiera między innymi kolejkę zadań. Przydziału zadania do procesora dokonuje się przy pomocy funkcji:
pthread_attr_setcpu(thread, cpu_id).
Pojęcie czasu w RTLinuksie.
Jak wspomniano w rozdziale 8 tej pracy (pt. „Czas i jego odmierzanie”), poprawne odmierzanie upływu czasu w każdym systemie jest bardzo istotne, a już dla systemów czasu rzeczywistego jest szczególnie ważne. Standardowo, zegar systemowy Linuksa „tyka” 100 razy na sekundę, czyli odstęp pomiędzy kolejnymi przerwaniami zegarowymi wynosi średnio 10ms. Nie jest to zbyt duża rozdzielczość. Stanowi natomiast pewien kompromis na rzecz zmniejszenia kosztów związanych z przełączaniem kontekstu. Dla zadań czasu rzeczywistego, niejednokrotnie owe 10ms to zdecydowanie za mało. Dlatego też RTLinux wprowadza dwie koncepcje generowania przerwań zegarowych. Pierwsza, to standardowe przerwania periodyczne (RTL_CLOCK_MODE_PERIODIC), generowane z częstotliwością 100Hz i są emulowane dla jądra Linuksa. Druga, polega na generowaniu przerwania po upływie zadanego czasu (RTL_CLOCK_MODE_ONESHOT). Dzięki temu, w komputerach klasy PC (zawierają programowalny układ czasowy Intel 8254), możliwe jest uzyskanie rozdzielczości zegara na poziomie 1 mikrosekundy bez znacznego zwiększania kosztów związanych z obsługą tak częstych przerwań. Pojawiają się one tylko wtedy, kiedy są potrzebne zamiast w stałych odstępach czasu.
RTLinux został zaopatrzony w kilka zegarów, dzięki którym możliwe jest odmierzanie czasu. Funkcje API (zgodne z POSIX) zawarte są w pliku time.h oraz operują na czasie, którego format przedstawia poniższa struktura (listing 9):
Listing 9. Format czasu w POSIX.
struct timespec {
time_t tv_sec; // sekundy
long tv_nsec; // nanosekundy
};
Przykładowo, aby ustawić parametry wybranego zegara, używa się funkcji:
clock_settime( clockid_t clock_id, const struct timespec *tp).
Ustawia ona zegar, podany jako pierwszy parametr, na wartość wskazaną przez strukturę podaną jako drugi parametr. Wartości, będące pomiędzy dwoma kolejnymi, nieujemnymi wielokrotnościami rozdzielczości danego zegara (rozdzielczość zegara można pobrać przy pomocy funkcji clock_getres()), są zaokrąglane w dół, do mniejszej wielokrotności. Parametr clock_id może przyjąć następujące wartości:
CLOCK_REALTIME - jego rozdzielczość nigdy nie jest gorsza niż 100Hz oraz określa czas w sekundach i nanosekundach od północy (00:00UTC) 01.01.1970. Praktycznie w większości przypadków parametr ten przyjmuje właśnie tą wartość.
CLOCK_VIRTUAL - zegar jest inkrementowany tylko gdy procesor działa w trybie użytkownika.
CLOCK_PROF - zegar jest inkrementowany gdy procesor działa w trybie użytkownika lub jądra.
CLOCK_MONOTONIC - nigdy nie jest modyfikowany ani restartowany od chwili startu.
CLOCK_RTL_SCHED - używany do szeregowania zadań. Względem tego zegara są przekazywane wartości do zadań periodycznych.
CLOCK_8254 - używa się go w systemach jednoprocesorowych, opartych na platformie Intel x86. Nie powinien być używany przez programy użytkownika.
CLOCK_APIC - używa się go na platformie Intel x86 z symetryczną wieloprocesorowością.
Aby określić tryb pracy danego zegara, używa się funkcji rtl_setclockmode(clockid_t clock_id, int mode, hrtime_t mode_param), gdzie jako drugi parametr podaje się albo RTL_CLOCK_MODE_PERIODIC i wówczas trzeci parametr określa okres podawany w nanosekundach, lub RTL_CLOCK_MODE_ONESHOT i wtedy trzeci parametr jest po każdym cyklu ignorowany.
Poza tym, API RTLinuksa jest bogate we wszelkiego rodzaju funkcje umożliwiające między innymi pobranie aktualnego czasu oraz jego wszelakie konwersje. Zainteresowanego czytelnika należy odesłać do plików nagłówkowych time.h oraz rtl_time.h.
Sygnały czasu rzeczywistego.
Sygnały czasu rzeczywistego w RTLinuksie reprezentują albo przerwania sprzętowe (IRQ), albo zdarzenia generowane przez generatory opóźnień (timery). Jeśli zostaną zablokowane, to będą generowane tzw. sygnały oczekujące. Dla sygnałów związanych z IRQ oznacza to wygenerowanie ich taką liczbę razy po odblokowaniu, jaką pojawiły się przed zablokowaniem. Sygnał timera natomiast, jest po odblokowaniu generowany tylko raz, niezależnie od tego ile razy nadszedł.
Mechanizmem zarówno przypisującym określonemu zdarzeniu procedurę jego obsługi, jak i pozwalającym na działanie odwrotne - ignorowanie określonego sygnału, jest dostępna od wersji 3 pakietu RTLinux, funkcja:
rtlinux_sigaction(int signum, struct rtlinux_sigaction *act, struct rtlinux_sigaction *oldact).
Można ją wywoływać tylko i wyłącznie z poziomu procesów Linuksa. Zadania RTLinuksa nie mogą z niej korzystać. Podczas jej wywołania ryglowana jest pamięć procesu do chwili jego zakończenia lub dobrowolnego odblokowania jej przez ten proces. Oznacza to, że proces taki musi zostać uruchomiony z prawami roota.
Pierwszy parametr określa zdarzenia (sygnały) źródłowe. Wartości od zera (0) do RTLINUX_SIGTIMER0 -1 reprezentują zdarzenia związane z przerwaniami sprzętowymi. Wartości od RTLINUX_SIGTIMER0 do RTLINUX_SIGTIMER15 to sygnały zegarowe.
Drugi parametr, wskazuje na strukturę, dzięki której można ustalić takie parametry jak np. wskaźnik do funkcji obsługi danego sygnału (sa_handler), okres pomiędzy sygnałami wysyłanymi do procesu (sa_period) oraz czy sygnał ma być generowany periodycznie, czy tylko raz (sa_flags). Trzeci parametr jest ignorowany.
Należy zwrócić uwagę na fakt, że funkcje obsługi takich sygnałów są wywoływane tak szybko, jak pozwala na to sprzęt. Nigdy nie czekają na reakcję systemu Linux. W dobie współczesnego sprzętu (rok 2004) ewentualne opóźnienia są na poziomie kilkudziesięciu mikrosekund. Poza tym, przez RTLinuksa są traktowane jak zadania czasu rzeczywistego i podlegają zasadom szeregowania. Są wykonywane gdy Linuks nie może dostarczyć normalnych usług dla działającego kodu. Z ich poziomu nie można używać funkcji systemowych jądra Linuksa oraz wszelkie wywołania muszą być wykonane w statycznie łączonych bibliotekach.
Wymiana danych pomiędzy procesami.
Wielokrotnie już było podkreślane, że RTLinuks stanowi odseparowany i nadrzędny w stosunku to Linuksa fragment systemu operacyjnego. Również aplikacje tworzone dla takiej platformy składają się z dwóch komponentów - zwykłych procesów Linuksa oraz zadań czasu rzeczywistego. RTLinux dostarcza kilka mechanizmów, dzięki którym obie te części mogą się ze sobą bezpiecznie komunikować bez powodowania różnych skutków ubocznych np. w postaci opóźnień. Najważniejsze z nich, to kolejki czasu rzeczywistego (RT FIFO) oraz pamięć dzielona.
Kolejki czasu rzeczywistego (ang. real time FIFOs) działają w oparciu o zasadę „pierwszy przyszedł, pierwszy zostanie obsłużony”. Stanowią bufory utworzone w przestrzeni jądra. Aby móc z nich korzystać, należy najpierw załadować dwa moduły: rtl_posixio.o oraz rtl_fifo.o. Dostarczają one niezbędne mechanizmy służące do wykonywania takich operacji, jak np. zapis/odczyt z kolejki czy jej utworzenie.
Procesy Linuksa widzą je jako zwykłe urządzenia znakowe o numerze głównym (ang. major number) 150, dostępne jako /dev/rtfX, gdzie X oznacza numer kolejnej kolejki (począwszy od zera). Mogą na nich wykonywać takie same operacje jak na plikach. Liczbę dostępnych kolejek RT określa się statycznie podczas kompilacji RTLinuksa i standardowo wynosi 64.
Zadania czasu rzeczywistego, korzystają z kolejek RT FIFO przy pomocy specjalnych funkcji, umożliwiających ich tworzenie, niszczenie oraz operacje wejścia - wyjścia (zapis i odczyt stanowią operacje niepodzielne i nieblokujące). Należy jednak pamiętać, że przed zapisaniem lub odczytaniem czegokolwiek z kolejki, należy ją najpierw zainicjalizować. Dokonuje tego funkcja: rtf_create(unsigned int fifo, int size). Alokuje ona w pamięci obszar o długości size bajtów oraz przydziela go dla kolejki numer fifo. Przeciwne działanie ma funkcja: rtf_destroy(unsigned int fifo), która podaną jako parametr kolejkę usuwa z pamięci. Obie funkcje mogą być wywoływane tylko i wyłącznie z kodu jądra Linuksa, czyli z poziomu metod: init_module() i cleanup_module(). Rozmiar kolejki można modyfikować przy pomocy funkcji: rtf_resize(unsigned int fifo, int new_size), podając jako drugi parametr nowy jej rozmiar.
Zapisu oraz odczytu z kolejki dokonuje się przy pomocy dwóch poniższych funkcji:
rtf_fifo_put(unsigned int fifo, char *buf, int count) - zapisze do kolejki fifo dane w skazane przez buf oraz w ilości count bajtów. Funkcja zwraca ilość zapisanych danych lub (-1) w przypadku przepełnienia bufora;
rtf_fifo_get(unsigned int fifo, char *buf, int count) - pobiera z kolejki fifo dane w ilości count bajtów oraz zapisuje je w miejscu wskazywanym przez buf. Zwraca ilość pobranych bajtów lub gdy jest ich mniej niż się spodziewano, zwraca (-1).
Zarówno zwykłe procesy, jak i zadania czasu rzeczywistego mogą z kolejek RT FIFO czytać i do nich pisać. Same kolejki są jednak jednokierunkowe. Oznacza to, że aby zapewnić dwukierunkową komunikację, należy użyć dwóch kolejek. Stanowią one więc kanały typu punkt - do - punktu (ang. point - to - point), w których nie są określone wielkości komunikatów. Poza tym, nie wymagają implementowania żadnego dodatkowego protokołu zapobiegającego nadpisywaniu danych. Nie trzeba również stosować odpytywania (ang. polling) na obecność nowych danych w celu uzyskania synchronizacji. Istnieje możliwość zainstalowania procedury obsługi zdarzenia generowanego po zapisie lub odczycie z kolejki. (funkcja: rtf_create_handler()).
Drugim mechanizmem służącym do komunikacji międzyprocesowej, jest pamięć dzielona. Do jego obsługi został stworzony specjalny moduł o nazwie mbuff. Jest to najszybszy sposób wymiany danych pomiędzy zadaniami. W przeciwieństwie do kolejek, wymaga zaimplementowania odpowiedniego protokołu dostępu oraz nie umożliwia rejestrowania uchwytów dla zadań związanych np. z pojawieniem się nowych danych. W celu synchronizacji, konieczne jest odpytywanie. Pamięć, może być natomiast dzielona przez dowolną liczbę zadań oraz umożliwia wymianę danych w postaci struktur. Jej rozmiar jest ograniczony tylko przez dostępną pamięć fizyczną. W przestrzeni adresowej jest ona logicznie ciągła. Fizycznie może być „porozrzucana”. Pamięć dzielona jest blokowana przed wymieceniem do pamięci wirtualnej.
Należy pamiętać, że operacje jej przydzielania i zwalniania nie mogą być wykonywane przez zadania czasu rzeczywistego oraz procedury obsługi przerwań i zegarów. Najlepiej jest tego dokonać z poziomu procesów Linuksa lub funkcji inicjalizującej moduł. Służą do tego następujące funkcje:
void *mbuff_alloc(const char *name, int size) - alokuje obszar o nazwie name i rozmiarze size bajtów. Licznik odwołań do przydzielonego bloku przyjmuje wartość 1 oraz zwracany jest do niego wskaźnik. W przeciwnym razie, zwracana jest wartość NULL. Jeśli nazwa name już istnieje w systemie, to licznik odwołań do takiego obszaru jest inkrementowany a otrzymany adres jest wskaźnikiem do niego.
void mbuff_free(const char *name, void *mbuf) - dekrementuje licznik odwołań do danego bufora i gdy osiągnie on wartość zero (0), zostaje usunięty z pamięci.
Jak widać, obie funkcje dostarczają prosty protokół uniemożliwiający usunięcie bufora używanego przez jakieś zadania. Oczywiście istnieją też funkcje, które przy tworzeniu bufora nie zwiększają licznika odwołań do niego. Pozwala to na automatyczne zwolnienie pamięci w wyniku „zabicia” procesu dokonującego wcześniej odpowiednich rezerwacji.
Mechanizmy synchronizacji w RTLinuksie.
RTLinux dostarcza mechanizmy dzięki którym można rozwiązać problemy wzajemnego wykluczania i synchronizacji zadań. Są one zgodne z POSIX i zaliczają się do nich opisane już w rozdziale 5 (pt. „Budowa systemu czasu rzeczywistego”):
zmienne warunkowe
muteksy
semafory nienazwane.
Są one dostępne w postaci całej rodziny funkcji. Oprócz standardowych operacji ryglowania i zwalniania, możliwe jest także czasowe oczekiwanie na zwolnienie semafora lub wystąpienie jakiegoś warunku (np. nadejście określonego sygnału). Również muteksy mogą zostać zaopatrzone w limity czasowe.
Oczywiście wszędzie tam, gdzie pojawiają się elementy współdzielenia zasobów, istnieje zaistnienia bardzo niebezpiecznych sytuacji jak np. blokada oraz inwersja priorytetów (omówione szczegółowo w rozdziale 9 (pt. „Problemy związane z szeregowaniem zadań”). W celu uniknięcia takich niekorzystnych zjawisk, twórcy RTLinuksa zdecydowali się zaimplementować protokół działający w oparciu o pułap priorytetów (opisany w rozdziale 9). Tutaj nosi on nazwę Ceiling Semaphore Protocol (CSP) - pułap semafora, który broni wyłącznego dostępu do danego zasobu. Możliwość korzystania z tego protokołu jest określana w czasie konfiguracji pakietu. Dodatkowo muteksy mogą zostać wyposażone w mechanizmy przeciwdziałające inwersji priorytetów.
Zastosowania systemu RTLinux.
Na stronie firmy FSMLabs (www.fsmlabs.com) można przeczytać o licznych dziedzinach, w których RTLinux znalazł zastosowanie. Są nimi między innymi:
sztuka - np. przy generowaniu dźwięku skrzypiec (symulacja ruchu smyczka)
lotnictwo - np. przy testowaniu silników odrzutowych w projekcie Joint Strike Project, w bezzałogowych jednostkach powietrznych, systemy ochrony przeciwrakietowej
robotyka - np. Fujitsu Automation Limited wraz z Fujitsu Laboratories używają RTLinuksa do kontroli 48 centymetrowego robota HOAP
nauka - np. do całkowicie automatycznych badań nieba wykorzystywany przez Węgierski Teleskop Automatyczny (HAT)
Poza tymi przykładami, na stronie można się również doczytać o zastosowaniach przy ożywianiu różnych postaci kreskówek, kontroli pojazdów podwodnych oraz w szpitalach do monitorowania respiratorów.
QNX Neutrino
QNX, to zdaniem wielu (np. AMD, IBM, Cisco Systems) najlepszy i jednocześnie najbardziej zaawansowany oraz przyszłościowy, rygorystyczny system operacyjny czasu rzeczywistego. Jest pierwszym w historii systemem wielozadaniowym i wielodostępnym przeznaczonym dla mikrokomputerów IBM PC. Został opracowany na początku lat 80 przez założycieli kanadyjskiej firmy Quantum Software System, Limited - G. Bella i D. Dodge'a. Jego początkowa nazwa QUNIX (Quick UNIX), ze względu na zbyt duże podobieństwo do nazwy UNIX, nie mogła przetrwać zbyt długo i w kilka miesięcy później, za sprawą firmy AT&T, musiała zostać zmieniona i przybrała znane do dzisiaj brzmienie: QNX.
Pierwsza wersja, była przeznaczona na komputery klasy IBM PC oraz wymagała 64kB pamięci RAM i 180kB napęd dyskietek. Pomimo swojej prostoty, umożliwiała już uruchomienie kilku procesów jednocześnie. Dzięki licznym użytkownikom, którzy system ten ciągle testowali oraz powiadamiali o błędach i ewentualnych ulepszeniach, rozwój QNX-a zaczynał przybierać coraz większego tempa. Wkrótce IBM wprowadził komputery PC AT, co pozwoliło pracować programom w trybie chronionym, bez konieczności ich ponownej kompilacji. QNX został wtedy również wzbogacony o obsługę sieci. Do tego celu wykorzystano standard Arcnet, który cechuje bardzo duża szybkość działania. Wersję 4.0 systemu napisano praktycznie od podstaw i była przeznaczona dla procesorów Intel 386. Dodano obsługę większości dostępnych kart sieciowych ale co najważniejsze, zapewniono zgodność z POSIX. Dalsze jej ewolucje umożliwiły uruchamianie aplikacji 32 bitowych oraz pracowały w środowisku graficznym nazwanym Photon.
Początek lat 90, to ekspansja QNX - a na inne architektury sprzętowe (MIPS, ARM, PowerPC, SH4). Od wersji QNX 6.0 (=QNX Neutrino 2.1), zaczęto kłaść ogromny nacisk na skalowalność, modularność oraz jak najlepsze dostosowanie do wymagań systemów wbudowanych. Neutrino, to jedyny na świecie systemem operacyjnym, którego interfejs graficzny mieści się w 1 MB pamięci ROM. W jego najnowszej wersji (6.3; 06.2004 r.), zapewniono obsługę najnowszych standardów (np. USB 2.0) oraz poprawiono wydajność i niezawodność systemu. Poza tym, QNX 6.3 obsługuje pamięć powyżej 4 GB (x86, PowerPC, MIPS), posiada zaktualizowane wersje wielu narzędzi oraz zmieniono w niej sposób kolejkowania wiadomości POSIX. Przesyłanie komunikatów odbywa się w sposób asynchroniczny. Pełną listę zmian w QNX 6.3 można znaleźć np. w [15]. System wydawany jest w dwóch wersjach. Do użytku niekomercyjnego, przeznaczona jest dystrybucja QNX Realtime Platform. Możliwości tego systemu najlepiej podkreślają słowa Dawida White'a, zamieszczone na łamach czasopisma Lan Magazine:
"gdyby firma IBM wybrała system QNX dla mikrokomputera IBM PC, wprowadzenie na rynek modelu AT mogłoby zostać opóźnione, gdyż aplikacje uruchamiane pod systemem QNX w komputerach PC zachowują się tak, jakby zostały uruchomione pod systemem DOS w komputerze 386" [16].
Oczywiście wypowiedź ta ma jedynie na celu bardzo ostre uwypuklenie jak silną i nowatorską jest platforma QNX. Wiadomo przecież, że rozwój technologii mikroprocesorowej jest niezależny od możliwości, jakie oferuje oprogramowanie.
Kolejne podrozdziały przybliżą świat systemu QNX. Zostanie przedstawiona jego architektura oraz implementowane w nim mechanizmy. Całość wzbogaci ogólny opis dystrybucji, dostępnych dla niej narzędzi oraz przykłady użycia tego systemu w przemyśle.
Dystrybucja systemu QNX
Najnowszą wersję systemu QNX Neutrino można pobrać ze strony producenta (http://www.get.qnx.com/). Jest to tzw. Realtime Platform i różni się nieco od wersji komercyjnej. Brak w niej np. mechanizmów związanych z silnym szyfrowaniem. Jest jednak w pełni funkcjonalna. Instalację najłatwiej przeprowadzić z poziomu systemu operacyjnego Windows i tylko na partycji FAT, gdyż nie jest obsługiwany system plików NTFS. Wymagane miejsce, to ok. 600MB. Przy pomocy programu setup.exe można utworzyć dyskietkę, która uruchomi komputer w trybie pracy QNX i pozwoli na wykonanie dalszych kroków instalacyjnych. Proces ten, jest podobny do instalacji systemów z rodziny UNIX. Tworzone jest konto administratora (ang. root) oraz rozpoznawane są istniejące partycje FAT (montowane w podkatalogu: /fs w trybie do odczytu i zapisu) Aby odinstalować system QNX, należy ponownie wrócić do systemu Windows oraz skorzystać z apletu: Dodaj/Usuń Programy.
Dla tych, którzy chcą jak najszybciej zapoznać się z najbardziej podstawowymi elementami systemu, opracowana została tzw. wersja dyskietkowa systemu. Można ją pobrać ze strony: http://www.quantum.com.pl/. Pomimo swoich małych rozmiarów, zawiera ona system operacyjny wraz z przeglądarką WWW, dialerem, menadżerem plików, edytorem tekstowym oraz środowiskiem graficznym.
Dla systemu QNX, dostępny jest także zintegrowany zestaw narzędzi programistycznych: QNX Momentics. Pozwala on przyspieszyć tworzenie projektów w każdym ich etapie. Dostarcza wiele bibliotek, gotowych do użycia rozwiązań oraz zestawów do budowy sterowników. Pozwala na programowanie w językach C, C++, Embedded C++ oraz Java. Możliwe środowiska to m. in. Windows, Solaris. Bardzo ciekawą możliwością tego pakietu, jest zdolność generowania kodu na platformy ARM, MIPS, PowerPC, SH-4, StrongARM, XScale i x86. Całość zbudowano w oparciu o otwartą i rozszerzalną platformę Eclipse, co ułatwia budowanie własnych narzędzi, jak i proste dołączanie ich zestawów zakupionych u innych producentów. Więcej na temat możliwości QNX Momentics można przeczytać np. na polskich stronach poświęconych tematyce QNX, czego bardzo dobrym przykładem jest np. www.qnx.com.pl.
Należy mieć świadomość, że system QNX Neutrino nie jest typowym systemem biurkowym. Brak w nim programów biurowych i do obsługi multimediów. Ogromnym uznaniem cieszy się natomiast w zastosowaniach związanych np. z telefonami, urządzeniami przenośnych oraz tzw. punktami sprzedaży (ang. Point Of Sale, POS), jak bankomaty czy terminale internetowe.
Architektura mikrojądra (Neutrino)
QNX, to jeden z pierwszych systemów operacyjnych, w którym zaimplementowano architekturę mikrojądra (ang. microkernel). Owe mikrojądro, począwszy od wersji 6.0 systemu, ze względu na swój mały rozmiar (8kB!; jądro systemu UNIX to co najmniej 700kB), nosi nazwę Neutrino. To, co odróżnia ten system ten od rodziny UNIX, to przede wszystkim struktura modułowa oraz architektura oparta o przesyłanie komunikatów (model klient - serwer). Poza tym, QNX daje możliwość zdeterminowania czasu reakcji na zdarzenia występujące w systemie. Również dzięki rozbudowanym możliwościom definiowania priorytetów, QNX jest stosowany jako system służący do sterowania automatyką przemysłową, gdzie pewne zdarzenia są krytyczne (np. otwarcie zaworu bezpieczeństwa w zbiorniku kiedy gwałtownie wzrasta ciśnienie) i muszą być zawsze obsłużone na czas.
Budowa mikrojądra (Neutrino)
Mikrojądro w systemie QNX zajmuje się bardzo niewielką liczbą zadań i praktycznie jego obowiązki sprowadzają się do obsługi kolejki zadań (ang. schedule) do wykonania oraz sterowania przekazywaniem wiadomości (ang message passing) między procesami, które działają w systemie. Znacznie wyróżnia go taka struktura od systemów klasycznych, takich jak Windows czy Linux, gdzie duże monolityczne jądro decyduje o wszystkim, co dzieje się w systemie - dostępie do plików, prawie dostępu, obsłudze sieci itd.
Samo jądro QNX, jak już wspomniano wcześniej, jest bardzo małych rozmiarów oraz składa się z czterech części (rys. 32):
IPC - (ang. Interprocess communication) - obsługuje komunikację między procesami.
Network Interface - przeźroczysta komunikacja pomiędzy procesami w obrębie sieci lokalnej.
Hardware Interrupt Redirector - przechwytuje pojawiające się przerwania oraz przekazuje je do odpowiednich procesów obsługujących je. Część ta sama nie obsługuje przerwań!
Realtime Scheduler - decyduje, który proces ma uzyskać dostęp do procesora w danej chwili (POSIX 1003.4 - dotyczy zagadnień czasu rzeczywistego).
Rysunek 32. Budowa mikrojądra QNX.
Taka architektura, zapewnia pełną ochronę wszystkim komponentom. W konwencjonalnym podejściu, błędy programistyczne, jak np. wadliwy wskaźnik w języku C, mogą spowodować nadpisanie współdzielonych obszarów pamięci oraz uszkodzenie jądra. Prowadzi to do powstania błędu systemu. System QNX potrafi uniknąć takich katastrof nawet na poziomie sterowników urządzeń i innych krytycznych programów, bez konieczności przeładowania. Otóż, każdy komponent systemu uruchamiany jest w swojej własnej chronionej przestrzeni adresowej.
Strukturę taką pokazuje rys. 33.
Rysunek 33. Ochrona komponentów w architekturze QNX.
Bardzo ważnym faktem jest to, że ochrona pamięci nie jest realizowana kosztem wydajności. Inne architektury albo uruchamiają wszystkie procesy w obszarze jądra (nie zapewniają ochrony pamięci), albo tylko ich część (monolityczna architektura - zapewnia ograniczoną ochronę pamięci).
Mikrojądro realizuje wywołania bardzo ograniczonej liczby funkcji (ang. kernell calls). Wszelkie funkcje, jak np. administrowanie procesami, gospodarka pamięcią operacyjną, zarządzanie zbiorami, urządzeniami wejścia/wyjścia, współpraca z siecią lokalną itp. są realizowane przez tzw. procesy zarządzające (ang. managers).
Procesy zarządzające są traktowane w identyczny sposób jak procesy użytkowe (np. proces kompilatora). Można je ładować, uruchamiać, zawieszać oraz usuwać niezależnie od siebie. Poza tym, czynności te mogą być wykonywane dynamicznie, w czasie normalnej pracy systemu ("w biegu" - ang. on the fly). Prowadzi to, do bardzo wygodnej sytuacji, gdyż w zależności od wymagań zewnętrznych, dany proces zarządzający może zostać zainstalowany bądź usunięty. Wyjątkiem jest tutaj tzw. Process Manager, który musi być zawsze obecny w systemie (więcej szczegółów na temat modułowej budowy systemu QNX oraz ich zadań w dalszej części pracy).
Jedyne co odróżnia procesy zarządzające od użytkowych to priorytety. Dla nich przydzielone są najwyższe. Dodatkowo zyskują poziomy uprzywilejowania, które zezwalają im na realizację niektórych instrukcji mikroprocesora.
W systemie QNX zaimplementowano trzy poziomy uprzywilejowania:
poziom 0 - mikrojądro
poziom 1 - procesy zarządzające (np. Proc, Fsys itp.)
poziom 2 - nie jest aktualnie wykorzystywany
poziom 3 - procesy użytkowe
Należy zwrócić uwagę na fakt, że na poziomie 3 instrukcje odwołujące się bezpośrednio do portów we/wy oraz podsystemu przerwań nie są dostępne. Tylko uprzywilejowany użytkownik może tworzyć własne procesy, które mają pełny dostęp do sprzętu (opcja kompilatora: cc -T1). Dzięki temu, możliwym staje się budowanie np. niestandardowych sterowników dla urządzeń zewnętrznych.
Komunikacja międzyprocesowa (IPC) w skali jednego komputera
Procesy w systemie QNX, zarówno te zarządzające, jak i użytkowe, do komunikacji między sobą używają techniki opartej o przekazywanie komunikatów (ang. message passing). Za jej właściwe funkcjonowanie odpowiedzialne jest mikrojądro, a właściwie jego moduł - IPC (ang. Inter-Process Communication). W formie podstawowej, komunikacja taka jest synchroniczna. Do przesyłanych pakietów nie są dodawane żadne dodatkowe informacje, co sprawia, że są one czytelne tylko dla procesów biorących udział w komunikacji.
QNX udostępnia trzy podstawowe funkcje, dzięki którym komunikacja międzyprocesowa jest możliwa. Ich prototypy przedstawione są poniżej.
wysyłanie komunikatów: Send( pid, smsg, rmsg, smsg_len, rmsg_len );
pid - ID procesu, który ma otrzymać komunikat. Wartość ta jednoznacznie identyfikuje dany proces w systemie.
smsg, rmsg - bufory danych. Pierwszy zawiera dane do wysłania, a w drugim zostaną umieszczone odebrane dane.
smsg_len, rmsg_len - rozmiary powyższych buforów. Określają maksymalną ilość danych wysłanych/odebranych, co pozwala uniknąć niebezpiecznego nadpisania danych.
odbieranie komunikatów: pid = Receive( 0, msg, msg_len );
pid - identyfikator procesu, którego komunikat odebrano.
0 (zero) - oznacza, że komunikaty będą odbierane od dowolnego procesu. Względnie można określić tutaj proces - nadawcę.
msg - bufor wskazujący gdzie zapisać otrzymane dane.
msg_len - rozmiar powyższego bufora. Jeśli jest inny niż rozmiar bufora danych wysyłanych (w funkcji send()), to wybierana jest mniejsza wartość.
wysyłanie odpowiedzi: Reply( pid, reply, reply_len );
pid - określa proces, do którego ma trafić odpowiedź.
reply - bufor zawierający odpowiedź.
reply_len - rozmiar powyższego bufora. Jeśli jest inny niż rozmiar bufora danych odbieranych (w funkcji send()), to wybierana jest mniejsza wartość.
Bardzo ciekawą sprawą jest to, że funkcje te służą zarówno do komunikacji w obrębie jednej stacji roboczej, jak i na skalę całej sieci lokalnej (o czym szerzej - w następnym podrozdziale!). Kolejną ciekawostką jest, że biblioteki służące do komunikacji międzyprocesowej zostały zbudowane w oparciu właśnie o te mechanizmy. Tak więc, procesy korzystające np. z potoków również korzystają pośrednio z dobrodziejstw oferowanych przez powyższe funkcje. Z punktu widzenia programisty, żądanie wykonania jakiejś usługi przez serwer może się sprowadzić do wywołania funkcji. W rzeczywistości, system przesyła odpowiednie komunikaty. Przykład użycia powyższych trzech funkcji najlepiej obrazuje rys. 34.
Rysunek 34. Przykład użycia funkcji: send(), receive(), reply().
Na początku, Proces A inicjalizuje komunikację i wysyła komunikat do Procesu B, przy pomocy funkcji Send(). Dopóki proces - odbiorca nie otrzyma komunikatu (wywoła funkcję Receive()), proces - nadawca jest blokowany (ang. SEND - blocked - odbiorca jeszcze nie otrzymał komunikatu, bo np. jest zajęty czym innym). Następnie proces - odbiorca wywołuje funkcję Receive() oraz zaczyna przetwarzać otrzymane informacje. Proces - nadawca dalej jest blokowany (ang. REPLY - blocked - odbiorca otrzymał komunikat ale jeszcze nie odpowiedział bo przetwarza go). Na koniec Proces B wywołuje nieblokującą funkcję Reply() oraz ewentualnie zwraca wyniki do Procesu A. W tej chwili, oba procesy są gotowe do wykonywania dalszych zadań. Który uzyska dostęp do procesora, jest zależne od wartości ich priorytetów. Można rozpatrzyć przypadek, kiedy proces - odbiorca pierwszy inicjalizuje komunikację poprzez wywołanie funkcji Receive(). Wówczas jest on blokowany (ang. RECEIVE - blocked) i oczekuje na pojawienie się danych. Nadawca po wywołaniu funkcji Send() natychmiast zaczyna oczekiwać na pojawienie się odpowiedzi (ang. REPLY - blocked).
Jak wynika z powyższego przykładu, funkcji tych można używać do prostej synchronizacji procesów w systemie. Proces może otrzymywać komunikaty od kilku innych oraz sortować je np. wg priorytetów. Poza tym, w żadnym przypadku nie następuje kopiowanie danych przesyłanych do obszarów mikrojądra. Wszystkie informacje są przechowywane w odpowiednich procesach.
Naturalnie, QNX dostarcza również funkcje, które służą tylko do sprawdzenia czy są przesłane nowe komunikaty oraz powrócenia do normalnego toku wykonywania - bez blokowania. Inne umożliwiają np. dzielenie komunikatów na części i wysyłanie pakietów przekraczających rozmiary buforów lub odczyt tylko części informacji.
Ciekawostką jest fakt, że sam system QNX dla rozróżnienia swoich wewnętrznych komunikatów, dodaje do nich 16 - bitowe nagłówki. Określają one np. komunikaty zarządcy procesów, systemem plików lub urządzeniami itd. Oczywiście jest to tylko pewna konwencja, która nie jest wymagana przez twórców oprogramowania.
Aby uniknąć tak niekorzystnej sytuacji, jak zakleszczenie (ang. deadlock) należy pamiętać, aby nie tworzyć procesów, które jednocześnie wysyłają do siebie jakieś komunikaty. Poza tym, procesy najlepiej organizować w hierarchię, a dane przesyłać tylko w górę drzewa ją opisującego, np. proces klienta wysyła komunikat do procesu obsługującego bazę danych, który z kolei przesyła do części odpowiedzialnej za zarządzanie systemem plików.
Komunikacja międzyprocesowa (IPC) w skali sieci lokalnej
Jeśli komunikacja między procesami odbywa się w obrębie jednego komputera, to komunikaty są przesyłane bezpośrednio. Jeśli natomiast adresatem jest proces uruchomiony na innym komputerze (w obrębie sieci QNX), to wówczas do komunikacji jest wykorzystywany moduł jądra odpowiedzialny za współpracę z siecią - Network Interface. Przy pomocy specjalnych sterowników, komunikaty docierają do odpowiednich węzłów sieci. Technika ta pozwala na łatwą wymianę danych, bez względu na to, gdzie znajdują się procesy biorące w niej udział.
Należy się w tym miejscu pewne wyjaśnienie konwencji nazewnictwa procesów w systemie QNX. Otóż, w obrębie pojedynczego węzła sieci, każdy proces może zarejestrować przy pomocy Process Manager'a swoją symboliczną nazwę. Wtedy może zażądać swoje ID, jakie zostało mu przydzielone. Istnieje tutaj pewne rozróżnienie nazw lokalnych i globalnych. Globalne - dostępne w skali całej sieci, zaczynają się od znaku: / (slash). Np.: qnx/fsys to proces lokalny; /quantum/abc to proces globalny.
Bazę nazw globalnych podtrzymuje specjalny proces (ang. process name locator, nameloc) oraz musi być uruchomiony przynajmniej na jednej maszynie (max. 10 na sieć).
Aby komunikacja pomiędzy procesami uruchomionymi na różnych węzłach sieci była możliwa, konieczne jest stworzenie tzw. wirtualnego połączenia (ang. virtual circuit). W efekcie, na obydwu jego końcach, powstają pseudoprocesy (ang. virtual processes), które są odpowiedzialne za przenoszenie komunikatów poprzez sieć do swojego odpowiednika na odległym jej węźle. Para taka zapewnia dwustronną komunikację dokładnie dwóch procesów. Zasadę działania tego mechanizmu najlepiej przedstawia rys. 35.
Rysunek 35. Komunikacja między procesami na różnych węzłach sieci QNX.
Proces nadawca (serwer) musi stworzyć wirtualne połączenie do odbiorcy swoich komunikatów. Dokonuje tego, przy pomocy funkcji qnx_name_attach(). W efekcie, powstają dwa procesy wirtualne - na obu końcach połączenia. Jeśli proces PID 1 chce coś wysłać do procesu PID 2, to komunikuje się z jego wirtualnym odpowiednikiem VID 2. Ten przesyła dane przez sieć, które docelowo trafią do procesu PID 2. Każdy proces wirtualny zawiera ID procesu lokalnego, ID procesu zdalnego, ID zdalnego węzła sieci oraz ID zdalnego procesu wirtualnego.
Aby odnaleźć proces o zadanej nazwie, należy użyć funkcji qnx_name_locate(). Zwraca ona ID danego procesu lub gdy znajduje się on na innym komputerze, najpierw tworzy połączenie wirtualne a następnie zwraca ID procesu wirtualnego, z którym należy się komunikować. Jak widać, upraszcza takie podejście w sposób znaczący życie programiście.
Ważną cechą jest to, że mechanizm ten jest używany w większości przypadków automatycznie i bez wiedzy użytkownika np. w czasie operacji wejścia - wyjścia poprzez sieć. Poza tym, może być wykorzystany nie tylko do przekazywania wiadomości, ale i również (opisanych niżej) sygnałów oraz zdarzeń.
Integralność połączenia jest sprawdzana przez Process Manager'a. Poprzez wysyłanie pakietów kontrolnych określa on ich prawidłowe funkcjonowanie. Pozwala to uniknąć wiele błędów oraz podjąć stosowne akcje.
Komunikacja międzyprocesowa (IPC) poprzez proxy
Mechanizm komunikacji międzyprocesowej z użyciem proxy został specjalnie stworzony do przekazywania zdarzeń. Może być używany zarówno z poziomu procesów, jak i przerwań. Bardzo ważną cechą, jest brak jakiegokolwiek blokowania. Nadawca nie czeka ani na odebranie komunikatu, ani na odpowiedź. Tylko informuje o zaistnieniu określonych okoliczności.
Proxy jest tworzone przy pomocy funkcji qnx_proxy_attach(). Oprócz określenia jego identyfikatora, przekazuje mu ona również domyślną wiadomość. Inne procesy pobudzają proxy przy pomocy obsługiwanej przez mikrojądro funkcji Trigger(). Pobudzenie to odpowiada operacji przesłania zawartego w proxy komunikatu do jego właściciela. Proces odbierający, komunikuje się z proxy tak, jak ze zwykłym procesem. Wywołania funkcji Trigger() mogą być kolejkowane i powodować późniejsze, sekwencyjne przesłania danego komunikatu. Sytuację taką przedstawia rys. 36.
Rysunek 36. Komunikacja międzyprocesowa z wykorzystaniem proxy.
Oczywiście QNX pozwala na utworzenie tzw. wirtualnych proxy, które pozwalają na przesyłania wiadomości o zdarzeniach na skalę całej sieci lokalnej. Wirtualne proxy może być pobudzone przez dowolny zdalny proces. Jest tworzone na zdalnym komputerze oraz stanowi odpowiednik lokalnego proxy.
Komunikacja międzyprocesowa (IPC) z wykorzystaniem sygnałów
Sygnały, to podstawowa metoda asynchronicznej komunikacji. Do ich generowania używa się narzędzi: kill lub slay oraz funkcji: kill() lub raise(). Są dostarczane, jeśli docelowy proces jest gotowy do wykonania. Odpowiadają przerwaniom programowym.
Proces odbiera dany sygnał, jeśli zawiera procedurę jego obsługi. Oczywiście, jeśli nie implementuje on jakiegoś specjalnego zachowania, to wykonywany jest domyślny kod, który zazwyczaj prowadzi do zakończenia procesu. Względnie proces, może ignorować pewne sygnały. Jeśli nie posiada jednak odpowiednich uprawnień, nie może ignorować następujących sygnałów: SIGCONT (kontynuuj gdy zawieszony), SIGKILL (przerwij proces) oraz SIGSTOP (wstrzymaj proces).
QNX implementuje sygnały zgodne z POSIX oraz wprowadza pewne swoje rozszerzenia. Dodatkowo dwa z nich (SIGUSR1, SIGUSR2) mogą zostać zdefiniowane przez użytkownika. System nie określa ich znaczenia.
Aby zdefiniować procedurę obsługi danego sygnału, można użyć np. funkcji sigaction(). Jest ona wykonywana asynchronicznie w stosunku do reszty procesu. Możliwa jest sytuacja, że zostanie wywołana w czasie wykonywania jakiejś innej procedury programu.
QNX dostarcza bardzo dobrego mechanizmu tymczasowego blokowania sygnałów bez zmieniania procedur ich obsługi. Dodatkowo po rozpoczęciu obsługi sygnału, na czas jej trwania, jest on standardowo blokowany. Eliminuje to zagnieżdżone wywołanie procedury obsługi. Później sygnał jest ponownie odblokowywany i ewentualnie dostarczany.
Z sygnałami wiąże się pewne niebezpieczeństwo. Otóż, jeśli dany proces jest zablokowany w oczekiwaniu na odebranie przez inny proces komunikatu lub gdy czeka na pojawienie się nowej wiadomości, pojawienie się sygnału przebudza go, następuje obsłużenie sygnału a następnie funkcje Receive()/Send() zwracają błąd. Kiedy jeszcze pierwsza opisana sytuacja nie jest zbytnio groźna, druga wprowadza do systemu pewien element niepewności. Można uniknąć tego typu problemów np. poprzez wzajemne informowanie się procesów o wystąpieniu sygnałów oraz podejmowanie stosownych akcji.
Inne metody komunikacji międzyprocesowej (IPC)
Poza wymienionymi powyżej metodami komunikacji międzyprocesowej, system QNX bogaty jest również w całe zaplecze związane z obsługą kolejek oraz pamięci współdzielonej. Kolejki są implementowane przez odrębny moduł Nqueue i są zgodne z POSIX. Istnieją niezależnie od procesów korzystających z nimi a operacje na nich nie są blokujące. Stanowią wolniejszy mechanizm w porównaniu do przesyłania komunikatów. Operacje na nich przypominają te, poznane przy pracy z plikami. Po utworzeniu (przy pomocy funkcji mq_open()) np. kolejki o nazwie: /data, powstaje w katalogu: /dev/mqueue plik o odpowiedniej nazwie, w tym przypadku: data. Rozmiar takiego pliku określa ilość oczekujących komunikatów. Kolejkę zamyka się przy pomocy funkcji mq_close() a niszczy poprzez wywołanie mq_unlink(). Operacje zapisu i odczytu z kolejki realizują odpowiednio: mq_send() i mq_receive().
Pamięć współdzielona, to najszybszy mechanizm komunikacji. Po stworzeniu obiektu współdzielonego, wszystkie procesy mające do niego dostęp, mogą poprzez wskaźniki zapisywać do niego dane lub je odczytywać. Oznacza to, że dostęp jest asynchroniczny. Oczywiście programista musi zaimplementować pewne protokoły, które wyeliminują nadpisywania danych lub pobierania niekompletnych. Można to rozwiązać korzystając z dobrodziejstwa mechanizmu przesyłania komunikatów. Dla większych porcji danych, zaleca się przesyłanie wskaźnika do obszaru współdzielonego. Wątki w obrębie procesu współdzielą jego dane. Wątki z różnych procesów, aby mogły korzystać z pamięci współdzielonej, muszą ją najpierw utworzyć. Dokonuje tego funkcja shm_open(). W kolejnym kroku należy przy pomocy funkcji mmap() zmapować współdzielony region do przestrzeni adresowej procesu. Zwraca ona adres, pod którym żądana część (lub całość - dla procesorów Intel rozmiar pamięci współdzielonej to krotność rozmiaru strony, czyli 4kB) pamięci współdzielonej jest dostępna wewnątrz procesu. Dodatkowo funkcja pozwala na ustalenie podstawowych praw dostępu, jak np. tylko do odczytu oraz rodzaj mapowania jak np. spraw dany obszar moim prywatnym i wykonaj jego kopię lub uczyń go dostępnym także dla innych. Cały ten mechanizm implementowany jest przez Process Manager'a (ProcNto).
Szeregowanie zadań (ang. schedule)
Kiedy w systemie pojawia się wyjątek, przerwanie sprzętowe lub wywoływana jest procedura jądra (ang. kernell cal), to aktualnie wykonywany wątek zostaje zawieszony. Niezależnie od tego, do jakiego procesu należą, wątki podlegają tym samym zasadom szeregowania. W warunkach normalnych, wątek zawieszony, w końcu zaczyna kontynuować swoje działania. Jeśli jednak zostanie zablokowany (ang. blocked; np.: gdy musi czekać na jakieś zdarzenie, jest usuwany z kolejki tzw. zadań „gotowych”), wywłaszczony (ang. preempted; np.: gdy w systemie pojawi się ważniejsze zadanie gotowe do wykonania, wywłaszczony wątek pozostaje w kolejce tzw. zadań „gotowych”), upłynie przydzielony mu czas lub sam odda procesor (ang. yields; np.: po wywołaniu funkcji sched_yields() trafi na koniec kolejki tzw. zadań „gotowych”), to decyzję o przydziale procesora podejmie scheduler.
Każdy wątek ma swój przypisany priorytet. Algorytmy szeregujące wybierają zawsze zadanie gotowe do wykonania i najważniejsze, czyli to o najwyższym priorytecie w danej chwili (rys. 37). W systemie QNX priorytety, to liczby całkowite z zakresu od 0 (zero) do 31 (najwyższy). Wątki dziedziczą priorytet od swoich zadań macierzystych. Najniższy priorytet, ma tzw. zadanie tła (ang. idle), które jest wykonywane wtedy, kiedy w systemie nie ma nic innego „do roboty”. Zadanie idle jest zawsze gotowe do wykonania (ang. ready to run). Zadania uruchomione przez powłokę standardowo uzyskują priorytet 10.
Rysunek 37. Szeregowanie zadań w oparciu o priorytety.
Jak widać na powyższym rysunku, w systemie istnieją zadania gotowe do wykonania. Są one przechowywane w odpowiedniej kolejce i zaznaczone kolejno A-F. Zadania G-Z, są z różnych powodów w stanie „blokowane”. Aktualnie wykonywane jest zadanie A. Zadania A, B, C mają jednakowy (najwyższy w opisywanej chwili) priorytet więc dzielą dostęp do procesora w oparciu o obecny w systemie algorytm szeregowania zadań.
Kolejka zadań gotowych do wykonania jest implementowana w postaci 32 innych kolejek. Każda wartość priorytetu, to osobna kolejka. Każde zadanie, w oparciu o swój priorytet, trafia do odpowiedniej kolejki, która działa zgodnie z algorytmem „pierwszy przyszedł, pierwszy zostanie obsłużony” - FIFO.
System QNX dostarcza trzy algorytmy szeregowania zadań. Są one używane tylko wtedy, gdy istnieją równoważne (w sensie priorytetów) zadania ubiegające się o procesor. Każde zadanie w systemie może działać używając jednej z metod szeregowania. Metody te dotyczą konkretnych zadań a nie wszystkich dostępnych na danym komputerze.
Pierwszy algorytm, to zwykła kolejka FIFO. Zadanie wybrane do wykonywania, kontynuuje swoje działania, aż samo odda sterowanie (np. wątek zostanie zablokowany lub proces wywoła funkcję systemową) bądź zostanie wywłaszczone przez zadanie o wyższym priorytecie.
Drugi algorytm, opiera swoje działanie o tzw. przydział czasu procesora (ang. time slice), który wynosi 50ms. Wówczas zadanie zostaje wywłaszczone, poza warunkami opisanymi we wcześniejszym algorytmie, również wtedy, kiedy wykorzysta przydzielony mu czas.
Ostatni algorytm, to tzw. adaptacyjny (ang. adaptive scheduling). Jego działanie jest następujące. Jeśli dane zadanie wykorzysta przydzielony czas procesora i nie zostanie zablokowane, to jego priorytet jest dekrementowany. Wtedy zazwyczaj następuje przełączenie kontekstu do innego zadania. Oryginalna wartość priorytetu jest natychmiast przywracana, kiedy zadanie przechodzi do stanu „blokowane”. Algorytm ten, znajduje zastosowanie w sytuacjach, kiedy zadania wykonujące ogromne ilości obliczeń dzielą czas procesora z zadaniami interaktywnymi. Jest to pewien kompromis. Zadania obliczeniowe zyskują zazwyczaj wystarczający dostęp do „mocy obliczeniowej”, nie blokując jednocześnie tych współpracujących z użytkownikiem. Jest to domyślny algorytm dla programów uruchamianych przez powłokę.
Wybór konkretnego algorytmu szeregowania może być dokonywany przy pomocy funkcji POSIX takich jak: sched_get_scheduler(), sched_set_scheduler().
Synchronizacja zadań
Oczywiście algorytmy szeregujące podają tylko receptę na rozwiązanie problemu współzawodnictwa w systemie. Synchronizacji zadań może dokonać również sam programista. QNX dostarcza do tego celu bardzo bogatego zbioru różnych mechanizmów. Oprócz tych opisanych wcześniej, jak wywołania funkcji: Send/Receive/Reply mających „zasięg” całej sieci lokalnej, istnieją również semafory (nazwane również mają zasięg sieci lokalnej). W obrębie konkretnej maszyny, programista może sterować dostępem w oparciu np. o muteksy, zmienne warunkowe, blokady typu reader/writer. Niektóre z nich są użyteczne nawet dla wątków uruchomionych w różnych procesach.
Muteksy (opisane bardzo dokładnie w rozdziale 5.4.3) zapewniają wyłączny dostęp do współdzielonego zasobu. Mają one wbudowany mechanizm pozwalający uniknąć bardzo niekorzystnej sytuacji jaką jest inwersja priorytetów (ang. priority inversion). Jego działanie, opiera się o algorytm dziedziczenia priorytetów (ang. priority inheritance), opisany w rozdziale 9.2. Poza tym, po ustawieniu odpowiednich parametrów funkcją pthread_mutex_setrecursive(), mogą być użyte do wywołań rekurencyjnych, co pozwala uniknąć zakleszczenia (ang. deadlock).
Zmienne warunkowe (opisane bardzo dokładnie w rozdziale 5.5), są odpowiedzialne za blokowanie zadań w obrębie sekcji krytycznej w oczekiwaniu na spełnienie jakiegoś warunku. Są zawsze używane we współpracy z muteksami, co pozwala na zaimplementowanie tzw. monitora.
Semafory (opisane bardzo dokładnie w rozdziale 5.4), pozwalają kontrolować synchroniczne przebudzanie oraz usypianie wątków. Zwiększenie wartości semafora wykonuje funkcja sem_post(), natomiast jej zmniejszenie: sem_wait(). Operacja zmniejszania wartości semafora, jest możliwa bez przejścia wykonującego ją wątku do stanu blokowania, dopóki semafor ma wartość dodatnią. Semafory mogą być używane przez np. procedury obsługi sygnałów, a tzw. semafory nazwane (są wolniejsze), zapewniają synchronizację procesów uruchomionych w różnych węzłach sieci lokalnej.
Blokady typu reader/writer, to praktycznie gotowe rozwiązania dla sytuacji, kiedy pojawia się wiele zadań czytających z obszaru współdzielonego oraz jedno, które do niego pisze. Wszystkie zadania, które chcą coś odczytać (funkcja: pthread_rwlock_shared()), uzyskują dostęp do zasobu. Jeśli natomiast zadanie chce zacząć pisać nowe dane (funkcja: pthread_rwlock_exclusive()), to zostaje ono zablokowane do czasu, aż wszystkie inne skończą czytać (funkcja: pthread_rwlock_unlock()). Oczywiście może być kilka zadań „piszących”. Wówczas są one sortowane wg priorytetów oraz mają pierwszeństwo w stosunku do konsumentów. Zadania odczytujące dane nie są szeregowane wg priorytetów. Mechanizm ten nie jest implementowany bezpośrednio w jądrze i nie może służyć do synchronizacji wątków uruchomionych w obrębie różnych procesów. Składają się na niego muteksy oraz zmienne warunkowe, które już znajdują się w jądrze.
Odmierzanie czasu
System QNX przechowuje aktualny czas, dzięki czemu możliwe jest zaimplementowanie liczników odmierzających jego upływ. Kolejne zdarzenia są przechowywane w posortowanej kolejce. Każda jej pozycja ma określony punkt w czasie, w którym powinna zostać przetworzona. Po wystąpieniu przerwania, aktualny czas jest porównywany z pierwszym elementem kolejki. Jeśli jest większy lub równy, to następuje odpowiednie przetworzenie kolejki. Do zapewnienia współpracy z zegarami zostało przeznaczonych kilka funkcji:
ClockCycles() - opiera swoje działanie o 64 - bitowy licznik wysokiej precyzji architektury Intel. Dla innych procesorów, licznik taki jest emulowany w oparciu o dostępny chip. Pozwala odczytać wskazanie tego licznika.
ClockPeriod() - pozwala na ustawienie lub odczytanie okresu „tykania” systemowego zegara z dokładnością do nanosekund. Przy ustawianiu, żądana wartość jest maksymalnie zbliżana do tej, jaką może obsłużyć dostępny sprzęt. Oczywiście nie powinno się wybierać zbyt małych wartości, gdyż wtedy procesor traciłby zbyt wiele czasu na obsługę przerwań zegarowych.
ClockTick() - przeznaczona jest do symulowania przerwań zegarowych z zewnątrz jądra. Może być użyta przez jakąś zewnętrzną procedurę obsługi przerwania (np. związaną z jakimś nietypowym układem czasowym) oraz informować jądro o upływie czasu.
Dla skryptów powłoki (ang. shell), dostępne są funkcje sleep() - zatrzymująca wykonanie na okres kilku sekund oraz delay() potrafiąca określić opóźnienie na poziomie milisekund.
Poza tym, QNX/Neutrino korzysta z dobrodziejstw jakie oferuje POSIX w zakresie odmierzania czasu (opisane w rozdziałach poświęconych systemowi RTLinux). Dzięki specjalnym funkcjom możliwe jest zaprogramowanie zarówno liczników informujących o upływie danego okresu lub wystąpieniu konkretnej daty, jak i cyklicznych zegarów. Dodatkowo funkcja qnx_ticksize(), pozwala na określenie rozdzielczość zegarów z zakresu 500 mikrosekund - 50 milisekund.
Ciekawą sprawą jest to, że Neutrino pozwala na powiązanie upływu czasu (ang. timeout) ze wszystkimi blokującymi stanami jądra. Zazwyczaj z możliwości takiej korzysta się przy przesyłaniu komunikatów gdy klient wysyła coś do serwera i nie powinien czekać na jego odpowiedź w nieskończoność. Poza tym, istnieje możliwość określenia maksymalnego czasu oczekiwania na zakończenie działania określonego wątku. Do tego celu używa się funkcji TimerTimeout(), która zaczyna odliczanie, w chwili wystąpienia określonego stanu blokowania jak np.: blokada w oparciu o muteks.
Obsługa przerwań
W systemach czasu rzeczywistego, bardzo ważną sprawą jest minimalizacja czasu, jaki upływa pomiędzy wystąpieniem jakiegoś zewnętrznego zdarzenia, a wywołaniem procedury związanej z jego obsługą. W zasadzie można mówić o dwóch źródłach takiego opóźnienia (ang. latency). Pierwsze, jest związane z szybkością reakcji jądra na asynchroniczne zdarzenia zewnętrzne. W systemie QNX jest ono bardzo małe i na komputerze Pentium 100MHz wynosi niecałe 2 mikrosekundy. Oczywiście, w najgorszym przypadku, może być nieznacznie powiększone o czas, na jaki przerwania zostały wyłączone. Drugi rodzaj opóźnień, wiąże się z działaniem schedulera. Często niskopoziomowa procedura obsługi przerwania musi zlecić coś do wykonania przez odpowiedni sterownik wysokiego poziomu. Wówczas generuje ona zdarzenie. Kontekst aktualnie wykonywanego procesu jest zapamiętywany oraz sterowanie przekazywane jest odpowiedniej funkcji sterownika. Dla komputera z procesorem Pentium 100MHz, opóźnienie takie wynosi ok. 5 mikrosekund i związane jest z czasem, jaki upływa pomiędzy wywołaniem ostatniej instrukcji procedury obsługi przerwania a pierwszej wykonanej przez wątek sterownika.
QNX również wspiera przerwania zagnieżdżone. Ponieważ przerwania posiadają swoje priorytety, możliwe jest, że jedno wywłaszczy inne. Sytuację taką i związane z nią opóźnienia przedstawia rys. 38.
Rysunek 38. Przerwania zagnieżdżone i ich obsługa.
Jak widać, wykonanie wątku A zostaje przerwane, ponieważ w systemie pojawia się przerwanie IRQx. Uruchamiana jest jego procedura obsługi (INTx), która z kolei jest przerywana przez wystąpienie przerwania o wyższym priorytecie: IRQy i jego procedurę obsługi INTy. INTy generuje zdarzenie, które uruchamia wątek B. Po jego zakończeniu, procedura INTx uruchamia wątek C. Na koniec, wątek A może kontynuować swoje obliczenia.
System QNX dostarcza całe zaplecze funkcji API zgodnych z POSIX, dzięki którym możliwe jest programowe implementowanie obsługi przerwań. Uprzywilejowany użytkownik może wywołać funkcję InterruptAttach(), dzięki której zainstaluje procedurę obsługi, dla konkretnego przerwania sprzętowego. Każde przerwanie, może mieć kilka uchwytów. Dostępne są również funkcje pozwalające wyłączyć przerwania, odinstalować uchwyt lub realizujące operacje maskowania.
Kiedy przerwanie ma miejsce, sterowanie przejmuje odpowiedni moduł mikrojądra - Interrupt Redirector. Kontekst aktualnie wykonywanego wątku jest zapamiętywany na stosie. Procedura obsługi przerwania uzyskuje dostęp do kodu i danych wątku, do którego należy. Zapisuje ona swoje wyniki np. do wspólnych buforów. Poza tym, procedura obsługi przerwania ma bezpośredni dostęp do tego sprzętu, do którego ma dostęp dany uprzywilejowany wątek. Sprawia to, że sterowniki urządzeń nie muszą być wbudowywane w jądro. Ich działanie musi być możliwie najkrótsze i sprowadzać się do prostych operacji, jak np.: pobierz bajt z urządzenia. Dalsze, bardziej skomplikowane prace, mogą zostać zlecone do wykonania przez konkretny wątek. Dokonuje się tego poprzez zgłoszenie odpowiedniego zdarzenia. Ważną cechą jest to, że procedura obsługi przerwania może zostać napisana w języku wysokiego poziomu oraz stać się częścią procesu użytkownika. Prostym wnioskiem płynącym z takiego podejścia jest to, że przerwania w systemie QNX są obsługiwane na poziomie procesów, a nie na poziomie jądra. Po jego nadejściu są uruchamiane wszystkie zarejestrowane procedury. Procedura obsługi danego przerwania może zostać wywłaszczona przez przerwanie o wyższym priorytecie.
Procedury obsługi przerwań zwracają identyfikator proxy, które ma zostać pobudzone. Jeśli zwróci 0 (zero), to znaczy, że żaden ma nie być pobudzany. Poza tym, należy pamiętać, że procedury obsługi przerwań powinny być uruchamiane w trybie uprzywilejowanym.
Struktura systemu QNX
Jak już wspomniano wcześniej, architektura systemu oparta jest o mikrojądro, które wykonuje najbardziej podstawowe operacje. Jego obowiązki praktycznie sprowadzają się do szeregowania zadań oraz przekazywania komunikatów. Usługi związane z plikami czy siecią, są realizowane przez cztery osobne procesy: Process Manager (Proc) - zarządza aktywnymi zadaniami; Filesystem Manager (Fsys) - odpowiada za system plików; Device Manager (Dev) - sterowniki urządzeń; Network Manager (Net) - obsługa sieci. Strukturę taką przedstawia rys. 39. Warto również jeszcze raz powtórzyć, że procesy systemowe nie mają żadnej uprzywilejowanej pozycji. Programiści mogą tworzyć dodatkowe usługi systemowe, zwiększając jednocześnie funkcjonalność całego systemu.
Rysunek 39. Współpraca mikrojądra i procesów zarządzających.
Taka budowa modułowa pozwala na dynamiczne konfigurowanie systemu, zależnie od wymagać zewnętrznych. Struktura modułowa, ma także bardzo istotne znaczenie przy realizacji rozwiązań sieciowych. Zadanie można załadować w dowolnym węźle sieci. Ma ono „przeźroczysty” dostęp do dowolnego zasobu znajdującego się w dowolnym węźle sieci. Poza tym, nie ma potrzeby instalacji dodatkowego oprogramowania sieciowego. Współpraca z nią, jest wbudowana w sam system i zajmuje się tym moduł NET.
Zadrądca procesów (ang. Process Manager - Proc)
Zarządca procesów (Proc; pid == 1), działa na poziomie uprawnień mikrojądra. Dzieli z nim przestrzeń adresową, jednakże podlega takim samym regułom, jak wszystkie inne procesy (np. przy szeregowaniu). Musi być zawsze obecny w systemie i jest jedynym modułem, którego nie można usunąć. Obsługuje przestrzeń nazw procesów, jest odpowiedzialny za ich tworzenie, zarządza ich zasobami oraz dodaje i usuwa procedury obsługi przerwań. Wszystkie te usługi są dostępne za pośrednictwem komunikatów. Przykładowo, jeśli jakieś wykonywane zadanie chce utworzyć nowy proces, to wysyła do modułu Proc komunikat opisujący go. Ponieważ przesyłanie komunikatów może być realizowane w skali całej sieci lokalnej, dlatego też, istnieje możliwość uruchomienia nowego procesu na dowolnym węźle sieci. Do tworzenia nowych procesów, API systemu QNX dostarcza trzech metod:
fork() - tworzy kopię bieżącego procesu. Nowy proces dostaje kopię danych procesu, który wywołał tę funkcję.
exec() - tworzy nowy proces i zastępuje nim proces wywołujący tę funkcję. Po jej wykonaniu nie ma już powrotu. Nowy proces otrzymuje identyczne ID jak ten, który je utworzył oraz dziedziczy część jego środowiska (np. oczekujące sygnały).
spawn() - tworzy nowy proces potomny. Praktycznie jej realizacja logicznie odpowiada wywołaniu funkcji fork() a później z poziomu procesu - kopii, funkcji exec(). W praktyce, spawn() o wiele szybciej realizuje to zadanie. Poza tym, spawn() potrafi stworzyć proces na dowolnym węźle sieci.
Proces w swoim cyklu życiowym przechodzi przez cztery fazy: stworzenie (ang. creation) - przydzielenie nowego ID oraz określenie jego środowiska; załadowanie (ang. loading) - przygotowanie do wykonania; wykonanie (ang. execution) - współzawodnictwo o dostęp do procesora. Procesy potomne konkurują również ze swoimi procesami macierzystymi. Nie są z nimi specjalnie powiązane. Zabicie jednych nie niszczy drugich. Ostatnia faza cyklu życiowego procesu związana jest z jego zakończeniem (ang. termination). Może to nastąpić poprzez dostarczenie do procesu odpowiedniego sygnału bądź poprzez bezpośrednie wywołanie funkcji exit() lub pośrednie - gdy swoje działanie zakończy funkcja main(). W efekcie, zwolnione zostają wszelkie otwarte przez proces pliki oraz zwolnione zostają zasoby, takie jak np.: połączenia wirtualne (ang. virtual circuits), bufory pamięci, zegary (ang. timers) itp. Oczywiście informacja o zakończeniu danego procesu jest także wysyłana do jego procesu macierzystego.
System QNX implementuje kilka stanów, w których może znaleźć się proces. Są one przedstawione na rys. 40.
Rysunek 40. Stany procesów implementowane w QNX.
Stan READY oznacza, że proces jest zdolny do wykonania. Nie jest blokowany z żadnych powodów oraz może zostać wybrany przez scheduler'a oraz uzyskać dostęp do procesora.
Stany SEND - blocked, REPLY - blocked, RECEIVE - blocked, SIGNAL - blocked, SEMAPHORE - blocked oraz WAIT - blocked, to mające różne przyczyny stany blokowania. Zazwyczaj wiążą się z wywołaniem określonych funkcji lub wykonaniem operacji np. próby opuszczenia semafora. Szczególnej uwagi wymaga tylko stan WAIT - blocked, który jest wynikiem oczekiwania na status od jednego z procesów potomnych.
Stan HELD oznacza, że proces otrzymał sygnał SIGSTOP. Nie może wówczas konkurować o procesor. Aby wznowić proces, należy mu wysłać sygnał SIGCONT.
Proces znajdujący się w stanie DEAD, to tzw. proces zombie. Pamięć przez niego zajmowana została już zwolniona ale proces nie może się zakończyć, gdyż proces macierzysty nie odebrał jeszcze jego kodu powrotu. Proces macierzysty może go usunąć poprzez wywołanie funkcji wait() lub waitpid(). Ewentualnie zombie zostanie usunięty, kiedy jego proces macierzysty zakończy działanie.
System QNX dostarcza kilku mechanizmów, dzięki którym można określić w jakim stanie znajduje się określony proces. Z poziomu powłoki (ang. shell), udostępnione są polecenia ps oraz sin. Dla programistów, przeznaczona jest funkcja qnx_psinfo().
Zarządca systemu plików (ang. Filesystem Manager - Fsys)
Zarządca systemu plików (Fsys), to komponent opcjonalny w systemie. Dostarcza podstawowych usług umożliwiających przechowywanie danych w podsystemach urządzeń dyskowych. Obsługuje takie operacje, jak otwieranie, zamykanie, zapisywanie, czytanie itp. Implementacja systemu plików w postaci modułu oznacza, że może on być dynamicznie włączany oraz wyłączany. Poza tym, wiele systemów plików może działać jednocześnie.
System plików systemu QNX składa się z kilku komponentów. Znajdują się one na samym początku partycji, na której system ten zainstalowano. Względnie taka struktura może zostać utworzona jako efekt inicjalizacji systemu plików poleceniem dinit. Są nimi kolejno:
blok programu ładującego (ang. loader block) - kod ładowany i wykonywany przez BIOS komputera. Jego zadaniem jest załadowanie systemu operacyjnego z partycji. Blok ten znajduje się w pierwszym fizycznym bloku partycji lub pierwszym fizycznym bloku dysku (gdy np.: nie podzielono go na partycje).
główny blok (ang. root block) - ma strukturę zwykłego katalogu. Zawiera informacje (np. w którym bloku dyskowym się znajduje) o podstawowym katalogu całego systemu plików (ang. root directory; /) oraz plikach: /.inodes (dalej w tym podrozdziale), /.boot oraz /.altboot. Te dwa ostatnie pliki, zawierają obraz systemu. Jeśli plik /.altboot nie jest pusty, to przy starcie użytkownik jest pytany, który obraz załadować.
mapa bitowa (ang. bitmap) - zawiera mapę wszystkich bloków na dysku. Określa, który blok jest używany, a który wolny. Jest przechowywana w pliku /.bitmap.
katalog główny (ang. root directory; /) - jest to najwyższy w hierarchii katalogów. Z jego poziomu, oba skróty o postaci „.” oraz „..” wskazują to samo miejsce - katalog główny.
dalej znajdują się inne katalogi, pliki, wolne bloki itp.
Dla każdego pliku są przechowywane data jego utworzenia, ostatniej modyfikacji, ostatniego zapisu oraz ostatniego odczytu. Poza tym, z każdym plikiem jest skojarzony tzw. i - węzeł (ang. inode). Jest to struktura danych, która go opisuje. Poza określeniem jego typu (plik zwykły, katalog, plik urządzenia), zawiera także identyfikator właściciela (UID) oraz wykaz bloków dyskowych i ich fragmentów, z których plik ten się składa. I-węzły nie posiadają nazw. Związek pomiędzy nazwą pliku a i-węzłem jest określany we wpisie katalogowym.
Dane, które są zawarte w pliku, mogą być dostępne poprzez jego nazwę. Oczywiście nazw takich może istnieć kilka - każda to tzw. dowiązanie (ang. link). Można je tworzyć np. przy pomocy polecenia ln lub funkcji link(). Jak już wspomniano wcześniej, wszelkie dane dotyczące pliku, poza jego nazwą, znajdują się w strukturze zwanej i - węzłem. Jeśli dany plik ma tylko jedną nazwę (jedno dowiązanie), to informacje o pliku są przechowywane rzeczywiście we wpisie katalogowym. Jeśli natomiast dany plik ma wiele dowiązań (lub ma jedno ponieważ inne usunięto, ale były wcześniej), to i - węzeł jest przechowywany w postaci struktury, w pliku o nazwie /.inodes. Również, jeśli nazwa pliku przekracza 16 znaków, to informacje o pliku są przechowywane zamiast we wpisie katalogu, to w pliku /.inodes, co pozwala zwiększyć jej rozmiar do 48 znaków. Należy zwrócić uwagę na to, że opisywane dowiązanie oraz plik muszą się znajdować w obrębie tego samego systemu plików.
Ważną sprawą jest również podejście do usuwania dowiązań. Otóż, dane pliku są dopiero wtedy usuwane z dysku, kiedy nie istnieje do nich żadne dowiązanie. Dlatego też, takie dowiązania zwykło się nazywać dowiązaniami twardymi (ang. hard linkd).
Oprócz dowiązań twardych, istnieją również dowiązania symboliczne (ang. symbolic links). Nie podlegają one już tak rygorystycznym zakazom. Po pierwsze, mogą wskazywać na katalogi oraz pliki porozmieszczane w obrębie różnych systemów plików. Po drugie, pliki przez nie wskazywane mogą być dowolnie usuwane. Linki symboliczne przechowują tylko ścieżkę dostępu. Tworzone są przy pomocy polecenia ln z opcją -s.
W systemie QNX, pliki regularne (nie mają predefiniowanej struktury, dostęp do kolejnych bajtów może być losowy) oraz katalogi (zawierają np. informacje potrzebne do lokalizowania plików regularnych) są przechowywane w postaci sekwencji regionów dyskowych, na które składa się ciągły zbiorów bloków (taki ciągły zbiór bloków to z ang. extent). Jeśli na dany plik składa się tylko jeden extent, to informacje o nim są przechowywane we wpisie katalogowym. Jeśli natomiast jest ich więcej, to informacje o każdym są przechowywane w specjalnej tablicy. Przy czym istnieje pewne ograniczenie. Tablica taka może mieć maksymalnie 60 wpisów. Jeśli rozmiar danego pliku musi zostać zwiększony, to w oparciu o tablicę /.bitmap wybierany jest pierwszy (możliwie najbliższy - by zapewnić ciągłość), spełniający wymagania, wolny obszar. Podejmowana jest próba rozszerzenia ostatniego extent - a i jeśli się nie uda, alokowany jest kolejny.
Moduł Fsys potrafi także operować na specjalnych plikach, które mają strukturę blokową (ang. block special files). Reprezentują one tzw. urządzenia blokowe, czyli dyski twarde, napędy taśmowe, partycje dyskowe itp. Są one widziane przez system jako sekwencja ponumerowanych (zaczynając od 1) bloków, każdy o rozmiarze 512 bajtów. Takie podejście znacznie upraszcza współpracę z tego typu urządzeniami i sprowadza do operacji analogicznych do tych przeznaczonych dla plików.
QNX został zbudowany w oparciu o pewne standardy. Tak więc, jeden dysk może być współdzielony przez wiele różnych systemów plików. Tablica partycji jest przechowywana w pierwszym bloku dyskowym i może definiować do 4 partycji głównych (ang. primary partitions). Każda partycja musi mieć określony typ. Ze względu na traktowanie przez system urządzeń blokowych jako pliki, możliwe jest uzyskanie dostępu do całego dysku (pomijając strukturę partycji), jak i również do każdej partycji osobno. Podziału na partycje dokonuje się standardowo przy pomocy narzędzia fdisk.
System plików, opiera się o tzw. prefiksy. Rejestracja nowego prefiksu następuje wraz z utworzeniem nowego katalogu lub pliku. Prefiksy te, mogą na siebie zachodzić. Zawsze wygrywa ten, który ma dłuższą ścieżkę. Jeśli sterownik dla danego kontrolera dyskowego jest uruchomiony, to Fsys automatycznie zarejestruje prefiksy, które zdefiniują pliki blokowe dla każdego podpiętego dysku. Przykładowo jeśli w systemie są dwa dyski IDE, to w wyniku takiej operacji, zostaną zdefiniowane dwa pliki blokowe: /dev/hd0 i /dev/hd1. Następnie trzeba zarejestrować prefiksy, które zdefiniują partycje dostępne na każdym z dysków. Do tego celu używa się polecenia mount, które dla powyższej, przykładowej sytuacji ma postać:
mount -p /dev/hd0 -p /dev/hd1.
W efekcie, powstaną kolejne pliki w katalogu /dev. Każdy będzie odpowiadał jednej partycji na konkretnym dysku. Aby móc z nich korzystać, należy je zamontować do odpowiednich katalogów. Służy do tego również polecenie mount, którego wywołanie może mieć postać np.: mount /dev/hd0t77 / - skojarzenie partycji hd0t77 z katalogiem głównym. Działanie odwrotne, ma polecenie umount. Przykładowo wywołanie: umount /dev/hd0t77 sprawi, że pliki na podanej partycji nie będą dostępne.
Oczywiście system plików wykonuje pewne podstawowe działania optymalizujące i zwiększające wydajność. Przede wszystkim, optymalizowana jest trasa głowicy dyskowej np.: poprzez sortowanie operacji dyskowych (np. od najniższych adresów). Kolejną sprawą jest tworzenie buforów pamięci podręcznej (ang. buffer cache). Przechowują one bloki danych oraz zmniejszają liczbę odwołań zarządcy systemu plików do fizycznego urządzenia. Domyślnie, rozmiar pamięci podręcznej jest ograniczony rozmiarem fizycznej pamięci dostępnej w systemie. Po wykonaniu operacji zapisu, dane najpierw trafiają do cache, a dopiero później (zwykle przed upływem 5 sekund) są przenoszone na dysk. W pamięci podręcznej przechowywane są również często odczytywane bloki. Niestety, przechowywanie danych w pamięci RAM, narażone jest na niebezpieczeństwo ich utraty (np.: w przypadku wystąpienia braku zasilania). Aby tego uniknąć, procesy mają prawo zażyczyć sobie natychmiastowy zapis danych na dysk. Wtedy, procesor jest zwalniany dopiero, po dokonaniu operacji zapisu, gdyż jest to operacja blokująca. QNX korzysta również z plików tymczasowych. Rezerwuje dla nich pewne bloki w cache oraz przenosi je na dysk tylko wtedy, kiedy jest to konieczne. Fsys ma także możliwość stworzenia tzw. symulowanego dysku w obszarze pamięci RAM (ang. ramdisk), którego rozmiar może wynosić maksymalnie 8MB. Dzięki technice przesyłania komunikatów, dane w nim zawarte mogą być przenoszone bezpośrednio do buforów aplikacji, z pominięciem cache. Obsługa RAM - dysków jest wbudowane w Fsys a nie implementowana w postaci osobnych sterowników.
Zarządca ten stosuje mechanizm pracy z płynnym priorytetem. Polega to na tym, że po otrzymaniu komunikatu, zaczyna on pracę z priorytetem jego nadawcy. Dzięki temu, operacje dyskowe, które mają niski priorytet, nie będą spowalniać innych. Inaczej, gdyby Fsys działał zawsze z najwyższym priorytetem, nie byłoby możliwe segregowanie np.: kolejności zapisu.
System plików podejmuje pewne działania, mające na celu zapewnienie wystarczającego poziomu bezpieczeństwa. Otóż, najbardziej krytyczne dane, jak np.: modyfikacje i - węzłów, aktualizacje katalogów itp. są zapisywane na dysku natychmiast, z pominięciem pamięci podręcznej. Takie postępowanie zapewnia, że struktura systemu plików jest zawsze spójna.
Bardzo ciekawą sprawą jest to, że możliwe jest również korzystanie z plików rozlokowanych na innych węzłach sieci lokalnej. Jeśli np.: wystąpi żądanie dostępu do pliku //5/dev/null (znajduje się on na węźle nr 5 w sieci), to jego poszukiwania będą realizowane na maszynie zdalnej. Po odnalezieniu procesu, który obsługuje ten plik, żądania będą kierowane do niego, przy pomocy procesu wirtualnego (szerzej o komunikacji sieciowej dalej w pracy, przy okazji omawiania modułu Net).
W systemie QNX dostępni są również inni zarządcy systemami plików. Potrafią oni operować na strukturach charakterystycznych m. in. dla systemu DOS, napędów CD - ROM, pamięci Flash.
Zarządca sieci (ang. Network Manager - Net)
Zarządca sieci (Net) odpowiada za przesyłanie komunikatów przez sieć. Dokładniej, to proces ten zleca transmisje sieciowe innym, obsługującym sprzęt sieciowy. Jego współpraca z mikrojądrem jest bardzo ścisła. Korzysta ono z jego pośrednictwa przy przesyłaniu komunikatów do różnych węzłów. Network Manager nie jest wbudowany w obraz systemu. Stanowi odrębny moduł, który może być dowolnie ładowany bądź usuwany. Po uruchomieniu, Network Manager informuje o swoim istnieniu dwa główne moduły systemu. Są nimi: Proc - odpowiedzialny za zarządzanie procesami oraz mikrojądro - realizujące komunikację między nimi. W następnym kroku, aktywowane są zawarte w tych modułach fragmenty kodu, odpowiedzialne za komunikację w sieci i zdalne tworzenie zadań (w innych węzłach). Oznacza to, że te dwie funkcje nie są realizowane przez kolejną warstwę dodawaną do systemu operacyjnego. Zostały zintegrowane w możliwie najniższej warstwie systemu (najbliżej sprzętu). Dzięki temu, osiągnięto swoistą „przeźroczystość” sieci, a system QNX można zakwalifikować jako rozproszony (ang. distributed operating system). Dowolny zasób sieci, jak np.: dysk, terminal, czy nawet procesor, jest dostępny dla dowolnego procesu uruchomionego w dowolnym jej węźle. Wszystkie stacje współpracują ze sobą, tworząc jeden logiczny komputer. Dla porównania, w systemach sieciowych (ang. networked operating systems), moduł komunikacji międzyprocesowej (IPC) znajduje się poniżej warstwy obsługującej system plików (ang. filesystem). Pozwala to zorganizować sieciowy system plików. Wszelkie inne funkcje, związane np.: z uruchomieniem procesu w zdalnym węźle, wymagają jednak wykonania przez aplikację specjalnych czynności. W systemie QNX, zdalne uruchomienie procesu w dowolnym węźle sieci, wydaje się być zdolnością naturalną. Dodatkowo proces taki może odziedziczyć środowisko procesu macierzystego. Przykładowe polecenie:
//1 //10/bin/convert <//2/user/Marcin/data.in >//3/dev/par
załaduje z katalogu /bin węzła nr 10 program o nazwie convert oraz wykona go w węźle nr 1. Program ten będzie pobierał dane z pliku data.in, który znajduje się w odpowiednim katalogu na węźle nr 2. Wyniki natomiast, trafią na drukarkę podłączoną do stacji nr 3. Oczywiście aby takie polecenie zadziałało, użytkownik musi posiadać odpowiednie uprawnienia.
Opisane już we wcześniejszych rozdziałach funkcje: send(), receive(), reply() mogą być z powodzeniem stosowane do komunikacji na skalę całej sieci lokalnej QNX. Mikrojądro oraz zarządca procesów komunikują się z Net przy pomocy specjalnej kolejki. Zawiera ona spis kolejnych transakcji do wykonania oraz konkretne dane dla funkcji je realizujących. Powstaje również specjalny bufor (ang. virtual circuit buffer; dalej określany jako bufor VC), dla połączeń wirtualnych między węzłami, który zawiera przesyłane dane. Zarządca sieci realizuje przesyłanie komunikatów przez sieć przy pomocy specjalnych sterowników (ang. drivers). To one wykonują całą pracę związaną np.: z pakietowaniem, kolejkowaniem i ewentualną retransmisję danych. Przykładowo, kiedy proces wywołuje funkcję send(), dane do przesłania są najpierw umieszczane przez mikrojądro w buforze skojarzonym z ustanowionym połączeniem wirtualnym. Następnie kolejkowana jest transakcja, która zawiera m. in. dane o nadawcy i odbiorcy oraz przesyłany jest komunikat do procesu Net, który informuje o pojawieniu się nowego obowiązku. Proces ten pobiera konieczne dla komunikacji informacje oraz przekazuje je do odpowiedniego sterownika sieciowego. Ten z kolei, pobiera dane z bufora VC, opakowuje je i realizuje ich transmisję. Przy realizacji funkcji receive(), występuje sytuacja odwrotna. To sterownik jako pierwszy umieszcza odebrane dane w buforze VC oraz informuje o tym fakcie proces Net. Ten z kolei, informuje dalej mikrojądro, które przenosi dane z bufora VC do buforów odpowiedniego procesu.
Ważną sprawą jest to, że zarządca sieci nie zawiera kodu specyficznego dla konkretnych urządzeń. Jak już zostało zaznaczone, to sterownik komunikuje się bezpośrednio z adapterem sieciowym. Dzięki temu, jeśli na rynku pojawi się np.: nowe urządzenie sieciowe, trzeba będzie tylko napisać dla niego nowy driver. Możliwe jest istnienie w systemie np.: kilku kart współpracujących z tymi samymi lub różnymi sieciami.
Węzły w sieci lokalnej QNX są identyfikowane przy pomocy dwóch numerów. Pierwszy, to tzw. numer fizyczny (ang. physical node ID). Określany jest przez fizyczny numer adaptera sieciowego. Jest zależny od przyjętego standardu oraz niejednokrotnie nie pozwala jednoznacznie określić wszystkich stacji lub ze względu na swoją różnorodność, prowadzi do niezgodności formatów. Aby takich problemów uniknąć, wprowadzono tzw. numerację logiczną (ang. logical node ID), która jest jako jedyna widoczna dla procesów. Numer logiczny, to kolejna liczba całkowita, począwszy od 1, która jednoznacznie identyfikuje dany węzeł sieci QNX. Takie podejście pozwala np.: na bardzo łatwe odpytanie (ang. poll) wszystkich stacji przy pomocy zwykłej pętli. Możliwa jest sytuacja, że dany węzeł posiada kilka numerów fizycznych (np.: został wyposażony w kilka kart sieciowych). Wówczas mapowania pomiędzy jedną a drugą numeracją dokonuje Net. Poza tym, sterownik otrzymuje także numer fizyczny.
Adaptery jednego typu, w które zostały wyposażone różne węzły, tworzą tzw. sieć logiczną (ang. logical network). Każdy węzeł natomiast, może korzystać z kilku sieci logicznych. Sytuacja taka została przedstawiona na rys. 41, gdzie węzły 5 i 6 potrafią korzystać ze wszystkich dostępnych rodzajów sieci. Węzeł 7 natomiast, nie ma karty zgodnej ze standardem Ethernet.
Rysunek 41. Węzły mające dostęp do różnych sieci logicznych.
Bardzo ciekawym faktem jest to, że zarządca sieci umożliwia dowolnej stacji funkcjonowanie jako most (ang. bridge), łączący dwie sieci QNX. Istnieje jednak pewne ograniczenie - mogą to być tylko Ethernet, Token Ring lub FDDI.
Przetwarzanie rozproszone stawia wysokie wymagania sieci lokalnej. Dzięki strukturze modułowej systemu QNX, możliwe stało się zaimplementowanie bardzo wydajnej sieci o nazwie FLEET (ang. Fault-tolerant, Load-balancing, Efficient, Extensible, Transparent). Poszczególne człony tego skrótu są rozumiane w następujący sposób:
Fault - tolerant - sieć jest odporna na awarie. Jeśli poszczególne węzły są połączone przy pomocy kilku sieci logicznych, to po awarii jednej z nich, proces Net automatycznie zacznie korzystać z innej. Takie przekierowanie jest realizowane automatycznie i bez udziału komunikujących się procesów.
Load - balancing - sieć korzysta z mechanizmów wyrównywania obciążenia. Jeśli np.: zaistnieje sytuacja, w której wąskim gardłem staje się konkretna sieć logiczna, to wówczas zarządca sieci może dodatkowe pakiety skierować na inną, dostępną sieć logiczną. Wówczas, jednoczesna praca kilku sieci logicznych przyczyni się do zwielokrotnienia ogólnej przepustowości.
Efficient - sieć działa efektywnie. W zależności od potrzeb, wykorzystywane są np.: gwarantujące bardzo wysokie transfery karty zgodne z Ethernet lub zapewniające determinizm transmisji karty zgodne z Arcnet czy Token Ring. Poza tym, podejmowane są takie działania, aby jak najlepiej wykorzystać możliwości dostępnego sprzętu.
Extensible - sieć jest łatwo rozszerzalna. Ponieważ system składa się z modułów, w każdej chwili można napisać kolejny sterownik oraz podłączyć dany węzeł do nowej sieci logicznej.
Transparent - sieć jest „przeźroczysta”. Zostało to już wiele razy wyjaśnione. Tutaj zostanie jednak podkreślona najważniejsza cecha: w systemie QNX nie ma różnicy pomiędzy komunikacją lokalną a sieciową.
W systemie QNX został także zaimplementowany protokół TCP/IP oraz udostępnia on API dotyczące operacji na gniazdkach sieciowych. Moduł, który nim zarządza nazywa się Sockets Resource Manager. Może być, tak samo jak i inne, dynamicznie uruchamiany i zatrzymywany w trakcie działania systemu. Ciekawą sprawą jest, że może on być również dostępny dla aplikacji uruchomionych w innych węzłach. Pozwala to uniknąć konieczności instalacji adapterów zgodnych z Ethernet na każdej stacji. Wystarczy, że jedna go zawiera i już wszystkie inne będą mogły korzystać z usług TCP/IP. Poza tym, aplikacje które korzystają z TCP/IP, komunikują się z siecią także poprzez zarządcę Net.
Zarządca urządzeń (ang. Device Manager - Dev)
Zarządca urządzeń (Dev) jest odpowiedzialny za obsługę znakowych urządzeń wejścia - wyjścia, które mogą być podłączone zarówno do portów szeregowych (np. terminale, modemy), jak i równoległych (np. drukarki). Poza tym, obsługuje także sterowniki monitorów ekranowych. Stanowi interfejs pomiędzy urządzeniami terminalowymi a procesami. Zgodnie ze standardem POSIX, urządzenia takie znajdują się w katalogu /dev a transmisja danych z nimi, opiera się o sekwencję bajtów. Nie są one organizowane w żadne bloki. Są przesyłane jeden za drugim - szeregowo (ang. serial).
Aplikacje korzystają z tych urządzeń jak ze zwykłych plików. Korzystają do tego celu ze standardowych funkcji jak np.: open(), clode(), read(), write(). QNX dostarcza również dodatkowe metody, które pozwalają uwzględnić charakterystyczne cechy takich urządzeń jak np.: bity stopu, parzystość itp.
Przepływem danych pomiędzy aplikacją a danym urządzeniem znakowym steruje oczywiście Dev. Dane te mogą podlegać wstępnej obróbce. Jej forma, zależy od zawartości tzw. terminalowej struktury kontrolnej (ang. terminal control structure) o nazwie termios. Z każdym urządzeniem znakowym jest skojarzona osobna taka struktura. Zmiany zawartości jej pól można dokonać przy pomocy narzędzia stty lub funkcji: tcgetattr()/tcsetattr().
Dość istotne znaczenie, ma zawarty w tej strukturze bit o nazwie ICANON. Jeśli ma przypisaną wartość 1, to dane otrzymane z urządzenia znakowego stają się dostępne dopiero, po odebraniu całej linii tekstu, czyli zazwyczaj kiedy kolejnym otrzymanym znakiem będzie CR (ang. Carriage Return) - znak nowej linii. Jeśli natomiast bit ten ma wartość 0 (zero), to wówczas dane urządzenie jest traktowane jako „surowe” (ang. raw). Wówczas wszelkie otrzymane z urządzenia dane są natychmiast dostępne dla procesów. Przy czytaniu z takiego „surowego” urządzenie, istnieje możliwość określenia pewnych warunków, względem których, otrzymane dane są akceptowane. Tak więc, można podać np.: minimalną liczbę odebranych znaków, maksymalny czas trwania przerwy w transmisji czy maksymalny czas oczekiwania na jej rozpoczęcie.
Z każdym urządzeniem skojarzone są trzy kolejki FIFO (rys. 42). Podobnie jak to miało w przypadku zarządcy sieciowego (Net), tak i tutaj, niskopoziomową obsługą urządzeń zajmują się specjalne sterowniki. Jeśli otrzymane zostaną dane, to sterownik umieszcza je w kolejce wejściowej (ang. input queue). Ich dalszym przetworzeniem zajmuje się już zarządca Dev. W odwrotnej sytuacji, kiedy jakieś informacje mają trafić do urządzenia, najpierw są umieszczane przez Dev w kolejce wyjściowej (ang. output queue) a odpowiedni sterownik przekazuje je dalej, do fizycznego urządzenia. W obu przypadkach, zarządca urządzeń i sterownik komunikują się ze sobą poprzez wywołanie odpowiednich metod. Metoda taka, oparta o pomocnicze bufory, pozwala na uniknięcia sytuacji, w których np.: sterownik „nie nadąża” wysyłać napływających od procesu danych.
Trzecia kolejka, tzw. kanoniczna (ang. canonical queue) jest wykorzystywana tylko wtedy, kiedy dane napływające z urządzenia mają być interpretowane. Jest ona w całości zarządzana przez Dev. Jej rozmiar określa maksymalny rozmiar linii, jaka może zostać przetworzona przez konkretne urządzenie.
Rozmiary tych trzech kolejek mogą być zmieniane. Jedynym ograniczeniem jest, aby ich sumaryczny rozmiar nie przekroczył 64KB.
Rysunek 42. Komunikacja procesów z urządzeniami znakowymi w QNX.
Wysoka wydajność podsystemu związanego z urządzeniami została osiągnięta dzięki dwóm głównym regułom. Otóż, zarządca Dev, tylko wtedy zaznacza swoją obecność, kiedy np.: na dane znajdujące się w kolejce wejściowej oczekuje jakiś proces. Poza tym, otrzymane dane, proces pobiera bezpośrednio z kolejki, co sprawia, że są one kopiowane tylko raz.
Dla konsoli używany jest sterownik o nazwie Dev.con. Porty szeregowe są obsługiwane przez Dev.ser, natomiast drukarka jest obsługiwana przez sterownik dla portu równoległego: Dev.par, który umożliwia tylko jednokierunkową transmisję danych - do urządzenia. Tworzona jest także tylko jedna kolejka - wyjściowa (ang. output queue).
Interfejs graficzny QNX (ang. Graphical User Interface)
Środowisko graficzne Photon MicroGUI to rewelacyjne osiągnięcie firmy QNX. Jest ono bardzo małe i szybkie. Jednocześnie stanowi kompletne rozwiązanie interfejsu graficznego. W swojej wersji podstawowej, przeznaczonej dla systemów wbudowanych, Photon zajmuje jedynie 256kB pamięci operacyjnej. Jego architektura jest modułowa i opiera się o mikrojądro, którego rozmiar wynosi 45kB. Podstawą Photon - a, jest nadzorowana przez jego jądro tzw. graficzna przestrzeń zdarzeń. W jej ramach inne procesy mogą umieszczać prostokątne obiekty zwane regionami. Manipulacja nimi odbywa się za pomocą komunikacji z jądrem. W ten sposób procesy mogą wykonywać np.: operacje na oknach. Niewątpliwą zaletą Photon - a jest to, że nie zakłóca on pracy innych, ważniejszych procesów. Mogą one wywłaszczyć GUI (ang. Graphical User Interface).
Dodatkowo można doinstalować serwer X-Window (xph), który pozwoli na uruchamianie programów przeniesionych prosto z systemów UNIX/Linux. Cały interfejs użytkownika przypomina swoim wyglądem nieco ten znany z systemów Windows.
Zastosowania systemu QNX
System QNX znajduje swoje zastosowanie głównie w trzech dziedzinach:
w systemach sterowania i monitorowania, czego przykładem może być np. sterowanie procesem produkcji;
przy przetwarzaniu transakcji w trybie online, np.: obsługa wpłat przy pomocy kart kredytowych;
w systemach bezwzględnego nadzoru jak np.: kontrola elektrowni jądrowej.
Z systemu tego korzysta wiele znanych firm jak np.: Ford, Goodyear, Hewlett Packard, Intel czy NASA.
Windows Embedded
Microsoft Windows Embedded Systems, to grupa systemów operacyjnych, które znajdują zastosowanie przede wszystkim we wszelkiego rodzaju urządzeniach współczesnej elektroniki i automatyki. Na czele tej grupy stoją dwie najnowsze produkcje o nazwach: Windows XP Embedded oraz Windows CE .NET. Oprogramowanie to, dzięki nowoczesnym rozwiązaniom, może być instalowane zarówno w skomplikowanych urządzeniach przemysłowych, jak i w mniejszych, które są stosowane powszechnie. Z punktu widzenia tej pracy, najciekawszym produktem jest oczywiście Windows CE .NET, gdyż jest on jedynym systemem czasu rzeczywistego (rygorystycznym - ang. hard real - time system), jaki stworzył gigant z Redmont. Natomiast Windows XP Embedded, nie jest zasadniczo przeznaczony do przetwarzania w czasie rzeczywistym. Jego budowa jest jednak tak zorganizowana, że łatwo można go wzbogacić o takie możliwości. Służą do tego specjalne komponenty oraz rozwiązania różnych firm.
Pierwsza wersja Windows CE (1.0), była przeznaczona dla prostych notesów elektronicznych (ang. organizers). Dopiero wersja 2.0 była przeznaczona do zastosowań wbudowanych. Posiadała wsparcie dla sieci, obiektów COM oraz zawierał sterowniki dla wyświetlaczy umożliwiających osiągnąć lepszą głębię kolorów niż tylko 2 bity na piksel. Dodatkową atrakcją był pakiet Windows CE Embedded Toolkit (ETK), dzięki któremu można było dostosować system do konkretnych platform sprzętowych. Prace nad kolejnymi wersjami systemu zaowocowały w zmniejszeniu jego ogólnego rozmiaru, dodano wsparcie dla konsoli oraz zaimplementowano podstawowe funkcje związane z bezpieczeństwem. Kolejnym udogodnieniem było wzbogacenie produktu o tzw. Platform Builder - narzędzie, które pozwala na łatwe wybieranie poszczególnych komponentów systemu oraz sterowników.
Wersja 3.0 wzbogaciła się m. in. o wsparcie dla obiektów DCOM. W roku 2001, wraz z wypuszczeniem na światło dzienne wersji 4.0, zmieniono nazwę systemu na Windows CE .NET. Była to zapowiedź nowego środowiska programistycznego oraz nowatorskich rozwiązań. Między innymi zmieniono organizację pamięci wirtualnej (zwiększając jej efektywny rozmiar dla każdej aplikacji dwukrotnie), dodano wsparcie dla technologii Bluetooth i 802.11. Modyfikacjom uległ też model ładowania sterowników. Programistom zaproponowano nowe środowisko o nazwie .NET Compact Framework, które w przyszłości ma zastąpić znane dotychczas, oparte o klasy MFC. Nowy system mógł także korzystać z protokołu IPv6 oraz zmodyfikowano w nim sposób zarządzania energią, co ma szczególne znaczenie przy zastosowaniach np. w telefonach komórkowych 3G.
W 2002 roku, na targach Consumer Electronics Show w Las Vegas, miała miejsce premiera najnowszej wersji systemu: Windows CE .NET 4.2. Przepisano w niej praktycznie od początku shella (dodając wsparcie dla przestrzeni nazw) oraz wzbogacono jądro o możliwość korzystania ze sprzętowego zarządzania stronami pamięci (tylko dla niektórych procesorów). Poza tym, jądro zajmuje jedynie 210 KB (dla porównania w CE 3.0: 400 KB).
Jak zapewnia Microsoft, Windows CE .NET poprawnie współpracuje z urządzeniami wyposażonymi w różne rodzaje procesorów np.: Intel x86, StrongARM, a wkrótce jego ekspansja rozszerzy się także o architekturę XScale. Na chwilę obecną (2004 rok), wykorzystanie nowego systemu zapowiedziały takie firmy, jak np.: Motorola, Hitachi, czy Casio.
Na potrzeby tego typu systemów, Microsoft stworzył specjalny kanał dystrybucyjny. Są w nim dostępne wszystkie systemy operacyjne Microsoftu, poza niektórymi produktami dla serwerów. Cechą charakterystyczną tego kanału, jest długotrwała dostępność oferowanych w nim systemów. Przykładowo, wciąż możliwy jest zakup np.: MS DOS albo Microsoft Windows NT. W innych kanałach dystrybucyjnych, starsze produkty są sukcesywnie wycofywane. Poza tym, licencjonowanie systemów operacyjnych Microsoft Embedded odbywa się wyłącznie dla producentów sprzętu (OEM). Dodatkowo, dzięki kolejnemu programowi o nazwie: Windows CE Shared Source Premium Licensing, firmy zajmujące się rozwiązaniami mobilnymi uzyskały dostęp do wybranych części kodu Windows CE. Zdaniem niektórych specjalistów, umowa taka pozwala jednak firmie Microsoft, na późniejsze, bezpłatne korzystanie ze wszelkich modyfikacji.
Kolejne podrozdziały przybliżą świat systemu Windows CE .NET. Zostanie przedstawiona jego architektura oraz implementowane w nim mechanizmy. Dodatkowo zaprezentowana zostanie także ogólna budowa systemu Windows XP Embedded oraz jego możliwości przetwarzania w czasie rzeczywistym.
Architektura Windows CE .NET
Microsoft Windows CE .NET jest otwartym, w pełni skalowalnym, 32 - bitowym, wielozadaniowym i wielowątkowym rygorystycznym systemem czasu rzeczywistego. Jest w taki sposób zaprojektowany, aby można go było łatwo przenosić pomiędzy różnymi platformami sprzętowymi. Poza tym, może znaleźć zastosowanie zarówno w prostych urządzeniach, wyposażonych w bardzo małe ilości pamięci, jak i w tych bardziej rozbudowanych. Ogólnie wymagane jest, aby platforma docelowa była zaopatrzona przynajmniej w układ pamięci, procesor oraz chip odmierzający czas. Wszelkie dodatkowe urządzenia, mogą być dodawane w fazie projektowania lub „w locie”. Dodatkowo, w system wbudowano wyrafinowane mechanizmy zarządzające zużyciem energii, co w znaczny sposób przedłuża żywotność baterii. Producenci, przy tworzeniu własnej, przystosowanej do odpowiednich warunków wersji systemu, mogą wybierać spośród szeregu dostępnych modułów. Niektóre z nich są podzielone na komponenty, co daje jeszcze większą kontrolę nad dostosowywaniem produktu docelowego do konkretnych wymagań. Przykładowo, bibliotekę obsługi klawiatury można zubożyć o obsługę niektórych klawiszy (np.: komputery kieszonkowe nie potrzebują klawiatury numerycznej), dzięki czemu, możliwe jest maksymalne zmniejszenie objętości całego kodu. Do "składania" systemu operacyjnego, Microsoft zaprojektował specjalną aplikację o nazwie Platform Builder. Pozwala ona na bardzo łatwy wybór odpowiednich komponentów i sterowników oraz stworzyć obraz systemu, który może być umieszczony np.: w pamięci Flash. Dodatkowo, Platform Builder proponuje kilka predefiniowanych konfiguracji, jak np.:
Tiny Kernel - przeznaczona dla najprostszych urządzeń, które nie mają możliwości wyświetlania czegokolwiek.
Cell Phone / Smart Phone - zawiera wsparcie dla przetwarzania danych zawierających głos, wysyłanie poczty elektronicznej, przeglądanie stron WWW. Poza tym, może korzystać z wyświetlacza o rozdzielczości 160 x 220, zawiera prostego shella oraz może zawierać pewne dodatkowe aplikacje jak np.: „małego” Internet Explorera.
Media Appliance - pozwala na stworzenie w pełni multimedialnej platformy, która poza aplikacjami służącymi do odtwarzania muzyki oraz filmów, może również służyć jako konsola do gier korzystająca z biblioteki DirectX.
Residential Gateway - predefiniowana konfiguracja dla wszelkich urządzeń sieciowych. Pozwala korzystać z połączeń telefonicznych, LAN/WLAN, zawiera Firewall oraz wspiera różne protokoły i technologie.
Industrial Automation Device - pozwala na stworzenie maszyny, która może sterować zarówno urządzeniami wyposażonymi w wyświetlacze dotykowe, jak i wszelkiego rodzaju kontrolerami np.: PLC (ang. Programmable Logic Controller).
To są oczywiście tylko niektóre proponowane konfiguracje. Po ich różnorodności widać od razu, jak bardzo głęboko system jest modularny oraz jak szerokie pole ogarnia swoim zasięgiem. Oczywiście, użytkownik wcale nie musi korzystać z gotowych konfiguracji. Może także zbudować własną, całkowicie od podstaw.
Wartym uwagi jest fakt, że Windows CE obsługuje kilkadziesiąt procesorów. Zaliczyć do nich można np.: Hitachi SH4, NEC VR4102, architektury ARM i MIPS oraz Intel 486 i Pentium. Tak ogromną przenośność osiągnięto dzięki zaimplementowaniu predefiniowanych pakietów o ogólnej nazwie BSP (ang. Board Support Package). Wspierane procesory są 32 - bitowe, zawierają jednostkę zarządzającą pamięcią MMU (ang. Memory Management Unit - mapuje adresy pamięci wirtualnej na adresy fizyczne) oraz jednostkę przechowującą ostatnio użyte adresy: TLB (ang. Translation Look - aside Buffer), co znacznie wspomaga działanie MMU. Pakiety BSP, których wybór jest dokonywany z poziomu Platform Builder - a, istnieją w postaci sterowników. W systemie Windows CE .NET implementują one specjalną warstwę o nazwie: OEM Adaptation Layer (OAL). Jej głównymi zadaniami, są inicjalizacja specyficznego dla danego urządzenia sprzętu, sterowników oraz zaprezentowaniu ich w odpowiedni sposób przed jądrem systemu. OAL zarządza wszelkimi układami czasowymi, przerwaniami sprzętowymi oraz implementuje mechanizmy oszczędzania energii dla urządzeń peryferyjnych. Ogólnie architektura całego systemu czasu rzeczywistego korzystającego z Windows CE .NET może być logicznie podzielona na 4 warstwy:
warstwa sprzętowa (ang. hardware layer) - ogólny zbiór urządzeń, z jakich składa się dany system;
warstwa OEM (ang. OEM layer) - zawiera wspomniany już pakiet BSP oraz jest charakterystyczna dla konkretnych rozwiązań sprzętowych. Do niej, zalicza się także jądro systemu, które korzysta ze specyficznych dla danego procesora funkcji. Oczywiście przenoszenie jądra na różne platformy nie dokonują już dostawcy OEM, tylko Microsoft lub specjalna grupa jego partnerów (ang. Porting Partners).
warstwa systemu operacyjnego (ang. operating system layer) - zawiera wszelkie komponenty, z jakich składa się system Windows CE .NET.
warstwa aplikacji (ang. application layer) - to warstwa rozszerzająca podstawowe możliwości systemu operacyjnego, np: o nowy, zlokalizowany interfejs użytkownika.
Serce systemu operacyjnego Windows CE .NET stanowi kilka kluczowych komponentów (rys. 43). Są nimi:
zarządca urządzeń (ang. device manager) - do obsługi sterowników urządzeń peryferyjnych, np. kart PCMCIA;
sterowniki urządzeń (ang. Device Drivers) i warstwa OAL - implementowane są przez producentów konkretnych urządzeń;
moduł jądra (NK.lib - ang. New Kernel) - bezwzględne minimum, które wraz z warstwą OAL, może być instalowane na urządzeniach;
GWES (ang. Graphics, Windowing, and Events Subystem) - interfejs użytkownika oraz interfejs graficzny. Zaliczają się do niego m. in. okna, menu, dialogi, czcionki, 32-bitowa paleta kolorów, drukowanie, schowek;
Filesys - tworzy system plików, który zarządza tabelą alokacji plików, rejestrem systemowym oraz bazą danych;
Poza tym, mogą istnieć moduły zapewniające podstawową komunikację w sieci oraz udostępniające interfejsy programistyczne.
Rysunek 43. Architektura systemu operacyjnego Windows CE .NET.
Moduł jądra (ang. kernel module)
Moduł ten, stanowi najbardziej krytyczną część systemu operacyjnego. Jądro, znajduje się w pliku: nk.exe (nk = new kernel) i nie ma nic wspólnego z tym, znanym z „większych” wersji systemów Windows. Praktycznie zostało napisane całkowicie od nowa. Łatwo jest przenieść cały system na różne platformy sprzętowe, gdyż jego większość została napisana w języku C. Z tego też względu, Microsoft udostępnia kilka wersji jądra - każda dedykowana konkretnej rodzinie procesorów (dla porównania Windows XP Embedded istnieje tylko dla architektury zgodnej z x86). Jądro pełni funkcję łącznika pomiędzy kluczowymi komponentami całego systemu (rys. 44).
Rysunek 44. Funkcje jądra w Windows CE .NET.
Jak widać na powyższym rysunku, specjalna biblioteka o nazwie: Coredll.dll składa się z wielu komponentów, których wybór może być dokonany na etapie tworzenia obrazu systemu. Udostępnia ona większość funkcji zgodnych z Win32 API. Do głównych zadań jądra, można zaliczyć m. in. obsługę przerwań i zdarzeń, przekazywanie komunikatów oraz zarządzanie pamięcią. Z punktu widzenia czysto technicznego, jądro umożliwia uruchomienie maksymalnie 32 procesów (każdy reprezentuje instancję uruchomionej aplikacji w systemie). Każdy może korzystać ze swojej prywatnej przestrzenia adresowej o rozmiarze 32 MB (dla porównania, w Windows 2000, rozmiar ten wynosi 2 GB). Jednostką podlegającą szeregowaniu, są wątki (każdy reprezentuje odrębne zadanie danej aplikacji). Każdy ma przypisany swój priorytet (Windows CE .NET ma 256 priorytetów, 255 to najniższy), a ich ilość jest ograniczona dostępną pamięcią w systemie. Jądro potrafi zaadresować maksymalnie 512 MB pamięci RAM. Poza tym, możliwe jest tworzenie jednostek wykonawczych o nazwie angielskiej: fiber. Muszą one być ręcznie szeregowane, przez aplikacje.
Jądro dostarcza także mechanizmów do zarządzania pamięcią. Przy czym, wykorzystywane są dwa podstawowe mechanizmy jej oszczędzania. Po pierwsze, przy uruchamianiu aplikacji, nie jest ona cała ładowana do pamięci. Konkretne strony pamięci wirtualnej są ładowane tylko wtedy, kiedy są potrzebne (rozmiar strony pamięci jest zależny od procesora i wynosi od 1KB do 4KB). Po drugie, w systemie zawsze istnieje tylko pojedyncza kopia biblioteki DLL. Każdy proces otrzymuje jedynie kopię własnych danych prywatnych.
Dodatkowo, Windows CE .NET, pozwala na stworzenie specjalnych obszarów pamięci, o nazwie angielskiej: Execute in Place regions (XIP). Są one tworzone w pamięci tylko do odczytu (ROM) i umożliwiają aplikacji na bezpośrednie wykonanie kodu w nich zawartego, zamiast ładowania go z pamięci RAM.
Jednostki wykonawcze w Windows CE .NET oraz ich szeregowanie
Jak już wspomniano wcześniej, w systemie Windows CE .NET istnieją trzy rodzaje jednostek wykonawczych. Są nimi: procesy, wątki oraz tzw. fibers.
Procesów w systemie może być maksymalnie 32. Jednakże w chwili startu „okienek”, jest już kilka aktywnych, jak np.: nk.exe, gwes.exe, filesys.exe, shell.exe. Tak więc, do dyspozycji użytkownika może pozostać np.: 28 możliwych procesów. Po uruchomieniu procesu, kiedy program ładujący trafi na funkcję WinMain(), tworzony jest jego wątek główny. Aby uruchomić proces z poziomu innego, używana jest funkcja: CreateProcess(). Koniec procesu zazwyczaj wiąże się z powrotem z funkcji WinMain() lub wywołaniem funkcji ExitThread() przez jego wątek główny.
Każdy wątek ma dostęp do wszelkich zasobów należących do procesu, który go utworzył przy pomocy funkcji CreateThread(). Wykonują się niezależnie od siebie oraz mogą się znajdować w jednym z poniższych stanów:
wykonywany (ang. running) - aktualnie „okupuje” procesor;
zawieszony (ang. suspended) - wywołał funkcję SuspendThread();
uśpiony (ang. sleeping) - został zawieszony na określony czas poprzez wywołanie funkcji Sleep();
zablokowany (ang. blocked) - oczekuje na dostęp do zasobu;
przerwany (ang. terminated) - zakończył swoje działanie;
W systemie Windows CE .NET, do szeregowania zadań wykorzystywany jest wywłaszczający algorytm opierający swoje działanie o priorytety i podział czasu (szczegółowo omówiony w rozdziale 4.2). Każdy wątek wykonuje się przez pewien kwant czasu (ang. quantum), który tutaj domyślnie wynosi 100ms. Producenci OEM mogą jednak tę wartość dowolnie zmieniać. W tym celu, wystarczy z poziomu funkcji OemInit() (OAL) nadpisać wartość globalnej zmiennej o nazwie: dwDefaultThreadQuantum. Z poziomu wątku, używa się do tego celu funkcji: CeSetThreadQuantum(). Jest ona bardzo przydatna w sytuacjach, kiedy trzeba zapewnić, aby dany wątek nigdy nie został wywłaszczony przez inne, mające ten sam priorytet. Wtedy trzeba ustawić jego kwant czasu na 0 (zero).
Priorytet przydziela się do wątku przy pomocy funkcji: CsSetThreadPriority(). Jego domyślną wartością jest 251. Najważniejsze zadanie (ang. real - time) ma priorytet 0 (zero).
Trzecią formą jednostek wykonawczych w Windows CE .NET, są tzw. fibers. Działają one w kontekście wątków, które nimi zarządzają. Przez system są widziane tak samo jak wątek, który je utworzył. Przykładowo, jeśli dany fiber wywoła funkcję ExitThread(), to zakończony zostanie jego wątek - właściciel. Ważną cechą tego typu jednostek wykonawczych jest jednak to, że nie podlegają one tym samym regułom szeregowania co wątki. Tutaj, ewentualnego przełączania kontekstu trzeba dokonywać samemu np.: przy pomocy funkcji: SwitchToFiber().
Wszędzie tam, gdzie mowa o wielozadaniowości, pojawia się problem współdzielenia zasobów. Oczywiście Windows CE .NET nie pozostaje na tym polu w tyle i udostępnia programistom cały zbiór przeznaczonych do tego celu obiektów, jak np.: obiekty sekcji krytycznej (ang. critical section objects), muteksy (patrz rozdz. 5.4.3), semafory (patrz rozdz. 5.4). Pierwszy wspomniany obiekt, tzw. sekcja krytyczna, może służyć do synchronizacji wątków w obrębie jednego procesu. Przykład jej użycia przedstawia listing 10.
Listing 10. Sekcja krytyczna w Windows CE .NET
int ZasobDzielony;
CRITICAL_SECTION cs;
WinMain()
{
InitializeCriticalSection(&cs);
CreateThread(…, Run1, …);
CreateThread(…, Run2, …);
}
DWORD WINAPI Run1()
{
EnterCriticalSection(&cs);
//rób coś ze zmienną: ZasobDzielony
LeaveCriticalSection(&cs);
}
DWORD WINAPI Run2()
{
EnterCriticalSection(&cs);
//rób coś ze zmienną: ZasobDzielony
LeaveCriticalSection(&cs);
}
Jak widać na powyższym przykładzie, obiekt sekcji krytycznej musi zostać najpierw zainicjalizowany przy pomocy funkcji: InitializeCriticalSection(). Później może go przejąć tylko jeden wątek. Dokonuje tego przy pomocy funkcji: EnterCriticalSection(). Dopóki go nie zwolni, przy pomocy wywołania: LeaveCriticalSection(), wszystkie inne wątki próbujące go przejąć, zostaną zablokowane. Oczywiście dany wątek przed próbą przejęcia danej sekcji krytycznej, może przy pomocy funkcji: TryEnterCriticalSection() sprawdzić jej ewentualną dostępność. To pozwala uniknąć stanu blokowania i być dalej dostępnym dla innych zadań.
Aby móc dokonać synchronizacji na skalę wszystkich wątków, a nie tylko tych znajdujących się w obrębie danego procesu, należy użyć muteksów (ang. mutex). Specjalnie dla nich przeznaczone są takie funkcje jak: CreateMutex(), WaitForSingleObject(), WaitForMultipleObjects() oraz ReleaseMutex(). Ważną ich cechą jest to, że tylko jeden wątek może posiadać dany mutex w określonym punkcie czasowym. Jeśli istnieje potrzeba określenia pewnej liczby zadań, które mogą uzyskać jednoczesny dostęp do zasobu współdzielonego, należy skorzystać z tzw. semaforów (ang. semaphores).
Niestety, ze współdzieleniem zasobów, wiąże się ryzyko powstania zjawiska, określanego mianem inwersji priorytetów (patrz rozdz. 8.2). Aby zminimalizować jego skutki, system Windows CE .NET korzysta z algorytmu opartego o dziedziczenie priorytetów (patrz rozdz. 9.2). Nie jest to rozwiązanie doskonałe, dlatego Microsoft zaleca, aby wszystkie wątki oczekujące na tym samym muteksie, miały albo ten sam priorytet, albo mogły na niego oczekiwać w nieskończoność. Poza tym, aby uniknąć zbędnych opóźnień, aplikacje powinny być tak projektowane, aby inwersja priorytetów w ogóle nie miała miejsca.
Wymiana danych pomiędzy zadaniami, oprócz zmiennych globalnych, może być także realizowana np.: w oparciu o kolejki. W Windows CE .NET wspierają one tylko i wyłącznie połączenia typu punkt - do - punktu. Jeśli wiele zadań chce z niej czytać, to każde może odebrać tylko aktualną wiadomość znajdującą się na początku kolejki.
Model pamięci w Windows CE .NET
System Windows CE .NET potrafi korzystać zarówno z pamięci ROM, jak i RAM. W pierwszej, przechowywany jest obraz systemu. W czasie jego tworzenia przez aplikację Platform Builder, cała zawartość pamięci ROM lub Flash jest zawarta w pliku: nk.bin. Pamięć RAM natomiast, jest podzielona na dwie części: magazyn obiektów (ang. object store) oraz pamięć programu (ang. program memory). Ich rozmiary mogą być modyfikowane.
Magazyn obiektów, ze względu na swoją funkcjonalność, przypomina wirtualny RAM - dysk. Emuluje prawdziwą pamięć zewnętrzną, której większość urządzeń jest pozbawiona. Dane w nim zgromadzone są podtrzymywane nawet w przypadku wystąpienia braku zasilania. Ta część pamięci, dzieli się dalej na rejestr, system plików oraz bazę danych. Owa baza danych nie jest ani relacyjna, ani SQL. Występuje w postaci prostej kartoteki i powinna wystarczyć do większości zadań. Z pewnością też przyspieszy tworzenie nowych aplikacji. Maksymalny rozmiar systemu plików, to 256 MB, przy czym pojedynczy plik, może mieć maksymalny rozmiar 32 MB. Plik bazy danych nie może przekroczyć 16 MB. Ogólna liczba obiektów przechowywanych w magazynie, może osiągnąć maksymalną wartość 4 milionów. W przeciwieństwie do programów systemowych, które zostały umieszczone w pamięci ROM, programy z magazynu obiektów są przed uruchomieniem kopiowane do obszaru pamięci programów.
Pamięć programu funkcjonuje w ten sam sposób, co pamięć RAM w zwykłych komputerach klasy PC. Są w niej przechowywane stosy, sterty itp.
Wszystkie aplikacje uruchomione w systemie, korzystają z jednej przestrzeni adresów wirtualnych, której rozmiar wynosi 4 GB. Wewnętrznie, dzieli się ona na dwie równe części. Górna jest zarezerwowana dla systemu operacyjnego, natomiast z dolnej, korzystają aplikacje. Strukturę taką przedstawia rys. 45.
Rysunek 45. Alokacja pamięci w przestrzeni adresowej dla Windows CE .NET.
Jak widać na powyższym rysunku, na samym początku przestrzeni adresowej, umiejscowione są 33 sloty pamięci, każdy po 32 MB. Slot o numerze 0 (zero), reprezentuje pamięć aktualnie wykonywanego procesu. Pozostałe, są przydzielane innym aplikacjom. Wewnętrznie, sloty te składają się ze stron pamięci, których rozmiar jest zależny od procesora i wynosi od 1KB do 4KB. Strony te są bezpośrednio mapowane na pamięć fizyczną. Dodatkowo, strony są organizowane w bloki, o rozmiarze 64KB.
Widoczna na rysunku pamięć współdzielona, jest wykorzystywana głównie w sytuacjach, kiedy zapotrzebowanie na pamięć przerasta ograniczenie 32MB. Może być użyta np.: do komunikacji międzyprocesowej.
Zaletą pamięci wirtualnej jest to, że nie jest ona nigdy pofragmentowana. Przy korzystaniu z niej, należy jednak pamiętać, że system zarządza nią w oparciu o bloki 64KB.
Dodatkowo, dla każdego procesu, tworzona jest sterta (ang. heap), a dla każdego wątku stos (ang. stack). Sterta nie podlega jakimś specjalnym ograniczeniom oraz pozwala na rezerwowanie mniejszych bloków pamięci np.: 4KB. Wszelkie operacje rezerwacji i zwalniania obszarów pamięci, należy dokonać samemu. Jeśli chodzi natomiast o stos, to są one wykonywane automatycznie. Domyślnie, dla każdego wątku, na potrzeby stosu alokowana jest jedna strona pamięci (np.: 4KB). Oczywiście rozmiar ten można modyfikować. Służy do tego przełącznik linkera o nazwie: /STACKSIZE.
Przerwania
Kiedy zostaje zgłoszone przerwanie sprzętowe, wówczas generowany jest wyjątek. W efekcie, jądro ładuje do pamięci swoją procedurę jego obsługi. Jedynym jej zadaniem, jest zablokowanie obsługi wszystkich przerwań o priorytetach niższych i równym w stosunku do aktualnego (działanie takie nie ma miejsca w architekturach ARM i Strong ARM). Następnie, wywoływana jest procedura jego obsługi, zawarta w warstwie OAL, która to z kolei wywołać kolejne, zarejestrowane procedury.
Procedura z poziomu OAL, zwraca w efekcie identyfikator danego przerwania. Jeśli zwrócona wartość to: SYSINTR_NOP, to znaczy, że z danym przerwaniem nie ma powiązanej żadnej procedury obsługi. Wówczas jądro włącza ponownie obsługę wszystkich przerwań. Jeśli natomiast wartość zwrócona to: SYSINTR, to oznacza, że taka procedura w systemie istnieje. Włączana jest obsługa wszystkich przerwań poza tym, które jest właśnie przetwarzane. Zgłaszane jest odpowiednie zdarzenie a dalsza obsługa przerwania, odbywa się z poziomu odpowiedniego wątku (ang. Interrupt Service Thread - IST - znajduje się w sterowniku danego urządzenia). Wątek ten, komunikuje się z odpowiednim urządzeniem, dokonuje odpowiednich operacji odczytu i zapisu danych, a na koniec, wywołuje funkcję: InterruptDone( ). Informuje w ten sposób o zakończeniu swojej pracy. W kolejnym kroku, jądro włącza obsługę tego przerwania i od tej chwili związane z nim urządzenie może ponownie zażądać obsługi.
System obsługuje tzw. przerwania zagnieżdżone. Polega to na tym, że tylko przerwania o wyższych priorytetach nigdy nie są blokowane przez te, o niższych. Jądro wspiera taki poziom zagnieżdżenia, jaki jest możliwy dla danej architektury. Dodatkowo czas, jaki upływa od chwili zgłoszenia przerwania, do wywołania procedury jego obsługi, jest z góry ograniczony. Jak podał Microsoft, na komputerze z procesorem Hitachi SH-3, taktowanym zegarem o częstotliwości 60 MHz wyniósł on 7,5 mikrosekundy. Był to oczywiście czas reakcji samego jądra systemu. Całkowity czas reakcji, jest powiększony o czas zużyty na obsługę realizowaną przez program użytkownika. Do mierzenia opóźnień związanych z przerwaniami, służy specjalne narzędzie o nazwie: ILTIMING. Jego wyniki mogą być oglądane przy pomocy debuggera.
Sterowniki urządzeń
Windows CE nie narzuca żadnych ograniczeń, co do rodzaju urządzeń wejścia-wyjścia, z jakimi może współpracować. Standardowo, obsługuje port szeregowy, łącze na podczerwień (IrDA), interfejs PCMCIA oraz interfejs USB. Nie jest to jednak zamknięta lista. Aby móc dodać nowe urządzenie, musi istnieć dla niego odpowiedni sterownik oraz konfiguracja sprzętowa musi umożliwiać jego podłączenie. Sterowniki stanowią warstwę pośrednią, pomiędzy urządzeniami a systemem operacyjnym. Dzięki nim, aplikacje mogą korzystać z realizowanych sprzętowo usług.
Platforma zbudowana w oparciu o system Windows CE .NET, może korzystać z dwóch rodzajów urządzeń. Pierwsze, to tzw. urządzenia wbudowane (ang. built - in devices). Są one zintegrowane z platformą. Zaliczają się do nich wszelkiego rodzaju wyświetlacze, porty szeregowe, klawiatura itp. Sterowniki je obsługujące są wbudowane w obraz systemu operacyjnego i znajdują się w pamięci ROM lub Flash. Nazywa się je lokalnymi (ang. native drivers). Ich obsługą zajmuje się specjalny moduł systemu, o nazwie: GWES - Graphics, Windowing and Event Subsystem (opisany dalej w pracy). Moduł ten, ładuje wszelkie swoje sterowniki w czasie uruchamiania systemu. Ich architektura może być monolityczna lub warstwowa. W pierwszym przypadku, implementują one swój interfejs w sposób bezpośredni. Każda funkcja wykonuje bezpośrednie operacje na sprzęcie. Są one bardzo wydajne, jednakże wymagają większych nakładów pracy np.: przy przenoszeniu ich na nowszy sprzęt. Architektura warstwowa, opiera się o podział obowiązków. Najniższa warstwa, reprezentuje kod charakterystyczny dla konkretnego sprzętu. Określa się ją mianem PDD (ang. Platform - Dependent Driver). Warstwa wyższa natomiast, jest niezależna od platformy i nie korzysta ze sprzętu bezpośrednio. Nazywa się MDD (ang. Module Device Driver). Przy przenoszeniu takiego sterownika z jednej platformy na drugą, wystarczy tylko zmienić jego dolną warstwę (PDD).
Drugą grupę urządzeń, stanowią tzw. urządzenia dołączane „w locie” (ang. installable devices). Ich sterowniki mają architekturę warstwową. Po instalacji, są umieszczane w magazynie obiektów (ang. object store). Z poziomu systemu operacyjnego, kieruje nimi zarządca urządzeń (ang. device manager). Te sterowniki, które są wyszczególnione w odpowiednim kluczu rejestru, są ładowane do pamięci przy starcie systemu. Reszta, może być uruchamiana na żądanie. Ich ogólna zasada, działania opiera się o strumienie (ang. stream interface drivers). Przedstawiają one urządzenia, jako specjalne pliki, na których można dokonywać standardowych operacji czytania, zapisu, otwierania, czy zamykania. Z tego też względu, muszą one takie funkcje implementować. Oczywiście z tego typu sterowników, korzystają również niektóre urządzenia wbudowane w platformę. Czasem strumieniowy przesył danych jest dla nich lepszy.
Sterowniki dla systemu Windows CE .NET występują w postaci bibliotek łączonych dynamicznie (ang. dynamic-link library - DLL) i są całkowicie niekompatybilne z tymi, znanymi z innych wersji „okienek”. Tutaj, mogą one np.: korzystać z API systemu, a ich architektura została znacznie uproszczona.
Moduł GWES (ang. Graphics, Windowing and Event Subsystem)
Moduł ten stanowi interfejs łączący użytkownika, system operacyjny oraz uruchomioną w nim aplikację w jedną całość. Ze wszystkich modułów Windows CE .NET, właśnie ten zawiera w sobie największą liczbę komponentów (ok. 90). Dzielą się one na dwie grupy. Pierwsza, jest odpowiedzialna za pobieranie danych z zewnątrz np.: poprzez rejestrowanie ruchu myszką. Dodatkowo, zajmuje się obsługą komunikatów oraz zdarzeń. Druga grupa natomiast, jest odpowiedzialna za graficzną interpretację zdobytych przez pierwszą informacji. Nazywa się interfejsem graficznym (ang. Graphics Device Interface - GDI) i korzysta z tzw. kontekstu urządzenia (ang. Device Context - DC). Określa on nie tylko takie parametry graficzne jak kolor rysowania i rodzaj wypełnienia, ale także może określać urządzenie docelowe jak np.: ekran, drukarka czy nawet pamięć.
Ważną sprawą jest fakt, że twórcy systemu Windows CE .NET nie nałożyli żadnych ograniczeń na jego sposoby komunikowania się ze światem zewnętrznym. Mogą to być zarówno metody znane powszechnie, jak te opierające się o klawiaturę, myszkę i monitor, jak i również korzystające z bardziej nowatorskich rozwiązań, jakimi są ekrany dotykowe, układy rozpoznawania mowy, syntezatory mowy czy układy rozpoznawania pisma. Wszystko to jest zależne od rodzaju urządzenia oraz jego przeznaczenia.
Moduł Filesys
Moduł ten, jest odpowiedzialny za poprawne funkcjonowanie systemu plików. W systemie Windows CE .NET zaproponowano całkowicie nowe rozwiązania przechowywania danych. Różnią się one znacząco od tych, znanych z większych wersji systemu, jak np.: Windows XP. Tutaj, główny sposób składowania informacji, opiera się o działanie wspomnianego już wcześniej magazynu obiektów (ang. object store). Magazyn taki, praktycznie symuluje działanie dysku twardego. Fizycznie dane mogą się znajdować zarówno w pamięci RAM, jak i ROM. Dla użytkownika nie ma to jednak znaczenia. W jednym katalogu mogą się znajdować pliki, które fizycznie umieszczone są w pamięci ROM (nie można ich modyfikować) oraz te, które zaalokowane zostały w pamięci RAM (zazwyczaj są to te, generowane przez użytkownika). Dzięki takiej strukturze, możliwe stało się zaimplementowanie mechanizmu dublowania danych (ang. file shadowing). Polega on na tym, że dany plik, występuje w dwóch, identycznych kopiach. Każda ma tę samą nazwę. Jedyną różnicą jest ich fizyczne umiejscowienie. Jedna znajduje się w obszarze pamięci ROM, natomiast druga, w RAM. Ta wersja z RAM przesłania drugą i kiedy występuje odwołanie do pliku, to udostępniana jest właśnie ona. Jeśli natomiast plik z RAM zostanie usunięty, to dane mogą być dalej dostępne. Wtedy ponownie widoczna staje się wersja pliku przechowywana w ROM. Mechanizm ten, poza wprowadzeniem pewnego poziomu bezpieczeństwa, daje również możliwość łatwego aktualizowania tych plików, które zostały dostarczone wraz z urządzeniem i umiejscowione w jego pamięci stałej.
Oczywiście użytkownik nie jest ograniczony tylko do takiej formy przechowywania danych. Istnieje możliwość doinstalowania dodatkowych systemów plików. Maksymalnie można podłączyć 256 urządzeń służących jako magazyny lub utworzyć w ich ramach 256 partycji. Mogą to być np.: systemy plików umożliwiające korzystanie z dysków twardych ATA, stacji dyskietek lub innych kart rozszerzających.
Bardzo charakterystyczną cechą systemu plików w Windows CE .NET jest to, że nie używa on kolejnych liter aby określić konkretne napędy, jak np.: „C:”, „D:” itd. W rezultacie, każde urządzenie jest odwzorowywane na jeden podkatalog, katalogu głównego. Standardowo przyjmuje się, że nazwa każdego takiego podkatalogu zawiera w swoim członie nazwę: „Storage Card”, a następnie numer danego urządzenia. Oczywiście konwencja ta nie jest sztywna i może być dowolnie zmieniana przez odpowiedni sterownik. Tak więc, przykładowa ścieżka dostępu do katalogu Windows, która w większych wersjach okienek może wyglądać następująco: „C:\Windows”, tutaj jest skrócona do formy: „\Windows”. Z kolei, po dołączeniu do urządzenia jakiejś karty pamięci, dostęp do niej jest poprzez katalog, który może być następujący: „\Storage Card”.
Nazwy plików i ich kompletne ścieżki dostępu nie mogą przekroczyć długości wskazywanej przez zmienną środowiskową: MAX_PATH. Obecnie, wynosi ona 260 bajtów. Same nazwy plików mają format: name.ext. Rozszerzenie (.ext) określa typ pliku i może mieć maksymalnie długość trzech znaków. Jest ono wykorzystywane przez shella, który swoją wiedzę o rodzaju pliku opiera właśnie o te trzy znaki. Zbiór dozwolonych znaków jest ten sam co w innych wersjach „Windowsów”, czyli należy unikać następujących: „. " / \ ? : ; | = , " < > * ”. Poza tym, do każdego pliku może zostać przypisanych kilka atrybutów jak np.: tylko do odczytu, plik systemowy, plik ukryty, skompresowany oraz informacja o tym, gdzie jest umiejscowiony: w pamięci ROM czy RAM.
Dla programisty nie ma większego znaczenia, czy dany plik znajduje się fizycznie w pamięci ROM czy RAM. Wstępnego rozpoznania może dokonać w oparciu o atrybuty. Należy jednak mieć świadomość, że pliki z pamięci ROM nie mogą być otwierane przy pomocy funkcji np.: CreateFile().
System plików oparty o magazyn obiektów cechuje się kilkoma ograniczeniami. Po pierwsze, rozmiar całego magazynu w RAM nie może przekroczyć 256MB. Biorąc pod uwagę domyślną kompresję, praktyczny maksymalny rozmiar danych wynosi ok. 512MB. Poszczególne pliki nie mogą mieć większych rozmiarów niż 32MB. Na szczęście ograniczenia te nie dotyczą innych systemów plików, jakie mogą być dołączane do systemu. Przykładowo dla systemu plików FAT32, rozmiar maksymalny wynosi 4GB.
Aby moduł Filesys był jak najmniejszy, zrezygnowano w nim z asynchronicznej obsługi dostępu do plików. Ani pliki, ani urządzenia, nie mogą być otwierane z wykorzystaniem flagi: FILE_FLAG_OVERLAPPED. Dostęp do nich jest możliwy tylko w sposób synchroniczny. Poza tym, operacje na plikach są dokonywane standardowo. Do tego celu udostępniona została większość funkcji Win32 API, jak np.: CreateFile(), ReadFile(), WriteFile(), czy CloseFile().
Ostatnią ciekawostką dotyczącą magazynu obiektów jest to, że nie zaimplementowano w nim pojęcia tzw. katalogu bieżącego (ang. current directory). Aby uzyskać dostęp do pliku, należy podać pełną jego ścieżkę dostępu. Nie można tutaj korzystać z dobrodziejstwa oferowanego przez ścieżki względne. Jedynie Shell podtrzymuje w swoim zakresie katalog aktualny. Nie ma to jednak nic wspólnego z systemem plików. W funkcjach API, jeśli w ramach katalogu nie jest podane nic, to domyślnie użyty zostaje główny katalog systemu plików.
Poza magazynem obiektów, moduł Filesys potrafi także zarządzać prostą bazą danych (ang. property database) oraz rejestrem (ang. registry). Są one opisane w kolejnych rozdziałach.
Rejestr (ang. registry)
Rejestr, to rodzaj bazy danych, której głównym przeznaczeniem jest przechowywanie informacji konfiguracyjnych systemu oraz poszczególnych aplikacji. Ma budowę hierarchiczną. Składa się z kluczy, podkluczy oraz wartości. Kolejne poziomy kluczy oddziela się przy pomocy znaku „\” (ang. backslash), czyli dokładnie tak samo, jak ma to miejsce w przypadku katalogów i plików. Istnieje pewne ograniczenie przy nadawaniu nazw odpowiednim kluczom i wartościom. Nie mogą one przekroczyć granicy 255 znaków. Poza tym, dane powiązane z wartością mogą mieć maksymalny rozmiar 4KB. Microsoft zaleca również minimalizację liczby kluczy, gdyż zajmują one fizycznie więcej miejsca w pamięci niż wartości. Rejestr fizycznie jest zawsze przechowywany w pamięci RAM i dlatego też może zostać łatwo skasowany. Aby jednak zabezpieczyć się przed taką sytuacją, należy w warstwie OAL zaimplementować dwie funkcje: WriteRegistryToOEM() oraz ReadRegistryFromOEM(). Będą one używane, odpowiednio do zapisu i odczytu zawartości rejestru z określonych miejsc pamięci stałej. Jeśli natomiast okaże się, że pamięć RAM nie dysponuje rejestrem, to może on być pobrany z określonego pliku w pamięci ROM.
Do manipulacji rejestrem, przeznaczone są specjalne funkcje API. Ogólnie, aby zmodyfikować lub odczytać jakąś wartość, należy najpierw otworzyć odpowiedni klucz, a po zakończeniu stosownych operacji, zamknąć go. Dane w rejestrze mają określony swój typ. Windows CE .NET pozwala na zdefiniowanie m. in. danych znakowych (string), numerycznych (np. 32 - bitowe liczby) oraz o nieokreślonej formie - dane binarne.
Organizacja rejestru opiera się o trzy klucze główne. Są nimi: HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER oraz HKEY_CLASSES_ROOT. Pierwszy, przechowuje dane o konfiguracji sprzętu oraz sterowników. Drugi, zawiera dane konkretnego użytkownika systemu, natomiast ostatni, zawiera np.: informacje o typach plików. Jak już wspomniano wcześniej, rejestr jest wykorzystywany przez aplikacje. Zapisują one w nim np.: swój aktualny stan, dzięki czemu możliwe jest jego przywrócenie przy kolejnym uruchomieniu. Standardowo, poszczególne aplikacje korzystają z wartości, wskazanych przez następującą ścieżkę dostępu: {KLUCZ_GŁOWNY}\Software\{Nazwa Firmy}\{Aplikacja}. Jest to zalecana forma. Jako klucz główny, może być podany zarówno HKEY_LOCAL_MACHINE, jak i HKEY_CURRENT_USER. Pierwszy jest dla danych globalnych np.: dodatkowe komponenty aplikacji, natomiast drugi, zawiera np.: opis wyglądu interfejsu graficznego dla konkretnego użytkownika.
Baza danych (ang. property database)
Baza danych implementowana przez system Windows CE .NET, jest bardzo prosta. Opiera się o istnienie tylko jednej tabeli zawierającej rekordy. Każdy rekord może posiadać właściwości, przy czym jego maksymalny rozmiar nie może przekroczyć 128KB. Każda właściwość w ramach rekordu jest określana przez trzy parametry: identyfikator (ID), typ oraz wartość. Właściwość taka, może mieć maksymalny rozmiar 64KB. Wszystkie obecne w systemie bazy danych, są przechowywane w specjalnym pliku (ang. database volume), którego maksymalny rozmiar wynosi 265MB. Nie można niestety definiować różnych zależności pomiędzy wartościami baz danych. Dodatkowo, każda baza może posiadać maksymalnie cztery (4) indeksy.
Jak już wspomniano, każdy rekord zawiera kilka swoich właściwości. Każda ma określony typ, przy czym Windows CE .NET implementuje następujące ich rodzaje:
IVal - 2 - bajtowa liczba całkowita, ze znakiem;
UiVal - 2 - bajtowa liczba całkowita, bez znaku;
Lval - 4 - bajtowa liczba całkowita, ze znakiem;
UlVal - 4 - bajtowa liczba całkowita, bez znaku;
FILETIME - struktura do przechowywania daty i czasu;
LPWSTR - string kodowany w UNICODE, zakończony przez NULL (0);
CEBLOB - kolekcja bajtów;
BOOL - wartość logiczna;
Double - 8 - bajtowa wartość, ze znakiem.
Ważną sprawą jest to, że dany rekord nie może zawierać innych rekordów. Dostęp do bazy danych nie może być blokowany. Istnieją jednak metody, które pozwalają na informowanie określonych wątków o dokonanych w niej zmianach.
W celu dokonywania wszelkich operacji związanych z bazami danych, Windows CE .NET udostępnia dwa mechanizmy. Pierwszy, opiera się o czyste API, specjalnie zaprojektowane na takie okazje. Pozwala ono otwierać i zamykać plik zawierający zbiór baz danych oraz przeglądać i modyfikować ich poszczególne rekordy. Dostępne są także funkcje wyszukujące. Ponieważ większość z nich jest dość złożona i wymagają podawania dużej ilości parametrów, Microsoft zaprojektował nowy model dostępu do danych. Jest to tzw. ActiveX Data Object for Windows CE (ADOCE). Swoje działanie opiera on o szereg obiektów COM. Spośród licznych ulepszeń, najistotniejsze jest z całą pewnością to, że dzięki ADOCE, możliwe jest zadawanie zapytań SQL do bazy danych.
Zarządzanie energią (ang. power management)
Dla wszelkich urządzeń przenośnych, dobre gospodarowanie dostępną energią jest bardzo ważne. Pozwala przedłużyć czas działania oraz dodatkowo ma zysk ekonomiczny. W systemie Windows CE .NET, możliwe stany, w jakich może się znaleźć urządzenie, są określane przez dostawców OEM. Teoretycznie, jest pięć takich stanów ale nie wszystkie muszą być implementowane. Pierwszy, najniższy stan zerowy oznacza, że urządzenie jest włączone i działa z pełną swoją wydajnością oraz konsumuje największą, możliwą porcję energii. Jest to jedyny stan, jaki każde urządzenie musi implementować. Poza tym, urządzenie jest wprowadzane w ten stan, gdy zostaną załadowane jego sterowniki. Kolejne, wyższe stany, charakteryzują się mniejszym zapotrzebowaniem na moc. Urządzenia albo pracują mniej wydajnie, albo pozostają w stanie uśpienia. Po zgłoszeniu odpowiedniego żądania, mogą zostać jednak przebudzone. Wreszcie najwyższy stan, odpowiada sytuacji, w której urządzenie jest całkowicie wyłączone. Jeśli jest on wspierany, to tuż przed usunięciem sterownika danego urządzenia, jest ono właśnie w taki stan wprowadzane.
Ponieważ nie wszystkie możliwe stany muszą być wspierane przez urządzenia, przyjęta została zasada, że jeśli jakiś nie jest implementowany, to po jego zażądaniu, urządzenie przechodzi do możliwie najbliższego, wyższego stanu. Przykładowo jeśli system zażąda uśpienia urządzenia, a ono takiego stanu nie potrafi obsłużyć, to może ono zostać np.: wyłączone.
Dodatkowe możliwości Windows CE .NET
Jak już wspomniano, system Windows CE .NET może być bardzo łatwo dostosowywany do konkretnych urządzeń. Jest nie tylko przenośny poprzez różne platformy, ale także jego funkcjonalność, jest zależna od decyzji producentów. System może oferować szeroki wybór wszelkiego rodzaju usług, służących do komunikacji, implementuje mechanizmy zapewniające bezpieczeństwo transmisji oraz zawiera bardzo duże wsparcie dla multimediów. Kolejne podrozdziały przybliżą te możliwości.
Komunikacja ze światem zewnętrznym
Windows CE .NET zawiera komplet najważniejszych funkcji, dzięki którym możliwa jest praca w sieci. W ich skład wchodzą zarówno te, służące do obsługi prostych połączeń szeregowych, jak i te bardziej wyrafinowane, umożliwiające realizowanie połączeń w oparciu o łącza na podczerwień. Ogólną architekturę podsystemu sieciowego przedstawia rys. 46.
Rysunek 46. Architektura podsystemu sieciowego.
Interfejs szeregowy jest obsługiwany przy pomocy modułu Serial. Jest to jedna z prostszych form komunikacji. Zapewnia przesyłanie danych pomiędzy dwoma urządzeniami. Sam interfejs szeregowy może obsługiwać połączenia bezprzewodowe i przewodowe. Bardzo często jest wykorzystywany przez modemy. Dlatego też w Windows CE .NET nie zabrakło standardowego ich sterownika o nazwie: Unimodem. Dla połączeń telefonicznych, zaimplementowano także moduł o nazwie: TAPI (ang. Microsoft Telephony API), który znacznie ułatwia ich obsługę. Nie jest to jego pełna wersja. Jego funkcjonalność została zmniejszona m. in. o obsługę połączeń przychodzących oraz znaczną część innych zaawansowanych funkcji.
Komunikacja z sieciami LAN i Internetem, jest realizowana dzięki obecności szeregu innych modułów oraz protokołów. Moduł WinInet, który ułatwia pisanie wszelkich aplikacji korzystających z protokołów HTTP i FTP. WNet (ang. Windows CE Networking API), umożliwia zrealizowanie takich zadań jak np.: zlokalizowanie drukarek sieciowych czy uzyskanie dostępu do innych zasobów udostępnionych. SNMP (ang. Simple Network Management Protocol), służy np.: do monitorowania przepływu danych w sieci. COM (ang. Common Object Model) umożliwia tworzenie niezależne od platformy, obiektowo - zorientowane komponenty. Umożliwiają one np.: realizację komunikacji pomiędzy procesami umieszczonymi na dwóch różnych maszynach. MSMQ (ang. Message Queuing) to usługa, która kolejkuje otrzymywane komunikaty od innych maszyn w sieci. W systemie Windows CE .NET została ona poddana dodatkowym działaniom optymalizacyjnym, mającym na celu zmniejszenie zapotrzebowania na pamięć. Web Server pozwala na bardzo wygodne przetwarzanie plików oraz dodatkowo umożliwia użycie skryptów. Winsock (ang. Windows Sockets) dostarcza interfejs, dzięki któremu możliwe jest niezależne od protokołu tworzenie połączeń sieciowych. RAS (ang. Remote Access Functions) - programowy router. Dzięki niemu, możliwe jest np.: po nawiązaniu połączenia ze stacją, która ma dostęp do różnych rodzajów sieci, rozpoczęcie korzystania z nich. IP Helper udostępnia interfejs, który znacznie upraszcza zarządzanie siecią na lokalnej maszynie. Pozwala np.: modyfikować ustawienia sieciowe. Windows CE .NET implementuje również takie protokoły jak: PPP (ang. Point to Point Protocol), SLIP (ang. Serial Line Interface Protocol - połączenie do sieci poprzez łącze szeregowe), TCP/IP (definiuje m. in. sposoby pakietowania danych oraz metody ich wysyłania) oraz DHCP (dynamiczne nadawanie adresów IP).
Poza tymi mechanizmami, system Windows CE .NET może być także korzystać z komunikacji w czasie rzeczywistym. W tym celu wspierane są takie technologie, jak VoIP (ang. Voice over IP), zapewniające dwukierunkową transmisję danych głosowych poprzez sieć IP.
Bezpieczeństwo przesyłania danych
Zawsze, kiedy dany system potrafi korzystać z jakiejkolwiek sieci, powinny być podjęte wszelkie próby, aby taka komunikacja była jak najbardziej bezpieczna. Bezpieczeństwo w systemie Windows CE .NET jest implementowane już na poziomie samych modułów. Dostawcy OEM mogą np.: odpytać aplikacje o wymagane certyfikaty oraz kazać im potwierdzić swoją autentyczność. Dodatkowo, możliwe jest ograniczenie możliwości wszelkich, nie podpisanych modułów. Przykładowo mogą one być pozbawione prawa modyfikowania rejestru. Do zapewnienia bezpiecznej komunikacji sieciowej jest używany protokół SSL (ang. Secure Socket Layer).
Multimedia w systemie Windows CE .NET
Większość wsparcia dla multimediów, w tym systemie jest realizowana poprzez kontrolkę Windows Media Player. Dostarcza ona interfejs programistyczny, dzięki któremu możliwe jest np.: przetwarzanie danych strumieniowych zawierających obraz. Została ona zbudowana w oparciu o technologię Direct Show i wykorzystuje różne filtry, które mogą zostać wykorzystane do przetwarzania danych. Poza tym, w systemie są także obecne różne komponenty pakietu DirectX. Są nimi m. in. DirectDraw (dla grafiki dwuwymiarowej), Direct3D (grafika trójwymiarowa), DirectSound (obsługa dźwięku) oraz DirectPlay (gry przez Internet).
Windows XP Embedded a przetwarzanie w czasie rzeczywistym
Jak już wspomniano na wstępie tego rozdziału, system Windows XP Embedded nie jest zasadniczo systemem czasu rzeczywistego. Można jednak przy pomocy dodatkowych narzędzi z niego taki system uczynić. Sam Windows XP Embedded składa się dokładnie z tych samych plików binarnych, co jego brat, przeznaczony na komputery typu desktop. Tylko niektóre zostały nieznacznie zmodyfikowane lub uproszczone. Tak samo, jak Windows CE .NET, system ten został podzielony na szereg komponentów. Producenci mogą wybrać te, które są przeznaczone dla konkretnych urządzeń. Obecnie, dostępnych jest ok. 10 tysięcy różnych komponentów. Bardzo dobrze rozwiązano problem różnych wersji językowych produktu. Odpowiednie pakiety przepakowano i w ten sposób docelowy obraz może zostać zlokalizowany, co jeszcze bardziej zmniejszy jego rozmiar. System ten, jest przeznaczony raczej do dość złożonych i wyposażonych w większe ilości pamięci urządzeń. Można pod jego kontrolą uruchamiać wszelkie starsze aplikacje (system Windows CE .NET nie jest zgodny z innymi wersjami „okienek” na poziomie binarnym) oraz korzystać z najnowszych technologii (tak jak w Windows XP). Ograniczeniem jest natomiast to, że Windows XP Embedded jest przeznaczony tylko dla procesorów rodziny x86. Nie są wspierane inne platformy.
Informacje o wszelkich komponentach w Windows XP Embedded, są utrzymywane w bazie danych SQL. Może ona być zarówno zdalna (dostępna poprzez Microsoft SQL Server), jak i lokalna (dostępna poprzez Microsoft Data Engine - MSDE; serwer ten jest rozpowszechniany wraz z systemem). W odróżnieniu od poprzednich wersji systemu (Windows NT Embedded 4.0), informacje te były zapisane w lokalnej bazie Jet (plik .mdb - Microsoft Access). Łączność pomiędzy narzędziami modyfikującymi strukturę systemu i jego komponentów (np.: Component Designer, Component Database Manager, Target Designer - odpowiednik Platform Builder'a z Windows CE .NET), a bazą danych jest realizowana przy pomocy specjalnego interfejsu o nazwie CMI (ang. Component Management Interface). Z tego względu, przed dokonaniem jakichkolwiek zmian, konieczne jest ustanowienie odpowiedniego połączenia. Wszelkie zmiany w bazie danych są dokonywane w ramach transakcji. Aby cokolwiek z niej usunąć, potrzebne jest posiadanie dostępu na wyłączność. Jeśli w tym czasie otwarte jest jakiekolwiek inne połączenie, to próba skasowania rekordu się nie powiedzie. Z drugiej strony jednak, kiedy jakieś narzędzie uzyska połączenie na wyłączność, to wówczas żadne inne nie będzie mogło ustanowić kolejnego. Moduł CMI występuje w postaci serwera COM. Z tego też względu, wszystko w systemie jest traktowane jak obiekt (komponenty, pliki, rejestr itp.).
Istnieje wiele rozwiązań, dzięki którym do Windows XP Embedded dodawane są możliwości przetwarzania w czasie rzeczywistym. Przykładowo, oprogramowanie o nazwie INtime, firmy TenAsyc sprawia, że aplikacje mogą się zachowywać w bardziej przewidywalny sposób oraz jednocześnie istnieć w środowiskach o dużych możliwościach rozbudowy. Pomimo ogromnej złożoności systemu Windows XP Embedded, programiści otrzymują możliwość tworzenia aplikacji spełniających najbardziej rygorystyczne ograniczenia czasowe. INtime, to tak naprawdę osobny system operacyjny czasu rzeczywistego. Jego jądro zostało odziedziczone z systemu iRMX produkcji Intela. INtime egzystuje sobie w swojej własnej wirtualnej maszynie, która zapewnia mu ochronę przed wszystkim, co pochodzi od systemu Windows. Jedyne co łączy oba systemy, to korzystanie z tego samego procesora oraz kontroler przerwań. Kiedy maszyna musi wykonać zadanie czasu rzeczywistego, przełączany jest kontekst i kontrolę przejmuje LNtime. Kiedy ono zakończy swoje prace, następuje ponowne przełączenie kontekstu i kontrola wraca do Windows. Tak więc, aplikacje korzystające z LNtime zyskują pewien poziom bezpieczeństwa. Jakiekolwiek błędy w nich, nie są w stanie zakłócić pracy całego systemu. Do dyspozycji programistów oddano ponad 8000 możliwych do użycia obiektów. Wszystkie mają priorytet ponad zadania systemu Windows. Oferowane są m. in. stos TCP/IP, interfejs do obsługi USB. Wspierane jest 256 możliwych priorytetów. Do szeregowania zadań został użyty algorytm umożliwiający wywłaszczenie i opierający się o podział czasu procesora. Do pisania aplikacji, może zostać użyty pakiet Microsoft Visual Studio 6.0 lub Microsoft Visual Studio .NET.
Podsumowanie
Autor poczynił wszelkie starania, aby niniejsza praca stanowiła kompletne i spójne źródło wiedzy na temat systemów czasu rzeczywistego. W pierwszych rozdziałach, Czytelnik poznał ogólne koncepcje ich budowy oraz opis implementowanych w nich mechanizmów. Okazało się, że taki system, aby poprawnie mógł funkcjonować, musi zostać wyposażony przynajmniej w jednostkę obliczeniową (procesor), jakąś formę pamięci (np.: Flash) oraz jednostkę odmierzającą czas. Również bardzo istotnym składnikiem, jest właściwie dobrana warstwa oprogramowania. To ona będzie stanowić interfejs pomiędzy sprzętem a użytkownikiem. Z punktu widzenia celów pracy, warstwa ta okazała się najistotniejsza. Dlatego też, została poddana bardzo drobiazgowej analizie. Systemy operacyjne czasu rzeczywistego są niejednokrotnie bardzo skomplikowane oraz gromadzą w sobie szereg różnych mechanizmów. Ich możliwości są ograniczone tylko przez wyobraźnię projektantów. Należy jednak zwrócić na bardzo istotny fakt. Otóż, są one dedykowane bardzo konkretnym zastosowaniom. Z tego też względu, muszą być bardzo modularne oraz w łatwy sposób umieć się dostosować do konkretnych wymagań. Takie podejście ma oczywiście bezpośredni zysk ekonomiczny. Jak się okazuje, dobra skalowalność systemu sprawia, że jest on tańszy w utrzymaniu oraz cechuje się większym powodzeniem wśród producentów urządzeń. W zależności od potrzeb, można go albo pozbawić jakiś funkcji, bądź wyposażyć w nowe. Przykładami modułów, które nie zawsze są wykorzystywane, to przede wszystkim wszelkiego rodzaju interfejsy graficzne, pakiety wzbogacające system o usługi multimedialne oraz zapewniające komunikację z różnymi rodzajami sieci. Te części, które występują zawsze, to oczywiście te najbardziej krytyczne. Ich obowiązki są podzielone i dotyczą m. in. szeregowania zadań oraz gospodarowania mocą procesora, zarządzanie pamięcią, obsługa przerwań. Poza tym, system musi udostępniać jakąś formę komunikacji pomiędzy poszczególnymi zadaniami. Ich forma jest zależna od konkretnej architektury. Zawsze są to jednak jakieś kolejki lub pewne mechanizmy umożliwiające tworzenie obszarów współdzielonych. Aby zapewnić integralność przesyłanych danych oraz uniemożliwić wzajemnego ich nadpisywanie, systemy te, udostępniają również liczne mechanizmy służące do synchronizacji.
Ogólnie, z przetwarzaniem w czasie rzeczywistym wiąże się ryzyko powstania bardzo niekorzystnych sytuacji. Przede wszystkim, aplikacje powinny zostać tak zaprojektowane, aby wykluczyć możliwość zakleszczenia się zadań. Co prawda systemy implementują pewne mechanizmy potrafiące przywrócić normalną pracę, jednakże jest to dokonywane zawsze kosztem dodatkowego czasu. Poza tym, jak to zawsze bywa, lepiej zapobiegać niż leczyć. Drugą zmorą współbieżnej realizacji kilku zadań, jest możliwość powstania tzw. inwersji priorytetów. Może ona w najgorszym przypadku stać się przyczyną powstania opóźnień o nieznanych wartościach! Na szczęście, protokoły wykorzystujące dziedziczenie priorytetów lub pułap priorytetów, potrafią całkowicie wyeliminować jej skrajnie niekorzystne skutki.
W pracy nie zabrakło także szczegółowych opisów architektur trzech najbardziej znanych systemów operacyjnych czasu rzeczywistego. Zaliczają się do nich: RTLinux, QNX (Neutrino) oraz Windows Embedded. Nasuwa się zasadnicze pytanie: czy można spośród nich wyłonić ten najlepszy, nadający się do wszelkich możliwych zastosowań? Otóż, odpowiedź jest negatywna. Analizując ich budowę, można wysnuć wniosek, że każdy jest przeznaczony dla różnego typu urządzeń. Wszędzie tam, gdzie pamięć stanowi bardzo wąskie gardło, z całą pewnością swoje zastosowanie znajdzie system QNX. Jego ogromną zaletą jest bardzo głęboko posunięta modularność oraz skalowalność. Dodatkowo fakt, że komunikacja pomiędzy procesami znajdującymi się na odległych węzłach sieci jest tak samo prosta, jak w obrębie jednego komputera, czyni z niego bardzo atrakcyjny produkt. Poza tym, system naprawdę bardzo dobrze radzi sobie z przetwarzaniem w czasie rzeczywistym, nawet w warunkach intensywnego obciążenia operacjami dyskowymi i sieciowymi. RTLinux natomiast, bardzo szybko potrafi obsługiwać przerwania w warunkach dużego obciążenia obliczeniami, przy jednoczesnych znikomych operacjach dyskowych. Jego architektura wymusza jednak pewien styl programowania. Otóż, każda aplikacja musi się dzielić na dwie części. Część obliczeniowa powinna pozostać pod kontrolą RTLinuksa, z kolei wszelkie operacje dyskowe i sieciowe, są dalej kontrolowane przez system Linux. RTLinuks implementuje kilka mechanizmów, dzięki którym obie te części mogą się ze sobą swobodnie komunikować. Jeśli chodzi o systemy z rodziny Windows Embedded, to są one dość zróżnicowane. Windows CE .NET potrafi sprostać rygorystycznym wymaganiom czasowym. Windows XP Embedded natomiast, bez dodatkowych mechanizmów praktycznie nie może zostać sklasyfikowany jako rygorystyczny system operacyjny czasu rzeczywistego. Na pewno oba systemy mają bardzo szerokie pole do popisu wszędzie tam, gdzie konieczne jest wykorzystanie wszelkich najnowszych technologii, a zwłaszcza tych związanych z multimediami. Można z nich tworzyć bardzo wydajne konsole służące do gier oraz instalować w nich różne pakiety biurowe. Dla porównania, system QNX jest pozbawiony tego typu funkcjonalności. Jego twórcy nie zamierzają wspierać tego typu zastosowań. Poza tym, systemy ze stajni Microsoftu, są modularne oraz mogą być poddawane różnym modyfikacjom. Służą do tego celu specjalnie zaprojektowane narzędzia.
Dość dobrym kryterium wyboru systemu najlepiej spełniającego oczekiwania danego użytkownika, jest analiza licencji, w oparciu o którą, dany produkt jest rozpowszechniany. W zależności od punktu widzenia, może ona przemawiać za wyborem konkretnej architektury, ale może także budować niemożliwe do przekroczenia bariery. Przykładowo, dla środowisk akademickich, wszystkich chcących zgłębić wiedzę o systemach operacyjnych oraz lubiących różnego rodzaju eksperymenty, bezkompromisowym faworytem wydaje się być duet Linux + RTLinux. Jest on nie tylko darmowy, ale również udostępniany wraz z całym kodem źródłowym. Modyfikacji można w nim dokonać nie tylko na poziomie konkretnych modułów, ale także na poziomie samego jądra. Jedynym warunkiem, jest jednak konieczność dokładnego udokumentowania wszelkich wprowadzonych zmian. Oczywiście taka forma dystrybucji (wynikająca w warunków licencji GPL), staje się źródłem problemów dla producentów komercyjnych. Bardzo często nie chcą oni ujawniać szczegółów proponowanych przez siebie rozwiązań. Dlatego też, swoją uwagę często kierują oni w stronę rozwiązań płatnych. Koszt zakupu takiego systemu jednak nie należy do niskich, tak więc na taką opcję pozwolić mogą sobie tylko dość dobrze sytuowane firmy.
Błędem jest także stwierdzenie, że skoro systemy czasu rzeczywistego bardzo dobrze radzą sobie z czasem, a opóźnienia generowanych przez nie odpowiedzi są bardzo małe, to są to jedyne słuszne systemy. Należy zwrócić uwagę na bardzo istotną sprawę. W tego typu systemach, większość czasu (jeśli nie cały), jest poświęcona na wszelkiego rodzaju obliczenia. Praktycznie nie ma w nich miejsca na tzw. prace interaktywne, jak np.: korzystanie z edytora tekstu. Sprawdzają się one wszędzie tam, gdzie wykonuje się jak najmniej operacji dyskowych, natomiast zleca się maksymalne ilości zadań, które w jak największym stopniu korzystają z procesora. Typowe komputery biurkowe, tak naprawdę są owocem wielu kompromisów. Muszą one zapewnić uśrednioną wydajność wszelkim możliwym sposobom ich wykorzystania. Nie mogą być ukierunkowane tylko na konkretną klasę zastosowań jak np.: przyjmowanie danych z portu szeregowego i generowanie odpowiedzi na ekran oraz przesyłanie jej dalej. Przeciętny komputer biurowy, ma oferować jak największą funkcjonalność. Nie powinien być jakoś szczególnie ograniczany.
Lektura kolejnych rozdziałów, powoduje również powstanie stwierdzenia, że programowanie w świecie systemów czasu rzeczywistego, w porównaniu do tradycyjnych, staje się niestety trudniejsze, bardziej złożone oraz nierzadko wywołuje frustrację. Reguły tego świata zmuszają twórcę aplikacji do ciągłych kompromisów. Pojawiają się skomplikowane problemy, jak wzajemne oddziaływanie jednocześnie wykonujących się zadań oraz inwersja priorytetów, skutecznie opóźniająca niekiedy czas wykonania, który przecież jest bardzo często krytyczny. Do tego, system jest źródłem różnych ograniczeń, jak np.: ilość dostępnej pamięci. Rozwiązanie tych kwestii jest bardzo ważne. System czasu rzeczywistego, to przecież nie tylko jednostka, która dokonuje jakiś tam obliczeń. Aby jego reputacja oraz popularność utrzymywały zadowalający poziom, system musi być przede wszystkim wiarygodny. W tym przypadku, oznacza to zwrócenie poprawnych wyników oraz dokonanie tego na czas.
Dołączona do pracy prezentacja, została wykonana w postaci strony internetowej. Taka jej forma, pozwoli każdemu bardzo wygodnie oraz bez konieczności instalowania dodatkowych komponentów przejrzenie prezentowanych w niej zagadnień. Została tak zaprojektowana, aby maksymalnie skrócić czas poszukiwania odpowiedzi na nurtujące Czytelnika pytania. Przy jej pomocy, znalezienie np.: definicji kolejki lub sposobu obsługi przerwań przez system Windows CE .NET, nie stanowi już problemu.
Literatura
Grupa dyskusyjna: comp.realtime, Real - Time FAQ.
Kazimierz Lal, Tomasz Rak, Krzysztof Orkisz : “RTLinux - system czasu rzeczywistego”, HELION, 2003.
Roman A. Plaza, Eugeniusz J. Wróbel: „Systemy czasu rzeczywistego”, Wydawnictwo Naukowo - Techniczne, Warszawa 1988.
John A. Stankovic: „Real - Time Computing”, University of Massachusetts, Amberst, MA 01003, 1992.
Qing Li, Carolyn Yao: “Real-Time Concepts for Embedded Systems”, CMP Books, 2003.
Bruce Powel Douglass: “Real-Time Design Patterns: Robust Scalable Architecture for Real-Time Systems”, Addison Wesley, 2002.
Patricia Balbastre, Ismael Ripoll: “Integrated Dynamic Priority Scheduler for RTLinux”, Universidad Politecnica de Valencia, Spain, 2001.
Neil C. Audsley: “Resource control for hard real - time systems: a review”, Real - Time Systems Research Group, Department of Computer Science, University of York, 1991.
A. Silberschatz, P. Galvin: „Podstawy systemów operacyjnych”, WNT, 2000.
Artykuł poświęcony testom systemu QNX: „QNX, A Real Time MultiEverything OS that Runs On LANs”, LAN Magazine (Styczeń 1989).
Douglas Boling: „Programming Microsoft Windows CE .NET, Third Edition”, Microsoft Press, 2003.
Microsoft Official Curriculum: “Intoduction To MS Windows CE .NET (2530A)”.
Microsoft Official Curriculum: “Developing Embedded Solutions For Windows CE .NET (2540B)”.
Dodatek A. Procesory Embedded
Jak już Czytelnik zdążył zauważyć, tematyka systemów czasu rzeczywistego jest bardzo rozległa a na rynku istnieje wiele rozwiązań. Należy jednak pamiętać, że jako system czasu rzeczywistego, rozumie się nie tylko system operacyjny czasu rzeczywistego (ang. Real-Time Operating System - RTOS) i przeznaczone dla niego aplikacje, ale także sprzęt, w oparciu o który one działają. Z systemami tzw. wbudowanymi (ang. embedded) bardzo często wiążą się specjalnie przeznaczone dla nich rodzaje procesorów. Rozdział ten, co prawda nie przedstawi szczegółowo architektury takich procesorów, zostanie jednak zaprezentowana w nim ich egzotyka oraz wynikające z ich zastosowań wymagania konstrukcyjne. Należy go traktować jako swego rodzaju dodatek.
Coraz większa liczba urządzeń powszechnego użytku zostaje wyposażona w jakieś formy jednostek obliczeniowych. Ich rozwój postępuje bardzo szybko. Cechuje je ogromna różnorodność i niejednokrotnie przyjęte rozwiązania są bardzo oryginalne. Zakres ich zastosowań, jest coraz szerszy i obejmuje np.: notebooki, routery, modemy kablowe, telefony komórkowe itd. W przeważającej liczbie przypadków, głównym wymaganiem stawianym tego typu procesorom, jest możliwie najmniejszy pobór mocy. Drugim jest oczywiście wydajność. Na rynku procesorów, niewątpliwie na uznanie zasłużyła sobie firma Intel. Jej produkty są stosowane niemalże we wszystkich dziedzinach życia, w których tylko pojawia się komputer. Ostatnio, firma ta ogłosiła powstanie nowego procesora dla zastosowań wbudowanych. Jej zdaniem, przyspieszy on powstawanie bardziej innowacyjnych oraz efektywnych urządzeń. Procesor został oznaczony symbolem PXA270 i jest zbudowany w technologii XScale. Charakteryzuje się ona zwiększoną wydajnością oraz mniejszym poborem mocy. PXA270 ma przynieść znaczne rozszerzenie multimedialnych możliwości kieszonkowych notatników elektronicznych oraz telefonów komórkowych. Jest pierwszym produktem, który integruje w sobie technologię o nazwie Intel Wireless MMX. Dostarcza ona zestaw nowych instrukcji, dzięki którym zbędne stają się w urządzeniu wszelkie dodatkowe procesory i akceleratory. Ma to oczywiście bezpośredni wpływ na mniejsze zapotrzebowanie całej platformy na energię. Poza tym, technologia ta zwiększa wydajność grafiki trójwymiarowej, dodaje zaawansowane opcje wideo oraz audio, włączając możliwość szyfrowania przesyłu danych strumieniowych.
Również po raz pierwszy, zintegrowane zostały istotne opcje związane z bezpieczeństwem. Intel Wireless Trusted Platform umożliwia korzystanie z takich usług jak np.: bezpieczne bootowanie, przechowywanie poufnych informacji, kluczy kryptograficznych oraz obsługa popularnych protokołów bezpieczeństwa.
Minimalizację zużycia energii osiągnięto dzięki implementacji technologii Wireless Intel SpeedStep. Zarządza ona w inteligentny sposób napięciem oraz częstotliwością. Wydajność urządzenia jest na bieżąco dostosowywana do aktualnych potrzeb. Pozwala to zwiększyć czas ciągłej pracy baterii nawet dwukrotnie.
Z myślą o zwolennikach fotografii, procesor został wyposażony w technologię Intel Quick Capture. Pozwala ona korzystać z czujników, których rozdzielczość obrazu wynosi 4-megapiksele i więcej. Poza tym, możliwe jest także przechwytywanie sekwencji wideo. Ponieważ przetwarzanie obrazu jest wykonywanie przez chip PXA270, użytkownik nie jest narażony na dodatkowe koszty związane z zakupem dodatkowych podzespołów. Oczywiście ma to również wpływ na mniejszy pobór mocy. Technologia ta umożliwia wykonanie trzech operacji. Pierwsza, charakteryzuje się najmniejszym poborem mocy oraz umożliwia wykonywanie podglądów (ang. Quick View Mode). Dwie kolejne, umożliwiają przechwytywanie zdjęć o wysokiej rozdzielczości (ang. Quick Shot mode) oraz sekwencji wideo (ang. Quick Video mode).
Wzorzec dla architektury tego typu procesorów, stanowi zaprojektowany w 1987 roku, układ o nazwie ARM (ang. Advanced RISC Machine). Początkowo stanowił on siłę napędową komputera o nazwie Acorn Archimedes. Przyczyną tak długiej popularności procesora ARM, jest jego model programowy o nazwie ISA (ang. Instruction Set Architecture). Jego lista instrukcji jest mało rozbudowana i jest zgodna z RISC. Potok wykonawczy, składa się z trzech faz: pobranie, wykonanie i zapis. Pozornie uboga konstrukcja, ma liczne zalety. Procesor jest prosty w konstrukcji, jego logika nie jest zbytnio rozbudowana i może pracować z szybkim zegarem. Poza tym, oczywiście całość cechuje mały rozmiar i niewielki pobór mocy. Znika także problem chłodzenia. Ponieważ lista instrukcji jest ograniczona, nie ma potrzeby stosowania jakiś skomplikowanych algorytmów optymalizacji kodu. Jest ona dokonywana dynamicznie w procesie kompilacji.
Oprócz jednostki ALU (ang. Arithmetic and Logic Unit), w procesorze ARM, znajduje się tzw. układ przesuwający (ang. barrel shifter). Dzięki temu, możliwe jest jednoczesne wykonanie dwóch operacji: przesunięcia i jakiejś innej arytmetycznej.
Kolejne wersje ARM umożliwiły operacje arytmetyczne na danych 32 - i 64 - bitowych. Wyposażono go również w jednostkę zarządzającą pamięcią MMU (ang. Memory Management Unit), która potrafi pracować w trybie chronionym. Zwiększono także liczbę faz przetwarzania - z trzech do pięciu.
Wkrótce architekturą tą zainteresowała się firma Intel i na jej bazie stworzyła układ o nazwie StrongARM. Jądro nowego procesora zostało znacznie uzupełnione. Dodano do niego m. in. sterownik wyświetlacza (LCD), sterownik klawiatury, kodeki, układ zarządzania poborem mocy. Pamięć cache została podzielona na dwie części: dla danych i dla instrukcji. Architekturę takiego procesora (w wersji: StrongARM SA-1110) najlepiej przedstawia poniższy schemat (rys. 47):
Rysunek 47. Architektura procesora StrongARM SA-1110
Dodatek B. Spis zawartości dołączonej płyty CD
PRACA - niniejszy dokument
MATERIAŁY - witryny WWW wykorzystane w pracy
WWW - prezentacja w postaci strony WWW
RYSUNKI - wszystkie rysunki zawarte w pracy
Spis ilustracji
Rysunek 1. Prosty schemat systemu czasu rzeczywistego.
Spis kodów źródłowych
EEPROM - (Electronically Erasable Programmable Read Only Memory) - jest to pamięć, której zawartość można zmieniać przy pomocy impulsów elektrycznych. Dodatkowo umożliwia kasowanie i przeprogramowanie wybranych bajtów. Obie te cechy odróżniają ją od pamięci EPROM, do której przeprogramowania używa się specjalnych programatorów.
Flash - jest to rodzaj pamięci EEPROM, która umożliwia na programowanie na poziomie bloków (np. 512B), co znacznie przyspiesza cały proces.
Protokół transmisji - są to wszelkie reguły, w oparciu o które dokonuje się transmisji. Przykładowo może mówić, że transmisja obrazu jest inicjalizowana przez loader, po czym host przesyła najpierw rozmiar obrazu a później jego zawartość. Po zakończeniu loader wysyła jakiś sygnał.
Parametry komunikacji - to np. szybkość transmisji, rozmiar pakietów itp.
JTAG - Joint Test Action Group. Celem tej grupy było stworzenie tanich sposobów do testowania (standard: IEEE1149.1).
BDM - Background Debug Mode. Jest to interfejs do debugowania wprowadzone przez koncern Motorola i implementowany w ich procesorach.
BSP - Board Support Package. Jest to zbiór sterowników dla sprzętu oraz innych urządzeń.
API - Application Programmer Interface. Jest to zbiór funkcji, klas oraz innych narzędzi umożliwiających programiście korzystanie z szeregu obiektów systemu.
Wiele systemów czasy rzeczywistego rezerwuje pewien zestaw priorytetów dla krytycznych zadań jądra. Zadania użytkownika nie powinny ich używać, gdyż może to obniżyć wydajność, a nawet doprowadzić do załamania całego systemu.
Zakleszczenie - to stan, w którym wszystkie zadania oczekują na niemożliwy do spełnienia warunek. Wykonywanie żadnego z nich nie może zostać wznowione.
Inwersja priorytetów - polega na tym, że zadanie o wyższym priorytecie jest blokowane przez zadanie o niższym priorytecie.
Głodzenie - (ang. starvation) - jest to stan, w którym część zadań blokuje inne, uniemożliwiając im dostęp do procesora.
POSIX - Operating System Interface for Computing Environments. Bazuje na systemach rodziny UNIX i definiuje sposób oddziaływania aplikacji i systemu operacyjnego. Określa np. sposoby zarządzania procesami, wielowątkowość i funkcje czasu rzeczywistego.
FIFO - First In First Out - jest to rodzaj struktury realizującej zasadę: „pierwszy przyszedł, pierwszy zostanie obsłużony”.
Zakleszczenie procesów - (ang. deadlock) - jest to sytuacja, w której zablokowane procesy oczekują na niemożliwy do spełnienia warunek. Przykładem może być sytuacja, w której proces A oczekuje na rezultat wykonania procesu B, podczas gdy ten oczekuje aż zakończy swe działanie proces A.
DMA - Direct Memory Access. Jest to kanał dający bezpośredni dostęp do pamięci. Wówczas urządzenie ma możliwość dokonania bezpośrednich do niej odwołań z pominięciem procesora, co znacznie polepsza wydajność całego systemu.
Cykl - to zamknięta droga prosta, której ciągiem różnych wierzchołków jest ciąg {vj, j=1,…, n}. Droga w grafie skierowanym G nazywamy ciąg krawędzi taki, że koniec jednej krawędzi jest początkiem następnej. Droga jest zamknięta, jeśli v1 = vn+1 oraz jest prosta jeśli wszystkie jej krawędzie są różne.
Sekcja krytyczna - to ta część zadania, która korzysta z zasobu współdzielonego. Powinna być jak najkrótsza oraz chroniona poprzez odpowiednie mechanizmy.
W chwili obecnej (2004 r.) system RTLinux jako jedyny implementuje ten profil.
Jest to bardzo precyzyjna technika mierzenia upływu czasu. Opiera się ona o tzw. licznik wysokiej rozdzielczości, który jest zerowany przy starcie procesora i zwiększany po każdym cyklu zegara taktującego. Wspomniana funkcja zwraca 64-bitową liczbę, tak więc przepełnienie takiego licznika jest praktycznie niemożliwe.
Tryb chroniony - sposób zarządzania pamięcią RAM komputera który stał się możliwy od czasów wprowadzenia na rynek procesora 286. Gdy program lub system operacyjny działa w trybie chronionym, może korzystać z wielozadaniowości, z pamięci RAM powyżej 1 MB, pamięci wirtualnej i chronionych obszarów pamięci (tzn. takich, z których nie mogą w tym samym czasie skorzystać inne programy). Tryb chroniony oferuje zaawansowany i znacznie lepszy sposób zarządzania pamięcią od gwarantowanego przez tryb rzeczywisty. Nowoczesne systemy operacyjne, takie jak Windows, OS/2 czy UNIX, pracują w trybie chronionym
Arcnet - standard wypierany obecnie przez Ethernet. Pozwala na transmisję danych z prędkością od 1Mb/s do 100Mb/s na odległość do 4 km. Ma wielu zwolenników ze względu na dużą odporność na zakłócenia i niskiej jakości kable.
COM (ang. Component Object Model) - to specyfikacja firmy Microsoft. Określa model obiektów, które mogą zostać wykorzystane w programach tworzonych w różnych językach programowania.
DCOM (ang. Distributed COM) - to rozszerzenie specyfikacji COM na obiekty w sieci. Komponenty programowe, zawarte w aplikacjach klienckich, mają możliwość korzystania z funkcji udostępnianych przez te, znajdujące się na innych komputerach w sieci. Transmisja danych odbywa się w oparciu o protokoły TCP/IP oraz HTTP.
W tym rozumieniu, system czasu rzeczywistego, to nie tylko system operacyjny (system operacyjny czasu rzeczywistego; ang. Real - Time Operating System - RTOS), ale także sprzęt i aplikacje.
Wszystkie pliki umiejscowione w pamięci RAM są skompresowane. Dlatego też pliki z RAM nie posiadają tej flagi. Dane umieszczone fizycznie w ROM nie podlegają automatycznej kompresji. Wówczas atrybut ten dostarcza dodatkowe informacje.
RISC (ang. Reduced Instruction Set Computers) - to architektura mikroprocesorów, której początki sięgają roku 1980. Jej cechy, to m. in. zredukowana liczba rozkazów (ok. kilkudziesięciu - dzięki temu ich dekodowanie jest prostsze), ograniczona komunikacja procesor - pamięć (do operacji na pamięci są specjalne instrukcje. Reszta operacji opiera się o rejestry), zwiększona liczba rejestrów oraz wykonywanie wszystkich rozkazów w jednym cyklu maszynowym (pozwala to np.: na zrównoleglenie wykonywania obliczeń poprzez przetwarzanie potokowe). Jeśli chodzi o procesory firmy Intel, to programista widzi je jako CISC (ang. Complex Instruction Set Computers - lista ich rozkazów jest bardzo rozbudowana oraz niejednokrotnie ich wykonanie zajmuje wiele cykli procesora). Ich rdzeń jest natomiast RISC - owy. W czasie przetwarzania, rozbudowane rozkazy CISC są rozbijane na tzw. mikrorozkazy.
- 3 -
Spis treści
- 166 -
Spis treści
Spis kodów źródłowych