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