C dla każdego (cz. 27.)

Ulepszanie systemu (cz. 2.)

Dziś dokończymy opis zaprezentowanego miesiąc temu listingu oraz, w związku z tematem kolejnego odcinka, omówimy tzw. semafory.

Jak to wszystko działa

Jak już o tym pisaliśmy przed miesiącem, najważniejszym obiektem jest broker. Można do niego podłączyć kolejkę obiektów, które będą analizować i modyfikować zdarzenia ze strumienia wejściowego.

Obecnie istnieje 7 rodzajów obiektów "Cx0bjects". W naszym przykładzie użyliśmy pięciu najważniejszych; broker, custom, filter, sender i translator. Obiekty "custom" i "filter" przyłączyliśmy bezpośrednio do brokera. Pierwszy z nich opisaliśmy miesiąc temu, czas więc na ten drugi:

Filter

Zazwyczaj nie jesteśmy zainteresowani wszystkimi przepływającymi zdarzeniami wejściowymi. W naszym przykładzie chcemy pokazać użytkownikowi działanie "hotkey". Jest to zwykle sekwencja klawiszy, która powoduje otwarcie okna programu. W takim przypadku tworzymy obiekt "filter", używając makrodefinicji CxFitter(), zdefiniowanej w pliku "libraries/commodities.h". Argumentem jej wywołania jest wskaźnik na napis opisujący interesujące nas zdarzenie. Rezultatem jest wskaźnik na "Cx0bj" lub NULL w przypadku błędu. W przypadku definiowania "hotkeya" standardem jest danie użytkownikowi wolności wyboru, przy użyciu opcji "CX_POPKEY".

Wyrażenie filtrujące można zmienić już po utworzeniu filtra za pomocą funkcji:

void SetFilter(Cx0bj *filter, STRPTR text);

Jej argumentami są: obiekt commodities będący filtrem oraz napis opisujący zdarzenia do filtrowania.

Opis zdarzeń

Wyrażenia opisujące zdarzenia są omówione w instrukcji do Workbencha, tyle że w formie mocno "ocenzurowanej" - tylko zdarzenia z klawiatury, a i to w ograniczonej formie. Ogólnie format zapisu jest następujący:

[klasa] [-1[kwalifikator] [o][upstroke] [wartość]

Pole "klasa" opisuje rodzaj zdarzenia. Domyślnie jest to "rawkey" (klawiatura), ale mogą być i inne, np.:

rawmouse - mysz, newprefs - zmiana preferencji, diskinserted - włożenie dysku, diskremoved - wyjęcie dysku.

W kolejnym polu znajdują się okoliczności (kwalifikatory) zdarzenia. Normalnie, aby zdarzenie zostało przez filtr zaakceptowane, te i tylko te kwalifikatory muszą być spełnione. Jeśli jednak kwalifikator jest poprzedzony znakiem "-", to oznacza to, że nie należy zwracać na niego uwagi. Kwalifikatorami są następujące napisy:

Ishift, rshift, capslock, control, lalt, rait, Icommand, rcommand, numericpad, repeat, midbutton, rbutton, leftbutton, relativemouse.

Można też użyć synonimów:

shift - prawy bądź lewy shift, caps - dowolny shift lub włączony CapsLock, alt - dowolny alt.

Po kwalifikatorach może wystąpić napis "upstroke", opcjonalnie poprzedzony znakiem "-". Jeśli napis ten nie występuje, docierają do nas informacje o wciśnięciu przycisku. W przeciwnym wypadku o jego puszczeniu. Gdy dodatkowo poprzedzimy napis minusem, będą docierały zarówno informacje o wciśnięciu jak i o puszczeniu przycisku.

Ostatnim elementem jest wartość, opisująca klawisz na klawiaturze. Jest to zwykle pojedyncza litera, ale dla klawiszy "nieliterowych" jest to cały napis. Na przykład dla kursorów "up", "down" itd., "f1"do "f10", "help", "del", "esc", "tab", "space", "enter", "return", "backspace".

Sender

Zadaniem nadawcy - obiektu "sender" - jest wysłanie wiadomości do wskazanego portu. Sender kopiuje i wysyła wszystkie informacje nadchodzące do niego ze strumienia wejściowego. Strumień przepływających danych nie jest modyfikowany. Aby utworzyć taki obiekt, należy wykorzystać makrodefinicję CxSender(), której argumentami są wskaźnik na strukturę "MsgPort" oraz liczbowy Identyfikator, pozwalający rozróżniać wiadomości napływające do portu (patrz niżej). Podobnie jak w przypadku pozostałych makrodefinicji, ta również zwraca wskaźnik na "Cx0bj" lub NULL w przypadku błędu. Ze względu na ilość zdarzeń w strumieniu wejściowym, obiekt ten podłącza się zwykle poprzez filtr, tak też czynimy w naszym przykładzie.

Translator

Zadaniem tłumacza - obiektu "translator" - jest modyfikacja zdarzeń ze strumienia wejściowego, a konkretnie ich podmiana na wcześniej zdefiniowane zdarzenie. Nie jest możliwe podejmowanie żadnych akcji warunkowych, co w praktyce zmusza do dołączania tego obiektu poprzez filtr. Tłumaczy tworzy się przy użyciu makrodefinicji CxTranslate(), której jedynym argumentem jest wskaźnik na strukturę "lnputEvent", opisującą nowe zdarzenie. Bardzo często argumentem jest NULL: wówczas obiekt usuwa wszystkie nadchodzące zdarzenia - tak jest i w naszym przykładzie.

Wstawiane zdarzenie można zmienić, już po utworzeniu tłumacza, przy użyciu funkcji:

void SetTranslate(Cx0bj *translator, struct InputEvent *events);

Podsumujmy może, do czego używamy w naszym przykładzie poszczególnych obiektów. Do brokera dołączone są obiekty: własny i filtrujący. Ten pierwszy modyfikuje strumień zdarzeń w zależności od stanu klawiszy CapsLock i shiftów, drugi zapewnia obsługę "hotkeya". Filtr przepuszcza przez swoją wewnętrzną listę obiektów tylko zdarzenia spełniające wyrażenie filtrujące, a więc informacje o przy- ciśnięciu "hotkeya". Docierają one najpierw do nadawcy, który wysyła odpowiednią wiadomość do naszego portu, a następnie do tłumacza, który je usuwa. Dzięki tej ostatniej operacji informacje nie wydostają się na zewnątrz i np. nie pojawiają się w oknie edytora.

Pozostało nam jeszcze do omówienia kilka technicznych szczegółów:

Dodawanie i usuwanie obiektów

Najprostszym sposobem na dodanie obiektu do listy innego obiektu commodities jest wywołanie funkcji:

void AttachCxObj (Cx0bj *head0bj, Cx0bj *co);

"headObj" to obiekt, do którego listy dodajemy nowy obiekt - "co". Obiekt jest dodawany na końcu listy.

Usunięcie obiektu z listy, do której jest on dołączony, jest realizowane przez funkcję:

void RemoveCxObj(Cx0bj *co);

Obsługa błędów

Trochę po macoszemu traktowaliśmy dotychczas obsługę błędów. Mogliśmy sobie na to pozwolić, gdyż biblioteka commodities posiada mechanizmy kumulacji informacji o błędach. Oznacza to, że nie trzeba co chwilę sprawdzać, czy wystąpił błąd - można to zrobić "hurtem", np. po utworzeniu całego drzewa obiektów. Sprawdzenia dokonuje się za pomocą funkcji:

LONG Cx0bjError(Cx0bj *co);

Zwraca ona informację o błędach, będącą maską bitową następujących flag:

COERFLBADFILTER - błędny napis dla filtra, COERRJSNULL - argument funkcji CxObjError() jest równy NULL, COERRJMULLATTACH - próba dołączenia NULL do listy obiektów, COERR_BADTYPE - operacja nie jest dostępna dla tego typu obiektu.

Informację o błędach można "wyzerować" wywołując funkcję:

void ClearCxObjError(Cx0bj *co);

TU WAŻNA UWAGA: analizując ponownie listing sprzed miesiąca, doszliśmy do wniosku, że zawiera on pewną niedoróbkę. Funkcję CxObjError() wywołujemy tylko dla filtra, podczas gdy powinniśmy ją wywołać również dla brokera. Co się stanie, gdy zawiedzie CxCustom()?

Aktywacja obiektów

W każdym obiekcie commodities zanotowany jest jego stan - obiekt może być aktywny bądź nie. Gdy obiekt nie jest aktywny, komunikaty omijają go i trafiają do następnego w kolejce. Wszystkie obiekty z wyjątkiem brokerów są aktywne po utworzeniu. Do zmiany aktywności obiektu służy funkcja:

LONG ActivateCxObj (Cx0bj *co, long true);

Argumentami są: obiekt oraz wartość określająca czy obiekt ma być aktywny, czy nieaktywny (0). Funkcja zwraca poprzedni stan obiektu.

Zwalnianie obiektów commodities

Do usuwania obiektów służą dwie funkcje. Pierwsza z nich usuwa pojedynczy obiekt, druga usuwa obiekt wraz z całym jego poddrzewem, tzn. ze wszystkimi dołączonymi doń obiektami:

void DeleteCxObj(Cx0bj *co);
void DeleteCxObjAll(Cx0bj *co);

Komunikaty brokera

Broker pełni nadrzędną rolę w stosunku do pozostałych obiektów commodities. To z nim komunikuje się program "Exchange". Przy tworzeniu brokera określamy port, który ma służyć do komunikowania się z nami. Ponieważ w naszym przykładzie używamy wspólnego portu dla wiadomości od "Exchange" i od "sendera", niezbędna jest możliwość ich rozróżniania. Typ wiadomości sprawdzamy przy użyciu funkcji:

ULONG CxMsgType(CxMsg *cxm);

Wiadomości przesyłane przez "Exchange" są typu CXM_COMMAND, a przesyłane przez "sender" - typu CXM_IEVENT. Identyfikator wiadomości można uzyskać przy użyciu funkcji:

LONG CxMsgID(CxMsg *cxm) ;

W przypadku wiadomości od "Exchange" przyjmuje on następujące wartości:

CXCMD_DISABLE - zawieś swoją działalność, CXCMD_ENABLE - wracaj do pracy, CXCMD_KILL - zakończ się, CXCMD_UNIQUE - ktoś próbował utworzyć broker o identycznej nazwie, CXCMD_APPEAR - pokaż interfejs, CXCMD_DISAPPEAR - schowaj interfejs.

W przypadku wiadomości od "sendera" funkcja CxMsgID() zwraca identyfikator ustawiony podczas tworzenia obiektu.

Różne

Ostatnią "standardową" opcją akceptowaną przez program jest "CX_POPUP", przyjmująca wartości "yes"/"no". Określa ona, czy przy uruchamianiu ma być otwierane okno programu (domyślnie: tak).

Zauważcie, że gdy okno jest już otwarte, a dotrze informacja "otwórz okno", wysuwamy okno na wierzch i uaktywniamy je. Okno może być po prostu gdzieś tam "zagrzebane" i użytkownik może nie być świadomy, że jest już otwarte.

Zwróćcie uwagę, że wychodząc z programu, przed usunięciem portu, zwracamy wszystkie wiadomości, które jeszcze się w nim znajdują. Robimy to dopiero po usunięciu brokera, kiedy mamy już gwarancję, że żadne nowe informacje nie napłyną. Na pytanie:

"Dlaczego nie muszę tego robić przy oknach? Przecież one też mają swój port" odpowiedź brzmi: "Bo robi to za Ciebie Intuition".

Na tym zakończymy opis biblioteki "commodities.library". Ponieważ zostało nam jednak trochę miejsca, zajmiemy się problemem wzajemnego wykluczania, co będzie nam potrzebne w niedalekiej przyszłości.

Zasoby krytyczne i semafory

Informatyka nie jest bezpośrednio związana z koleją, jednak pojęcie semafora jest istotne i nie sposób go pominąć w przypadku systemów wielozadaniowych. Zadaniem semaforów jest zabezpieczanie przed kolizjami w dostępie do wspólnych obszarów pamięci oraz sprzętu. Semafory pilnują, aby w "sekcji krytycznej" (obszarze chronionym) przebywał tylko jeden proces z prawami dokonywania zmian lub też kilka procesów mających tylko prawo odczytu. Jest to dość szczególny rodzaj semaforów, znany w teorii współbieżności z "problemu czytelników i pisarzy".

Każdy proces wykorzystujący zasoby krytyczne musi najpierw sprawdzić stan semafora. Jeśli semafor pozwala mu dostać się do sekcji krytycznej, to proces musi natychmiast zmienić stan semafora, tak aby nikt inny nie znalazł się w sekcji krytycznej wraz z nim. Widać, że proces sprawdzania stanu semafora i jego zmiany na stan przeciwny musi być dokonany w jednym kroku, musi to być operacja niepodzielna, a tylko system jest w stanie nam to zagwarantować.

W systemie semafor jest reprezentowany przez strukturę "SignalSemaphore", zdefiniowaną w pliku "exec/semaphores.h". Jej szczegóły nie są jednak istotne poza faktem, że jej pierwszym polem jest "ss_Link" typu "struct Node", stanowiące podstawę budowy list.

Do zainicjowania semafora służy funkcja:

void InitSemaphore(struct SignalSemaphore *sigSem);

Jej argumentem jest wskaźnik na pustą strukturę "SignalSemaphore", która zostanie wypełniona.

Semafor zainicjowany w taki sposób jest widziany tylko przez nasz proces, nikt inny nie może się do niego dostać. Jeśli chcemy utworzyć semafor "publiczny", należy wypełnić w strukturze "Node" (pole "ss_Link" struktury "SignalSemaphore") pola "ln_Name" i "ln_Pri". "ln_Name" jest wskaźnikiem na napis, zaś "ln_Pri" jest liczbowym priorytetem. Jeśli wspomniane pola są już wypełnione, należy wywołać funkcję:

void AddSemaphore(struct SignalSemaphore *sigSem);

Funkcja ta podłącza semafor do systemowej listy semaforów. Inne procesy mogą odnaleˇć ten semafor poprzez nazwę za pomocą funkcji:

struct SignalSemaphore *FindSemaphore(UBYTE *sigSem);

Jej jedynym argumentem jest wskaźnik na nazwę semafora. Rezul- tatem jej działania jest wskaźnik na strukturę "SignalSemaphore" lub NULL w przypadku, gdy semafor nie został odnaleziony.

Dostęp do semafora

Aby dostać się do obszaru chronionego, należy zmienić stan semafora. Co jednak robić, gdy semafor jest "opuszczony" i nie wolno nam wejść do sekcji krytycznej? Każdy kolejarz zapewne powie: "stać i czekać", jednak w informatyce tak być nie musi. Możemy sprawdzić stan semafora, a jeśli nie pozwala on na "wjazd", zająć się innymi czynnościami. W związku z tym Istnieją dwie funkcje dostępu do semafora:

void ObtainSemaphore (struct SignalSemaphore *sigSem);
ULONG AttemptSemaphore(struci SignalSemaphore *sigSem);

Pierwsza z nich zawiesza działanie procesu do czasu zwolnienia się semafora, druga natychmiast zwraca nam sterowanie. Jeśli wartością zwróconą jest zero, to nie wolno nam wejść do sekcji krytycznej.

Po zakończeniu sekcji krytycznej musimy zwolnić semafor, wywołując funkcję:

void ReleaseSemaphore(struct SignalSemaphore *sigSem);

Funkcja ta musi zawsze być wywołana, gdy wcześniej wywołaliśmy ObtaInSemaphore() oraz wówczas, gdy Attempt8emaphore() zwróciła wartość różną od zera.

Często aby wejść do sekcji krytycznej, proces musi otrzymać zezwolenie od kilku semaforów. Nie możemy w tej sytuacji korzystać z sekwencji kilku wywołań ObtainSemaphore(), ponieważ może to doprowadzić do sytuacji, w której kilka procesów dostanie np. po jednym semaforze, ale nigdy nie otrzyma wszystkich (zagadnienie to znane jest w literaturze jako "problem pięciu filozofów" oraz "deadlock"). W takim przypadku niezbędne jest połączenie wszystkich semaforów w listę i użycie funkcji:

void ObtainSemaphoreList(struct List *sigSem);

Czeka ona aż do chwili, gdy wszystkie semafory będą wolne. Jej argumentem jest wskaźnik na listę semaforów, które zamierzamy otrzymać.

Aby zwolnić listę przydzielonych semaforów należy wywołać funkcję:

void ReleaseSemaphoreList(struct List *sigSem);

W wielu wypadkach do krytycznego obszaru, np. listy, może mieć dostęp kilka procesów naraz, o ile wszystkie dokonują jedynie odczytu. Aby to umożliwić w wersji 2.04 systemu operacyjnego dodano funkcje:

void ObtainSeraaphoreShared(struct SignalSemaphore *sigSem);
ULONG AttemptSemaphoreShared(strucfc SignalSemaphore *sigSem);

Aby zwolnić współdzielony semafor, należy skorzystać z funkcji ReleaseSemaphore().

Kończymy już na dzisiaj temat semaforów, więc warto wiedzieć, jak usunąć z systemu istniejący "publiczny" semafor. Służy do tego funkcja:

void RemSemaphore(struct SignalSemaphore *sigSem);

Jej jedynym argumentem jest wskaźnik na semafor.

Zainteresowanym teoretycznymi aspektami współbieżności polecamy klasyka gatunku:
M. Ben-Ari: "Podstawy programowania współbieżnego i rozproszonego", WNT, Warszawa, 1996.