SYSTEMY OPERACYJNE CZASU RZECZYWISTEGO (OPRACOWANIE WYKONANE NA PODSTAWIE WYKŁADÓW DR HAB. INŻ. PROF. PS STANISŁAWA BAŃKI) OPRACOWANIE WYKONALI: ANDRZEJ KIDO GRZEGORZ MIKOŁAJCZYK
Literatura: 1. K. Sacha: Systemy czasu rzeczywistego 1993r. 2. K. Sacha: QNX - System operacyjny X-Serwis Sp.z.o.o 1995 3. W. Cellary, W. Wieczerzycki: Wielozadaniowy system operacyjny czasu rzeczywistego, irmx-88 WNT 1988 4. T. Szmuc, M. Szymkat: Definicja: Pojęcie czasu rzeczywistego występuje w systemach mikrokomputerowych, wykorzystywany jest do zadań sterowania, obsługi, nadzoru i wspomagania procesów technologicznych. Mówimy, że system mikrokomputerowy działa w czasie rzeczywistym, jeśli decyzje wypracowane przez ten system realizowane są w tempie obsługiwanego procesu, tzn. że czas reakcji systemu sterowania powinien być niezauważalny przez proces. Typowe przykłady zastosowania SOCR to: monitoring i/lub sterowanie procesami przemysłowymi, systemy telekomunikacyjne (GPS, telefonia komórkowa), wojskowe systemy dowodzenia i sterowania, systemy kontroli ruchu lotniczego, systemy bankowe i hotelarskie, aparaty fotograficzne, pralki do prania, regulatory mikroprocesorowe. Policja w Szczecinie, kolej francuska w TGV, metra w Paryżu, Kairze, Kalkucie. W zależności od zakresu zastosowania SORC dzielimy na : Uniwersalne SCR o otwartej strukturze, rekonfigurowane ( irmx-88, irmx-86, VENIX, QNX, VxWorks, OS-9 ) Wyspecjalizowane, dedykowane (embedded) przeznaczone do sterowania konkretnymi urządzeniami lub instalacją Uniwersalne systemy składają się z modułów funkcjonalnych, których można tworzyć mniejsze lub większe systemy operacyjne. Powinny one dawać możliwość uzupełniania o nowe moduły, jak również zastępowanie modułów standardowych nowymi własnymi. Embedded zamknięte, bez możliwości jakiejkolwiek zmiany. Główne wymagania stawiane systemom operacyjnym czasu rzeczywistego: 1. Systemy musza mieć możliwość przyłączania dużej ilości wejść i wyjść, zapewniających sprzężenie komputera z obiektem sterowanym jak również z innymi systemami mikrokomputerowymi. 2. Systemu muszą pracować niezawodnie przez 24 godz./dobę. Aby to było możliwe stosuje się nadmiar sprzętowy lub średni czas miedzy uszkodzeniami powinien wynosić kilka tysięcy godzin, lub przez odpowiednie środki programowe, tak aby średni czas odtworzenia bazy danych nie przekraczał kilku minut. Główne cechy systemów SOCR: 1. Wielozadaniowość i współbieżność, tzn. że oprogramowanie zorganizowane jest w postaci zadań, będących w pamięci operacyjnej. Zadania te obsługują asynchroniczne zdarzenia zachodzące w procesie, mogące pojawiać się losowo. Zadania te wykonywane są współbieżnie i zwykle, każde z tych zadań wykonywane jest przez pewien krótki czas.
2. Wieloprocesorowość ( nie występuje we wszystkich systemach ) zadania mogą być wykonywane równolegle przez kilka procesów, rozlokowanych w różnych węzłach sieci. Systemy operacyjne powinny dawać możliwość pracy wieloprocesorowej. Ta cecha podwyższa efektywność, niezawodność, modularność. 3. Rozbudowany, wielopoziomowy mechanizm przerwań. Systemy operacyjne organizują przypadkowo napływające zdarzenia w postaci kolejek zdarzeń do obsługi, i zdarzenie o najwyższym priorytecie jest zgłaszane jako przerwanie i jest jako pierwsze obsługiwane. 4. Stan STOP jest niedozwolony. 5. W systemach uniwersalnych charakterystyczna jest duża pamięć operacyjna, zwykle bez stronicowania. Część pamięci musi być podtrzymywana bateryjnie. System powinien posiadać rejestry ochrony pamięci, które chronią obszary systemu operacyjnego przed ingerencją programów użytkowych. 6. System operacyjny jest praktycznie bezdyskowy. 7. Praktycznie nie występuje duża baza danych, ale za wyjątkiem systemów stosowanych w bankach, bibliotekach, hotelach, centrach handlowych, sieciach rezerwacji miejsc. 8. System musi mieć oprócz zegara systemowego zegar czasu rzeczywistego zsynchronizowanego z krajowym wzorcem czasu. Ponad to system posiada liczne rejestry i liczniki, które umożliwiają organizację uwarunkowań czasowych lub ich kontrolę. Istotnym parametrem systemu jest kwant czasowy zegara czasu rzeczywistego, powinien on być możliwie jak najmniejszy, jest to krotność taktu zegara systemowego. Ogólna struktura systemów SOCR: Zwykle systemem operacyjnym czasu rzeczywistego nazywamy jądro z warstwą systemową. Całość jest systemem aplikacyjnym. Z punktu widzenia zarządzania systemem aplikacyjnym, w systemie wyróżnia się tylko jądro i zadanie ( ang. kernel & tasks ). Zadaniem nazywamy najmniejszą jednostkę programową, która może samodzielnie ubiegać się o zasoby systemu mikroprocesorowego. Każde z zadań, które są umieszczone w pamięci po przydzieleniu zasobów stają się procesami. Procesem nazywamy strukturę dynamiczną, która zawiera w sobie kod programu, miejsca dla danych i otoczenie programu.
Z punktu widzenia działania systemu zadania z warstwy systemowej nie różnią się od zadań z warstwy aplikacyjnej. Zadaniem jądra systemu jest szeregowanie(kolejkowanie) procesów utworzonych z zadań, implementacja, obsługa komunikatów wymienianych między procesami(zadaniami), przyjmowanie zgłoszeń przerwań. Dlatego większość kodu jądra jest bardzo mała. Zadania mogą być dzielone na zadania systemowe i użytkowe. Przy czym zadania systemowe mogą być grupowane w postaci tzw. Modułów lub menedżerów. Typowymi modułami zadań systemowych są: moduł zarządzania pamięcią operacyjną, moduł konsoli operacyjnej, moduł zegara systemowego, moduł uruchamiania zadania(debager). W nowszych są jeszcze moduły do komunikacji w sieci. Natomiast w systemie QNX występują; menedżer procesów(tworzy i usuwa zadania, odmierza czas, rejestruje nazwy zadań), menedżer plików(organizuje system plików na dyskach i innych nośnikach), menedżer urządzeń( formatuje dane pomiędzy zadaniami a urządzeniami zewnętrznymi), menedżer sieci( obsługuje łącza i komunikację między węzłami). Zadania mogą być rezydentne i nierezydentne, (których kod znajduje się na dysku). Wszystkie inne występują w pamięci. *STANY ZADAŃ I PRIORYTETY* Wszystkie zadania traktowane są jednolicie, niezależnie od tego czy należą do modułów menedżerów systemowych czy też do grupy zadań systemowych. Każde z zadań musi starać się o przydział zasobów na ogólnie przyjętych zasadach. Każde z zadań ma określony priorytet oraz stan, w którym zadanie może się znajdować. Priorytet jest liczbą całkowitą z odpowiedniego zakresu, przy czym na ogół najmniejszej liczbie odpowiada najwyższy priorytet. VENIX <0-128> irmx-88 <0-255> QNX <31-0> VxWorks <0-255> OS-9 <0-65535> Stan zadania charakteryzowany jest jego aktualnym żądaniem zadania do jądra lub warstwy systemowej. Zwykle jest to 4-5 stanów, w którym zadanie może się znajdować. Najczęściej są to: stan wykonywania (E a =W), stan gotowości(r A =G), stan oczekiwania(zablokowania), stan zawieszenia(uśpienia), stan martwy(m). Dwa pierwsze są zdefiniowane identycznie we wszystkich znanych systemach. Stan (W): stan, w którym wszystkie żądania zgłoszone do systemu są spełnione włącznie z przydzieleniem mu czasu mikroprocesora. Stan (G): to stan, w którym wszystkie żądania zgłoszone do systemu zostały spełnione z wyjątkiem przydzielenia stanu do procesora. Pozostałe stany są różnie zdefiniowane dla różnych systemów. Zadania mogą przychodzić z jednego stanu na drugi, co przedstawia się za pomocą grafów. a) irmx
Stan (Z): stan, w którym zadanie czeka na odbiór komunikatu od innego zadania. Po otrzymaniu komunikatu lub po przekroczeniu czasu oczekiwania przechodzi do stanu (G). Stan (B-Z): stan zablokowania, w którym dane zadanie czeka na otrzymanie konkretnego komunikatu wyjść ze stanu zawieszenia, które odnosi się tylko do tego zadania. Komunikat może otrzymać od jądra lub od innego dowolnego zadania. Stan (B-G): stan zablokowania i zawieszenia, w którym zadanie może wyjść ze stanu gotowości tylko na skutek zadania aktualnie wykonywanego. b) QNX Stan (Z): stan zawieszenia czeka na odbiór komunikatu od innego zadania. Stan (Z S ): stan, w którym znajduje się przed wysłaniem do niego komunikatu. Po odczytaniu komunikatu zadanie przechodzi do stanu (Z r ) i odpowiadając na komunikat przechodzi do stanu (G). Odbywa się to w tle. Stan (M): jest to przeważnie taki stan zadania, w którym zadanie zostaje wyrzucone na dysk, bo system nie przewiduje wykonania tego zadania. c) VxWorks Stan (W) i (G): są tak samo zdefiniowane jak w w/w systemach. Stan (Z): jest to stan, który wprowadzono głównie do uruchamiania zadań. Przejście z tego stanu do stanu (W)-bezpośrednie- nie jest możliwe, natomiast możliwe jest przejście do innych stanów. Stan (O): stan oczekiwania, w którym zadanie przebywa tak długo, dopóki niedostępne są pewne zasoby. Stan (U): stan uśpienia, w którym zadanie jest zawieszane lub uśpione na pewien określony czas. Przy czym dodatkowo w tym systemie możliwe są kombinacje dwóch stanów, w których zadanie może się znajdować. Np. zadanie może być w stanie U+Z;O+Z;O+U
d) OS-9 Stan (Z): stan zawieszenia na pewien czas Stan (M): stan martwy W stanie wykonywania (W) może być zadanie o najwyższym priorytecie. Oznacza to, że jeżeli w stanie (G) pojawi się zdania o wyższym priorytecie, to to nowe zdania przejdzie w stan wykonywania, przy czym jądro odłoży cały kontekst uprzednio wykonywanego zadania i rozpocznie wykonywanie nowego zadania. W przypadku, gdy w stanie gotowości (G) znajduje się kilka zadań o tym samym priorytecie, to mogą być obsługiwane przez jeden z trzech algorytmów: 1. FIFO- algorytm, w którym pierwsze jest wykonywane zadanie to, które czekało najdłużej.w tym algorytmie zadanie to jest wykonywane aż do zakończenia lub jego zawieszenia spowodowane pojawieniem się zadania o wyższym priorytecie w stanie (G). 2. Karuzela (Round Robing), w tym algorytmie zadanie o takim samym(najwyższym priorytecie) są wykonywane kolejno w przydzielonym kwancie czasu (slice). 3. Karuzela adaptacyjna- stosowany w OS-9, algorytm, w którym każde z zadań ma przydzielony odcinek czasu, przy czym po wykonaniu zadania w danym odcinku czasowym, obniża mu się priorytet o 1. Jeśli czas oczekiwania na ponowne wykonanie przekroczy, np. 2s. to priorytet zadania podnosi się o 1. Dla zapobiegnięcia całkowitej dezorganizacji struktury oprogramowania zadanie zawieszane odzyskuje swój priorytet. 4. Są przewidziane miejsca na inne algorytmy obsługujące zadania o tym samym priorytecie. Typowymi funkcjami jądra są: a) Zarządzanie i szeregowanie zadań b) Zarządzanie i komunikacja między zadaniami c) Przyjmowanie zgłoszeń i obsługa przerwań
*ZARZĄDZANIE I SZEREGOWANIE ZADAŃ PRZEZ JĄDRO* Typowymi czynnościami jądra są: a) Tworzenie zadań b) Usuwanie zadań c) Zawieszanie zadań d) Wznawianie zadań SYSTEM irmx Tworzenie zadań: W systemie irmx-88,86 zadanie jest tworzone za pomocą wywołania systemowego: CALL rq$ctsk(stanic$task$description); Aby to polecenie było skuteczne w treści zadania, które tworzy to nowe zadanie powinna być umieszczona deklaracja typu: rq$ctsk PROCEDURE(stanic$task&desription)EXTERNAL; DECLARE static$task$desription POINTER; END rq$ctsk DECLARE static$task$desription STRUCTURE( name (6) BYTE program$counter POINTER, stack$pointer POINTER, stack$length WORD, data$segment POINTER, exange$adress POINTER, task$pointer POINTER, task$ndp BYTE); NEWTASK: PROCEDURE, /*.*/ END NEWTASK DATA(.); W deklaracji struktury stanowiącego opis statyczny nowego zadania są użyte zmienne o następujących znaczeniach: name - nazwa zadania( max. 6 znaków w kodzie ASCII), program$counter adres startowy zadania zajmujący podwójne słowo stack$pointer adres początku obszaru stosu zadania ( podwójne słowo) stach$length długość stosu data$segment adres segmentu danych(podwójne słowo) priority bajt zawierający priorytet zadania exange$adress słowo zawierające adres tzw. stałej skrzynki przypisanej do zadania. Jeśli 0 to zadanie było tworzone bez skrzynki. task$pointer adres dynamicznego opisu zadania(podwójne słowo) task$ndp zmienna umożliwiająca korzystanie lub nie z koprocesora DATA(,,.,., OH/OFFH )
Przedstawione na wstępie wywołanie, które jest jedną z instrukcji napisanej w języku PL/M- 86 powoduje: zainicjowanie dynamicznego opisu zadania w stanie gotowości, z odpowiednim priorytetem. Przy tworzeniu nowego zadania nie przewidziano żadnych mechanizmów dziedziczenia, i aby system ze wszystkimi przewidzianymi wcześniej zadaniami działał, należało przy pierwszym ładowaniu systemu przewidzieć- zarezerwować odpowiedni obszar na dynamiczny opis wszystkich zadań, zwykle41 bajtów, oraz obszar na prywatne stosy zadania, zwykle 128 bajtów. Usuwanie zadania: W podobny sposób po zadeklarowaniu wywołania systemowego: rq$dtsk: PROCEDURE ( dynamic$task$description) EXTERNAL; DECLARE dynamic$task$desription POINTER, END rq$dtsk Usunięcia zadania jest możliwe po wywołaniu: CALL rq$dtsk ( dynamic$task$desription ) Wywołanie to zwalnia pamięć operacyjną zajmowaną przez dynamiczny opis zadania, przy czym adres początku dynamicznego opisu zadania jest parametrem tego wywołania. Adres ten został utworzony w momencie utworzenia zadania. Zdanie, które usunęło dane zadanie może wykorzystać wolną pamięć do swoich celów lub przesłać ją do modułu zarządzania pamięciom za pomocą komunikatu do odpowiedniej skrzynki RQFSRX. Zadanie może również usunąć samo siebie podając jako parametr adres swojego opisu dynamicznego albo też podać nazwę zmiennej systemowej o nazwie rq$actr. Zmienna ta zawiera zawsze adres aktualnie wykonywanego zadania. Zawieszanie zadania: Zawieszanie zadania odbywa się za pomocą wywołania systemowego treści: CALL rq$susp ( dynamic$task$desription ); Jeśli kod programu, w którym dojdzie do wywołania w/w komendy to powoduje ono zawieszenie aktualnie wykonywanego zadania. Po odwieszeniu zadania (które samo siebie zawiesiło) zadanie będzie wykonywane od następnej instrukcji po CALL rq$susp lub po instrukcji, która korzystała ze zmiennej systemowej rq$actr. Uwaga: Zawieszenie zadania będącego w stanie gotowości nie powoduje żadnej reakcji. Jednakże fakt próby zawieszenia jest zapamiętywany przez jądro i zadanie to jest natychmiast zawieszane po przejściu do stanu wykonywania. Wznawianie zadania: Wznawianie zadania wykonywane jest przez wywołanie systemowe: CALL rq$resm ( dynamic$task$desription ) To polecenie musiało być deklarowane w treści zadania wznawiającego wywołanie powyższe powoduje przejście zadania, którego początek opisu dynamicznego znajduje się pod wskazanym adresem do stanu gotowości (G). Uwaga: Wznowienie zadania nie zawieszonego nie powoduje żadnych skutków.
System QNX W systemie QNX wszystkie zadania są pisane za pomocą języka C. Tworzenie zadania: Do tworzenia nowych zadań korzysta się z następujących trzech funkcji: a) fork ( ); b) exec ( ); c) spawn ( ); Fork ( ) i exec ( ) działają w obrębie jednego węzła, natomiast funkcja spawn ( ) może działać w dowolnym węźle. Składnia funkcji fork ( ): #include <sys/types.h> #include <unisted.h> pid_t fork (void); Powyższa funkcja tworzy dokładnie kopię zadania będącego nowym zadaniem. Kopia ta ma ten sam kod, ale różne obszary danych. Nowe zadanie potomne do zadania macierzystego wykonuje się współbieżnie w zadaniu macierzystym. Zdanie potomne dziedziczy wszystkie pliki zadania macierzystego, jednak dostęp do tych plików uzyskuje poprzez deskryptory. Również otrzymuje własne kopie wszystkich strumieni otwartych przez zadanie macierzyste. Oprócz tego zadanie to dziedziczy również identyfikator sesji, grupy zadań, priorytet, algorytm szeregowania, katalog roboczy, zmienne środowiskowe. Nie dziedziczy natomiast identyfikatora zadania macierzystego, liczników czasu(otrzymuje własne liczniki: tms_utime, tms_stime,tms_cutime,tms_cstime). Jeżeli operacja utworzenia nowego zadania zakończy się powodzeniem, to funkcja ta zwraca wartość zero do zadania potomnego oraz wartość identyfikatora zadania potomnego do zadania macierzystego. W przeciwnym przypadku funkcja fork ( )zwraca do zadania macierzystego wartość -1 przy czym rodzaj błędu, który wystąpił określa wartość zmiennej errno. Ta zmienna przyjmuje wartości: EAGAIN lub ENOMEM. Składnia funkcji exec( ): Składnia zależy od typu przyrostka, który występuje przy tej funkcji. Nazwa exec może być uzupełniona o jedną z trzech liter: le.pv/le/lp/lpv. #include <proces.h> int execl (pgm, arg0,,argn, NULL); int execle (pgm, arg0,,argn, NULL,envp); int execlp (pgm, arg0,,argn, NULL); int execlpe (pgm, arg0,,argn, NULL,envp); int execv (pgm, argv); int execve (pgm, argv, envp); int execvp (pgm, argv); int execvpe (pgm, argv,envp); const char *pgm; const char *arg0,,argn; char *const arg[]; char *const envp[];
Funkcja exec ( )zastępuje treść zadania-usuwa go przez nowe zadanie z treścią odczytaną z pliku o nazwie pgm. Jeśli operacja zakończy się powodzeniem to miejsce zadani macierzystego zajmuje zadanie potomne z takim samym identyfikatorem pgm. Końcowe znaki w nazwie określają postać argumentów, mianowicie: p znak p jeśli występuje to funkcja exec ( ) szuka pliku pgm używając ścieżek używając zmiennej środowiskowej path, jeżeli nie występuje to plik pgm jest poszukiwany w katalogu głównym lub roboczym. e jeśli występuje to funkcja musi zawierać dodatkowy argument tablice envp określającą nowe środowisko uruchamiania zadania. Tablica envp zawiera wskaźniki do tekstów definiujących zmienne środowiskowe w postaci np. HOME=/name/chris. Ostatni element tej tablicy musi być NULL. Jeżeli e nie występuje to zadanie potomne dziedziczy zmienne środowiskowe zadania macierzystego. Przykładowe wywołanie: l argumenty funkcji są podawane w postaci listy wskaźników arg0,..,argn, zakończonych wskaźnikiem NULL, jeśli r - to argumentem funkcji jest tablicowy wskaźnik argv. arg0 lub arg[0] powinien zawierać nazwę pliku zawierającego program Składnia funkcji spawn ( ): Składnia funkcji spawn ( ) podobnie jak składnia funkcji exec ( ) zależy od przyrostka w formie liter l,p,v,e #include <process.h> int spawnl (mode, pgm, arg0,, argn, NULL); int spawnle (mode, pgm, arg0,, argn, NULL,envp); int spawnlp (mode, pgm, arg0,, argn, NULL); int spawnlpe (mode, pgm, arg0,, argn, NULL,envp); int spawnv (mode, pgm, argv); int spawnvpe (mode, pgm, argv, envp); int mode; const char *pgm; const char *arg0,, argn; char *const argv[]; char *const envp[] Funkcja ta napisana w treści jakiegoś zadania powoduje odczytanie pliku o nazwie pgm i utworzenie zadania wykonującego program zapisany w pliku. Relacje w jakich pozostają między zadaniem macierzystym i zadaniem potomnym, a także dalsze ich zachowanie określa wartość argumentu mode mode P_WAIT- zadanie macierzyste jest zawieszane aż do momentu zakończenia zadania potomnego P_NOWAIT obydwa zadania wykonują się współbieżnie, zachowując priorytet P_NOWAITO obydwa zadania wykonują się współbieżnie, ale zadanie potomne tworzy nową grupę zadań. P_OVERLAY- wtedy wywołanie jest równoważne wywołaniu funkcji exec ( ). Numer węzła oraz sposób dziedziczenia parametrów środowiskowych określają wartości wpisane do globalnej struktury QNX_spawn_globs, o następującej składni: #include <sys/types.h> #include <ys/qnx_blob.h>
struci_qnx_spawn_globs { struct_proc_spawn *msg bnf; nid_t node; char priority; char scheduler; short int flags; char iov[]; short int reserved[6]; } qnx_spawn_options; *msg bnf wskaźnik na początek obszaru pamięci, do którego będzie wpisywana wiadomość do menedżera procesu. Wartość NULL powoduje automatycznie ulokowana pamięć. node numer węzła, którym będzie tworzone nowe zadanie. Difoltowo podawane jest 0, czyli utworzy się zadanie w tym samym węźle. priority są to liczby z zakresu 1-19 dla zadań normalnych lub 1-29 dla administratora systemu. Wpisanie -1 oznacza przyjęcie priorytetu zadania macierzystego. scheduler określa algorytm obsługi zadań mających ten sam priorytet. SCHED_FIFO w tym działa algorytm FIFO SCHED_RP obsługa karuzelowa, z przydzielonym odcinkiem czasu t SCHED_OTHER algorytm adaptacyjny Podanie wartości -1 powoduje dziedziczenie tego parametru od zadania macierzystego. flags znaczniki określające szereg parametrów środowiskowych iov[ ] w niej przekazywane są deskryptory(nazwy) pliku z zadania macierzystego do zadania potomnego. Wpisanie wartości -1 oznacza przekazanie deskryptora pliku z zadania macierzystego do zadania potomnego. Domyślnie tablica ta zawiera 10 elementów elementów wartości -1. Deskryptory, które nie mają być przekazywane do zadania potomnego powinny mieć wpisaną wartość CLONE_ON_EXEC. Tak zdefiniowana struktura qnx_spawn wykorzystywana jest również w funkcji exec ( ). W przypadku powodzenia operacji utworzenia nowego zadania funkcja spawn ( ) zwraca wartość, która zależy od parametru mode. Jeżeli mode=p_wait, to funkcja spawn ( ) zwraca kod zakończenia zadania potomnego. Jeżeli mode=p_nowait to funkcja spawn ( ) zwraca identyfikator zadania potomnego. aby uzyskać kod zakończenia zadania potomnego w treści zadania macierzystego musi być wywołana funkcjawait_pid. Jeżeli mode=p_nowaito, to funkcja spawn ( ) zwraca identyfikator zadania potomnego. ( Otrzymanie kodu zakończenia zadania potomnego jest niemożliwe ). Jeżeli mode=overlay to funkcja spawn ( ) niczego nie zwraca. Jeżeli przy realizacji funkcji spawn ( ) wystąpią błędy to funkcja ta zwraca wartość -1. Rodzaj błędu jest odczytywany poprzez odczytanie wartości zmiennej errno. W związku z tym, że przy tworzeniu kody zakończenia zadania potomnego potrzebne są dwie funkcje P_WAIT i wait_pid, które mają składnię: #include <ys/wait.h> pid_t wait (int *stat_loc); Funkcja ta powoduje zawieszenie zadania wywołującego tą funkcję, aż do zakończenia jednego z zadań potomnych lub otrzymaniu sygnałów. Gdyby zadanie potomne zakończyło się przed wywołaniem funkcji wait, to funkcja zwraca natychmiast bez zawieszania zadania ją wywołującego. Funkcja zwraca identyfikator zakończenia zadania potomnego. Jeżeli wskaźnik stat_loc jest różny od NULL, to 6 dodatkowych makro instrukcji określa kod zakończenia zadania potomnego.
Makroinstrukcje: WIFEXITED (*stat_loc) WEXITSTATUS (*stat_loc) WIFSIGNALED (*stat_loc) WTSIG (*stat_loc) WIFSTOPED (*stat_loc) WSTOPSIG (*stat_loc) Jeżeli funkcja wait zakończy się przy braku zadania potomnego lub przy momencie odebrania sygnału, to funkcja ta zwraca wartość -1. Interpretacja błędu określa zmienna errno, o dwóch wartościach(echild, EINTR). Składnia funkcji wait_pid: #include <sys/wait.h> pid_t wait_pid(pid_t, pid, int *stat_loc, int option); Powyższa funkcja jest rozszerzeniem funkcji wait. Jeżeli przy wywołaniu pid=-1 i opcja=0, to działanie tej funkcji jest identyczne jak funkcji wait. Jeżeli pid<-1, to zadanie oczekuje na zakończenie jednego z zadań potomnych, należących do grupy procesów zadania potomnego. Jeżeli pid=0 zadanie będzie oczekiwać na zakończenie jednego z zadań potomnych należących do tej samej grupy, co zadanie macierzyste. Jeśli pid>0 to zadanie oczekuje na zakończenie konkretnego zadania o identyfikatorze pid. Jeśli wartość zmiennej option=wnohang, to zadanie wywołujące tą funkcję nie jest zawieszane, natomiast funkcja zwraca wartość 0, co oznacza, że istnieją zadania potomne, z których żadne nie zostało zakończone. Jeśli funkcja zwraca wartość -1 oznacza to brak zadań potomnych, lub że odebrały sygnał- traktowane jest to jako błąd tej funkcji. Rodzaj błędu określa wartość zmiennej globalnej errno, która ma trzy wartości (ECHILD, EINVALD, EINTR). Uwaga: W systemie QNX nie przewidziano dodatkowych funkcji służących do zawieszania lub przywracania zadań do stanu gotowości poza wait. Natomiast usuwanie zadań umożliwiają trzy funkcje: ABORT(void),ATEXIT(nawa programu), ELIT(int status); Podstawową procedurą, która kończy zadanie i je usuwa jest funkcja elit, która jest albo jawnie sformułowana w tekście zadania, albo też jest wykonywana w momencie dojścia do { funkcji main. Składnia tej funkcji jest następująca: #include <stdlib.h> void exit (int status); Wywołanie funkcji powoduje następujące czynności: 1. Wykonanie funkcji, które zostały zarejestrowane przez funkcję atexit. 2. Zapisanie i zamknięcie plików otwartych przez zadanie usuwane, a także usunięcie wszystkich plików tymczasowych.tmp 3. Przekazanie zadaniu macierzystemu wartości zmiennej status. Jeżeli wartość równa jest 0, zakończenie jest poprawne, jeżeli inne wartości-oznaczają błąd. Składnia funkcji atexit: #include < stdlib.h> void atexit (void(*func)(void)status); Funkcja ta rejestruje wskaźnik *func na stosie do funkcji, która powinna być wykonana w chwili zakończenia zadania. Zdejmowanie wskaźnika ze stosu i kolejne wykonywanie odpowiadających jej funkcji następuje po wywołaniu funkcji elit lub po dojściu do { funkcji main. Składnia funkcji abort: #include <stdlib.h> void abort (void);
Jest przeznaczona do awaryjnego zakończenia zadania. Jej działanie polega na wygenerowaniu sygnału SIGABRT, którego akcją domyślną jest usunięcie zadania, natomiast zadanie macierzyste otrzymuje kod o wartości 12, czyli awaryjne zakończenie zadania potomnego. System VxWorks W przypadku systemu VxWORKS wszystkie zadania są pisane w C. Do tworzenia i uaktywniania zadań używa się trzech funkcji: a) taskspawn ( )- która tworzy i uaktywnia nowe zadanie b) taskinit ( )- która inicjalizuje nowe zadanie c) taskactivate ( )- która uaktywnia utworzone zadanie Wywołanie funkcji tworzenia zadania odbywa się w prosty sposób: id=taskspawn (name, priority, options, stacksize, main, arg1,, arg10); np. tid=taskspawn ( tmytask, 90, VX_FP_TASK, 20000, myfunc, 2378, 0-0); Wykonanie tej funkcji powoduje utworzenie nowego zadania łącznie z alokacją stosu i otoczenia; i zadanie to jest standardowo ustawiane w stanie gotowości. Treść zadania jest zawarta w pliku MyFunc, a nowo tworzone zadanie ma nazwę tmytask. Nazwa nowego zadania nie może być w konflikcie z nazwami innych zadań istniejących w systemie, a w przypadku wersji VxWorks z jądrem (shell) Tornadonazwa ta nie może być w konflikcie ze zmiennymi globalnymi. Dla zapobieżenia takim konfliktom korzysta się zwykle z prefikcji, gdzie dodaje się do nazwy z przodu t (target) lub, u jeżeli to jest dla zadań tworzonych na hoście. Można też nie nazywać zadania, przydzielając zmiennej name argument NULL. Wtedy system sam utworzy niekonfliktową nazwę i sam prefiksuje. Polecenie taskinit i taskactivate wykorzystuje się,gdy jest potrzebna kontrola nad momentem aktywacji zadania. Do kontroli nazw zadań i ich identyfikatorów służą dodatkowe funkcje: a) taskname( ) argument jest identyfikatorem, funkcja zwraca nazwę dla zadanego identyfikatora id. b) tasknametoid( ) funkcja zwraca nazwę zadania dla podanego id. c) taskidself( ) funkcja zwraca identyfikator nazwy d) taskidverity( ) sprawdza czy istnieje określone zadanie Przy tworzeniu nowych zadań podaje się dla zmiennej options jedną z następujących opcji: VX_FP_TASK nowe zadanie będzie korzystało z koprocesora o zmiennej przecinkowej VX_NO_STACK_FILL nie wypełnia się stosu w nowym zadaniu VX_PRIVATE_ENV nowe zdanie nie dziedziczy środowiska i musi być podane w argumentach VX_UNBREAKABLE nowe zadanie jest bezwzględnie wykonywane bez możliwości jego przerwania Do sprawdzenia aktualnej opcji nowego zadania służą dwie funkcje: a) taskoptionget( ) która zwraca z jaką opcją było utworzone zadanie b) taskoptionset( ) która zmienia opcję podanego zadania
Do uzyskania zbiorczych informacji o istniejących zadaniach oraz o stanie tych zadań służy 8 funkcji: a) tasklistget( ) drukuje id wszystkich aktywnych zadań b) taskinfoget( ) uzyskuje informacje o jednym zadaniu c) taskpriorityget( ) uzyskuje się informacje o priorytecie zadania d) taskregsget( ) wyszczególnienie rejestru w zadaniach e) taskregsset( ) ustawienie rejestru w zadaniu f) taskissusspended( ) sprawdzenie czy są zadania w stanie zawieszenia g) taskisready( ) sprawdzanie czy są zadania w stanie gotowości(lista zadań w gotowości) h) taskteb( ) pobranie wskaźnika od zadania Do usuwania jak i do zapobiegania przypadkowym usunięciom zadań służą następujące 4 funkcje: a) exit( ) funkcja, która kończy i przerywa zadanie, przy czym zwalniana jest pamięć na stos i blok kontrolny. Dojście do ostatniej klamry programu main i jest tożsamościowo równe funkcji exit( ). b) taskdelate( ) przerywa określone zadanie i zwalnia również tylko pamięć przeznaczoną na stos i blok kontrolny Teb. c) tasksafe( ) zapobiega usunięciu zadania(samego siebie) d) taskunsafe( ) usuwa działanie tasksafe( ). z tymi dwoma ostatnimi funkcjami związany jest licznik, który zlicza liczbę wywołań safe i Unsafe. Zwykle poleceniem tasksafe( ) i taskunsafe( ) zabezpiecza się tzw. krytyczne rejony, które nie mogą być przerwane podczas wykonywania, lub które nie mogą być zaczęte jeżeli nie możliwe jest ich wykonanie. Wtedy wewnątrz bloku pomiędzy funkcją tasksafe( ) i taskunsafe( ) stawia się semafory zabezpieczające tenrejon. Przykład: tasksafe( ); semtake( ); rejon krytyczny semgive( ); Polecenie taskdelate( ) jest skuteczne jeśli licznik związany z poleceniem taskunsafe( ) jest ustawiony na 0. Do przywrócenia zadania, które z różnych powodów zostało usunięte, przewidziano funkcję taskrestart( ). Ta funkcja wznawia zadanie z oryginalnymi danymi zadania. Ponadto w systemie tym przewidziano dwie funkcje, które są wykorzystywane tylko przy testowaniu programu. Są to funkcje: tasksuspend( ) i taskresume( ). W czasie normalnej pracy przewidziane są też dwie funkcje: a) taskdelay( ) który usypia zadanie na pewien określony czas, parametrem tej funkcji jest czas podany w liczbie taktów np.: taskdelay(sysclkrateget( )/2) b) Nanosleep( ) - jest zgodna ze standardem POSIL, czas którego opóźnienie jest podawane w nanosekundach. Funkcja taskdelay( ) umożliwia również przy wywołaniu z parametrem no_wait przesunięcia zadania z początku kolejki na koniec kolejki., w grupie zadań o tym samym priorytecie. Dodatkowo w tym systemie przewidziano 6 dodatkowych funkcji, które umożliwiają wstawianie bądź też usuwanie dodatkowych czynności, które mogą być
wykonywane przy operacjach tworzenia przed usunięciem zadania i przełączaniu kontekstu. Do wprowadzenia tych dodatkowych procedur służą następujące funkcje: a) taskcreatehookadd( ) funkcja ta dodaje procedurę, która będzie wywołana przy każdym tworzeniu zadania. b) taskcreatehookdelate( ) usuwa dodaną wcześniej procedurę c) taskswitchhookadd( ) dodaje procedurę, która będzie wywoływana za każdym razem, gdy będzie przełączany kontekst zadania. d) taskswitchhookdelate( ) usuwa w/w procedurę e) taskdelatehookadd( ) dodaje procedurę, która będzie wykonywana tuż przed usunięciem. f) taskdelatehookdelate( ) usuwa w/w procedurę. *SYNCHRONIZACJA I ZARZĄDZANIE KOMUNIKACJĄ MIĘDZY ZADANIAMI* W systemie wielozadaniowym wiele zadań jest wykonywanych równolegle, przy czym kolejność wykonywania operacji realizowanych w tych zadaniach od zewnętrznych zdarzeń i kolejność ta jest niemożliwa do przewidzenia. Konieczne jest, zatem wprowadzenie mechanizmów synchronizacji i ochrona całego systemu aplikacyjnego przed zakleszczeniami na tzw. rejonach krytycznych. Rejonem krytycznym nazywamy te fragmenty instrukcji, które mogą być wspólną częścią różnych zadań lub też te fragmenty, które dotyczą wspólnych zasobów. Podstawowymi mechanizmami służącymi do tego celu są: 1. Asynchroniczne przesyłanie komunikatów 2. Synchroniczne przysyłanie komunikatów 2.a Spotkania(randezvous) 3. Semafory: a) semafory zliczające b) semafory binarne W starszych systemach korzystało się wyłącznie z asynchronicznego przesyłania komunikatów. W nowych wykorzystuje się dwie możliwości przesyłania komunikatów, w najnowszych korzysta się z semaforów. Ogólnie istnieją dwa sposoby fizycznego przesyłania treści komunikatu: a) Polega na przesyłaniu adresu początku komunikatu, przy czym sama treść komunikatu znajduje się w obszarze wspólnej pamięci. b) Treść komunikatu jest przepisywana z jednego obszaru do drugiego. Ad.1 Pierwszy sposób może być wykorzystywany w systemach jednoprocesorowych. W systemach wieloprocesorowych (np.qnx) konieczne jest przesyłanie fizyczne komunikatów. Ad.2 Mechanizm ten działa w ten sposób, że komunikat M przesyłany jest z obszaru adresowego jednego zadania do obszaru adresowego drugiego zadania. Wygląda on następująco: ZadT1 ZadT2 ------- ------- ------- ------- ------- ------- Send(T2,M) Receive(T1,M) ------ ------- ------ -------
Obydwie instrukcje send i receive muszą być wykonane w tym samym momencie i aby to było możliwe zadanie, które jest pierwsze wykona jedną z instrukcji send lub receive zostaje zawieszone, aż do momentu wykonania odpowiednich instrukcji przez zadanie współpracujące. Odmianą synchronicznego przesyłania są spotkania. Jest to mechanizm, który jest głównie wykorzystywany do zdalnego wywoływania przez jedno zadanie usług realizowanych przez inne zadanie. ZadT1 ZadT2 } ------- ------- } ------- ------- } ------- ------- } Send(T2,M1) M1 Receive(T1,M1) } w QNX ------ ^ ------- } ------ M2 ------- } ----- Replay(T2,M2) } ------ ------- } ------ ------- } Spotkania są synchronizowane obustronnie, czyli zadanie, które jako pierwsze osiągnie punkt spotkania zostaje zawieszone w oczekiwaniu na zadanie współpracujące. Po rozpoczęciu spotkania zadanie T1 pozostaje zawieszone (stan Z s ), natomiast zadanie T2 wykonuje odpowiednie operacje. Po dotarciu do miejsca do instrukcji replay zadanie T1 przechodzi do stanu zawieszenia Z R., który trwa aż do zakończenia T2, po czym dwa wykonywane są równolegle. Cechą charakterystyczną tego rodzaju przesyłu jest to, że nie ma potrzeby rezerwowania jakiegoś buforu na komunikat. Ad.1 ZadT1 ZadT2 ------- ------- ------- ------- ------- ------- Send(P,M) M [Bufor]Receive(P,M) ------ ------- ------ ------- Zadanie nadające umieszcza wiadomość w buforze wykonuje kolejne instrukcje bez czekania na odpowiedź. Zadanie odbierające komunikat po dotarciu do instrukcji receive odczytuje komunikat M, lub zostaje zawieszone, jeśli bufor jest pusty. Istnieją dwie wersje tego mechanizmu: 1. Buforem może być ogólna skrzynka na komunikaty, przy której zlokalizowana jest kolejka komunikatów oraz kolejka zadań, które oczekująca komunikaty. Najczęściej stosowanym mechanizmem obsługi kolejek są algorytmy FIFO i/lub lista priorytetowa. System irmx W tym systemie a zwłaszcza irmx-88 jedynym sposobem synchronizacji i komunikacji między zadaniami były komunikaty przesyłane asynchronicznie do skrzynek zwanych exchange. Każde zadanie mogło otrzymać 1 skrzynkę prywatną(można je było usuwać dynamicznie odpowiednimi poleceniami). Inne skrzynki typu public były dostępne
dla wszystkich zadań i przy każdej z takich skrzynek tworzyły się kolejki komunikatów(fifo) oraz kolejki zadań(fifo). W późniejszych wersjach dodano mechanizm semaforów. Ponieważ był tylko taki mechanizm, w związku z powyższym wymagane było korzystanie z funkcji zawieszenia i wznawiania zadań. Przesłanie komunikatu do skrzynki odbywało się za pomocą odwołania systemowego, które miało następującą składnię: CALL rq$sndm (exchange$location$message$location); Struktura komunikatu wyglądała następująco: DECLARE message link length TYPE Homexchange response$exchange message$content STRUCTURE( POINTER WORD BYTE POINTER POINTER BYTE); Pobieranie komunikatu ze skrzynki można wykonać za pomocą dwóch wywołań systemowych tj.: funkcji wait o składni: message$location = rq$wait(exchange$location, time$limit) Zadanie które wywołało tą funkcję przechodziło do stanu oczekiwania i pozostawało w nim, aż do otrzymania komunikatu. Zmiana time$limit mogła zawierać 0 lub max. czas oczekiwania na komunikat(liczby w tickach). Zero oznaczało, że zadanie zostało zawieszone aż do skutku. Jeżeli inne niż 0, to zadanie czekało na komunikat i jeśli czas oczekiwania był większy od podanej wartości, jądro wpisywało na polu TYPE wartość 3, co oznaczało brak komunikatu. Drugą funkcją jest funkcja akcept o składni: message$location = rq$acpt(exchange$location); Zadanie które miało w treści to wywołanie nie zmieniało swojego stanu. Korzystając z tych mechanizmów realizowano: a) Przekazywanie pewnych danych w typowy sposób tzn. komunikat, który był na początku kolejki był przekazywany pierwszemu zadaniu czekającemu w kolejce za nim. b) Synchronizacja zadań - wykonywało się za pomocą przesyłania pustych komunikatów do różnych skrzynek uaktywniających kolejne zadania, które oczekiwały przy tych skrzynkach i zwykle były to skrzynki prywatne. c) Wzajemne wykluczenie rejonów krytycznych było realizowane za pomocą jednej wspólnej skrzynki i jednego pustego komunikatu przesyłanego między zadaniami wykluczającymi się. Zadanie, które odczytywało komunikat było aktywne, pozostałe były zawieszane. Aby to było możliwe do zrealizowania, każdy region krytyczny musiał być w oddzielnych zadaniach. W nowym systemie wprowadzono semafory i dopuszczono kolejkowanie wg. priorytetów, gdyż system się zakleszczał.
System QNX W systemie QNX wykorzystuje się obydwa mechanizmy komunikacji tj. komunikację synchroniczną i asynchroniczną. W mechanizmach komunikacji synchronicznej stosuje się miedzy innymi metodę spotkań oraz bezpośredniego przekazywania komunikatu między zadaniami. Mechanizm komunikacji asynchronicznej polega na specyficznym wykorzystaniu tzw. depozytów (proxy), które to są pobudzane przez funkcje (trigger). Komunikaty w odróżnieniu od systemu irmx są przepisywane z jednego obszaru danych jednego zadania do obszaru danych drugiego zadania. Przesyłanie komunikatów odbywa się w tle za pomocą funkcji Readmsg( ) i Writemsg( ). Poza tym w QNX jest mechanizm przekazywania danych w postaci strumieni i potoków (pipe). Komunikacja asynchroniczna odbywa się za pomocą pary wiadomości (wiadomość-odpowiedź) za pomocą następujących 4 funkcji: a) send (send message) - która ma następującą składnie #include <sys/kernel.h> int Send (pid_t pid, void *smsg, void * rsmg, unsigned sn, unsigned rn); Funkcja ta powoduje wysłanie komunikatu oznaczonego wskaźnikiem *smsg do zadania o identyfikatorze pid i zawiesza zadanie wykonujące ten komunikat. Długość komunikatu określa zmienna rn w bajtach. Zawieszone zadanie czeka na odpowiedź, której treść jest wpisywana do obszaru o początku *rsmg. Długość tej odpowiedzi musi się zmieścić w rn bajtach. Jeżeli odpowiedź jest dłuższa to nadmiarowe bajty są tracone. Zadanie, które odebrało odpowiedź przechodzi do stanu gotowości, a także na skutek wcześniejszego zakończenia zadania wysyłającego komunikat. Funkcja ta zwraca wartość 0 po nadaniu i po odebraniu odpowiedzi w przeciwnym wypadku zwraca wartość -1 co sygnalizuje błąd i rodzaj błędu (jego interpretacja) po analizie zmiennej errno. Składnia funkcji Receive( ): #include <sys/kernel.h> pid_t Recieve(pid_t pid, void *msg, unsigned nb); Funkcja odbiera wiadomość od procesu o identyfikatorze pid. Treść wiadomości jest wpisywana do obszaru o długości nb bajtów wskazywanego przez wskaźnik *msg. Jeżeli oczekiwana wiadomość została nadana wcześniej to wykonanie funkcji recieve( ) kończy się natychmiast. Jeżeli komunikat nie został jeszcze nadany to funkcja zawiesza zadanie odbierające. Otrzymanie wiadomości powoduje przejście do stanu gotowości tego zadania, które wywołało funkcje recieve( ). Jeżeli pid=0 to funkcja odbiera komunikaty od dowolnych zadań lub depozytów natomiast, jeżeli parametr pid jest rożny od 0 to funkcja powoduje odebranie komunikatu tylko od zadania o identyfikatorze pid W przypadku gdy pid=0 komunikaty są odbierane w kolejności ich nadawania czyli FIFO. Można to zmienić za pomocą funkcji qnx_pflags, i wtedy będą w kolejności zgodnej z priorytetami zadań zgodnych nadających komunikaty. Po poprawnym odebraniu komunikatu funkcja zwraca identyfikator zadania lub depozytu, od którego komunikat został nadany. W przypadku wystąpienia błędu wartość funkcji zwraca -1 interpretacja w errno. Zadanie oczekujące na komunikat może przejść do stanu gotowości również w wyniku odebrania sygnału lub w przypadku wcześniejszego zakończenia zadania, od którego oczekiwano na wiadomość. Oba przypadki występują w przypadku zwrotu wartości -1.
Składnia funkcji Replay( ): # include <sys/kernel.h> int Replay(pid_t pid, void *msg, unsigned nb) Funkcja wysyła odpowiedź o długości nb do miejsca wskazanego wskaźnikiem *msg. Musi to być zadanie, które wcześniej przysłało do tego zadania komunikat. Po poprawnym nadaniu odpowiedzi funkcja zwraca wartość 0 a w razie wystąpienia błędu -1. Rodzaj błędu określa errno. Funkcją czwartą jest dodatkowa funkcja, która służy do odbioru komunikatu tak jak funkcja receive, zwana jest Creceive( ) i ma składnię: Składnia funkcji Creceive( ): # include <sys/kernel.h> pid_t crecieve(pid_t pid,viod *msg,unsigned nb); Funkcja ta odbiera komunikat od zadania pid tak samo jak funkcja receive, z jedna różnicą, że jeśli komunikat nie został nadany, wtedy działanie funkcji kończy się natychmiast ze zwróceniem wartości błędu -1. Natomiast po poprawnym odebraniu komunikatu podobnie jak funkcja recieve funkcja zwraca identyfikator zadania lub depozytu, od którego została nadana. Do przepisywania komunikatów w sposób przezroczysty wywoływane są dwie funkcje: readmessage i writemessage o składniach: # include <sys/kernel.h> unsigned readmsg(pid_t pid, unsigned offset, void *msg, unsigned nb); Funkcja odczytuje (kawałkami) treść komunikatu od zadania o identyfikatorze pid. Kolejne dane są odczytywane od bajtu o numerze offset i są wpisywane do obszaru wskazywanego przez *msg. Wykonanie funkcji może być przerwane nadejściem sygnału lub wcześniejszym zakończeniem zadania, które nadało komunikat. Oba wypadki traktowane są jako błąd i funkcja zwraca wartość -1. Błąd określa zmienna errno. Składnia funkcji writemsg( ): # include <sys/kernel.h> unsigned writemsg(pid_t pid, unsigned offset, void *msg, unsigned nb); Mechanizm zapisywania jest podobny do mechanizmu czytania i wykonanie może być przerwane również nadejściem sygnału lub wcześniejszym zakończeniem zadania, które odbiera wiadomość. Podobnie jak poprzednio oba przypadki są traktowane jako błąd i oznaczane -1. Do komunikacji asynchronicznej przewidziano mechanizm depozytów. Depozyty się zakłada i usuwa, a pobudza za pomocą funkcji tigger( ) Wykonuje się to przy użyciu funkcji depozytów następujących składniach:
a) Zakładanie depozytów: # include <sys/proxy.h> pid_t qnx_proxy_attach(pid_t pid, char *data, int nb, int priority) Funkcja powyższa tworzy depozyt przechowujący komunikat od miejsca *data o długości nb. Depozyt otrzymuje priorytet o wartości określonej zmienną priority i jest własnością zadania o identyfikatorze pid. Jeżeli pid =0 to właścicielem depozytu jest zadanie, które tą funkcje wywołało i jeżeli zmienna priority = -1 to depozyt ten otrzymuje priorytet taki, jaki miało zadanie, które tą funkcje wywołało. Jest to najczęściej stosowane wywołanie tej funkcji. Szczególnym rodzajem zdeponowanej wiadomości jest komunikat pusty, dla którego nb=0 i *data=0. Funkcja powyższa zwraca identyfikator depozytu, który stanowy argument dla funkcji trigger( ). W razie błędu zwraca -1 interpretacja w errno. b) Usuniecie jest możliwe za pomocą funkcji o składni: # include <sys/proxy.h> int qnx_proxy_detach(pid_t proxy); Funkcja usuwa depozyt o identyfikatorze będącym wartością zmiennej proxy. Operacje ta może wykonać tylko zadanie właściciel lub zadanie, które utworzyło depozyt. Funkcja zwraca, 0 jeżeli depozyt usunięto, a jeśli nie to -1. Do utworzenia i usunięcia dodatkowych reprezentantów depozytów w oddalonym węźle korzysta się z dwóch funkcji o składni: a) # include <sys/proxy.h> pid_t qnx_proxy_rem_attach(nid_t, pid_t proxy); b) # include <sys/proxy.h> int qnx_proxy_rem_dettach(nid_t, pid_t vproxy); Funkcja tworzy w węźle nid reprezentanta w oddalonym węźle jeżeli parametr nid=0 lub jest numerem węzła, w którym wykonuje się zadanie właściciela depozytu, to reprezentant nie jest tworzony, a funkcja zwraca identyfikator depozytu. Jest on wykorzystywany w funkcji trigger( ). W razie błędu zwraca wartość -1. Druga funkcja usuwa reprezentanta vproxy. Operację te może wykonać tylko zadanie właściciela. Jeżeli nid=0 lub jest równe numerowi węzła, w którym jest wykonywane zadanie właściciela, to funkcja ta otrzyma depozyt o identyfikatorze zmiennej vproxy, ale go nie usunie. Wynikiem w takiej sytuacji może być tylko błąd. Depozyty SA pobudzane o identyfikatorze równym wartości zmienne proxy. Powoduje wysłanie zdeponowanego tam komunikatu do zadania właściciela. Wszystkie wysłane i nieodebrane komunikaty(może ich być 65535) są gromadzone przez system i odczytywane podczas kolejnych wywołań funkcji receive( ) w zadaniu, które jest właścicielem depozytu. Po wysłaniu wiadomości funkcja ta zwraca identyfikator zadania właściciela depozytu.
System VxWorks W systemie VxWorks synchronizację jak i komunikację między zadaniami zabezpieczają następujące mechanizmy: a) Asynchroniczny przekaz komunikatów pomiędzy zadaniami za pomocą buforów Queues (kolejki). Podobny w swoim mechanizmie do irmx. b) Dane mogą tworzyć wspólny obszar w pamięci i aby wykluczyć możliwość przypadkowego niszczenia danych jednemu zadaniu przez inne stosuje się semafory blokujące dostęp do pamięci. c) Semafory są trzy rodzaje semaforów: - binarny - zliczający - wykluczający d) Podobnie jak w QNX wykorzystuje się mechanizm wysyłania sygnałów i zgłaszania przerwań od urządzeń zewnętrznych. Ad.a Przekaz komunikatów miedzy zadaniami może być realizowany według następującego schematu SCHEMAT++++++++++++++++++++++++ Dowolne zadanie może wysyłać swój komunikat do odpowiednio zdefiniowanej kolejki i również dowolne zadanie może odczytywać komunikat z odpowiedniej kolejki. Stosowany jest również mechanizm wykorzystywania jednej skrzynki przez wiele zadań jak również wysyłania wielu komunikatów do jednej skrzynki. W systemie VxWorks są dwie biblioteki związane z tworzeniem i wysyłaniem, jak również funkcje związane z tworzeniem i odczytywaniem komunikatów. Pierwsza msgqlib i druga mqpxlib zgodna ze standardem POSIX. Można korzystać z obydwu mechanizmów po uprzednim zgłoszeniu, że będziemy z nich korzystać. Przy stosowaniu biblioteki msglib ma się do dyspozycji cztery funkcje o nazwach: msgqcreate(); - która tworzy i inicjalizuje bufor na skrzynkę msgqdelete(); - która usuwa bufor msgqsend(); - przesyła komunikat do wskazanej skrzynki msgqrecieve(); - odbiera komunikat ze wskazanego buforu Przy tworzeniu bufora podaje się jako parametry: maksymalna liczbę komunikatów, które mogą być tam umieszczone oraz w bitach maksymalną długość komunikatów, której żaden nie może przekroczyć. Komunikat do kolejki może przesłać dowolne zadanie lub jądro zgłaszając przerwanie. Przy każdym buforze tworzone są dwie kolejki: kolejka komunikatów i zadań czekających na komunikat. Sposób załatwiania kolejki jest z wykorzystaniem algorytmu FIFO lub według priorytetu zadań. Jeśli przy buforze nie czeka żadne zadanie to komunikat jest dodawany do kolejki komunikatów. Przewidziane są dwa sposoby dodawania: a) Jeżeli komunikat jest normalny to dodawany jest on do końca kolejki. Wysyłany jest z parametrem(msg_pri_normal). b) Jeżeli będzie szedł z parametrem pilny(msg_pri_urgent) będzie dodawany do początku kolejki. Jeżeli przy buforze będą stały zadania to pierwsze zadanie będzie pobierało ten komunikat.
Funkcje msgqsend i msgrecieve mogą być wywoływane z dwoma opcjami NO_WAIT(0) i WAIT_FOREVER(-1). Parametry te powodują w przypadku NO_WAIT, że jeśli nie ma miejsca w kolejce na umiejscowienie to informacja o tym fakcie jest natychmiast zwracana do zadania, które to wysłało. W drugim przypadku wysłany komunikat czeka aż do skutku, czyli aż zwolni się miejsce na jego zapisanie. Normalnie podaje się czas oczekiwania na umiejscowienie w kolejce lub czas na czytanie. Przykład tworzenia kolejki na wybrane komunikaty oraz wysłanie zadania od 1 do 2. #include vxworks.h #include msgqlib.h #define MAX_MSGS(10) #define MAX_MSG_LEN(100) MSG_Q_ID mymsgid; t2(void) { char msgbuf[max_msg_len]; if(msgqrecieve(mymsqid,msgbuf,max_msg_len,wait_forever)==error) return(error); printf( tekst o bledzie, ); #define MESSAGE Pzdrowienia od T1 t1(void) { if((mymsgqid=msgqcreate(max_msg,max_msg_len,msg_q_priority)) ==NULL return(error); /*przesyl normalnego komunikatu*/ if(msgqsend(mymsgid,message,sizeof(message); WAIT_FOREVER,MSG_PRI_NORMAL)==ERROR return(error); } W przypadku korzystania z mechanizmów dostarczonych przez mqpxlib mamy do dyspozycji dziewięć funkcji: mqpxlibinit(); - która uruchamia bibliotekę funkcji zgodnych ze standardem POSIX mq_open(); - która tworzy kolejkę komunikatów lub otwiera istniejącą mq_close(); - zamyka dostęp do kolejki komunikatów, ale jej nie usuwa mq_unlink(); - usuwa kolejkę mq_send(); - wysyła komunikat do wskazanej kolejki mq_read(); - pobiera komunikat ze wskazanej kolejki mq_notify(); - sygnalizuje zadaniu, że komunikat oczekuje na umieszczenie w kolejce mq_set after(); - ustawienie atrybutów dla bufora na komunikaty mq_getafter();- odczytanie aktualnych atrybutów bufora
Tworzenie bufora zachodzi wtedy, jeżeli mq_open jest wywoływana z atrybutem O_Create kolejne wywołania funkcji mq_open dają możliwość otwierania istniejącego już bufora w zależności od tego czy bufor był tworzony z opcją O_RDONLY lub O_WRONLY lub O_RDWR. Przy korzystaniu z funkcji mq_send jeśli kolejka na komunikat jest pełna zadanie wysyłające komunikat jest zawieszone, można temu zapobiec wywołując funkcję send z parametrem O_NONBLOCK. Jednym z podstawowych argumentów funkcji mq_send jest priorytet komunikatu, który może przyjmować wartości od 0 - najniższy do 31 - najwyższy. Kolejkowanie odbywa się według priorytetu. W przypadku, jeżeli komunikaty będą miały ten sam priorytet to komunikaty o tym samym priorytecie są ustawiane według algorytmu FIFO. Przy wywołaniu funkcji mq_recieve(); jeśli kolejka na komunikat jest pusta zadanie jest blokowane. Aby temu zapobiec bufor na komunikaty w momencie jego tworzenia powinien być tworzony z opcją O_NONBLOCK. Funkcja mq_close zamyka dostęp do bufora, ale go nie likwiduje, co oznacza, że zadanie które go zamknęło nie będzie korzystać z bufora. Funkcja mq_unlink uniemożliwia dostęp do bufora przez zadania, które jeszcze z niego nie korzystały. Natomiast zadania, które miały otwarty dostęp do kolejki mogą z niej ciągle korzystać. Kiedy ostatnie zadanie przestanie z niej korzystać to bufor jest czyszczony (kolejka jest zniszczona). Funkcja mq_notify umożliwia odczytanie komunikatu z bufora kolejek i jej zaleta jest to, że zadanie odczytujące komunikat nie jest zawieszane, jeśli kolejka jest pusta. Funkcja ta umożliwia również stworzenie mechanizmu, w którym dane zadanie jest zawiadamiane tylko o nowych komunikatach, które przychodzą do kolejki. Atrybutami kolejek są: opcja O_NONBLOCK, maksymalna liczba komunikatów, które można umieścić w kolejce, maksymalna długość komunikatu, liczba komunikatów, które aktualnie znajdują się w kolejce. Funkcja mq_setattr(); może zmieniać O_NONBLOCK natomiast funkcja mq_getattr(); może odczytać wszystkie cztery atrybuty. Różnice pomiędzy działaniem funkcji zgodnych ze standardem POSIX ilustruje tabela: Cecha Wind POSIX Priorytety komunikatu 0,1 0-31 Algorytm FIFO / Priorytet Priorytet kolejkowania(scheduling) Możliwość ustalania czasu oczekiwania na komunikat lub zwolnienie miejsca w Opcjonalne Niedostępne kolejce Task Notification (odczytanie komunikatu bez zawieszania zadania) Niedostępne Dostępne Omawiane powyżej mechanizmy komunikacji pomiędzy zadaniami w sposób asynchroniczny przez odpowiednie bufory dają dużą elastyczność przy tworzeniu warstwy aplikacyjnej, jednak nie gwarantują ochrony zasobów ani też zakleszczania się zadań. Gdyby te mechanizmy były jedynym mechanizmami to system oparty o te funkcje musiałby być przebadany pod kątem działania. Dlatego we współczesnych systemach operacyjnych wbudowane są dodatkowe mechanizmy, które w przypadku systemu VxWorks są dwie dodatkowe funkcje oraz semafory, przy czym za główny mechanizm uważa się semafory. Dodatkowymi funkcjami w VxWorksie są funkcje zabezpieczające przed przypadkowym przerwaniem krytycznego regionu. Pierwsza funkcja, której struktura wygląda następująco: