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.