2. - cygnus
Transkrypt
2. - cygnus
Temat: System zarządzania zadaniami i ich komunikacja (Scheduler, MessageBox). 1.Uruchamianie zadań / przełączanie zadań. Praca wielozadaniowa (multitasking), daje programiście aplikacyjnemu możliwość tworzenia oprogramowania (zadań) działającego równolegle z innym kodem (procedury systemu operacyjnego). Teoria i sposoby realizacji wykraczają poza ramy tego dokumentu poruszone, zatem zostaną tylko najprostsze i najważniejsze zagadnienia niezbędne do zbudowania własnego systemu. Generalnie upraszczając, systemy wielozadaniowe można podzielić na: Kooperujące -Programista musi zrobić wszystko pisząc aplikacje, aby jego zadanie było kończone najszybciej jak to jest tylko możliwe. Jeżeli jest to niemożliwe musi być przekazane sterowanie do zadania przełączającego (pozwala innym zadaniom coś wykonać), a po następnym wywołaniu podjąć przerwane zadania (np. długotrwałe, skomplikowane obliczenia, oczekiwanie na interakcje z użytkownikiem lub wolnymi urządzeniami peryferyjnymi). Z reguły takie działanie jest akceptowalne w małych i prostych systemach, wymaga od programisty dużo uwagi nad projektowaniem aplikacji. Wywłaszczające -Programista pisze aplikacje nie wiedząc nic o tym, kiedy i jak przekazywane jest sterowanie między zadaniami. Takie podejście powoduje, że nie wiadomo, kiedy zadanie zostanie przerwane. Istnieje, zatem konieczność dbania o spójność danych wykorzystywanych przez wiele procesów, wprowadzenie operacji atomowych (nie przerywalnych, wykluczających wyścig między procesami), natomiast system przełączania jest bardziej skomplikowany, gdyż konieczne jest zachowywanie kontekstu przerwanego zadania, a w odpowiednim momencie jego odtworzenie. Proces przełączania zadań (Scheduler), dba o odpowiednie wywoływanie procesów zgodnie z powyższymi modelami. Kolejność wywoływania zadań może być realizowana według algorytmu "wszystkim po równo" (algorytm prosty w implementacji, lecz mało efektywny), lub z uwzględnianiem priorytetów każdego zadania. 2.Implementacja przełączania zadań Załóżmy że elementarnym zadaniem może być funkcja o prototypie: void funcX(void); Zakładając że jest zbiór takich zadań i jest on jest ponumerowany od 0 do N, mamy następujące funkcje ich obsługi o podobnych prototypach: func1, func2, func3, ..., funcN. W przypadku tworzenia kodu dla kompilatora SDCC, dla takiego zestawu można utworzyć w obszarze pamięci kodu (Code) tablicę: code void (*code f[])(void)={ func1, func2, func3, ... }; Wywołanie zatem zadania sprowadzałoby się do wywołania odpowiedniej funkcji: (*f[ task ])(); Gdzie zmienna 'task' jest odpowiednikiem numeru funkcji aktualnie wywoływanej. Wartość task nie może wskazywać (indeksować) obszaru poza danymi zapisanymi w tablicy 'f'. Zatem bardzo rygorystycznie trzeba sprawdzać ten warunek tak aby nie doprowadzić do zawieszenia systemu, a w ogólności powodować błędną pracę. Przy pomocy prostego przełącznika (schedulera) można po zakończeniu jednego zadania wywołać inne. Ważne jest, aby każde zadanie wykonywało się jak najkrócej. W szczególności nie wykonywały pętli nieskończonych lub bardzo długich operacji bez przekazywania obsługi przełącznikowi. Poniższy przykład ilustruje jak unikać tworzenia złego opisu zadania na przykładzie bardzo długich pętli: Oryginalny kod z pętlą bardzo długą Zmieniona wersja kodu bez pętli bardzo długiej ... ... void funcN(void){ long l; //zmienna lokalna funkcji!!! for(l=0; l<2147483647; l++){ ... //zadanie do //wykonania wewnątrz „pętli” } } ... ... long l=0; //zmienna globalna!!! void funcN(void){ if(l<2147483647){ ... //zadanie do wykonania //wewnątrz „pętli” l++; } } ... Ja widać zadanie wykonywane w funkcji: funcN, będzie wykonywane w obu realizacjach, tyle samo razy, różnica polegać będzie na sposobie wykonywania samej pętli. W przypadku „złym” (lewa strona tabeli) funkcja raz wywołana będzie realizować wszystkie iteracje pętli. W drugim „dobrym” (prawa strona tabeli) funkcja wykona tylko jedną iterację pętli, resztę iteracji wykonane zostaną przy następnych wywołaniach zadania przez przełącznik. W sumie obie realizacje wykonają tyle samo iteracji, ale rozwiązanie dobre pozwoli innym zadaniom wykonywać się w „tle”. Na uwagę zasługuje zmienna 'l', jest ona używana dla sterowania przebiegiem działania pętli. W większości przypadków używa się odpowiednich struktur, które przechowują stan przerwanego zadania (stan nie musi być przechowywany w jednej zmiennej), dzięki czemu następne wejście w zadanie może odtworzyć stan z poprzedniej iteracji. 3.Modularyzacja Tworzenie implementacji zadań w przypadku rozbudowanych projektów może być powierzone niezależnym osobom, zespołom czy wręcz firmom. Wtedy to ze względu na przejrzystość kodu i możliwość łatwego jego ogarnięcia, każdą implementację osobnego zadania dobrze jest umieszczać w innym pliku (module). Sam podział na pliki byłby czysto kosmetycznym zabiegiem gdyby nie wykorzystanie mechanizmu języku "C" jakim jest separacja obiektów. Obiekty w ramach jednego modułu nie są widziane w kodzie innych modułów. Tylko specjalne zabiegi (prototypy) mogą łamać tę zasadę. Jednym z założeń systemów wielozadaniowych jest separacja zadań. Komunikacja w takich systemach jest możliwa, ale realizowana jest w ściśle kontrolowany sposób za pomocą usług systemu operacyjnego (patrz, pkt. 5). W szczególności nie dozwolone są chaotyczne odwoływania do obiektów innych zadań. W uroszczeniu - zwłaszcza gdy sprzęt nie wspiera ochrony pamięci - separacje można osiągnąć przez umieszczenie zadań (tutaj funkcji) w różnych plikach: Plik: taska.c int ita,jta; void TaskA(void){ ... } Plik: taskb.c int itb,jtb; void TaskB(void){ ... } Plik: taskc.c int itc,jtc; void TaskC(void){ ... } Plik: taska.h void TaskA(void); Plik: taskb.h void TaskB(void); Plik: taskc.h void TaskC(void); W tak podanym zestawie plików mamy trzy zadania: TaskA, TaskB, TaskC. Pliki nagłówkowe będą użyteczne wyłącznie przełącznikowi zadań, który w bardzo dużym uproszczeniu wyglądałby następująco: #include „taska.h” #include „taskb.h” #include „taskc.h” #define MAX_TASK 3 code void (*code f[])(void)={ TaskA, TaskB, TaskC }; void main(void){ char task=0; for(;;){ (*f[task])(); task++; if(task==MAX_TASK) task=0; } } Nazwy zmiennych w plikach: taska.c, taskb.c i taskc.c, dobrano nie przypadkowo, sugeruje iż stanowią one „własność” modułów deklarujących je – i sugeruje programiście że nie wolno bezpośrednio innym modułom korzystać z nich, dodatkowo nazwy pomagają wyśledzić ewentualne odstępstwa od tej reguły. 4.Przłącznik zadań z kolejka W poprzednim punkcie zamieszczono opis trywialnego przełącznika. Działanie jego polega na ciągłym wywoływaniu funkcji obsługi każdego z zadań zgodnie z zawartością stałej tablicy „f”. Taka praca przełącznika zakłada, iż każde z zadań być uruchamiane w nieskończoność, a zarazem proces będzie musiał je obsługiwać bez względu na to czy zadanie ma coś do zrobienia czy nie. Rozwiązanie takie jest mało efektywne, zwłaszcza, gdy dane zadanie pełni rolę usługową względem innych zadań (serwer) i przez długi czas nie ma nic do zrobienia. Najlepszym rozwiązaniem powyższych problemów jest utworzenie dynamicznej kolejki zadań. Choć implementacja takiego systemu staje się nieco bardziej skomplikowana. Dla takiego systemu trzeba przyjąć, że istnieją dwa spisy: -spis zadań, które są przez system zarejestrowane, choć mogą nigdy nie być uruchomione (spis statyczny), -spis zadań, które trzeba w kolejności uruchomić (scenariusz pracy) System operacyjny zakłada, że każde zadanie wpierw jest rejestrowane a potem dopiero potem używane. Rejestracji dokonuje się za pomocą funkcja systemowej: PID InitTask(void(*tp)()); W wyniku działania powyższej funkcji, nowe zadanie będzie w systemie jednoznacznie identyfikowalne. Przełącznik będzie odtąd wiedział, jaka funkcja obsługuje to zadanie oraz utworzone zostaną dla tego zadania odpowiednie struktury komunikacji. W większości systemów operacyjnych liczba zadań, jakie mogą być zarejestrowane oraz jakie mogą równocześnie pracować jest ograniczona. W komputerach klasy PC mogą być ich tysiące, natomiast w systemach klasy STRC51 ich liczba powinna być bardzo mała (z reguły <32 zarejestrowanych i podobnie <32 równocześnie pracujących). Niech systemowym identyfikatorem zadania będzie obiekt PID zadeklarowany jako (oczywistym jest że zwiększenie liczby zadań mogących być zarejestrowanych w systemie wymaga zmiany tego typu danych): typedef char PID; Przykładowym wywołaniem rejestracji zadania - w kodzie aplikacji użytkowej - mógłby być poniższy pseudokod: void TaskA(void){ ... } ... void main(void){ ... PID p1=InitTask(TaskA); ... } Funkcja obsługi zadania jest w powyższym listingu podawana jako parametr wywołania funkcji systemowej InitTask(). Wynik działania funkcji rejestracji to unikatowy identyfikator przypisany danemu zadaniu. System operacyjny, aby poprawnie zaimplementować funkcję InitTask(), musi utworzyć tablicę opisującą zarejestrowane w systemie zadania: TCB TCB_list[OS_MAX_TASKS]; Każdy element takiej tablicy 'TCB' - odpowiadający jednemu zadaniu - to struktura opisana w następujący sposób: typedef struct { char flags; void (*tp)(); MSGQueue m; }TCB; Gdzie: -stan danego zadania w TCB_list (TCB_FREE-> wpis nieużywany, TCB_RUN-> wpis zajęty, TCB_REMOVE-> zadanie do usunięcia) tp -wskaźnik na funkcje obsługi danego zadania (o prototypie: void tp(void)) m -kolejka wiadomości do danego zadania (opisana dalej) Tablica TCB_list nie jest scenariuszem, według którego system operacyjny ma wywoływać kolejne zadania, a jest jedynie informacją, jakie zadania zarejestrowano w systemie i jakie można uruchamiać. Na tym etapie wyjaśnienia wymaga związek między wartością zwracaną przez InitTask() a pozycją zajmowaną przez zadanie w strukturze TCB_list[]. Dla uproszczenia implementacji funkcji InitTask(), przyjmuje się że w trakcie jej działania wyszukuje ona w TCB_list[] wolną pozycję, (czyli taką, w której pole '.flags' jest równe TCB_FREE) a numer wolnej tak odnalezionej pozycji jest zwracany jako unikatowy identyfikator zadania. Powołanie zadania do życia (z puli wcześniej zarejestrowanych zadań) polega na włożeniu jego identyfikatora do kolejki zadań do wykonania. Operację taką wykonuje się za pomocą funkcji systemowej: flags Post(PID_TaskA); Funkcja Post jest typu: char Post(PID ID); Parametr ID to identyfikator zadania wkładanego do kolejki. Funkcja Post(), powinna w pierwszej kolejności sprawdzić czy podany w wywołaniu ID jest związany z jakimś konkretnym zadaniem (czyli czy pozycja w TCB_list[] jest zajęta). Sprawdzenie takie wyklucza ewentualne nadużycia. Zadaniem systemowego przełącznika - pracującego jako główne zadanie tła - jest ciągłe sprawdzanie stanu kolejki zadań i uruchamianie zadania na jej szczycie. Z reguły w tej klasy systemach zadaniem tym zajmuje się pętla główna wyglądająca tak: void main(void){ ... while(1) { RunTask(); } } Zatem właściwe wywoływanie zadań - z punktu widzenia systemu operacyjnego - następuje przez wywołanie funkcji systemowej: RunTask(). Scenariusz wywoływania poszczególnych zadań jest zapisany w tablicy: PID PID_list[MAX_TASK]; A w implementacji funkcji RunTask(), jest zaszyte odnalezienie w PID_list[] zadania do uruchomienia i wywołanie funkcji obsługi tego zadania. Z reguły wykorzystywaną tutaj strukturą są bufory okrężne. A implementacje RunTask(), można zapisać następująco: void RunTask(void){ ... //wyznacznie obecnej wartosci 'actualPID' i=PID_list[actualPID]; (*TCB_list[i].tp)(); } Dla poprawnej pracy tak zapisanego przełącznika wymagane są jeszcze dwie zmienne: PID lastAddPID; Wskazuje ona na pozycje w PID_list[] na której jest ostatnio dodano zadanie do wykonania (koniec kolejki). PID actualPID; Wskazuje ona na pozycje w PID_list[] na którym jest zapisane aktualnie wykonywane zadanie. Z tego pola system operacyjny zna PID aktualnie wykonywanego zadania (np.: aby wiedzieć kto wysyła wiadomość do innego zadnia). Warte uwagi są jeszcze dwie stałe: MAX_TASK -określa długość scenariusza OS_MAX_TASKS -określa jak wiele zadań może być uruchomionych (różnorodność) Dla przypadku gdy MAX_TASK = OS_MAX_TASKS byłaby to sytuacja gdy każde zadanie może włożyć do kolejki tylko same siebie lub tylko jedno inne zadanie. W idealnym systemie należałoby obie te stałe dobrać zgodnie z równością: MAX_TASK = OS_MAX_TASKS * MAX_TASK Wtedy kosztem drogocennej pamięci, możliwości systemu byłyby niemal nieograniczone - każde zadanie mogłoby powołać siebie jak i wszystkie inne zadania z puli zarejestrowanych. Widać zatem że łagodne założenie: MAX_TASK > OS_MAX_TASKS Staje się najbardziej sensowne, bo zapewnia największą uniwersalność jak i zmniejsza zużycie cennej pamięci. Zapewnia się w ten sposób możliwość wielokrotnego wywoływania tego samego zadania, przy założeniu że nie będzie ono za każdym razem wywoływać wszystkie inne zadania. Nasunąć się może pytanie a co się stanie w przypadku, gdy kolejka się opróżni. Do takiej sytuacji nie powinno się dopuścić, jeżeli system ma działać dowolnie długo. W większości systemów zawsze działa jedno zadanie, np.: IDLE – czyli zadanie które sprawdza czy nie istnieją warunki do rozpoczęcia innego zadania. Bardzo często sytuacjami takimi może być interakcja z użytkownikiem (np.: wydanie polecenia z klawiatury: "uruchom zadanie XXX"), lub zdarzenie zewnętrzne (np.: przerwanie sprzętowe). Implementacja zadania IDLE (dalej nazywanego TaskA), mogłoby wyglądać tak: void func_TaskA(void){ if(sprawdzeni_warunkow_wywołania_TaskB()==0) Post(TaskB); if(sprawdzeni_warunkow_wywołania_TaskC()==0) Post(TaskC); Post(TaskA); } Przy tak napisanym zadaniu A, wystarczy, aby system przełącznika przed uruchomieniem pętli sprawdzającej kolejkę włożył zadanie A na jej początek. Zapis takiego przypadku w pseudokodzie wyglądał by następująco: void main(void){ ... PID=InitTask(func_TaskA); Post(PID); while(1) { RunTask(); } } Zastanawiający może być że w kodzie funkcji związanej z zadaniem func_TaskA, wpisano na jego końcu wywołanie: Post(TaskA). Jest to niezbędne, aby zadanie A mogło działać w nieskończoność. Implementacja systemu przełącznika powinna tak być realizowana, aby ostatnio wkładane zadanie (A) zawsze trafiało na koniec kolejki zadań i w ten sposób dawało szansę zadaniom innym (B i C) na wykonanie. Innym przykładem, w którym możliwe jest opróżnienie kolejki zadań i nadal prawidłowe działanie systemu, to przypadek, gdy obsługa przerwań może za pomocą funkcji Post() powoływać nowe zadania. Dobrym przykładem takiego scenariusza jest obsługa klawiatury - po naciśnięciu klawisza, procedura obsługi przerwania odpytują układy elektroniczne i wyciągają z nich numer klawisza, oraz powołują do wykonania właściwe zadanie obsługi klawiatury, które to zadanie w późniejszym czasie właściwie obsłuży odebrany znak. Warto wspomnieć, że bardziej zaawansowane systemy operacyjne realizują także kolejki priorytetowe. Można za ich pomocą jawnie sprecyzować jakie zadania w systemie są ważniejsze. Ramy tego dokumentu uniemożliwiają dokładniejszą analizę tego typu rozwiązań. 5.Komunikacja miedzy zadaniami Idea systemów wielozadaniowych zakłada możliwość przesyłania informacji między procesami (po to by np. synchronizować zadania, dzielić się danymi). W najprostszym podejściu można założyć że dowolne zadanie chce wysłać wiadomość do innego dowolnego zadania znając jego identyfikator. Każde z zadań, musi zatem mieć swoją kolejkę (w prostszych systemach kolejka może mieć długość 1), do której inne zadania za pomocą funkcji systemu operacyjnego (i tylko za jego pomocą!!!) będą wkładać wiadomości. Wymagane jest, zatem zaimplementowanie w systemowych funkcji dla potrzeb aplikacji użytkowej: int Send(PID *s, void *m, int n); System operacyjny ma wysłać (od aktualnie przetwarzanego zadania) wiadomość 'm', o długości 'n', do zadania, o PID wskazanym przez 's'. Zwracana wartość to: =0 -udało się wysłać wiadomość -(kod błędu) -nie udało się wysłać wiadomości - patrz kod błędu (listę błędów trzeba opracować, powinna ona uwzględniać błędy: wiadomość nie mieści się w skrzynce, nie ma takiego odbiorcy, inne) int Recive(void *m, int n, PID *s); System operacyjny odbiera najstarszą w kolejności wiadomość i wpisuje jej maksymalnie 'n' znaków do pamięci pod adres 'm' a pod wskazanie 's' wpisz PID nadawcy. Zwracana wartość to: >0 -długość wiadomości =0 -nie ma żadnej wiadomości W powyższych funkcjach zmienna 'm' reprezentuje niemal dowolny format wysyłanej wiadomości. Zakłada się, że dla wzajemnego zrozumienia nadawcy z odbiorcą wiadomość musi być tak sformatowana, aby oba zadania potrafiły ja poprawnie zinterpretować. Dobrym rozwiązaniem jest stosowanie unii, dzięki której możliwa będzie estetyczne zapisanie wszelkich możliwych wiadomości przesyłanych między nadawcą a odbiorcą: union _MSG{ struct _TASK_A_TO_TASK_B{ int x,y; }TASK_A_TO_TASK_B; struct _TASK_B_TO_TASK_A{ PID sender; char result; }TASK_B_TO_TASK_A; struct _OS{ OS_MSG request; }OS; //właściwa wiadomość //właściwa odpowiedz na wiadomość //wiadomość od systemu operacyjnego }MSG; Jest to bardzo praktyczny przykład gdzie jedno zadanie 'A' wysyła w wiadomości dwie wartości np.: parametry niezbędne do wykonania jakiejś czynności, a drugie zadanie 'B' odsyła odpowiedz po wykonaniu zleconego zadania (np.: wynik). W takim przypadku zadanie 'A' będzie wypełniać pola 'x' i 'y', natomiast zadanie 'B' będzie wypełniać jedynie pole 'result', a sam kompilator zadba o właściwą prezentacje danemu zadaniu odpowiednich pól. Aby całość mogła poprawnie działać, każde zadanie musi sprawdzić pole 's', przy odbieraniu wiadomości. Typowo zatem zadanie 'A' odbierając wiadomość, zauważywszy że jest ono od zadania 'B' z unii MSG będzie szukać wyniku operacji w polu 'result' a nie w innych polach (np.: x,y). W przypadku, gdy pole 's' przy odbieraniu wiadomości równe jest OS_SENDER (typowo można przyjąć że jest równe 0), sprawdzana powinna być obowiązkowo wartość zapisana w polu 'request'. W taki sposób system operacyjny mógłby komunikować się z aplikacjami, przekazując w ten sposób specjalne wiadomość. Na przykład po otrzymaniu specjalnej wiadomości np.: 'END_PROCESS', każdy proces miałby obowiązek w możliwe krótkim czasie zwolnić wszelkie zasoby kończąc swoje działanie i wywołać funkcje systemową funkcję exit(). Wartym zwrócenia uwagi jest sposób przechowywania wiadomości przez system operacyjny. W wspomnianej wcześniej strukturze TCB, znajdujemy pole 'm', ustala on gdzie w pamięci dla danego zadania, przechowywana jest struktura skrzynki pocztowej. Skrzynka pocztowa może mięć następującą strukturę: typedef struct{ int in; int out; MSG t[MAX_MESSAGE]; }MSGQueue; Gdzie in - wskazuje na miejsce gdzie ostatnio wstawiono wiadomość do tablicy 't' out - wskazuje na miejsce skąd ostatnio odczytano wiadomość w tablicy 't' t - tablica wiadomości A elementami tablicy 't' są struktury przechowujące właściwe wiadomości: typedef struct{ PID sender; int len; char c[MAX_LEN_MESSAGE]; }MSG; Gdzie -kto nadał tą wiadomość -jaka jest długość tej wiadomości -właściwa składnica treści tej wiadomości Stała MAX_LEN_MESSAGE, wyznacza maksymalną wielkość wiadomości do przesłania dla dowolnego zadania. Warto zwrócić uwagę na pojemność wszystkich struktur opisujących zadania w tym systemie operacyjnym. Mamy, zatem: sender len c Pole 'C' jest o wielkości: sizeof(char) * MAX_LEN_MESSAGE Pole 'MSG' jest o wielkości: sizeof(PID)+sizeof(int)+sizeof(C) Pole 'MSGQueue' jest o wielkości: 2*sizeof(int)+sizeof(MSG)*MAX_MESSAGE Jedno pole 'TCB_list' jest o wielkości: sizeof(char)+sizeof(void*)+sizeof(MSGQueue) Po podstawieniu konkretnych i dość typowych wartości: OS_MAX_TASKS=8 MAX_MESSAGE=7 MAX_LEN_MESSAGE=16 Czyli w systemie będzie można zarejestrować 8 zadań, i każde z nich będzie mogło wysłać do dowolnego innego zadania po 7 wiadomości, oraz wiadomości te będą o maksymalnie 16 bajtowej długości, to cała TCB_list[] zajmie w pamięci przestrzeń o wielkości zgodnej z równaniem: {sizeof(char) + sizeof(void*) + 2*sizeof(int) + +[sizeof(PID) + sizeof(int) + sizeof(char)*MAX_LEN_MESSAGE]*MAX_MESSAGE}*OS_MAX_TASKS czyli po uproszczeniach wynikających z wstawienia właściwych wielkości typów danych otrzymujemy: {1+2+2*2+[1+2+1*16]*7}*8 = 1120 Widać od razu że tak dużej struktury nie można umieścić w pamięci IDATA procesora 80C51. Deklaracja tablicy TCB_list[] powinna być, zatem przy deklaracji poprzedzona specyfikacją xdata, informującą kompilator, w jakiej pamięci umieścić tablicę (tu zewnętrznej pamięci RAM - której jest 32KB). Inną metodą ograniczenia wielkości tablic opisu zadań systemowych jest zmniejszenie liczby możliwych do zainicjowania zadań lub liczby wiadomości do danego zadania lub w końcu zmiany samej długości maksymalnej wiadomości. Takie postępowanie mocno zależy od użyteczności systemu z aplikacjami i środowiska, w jakim będzie on pracować. Inną metodą obejścia ograniczeń pamięciowych jest odpowiednie ustawienie wielkości niektórych pól używanych w ramach opisanych struktur. Dla przykładu struktura MSG zawiera pole 'len’, które określa ile pozycji zajmuje wiadomość w polu 'c'. Jak widać z innych deklaracji pole 'c' ma wielkość MAX_LEN_MESSAGE, zatem jeżeli w efekcie pole to nie będzie zajmować więcej niż 255 znaków, to warto zmienić typ pola 'len' z 'int' na 'unsigned char' zaoszczędzając w ten sposób cenną pamięć. Podobna uwaga dotyczy pól 'in' i 'out' w strukturze MSGQueue. Jednak najlepsze efekty uzyskuje się zastosowanie mechanizmów pamięci dynamicznej. Dzięki czemu nie będzie potrzeby statycznego kreowania olbrzymich struktur opisu zadań. Wynika to głównie z faktu że w trakcie pracy systemu ujawnia się dopiero jakie wielkości struktury są najpotrzebniejsze, dla przykładu nie każde zadanie będzie potrzebowało zawsze takiej samej wielkiej kolejki wiadomości lub pojawią się zadania które nie będą korzystały z systemu wiadomości w ogóle. Niestety w pakiecie SDCC, możliwość stosowania pamięci dynamicznej jest nieco utrudniona i ograniczona, w szczególności odpowiednie biblioteki nie są dołączane domyślnie pod czas kompilacji. W prostych systemach należy także ograniczać liczbę procesów – głównie z powodu problemu wielkości stosu i obszaru zmiennych związanych z wykonywanymi zadaniami. 6.Podsumowanie W dokumencie przedstawiono podstawowe funkcje systemu wielozadaniowość i komunikację między zadaniami. Są to funkcje: PID InitTask(void(*tp)()); char Post(PID ID); void RunTask(void); int Send(PID *s, void *m, int n); int Recive(void *m, int n, PID *s); operacyjnego wspierającego