Temat: System zarządzania zadaniami i ich komunikacja (Scheduler, MessageBox).



Podobne dokumenty
Temat: System zarządzania zadaniami i ich komunikacja (Scheduler, MessageBox).

METODY I JĘZYKI PROGRAMOWANIA PROGRAMOWANIE STRUKTURALNE. Wykład 02

Podczas dziedziczenia obiekt klasy pochodnej może być wskazywany przez wskaźnik typu klasy bazowej.

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

ISO/ANSI C - funkcje. Funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje

Rozdział 4 KLASY, OBIEKTY, METODY

Podstawy programowania, Poniedziałek , 8-10 Projekt, część 1

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Ćwiczenie 3 stos Laboratorium Metod i Języków Programowania

Czym są właściwości. Poprawne projektowanie klas

4. Procesy pojęcia podstawowe

Lista 5 Typy dynamiczne kolejka

Zmienne i struktury dynamiczne

Informacje ogólne. Karol Trybulec p-programowanie.pl 1. 2 // cialo klasy. class osoba { string imie; string nazwisko; int wiek; int wzrost;

4. Procesy pojęcia podstawowe

Temat: Dynamiczne przydzielanie i zwalnianie pamięci. Struktura listy operacje wstawiania, wyszukiwania oraz usuwania danych.

PROE wykład 2 operacje na wskaźnikach. dr inż. Jacek Naruniec

SYSTEMY OPERACYJNE: STRUKTURY I FUNKCJE (opracowano na podstawie skryptu PP: Królikowski Z., Sajkowski M. 1992: Użytkowanie systemu operacyjnego UNIX)

- - Ocena wykonaniu zad3. Brak zad3

Podstawy programowania komputerów

Język C++ zajęcia nr 2

Deklaracja struktury w C++

Algorytmy i złożoności. Wykład 3. Listy jednokierunkowe

Laboratorium 5: Tablice. Wyszukiwanie binarne

Lab 9 Podstawy Programowania

1. Wartość, jaką odczytuje się z obszaru przydzielonego obiektowi to: a) I - wartość b) definicja obiektu c) typ oboektu d) p - wartość

Wskaźniki w C. Anna Gogolińska

/* dołączenie pliku nagłówkowego zawierającego deklaracje symboli dla wykorzystywanego mikrokontrolera */ #include <aduc834.h>

4. Procesy pojęcia podstawowe

Kompilator języka C na procesor 8051 RC51 implementacja

Algorytm. a programowanie -

Jeśli chcesz łatwo i szybko opanować podstawy C++, sięgnij po tę książkę.

Argumenty wywołania programu, operacje na plikach

Zmienne, stałe i operatory

Programowanie strukturalne i obiektowe. Funkcje

Zapisywanie algorytmów w języku programowania

Programowanie w języku Python. Grażyna Koba

Ciekawym rozwiązaniem służącym do obsługi zdarzeń dla kilku przycisków w ramach jednej aktywności może być następujący kod:

Dodatek B. Zasady komunikacji z otoczeniem w typowych systemach komputerowych

Aplikacja Sieciowa wątki po stronie klienta

TEMAT : KLASY DZIEDZICZENIE

Tablice (jedno i wielowymiarowe), łańcuchy znaków

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Pętle. Dodał Administrator niedziela, 14 marzec :27

IPC: Kolejki komunikatów

Procesy i wątki. Krzysztof Banaś Obliczenia równoległe 1

Podstawy programowania skrót z wykładów:

Obiektowy PHP. Czym jest obiekt? Definicja klasy. Składowe klasy pola i metody

Program 6. Program wykorzystujący strukturę osoba o polach: imię, nazwisko, wiek. W programie wykorzystane są dwie funkcje:

Podstawy programowania

Wykład 5: Klasy cz. 3

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

PMiK Programowanie Mikrokontrolera 8051

Start Bity Bit Stop 1 Bit Par Rys. 1

Obiekt klasy jest definiowany poprzez jej składniki. Składnikami są różne zmienne oraz funkcje. Składniki opisują rzeczywisty stan obiektu.

Wykład 8: klasy cz. 4

1. Liczby i w zapisie zmiennoprzecinkowym przedstawia się następująco

Analiza konstrukcji zawierających wskaźniki. Piotr Błaszyński

Język C, tablice i funkcje (laboratorium, EE1-DI)

Rozpoczynamy import Kreator uruchamiamy przyciskiem Z tekstu, znajdującym się na karcie Dane, w grupie Dane zewnętrzne.

Ogranicz listę klasyfikacji budżetowych do powiązanych z danym kontem księgowym

Cwiczenie nr 1 Pierwszy program w języku C na mikrokontroler AVR

Wymagania Uczeń zna zasady bezpiecznej pracy z komputerem. Uczeń stosuje się do regulaminu szkolnej pracowni komputerowej.

Jak napisać program obliczający pola powierzchni różnych figur płaskich?

Laboratorium Informatyka (I) AiR Ćwiczenia z debugowania

IMIĘ i NAZWISKO: Pytania i (przykładowe) Odpowiedzi

Tablice i struktury. czyli złożone typy danych. Programowanie Proceduralne 1

Programowanie współbieżne Wykład 2. Iwona Kochańska

Struktura programu. Projekty złożone składają się zwykłe z różnych plików. Zawartość każdego pliku programista wyznacza zgodnie z jego przeznaczeniem.

Informatyka I. Klasy i obiekty. Podstawy programowania obiektowego. dr inż. Andrzej Czerepicki. Politechnika Warszawska Wydział Transportu 2018

Wymagania Uczeń zna zasady bezpiecznej pracy z komputerem. Uczeń stosuje się do regulaminu szkolnej pracowni komputerowej.

Aplikacje w środowisku Java

Wyjątki. Streszczenie Celem wykładu jest omówienie tematyki wyjątków w Javie. Czas wykładu 45 minut.

5. Model komunikujących się procesów, komunikaty

Wstęp do programowania INP003203L rok akademicki 2018/19 semestr zimowy. Laboratorium 3. Karol Tarnowski A-1 p.

#include <iostream> using namespace std; void ela(int); int main( ); { Funkcja 3. return 0; }

Rekurencja (rekursja)

Od programowania wizualnego do tekstowego

Lekcja 8, 9 i 10. Konspekt lekcji Poczta elektroniczna. Materiał z podręcznika: Rozdział 5. Poczta elektroniczna

Wydział Elektryczny. Katedra Telekomunikacji i Aparatury Elektronicznej. Konstrukcje i Technologie w Aparaturze Elektronicznej.

Wstęp do programowania 2

Podstawy Programowania C++

Podstawy technologii WWW

Serwis jest dostępny w internecie pod adresem Rysunek 1: Strona startowa solidnego serwisu

Kontrola topto. 1. Informacje ogólne. 2. Wymagania sprzętowe i programowe aplikacji. 3. Przykładowa instalacja topto. 4. Komunikacja.

I. EDUKACJA WCZESNOSZKOLNA

Projektowanie oprogramowania systemów PROCESY I ZARZĄDZANIE PROCESAMI

Ćwiczenie 1. Kolejki IBM Message Queue (MQ)

Zaawansowane programowanie w C++ (PCP)

RPC. Zdalne wywoływanie procedur (ang. Remote Procedure Calls )

Wątek - definicja. Wykorzystanie kilku rdzeni procesora jednocześnie Zrównoleglenie obliczeń Jednoczesna obsługa ekranu i procesu obliczeniowego

1. Które składowe klasa posiada zawsze, niezależnie od tego czy je zdefiniujemy, czy nie?

Podstawy programowania 2. Przygotował: mgr inż. Tomasz Michno

Podstawy programowania. Wykład 7 Tablice wielowymiarowe, SOA, AOS, itp. Krzysztof Banaś Podstawy programowania 1

Dokumentacja wstępna TIN. Rozproszone repozytorium oparte o WebDAV

Instrukcje sterujące. wer. 11 z drobnymi modyfikacjami! Wojciech Myszka :53:

Program jest więc strukturą statyczną zapisaną na jakimś nośniku. Natomiast proces jest wykonującym się programem.

Autor: Joanna Karwowska

Pytania sprawdzające wiedzę z programowania C++

Uproszczony opis obsługi ruchu w węźle IP. Trasa routingu. Warunek:

Transkrypt:

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:

void funcn(void){ Oryginalny kod z pętlą bardzo długą long l; //zmienna lokalna funkcji!!! for(l=0; l<2147483647; l++){ //zadanie do //wykonania wewnątrz pętli Zmieniona wersja kodu bez pętli bardzo długiej long l=0; void funcn(void){ if(l<2147483647){ //zadanie do wykonania //wewnątrz pętli l++; //zmienna globalna!!! 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 Plik: taska.h int ita,jta; void TaskA(void){ Plik: taskb.c int itb,jtb; void TaskB(void){ Plik: taskc.c void TaskA(void); Plik: taskb.h void TaskB(void); Plik: taskc.h int itc,jtc; void TaskC(void){ 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: flags -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: 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; //właściwa wiadomość //właściwa odpowiedz na wiadomość MSG; struct _OS{ OS_MSG request; OS; //wiadomość od systemu operacyjnego 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 sender len c -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:

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 operacyjnego wspierającego 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);