System operacyjny UNIX dla poczatkujacych i zaawansowanych, rozdzial6


Jąd ro

Funkcje i struktura jądra

Jądro jest najgłębszą częścią systemu operacyjnego LTNIX, realizującą podstawowe funkcje sterowania i nadzoru pracy całego systemu. Stanowi on~~ jedyną część systemu, do której zwykły użytkownik nie ma bezpośredniego dostępu i nie może wymienić jej na własną wersję.

Najważniejsze usługi świadczone przez jądro systemu operacyjnego UNIX to: zarządzanie procesami, zarządzanie pamięcią, zarządzanie plikami i obsługa operacji wejścia/wyjścia. Nie dla wsrystkich użytkowników usługi te są dostępne w jednakowym zakresie; niektóre z nich są dostępne tylko dla administratora systemu. Przedstawimy tutaj te usługi, które mogą interesować stosunkowo liczną grupę użytkowników. Są to przede wsrystkim crynności związane z tworzeniem pracujących współbieżnie procesów, sterowanie ich wykonywaniem, synchronizacją oraz komunikacją mlędry nimi.

Działania systemu operacyjnego w obrębie jądra dzielimy na: obsługę funkcji systemowych; putapek i przerwań. Działania te omówimy szczegółowo w następnym podrozdziale.

W większości realizacji systemu IJNIX można wyróżnić w jądrze dwie części; jedna z nich obsługuje zastosowania, a druga przerwania (rys. 6.1).

Proces uzyfkownika Funkcja systemowa

Pulapka CzęSt obshiguj~ca zastosowania Częśt obsiugujaca przanvania

P2erwania Sp2et

Rys. 6.1. Struktura jądra

i~1,..,.._

96 Rozdziaf 6.

Część obsługująca zastosowania podejmuje określone akcje w odpowiedzi na tak zwane pułapki i wywołania funkcji systemowych. Jest ona biblioteką programów wykorzystywanych przez wiele procesów w trybie jądra.

Część obsługująca przerwania jest zbiorem specjalistycznych programów, które są wywoływane w chwili pojawienia się przerwań sprzętowych. Przerwania są generowane przede wszystkim przez urządzenia wejścia/wyjścia oraz ewentualnie inne urządzenia, np. zegar czasu rzeczywistego. Działania w tej części jądra występują asynchronicznie.

Obie części jądra współpracują w rozdziale zasobów systemu oraz realizacji operacji wejścia i wyjścia. Zazwyczaj część obsługująca zastosowania rozpoczyna operację wejścia/wyjścia. Rezygnuje z procesora i następnie czeka na powiadomienie przez drugą część, że obsługa wejścia/wyjścia została zakończona.

Część obsługująca zastosowania może ustawić poziom priorytetu procesora na taką wartość, która zablokuje określone przerwania. Tego mechanizmu używa się dla zapewnienia spójności struktur danych wykorzystywanych wspólnie przez poszczególne elementy jądra.

Funkcje systemowe, pułapki i przerwania

Jednym z działań najczęściej wykonywanych w jądrze jest żądanie wykonania funkcji systemowej. Funkcje systemowe zapewniają

dostęp do usług świadczonych przez jądro, a żądanie ich wykonania można uważać za polecenia dla jądra. Program obsługujący wywołanie funkcji systemowej, zanim funkcja systemowa zostanie zainicjowana, wykonuje następujące działania wstępne:

• odczytuje parametry funkcji systemowej,

• sprawdza, czy parametry są umieszczone pod poprawnymi adresami i następnie kopiujeje z procesu uiytkownika do jądra,

• wywołuje w jądrze odpowiedni program wykonujący funkcję systemową.

Wyniki działania funkcji systemowej są zwracane do procesu użytkownika. Przy wywołaniu zakończonym sukcesem program obsługi funkcji systemowej w jądrze zwraca wartość specyficzną dla danej funkcji, przy wywołaniu zakończonym porażką - wartość I.

W czasie wykonywania funkcji systemowej mogą wystąpić dwa zdarzenia. Do procesu może być przesłany sygnał albo inny proces może osiągnąć wyższy priorytet. Jeżeli do procesu zostaje przesłany sygnał, to może on spowodować zaniechanie

wykonywania funkcji systemowej. Inna możliwość to zawieszenie sygnału do czasu zakończenia funkcji. Jeżeli w systemie istnieje proces o wyższym priorytecie, to program koordynujący wywołuje program szeregujący procesy, czyli program obsługujący przydział procesora do procesów. Program ten spowoduje, że procesor zostanie przydzielony do procesu o wyższym priorytecie. Wówczas proces bieżący uzyska najwyższy priorytet i wznowi wykonywanie przez powrót z funkcji systemowej do przerwanego fragmentu w procesie użytkownika.

Pułapki mogą wystąpić z powodu błędów w programach (na przykład dzielenie przez zero) czy też błędów systemowych (na przykład błąd strony). Proces jest informowany o powstałym problemie albo za pomocą sygnału, albo przez sam fakt swojego zakończenia.

IW 1

,lądro 9 %

Przerwania spowodowane przez urządzenia wejścia/wyjścia są obsługiwane przez moduły obsługi urządzeń (ang. device drivers). Moduły te są włączone do jądra podczas instalacji lub rozbudowy systemu. Przerwania spowodowane przez inne urządzenia są obsługiwane przez programy przerwań stanowiące standardową część jądra. Programy te obsługują między innymi styk z konsolą operatorską i zegar czasu rzeczywistego.

Przerwania spowodowane przez urządzenia występują asynchronicznie. Programy obsługi urządzeń są uruchamiane na żądanie. Kiedy występuje przerwanie, moduł obsługi urządzenia odwołuje się do tzw. stosu przerwań, znajdującego się w części stosu jądra danego procesu. Program obsługi przerwań musi być wykonany do końca, to znaczy jego wykonywanie nie może być przerwane.

~, 6.3

Zarządzanie procesami

Charakterystyka ogólna

i Omommy obecnie lalka elementów charakteryzujących procesy

zarządzanie nimi. Będą to takie zagadnienia, jak obraz procesu w pamięci operacyjnej, priorytet procesu czy efektywny identyfikator procesu. Na pierwszy rzut oka może się wydawać, że niektóre z tych zagadnień są dość lużno ze sobą powiązane i przynajmniej w początkowej części rozdział niniejsry jest z tego powodu nieco chaotyczny. Jednakże omówienie tych zagadnień właśnie w tym miejscu i w takiej postaci wydaje się o tyle zasadne, że bez tych wiadomości charakterystyka LTNIX-owych procesów byłaby niestety niepełna.

W systemie LJNIX rozróżniamy dwa typy procesów: jądra i użytkowe. Procesy jądra funkcjonują całkowicie wewnątrz jądra. Przykładami takich procesów mogą być: w systemie UNIX System V - swapper, natomiast w systemie Berkeley IJNIX - swapper i pagedaemorr. Procesy użytkowe to wszystkie pozostałe procesy, tzn. utworzony przez proces systemowy irrit i wszystkie procesy z niego wyprowadzone. Proces init wykonuje funkcje administracyjne w systemie, obejmujące obsługę prawidłowego przejścia systemu z trybu jednego użytkownika do trybu wielodostępnego i utworzenie procesu rejestracji (getry) użytkowników. Przejście do trybu wielodostępnego odbywa się zgodnie ze skryptem . elc-rc i obejmuje następujące czynności: ustawienie daty, sprawdzenie systemu plików, usunięcie plików tymczasowych oraz zainicjowanie programów szeregujących operacje wydruku (mechanizm spooler), a także programów prowadzenia "księgowości" systemu i regularnego zapisywania bloków buforowanych wewnętrznie przez system na dysk.

Podstawą uruchomienia procesu w systemie jest umieszczenie go w pamięci operacyjnej oraz przydzielenie mu procesora. LJNIX, tak jak każdy system wielozadaniowy, stwarza iluzję współbieżnego wykonywania wielu procesów. Jądro systemu określa porządek przydziah~ zasobów do procesów gotowych do wykonania ­mówimy o szeregowaniu zasobów systemu. Jądro wykonuje również szeregowanie procesów, tzn. dokonuje ustalenia kolejności, w jakiej procesor ma być przydzielony do

98 aaZ~rzaat r.

procesów gotowych do wykonania. Zastosowany mechanizm szeregowania procesów preferuje procesy interakcyjne, które nie wymagają dużo czasu procesora. Najczęściej w systemie LJNIX jest wykorzystywany algorytm Round Robin, lecz w porównaniu ze standardową wersją tego algorytmu prrydzielane procesom kwanty czasu są małe.

Każdy proces ma określony priorytet, prry czym obowiązuje zasada: im większa liczba, tym niższy priorytet. Procesy systemowe wykonujące operacje wejścia/wyjścia oraz inne ważne zadania systemowe mają priorytety o wartościach ujemnych i nie mogą być usunięte przez sygnały. Zwyczajne procesy użytkowników mają priorytety dodatnie, tak więc nie mogą być wykonywane przed systemowymi. Stosowany w systemie UNIX algorytm szeregowania to wielokolejkowy algorytm priorytetowy (rys. 6.2).

Najniższy priorytet

_2 -3 -4

Najwyższy priorytet

Proces użytkownika - priorytet 3 Proces użytkownika - priorytet 2 Proces użytkownika - priorytet 1 Proces użytkownika - priorytet 0 Proces oczekujacy na wykonanie przez proces potomny funkcji exit

Proces obsługujacyterminal Proces obsługujący bufor sterownika dysku

Proces oczekujący na wykonanie operacji dyskowych

Kolejka procesów z priorytetem 1

U Procesy oczekującc - tryb uytkownika Procesy oczekujące - tryb j ądra

Rys. 6.2. Algorytm szeregowania w systemie UNIX

Im więcej czasu procesora dany proces zużywa, tym niższy staje się jego priorytet, to znaczy, że w algorytmie szeregowania procesów jest wykorzystywane negatywne sprzężenie zwrotne. We współczesnych systemach kwant czasu procesora przydzielany procesowi to najczęściej O,I sekundy (np. w systemie 4.3BSD UNIX), natomiast co sekundę system przelicza na nowo wartości priorytetów, według następującej reguły:

nowy priorytet = wartość bazowa + wykorzystanie procesora.

Licznik wykorzystania procesora jest zwiększany o 1 z każdym taktem zegara systemowego i jest przechowywany w tak zwanej tablicy procesu. Przekroczenie określonej wartości powoduje przesunięcie procesu do kolejki o niższym priorytecie. Obliczanie nowych wartości priorytetów rozpoczyna się od podzielenia liczników wykorzystania procesora przez 2; tym sposobem procesy ostatnio wykorzystujące procesor nie są za to "karane". Wartość bazowa jest zazwyczaj równa zero. Użytkownik, który pragnie obniżyć priorytet swoich procesów, może tę wartość zwiększyć

Jqdro

99

wykorzystując w tym celu funkcję systemową nice. Tylko administrator systemu może obniżyć wartość bazową zwiększając tym samym priorytet swoich procesów.

W ramach określonej kolejki do szereggwania procesów jest wykorzystywany algorytm Round Robin. Współpracuje on z systemową procedurą timeor~t, która przekazuje modułowi obsługi przerwań zegara informację o tym, że upłynął określony kwant czasu. Odpowiednia procedura jądra dokonuje przeszeregowania procesów. Proces traci prrydział procesora, gdy skońcry się kwant czasu lub gdy oczekuje na zakończenie operacji wejścia/wyjścia. Proces może również zrezygnować z przydziału procesora w efekcie zajścia określonego zdarzenia (ang. event). Do obsługi takich sytuacji wykorzystywane są funkcje systemowe sleep i wakeup, z dwoma argumentami: evezzt i priority.

Jak już wspomnieliśmy wcześniej, każdy użytkownik jest identyfikowany przez liczbę całkowitą zwaną identyfikatorem użytkownika (ang. user identifrcation, w skrócie uid). Identyfikator przydzielany jest użytkownikowi przez administratora systemu i możemy znaleźć go w wierszu odpowiadającym nazwie danego użytkownika w pliku /etc/passwd. Każdy proces ma automatycznie przydzielony identyfikator użytkownika, który go utworzył. Można to sprawdzić wydając polecenie ps -ef.

Użytkownik z identyfikatorem równym zero jest użytkownikiem specjalnym, zwanym użytkownikiem nadrzędnym (ang. ront user albo .superu.ser) albo administratorem systemu. Użytkownik ten ma prawo do odczytu i zapisu wszystkich plików w systemie, niezależnie od tego, kto jest ich właścicielem i jak są zabezpieczone. Procesy z identyfikatorem uid równym zero mają możliwość wykonywania niewielkiej liczby chronionych funkcji systemowych niedostępnych dla zwykłych użytkowników.

Niektóre programy realizujące określone polecenia powinny być dostępne nie tylko administratorowi, ale również zwykłym użytkownikom systemu. Ponieważ, ze względu na wymogi bezpieczeństwa systemu, administrator nie może udostępnić hasła do systemu innym użytkownikom, wprowadzono mechanizm udostępniający te programy. Mechanizm ten polega na zastąpieniu niektórych bitów określających prawa dostępu do pliku zawierającego program realizujący dane polecenie przez tak zwane bity .setzud. Bity te, oznaczane przez s, zastępują wybrane bity x zezwalające na wykonanie programu, a ich obecność oznacza, że efektywnym identyfikatorem użytkownika (ang. effective nic!) danego procesu nie jest identyfikator użytkownika, który ten proces uruchomił, ale identyfikator użytkownika, który jest właścicielem pliku zawierającego wykonywany program. W ten sposób administrator systemu umożliwia w kontrolowany sposób dostęp zwykłym użytkownikom do pewnych informacji o systemie, na przykład informacji o wolnym obszarze na twardym dysku uzyskiwanej za pomocą polecenia df -v.

Każdy proces dysponuje wydzielonym obszarem pamięci, w którym zawarta jest jego wirtualna przestrzeń adresowa oraz obszary pomocnicze, z których korzysta jądro do zarządzania pracą procesów (rys. 6.3).

W wirtualnej przestrzeni adresowej procesu wyróżnia się cztery segmenty segment tekstu progranzzr, segment dar:ych, .segment sto.s~z! i tzw. .szczelizzę[14].

Segment tekstu programu rozpoczyna się od adresu 0 w wirtualnej przestrzeni adresowej. W segmencie tym nie można niczego zapisywać, można go tylko odczytać Jest on dzielony między różne procesy wykonujące ten sam kod.

W drugim segmencie, znajdującym się za tekstem programu, mieszczą się dane użytkownika. W tym obszarze proces może je zarówno zapisywać, jak i odczytywać.

Rozdzia! 6.

Stos jadra Obszar

pomocniczy Struktura użytkownika Obszar przeznaczony giok sterujacy uzYtkownika dla

użytkownika ~ Wektory środowiska i

argumentów

Stos użytkownika Wirtualna

przestrzeń Dane adresowa

procesu Szczelina

Tekst programu

Rys. 6.3. Obraz procesu w pamięci operacyjnej

W trzecim fragmencie pamięci przechowuje się stos. Rozpoczyna się on od największego adresu w pamięci wirtualnej procesu. W miarę zapełniania stosu jego wierzchołek przesuwa się w kierunku mniejszych adresów. Jeśli stos próbuje przekroczyć granicę segmentu stosu, pojawia się przerwanie sprzętowe. Odpowiednie procedury jądra systemu powiększają rozmiar segmentu stosu obniżając jego granicę o kilka tysięcy bajtów (najczęściej o rozmiar strony). Programy nie mają możliwości zmiany rozmiaru segmentu stosu.

Gdy program rozpoczyna pracę, na stos są wpisywane zmienne charakteryzujące środowisko pracy programu oraz samo polecenie i jego argumenty.

Jeśli kilku użytkowników w danej chwili wykorzystuje ten sam program (na przykład edytor tekstu), to w pamięci operacyjnej jest przechowywana tylko jedna kopia programu, a nie kilka (po jednej dla każdego użytkownika), jak to się zdarza w innych systemach. Jest to możliwe dzięki mechanizmowi współdzielonego segmentu tekstu programu (rys. 6.4).

Zazwyczaj segment danych i stosu danego programu współdzielą tę samą przestrzeń adresową, lecz rozrastają się niezależnie w przeciwnych kierunkach (rys. 6.3). Pomiędzy nimi znajduje się obszar nie wykorzystanej przestrzeni adresowej, tzw. szczelina. Segment stosu rozrasta się automatycznie, natomiast "ekspansja" segmentu danych jest wykonywana z wykorzystaniem funkcji systemowej brk. Ma ona jeden parametr podający adres końca segmentu danych. Adres ten może być większy od aktualnej wartości (segment rośnie) lub mniejszy (segment maleje), ale zawsze musi być mniejszy od aktualnej wartości wskaźnika stosu. Zamiast funkcji systemowej brk można również wykorrystywać funkcję sbrk - jej parametrem jest liczba bajtów, które powinny być dodane do segmentu danych.

Jadro

gal

Proces A ~/ Stos ~ Proces B

Stos

Stos

Stos

Obszar nie wykorry

stany \ Dane

Dane I > >I D~e ~ l Dane

Tekst 1 y Tekst V 1 Tekst

Pamięć fizyczna

System operacyjny

Rys. 6.4. Wirtualna przestrceń adresowa procesów i pamięć fizyczna [14]

Nad wirtualną przestrzenią adresową procesu znajduje się tzw. obszar użytkownika, zawierający informacje o stanie procesu. Są to unikalne identyfikatory związane z procesem, wskaźniki maksymalnego i bieżącego wykorzystania zasobów oraz informacje o prawach i przywilejach związanych z procesem. W obszarze tym jest również przechowywany opis zdarzeń zewnętrznych dotyczących procesu. Obszar ten zawiera wszystkie deskryptory związane z procesem.

Czasami w ramach obszaru użytkownika wyróżnia się strukturę użytkownika i blok sterujqcy. W strukturze użytkownika (ang. user structure) przechowywane są informacje o procesie potrzebne tylko wtedy, gdy znajduje się on w pamięci operacyjnej. Przechowywane są tutaj identyfikator użytkownika, który uruchomił proces, oraz identyfikator jego grupy. Swoje dane przechowują tutaj sygnały i programy obsługi budżetu. Pamiętany jest tutaj również katalog bieżący i tablica otwartych plików procesu. W bloku kontrolnym są przechowywane aktualne wartości rejestrów systemowych, wskaźnik stosu, licznik rozkazów oraz rejestr bazowy tablicy odwzorowania stron.

Informację o procesie dopełniają wektory środowiska i argumentów procesu, zawierające wartości parametrów funkcji systemowych i wartości zwracane przez te funkcje.

Ogólnie procesy są wykonywane w dwóch trybach: jądra i użytkownika. Wspomniane procesy jądra są wykonywane tylko w trybie jądra, natomiast procesy użytkownika mogą być wykonywane zarówno w trybie użytkownika, jak i w trybie jądra. Procesy użytkownika mają dostęp do większości usług jądra za pomocą funkcji systemowych. Wywołanie przez proces użytkownika funkcji systemowej powoduje uruchomienie procesu systemowego lub inaczej procesu jądra, który obsługuje wykonanie tej funkcji. Proces jądra wykorzystuje stos różny od stosu procesu użytkownika. Stos jądra (ang. kernel stack) znajduje się zaraz za strukturą uźytkownika. Stos jądra i

Rozdziaf 6.

struktuca uŻycko.~.~nka t..,orzą razem systemowy segment danych (ang. system data segment) (rys. 6.5).

Struktura Stosjądra Struktura użytkownika

procesu

Systemowy segment danych

Struktura ~ Stos tekstu pro~ramrl~ I 1 1~

Dane

Tablice rezydujace w I I Tekst pamięci operacyjnej

Obszar użytkownika

Elementy obrazu procesu wymiatane na dysk

Rys. 6,5. Bloki kontrolne dla zaraądzania procesami.

Do zarządzania procesami jądro wykorzystuje róźnego rodzaju bloki kontrolne. Są to następujące struktury danych: struktura procesu, struktura tekstu programu (czasami są one traktowane łącznie jako tablica procesu), tablice odwzorowania stron i omówiona wcześniej struktura użytkownika. Pierwsze dwie struktury pozostają w pamięci podczas realizacji procesu, pozostałe są potrzebne jedynie, gdy proces jest właśnie uruchamiany (choć czasami również podczas jego realizacji).

Jak wiemy, proces jest wykonywany albo w trybie użytkownika, albo w trybie jądra. Kiedy proces wykonywany w trybie użytkownika żąda zasobów od systemu operacyjnego za pomocą funkcji systemowej, to następuje zmiana jego trybu na tryb jądra. Każdy tryb pracy wymaga dostępu do różnych zasobów. W trybie użytkownika są to rejestry procesora, licznik rozkazów, wskaźnik stosu oraz zawartość segmentów pamięci tworzących program, to znacry tekst programu, dane i segment stosu. Zasoby te tworzą tak zwany kontekst procesu. W trybie jądra kontekst procesu tworzą rejestry procesora, licznik rozkazów i wskaźnik stosu oraz zasoby wymagane przez jądro, aby mogło ono wykonać określone usługi dla procesu.

Innymi słowy, kontekst procesu tworzą:

na poziomie użytkownika: zawartość przestrzeni adresowej procesu i jego środowisko uruchomieniowe,

na poziomie jądra: oprócz wymienionych powyżej, parametry szeregowania (jak na przykład priorytet procesu, ostatnio wykorzystany czas procesora, czas, podczas upływu którego proces przebywał ostatnio w stanie zawieszenia) i sterowania zasobami oraz informacja identyfikująca.

~aaro

103

Elementy tworzące kontekst procesu na poziomie jądra są opisane w strukturze procesu. Odwołanie do struktury procesu odbywa się poprzez wskaźnik w strukturze użytkownika (rys. 6.5). Jak już wspomnieliśmy, struktury użytkownika i procesu są wykorzystywane przez jądro systemu do zarządzania procesami. Typowym problemem zarządzania procesami jest zakończenie kwantu czasu procesora przydzielonego do procesu. Praca procesu musi być wówczas zawieszona i tym samym musi być zapamiętany kontekst procesu na poziomie jądra i użytkownika. Kontekst procesu jest zapamiętany właśnie w strukturach użytkownika i procesu. Gdy procesor zostanie ponownie przydzielony do procesu, to jądro musi odtworzyć kontekst procesu sprzed przerwania. I tutaj również wykorzystywane są wspomniane wcześniej struktury danych. Zwróćmy uwagę, że do zapamiętania i odtworzenia kontekstu procesu mogą być również wykorzystywane struktury danych związane z systemem plików - oczywiście jeżeli pliki były jednym z zasobów systemu posiadanym przez proces.

W strukturze procesu jest zawarty wskaźnik do struktury tekstu programu. Są tam pamiętane informacje o tym, ile i jakie procesy wspólnie wykorzystują dany segment tekstu programu, oraz gdzie znajduje się tablica odwzorowania stron programu. Gdy segment programu jest sprowadzany do pamięci operacyjnej, podlega stronicowaniu.

Tablica odwzorowania stron przechowuje informację o odwzorowaniu pomiędzy pamięcią wirtualną procesów a pamięcią fizyczną. W strukturze procesu jest zawarty wskaźnik do tej tablicy, wykorzystywany gdy proces znajduje się w pamięci operacyjnej, oraz adres dyskowy procesu, gdy zostaje on usunięty z pamięci operacyjnej. Zwróćmy uwagę, że każdy z procesów współdzielących segment tekstu programu dysponuje odpowiednimi zapisami we własnej tablicy odwzorowania stron; nie istnieje wspólna taka tablica dla tych procesów.

Stan procesu, opisany w strukturze procesu, może przyjmować następujące wartości:

• SIDL - stan pośredni podczas tworzenia procesu; • SRUN - wykonywany;

• SSLEEP - oczekujący (na zdarzenie);

• SSTOP - zawieszony (proces jest zatrzymany przez sygnał lub proces macierzysty);

• SZOMB - stan pośredni podczas usuwania procesu; proces w tym stanie nazywamy procesem "upiorem" (ang. zombie).

Proces po utworzeniu znajduje się w stanie SIDL. Stan ten ulega zmianie na SRL1N, gdy zostaną przydzielone wystarczające zasoby do rozpoczęcia wykonywania procesu. Od tej chwili stan procesu oscyluje między SRUN, SSLEEP i SSTOP, dopóki proces się nie zakończy. Proces zakończony znajduje się w stanie SZOMB do czasu powiadomienia procesu macierzystego o tym fakcie (rys 6.6).

Struktury procesów znajdujących się w stanie oczekiwania są połączone ze sobą w formie podwójnej listy łańcuchowej, przy czym jeden z łączników pokazuje powiązania pomiędzy procesami macierzystymi i potomnymi.

J i~~i Rozdział 6.

przydziel

Rys. 6.6. Stany procesu

Użytkownik może tworzyć procesy, sterować ich wykonaniem i odbierać informację o zmianie statusu wykonywania. Każdy proces ma prrydzieloną unikalną wartość, zwaną identyfikatorem procesu (PID). Wartość ta jest używana przez jądro do identyfikacji procesu oraz przez użytkownika do wywolywania funkcji systemowej.

Jądro tworzy proces przez powielenie kontekstu innego procesu. Ten nowy proces jest zwany procesem potomnym oryginalnego procesu macierzystego z identyfikatorem PPID (ang. Pareru Process Identificatiort). Kontekst powielony przy tworzeniu procesu zawiera zarówno stan wykonania procesu na poziomie użytkownika, jak i jądra.

Współistnienie procesów można zilustrować na przykładzie procesu powłoki, interpretującego polecenia użytkownika. Proces ten w chwili odczytania polecenia z klawiatury tworzy proces potomny odpowiedzialny za realizację tego polecenia. Standardowo proces macierzysty (czyli powłoka) oczekuje na zakończenie procesu potomnego i dopiero wówczas pobiera kolejne polecenie. Użytkownik może spowodować, aby dane polecenie było wykonywane w tle. Wtedy proces powłoki, nie czekając na zakończenie danego procesu potomnego, pobiera nowe polecenie i tworzy dla niego nowy proces. Czynności te są wykonane za pomocą omówionych dalej funkcji systemowych fork, exec, exit oraz wait.

Tworzenie, usuwanie i zawieszanie procesu

Jak już wspomnieliśmy, proces może utworzyć nowy proces, który jest kopią oryginału, za pomocą funkcji systemowej fork. Jej wykonanie polega na rozwidleniu procesu wydającego to zlecenie na dwa procesy: nracierysty i potonrrry, które mogą od tej pory pracować współbieżnie, niezależnie od siebie. Dla procesu potomnego jest inicjowany oddzielny obszar pamięci operacyjnej.

W języku C zlecenie utworzenia nowego procesu możemy zapisać następująco:

pid = fork~;

gdzie po zakończeniu wykonywania zlecenia zmienna pid w procesie macierrystym przybiera wartość będącą identyfikatorem procesu potomnego (w przypadku niemożności utworzenia procesu - wartość -1), a w procesie potomnym wartość równą 0. Innymi

0x01 graphic

Jqdro I ~.5

słowy, funkcja fork przekazuje do procesu pewne parametry powrotne, raz w procesie macierzystym - gdzie wartością zwracaną jest identyfikator procesu potomnego, raz w procesie potomnym - gdzie wartością zwracaną jest 0. Zwróćmy uwagę, że właśnie relacja macierzysty - potomny wymusza hierarchiczną strukturę zbioru procesów w systemie. Nowy proces korzysta ze wszystkich zasobów swojego przodka, takich jak na przykład pliki, status obsługi sygnału i mapa pamięci.

Ilustracją zagadnień związanych z utworzeniem procesu jest poniższy przykład. Zarówno w tym, jak i w innych przykładach z tego rozdziału przedstawiamy programy napisane w języku C [S]. Ze względu na fakt, że Czytelnik może nie znać tego języka, dołączamy, przynajmniej na początku, dokładne komentarze objaśniające znaczenie poszczególnych instrukcji programu.

Przykład 6.1

Przeanalizujmy działanie następującego programu:

#include <stdio.h> l* dolaczenie opisu funkcji */ /* wejscialwyjscia z pliku naglowkowego */

main() I* rozpoczecie dzialania programu *l int pid; !" deklaracja zmiennej calkowitej *1 if {(pid = fork()) _= 0) !* utworzenie procesu *! printf("proces potomnyln"); !* zmienna pid = 0 *!

!*funkcja fork przekazuje zero do procesu potomnego */ !* wyswietlenie napisu "proces potomny" - jest to *l

/* efekt działania kodu procesu potomnego *l else

printf("proces macierzystyln"); I' zmienna pid $ 0 *l l*funkcja fork przekazuje identyfikator procesu potomnego *!

J* do procesu macierzystego, wyswietlenie "proces macierzysty " *! i

W wyniku wykonania tego programu na ekranie terminalu otrzymamy:

proces potomny proces macierzysty

Program ten tworzy nowy proces za pomocą funkcji systemowej fork. Nowo utworzony proces (proces potomny) otrzymuje zero jako parametr zwracany przez tę funkcję i wyświetla napis "proces potomń~". Proces wywołujący (proces macierzysty) otrzymuje identyfikator nowo utworzonego procesu jako parametr zwracany przez tę funkcję i wyświetla napis "proces macierzysty". Wyraźnie widać, że program ten dokonuje rozwidlenia procesu na dwa procesy. Każdą linię na ekranie wyświetla inny proces.

.,;.:~...

Wartość parametru zwracanego przez funkcję fork zostaje zachowana w zmiennej pid. Zauważmy, że wartość przekazywana przez funkcję fork do procesu macierzystego, będaca identyfikatorem procesu potomnego, jest wartością dodatnią Program powyższy

I 06 noZazrat 6.

powinien uwzględniać również przypadek przekazywania wartości -I, oznaczający fakt, że nie można w ogóle wykonać funkcji systemowej fork. Przypadek ten przedyskutujemy w jednym z następnych prrykładów.

Zwykle, kiedy proces jest tworzony, ma on wykonywać program róiny od wykonywanego przez jego proces macierzysty (rys 6,7).

Rodzina funkcji systemowych

exec (plik, arg1, ..., argn)

umożliwia procesowi wykonanie kodu zawartego we wskazanym pliku, przy czym plik een zawiera polecenia UNIX-a lub program w postaci binarnej, natomiast argl, ,.., argn są wartościami parametrów tego polecenia. Jego realizacja polega na wprowadzeniu w obszar pamięci operacyjnej procesu nowego kodu i danych. Następną instrukcją procesu jest wówczas pierwsza instrukcja wczytanego kodu. Tak więc proces potomny mający wykonać program zawarty w pliku np. alfa powinien wykonać zlecenie exec. Oczywiście nie może być ono wydane z procesu macierzystego, czyli typowy zapis tworzenia nowego procesu może być następujący (exec oznacza tu jedną z funkcji z rodziny exec):

if ( (pid = forkQ) _= 0 ) exec ("alfa", NULL);

wait

fork

exec IIAllllll~u~ exit

Rys. 6.7. Rozwidlanie procesów

Rodzina funkcji systemowych exec obejmuje funkcje: execl, execle, execlp, execv, execve i execvp. Wywołania tych funkcji różnią się określeniem ścieiki dostępu do pliku wykonywanego przez proces potomny, sposobem przesyłania argumentów i przekazywaniem parametrów środowiska od procesu macierzystego do procesu potomnego.

Wykorzystanie ścieżki dostępu dotycry zmiennej PATH w poleceniu env. Sposób przesyłania argumentów do procesu potomnego może być dwojaki: za pomocą listy lub tablicy argumentów. W przypadku użycia listy argumenty przesyłane do procesu potomnego są umieszczone jako oddzielne argumenty funkcji systemowej exec. W przypadku użycia tablicy argumenty są zapamiętywane w tablicy, a do procesu potomnego trafia wskaźnik do tablicy. Metoda z listą argumentów jest stosowana, gdy liczba argumentów jest stała albo znana w czasie kompilacji. Metodę z tablicą argumentów stosujemy, gdy liczba argumentów musi być określona dopiero podczas uruchomienia. Przekazywanie parametrów środowiska od procesu macierzystego do procesu potomne;o odbywa się w dwojaki sposób: albo proces potomny "dziedziczy

Jadro

107

środowisko" od procesu macierzystego, albo wskaźnik do tablicy środowiska jest przekazywany jako ostatni argument. W tym ostatnim przypadku środowisko procesu potomnego jest różne od środowiska procesu macierrystego.

Tabela 6.1 przedstawia różnice między funkcjami systemowymi z rodziny exec. Funkcje exec przekazują sterowanie do programu pierwotnego tylko wtedy, gdy wystąpi błąd podczas próby odszukania danego programu w katalogach, taki jak źle podana nazwa ścieżki albo brak zezwolenia na wykonanie. Program pierwotny wyświetla stosowny komunikat o wykrytym błędzie.

Tabela 6.1

Postacie funkcji systemowych exec

Funkcja Użycie PAT'>EI Priekazanie Srodowisko

a umentów

execl nie u' a lista ar mentów dziedziczone

execle nie u' a Lista ar mentów nie dziedziczone

execl u' a lista ar mentów dziedziczone

execv nie u' a tablica ar mentów dziedziczone

execve nie u' a tablica ar mentów nie dziedziczone

execvp ~ _ tablica argumentów dziedziczone

używa

Zwróćmy uwagę, że funkcje exec nie przekazują znaczenia metaznaków (<,>,?, i []) podanych w liście argumentów. Jeżeli program tego wymaga, należy użyć funkcji systemowej exec do wywołania programu powłoki i dalej on sam wykonuje wymagane polecenie.

Przykład 6.2 #include <stdio.h>

main() l* proces4.c, uruchomienie nowego */ I* programu funkcja execl *l

!* wykonanie programu przez powloke *!

char cmd[80]; I* tablica cmd ma 80 znakow *J gets(cmd); J* czytanie lancucha znakow do tablicy cmd *l if (fork() _= 0)

{ I* proces potomny "! printf("proces potomnyln"); I* wyswietlenie napisu */ execl("llaintsh", "sh", "-c", cmd, NULL);

J* wykonanie polecenia wpisanego do tablicy cmd *l

else printf("proces macierzystyln"); i

Po uruchomieniu programu i otrzymamy następujący obraz;

l* wyswietlenie napisu "l

wprowadzeniu polecenia date na ekranie terminalu

date proces potomny

1 ~Ó Rozdziaf 6.

proces macierzysty

$ Wed Jan 22 11:16:55 CEU 1992

Zauważmy, że w wywołaniu funkcji execl pełna ścieżka "/bin/sh" i nazwa polecenia "sh" uruchamiają powłokę, argument "-c" powoduje, że powłoka traktuje łańcuch znaków podany w "cmd" jako jedno polecenie, a NIJLL jest wartością wskaźnika zera kończącego listę parametrów funkcji. Proces potomny wyświetlający napis "proces potomny" i datę wykonywany jest na przemian z procesem macierzystym wyświetlającym napis "proces macierzysty" .

Ponownie uruchamiając ten program i wprowadzając potok Is -la ~ pg, otrzymamy na ekranie terminalu przykładowo obraz:

$Is-al~pg proces potomny proces macierzysty $ total 502

drwi------ 5 sajkowsk adm 1296 May 25 10:50 drwxrwxr-x 22 root auth 352 May 15 15:08 .. -rw------- 1 sajkowsk adm 539 Mar 23 14:56 .profile

Następnie kontynuowana jest lista plików i katalogów z bieżącego katalogu z podziałem na strony. Wyrażnie widać, że łańcuch znaków podany w tablicy cmd jest traktowany jako jedno polecenie.

Funkcja wait jest zwykle używana do synchronizacji wykonania procesu macierzystego i procesu potomnego i okazuje się szczególnie użyteczna, gdy oba procesy mają dostęp do tych samych plików. Mianowicie, proces może zawiesić swoje działanie, dopóki któryś z jego procesów potomnych nie zakończy się. Należy w tym celu wykorzystać funkcję systemową wait, na przykład w następujący sposób:

pid = wait(&status)

gdzie &status jest parametrem (wskaźnikiem do zmiennej) określającym rodzaj zdarzenia, które spowoduje wznowienie działania procesu, a pid -identyfikatorem procesu, który spowodował to zdarzenie.

Proces macierzysty za pomocą sygnału może żądać informacji, kiedy nastąpi wyjście z procesu potomnego lub kiedy kończy się on nieprawidłowo. Jeżeli proces został osierocony, tzn. nastąpiło wyjście z procesu macierzystego, zanim on sam się zakończył, to wtedy w celu obsługi wyjścia z niego jądro kontaktuje się z procesem systemowym init.

Przykład 6.3

Przeanalizujmy działanie poniższego programu. Różni się on od poprzedniego tylko wprowadzeniem instrukcji czekania na zakończenie procesu potomnego.

#include <stdio.h>

main() /* proces6.c, czekanie na proces */ int status; J* zmienna calkowita zawierajaca status */

jadro _ 109

char cmd[80]; 1* tablica cmd ma rozmiar 80 znakow "/

gets(cmd); /* czytanie lancucha znakow do tablicy cmd */

if (fork() _= 0) /* utworzenie procesu x/

/* proces potomny "/

printf(,'proces potomnyln"); /" wyswietlenie napisu "/

execl('/bin/sh ", "sh", "-c", cmd, NULL);

/* wykonanie połecenia wpisanego do tablicy cmd "/

i

wait(&status); /" czekanie na koniec procesu potomnego */

printf("proces macierzystyln"); /" wyswietlenie napisu */

Tym razem w wyniku wykonania polecenia date na ekranie terminalu otrrymamy obraz wyglądający na przykład tak:

date proces potomny

Thu Jan 23 11:53:01 CEU 1992 proces macierzysty

Zauważmy, że proces macierzysty czeka, aż zakończy się wykonywanie procesu

potomnego.

Argumentem funkcji wait jest zawsze wartość statusu. Wartość statusu okresa~~~ IJWd bajty.' mJOdszy zawrera status zakończenia procesu potomnego, tak ja& to zdefiniował system; starszy - status zakończenia procesu potomnego, tak jak to zdefiniował sam proces potomny. Jeżeli proces potomny został zawieszony, wartość młodszego bajtu wynosi 177, a starsry zawiera numer sygnału zawieszającego proces. Jeżeli proces potomny został zakończony funkcji exit, mtodszy bajt jest zezem, a starszy zawiera młodszy bajt argumentu funkcji exit. Jeżeli proces potomny został zakońCZOny przez sygnał, to młodszy bajt zawiera numer sygnału, a starszy jest zerem [1, 6].

Proces może zakończyć swoje działanie operacją exit(status). Powoduje ona zakończenie wykonywania procesu, zamknięcie plików i skasowanie obrazu pamięci procesu. Wartość parametru status jest przekazywana do procesu przodka, jeżeli wykonał on operację wait(&starus). Proces może również bym zakończony przymusowo w rezultacie wystąpienia sygnału. W obu przypadkach zakończenie procesu powoduje przekazanie kodu statusu do procesu macierzystego (jeżeli on jeszcze islrrieje) za pośt'edntCftt'em funkcji systemowej ii"d%t .Jednym z zadań funkcp wait fiest wtaśnie śledzenie, CZy procesy potomne dahe~.- Q~o=-~~~ z~~~~y zakoóc-s_o~e..

Przykład 6.4

Przeanalizu. jmy nast~~~~~3Lypf~~fa~

#include <stdio.h>

main() /~ proces7.c, czekanie na proces *l

Rozdziaf 6.

int status; I* zmienna calkowita zawierajaca status "/ int pid; 1* zmienna calkowita zawierajaca */

!* identyfikator procesu pid */ if ((pid = fork()) __ -1)

i

perror("nie mogę wykonać fork");

exit(1); / * nie utworzono procesu potomnego */ l* przekazanie statusu 1 do procesu macierzystego */ ł

else if (pid == 0) I* utworzenie procesu "/ l* proces potomny *! J* wyswietlenie napisu *! printf("potomny: pid potomnego = %d,

pid macierzystego = °l°d1n", getpid(), getppidQ); l* getpid ustala numer procesu zapytujacego (tu potomnego) *!

f* getppid ustala numer przodka procesu zapytujacego *l exit(0); I* koniec procesu potomnego "I /* przekazanie statusu 0 do procesu macierzystego *l i

wait(&status); /* czekanie na koniec procesu potomnego *! printf("macierzysty: status potomka = %oln", status);

f /* proces macierzysty *J I* wyswietlenie napisu *i printf("macierzysty: pid potomnego = %d,

pid macierzystego = °l°d1n", pid, getpid());

*getpid ustala numer procesu zapytajacego (tu macierzystego)"/ exit(0); /~ zakonczenie procesu macierzystego */ )

W wyniku wykonania programu na ekranie otrzymamy obraz wyglądający na przykład tak:

potomny: pid potomnego = 1502, pid macierzystego = 1501 macierzysty: status potomka = 0

macierzysty: pid potomnego = 1502, pid macierzystego = 1501

Zauważmy, że wartości identyfikatorów procesu macierzystego i procesu potomnego, podane osobno przez oba procesy, są zgodne. Status procesu potomnego jest zerem, gdyż proces potomny został zakończony za pomocą funkcji exit i argument tej funkcji jest zerem.

° 111

Polecenia dotycz~ce procesów

Oprócz funkcji systemowych użytkownik ma do dyspozycji także kilka poleceń bezpośrednio operujących procesami. Niektóre z nich, jak na przykład: ps, kill i sleep, zostały omówione w rozdziale 5. Obecnie omówimy pozostałe z tych poleceń.

Polecenie nice daje użytkownikom mOżltwośĆ wpływania na szeregowanie procesów. Procesy są szeregowane do wykonania zgodnie z parametrem priorytet procesu. Wartość, o którą może być zwiększony priorytet procesu, określa parametr nice (standardowo wynosi on 10, a maksymalnie 20). Priorytet procesu oraz wartość parametru nice można poznać wykonując wspomniane polecenie ps -l i odczytując wartości w kolumnach PRI i NI. Polecenie nice umożliwia zmniejszenie priorytetu procesu w celu uniknięcia konfliktu z wykonywaniem innych poleceń z tego samego terminalu, np.

$nice cp plik a plik_b&

spowoduje zmniejszenie o 10 priorytetu procesu odpowiadaja~cego poleceniu cp wykonywanemu w tle.

Polecenie at powoduje zainicjowanie w określonym czasie procesu odpowiadającego danemu poleceniu lub plikowi poleceń. Użytkownik może zażądać, aby jądro systemu zainicjowało pewną ustalona akcję w określonym czasie i terminie. Przykładowo, można zażądać wypisania komunikatu na terminalu użytkownika po upływie pół godziny lub usunięcia nie używanych plików w końcu miesiąca. W każdym przypadku należy skorzystać z polecenia at w następującej postaci:

at czas data <plik

przy czym plik zawiera procedurę powłoki, która ma być przekazana powłoce we wskazanym czasie i wykonana.

Opcja czas może być podana jako sekwencja jedno-, dwu- lub czterocyfrowa. Sekwencje jedno- i dwucyfrowe traktowane są jako godziny, czterocyfrowe jako godziny i minuty. Można teź podawać czas w postaci dwu liczb oddzielonych dwukropkiem (godziny:minuty). Można stosować skróty am (przed południem) i pm (po południu). W przeciwnym wypadku zakłada się użycie czasu 24-godzinnego. Ponadto program rozpoznaje określenia takie, jak noon (południe), midnight (północ), now (teraz).

Opcja data jest nieobowiązująca. Może być podana jako nazwa miesiąca wraz z następującym po niej numerem dnia (i ewentualnie numerem roku oddzielonym przecinkiem) lub dniem tygodnia. Dopuszcza się użycie dwu nazw dni: today (dzisiaj) oraz tomorrow (jutro). Jeśli opcja została pominięta, dzień ustalony jest jako bietący.

Nieobowiązkowa opcja +liczba stanowi liczbę użytą wraz z jedną z następujących jednostek: minutes, hours, days, weeks, months lub years (również w liczbie pojedynczej). Oto kilka prrykładów poleceń z prawidłowo określoną datą i czasem:

$ at 0815am Jan 24 $ at now +1 day

$ at 5 pm Friday

W przedstawionej powyżej postaci program at odczytuje ze standardowego urządzenia wejścia polecenia przeznaczone do wykonania w późniejszym terminie.

I12 Rozdziaf G.

Polecenie nohup (ang. no hang up - nie zawieszaj) zapewnia wykonywanie procesu nawet wtedy, gdy użytkownik wyrejestruje się z systemu lub gdy nastąpi przerwanie połączenia telekomunikacyjnego z terminalem, np.:

$ nohup cp plik_1 plik 2&

Sygnały i ich wykorzystywanie

Sygnał jest odpowiedzią jądra systemu na wyjątkowy stan, jaki wystąpił podczas wykonywania programu przez proces użytkownika. Stanem tym może być naciśnięcie klawisza Del albo wykrycie przez system nielegalnej operacji. Sygnał przerywa normalne wykonanie programu i rozpoczyna akcję jego zakończenia albo wyświetla komunikat o błędzie.

W systemie operacyjnym UNIX zdefiniowano zbiór sygnałów dla pewnych stanów oprogramowania i sprzętu, które mogą się pojawić w czasie wykonywania programu. Sygnały mogą być dostarczane do procesu przez inny proces albo przez jądro systemu.

Sygnały najczęściej występujące w systemie UNIX przedstawiono w tabeli 6.2. Podajemy tutaj łącznie sygnały implementowane w UNIX System V i w systemie UNIX 4.3BSD. Dla części sygnałów podano ich wartości (numery) [1]. Sygnały występujące tylko w systemie UNIX System V oznaczono indeksem 1, tylko w systemie UNIX 4.3BSD - indeksem 2, a sygnały występujące w obu systemach nie mają indeksu.

Tabela 6.2

Sygnały w systemie UNIX

Nazwa Wartość O is Akc'a standardowa

SIGHIIP O1 linia terminalu zawieszona zakończ roces

SIGINT 02 rzerwanie ro ramu zakończ roces

SIG UIT 03 'ście z ro ramu utwórz obraz ami ci

SIGILL 04 niele alna instrukc a utwórz obraz ami ci

SIGTRAP 05 nakaz śledzenia rocesu utwórz obraz ami ci

SIGIOT 06 wystąpienie pułapki przy utwórz obraz pamięci

konaniu o erac~i we/

SIGEMT 07 konana instrukcja emulac i utwórz obraz ami ci

SIGFPE 08 błąd przy operacji utwórz obraz pamięci

zmienno rzecinkowe'

SIGKILL 09 zabicie rocesu zakończ roces

SIGBUS 10 bł d ma istrali utwórz obraz ami ci

SIGSEGV 11 naruszenie se mentac i utwórz obraz amięci

SIGSYS 12 zły argument funkcji utwórz obraz pamięci

s stemowe~

SIGALRM 14 upłynął czas zegara czasu zakończ proces

rzecz iste o

SIGTERM 15 programowy sygnał zakończ proces

zakończenia

Jqdro I 13

SIGURGz -- pilny warunek w kanale odrzuć sygnał

we/

SIGSTOP~ -- sygnał zatrzymania spoza zawieś proces

terminalu

SIGSTP~ -- sygnał zatrrymania z zawieś proces

terminalu

SIGCONT~ -- zatrzymany proces jest odrzuć sygnał

kon uowan

SIGCHLDZ -- powiadomienie przodka o odrzuć sygnał

zawieszeniu / usunięciu

otomka

SIGTTINz -- czytanie z terminalu przez zawieś proces

roces dru o lano

SIGXCPUz -- przekroczony limit czasu zakończ proces

'ednostki centralne

SIGWINCH -- zmieniony rozmiar okna odrzuć sygnał

terminalu

SIGUSRl 16 sygnał zdeńniowany przez zakończ proces

u' kownika I

SIGUSR2 17 sygnał zdefiniowany przez zakończ proces

u' kownika 2

SIGCLD 18 usuni cie rocesu otomka odrzuć s nał

SIGPWR~ 19 błąd zasilania zakończ proces

Z każdym sygnałem związana jest akcja podająca sposób jego obsługi. Program użytkownika może w różny sposób reagować na sygnały wykorzystując w tym celu funkcję systemową signal. Reakcje programu mogą być następujące:

• podjęcie akcji standardowej, ignorowanie sygnału,

• podjęcie akcji własnej.

Zestaw akcji standardowych przedstawiono w tabeli 6.2. Akcje najczęściej wykorzystywane przez sygnały to utworzenie obrazu pamięci i zakończenie procesu. Akcja standardowa "utwórz obraz pamięci" oznacza zakończenie procesu z zachowaniem jego stanu, tj. z wygenerowaniem pliku core, który zawiera opis stanu wykonania procesu w chwili, gdy sygnał został dostarczony. Plik ten następnie można analizować za pomocą programu wyszukiwania i analizy błędów (ang. debugger).

Akcję własną definiuje użytkownik w programie obsługi sygnału. Program obsługi sygnału wywoływany jest przez system po odbiorze sygnału przez proces. Mówimy wtedy, że program obsługi przechwyci) lub złapal sygnał.

Przykładami sygnałów, które nie mogą być zignorowane ani złapane, są dwa sygnały: SIGSTOP i SIGKILL. Wykorzystując te sygnały można utworzyć mechanizm programowy zatrzymywania i usuwania (w terminologii systemu UNIX używa się określenia "zabijania") procesów.

Implementacja sygnału jest podzielona na dwa etapy: zgłoszenie sygnału do procesu oraz jego wykonanie. Zgłoszenie sygnału obejmuje: określenie akcji, jaką proces podejmie, dołączenie sygnału do kolejki zawieszonych sygnałów związanych z procesem, sprawdzenie, czy sygnał jest maskowany. Jeżeli sygnał nie jest maskowany, a akcja z nim związana jest akcją standardową, obejmującą zakończenie procesu, zawieszenie procesu

114 Rozdział 6.

albo odrzucenie sygnału, to akcja ta będzie wykonana. Wykonanie sygnału obejmuje: pobranie pierwszego z zawieszonych sygnałów i wywołanie modułu obsługi sygnału w celu wykonania przez proces akcji własnej związanej z sygnałem albo wywołanie procedury utworzenia pliku core, gdy z sygnałem jest związana akcja standardowa.

Podobnie jak w przypadku przerwań, dostarczanie sygnałów może być maskowane przez proces. Stan wykonania każdego procesu zawiera zbiór sygnałów aktualnie maskowanych przy ich dostarczaniu. Jeżeli sygnał zgłoszony do procesu jest maskowany, to zostaje on zapisany w zbiorze zawieszonych sygnałów procesu i żadna akcja nie jesc podejmowana, dopóki maskowanie sygnału nie zostanie usunięte. Zbiór sygnałów maskowanych dla danego procesu jest modyfikowany przez funkcje systemowe sigblock i sigsetmask w systemie I1NIX 4.3BSD [6], a przez funkcję sigprocmask w systemie SCO I1NIX System V [ 1 ]. Standardowa wersja LJNIX System V nie stosuje pojęcia maski sygnałów, a argumentami funkcji systemowych są pojedyncze sygnały [2, 8, 9].

Zauważmy, że system nie pozwala na maskowanie sygnałów SIGKILL, SIGSTOP i SIGCONT. Ochrona SIGCONT przed maskowaniem jest konieczna, ponieważ od jego dostarczenia uzależnione jest podjęcie pracy przez zatrzymane procesy.

Program użytkownika deńniuje akcję, która ma być podjęta w przypadku odbioru sygnału za pomocą funkcji systemowej signal. Aby użyć funkcji signal, należy wpierw umieścić na początku programu w języku C następujący wiersz:

#include <signal.h>

Zbiór signal.h definiuje różne literały (stałe literowe) używane jako argumenty przez funkcję systemową signal. Wywołanie funkcji signal ma postać

signal(sigtype, ptr)

gdzie sigtype jest liczbą całkowitą albo literałem określającym sygnał, którego akcja ma być zdeńniowana, a ptr - wskaźnikiem funkcji deńniującej akcję albo literałem podającym wcześniej zdefiniowaną akcję.

Wskaźnik ptr może być następującym literałem:

• SIG_IGN - brak akcji (ignorowanie sygnału), • SIG_DFL - akcja standardowa.

Na przykład podana tu funkcja zmienia akcję sygnału przerwania na brak akcji:

signal(SIG/NT, SIG IGN);

Sygnał nie będzie miał żadnego wpływu na program. Akcją poprzednią mogła być akcja standardowa oznaczająca zakończenie programu.

Funkcja signal zawsze zwraca wartość wskaźnika, który definiuje poprzednią akcję sygnału. Wartość ta może być zastosowana w kolejnych wywołaniach do przywrócenia sygnałowi jego poprzedniej akcji.

Przykład 6.5 #include <stdio.h>

main() /" signal1.c, uzycie sygnalu "/ /" przerwanie DEL przerywa oba procesy */

int i, j; /" deklaracja licznikow */

Jqdro I I S if (forkQ == 0) /* proces potomny */

for (i = 1; i <= 1000; ++i) /* wyswietla 1000 razy */ printf("proces potomny\n"); /* napis */ else /* proces macierzysty */ for (j = 1; j <= 1000; ++j) /* wyswietla 1000 razy */ printf("proces macierzysty\n"); /* napis */ )

W wyniku uruchomienia programu i następnie naciśnięcia klawisza Del na ekranie terminalu otrzymamy następujące wyniki:

proces potomny procespotomny proces macierzysty proces macierzysty proces-macie

Wyraźnie widzimy, że naciśnięcie Del przerywa oba procesy. Stąd wniosek, że oba procesy nie są chronione przed wpływem wykorrystywanych tutaj sygnałów.

Blokadę (inaczej - zakaz) sygnału, czyli ochronę programu przed sy~nałe.*,.

UZyS1CUJ0 Slę Za pOCriOC~ funkcji svgnal ze stałaSiG IGlV . Wywolania f~nlccji ma postać-.

signal(sigtype, SIG IGN)

gdzie sigtype jest literałem sygnału, który ma być zakazany. Na przykład następująca funkcja systemowa blokuje sygnał przerwania:

signal(SIGINT, SIG IGN);

Funkcję tę można stosować, aby ochronić program wykonywany w tle (na przykład, proces potomny, który nie wykorzystuje terminalu jako urządzenia wejścia/wyjścia) przed zakończeniem go przez sygnał. System przesyła sygnały generowane przez naciśnięcie klawiszy do wszystkich programów, które były wywołane z tego terminalu. Oznacza to, że naciśnięcie klawisza Del w celu zatrzymania programu pierwszoplanowego zatrzyma również program uruchomiony w tle, jeżeli nie wprowadzono blokady sygnału dla tego programu. 3eżeli wprowadzono blokadę, proces potomny będzie wykonywany do końca. Ilustruje to następujący przykład.

Przykład 6.6

W prezentowanym poniżej programie wprowadzono w odpowiednie miejsce funkcję systemową signal - blokującą sygnał przerwania dla procesu potomnego.

#include <stdio.h>

#include <signal.h> /* plik naglowkowy dla funkcji signal */ main() /* signal3.c, Del przerywa proces macierzysty */ i

116

Rozdziat 6.

int i, j;

if (fork() _= 0) f

signal(SIGINT, SIG_IGN);

for(i = 1; i <= 100; ++i)

printf("proces potomny %d\n", i); )

else for (j = i; j <= 200; ++j)

printf("proces macierzysty %d\n", j);

/* deklaracja licznikow */

/* proces potomny */ /* blokada sygnalu */

/* przerwania */ /* wyswietlenie 100 */ /* razy napisu */

/* proces macierzysty */ /* wyswietlenie 200 razy */

/* napisu */

Wykonanie tego programu i wciśnięcie klawisza Del podczas wyświetlania wyników na ekranie powoduje, że możemy otrzymać przykładowo następujący obraz:

proces macierzysty 190 proces macierzysty 191 prproces potomny 86 proces potomny 87

proces potomny 99 proces potomny 100

Widoczne jest, że przerwanie programu klawiszem Del ma tylko wpływ na proces macierzysty, przerywając od 192 drukowanie napisu proces macierzysty. Natomiast proces potomny jest wykonywany bez przeszkód i dochodzi do końcowego, setnego wydruku napisu proces potomny.

Przywrócenia sygnałowi jego akcji standardowej dokonuje się za pomocą funkcji signal ze stałą SIG DFL. Wywołanie funkcji ma wtedy postać:

signal(sigtype, SIG DFL)

idzie si~t~ype jest literałem definiującym sygnał, który chcemy przywrócić. Funkcję tę można stosować na przykład do przywrócenia sygnału po tym, jak był on czasowo zakazany, aby nie przerywał operacji w sekcji krytycznęj. Przykład 6.7 jest ilustracją tych zagadnień, a jednocześnie pokazuje, jak użytkownik może sam, nie zważając na mechanizmy systemowe, zrealizować technikę wzajemnego wykluczania.

Przykład 6.7

#include <signal.h> #include <stdio.h>

#define BUF 5 /* BUF liczba przeczytanych znaków */ main Q /* signal5.c, przywrocenie sygnalowi */ I* akcji standardowej *I

Jądro

FILE *fp; char *rekord; rekord = "Test";

signal (SIGINT, SIG_IGN);

/* deklaracja deskryptora pliku */ /* wskaznik do tekstu */

/* przypisanie zmiennej rekord */ /* wskazania na tekst */ /* poczatek sekcji krytycznej */

/* blokada sygnalu */ /* przerwania */

fp = fopen("/staff/sajkowsk/plik", "w");

/* utworzenie pliku z prawem pisania do niego */ fwrite(rekord, 1, BUF, fp); /* zapis BUF bajtow */ /* z tablicy rekord do pliku podanego w fp */

signal (SIGINT, SIG DFL); /* przywrocenie */ /* sygnalowi akcji standardowej */ /* koniec sekcji krytycznej */

fclose(fp); /* zamkniecie otwartego pliku */

117

W przykładzie tym sygnał przerwania jest ignorowany w czasie wczytywania BL1F bajtów z tablicy rekord do pliku podanego w fp. Po wykonaniu programu w pliku plik pojawi się napis Test. Operacja ta nie jest przerywana.

Przechwycenie sygnału i określenie akcji własnej jest wykonywane z wykorzystaniem odpowiedniej funkcji systemowej, definiującej tę nową akcję. Funkcja ta występuje jako argument funkcji systemowej signal. Jej wywołanie ma wtedy postać:

signal (sigtype, newpt~

gdzie sigtype jest literałem określającym sygnał, który ma być przechwycony, a newptr ­wskaźnikiem funkcji definiującej nową akcję. Dzięki niej, na przykład, program wykonuje pewne dodatkowe operacje przed przerwaniem przez sygnał.

Program może zredefiniować akcję sygnału w dowolnym czasie. Stąd wiele programów definiuje różne akcje dla różnych warunków. Akcja sygnału przerwania może zależeć od wartości zwracanej przez zdefiniowaną funkcję. Ilustruje to następujący przykład.

Przykład 6.8

#include <stdio.h> #include <signal.h>

main() /* signal7.c, zlapanie sygnalu */

int przechwyc(), i = 100; /* deklaracja wartosci funkcji */ /* deklaracja zmiennej i ustalenie jej wartosci poczatkowej */ printf("nacisnij klawisz przerwania aby zatrzymac\n");

/* wyswietlenie napisu */ signal(SIGINT, przechwyc); /* wywolanie funkcji */ /* definiujacej nowa akcje `I

wKv\ey > ~~ 1~ dopoW spe\W ony warunek "I

118

Rozdzial 6.

f I" wyswietlaj napis *t printf("program sie wykonuje °l°dln", i);

I" zmniejsz i o jeden "l )

) przechwyt() l" funkcja definiuje nowa akcje podjeta 'l printf("program sie zakonczylln"); i" Po wystapieniu ~Ygap ś exit(1); I" zakonczenie procesu *I i

W efekcie wykonania tego programu i naciśnięcia klawisza Del otrzymamy następujące wyniki:

nacisnij klawisz przerwania aby zatrzymac program sie wykonuje 100

program sie wykonuje 91

program sie wykonprogram sie zakonczyl

Zauważmy, że przerwanie programu nie kończy natychmiast jego wykonywania, jak to jest w prrypadku podjęcia akcji standardowej po wystąpieniu sygnału SIGINT, ale prowadzi do podjęcia akcji własnej, polegającej na wyświetleniu napisu 'program sre zakonczyl" i dopj~lD yy~edyprocesJest zakończony.

Wartość zwracana przez funkcję systemową signal może być zachowana, a następnie wykorzystana w kolejnym jej wywołaniu, w celu przywrócenia sygna~owi jego poprzedniej akcji. Wywołanie tej funkcji ma wtedy postać'.

signal (sigtype, oldptt~

gdzie sigtype jest literałem określającym, który sygnał ma być przywrócony, a oldptr wartością wskaźnika zwracaną przez poprzednie wywołanie funkcji signal.

Sygnały nie służą jedynie do natychmiastowego zakończenia programu. Możemy zdefiniować na nowo sygnały tak, aby opóźnić akcje przez nie podejmowane, a nawet spowodować, że akcje te zakończą część programu - bez zakończenia go w całości. Obecnie omówimy różne sposoby przechwytywania sygnałów i tym samym sterowania programem.

Opóźnienie akcji sygnału jest realizowane przez przechwycenie sygnału i określenie dla niego nowej akcji, polegającej na ustawieniu globalnie zdefiniowanej flagi. FIOgtam kontynuuje swoje działania aż do momentu testowania flagi. Test ten polega na sprawdzeniu, czy sygnał został odebrany. 3eżeli tak, to jest on obsługiwany. Wykorzystano tutaj fakt, że wszystkie funkcje wracają do wykonania dokładnie w tym punkcie, w którym program był przerwany. 1eźeC~ fiwkcja określa~~ca nową akćłę aokonye powrotu w normalny sposób, program kontynuuje wykonanie tak, jakbyżaderr sv.~naF nie ,ri rą~il

Opóźnianie sygnału jest szczególnie użyteczne w programach, które nie mogą być zatrzymane w dowolnym punkcie. Przed obsługą opóźnionego sygnału należy zablokować sygnał przerwania, aby ochronić flagę przed zmianą w czasie jej testowania. Bezpośrednio po wykonaniu nowej akcji dla sygnału system automatycznie ustawia sygnał na jego akcję standardową. Dlatego należy pamiętać o ponownym definiowaniu

Jqdro 119

sygnału po każdym przerwaniu - w przeciwnym razie podjęta zostanie akcja standardowa przy następnym jego wystąpieniu. Ilustruje to następujący przykład.

Przykład 6.9 #include <stdio.h> #include <signal.h>

int sigflag; /* deklaracja zmiennej calkowitej */ main() /* signa110.c, opoznienie akcji sygnalu */ (

int delayQ, i; /* deklaracje wartosci funkcji */ int (*savesig)(); /* i zmiennych */ extern int sigflag;

printf("flaga = %x\n", sigflag); /* napis */ signal(SIGINT, delay); /* opoznij sygnal */ printf("flaga sygnalu po opoznieniu = %x\n", sigflag);

for(i = 1; i <= 200; ++i) /* wyswietlenie napisu */ /* 200 razy */ printf("program wykonuje nieprzerywalna akcje\n");

savesig = signal(SIGINT, SIG_IGN); /* zakaz sygnalu */ printf("flaga sygnalu po zakazie = %x\n", sigflag);

/* wyswietlenie napisu */ if(sigflag) /*jezeli warunek spelniony */ /* to wyswietlenie napisu */ printf("flaga sygnału = %x\n", signal);

/* obsluga opoznionego sygnalu */

)

delay() /* nowa akcja podjeta przez sygnal */ extern int sigflag; /* zmienna zewnetrzna */ sigflag = 1; /* ustawienie flagi */

Gdy nie ingerujemy w wykonanie programu, otrzymujemy następujący obraz na ekranie terminalu:

flaga = 0

flaga sygnalu po opoznieniu = 0 program wykonuje nieprzerywalna akcje flaga.sygnalu.po zakazie--.0

Naciśnięcie klawisza przerwania Del spowoduje, że obraz na ekranie będzie następujący:

flaga = 0

program wykonuje nieprzerywalna akcje

program wykprogram wykonuje nieprzerywalna akcje program wykonuje nieprzerywalna akcje

j20 Rozdział 6.

flaga sygnalu po zakazie = 1 flaga sygnalu = 1

Z kolei dwukrotne naciśnięcie klawisza Del spowoduje, że otrzymamy następujący obraz:

flaga sygnalu = 0

flaga sygnalu po opoznieniu = 0 program wykonuje nieprzerywalna akcje

program wykprogram wykonuje nieprzerywalna akcje program wykonuje nieprzerywalna akcje

program wykonuje nieprzerywalna akcje

Zauważmy, że w przypadku jednokrotnego naciśnięcia klawisza Del program wykonuje z opóźnieniem obsługę sygnału SIGINT, o czym świadczy pojawienie się napisu "flaga sygnalu = 1" na terminalu. W przypadku dwukrotnego naciśnięcia klawisza Del następuje akcja standardowa dla sygnału SIGINT i program jest przerywany. Oczywiście, jeżeli nie ingerujemy w wykonanie programu, to pojawia się napis "flaga sygnalu po zakazie = 0" świadczący o tym fakcie.

Możemy stosować sygnały w programach interakcyjnych do sterowania wykonywaniem różnych poleceń i operacji. Na przykład możemy zastosować sygnał podczas pracy z edytorem tekstu do przerwania bieżącej operacji (jak np. wyświetlanie pliku) i następnie do powrotu programu do poprzedniej operacji (np. czekania na polecenie).

Aby zapewnić takie wykonywanie poleceń, funkcja, która ponownie definiuje akcję sygnału, musi być zdolna do powrotu w programie do miejsca dla niego znaczącego, a nie tylko do punktu przerwania. Standardowa biblioteka języka C zapewnia dwie funkcje systemowe, które to robią: setjmp i longjmp [6]. Funkcja setjmp zachowuje kopię stanu wykonania programu, a funkcja longjmp zmienia aktualny stan wykonania programu na poprzednio zachowany. Funkcje te powodują, że program będzie kontynuował swoje działania od poprzedniego miejsca z poprzednimi wartościami rejestrów i statusem, tak jakby żadne operacje nie zostały wykonane pomiędzy chwilą, gdy stan był zachowany, a chwilą, gdy stan został odtworzony.

Wywołanie funkcji setjmp ma postać:

setjmp(buffer~

gdzie bvffer jest zmienną, która ma zachować stan wykonania. Zmienna ta musi być zdefiniowana wcześniej z wykorrystaniem jmp buf. Wywołanie funkcji longjmp ma postać:

longjmp(buffe~ gdzie br~ffer jest zmienną zawierającą stan wykonania.

Aby użyć funkcji systemowych setjmp i longjmp, należy wpierw umieścić na początku programu wiersz:

#include <setjmp.h>

Plik setjmp.h zawiera deklarację jmp buf, która jest szablonem do zapamiętania bieżącego stanu wykonania programu.

Jqdro 121 Rozważmy przykład ilustrujący te zagadnienia.

Przykład 6.10 #include <signal.h> #include <setjmp.h> #include <stdio.h> jmp buf sjbuf;

main() /* signall3.c, zastosowanie sygnalow "J l* w programach interakcyjnych *l

f int onintr(), i; I* deklaracje wartosci funkcji

i` i zmiennych *I setjmp(sjbuf); t* zachowanie stanu programu */ signal (SIGINT, onintr); 1* wykonanie nowej akcji *l

l"W pRypBdkU OdblOt'U Sygnalu przerwania *l for(i = 1; i <= 100; ++i) l* napis 100 razy *l printf("glowna petla przetwarzania %d1n", i);

onintr() /* nowa akcja podjeta po odbiorze sygnam *~ (

int val; 1* zmienna calkowita *l printf("lnprzerwanieln"); /* wyswietlenie napisu *l longjmp(sjbuf, val); l* odtworzenie stanu programu *l

Po uruchomieniu programu i naciśnięciu klawisza przerwania Del otrzymamy następujące wyniki:

glowna petla przetwarzania 1

glowna petla~przetwarzania 5 przerwanie

glowna petla przetwarzania 1 glowna petla.przetwarzania 100

Zauwaźmy, że naciśnięcie klawisza Del podczas wykonywania tego programu przerywa jego pracę przy wyświetlaniu napisu "glowna petla przetwarzania 5", wyświetla napis "przerwanie" i wraca do początku, czyli do wyświetlania napisu "glowna petla przetwarzania I ". Stąd wniosek, że wykonanie programu maca do wybranego punktu, a nie do punktu przerwania.

System przekazuje wszystkie sygnały generowane z danego terminalu do wszystkich procesów wywoływanych z tego terminalu, Tymczasem może się zdarzyć, że proces macierrysty czeka na zakońCZettće wszystkićh procesów potomnych, zanim wznowi swoje działania. Powinien być on wtedy chroniony przed sygnałami przeznaczonymi dla procesów potomnych. Wynika to z faktu, że sygnały te mogą nie dopuścić do wykonania funkcji systemowej wait, odpowiedzialnej za zawieszenie

122

Rozdziat 6.

wykonania procesu macierzystego do chwili zakończenia procesów potomnych. Dlatego proces macierzysty musi wprowadzić zakaz wsrystkich sygnałów, zanim wykona funkcję systemową wait. To chroni przed poważnymi błędami, które mogłyby powstać, gdyby proces macierzysty kontynuował wykonywanie, zanim procesy potomne zostałyby zakończone.

Ilustruje to następujący przykład.

Przykład 6.11

#include <stdio.h> #include <signal.h>

main() /* signal16.c, ochrona procesu macierzystego */ {

int (*zachprzerw)Q, status; /* deklaracje */ /* wartosci funkcji i zmiennych calkowitych */ char cmd[80]; /* tablica cmd ma 80 znakow */ gets(cmd); /* czytanie lancucha znakow do tablicy cmd *1 if (fork() _= 0)

{ /* proces potomny */ printf("proces potomny\n"); /* wyswietlenie napisu */ execl("/bin/sh", "sh", "-c", cmd, NULL);

/* wykonanie polecenia wpisanego do tablicy cmd */

i

zachprzerw = signal(SIGINT, SIG_IGN); /* zakaz sygnalu */ wait(&status); /* zwrot sterowania do */ /* procesu macierzystego */

signal(SIGINT, zachprzerw); /* przywrocenie */ /* akcji sygnalowi */ for(i = 1; i <= 100; ++i) /* wyswietlenie napisu */ printf("proces macierzysty °~d\n", i); /* 100 razy */

Jeśli po uruchomieniu powyższego programu wprowadzimy polecenie ls i następnie wciśniemy klawisz Del, otrzymamy prrykładowo następujące wyniki:

Is proces potomny

a.out core signal1.c signa110.c signall 3.c

siproces macierzysty 1 proces macierzysty 2 proces.macierzysty 100

Zauważmy, że naciśnięcie klawisza Del powoduje przerwanie procesu potomnego wyświetlającego listę plików w aktualnym katalogu, a następnie sterowanie jest

- aru~nlcazad aut?~~fs~fnt ~salnula~SrIS Z~1pt~lop oi `fauf~Caeiado ~a~~w~d n~ ~w aw rymqa fauzp nn fa~oy `~Cuoa~s atnqaz.r~od saoo.rd ysaf `~Czazuz oI `rupalsodzaq qosods nr 2u~nrozrp?al isaf ~t epuyaa~ aru>?p~z pu tuaruerr~roaruolls az `~u~~nurm ~ro~lw>?d z ~CuaaIS.Cs o~ ~s n rua~sA's qa~Csiam qa~Czsmouf~u r xl~jf~ ~falaXlag qat~utals~Cs m ~f ~y atc~,,sf~mo~fm a~uaaq~ XI11~I1 ~sQ~ a"'Sa`S~S ``' r,ZS~p~~z~~0l~PIlOZ(IL~~(tOlCl~hc t~f~;soz aruzn~oaruoi;s epuqoaZ

(bL `OI `8) Eruzmooruo.rls awuqoal i'u drs ~fi?.rardo XI~n rCa~axlag afs~am ar~Ls~izsm 1s>'rwoleN aruzaźfim ~ walsds iwzfsiam nu~Czsfarusazom z (dno.rrJ uoddns XINfI) iJSf1 ~~als~s arxls~izsm :XI~ nurals~Ss rfslam alarm afn~s~CzloyCm ~uf~toz~ado ~m~rwzd EIU22p~Z1EZ Izuaaqos rxz,I, 'szzo ~Suoisayo zaz.rd arwa;s~is m ~iLr;n~tqaz.rd aru a.rol~ `mosaooid aro~runsn au ~rs E~~MZOd aru `Erue;o8rw nwa~qord q~u~run ~Sqd afew źs qn~ faznfp(~u wawals~is ~zod ~(ł~Cq a.rolx `~Csaoo~d ~s auzzpi?moidm aruqopod r aznp ~Csaooid qn~ io~rured m aoifsmdqazld faznłpf~u ~fsaaold au~mnsn ~s rasoufa~ox fazsm.iard M ~auozpi?mo.rdm faru op a.rol~ s `Wowt?d z al~runsn g~iq ~Cumn~od ~Csaooid aiolx `t?Zpmzlds I (punxas ex~y oo f~zo~Cmaez) omosaixo .iuozpnq Isaf ~Comopymez saao.rd (s:sa~o.rd .raddbn~s ~mz~u pod zarumoi ~iueuz qo~wals~is qo~olxay m) 0 saoold yzo `(.ralnpayas '~uE) ~fjnlopocmvz saaotd afnwfapod `~iidrunsn faru z qnI fauf~iae.rado ra~rw~d op ~Cuozpemoldm p~Cq zur saaoid ~C.rolx `wdl o ~fz~CoaQ 'nruazsarm~z E~3a~n saaoid `WowBd ruszsqo o~aufods `o~aznp orupaimodpa zw aru yrnnqa fau~p m ysa f auzysnlxi?n żs amowalsds aar~qel arupa!modpo z `au~tulemz g~rured ~r ~Cusnsordox Isaf nsaao.rd ro~twed zElqp iuaw~z.r3 faf ~imou Isaf ~iuemoxo~>; oI `~Is>?lzm nsaoo.rd o~auzp op fauo~atzp~tzid fauf~Coelado ra~rured .terruzo.t t~sap '(jtd js.~r~ 'buz) ,dd nuz~Crodle ~nłpam qo>?wals~Ss nd~Cl obal m ~ts em~Cqpo ro~rured efoi?xo~ ~fauf~Coe.rado ro~tuz~d aezsqo ~Cznp oa~fezaiers.Gw W s cu~oMZ ze `~jsrCp >;u (8urddpnts vuL - eitit~jt7lrl(r(M 1?IIIaISa.1~0 ~rs >'m~Czn olszo) au~mnsn ~s mosa~old qo~C1 z a.roh[atu ol `fauf~ioelado ra~rwed n,r~zsqo o~aulom afn~E.cq r mosaao~d alarm uCqz ~rs afnpfeuz atmals~Cs m r~saf~az `~SzoEUZ oz 'arsayEz w~Cuozotuei~o m ~uennosozs isaf ~MO~s~cp 8 ~uf~Coz.rado ~ ~raW ur$d ~izp~rmod to~cwed molzzsqo ,cueru.om Byuuoat `~d ~iselx aro~zids su uo~iuzn~C~sOz.rox~Cm `nruals~is qozfs~ant qa~iso~d z>?~o XINfI CI$g~ zru ya~izslarusazan~ nwals~s qa~fs.ram M nut,(.MOldzrds ruteruazorusi~o z au~z~rmz a~sros~ol lsar 'nurals~s rfs.ram po rosouzalez

n~ aruzo>?uz ys puzon ~uGCa,e~ado yoWwEd smszp~zrez ~CwzruECjaay~ ~uf~fau.~ado pa3~umd a~uuzp~za~ ,~/.hz

(6 `8 `9 `S~ 4oEOEld m af pzaleuz azoty rmspzłx~(zrd rWduEMOSUEMeeZ farzp.rEq ~cuzmosa.~a~uoz yula~za 'XINfI o~aufdos.rado ncua;sds azlp~f n~ rw>?saaoid aru>?zp~z~EZ qo~Co~fruLsnp mopi;fix~izrd lorqz Uzauox p>;fyCz,cd cCzsz~Crnod

-„~sfiz.rarogur saaold„ ns!d~u em~!Iarms~inn ~fo~rulsur ~z (nuaz.~dyap~ ;INIJIS)I~u$!s ~foxrulsur g~unsazrd pzłyCzid ~u ~fza~au `~zxs~izn o; óqd vlstz~am~ru saooid ~iuoruo.zqa iserwoleu ~Isaf aru `;IeM EfoxculsaT ewas rweruzm.raz.rd pazid ;saf euoruoJqo urn arurz.r~ocd nn atasrnn~zap 'o~aurenixe n~śo~~Le~ z moyld ~ISr1 Eutad ~~1s rM~fod nl~uruual arue~xa ~u oI~`ameuox~nn o~af nn doua~a~ur zaq wE.r~Old ut~s ual ~w~uox~ t~azap 'odalsrCziarotm nsaoold op auezexazld

GZI

zwal ~ue) fl~l uoils Auzcui;Cm ua~C.io~~E ~rs afn~s~zJOx;Cm Caros~zofEN fauzll~umaz Wvured z faf Em~zpEn~oids ~saf o~ `~rasGCm/Eysfann daeaado EmEUOyCn~ msouz~acUO?j zeq aE;srtz.ip?[rCM $( gUZOCU t (~71JY7A '~uL) ~uz~M ox~f aru~nouod Euozozuzo ~Cpaam p~Cq azotu `;Cza;C~op o~ aiuEn~.razcd fa.~ql~ `zuoils ol `„~Cuons p~fq„ EmEmlazid ł~u°;Cs ;CuEneoaaua°;Cnn aru~~soz ardena uz;C~ Eu ysar afa~E aupEZ auEmorufapod ~s aru faru op n~unsots n~ arzEi Eu c (prlvnrfr vuE) EuzEnnaru oxEf nsaaold U01jS EIUEMOdOZMpo ;CayqEl n~ EaEZaEUZO ~saC Euor~s nvardfE~ t~aEdEla qaon~p n~ E°arqazld fauf;CaElado WouzEd z uoys aruEn~nsn

rfa~ziuoauaufs mzm~qaaw zE~to uoa~s ~Cuem;Cm uyo~~2 zaz.rd auzm~~s;izioxzCm wzp EiarmEZ `uoys ~~uzm ~uEp Ea~fnsrdo `WmEd ardEU~ n~ ~fo;izod Eru~ezsp vnosaaold ~ayqE~ zazad rcu;Cuur ;Czpdruz `mzESaoold muEZp~uEZ nn auqazczod ~s Wuumd ardEUr nn afa;Czod auddlsE~ woi~s ~auzel rasouEn~EZ qa;Ca~fEpErn~odpo qa;Cn~oxs~Cp nnoxo~q !faEZyExol ;izid - faz~(n~od auEmnodsnn afaEUmo~ur E.rarn~EZ ExwEi ;Cp~ `aruEmoso;sEZ ~CnpCEUZ afa;Czod ;Czl~ aufa~ox ao~is ~awm qa~C~snd fauoza~ł ;C~sy ~ruaz~on~;n op auem.C~stzio~~m auo ~s ~lsnd ~saf E~wEi rCp~ `au~nnl;slz,co~;Cn~ ~s ra~rm~d ard~m n~ aCarCzod am~p azsn~,.iard fauf~CaEaado Wauz~d ruezsqo o~a~rrno~łEa odo ~ EzoEixazid aru ra~cmEd EdEm ;Cłn~ai z a~no~fzq-9f ~s cxurEa aa~fnsrdo caW ued ard~uz n~ afa;Czod au!oDazazsod lswcuo~EU `gX! lEruczoi Caros~zofeu ~fEUr uoi~s ymEg pn C iu d~uaEl - I lu Efa;Czod `0 su uol~s ~~urEa afnsrdo Wnu~Ed rCdEUa p .ru Ef~;Czod woi~s J~aUIE1 I~SOj1EMEZ O rf~EU11o~Lrl lolqZ IMOUEIS I~~IU1Ed EdEy~

auE~s~Cziox.Cn~ aw n~sold od p;Cq ~~our qn~ UO1jS EIUEMOI0ZMP0 ~ayqE~ r Eyur,~o~;fzn ~m~~ruls `sols `qafuEp ~uauz~as `nu~~Woid ;sial q~iarmez ~~ow aio~~ `uoys yurei ~u ~uo~atzpod lsaf fauC;Ca~iado Woumd ps~za e~~;sozod (rlvru a.roa vuE) Wvumd Edeuz ugru pEU E `mua~s;Ss o~p~f ~rs afnpCEUZ Wnu~Ed azJezsqo unu!op M

'[bil QSgb XI~IfI a!wałsds w I'auGfaeaado !a~!wed zeaqp ~g~9 -s,f~

nys.~p z nys.%p z euozpe.MO~ds e_W ezpe.MO~ds elsnd

q,iq isnw a~ayey m e~~ue~

yo~(yann ~awe~

aysy eu ps af / -npfeuz e~we~ ~p6 L

~auenv(~sr(mo~r(M

amoys au am3mnsavd

so1S /aus~;sya.L

m9saoo~d .4aqqal ap syapur

ewazpL~n ~o~ey;fyuap~

nys,Cp eu ńuoys nyo~q iawnty

ns~dez o3ampazJdod op syapur

nsidez o~aud37seu op syapur

Wwed aodew on sodez ńzouńpatod

l eyme2ł

~ eymc~ ~ eymeH

eufRoe~ado ppwed

(g'9 srCi) ras~za ;izy eu Euola!zpod ~saf n~oma;s;is faz~Cnnod qa;CuEwwodsn~ EufiaE.rado q~rwEd

nxsńp z EuEuCza zuoys znoosEłnn r fauCfoE.rado WmEd n~ Eyuzi mupay~odpo EuEn~o~olE `„;iuo~ls p~łq„

ia~iwed

edew

eoapunsn op „ y~epApue~„ - Auoys

eyaiza~euz nlao

M ddew ~4a§lnp

-nzsazid saaoud

p eytue~j

'9 tpzpao~ vz r

Jqdro I ZS

Recently UsecT). Algorytm ten opiera się na założeniu, że strona z pamięci operacyjnej, która najdłużej nie była wykorzystywana, nie będzie prawdopodobnie wykorzystywana również w najbliższej prryszłości. Mapa pamięci operacyjnej (z wyłączeniem jądra) jest cyklicznie i liniowo przeglądana przez program o nazwie clock hand (w Berkeley LTNIX ­two handed clock, w System V LTNIX - one handed clock). Jeśli ramka jest wolna, program bada następną i jeśli jest w niej wpisana jakaś strona, to następuje sprawdzenie odpowiedniego zapisu w tablicy odwzorowania stron. Jeśli strona występuje jako nieważna, to jest usuwana, a ramka dodawana do listy ramek wolnych. Natomiast jeśli strona jest oznaczona jako ważna i aktualnie nie jest wykorzystywana przez operację wejścia/wyjścia, to jej status ulega zmianie na nieważny i stronę pozostawia się na razie w spokoju. W systemie kontroli podlega również liczba stron ze statusem ważna, tak aby nie obniżyła się ona zbytnio, co mogłoby spowodować nagły wzrost żądań sprowadzania stron z pamięci dyskowej. Proces LRU clock band jest zaimplementowany jako część procesu 2 - pagedaemon (zawiadowca - swapper - jest jest procesem 0, init procesem 1). Proces ten większość czasu "śpi", lecz w tym czasie trwa sprawdzanie (kilka razy na sekundę), czy konieczna jest w systemie interwencja tego procesu. Jeśli tak, proces 2 zostaje obudzony, na przykład gdy liczba wolnych ramek w pamięci operacyjnej spadnie poniżej pewnej wartości progowej (lotsfree) - zazwyczaj 1/4 pamięci. Proces clock hartd uwzględnia podczas pracy zarówno to, ile ramek brakuje do osiągnięcia wartości progowej, jak i wskazówki zawiadowcy. Jeśli proces zawiadowcy zadecyduje, że system stronicowania jest przeładowany, to pewne procesy są z systemu usuwane, aż przeładowanie zniknie. Brane są przy tym pod uwagę pewne dodatkowe parametry, tj. mirrfree - najniższy limit wolnej pamięci oraz desfree - średni rozmiar dostępnej pamięci w ostatnim okresie. Innymi słowy, procesy są usuwane, jeśli w systemie występuje chroniczny brak pamięci oraz istnieje kilka procesów, które próbują podjąć pracę.

Omówiony powyżej mechanizm zarządzania pamięcią operacyjną jest mechanizmem systemowym, a więc ingerencje użytkowników są w nim niemożliwe. System UNIX dostarczajednak pewne dodatkowe możliwości ingerowania w organizację pamięci wykorzystywanej przez programy aplikacyjne. Służy do tego zestaw funkcji systemowych, pozwalających na zdefiniowanie i manipulowanie tzw. pamięcią współdzieloną.

Pamięć wspbldzielona (ang. shared memory) służy do przekazywania danych między procesami. Obszar współdzielony musi zostać utworzony z wykorzystaniem funkcji systemowej shmget z argumentami kh~cz, rozmiar i tryb dostępr~ (inaczej - flagi doslęptr - ang. shmflag). Flaga SHM_R oznacza zezwolenie odczytu, SHM_W - zapisu, SHM_INIT - inicjację zawartości obszaru. Funkcja ta zwraca identyfikator obszaru. Każdy z procesów, który chce wymieniać dane poprzez ten obszar musi, dołączyć go funkcją shmat (ang. .s~hared memory aftach), podając jako argumenty identyfikator uzyskany z shmget, adres, pod którym proces chce widzieć obszar (gdy adres jest równy 0, system przyłącza obszar pod pierwszym wolnym adresem), oraz flagi dostępu. Gdy flagi zawierają SHM_RND, podany adres jest zaokrąglany do wartości podanej w stałej S~~,~A. W zy przy~ączaW u ostW elącego obszaru bez inicjacji flagi nie zawierają SI~M_i7~IT. FunkćJa shmat zwraca adres, pod którym obszar współdzietony jest widoczny w danym procesie. Po zakończeniu korzystania z obszaru proces oci~ącza ~o wykonując funkcję shmdt (ang. shared memory detach), przekazując jako argument adres obszaru.

Zasady posługiwania się pamięcią współdzieloną ilustruje przykład 6.13, zawierający program, który dokonuje mnożenia macierzy.

126 Rozdzia! 6.

Przykład 6.12

/x

//written by Zbigniew M. Kaczmarek Wiktor Wysocki Artur Zakrzewski "/

#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <string.h> #include <stdlib.h>

int a[10J[10]; int b[10][10]; int x,y,z;

int czytaj(int a, int b) f

int rob;

pri ntf("%d, °~d: ", a, b); scanf("%d",&rob); return rob;

) int multi(int x,int y,int z) f

int i;

int buf=0; for(i=0; i<z; i++) buf=buf+a(x][i]*b[i][y]; return buf;

void main() f

int i,j;

int size,shmflg=IPC CREAT+SHM W+SHM R+SHM_INIT; key t key;

int val; char *t; char *k; int "znak;

int pid,status,rob; int ndigit;

printf("Podaj liczbe wierszy pierwszej macierzy: "); scanf("%d", &x);

printf("Podaj liczbe wierszy drugiej macierzy(I. kolumn pierwszej)"); scanf("%d",&z);

printf("Podaj liczbe kolumn drugiej macierzy: ");

Jqdro 12 %

scanf("°r6d", &y);

printf('v1/prowadz macierz pierwsza:\n"); for(i=0; i<x; i++)

for(j=o; j<z;j++) a(i]Gl=~Ytaj(i,j); printf("Wprowadz macierz druga:\n"); for(i=0; i<z; i++)

for(j=O;j<Y;J++) bf i1G]=czytaj(i,j); size=x*y*2; val=shmget(key,size,shmflg); for(i=0; i<x; i++)

for(j=O;j<Y;J++) f

if ((pid=fork())==0) /* tworzone sa procesy potomne, ktore beda */ /* wspolbieznie wykonywac mnozenie podanych */ /* macierzy */

char liczba[7]; shmflg=SHM_R+SHM W; rob=multi(i,j,z); ndigit=5;

gcvt((double) rob, nd igit, I iczba);

t=shmat(val,k,shmflg);/* kazdy proces dokonuje przylaczenia wspolnego */ /* obszaru pamieci */

strcpy((t+7*i*y+7*j), &I iczba[0]);

shmdt(t); /* po wykorzystaniu pamiec jest odtaczana */ exit(status);

)

wait(&status); t=shmat(val,k,SHM R+SHM W); for (i=0; i<x; i++)

for(j=O;j<y;j++) printf("%7s ",t+7*i*y+7*j); printf("\n");

)

shmdt(t);

Do uryskania informacji o obecnych w systemie obszarach pamięci współdzielonej służy funkcja ipcs (ang. interprocess communication status). Informuje ona również o semaforach i kolejkach zdarzeń czekających na obsługę. Aby wyświetlić dane dotyczące tylko obszarów pamięci współdzielonej, należy użyć opcji -s (od ang. shared memory). Przykładowa informacja o obszarze wykorzystywanym w trakcie działania powyższego programu wygląda następująco:

128 Rozdział 6.

T ID KEY MODE OWNER GROUP Shared Memory:

m 200 0x00000000 --rw------- martin staff

ID zawiera identyfikator obszaru zwracany przez funkcję shmget. MODE określa tryb dostępu do obszaru podawanym przez funkcję shmflg, a OWNER oraz GROUP określają użytkownika, który utworzył dany obszar. Właściciel obszaru współdzielonej pamięci może go usunąć, wykorzystując w tym celu polecenie ipcrm z opcją -m i identyfikatorem obszaru jako argumentem. Aby usunąć obszar wykorzystywany w naszym przykładzie, należałoby użyć tego polecenia w postaci:

ipcrm -m 200

Zarządzanie urządzeniami wejścia/wyjścia

W systemie UNIX, tak jak w wielu innych systemach operacyjnych, szczegóły urządzeń fizycznych są ukryte przed użytkownikiem.

Jednakże rozwiązanie zastosowane w tym systemie jest unikalne. Zadanie ukrycia wspomnianych szczegółów należy do jądra, a ściślej - do systemu zarządzania urządzeniami wejścia/wyjścia. Jak wiemy, w systemie UNIX wprowadzono zasadę, że urządzenia fizyczne są postrzegane przez użytkownika jako pliki. Osiągnięto to definiując urządzenie wirtualne funkcjonalnie odpowiadające dyskowi i pisząc specjalne programy obsługi urządzeń (ang. denice drivers), odwzorowujące operacje urządzenia wirtualnego w operacje urządzenia fizycznego. Ze względu na konieczność specjalnego traktowania przez system wspomnianych plików odpowiadających urządzeniom nazwano je plikami specjalnymi. Pliki specjalne są zazwyczaj pamiętane w katalogu /den, który stanowi część tradycyjnego systemu plików.

Na plikach specjalnych mogą być wykonywane wszystkie standardowe operacje jak na plikach zwykłych. Na prrykład polecenie

$ cp plik_1 Idevllp

spowoduje skopiowanie zawartości pliku plik-I do pliku specjalnego dev.'7p, reprezentującego drukarkę, czyli po prostu wydrukowanie pliku plik 1.

Zaletą tego rozwiązania jest to, że standardowe mechanizmy ochrony plików systemu UNIX stosują się również do plików specjalnych. Dostęp do tych plików w nieograniczonym zakresie ma tylko administrator systemu; pozostali użytkownicy mogą korzystać z nich w sposób z reguły mocno ograniczony.

Istnieją dwa podstawowe typy plików specjalnych: blokowe i znakowe. Pliki blokowe reprezentują urządzenia blokowe (ang. block devices), czyli dyski i taśmy. Mają one dostęp do zbioru buforów danych o stałej pojemności, zazwyczaj S I2 bajtów lub 1034 bajty, tworzących tzw. pamięć notatnikową buforów (ang. block br~ffer cache). Pamięć ta zawiera informacje o adresach buforów danych w pamięci operacyjnej, numerach urządzeń i numerze bloku danych na danym urządzeniu. Pliki znakowe reprezentują urządzenia znakowe (ang. rau~ devices), czyli terminale i drukarki. Urządzenia te mają swoje własne bufory. Do tej grupy zalicza się również złącze pamięci

.7qctro 129

operacyjnej - dev/mem oraz dev/null - tak zwane urządzenie puste, tj urządzenie "pochłaniające" dane przy zapisie do niego albo generujące znak końca pliku przy odczycie z niego.

Urządzenie może być wyróżnione przez klasę (ang. class), dzielącą urządzenia na blokowe albo znakowe, oraz przez numer urządzenia (ang. device numher). Numer urządzenia składa się z dwóch części: numeru głównego i pobocznego. Numer główny (ang. major device number) służy do adresowania odpowiedniego programu obsługi urządzenia. Numer poboczny (ang. minor device number) jest wykorzystywany do zaadresowania konkretnego urządzenia.

System wejścia/wyjścia składa się ze zbioru modułów obsługi urządzeń, zazwyczaj jednego modułu obsługi na urządzenie określonego typu. Moduły te są dołączane do systemu operacyjnego podczas generowania jądra i nie mogą być później usuwane. Ich zadaniem jest odseparowanie reszty systemu od sprzętu. Ze względu na istnienie standardowych złącz międry modułami obsługi a resztą systemu operacyjnego większa część systemu wejścia/wyjścia może być umieszczona w niezależnej od sprzętu części jądra.

System wejścia/wyjścia systemu UNIX jest podzielony na dwie części, zgodnie z klasyfikacją urządzeń, tj. część obsługującą urządzenia blokowe i część obsługującą urządzenia znakowe. Przedstawimy teraz obie części dokładniej.

Celem części systemu, która obsługuje operacje wejścia/wyjścia urządzeń blokowych, czyli przede wszystkim dysków, jest minimalizacja liczby przesyłań do i z dysku. Jak już wspomnieliśmy, w systemie UNIX stosowana jest w tym celu pamięć notatnikowa buforów, którą umieszczono między systemem plików a modułami obsługi dysków (rys. 6.8).

Przestrzeń Złącze z /dev~tły Złącze bez użytkownika) Operacje zapisu/odczyłu plików z konwersja danych konwersji

Jądro

System plików

Moduł konwersji danych

Pamięć notatnikowa ~ ~ - lista

Moduł obsługi dysków ~ j Moduł obsługi terminali

Rys. 6.8. System wejścia/wyjścia [.14~

Rozdział 6.

Przyjrzyjmy się wykorzystywaniu pamięci notatnikowej buforów nieco dokładniej. Przy wykonywaniu operacji odczytu z urządzenia w pierwszej kolejności przeszukiwana jest pamięć notatnikowa buforów. Jeżeli w buforze znaleziono określony blok danych z tego urządzenia, to nie ma potrzeby wykonywania czasochłonnej operacji wejścia/wyjścia. Jeżeli nie znaleziono, to wyszukuje się bufor z listy wolnych buforów, aktualizuje się numer urządzenia i numer bloku i wykonuje operację odczytu z urządzenia do pamięci notatnikowej. Jeżeli nie ma wolnych buforów, to wyszukuje się bufor z danymi przeznaczonymi do zapisu na urządzeniu wyjściowym - z danymi, które najdłużej czekały na zapis. Dane z tego bufora zostają zapisane na urządzenie wyjściowe, a zwolniony bufor może być wykorzystany przez operację odczytu. Informacja o tym, że blok danych znajduje się już w buforze, zawarta jest w nagłówku bufora.

Pamięć notatnikowa jest wykorrystywana zarówno prry wykonywaniu operacji zapisu, jak i odczytu. W celu utrzymania spójności systemu plików w przypadku awarii zawartość pamięci notatnikowej jest zapisywana na dysk co 30 sekund.

Zarządzanie urządzeniami znakowymi odbywa się w następujący sposób. Urządzenia znakowe operują strumieniami znaków, a nie blokami danych i z tego względu w ich przypadku nie stosuje się pamięci notatnikowej. Jednakże programy obsługi urządzeń znakowych (np. terminali) korzystają z własnego systemu buforowania znaków. System ten składa się z list buforów, zwanych Glistami. Z list tych korzystają odpowiednie podprogramy dopisywania i usuwania znaków. Każda taka struktura jest łańcuchem bloków zawierających maksymalnie 64 znaki, licznik i wskaźnik do następnego elementu. Znaki przychodzące z terminali i innych urządzeń znakowych są zapamiętane w łańcuchu tych bloków w kolejności przychodzenia. Przesyłanie i usuwanie znaków z listy jest sterowane przerwaniami. Odczyt znaków z terminalu także jest sterowany przerwaniami.

Kiedy proces czyta że standardowego wejścia /dev/tty, to znaki nie są przesyłane bezpośrednio do tego procesu z C-listy, ale przekazywane przez część kodu jądra zwaną modułem konwersji danych (ang. lirce disciplitte). Moduł ten działa jak filtr pobierający nie przetworzony strumień znaków z modułu obsługi terminalu i tworzący nowy strumień znaków. W przetworzonym strumieniu znaków dokonywana jest konwersja wierszy, na przykład zamiana znaków powrotu karetki CR na znak nowej linii NL. Taki przetworzony strumień jest przesłany do procesu. Jednakże gdy proces sam bezpośrednio obsługuje każdy pojedynczy znak, wtedy moduł konwersji danych jest omijany.

Zapis danych przez proces na standardowe urządzenie wyjścia jest przeprowadzany podobnie. Strumień danych może przechodzić przez moduł konwersji danych albo go omijać. Omijanie konwersji danych jest konieczne przy przesyłaniu danych binarnych do innych komputerów za pomocą łącza szeregowego.

Zarządzanie plikami

Omówimy obecnie mechanizmy dostępu do plików. Wykorzystamy do tego funkcje systemowe dostępne w języku C [5]. Oczywiste jest, że aby program mógł odczytać lub zapisać dane w pliku, to musi ten plik otworzyć, a gdy go jeszcze nie ma, to powinien go utworzyć. Przed zakończeniem programu należy zamknąć utworzony przez niego plik. Jeszcze inną operacją, która może być wykonana na pliku, jest jego usunięcie. Zacznijmy zatem od omówienia utworzenia pliku.

Jądro

13l Nowy plik tvvorzyrny ~nkcją systemową creat. Jeżeli plik już istn;e;e ; ;ego *,

pojawi się jako parametr funkcji creat. ro z~st ;e

WYwo~anie funkcji creat ma postać: ^ n.~~~s~*a+~~>..~. . , ,., , fd = creat(name, pmode);

gdzie name jest nazwą pliku, a pmode określa prawa dostępu do pliku za pomocą trzycyfrowej liczby ósemkowej (patrz polecenie chmod). Po zakończeniu wykonania funkcji zmienna fd przybiera wartość będącą deskryptorem nowo utworzonego pliku o podanej nazwie albo -1. jeżeli nie „,oz*,z ~.t.,~o~sy~ f,t;h„. wprawny uc~Ktyp~pr pliku Jest nieujemną liczbą całkowitą. W trakcie wywołania funkcji creat system tworzy opis nowego pliku w postaci i-węzła oraz w odpowiednim katalogu, do którego jest przypisany nowo tworzony plik, wstawia parę danych: nazwę pliku i numer jego i-węzła. zapisu.

Plik o deskryptorze 0 jest standardowym wejściem, o deskryptorze l - standardowym wyjściem, a o deskryptorze 2 - standardowym strumieniem diagnostycznym. Pliki te są standardowo dowiązane do terminalu.

Do otwarcia istniejącego pliku stosujemy funkcję systemową open. Wywołanie tej funkcji ma postać:

fd = open(name, rwmode);

gdzie rrame jest nazwą pliku, a rwmode parametrem określającym prawa dostępu do pliku. Jeśli plik ma mieć nadane prawo czytania, to parametr rwmode powinien przyjąć wartość 0, prawo pisania - 1, prawo czytania i pisania - 2. Po zakończeniu wykonania funkcji zmienna,fdprzybiera wartość będacą deskryptorem pliku o podanej nazwie albo -1, jeżeli nie można otworzyć pliku. Operacja otwierania pliku ma za zadanie przekształcenie jego nazwy w odpowiedni opis pliku. System wyszukuje i-węzeł danego pliku i umieszcza go w tablicy aktywnych węzłów.

Do zamknięcia otwartego pliku stosujemy funkcję systemową close. Wywołanie tej funkcji ma postać:

cfose(fd); gdzie fdjest deskryptorem otwartego pliku. Zamykanie pliku ma znaczenie ze względu na fakt, że dozwolona jest ograniczona liczba jednocześnie otwartych plików w programie. Zakończenie wykonania programu przez funkcję exit lub powrót z funkcji main również zamyka wszystkie otwarte pliki.

Do realizacji zapisu i odczytu danych z pliku do bufora w programie służą funkcje read r write. Wvwołama tych funkcji mają następującą postać'.

r= read(fd, buf, n); w = write(fd, buf, n);

gdzie fd jest deskryptorem pliku, bhf - buforem w programie, a rr liczbą bajtów, które mają być odczytane lub zapisane. Po zakończeniu wykonania funkcji zmienna r przybiera wartość równą liczbie faktycznie przesłanych bajtów. Jeżeli operacja zakończyła się poprawnie, to r = rr, jeżeli przy odczycie osiągnięto koniec pliku, to r przyjmuje wartość równą 0, a -1, gdy nie można pisać lub czytać do/z pliku. Odczyt i zapis traktuje się jako operacje sekwencyjne, o ile nie jest to explicite realizowane inaczej. Dla każdego otwartego pliku system przechowuje w tablicy struktury pliku, wskaźnik kolejnego bajtu

132 Rozdziaf 6.

do odczytu lub miejsce do zapisu. Każda operacja zapisu lub odczytu przesuwa wskaźnik pozycji w pliku w przód o wartość równą liczbie przesyłanych bajtów.

Do zmiany wartości tego wskaźnika służy funkcja lseek. Umożliwia ona realizację bezpośredniego dostępu do danych przechowywanych na dysku. Dla plików specjalnych niektórych urządzeń, np. drukarek i monitorów, jest ona zabroniona. Aby móc wykonać operacje zapisu lub odczytu w trybie dostępu bezpośredniego, powinniśmy mieć możliwość przesuwania wskaźnika w pliku do miejsca, od którego chcemy rozpocząć odczyt albo zapis. Dokonujemy tego funkcją systemową Iseek. Wywołanie tej funkcji ma postać:

newloc = Iseek(fd, offset, ońgin);

gdzie fd jest deskryptorem pliku, a offset przesunięciem (w bajtach) wskazującym nową lokalizację wskaźnika w pliku względem miejsca określonego przez origin. Argument offset to liczba całkowita. Argument origin przyjmuje wartość 0, gdy przesunięcie ma być mierzone od pocaątku pliku, 1 - gdy od bieżącej pozycji wskaźnika, a 2 - jeśli od końca pliku. Po zakończeniu wykonania funkcji Iseek zmienna t~eH~loc przyjmuje wartość równą nowej pozycji wskaźnika w pliku w odniesieniu do początku pliku.

Przykładami zastosowania funkcji Iseek mogą być instrukcje w programie:

albo

Iseek(fd, OL, 2);

Iseek(fd, long(0), 0);

Pierwsza z nich ustawia wskaźnik po ostatnim znaku w pliku, a druga na jego początku. Prrykłady zastosowań funkcji open, write i close znajdzie Czytelnik w przykładzie 6.7. Zastosowano tam odpowiednio funkcje standardowe fopen, fwrite i fclose, które tym się różnią od funkcji open, write i close, że efektem ich wykonania jest zwrot do programu wskaźnika pliku zamiast deskryptora pliku.

Zauważmy, że funkcje read i write realizują operacje wejścia/wyjścia na najniższym poziomie systemu UNIX, nie ma więc tutaj buforowania danych, a program styka się bezpośrednio z jądrem systemu operacyjnego.

Wspótbieżne wykonywanie procesów pozwala na osiągnięcie większej szybkości pracy systemu. Jednakże w niektórych sytuacjach współbieżny dostęp do tych samych danych może powodować nieprzewidziane błędy, na przykład zniszczenie przez jeden z procesów danych zapisanych przez inny proces. Rozważmy taką właśnie sytuację na przykładzie systemu bankowego. Załóżmy, że dwa procesy modyfikują równolegle stan pewnego konta, które jest zapisane jako fragment pewnego pliku. Modyfikacja składa się z operacji odczytu i zapisu. Pierwszy proces zwiększa stan konta o 300, a drugi o 200.

Czas Operacje Stan konta po operacji

0 Stan poczatkowy 300

1 Proces 1 odczytuje 300 300

2 Proces 2 odczytuje 300 300

3 Proces 7 zap~sufe 300+300 600

4 Proces 2 zapisuje 300+200 500

Wspóibieżnie wykonywane operacje modyfikacji wspólnych danych spowodowafy bfąd ­

«==r=~o z~: procesy rniaty w sumie dopisać do stanu konta 500, co dałoby ostateczny jego

~aaro

l33

stan - 800, to wskutek zniszczenia przez proces 2 (w kroku 4) zapisu wykonanego przez proces 1 w kroku 3 stan konta wzrósł tylko 0 200.

Dla uniknięcia podobnych błędów stosuje się technikę wzajemnego wykluczania, tutaj można ją zrealizować przez blokowanie fragmentów pliku, wykorzystywanych wspólnie przez procesy współbieżne. Przy zastosowaniu blokowania proces 2 musiałby czekać na zakończenie wsrystkich crynności wykonywanych przez proces I - w rezultacie nie pojawiłby się błąd opisany powyżej.

Do zablokowania fragmentu pliku służy funkcja systemowa:

locking(fd, tryb, offset)

gdzie fd jest deskryptorem pliku, na którym chcemy założyć blokadę, ~~,fft~~r to przesunięcie od aktualne] pozycji w pliku, określające liczbę bajtów, które zostaną zablokowane, natomiast WartOŚĆ parametru tryb oznacza jedną z nast~pujących operacji. LK LJNLCK - usunięcie blokady z danego fragmentu pliku,

LK_LOCK - blokada fragmentu pliku dla operacji zapisu i odczytu wykonywanych przez inne procesy; jeśli założenie takiej blokady nie jest możliwe w chwili realizacji polecenia (ponieważ inny proces już zablokował dany fragment pliku), to proces musi czekać do chwili uzyskania pozwolenia na założenie blokady,

LK NBLCK - jak LK LOCK, lecz proces nie czeka na uzyskanie pozwolenia na założenie blokady; w przypadku niepowodzenia funkcja locking zwraca kod -1 i zmienna errno ustawiana jest na EAGAIN (w UNIX System V),

• LK_RLCK - jak LK_LOCK, ale blokada dopuszcza wykonywanie operacji odczytu przez inne procesy (blokowany jest zapis),

LK_NBRLCK - w tym trybie dopuszcza się odczyt przez inny proces jak przy LK_RLCK; podobnie jak w przypadku LK_NBLCK nie ma oczekiwania na założenie blokady i w przypadku niepowodzenia operacji zwracany jest kod - I i ustawiana zmienna errno.

Przykład 6.13

Najprostszą strategią blokowaniajest użycie blokady z oczekiwaniem LK-I.OCK. Ilustruje to następujący przykład:

l~ Blokada z oczekiwaniem ~/ #include <fcn~tl.h>

#include <syśJtypes.h> #include <sysrloeking.h> int f;

chat fn[]="Ifile"; int d;

chat buf[20]; main() d=20;

f=open(fn,0 RDWR);

~.i,

134

Rozdział 6.

Iseek(f,1 OL,O);

if (locking(f,LK_LOCK,20L)==-1)

{ printf("Bład przy zakładaniu blokady\n"); exit(1);

}; printf("Blokada ok\n"); read(f, buf,20);

sleep(20); /' Przerwa - tu mogą być wykonywane "/ /" operacje na zablokowanym obszarze ~/ write(f, buf,20);

Iseek(f,10L,0); /~` Powrót do miejsca założenia blokady "/ locking(f,LK UNLCK,20L);

printf("Koniec blokady\n"); }

W następnym programie proces kilkakrotnie próbuje założyć blokadę. Choć w tym programie w trakcie czekania jest wykonywana instrukcja sleep, w rzeczywistości proces może wykonywać inne operacje - w szczególności przetwarzanie następnych fragmentów pliku.

/" Blokada bez oczekiwania "/ #include <fcntl.h>

#include <sys/types.h> #include <sys/locking.h> int f;

char fn(]="Ifile" int d;

char buf[20]; maino{ d=20;

f=open(fn,0_RDWR); Iseek(f,1 OL,O);

repeat_count=3; /" Ilość prób blokowania `/ while(locking(f,LK_NBLCK,20L)==-1 && repeat count--)

sleep(5); if (!repeat count)

{ printf("Brak możliwości założenia blokady\n"); exit(1);

};

printf("Blokada ok\n"); read(f,buf,20); sleep(20); /" Przerwa - tu mogą być wykonywane operacje "/

/' na zablokowanym obszarze `/

write(f, buf, 20); Iseek(f,1 OL,O); locking(f,LK_UNLCK,20L);

printf("Koniec blokady\n"); }

Jqdro

135

Inną możliwością jest korrystanie z blokady z ograniczonym oczekiwaniem (timeout). Do tego celu wykorzystamy procedurę alarm, która po upływie określonego czasu wysyła do procesu sygnał SIGALRM. Procedura obsługi tego sygnału może zakończyć oczekiwanie na uzyskanie blokady.

#include <fcntl.h> #include <sys/types.h> #include <sys/locking.h> #include <signal.h>

int f;

char fn[]="Ifile"; char buf[20]; int d,waiting; void killproc() (

if (waiting)

printf("\nCzas minął...\n"); exit(1);

main(){ d=20; f=open(fn,0_RDWR);

Iseek(f,1 OL,O);

alarm(30); /* Po 30 sekundach nastapi sygnał '/ signal(SIGALRM,killproc);

waiting=1; printf("Czekam...\n"); locking(f,LK_LOCK,20L); waiting=0;

printf(" Blokada ok\n"); read(f,buf,20); sleep(d);

write(f, buf, 20); Iseek(f,1 OL,O); locking(f,LK_UNLCK,20L);

>~:a .

Oprócz funkcji wymienionych powyżej w systemie UNIX występuje jeszcze kilka innych, lecz rzadziej wykorzystywanych. Dotyczą one zmiany właściciela pliku, zmiany praw dostępu do pliku i przyłączania się do pliku już istniejącego.



Wyszukiwarka

Podobne podstrony:
System operacyjny UNIX dla poczatkujacych i zaawansowanych, rozdzial1 i2
System operacyjny UNIX dla poczatkujacych i zaawansowanych, rozdzial3
System operacyjny UNIX dla poczatkujacych i zaawansowanych, rozdzial8 i reszta
System operacyjny UNIX dla poczatkujacych i zaawansowanych, rozdzial5
System operacyjny UNIX dla poczatkujacych i zaawansowanych, podsumowanie
System operacyjny UNIX dla poczatkujacych i zaawansowanych, skorowidz
Strzelecki - kolos-wejściówka -pytania i odp, WAT, semestr VI, systemy operacyjne UNIX
Pierwsza wersja systemu operacyjnego Unix powstała w Bell Labs firmy AT
Pierwsza wersja systemu operacyjnego Unix powstała w Bell Labs firmy AT
Solaris pytania 02, WAT, semestr VI, systemy operacyjne UNIX
Systemy operacyjne unix polecenia podstawowe
Przepisy z wołowiny dla początkujących i zaawansowanych
System operacyjny UNIX

więcej podobnych podstron