1. CELE ISTNIENIA SYSTEMÓW OPERACYJNYCH
Zarys historii rozwoju systemów operacyjnych
Najwcześniejsze komputery (konstruowane w latach 40-tych i na początku 50-tych) nie miały
żadnego stałego oprogramowania. Programy użytkowników były pisane w kodzie maszynowym
i bezpośrednio (w postaci ciągu bitów) wprowadzane do pamięci operacyjnej komputera.
Wyniki działania programów również były odczytywane bezpośrednio z pamięci. Takie
programowanie „surowej maszyny” było bardzo niedogodne dla użytkowników.
Aby ułatwić używanie komputera, zaczęto do niego przyłączać dodatkowe urządzenia służące do
przyspieszenia wprowadzania programów i danych do pamięci komputera oraz odczytywania
wyników. Zostały one nazwane urządzeniami zewnętrznymi komputera (dawniej nazywano je
też urządzeniami peryferyjnymi lub peryferałami (peripheral)).
Rodzaje najwcześniejszych urządzeń zewnętrznych:
1) urządzenie typu „dalekopis” (sterowana elektryczna maszyna do pisania) pełniące rolę konsoli
operatorskiej, to jest służące do wprowadzania i odczytywania niewielkich porcji informacji
w postaci znakowej;
2) czytniki nośników papierowych (taśmy i karty perforowane) umożliwiające dość szybkie
wprowadzenie większych programów i danych przygotowanych na oddzielnych urządzeniach
(perforatorach);
3) drukarki (wierszowe i głowicowe) umożliwiające dość szybki wydruk wyników.
Nieco później pojawiły się przewijaki taśm magnetycznych, umożliwiające wielokrotny zapis
i odczyt większych ilości danych przeznaczonych do czasowego przechowania (na przykład
pośrednich wyników obliczeń lub wyników kompilacji programów).
Programowanie obsługi przez komputer urządzeń zewnętrznych (czyli prawidłowego
komunikowania się komputera z nimi) należało do najtrudniejszych i najbardziej
skomplikowanych zadań programistycznych - i tak jest po dzień dzisiejszy.
W początkowym okresie komputery były bardzo drogimi urządzeniami. Czas ich pracy był
wyceniany bardzo wysoko i w związku z tym zaczęły pojawiać się koncepcje mające na celu
zaoszczędzenie czasu ich używania przez poszczególnych użytkowników.
Podstawowe koncepcje to:
1) zatrudnianie zawodowych operatorów, którzy szybko i bezbłędnie komunikowali się
z komputerem za pośrednictwem konsoli;
2) przygotowywanie na oddzielnych urządzeniach większej liczby tekstów programów i zestawów
danych dla nich i łączenie ich we wsady (batch), które umożliwiają natychmiastowe rozpoczęcie
wykonywania następnego programu po zakończeniu poprzedniego (lub odrzuceniu go z powodu
błędów);
3) uruchamianie na początku pracy komputera specjalnego programu przebywającego w jego
pamięci przez cały czas i zawiadującego współpracą komputera z urządzeniami zewnętrznymi
oraz kolejnością uruchamiania innych programów.
Trzecia z powyższych koncepcji doprowadziła do powstania systemów operacyjnych, których
pierwotną postacią był tak zwany monitor rezydujący. Był to program przebywający przez cały
czas w pamięci operacyjnej komputera i wprowadzający do pamięci i nadzorujący wykonywanie
innych programów (przekazanie im sterowania i przejęcie go z powrotem po ich zakończeniu).
O kolejności wprowadzania programów decydował operator podając odpowiednie polecenia
z konsoli, mogło to być też zautomatyzowane poprzez użycie tak zwanych kart sterujących
poprzedzających karty z zapisem treści programu.
Jedną z podstawowych funkcji operatora systemu komputerowego (poza fizyczną obsługą urządzeń
wejścia/wyjścia - wkładaniem i wyjmowaniem nośników papierowych, zakładaniem i zdejmowaniem
taśm magnetycznych) było zainicjowanie pracy komputera po włączeniu zasilania. Ponieważ
komputery nie posiadały pierwotnie pamięci trwałych (ROM) umożliwiających automatyczny start,
operator musiał ręcznie (przy użyciu kluczy) wprowadzić do pamięci kilka rozkazów maszynowych
umożliwiających wczytanie z nośnika papierowego bardziej złożonego programu (odpowiednika
dzisiejszego BIOS-u), który z kolei umożliwiał wczytanie systemu operacyjnego z taśmy
magnetycznej.
Rozwój systemów operacyjnych miał na celu jak najefektywniejsze wykorzystanie możliwości
sprzętu komputerowego i zawsze był nierozłącznie związany z rozwojem samego sprzętu.
Ponieważ urządzenia zewnętrzne zawsze działają znacznie wolniej od samego komputera (jego
procesora), dążono do jak największego uniezależnienia pracy procesora od czasu wprowadzania
danych i wyprowadzania wyników. Uzyskiwano to poprzez:
1) odchodzenie od współpracy procesora bezpośrednio z urządzeniami operującymi na nośniku
papierowym (są zdecydowanie najwolniejsze) poprzez przepisywanie danych i wyników na/z
taśmy magnetyczne przy użyciu wyspecjalizowanych urządzeń lub mniejszych, pomocniczych
komputerów (tak zwane przetwarzanie satelitarne);
2) buforowanie, czyli wydzielenie fragmentu pamięci operacyjnej do celów komunikacyjnych
i umożliwienie urządzeniom zewnętrznym (po zainicjowaniu transmisji przez procesor) niezależny
(autonomiczny) zapis/odczyt do/z tego fragmentu;
3) spooling, związany z zastępowaniem taśm magnetycznych dyskami magnetycznymi (szybszy
dostęp do danych), będący w istocie buforowaniem na dysku większej ilości zarówno danych, jak
i programów, i umożliwiający wykonywanie programów i wykorzystywanie zestawów danych
w innej kolejności, niż były wczytane.
Wspólną ideą powyższych rozwiązań było dążenie do zrównoleglenia pracy procesora i urządzeń
zewnętrznych poprzez zapewnienie im jak największej niezależności czasowej od siebie (czyli
spowodowanie, aby mogły one pracować asynchronicznie). Oczywiście nawet przy pełnym
zrównolegleniu nie można oczekiwać, że łączny czas pracy urządzeń zewnętrznych oraz pracy
procesora będą sobie równe - zadania obliczeniowe wymagające istotnie większego nakładu czasu
procesora nazywamy zadaniami zorientowanymi na przetwarzanie (typowe przykłady - duże
obliczenia naukowe i inżynierskie), zaś wymagające głównie pracy urządzeń zewnętrznych -
zadaniami zorientowanymi na operacje wejścia/wyjścia (typowy przykład - drukowanie
rachunków).
Uwaga
Istotną cechą architektury współczesnych komputerów jest wyposażenie prawie wszystkich urządzeń
zewnętrznych (oraz komunikujących się z nimi układów elektronicznych wewnątrz komputera)
w wyspecjalizowane mikroprocesory zawiadujące ich pracą i komunikacją. W tym sensie prawie
każdy współczesny system komputerowy jest maszyną równoległą, w której jeden (lub więcej)
procesorów ogólnego użytku oraz pewna liczba procesorów wyspecjalizowanych pracują w dużym
stopniu współbieżnie.
Po utworzeniu ośrodków obliczeniowych i zatrudnieniu w nich wykwalifikowanego personelu (a tym
samym uniemożliwieniu użytkownikom bezpośredniej styczności z komputerem) podstawowym
problemem stał się długi łączny czas przetwarzania zadania z punktu widzenia pojedynczego
użytkownika (zaniesienie danych i programu, odebranie następnego dnia wydruków komunikatów
o błędach, dostarczenie skorygowanych danych ...). Użytkownicy byli zmuszeni do bardzo dużej
koncentracji przy przygotowywaniu tekstów programów i danych, jak również musieli przygotowy-
wać dla operatorów komputerów szczegółowe instrukcje postępowania, uwzględniające wszystkie
możliwe scenariusze rozwoju sytuacji w trakcie przetwarzania.
Pierwszym krokiem w kierunku rozwiązania problemu stało się wprowadzenie wielozadaniowości
(wieloprogramowości) systemów komputerowych. Systemy operacyjne zaczęto konstruować tak,
aby mogły ładować do różnych fragmentów pamięci komputera wiele programów jednocześnie
i wykonywać je kawałkami, przerzucając sterowanie od jednego programu do drugiego wtedy, kiedy
wykonywany program musi na coś zaczekać (na przykład na transmisję większego bloku danych):
program A program B program A program C program B
Wiązało się to z zaprojektowaniem systemu operacyjnego tak, aby skutecznie radził sobie z:
a) zarządzaniem przydziałem pamięci; b) planowaniem przydziału procesora programom.
Wielozadaniowość zmniejszyła średni czas przetwarzania pojedynczego zadania, ale w dalszym
ciągu nie umożliwiła użytkownikom bezpośredniej współpracy z komputerem. Rozwiązaniem
okazało się dopiero:
1) rozwinięcie technologii monitorów ekranowych (oszczędność ogromnej ilości papieru);
2) konstrukcja systemów operacyjnych z podziałem czasu, czyli takich, w których wielu
użytkowników może współpracować z jednym komputerem pozornie jednocześnie w trybie
interakcyjnym (czyli w trybie wymiany komunikatów: polecenie użytkownika - odpowiedź
systemu) korzystając z wielu podłączonych do komputera terminali składających się z monitora
ekranowego i klawiatury (nieco później również myszy).
Konstrukcja dobrze działających systemów operacyjnych z podziałem czasu okazała się bardzo
trudna. Tylko w najprostszych przypadkach można było stosować cykliczny przydział równych
kwantów czasu wszystkim użytkownikom. Na ogół stosowany jest system priorytetów zadań
i zmienna długość oraz kolejność przydzielanych odcinków czasu.
Współczesne rozwiązania i tendencje rozwojowe
1) Wielodostęp vs. komputery indywidualne
Ogromny rozwój technologii półprzewodnikowych i związany z nim gwałtowny spadek cen
sprzętu komputerowego umożliwił pod koniec lat 70-tych produkcję małych komputerów
dostępnych dla dużej liczby indywidualnych nabywców (personal computer). Początkowo były
one wyposażane w bardzo proste, jednozadaniowe systemy operacyjne umożliwiające
gospodarowanie niewielkimi zasobami komputera. W następnych latach komputery osobiste
zrobiły nieoczekiwanie dużą karierę, zaspokajając część zapotrzebowania na moc obliczeniową
małych firm i prywatnych osób. Współczesne PC-ty mają moc obliczeniową i pojemność pamięci
operacyjnej oraz zewnętrznej wielokrotnie przewyższającą zasoby większości dużych komputerów
sprzed 20 lat.
Przeciwieństwem komputerów osobistych są teraz naprawdę duże (wieloprocesorowe, o pojem-
ności pamięci operacyjnej rzędu gigabajtów) komputery wielodostępne (mainframe). Oferują one
swoim użytkownikom czasowy dostęp do zasobów znacznie większych, niż w PC-tach.
2) Systemy skupione vs. systemy rozproszone
We współczesnych dużych komputerach coraz częściej instalowanych jest wiele procesorów, co
umożliwia: a) rzeczywiste (nie pozorne) wykonywanie zadań wielu użytkowników jednocześnie;
b) realizację rzeczywistej (nie pozornej) współbieżności procesów wspólnie pracujących nad
zadaniem obliczeniowym jednego użytkownika. Systemy komputerowe wieloprocesorowe ogólnie
dzielimy na:
a) systemy skupione (ściśle powiązane) - procesory mają wspólną pamięć operacyjną i zegar,
odległości pomiędzy elementami są niewielkie (wspólna obudowa);
b) systemy rozproszone (luźno powiązane) - procesory mają odrębne pamięci operacyjne i są
taktowane oddzielnymi zegarami (czyli pracują asynchronicznie), mogą być zarówno zbiorem
oddzielnych płyt umieszczonych we wspólnej obudowie (cluster), jak i zbiorem oddzielnych
komputerów połączonych kablami lub łączami telekomunikacyjnymi w sieć.
Konstrukcja zarówno systemów operacyjnych przeznaczonych dla wieloprocesorów, jak i rozproszo-
nych systemów operacyjnych jest zadaniem bardzo trudnym i jest obecnie przedmiotem wielu prac
naukowych.
Wskutek gwałtownego rozwoju technologii cyfrowych łącz telekomunikacyjnych w ostatnich latach,
model sieciowy systemu komputerowego bardzo się rozpowszechnił i prawdopodobnie jego znaczenie
będzie nadal rosło. Obecne komputery indywidualne często pełnią rolę terminali inteligentnych, czyli
same przetwarzają takie zadania, które nie przekraczają ich możliwości, a w przypadku zapotrzebowania
na większą moc obliczeniową lub pojemność pamięci komunikują się przez sieć lokalną lub Internet
z dużymi komputerami, często zgrupowanymi w farmie serwerów. Terminale mogą komunikować się
z serwerami w trybie tekstowym, ale coraz częściej komunikują się w trybie graficznym - są wtedy
określane jako graficzne stacje robocze.
Zaletą systemów sieciowych jest zarówno lepsze wykorzystanie sprzętu, jak i informacji przechowy-
wanych w pamięciach komputerów. Istotne są też aspekty niezawodnościowe i komunikacyjne.
Podział systemów operacyjnych ze względu na czas reakcji
Systemy operacyjne pracujące w trybie wsadowym (bardziej ogólnie: programy komputerowe,
których czas reakcji na podanie danych może być dowolnie długi) nazywamy też systemami
pracującymi off-line. Ich przeciwieństwem są systemy, których czas reakcji musi zmieścić się
w z góry określonych granicach (zazwyczaj systemy interakcyjne) - nazywamy je pracującymi
on-line. Szczególnym przypadkiem tych drugich są systemy, od których wymagana jest praktycznie
natychmiastowa reakcja na dane (w każdym razie przed nadejściem następnych) - nazywamy je
systemami czasu rzeczywistego (real-time). Typowymi przykładami systemów czasu rzeczywistego
są systemy sterujące procesami technologicznymi w zakładach przemysłowych.
Podsumowanie
Głównymi celami tworzenia systemów operacyjnych są:
1) wygoda użytkowników komputerów;
2) efektywność wykorzystania sprzętu komputerowego;
3) niezawodność pracy systemów komputerowych.
2. SPRZĘTOWE MECHANIZMY OCHRONY PROCESÓW OBLICZENIOWYCH
procesor
urządzenia
pamięć zewnętrzne
operacyjna
Płyta główna
Ogólny schemat logiczny komputera (pomijający organizację magistral, pamięć podręczną itd.)
W pamięci komputera może jednocześnie przebywać wiele programów (wśród nich system
operacyjny) oraz danych dla nich.
program 1
dane1
program 5
dane 5 Przykładowa zawartość pamięci operacyjnej
(dane niekoniecznie muszą być umieszczane
w pobliżu programów, które z nich korzystają)
program 17
dane 17
Programem (wykonywalnym) nazywamy zapis ciągu rozkazów (instrukcji) przeznaczonych do
wykonania przez procesor komputera.
Procesem (obliczeniowym) nazywamy „wykonywanie programu”, czyli ciąg stanów, jakie kolejno
przyjmuje procesor (jego rejestry) oraz obszar danych programu w związku z wykonywaniem przez
procesor tego programu (uwaga: kolejność wykonywanych instrukcji nie musi być taka sama, jak
kolejność ich zapisu w programie).
Pierwotnie użytkownik komputera sprawował nad nim „nieograniczoną władzę” - miał dostęp do
wszystkich instrukcji procesora, wszystkich lokat pamięci operacyjnej oraz wszystkich portów.
Dawało mu to pełną swobodę poczynań, ale jednocześnie nieograniczone możliwości szkodzenia
zarówno samemu sobie, jak i innym użytkownikom komputera.
Obecnie tak szerokie możliwości mają jedynie:
1) użytkownicy komputerów indywidualnych (którzy świadomie chcą z tych możliwości korzystać);
2) administratorzy wielodostępnych systemów komputerowych.
Zadaniem systemu operacyjnego jest chronienie sprzętu komputerowego oraz wykonywanych na nim
procesów obliczeniowych przed uszkodzeniem przez nieświadomych lub złośliwych użytkowników.
Jakiego rodzaju szkody może wyrządzać błędny program ?
1) Niewłaściwa komunikacja z urządzeniem zewnętrznym może spowodować:
a) utratę części bądź całości danych przekazywanych do / z urządzenia;
b) trwałe uszkodzenie urządzenia lub jego przedwczesne zużycie.
2) Program użytkownika może się zapętlić i angażować procesor komputera aż do wyłączenia
zasilania (restartu systemu).
3) Program użytkownika może wywołać rozkaz zatrzymania pracy procesora (zazwyczaj jest to
rozkaz halt).
4) Program może błędnie zmodyfikować swój własny kod (zapis) lub zniszczyć swoje dane.
5) W przypadku pracy w systemie wielozadaniowym program może również zniszczyć programy
i dane należące do innych użytkowników (zarówno w pamięci operacyjnej, jak i na nośnikach
zewnętrznych). W szczególności może popsuć system operacyjny.
Nawet najlepiej zaprojektowane systemy operacyjne nie są w stanie zapobiegać takim zjawiskom,
jeśli nie umożliwiają tego odpowiednie rozwiązania sprzętowe. Ogólnie, systemy operacyjne są
projektowane pod kątem funkcjonowania na konkretnym sprzęcie, a dokładniej, na sprzęcie od
którego można oczekiwać pewnych konkretnych własności.
Jakie są minimalne wymogi wobec sprzętu komputerowego, aby można było zapewnić ochronę
jemu oraz procesom obliczeniowym ?
Musi być zapewniona możliwość ciągłego śledzenia pewnych aspektów wykonywania programów
użytkowników oraz możliwość natychmiastowej interwencji w przypadku próby wykonania
instrukcji potencjalnie szkodliwej.
Minimum sprzętowe niezbędne w każdym przypadku to posiadanie przez procesor bitu trybu pracy
(co najmniej jednego). Tradycyjnie był to jeden bit, którego ustawienie na 1 oznaczało tryb
użytkownika, zaś ustawienie na 0 - tryb monitora (nadzorcy systemu).
Instrukcje procesora, które są potencjalnie niebezpieczne (w sensie wyżej wymienionych zagrożeń)
załadowane do rejestru rozkazów procesora będącego w trybie pracy użytkownika powodują:
1) przełączenie trybu pracy na tryb nadzorcy;
2) zarzucenie wykonywania programu użytkownika i przejście do wykonywania kodu systemu
operacyjnego.
Aby stwierdzić, czy instrukcja pobrana do wykonania jest potencjalnie niebezpieczna, procesor musi
przeanalizować: 1) co dana instrukcja ma wykonać; 2) na czym (na jakim miejscu w pamięci) ma być
wykonana. Musi zatem sprawdzić zarówno część kodową instrukcji, jak i jej część adresową.
Instrukcje potencjalnie niebezpieczne ze względu na swoją część kodową (na przykład instrukcja
zawieszenia lub zatrzymania procesora) nazywane są czasem uprzywilejowanymi (dawniej
nazywano je nielegalnymi). Instrukcje odwołujące się do lokat w pamięci, które nie zostały
przydzielone danemu procesowi, powodują błąd adresowania.
Opisany wyżej przeskok do wykonywania innego programu, połączony z zapamiętaniem informacji
o aktualnym stanie procesora (w tym jego wskaźnika instrukcji) nazywany jest przerwaniem
(interrupt). Mechanizm przerwania jest dużo bardziej ogólny i niekoniecznie musi wiązać się z próbą
wykonania „niebezpiecznego” rozkazu przez program użytkownika. Możliwość wywoływania
i obsługi przerwań wykorzystywana jest w praktycznie wszystkich procesorach, niezależnie od
istniejących w nich zabezpieczeń.
Ogólna idea przerwań: procesor powinien móc natychmiast reagować na zdarzenia asynchroniczne,
czyli takie, których moment pojawienia się był niemożliwy do przewidzenia (nie wynikał w logiczny
sposób z ciągu wykonywanych instrukcji programu).
Ogólna klasyfikacja przerwań:
1) przerwania sprzętowe (hardware interrupt);
2) przerwania związane z sytuacjami wyjątkowymi (exception interrupt);
3) przerwania programowe (software interrupt).
ad. 1) Przerwania sprzętowe mogą pochodzić od autonomicznie pracujących układów zawiadujących
pracą urządzeń zewnętrznych (kontrolerów urządzeń), które na przykład mogą powiadamiać
procesor o zakończeniu transmisji danych. Mogą też pochodzić od zegara systemowego, co
umożliwia procesorowi cykliczne przeprowadzenie pewnych rutynowych czynności (nie musi
tego wykorzystywać).
ad.2) Przerwania związane z sytuacjami wyjątkowymi pozwalają reagować nie tylko na próby
wykonania instrukcji niebezpiecznych, ale również na próby wykonania instrukcji uznanych
za błędne z innych powodów (na przykład próba dzielenia przez zero).
ad.3) Mechanizm przerwań objawił tak wiele zalet, że w listach instrukcji procesorów przewidziano
instrukcję „specjalnego” wywołania przerwania przez program użytkownika (to już trudno
nazwać „zdarzeniem asynchronicznym”), która powoduje zapamiętanie bieżącego stanu
procesu i przeskok do podprogramu obsługi przerwania.
Typowy ciąg czynności procesora po otrzymaniu sygnału przerwania:
1) identyfikacja przyczyny przerwania;
2) stwierdzenie, czy przerwanie może być obsłużone (być może sygnał przerwania nadszedł
w trakcie obsługiwania innego przerwania o wyższym priorytecie, wtedy obsługiwanie
przerwań o niższym priorytecie jest czasowo zamaskowane);
3) jeśli może być obsłużone, zapamiętywana jest bieżąca zawartość rejestrów procesora i ustawiana
jest maska sygnałów;
4) następuje przeskok do odpowiedniego miejsca w pamięci (czyli wpisanie odpowiedniej wartości
do wskaźnika instrukcji, zależnej od wykrytej przyczyny przerwania) i wykonanie podprogramu
obsługi przerwania;
5) na ogół (jeśli umożliwia to przyczyna przerwania) po zakończeniu wykonywania podprogramu
obsługi przerwania następuje odtworzenie zapamiętanego stanu rejestrów procesora i powrót do
programu, z którego nastąpił wyskok.
Jak zatem systemy operacyjne dysponujące mechanizmem przerwań radzą sobie z wymienionymi
wcześniej rodzajami błędów w programach użytkowników ?
1) Każda próba bezpośredniego wykonania operacji na urządzeniach zewnętrznych przez program
użytkownika będzie uznana za nielegalną - użytkownik może tylko poprosić o taką przysługę
system operacyjny (wywołać funkcję systemową);
2) Zapętleniu zapobiega możliwość czasowego przyjmowania przerwań od zegara systemowego
i wykorzystanie ich obsługi do zatrzymania zapętlonego procesu;
3) Instrukcja zatrzymania (oraz inne instrukcje uprzywilejowane) nie będą wykonane z poziomu
programu użytkownika;
4) i 5) Ochrona zawartości pamięci operacyjnej musi być zapewniona sprzętowo. Procesy mają
przydzielone oddzielne segmenty pamięci na kody programów oraz na dane. Z każdym segmentem
związane są dwa rejestry: rejestr bazowy oraz rejestr graniczny. W rejestrze bazowym pamiętany
jest początkowy adres w danym segmencie, a w rejestrze granicznym - wielkość segmentu.
wzrost segment base base
adresów pamięci base + limit limit
Część adresowa instrukcji pobranej do wykonania z programu użytkownika musi mieścić się
w przedziale wyznaczonym przez zawartości tych dwóch rejestrów (dotyczy to rejestrów zwią-
zanych z segmentem danych w przypadku instrukcji operujących na danych, zaś rejestrów
związanych z segmentem kodu w przypadku instrukcji skoku). W przypadku wykrycia przekroczenia
system zapamiętuje przyczynę i przechodzi do uprzywilejowanego trybu pracy (przeskakuje do
odpowiedniego podprogramu obsługi przerwania).
Ochrona dostępu do plików realizowana jest przez wielozadaniowe systemy operacyjne programowo.
Każdy plik musi mieć określonego właściciela oraz prawa dostępu. Właściciel pliku ustanawia
prawa dostępu do niego (będzie to bardziej szczegółowo omówione przy okazji omawiania
konkretnych systemów). Ponieważ procesy również mają swoich właścicieli, przy każdym odwołaniu
procesu do pliku system operacyjny sprawdza najpierw, czy odwołanie to jest legalne z punktu
widzenia obowiązujących praw.
Ochrona plików jest zatem dwojakiego rodzaju:
1) niskopoziomowa (podobnie jak dostępu do każdego innego urządzenia zewnętrznego);
2) wysokopoziomowa (w sensie wymuszenia przestrzegania praw własności).
Oddzielnym problemem jest chronienie procesów przed skutkami awarii sprzętu w systemach
komputerowych, od których sprawności działania bardzo dużo zależy. Tu również możliwości
systemów operacyjnych w istotny sposób zależą od własności sprzętu (wykrywanie czasowego
zaniku zasilania i awaryjne zapamiętywanie najważniejszych danych, instalowanie dodatkowych
procesorów i dublowanie obliczeń na wypadek awarii jednego z nich itd.).
3. PODSTAWOWE FUNKCJE SYSTEMÓW OPERACYJNYCH
Głównym celem istnienia systemu operacyjnego jest wygoda użytkowników komputera. System musi
zatem umieć przyjmować polecenia od użytkowników i je wykonywać (lub zgłaszać, że wykonać ich
nie jest w stanie, lub stwierdzać składniową niepoprawność wydanego polecenia). Jedną z podstawo-
wych części składowych każdego systemu operacyjnego przeznaczonego do bezpośredniego komuniko-
wania się z użytkownikiem jest interpretator poleceń (interpreter), który współpracuje z użytkowni-
kiem w sposób interaktywny (konwersacyjny), czyli prowadząc z nim dialog.
Gdyby funkcjonowanie systemu operacyjnego porównać do funkcjonowania dużej firmy, interpreter
poleceń pełniłby w niej rolę „biura obsługi klienta”.
Język porozumiewania się użytkownika z systemem musi podlegać pewnym formalnym regułom
składniowym (gramatycznym). Projektanci systemów dążą do tego, aby język ten był możliwie prosty
i wymagał jak najmniejszej wiedzy o konstrukcji systemu. Dlatego też zakładają zawsze pewien
uproszczony, abstrakcyjny (tak zwany logiczny) model systemu komputerowego, w którym są
pominięte różne nieistotne z punktu widzenia użytkownika szczegóły jego fizycznej konstrukcji.
Podstawowy sposób prowadzenia dialogu użytkownika z interpreterem poleceń to naprzemienne
wypisywanie tekstów (obecnie na ekranie monitora, kiedyś na papierze konsoli operatorskiej).
Dążąc do maksymalnego uproszczenia porozumiewania się, w połowie lat 80-tych zaprojektowano
pierwsze graficzne interfejsy użytkownika, które umożliwiają wydawanie poleceń dla systemu bez
wypisywania tekstów, tylko poprzez wykonywanie operacji przy użyciu wskaźnika (pointer) na
wyświetlanych na ekranie obiektach graficznych. Zaoszczędza to użytkownikom konieczności
pamiętania składni poleceń tekstowych (która jest na ogół traktowana jako uciążliwość) i przyspiesza
komunikację.
Tryb tekstowy wydawania poleceń w dalszym ciągu uważany jest jednak za podstawowy tryb
porozumiewania się z systemem, gdyż daje dużo szersze możliwości, niż tryb graficzny (na ogół
obejmujący tylko najbardziej „pospolite” czynności). Co więcej, tryb tekstowy umożliwia
konstruowanie poleceń złożonych oraz tworzenie plików wsadowych (skryptów) zawierających
całe ciągi poleceń, które na przykład rutynowo wykonuje codziennie administrator systemu.
Pewną namiastką możliwości zapamiętywania ciągów poleceń w trybie tekstowym jest możliwość
tworzenia makr w trybie graficznym.
Jakiego rodzaju typowe usługi są oczekiwane od systemu operacyjnego przez użytkowników ?
Jest ich dużo, najbardziej typowe przykłady to:
1) ładowanie do pamięci i uruchamianie programów;
2) obsługa systemu plików (tworzenie i usuwanie plików i katalogów);
3) obsługa urządzeń zewnętrznych (na przykład drukowanie);
4) jeśli system jest wielodostępny, to umożliwienie otwarcia i zamknięcia sesji pracy (zalogowanie
i wylogowanie się);
5) jeśli system jest sieciowy, to umożliwienie komunikacji w sieci (przesyłanie plików, zdalne
sesje pracy, porozumiewanie się w czasie rzeczywistym);
6) jeśli system wielodostępny jest używany do celów komercyjnych, to umożliwienie rozliczania się
z użytkownikami na podstawie rejestrowania wykorzystywania przez nich zasobów systemowych.
Każda z tego rodzaju usług jest, z punktu widzenia programisty, czynnością bardzo skomplikowaną.
W trakcie jej wykonywania system musi (w sposób niewidoczny dla użytkownika) dbać o bardzo
wiele różnych jej aspektów - musi prawidłowo i bezkonfliktowo przydzielać zasoby wielu procesom
wykonywanym współbieżnie, oszczędnie gospodarować pamięcią, chronić dane przed zniszczeniem itp.
Ze względu na swój stopień komplikacji systemy operacyjne (tak jak każdy duży program) mają
strukturę warstwową. Oznacza to, że konstrukcja ich opiera się na dużej liczbie podprogramów,
które są uporządkowane hierarchicznie - na bazie zbioru najbardziej podstawowych procedur oparte
są procedury wykonujące bardziej złożone czynności itd.
Ogólnie, zbiór podprogramów wykorzystywanych jako „elementy konstrukcyjne” systemu
operacyjnego i wykonywanych w całości w trybie jądra systemu (czyli w trybie, w którym mogą być
wykonywane operacje uprzywilejowane) nazywamy funkcjami systemowymi. Najbardziej podstawowe
funkcje systemowe wykonujące bezpośrednio operacje na urządzeniach i lokatach pamięci nazywamy
niskopoziomowymi.
Co najmniej część spośród funkcji systemowych może być wywoływanych z poziomu procesów
działających w trybie użytkownika - następuje wtedy czasowe przełączenie z trybu użytkownika
w tryb jądra i „fachowe” wykonanie żądanej usługi przez system operacyjny, a następnie zwrócenie
sterowania do procesu użytkownika. Te funkcje systemowe, które są dostępne dla programistów
tworzących programy przeznaczone do pracy w trybie użytkownika nazywane są wywołaniami
systemowymi, a ich zbiór zaimplementowany w postaci biblioteki w konkretnym języku
programowania (na przykład w C lub BASIC-u) - interfejsem programisty w danym języku.
Klasyfikacja rodzajów funkcji systemowych (według [A. Silberschatz et al.])
1) Funkcje związane z nadzorowaniem procesów:
a) utworzenie, załadowanie, wykonanie, zakończenie (lub zaniechanie), usunięcie procesu;
b) zawieszenie, wznowienie procesu;
c) pobranie, określenie (ustawienie) atrybutów procesu;
d) przydział pamięci, zwolnienie pamięci przydzielonej procesowi.
2) Funkcje wykonujące operacje na plikach:
a) utworzenie, usunięcie pliku;
b) otwarcie, zamknięcie pliku;
c) odczyt z pliku, zapis do pliku;
d) pobranie, określenie (ustawienie) atrybutów pliku.
3) Funkcje wykonujące operacje na urządzeniach:
a) żądanie przydzielenia urządzenia, zwolnienie urządzenia;
b) logiczne przyłączenie, logiczne odłączenie urządzenia;
c) odczyt z urządzenia, zapis do urządzenia;
d) pobranie atrybutów urządzenia, określenie (ustawienie) atrybutów urządzenia.
4) Funkcje operujące na informacji systemowej:
a) pobranie czasu lub daty, określenie (ustawienie) czasu lub daty;
b) pobranie danych systemowych, określenie (ustawienie) danych systemowych.
5) Funkcje komunikacyjne
a) utworzenie połączenia, usunięcie połączenia;
b) nadanie komunikatu, odebranie komunikatu;
c) nadanie, odebranie informacji o stanie połączenia;
d) przyłączenie, odłączenie urządzenia wymiennego.
Interfejs funkcji systemowych wykorzystują programiści (wymaga to dość zaawansowanej wiedzy
technicznej), natomiast użytkownicy systemu komputerowego korzystają z możliwości wydawania
poleceń systemowych interpreterowi poleceń, który prezentuje użytkownikom znacznie prostszy
(będący na wyższym poziomie abstrakcji) logiczny model komputera. Interpreter (przyjmujący
polecenia w trybie tekstowym lub w trybie graficznym) albo sam zajmuje się obsługą wydanego
polecenia (jeśli jest to jego polecenie wewnętrzne), albo uruchamia w celu obsłużenia go inny
program systemowy (mówimy wtedy o poleceniu zewnętrznym). W gruncie rzeczy z punktu
widzenia interfejsu użytkownika nie ma różnicy pomiędzy wywołaniem i wykonaniem polecenia
wewnętrznego, programu systemowego lub dowolnego innego programu zainstalowanego lub
utworzonego przez użytkownika.
Polecenia systemowe w trakcie swojego wykonywania dokonują zazwyczaj wielu wywołań
systemowych (na przykład otwarcie, odczyt i zamknięcie pliku).
Jest rzeczą w dużym stopniu umowną, jakie elementy określa się jako części składowe systemu
operacyjnego. Przyjmuje się, że sterowniki urządzeń oraz jądro systemu składające się ze zbioru
funkcji systemowych oraz abstrakcyjnych struktur danych, na których te funkcje operują, stanowią
jego integralne elementy. Często przyjmuje się również, że programy wykonywane na poziomie
użytkownika, ale standardowo udostępniane przez producenta wraz z jądrem (interpreter komend
oraz stowarzyszone z nim programy, edytory tekstu, kompilatory, programy konfiguracyjne i dia-
gnostyczne itp.) również stanowią elementy systemu.
Koncepcja maszyny wirtualnej
Systemy wielodostępne z podziałem czasu są konstruowane w taki sposób, żeby ich użytkownicy
pracujący jednocześnie przy wielu terminalach nie mogli sobie wzajemnie przeszkadzać. Często
system zapewnia wręcz poszczególnym użytkownikom całkowitą izolację od innych tak, że mają
wrażenie, że są jedynymi użytkownikami komputera (co najwyżej nieco wolniejszego i o mniejszej
pojemności pamięci). Taki logiczny obraz systemu z punktu widzenia pojedynczego użytkownika
nazywany jest maszyną wirtualną. Każda maszyna wirtualna pozorowana przez dany system
zapewnia taki sam interfejs programisty i interfejs użytkownika - umożliwia wywoływanie funkcji
systemowych i ich wykonywanie w trybie jądra, udostępnia egzemplarz interpretera komend oraz
innych programów realizujących polecenia systemowe, udostępnia wirtualny dysk do zapisu plików
oraz inne wirtualne urządzenia zewnętrzne.
Wykonywanie programów na maszynach wirtualnych związane jest między innymi z translacją
(przeliczaniem) adresów w wykonywanych instrukcjach programów tak, aby niezależnie od
umiejscowienia programu we wspólnej pamięci fizycznej był on zawsze wykonywany w taki sam
sposób, i nie powodował kolizji z innymi wykonywanymi współbieżnie programami.
4. ZARZĄDZANIE PROCESAMI
Pojęcie procesu jest trudne do formalnego zdefiniowania. Określenie podane w wykładzie 2 jest
nieprecyzyjne, gdyż nie uwzględnia wielu możliwości stwarzanych przez współczesne systemy
operacyjne - na przykład wykonywania wielu procesów jednocześnie i ich komunikowania się przy
użyciu współdzielonego segmentu pamięci. Nie uwzględnia też czasowego przekazywania sterowania
z programu użytkownika do jądra systemu. Intuicyjnie przez proces rozumiemy ciąg kolejno wykony-
wanych przez procesor i logicznie powiązanych ze sobą instrukcji. Z punktu widzenia systemu
operacyjnego każdy proces jest skojarzony z logiczną strukturą danych utrzymywaną przez jądro
systemu i zawierającą niezbędne informacje pozwalające systemowi decydować, kiedy proces
powinien być wykonywany, a kiedy zawieszony, kiedy jakie zasoby systemowe może mieć
przydzielone, czy należy czasowo zawartość jego segmentów pamięci przepisać na dysk do pliku
wymiany, jakim kosztem jego wykonywania należy obciążyć jego właściciela itp.
Co należy znać, aby scharakteryzować jakiś proces w pewnym momencie jego wykonywania ?
- kod binarny jego programu;
- dane zapisane w pamięci (wartości stałych i aktualne wartości zmiennych);
- zawartość stosu;
- zawartości rejestrów procesora, a w szczególności wskaźnika instrukcji;
- usytuowanie programu, danych i stosu w pamięci;
- aktualny przydział zasobów (np. dostęp do urządzeń zewnętrznych);
i wiele innych danych przechowywanych przez system operacyjny. W szczególności trzeba znać
aktualny stan procesu.
Ogólny graf stanów procesu:
nowy gotowy aktywny zatrzymany
czekający
Podstawowe dane o procesie wykorzystywane przez system operacyjny do planowania i zarządzania
są zebrane w pamięci jądra systemu w postaci bloku kontrolnego procesu (Process Control Block).
Na ogół zawiera on następujace informacje:
- identyfikator procesu;
- stan procesu (np. wg diagramu na poprzedniej stronie);
- stan rejestrów procesora (utrwalony w momencie czasowego zawieszenia procesu);
- informacje istotne dla planowania przydziału procesora (priorytet procesu, wskaźniki do położeń
w kolejkach szeregujących zamówienia na zasoby itp.);
- informacje istotne dla zarządzania pamięcią (zawartości rejestrów bazowych i granicznych, tablic
stron w pamięci itp.);
- informacje do rozliczeń (zużyty czas procesora, zużyte czasy wykorzystania urzadzeń zawnętrznych,
ograniczenia czasowe, numer rachunku i in.);
- informacje o stanie przydziału urządzeń zewnętrznych procesowi (urządzenia aktualnie przydzielone,
zamówienia oczekujące na realizację, wykaz otwartych plików itp.).
Zawartość pamięci operacyjnej przydzielonej procesowi oraz informacje przechowywane w bloku
kontrolnym nazywamy kontekstem procesu.
Dwa procesy nazywamy współbieżnymi, jeżeli każdy z nich rozpoczął się przed zakończeniem
drugiego procesu.
Przykłady
1)
procesy są współbieżne
2)
procesy są współbieżne
3)
procesy nie są współbieżne
Uwaga.
O współbieżności możemy mówić zarówno w przypadku wykonywania procesów na tym samym
komputerze, jak i na oddzielnych komputerach.
W literaturze można napotkać następujące określenia:
* współbieżny (concurrent)
* równoległy (parallel)
* rozproszony (distributed)
Są one używane w różnych kontekstach. Zazwyczaj pierwsze dwa używane są zamiennie, przy
czym nieco częściej określenie „równoległy” oznacza „wykonywany współbieżnie na tym samym
komputerze”. Natomiast określenie „rozproszony” oznacza „wykonywany współbieżnie na
oddzielnych komputerach połączonych w sieć”.
Obliczenia równoległe (na jednym komputerze) mogą odbywać się jako:
1) rzeczywiście równoległe - jeśli komputer ma wiele procesorów i każdy procesor obsługuje co
najwyżej jeden proces w dowolnej chwili czasu;
2) pozornie równoległe - jeśli czas pracy jednego procesora dzielony jest na krótkie odcinki
przydzielane różnym procesom na zasadzie przeplotu.
Współbieżność wykonywania procesów jest opłacalna ze względu na:
1) lepsze wykorzystanie zasobów fizycznych (sprzętu);
2) lepsze wykorzystanie zasobów logicznych (na przykład informacji zawartych w plikach);
3) przyspieszenie obliczeń (jeśli są wykonywane w warunkach rzeczywistej równoległości);
4) ułatwienie konstrukcji dużych programów (modularność);
5) wygoda użytkowników (przykład: kopiowanie za pośrednictwem schowka w Windows).
Dwa procesy współbieżne nazywamy niezależnymi, jeżeli fakt wykonywania któregokolwiek z
nich w żaden sposób nie wpływa na wykonywanie drugiego. W przeciwnym przypadku procesy
nazywamy zależnymi lub współpracującymi.
Uwaga
W rzeczywistości to, czy dwa procesy uznamy za niezależne, czy za zależne, w wielu przypadkach
zależy od poziomu abstrakcji (ignorowania szczegółów) na jakim rozpatrujemy działanie tych
procesów.
Własności procesu niezależnego:
- żaden inny proces nie komunikuje się z nim (nie wpływa na jego kontekst);
- działa deterministycznie (wynik zależy wyłącznie od danych i stanu początkowego);
- może być dowolną liczbę razy wstrzymywany i wznawiany.
Własności procesu współpracującego:
- jego bieżący stan jest zależny zarówno od jego poprzedniego stanu, jak i od stanów innych
procesów;
- może wykazywać niedeterminizm (obliczane wyniki mogą zależeć od względnej kolejności
wykonywania procesów w systemie);
- zachowanie procesu może nie być przewidywalne (na przykład może czasem zapętlać się,
a czasem nie).
Uwaga
Programy współbieżne powinny być projektowane tak, aby nie wykazywały losowości wykonania.
Tradycyjnie procesy w systemie wykonują się w odrębnych przestrzeniach adresowych pamięci,
mogąc komunikować się ze sobą jedynie za pośrednictwem plików lub łączy komunikacyjnych.
Takie procesy nazywane są czasem procesami ciężkimi (ich utworzenie wiąże się z dość dużym
narzutem czasowym ze strony systemu operacyjnego). Dużo później, niż koncepcja procesu pojawiła
się koncepcja wątku (thread), nazywanego też procesem lekkim. Zbiór wątków pracujących nad
wspólnym zadaniem obliczeniowym wykonuje wspólny kod programu (co najwyżej różne jego
podprogramy) i operuje we wspólnej przestrzeni adresowej.
Uwaga
Rzeczywisty obraz sytuacji we współczesnych systemach operacyjnych jest bardziej skomplikowany,
niż by wynikało z powyższych definicji. Procesy ciężkie mogą mieć wspólny segment kodu (gdyż
jest on przeznaczony tylko do odczytu), dopóki jeden z nich nie wczyta nowego kodu z pliku. Mogą
też współdzielić segment danych, dopóki jest on wykorzystywany przez nie tylko do odczytu.
Ponadto mogą mieć przydzielony dodatkowy segment pamięci wspólnej (dzielonej), który jest
wykorzystywany jako jeden z możliwych środków komunikacji międzyprocesowej.
Wątki posiadają najbardziej istotne cechy procesów - dysponują własnymi zestawami rejestrów,
a w szczególności wskaźnikami instrukcji (zatem mają przydzielone procesory - rzeczywiste lub
wirtualne). Mają też własne stosy (więc mogą wywoływać funkcje). Wiele cech wątków zależy od
konkretnej implementacji - w Linuksie zaimplementowana jest biblioteka wątków odpowiadająca
normie POSIX (pthread).
Jednym z podstawowych problemów, jakie muszą rozstrzygnąć implementatorzy, to czy wątki mają
być reprezentowane w strukturach jądra systemu (jako procesy lekkie) i podlegać szeregowaniu
globalnemu, czy też mają być niewidoczne dla jądra i podlegać szeregowaniu lokalnemu (w obrębie
swojego procesu). W tym drugim przypadku programista może mieć wybór strategii szeregowania
w obrębie poszczególnych procesów. Szeregowanie wiąże się z pojęciem priorytetu (wątki, tak jak
procesy ciężkie, mają swoje indywidualne priorytety): czy priorytety wątków mają być traktowane
jako bezwzględne współczynniki pilności wykonania, czy też mają być relatywizowane względem
priorytetu ich procesu ?
Kolejnym problemem do rozstrzygnięcia są skutki wywołań funkcji systemowych przez poszczególne
wątki. Intencją projektantów jest uniezależnienie innych wątków w procesie od skutków wywołania
funkcji przez jeden z nich. W szczególności wątek czasowo zawieszony z powodu oczekiwania na
dane do wczytania nie powinien blokować pracy innych wątków tego samego procesu.
Uproszczony diagram stanów wątku jest taki sam, jak uproszczony diagram stanów procesu.
Jeżeli jeden proces powołuje do życia drugi proces, to ten pierwszy jest nazywany procesem
rodzicielskim, a ten drugi - procesem potomnym. Tylko proces rozpoczynający pracę systemu
operacyjnego nie ma swojego procesu rodzicielskiego. Proces potomny może otrzymać wstępny
przydział zasobów wprost od systemu operacyjnego (niezależnie od zasobów dzierżawionych przez
proces rodzicielski), ale zazwyczaj dziedziczy zasoby procesu rodzicielskiego. W szczególności
dziedziczy zawartość segmentów pamięci operacyjnej - kod programu i dane. Proces rodzicielski
może też przekazać swojemu potomkowi pewne dane początkowe (wartości zmiennych środowiska).
Możliwe są dwa scenariusze wykonywania procesów:
1) proces rodzicielski zostaje zawieszony aż do zakończenia pracy jego procesu potomnego (tak
jest na przykład w systemie DOS);
2) proces rodzicielski i wszystkie jego procesy potomne wykonują się współbieżnie (tak jest na
przykład w systemie Unix);
Możliwe sposoby zakończenia procesu:
1) zakończenie naturalne (dojście do ostatniej instrukcji, przekazanie informacji rodzicowi);
2) zakończenie przedwczesne (usunięcie przez inny, uprawniony do tego proces);
Jeżeli nastąpi zakończenie procesu, który ma współbieżnie wykonywane procesy potomne, to
również możliwe są różne scenariusze:
1) może nie mieć to wpływu na wykonywanie procesów potomnych (w systemie Unix są one
„adoptowane” przez systemowy proces Init (numer 1) i od tej chwili pamiętają jego numer jako
numer procesu rodzicielskiego);
2) może nastąpić natychmiastowe usunięcie wszystkich procesów potomnych, ich potomków itd.
(całego „drzewa genealogicznego” procesów) - jest to tak zwane zakończenie kaskadowe.
Powyższe określenia i scenariusze dotyczyły procesów w tradycyjnym sensie (procesów ciężkich).
W przypadku wątków relacje pomiędzy rodzicami i potomkami są bardziej zatarte i zależą od
konkretnej implementacji. Na ogół implementatorzy dążą do tego, aby wątki wykonywane we
wspólnej przestrzeni adresowej dostrzegały się wzajemnie jako równoprawne „rodzeństwo”,
niezależnie od tego, który z nich w rzeczywistości zainicjował utworzenie którego wątku.
Zagadnienia planowania przydziału procesora
Procesy umieszczone w pamięci operacyjnej, lecz aktualnie nie wykonywane, a oczekujące na
przydział pewnego zasobu - procesora (procesy gotowe) lub urządzenia zewnętrznego, są ustawiane
w kolejki. Kolejki mogą być obsługiwane na zasadzie FIFO (First In, First Out), lecz częściej
procesy są wybierane z nich według bardziej skomplikowanych reguł (np. przy uwzględnieniu
ich priorytetów ). Proces systemowy, który zajmuje się wybieraniem procesów z kolejek
i przydzielaniem im zasobów, nazywany jest planistą (scheduler). Rola planisty może być
rozdzielona między kilka procesów (np. planista długoterminowy i planista krótkoterminowy).
Na następnym slajdzie podany jest ogólny diagram kolejek w systemie (wg [Silberschatz et al.]).
Zatrzymanie lub zawieszenie wykonywania procesu przez procesor może nastąpić wskutek:
1) zakończenia procesu;
2) przejścia do stanu oczekiwania (na przydział urządzenia zewnętrznego lub przekazanie
informacji o zakończeniu procesu potomnego);
3) przerwania (związanego z upływem kwantu czasu lub nastąpieniem oczekiwanego czy
nieoczekiwanego zdarzenia).
Jeżeli zmiana przydziału procesora może nastąpić tylko w przypadku 1) lub w przypadku 2) ale
tylko na czas oczekiwania na przydział urządzenia, mówimy o niewywłaszczeniowym algorytmie
przydziału procesora. W pozostałych przypadkach mówimy o algorytmie wywłaszczeniowym.
Zmiana przydziału procesora wiąże się z:
1) zapamiętaniem parametrów dotychczasowego procesu (zawartości rejestrów procesora itd.)
w bloku kontrolnym procesu (jeśli proces ma być jeszcze kontynuowany);
2) załadowaniem parametrów kolejnego procesu wybranego przez planistę (nowego lub
kontynuowanego).
Zespół czynności systemu operacyjnego wiążący się z 1) i 2) nazywany jest przełączaniem
kontekstu. Od prędkości przełączania kontekstu w istotnym stopniu zależy wydajność systemu.
Kryteria brane pod uwagę w algorytmach planowania przydziału procesora:
1) wykorzystanie procesora (minimalizacja przestojów);
2) przepustowość systemu (liczba zakończonych procesów w jednostce czasu);
3) czas cyklu przetwarzania (od utworzenia do zakończenia procesu);
4) łączny czas oczekiwania w kolejkach na przydział zasobów;
5) czas reakcji procesu na otrzymane dane (szczególnie w systemach czasu rzeczywistego).
Ostateczny algorytm przydziału procesora realizuje na ogół skomplikowaną funkcję biorącą pod
uwagę powyższe kryteria z uwzględnieniem różnych współczynników wagowych.
5. ZAGADNIENIA KOORDYNACJI PROCESÓW
Przez koordynację procesów będziemy rozumieli ich synchronizację (czyli uwzględnianie
wzajemnych uzależnień czasowych) oraz komunikację (czyli wzajemne przekazywanie sobie
informacji).
Potrzeba synchronizacji jest na ogół związana z komunikacją - informacja nie może zostać odczytana
przez jeden proces, jeśli nie została wcześniej zapisana przez inny proces. Komunikacja pomiędzy
procesami może odbywać się przez kanał komunikacyjny (na przykład realizowany jako bufor
współdzielonego pliku) lub przez pamięć dzieloną (segment pamięci fizycznej udostępniony przez
system operacyjny obydwóm procesom).
Zapisy / odczyty do / z pamięci dzielonej mogą być dokonywane przez oba procesy bez żadnych
ograniczeń, więc muszą być synchronizowane przez dodatkowe mechanizmy systemowe.
Kanał komunikacyjny służy zazwyczaj tylko do komunikacji jednokierunkowej, a mechanizmy
synchronizacji są zawarte w samych procedurach zapisu / odczytu do / z kanału.
Jeśli jeden proces tylko produkuje dane i umieszcza je w pewnym medium komunikacyjnym, a drugi
proces tylko je stamtąd pobiera (w takiej samej kolejności, w jakiej zostały włożone), mówimy, że te
procesy tworzą układ producent - konsument (producer - consumer). Jeśli medium komunikacyjne
posiada pewną pojemność (bufor), działania producenta są w pewnym stopniu niezależne od działań
konsumenta - może on produkować dane „na zapas” i umieszczać je w buforze. Konsument natomiast
nie może „konsumować” danych szybciej, niż dostarcza je producent.
Jeśli pojemność bufora jest nieskończenie duża, proces producenta jest całkowicie niezależny od
procesu konsumenta. Jeśli pojemność bufora jest zerowa, musi zachodzić pełna synchronizacja
pomiędzy produkcją i konsumpcją (włożenie i pobranie każdej danej musi nastąpić w tej samej
chwili). Najbardziej typowym przypadkiem jest skończona, niezerowa pojemność bufora. Bufor
skończony jest często realizowany jako tablica cykliczna.
Niektóre procesy systemowe pełnią rolę serwerów, czyli dostarczycieli określonych usług (na
przykład obliczają jakąś funkcję, podają czas, wysyłają pliki pod podany adres itp.). Serwery
przeważnie przebywają w stanie zawieszenia, oczekując na zgłoszenia procesów potrzebujących
usługi, czyli klientów. Po zgłoszeniu zapotrzebowania klient zostaje przeważnie zawieszony i czeka
na dostarczenie odpowiedzi przez serwer. Potem pobiera odpowiedź, a serwer zostaje ponownie
zawieszony.
serwer aktywność
klient zawieszenie
t
Uwaga
1) Okresowo mogą występować spiętrzenia zgłoszeń klientów przysyłanych szybciej, niż serwer
jest w stanie je obsłużyć. W takim przypadku są one wpisywane do kolejki zgłoszeń serwera
i wybierane stamtąd w takiej samej kolejności, jak przybyły.
2) Serwery dzielą się na iteracyjne i współbieżne. Serwery iteracyjne same (jako jeden proces)
zajmują się pobieraniem zgłoszeń z kolejki, obsługą i odesłaniem odpowiedzi. Serwery współ-
bieżne pobierają tylko zgłoszenia, a następnie tworzą procesy potomne i im zlecają całą dalszą
obsługę. Obecnie większość serwerów jest realizowanych jako współbieżne.
Przekazywanie informacji pomiędzy procesami powinno odbywać się w określonych porcjach.
Byłoby niekorzystne, gdyby proces odczytujący pobrał tylko fragment komunikatu przekazywanego
przez inny proces. Podobnie w przypadku aktualizacji zapisu pewnego rekordu w bazie danych przez
jeden proces, inne procesy nie powinny w tym samym czasie próbować odczytać tego rekordu, gdyż
mogłyby odczytać częściowo nową, a częściowo starą zawartość - otrzymałyby wtedy niespójne dane.
Fragment kodu procesu, który wykonuje operację na współdzielonych danych taką, że w tym samym
czasie żaden inny proces nie powinien operować na tych danych, nazywamy sekcją krytyczną
(critical section) tego procesu. Założenie, że w dowolnej chwili co najwyżej jeden proces (spośród
grupy współpracujących procesów) może wykonywać swoją sekcję krytyczną, nazywamy zasadą
wzajemnego wykluczania (mutual exclusion).
Aby wzajemne wykluczanie mogło być zrealizowane, każde wykonanie sekcji krytycznej musi być
poprzedzone wykonaniem sekcji wejściowej, w której proces zgłasza potrzebę wejścia do sekcji
krytycznej (i czeka na zgodę), a po wykonaniu sekcji krytycznej musi nastąpić wykonanie sekcji
wyjściowej, w której proces informuje inne o opuszczeniu sekcji krytycznej. Pozostałą część kodu
procesu (nie należącą do żadnej z powyższych sekcji) nazywamy resztą.
Przyjęcie założenia o istnieniu sekcji krytycznych pociąga za sobą konieczność spełnienia
następujących warunków:
1) wzajemne wykluczanie;
2) postęp (progress) - jeżeli w pewnym momencie żaden proces nie wykonuje swojej sekcji
krytycznej, a pewna liczba procesów kandyduje do tego (weszła do swoich sekcji wejściowych),
to wybór jednego z nich musi nastąpić w ograniczonym czasie;
3) jeżeli którykolwiek proces wszedł do swojej sekcji wejściowej, to przed otrzymaniem przez niego
zgody na wejście do sekcji krytycznej tylko ograniczona liczba innych procesów może taką zgodę
otrzymać.
Powyższe warunki związane są z unikaniem pewnych niekorzystnych zjawisk, które mogą wystąpić
w przypadku niewłaściwej koordynacji procesów. Te zjawiska to blokada (deadlock) oraz głodzenie
(starvation) procesów.
Blokada zachodzi, gdy pewna liczba procesów jest w stanie oczekiwania na zdarzenie, które może być
wynikiem jedynie wykonywania (postępu) jednego z nich.
Przykład
Przepis prawny obowiązujący w stanie Kanzas na przełomie XIX i XX wieku: „Jeżeli dwa pociągi
zbliżają się do siebie jadąc po krzyżujących się torach, to każdy z nich powinien zatrzymać się i nie
ruszać, dopóki ten drugi nie odjedzie”.
Przykład
Jeśli cztery pojazdy dojadą jednocześnie z różnych stron do skrzyżowania dróg równorzędnych,
każdy z nich powinien udzielić pierwszeństwa pojazdowi po jego prawej stronie.
Przykład
Problem ucztujących filozofów (por. następny slajd).
Problem ucztujących filozofów.
n = 5
Założenia:
a) każdy filozof może przebywać w dwóch stanach: myślenie i jedzenie ;
b) każdy widelec jest zasobem współdzielonym przez dwóch sąsiadów na zasadzie wyłączności
dostępu;
c) do jedzenia potrzebne są dwa widelce;
d) widelce muszą być brane sekwencyjnie (nie jednocześnie) przez jednego filozofa;
e) czasy przebywania w stanach myślenie i jedzenie są zawsze skończone.
Jeżeli wszyscy filozofowie naraz podniosą np. lewe widelce, dojdzie do blokady.
Warunki konieczne i dostateczne powstawania blokad:
1) wzajemne wykluczanie (niepodzielność pewnego zasobu - np. procesora lub wybranej lokaty
w pamięci);
2) brak wywłaszczeń (system nie może „siłą” odbierać pewnych zasobów procesom);
3) oczekiwanie cykliczne (musi istnieć zbiór procesów { P1, P2, ... , Pn }, gdzie n > 1, taki, że P1 czeka
na zasób przetrzymywany przez P2, ... , Pn czeka na zasób przetrzymywany przez P1).
Wynika z tego, że każdy z procesów biorących udział w blokadzie musi mieć przydzielony pewien
zasób i dodatkowo czekać jeszcze na przydzielenie pewnego innego zasobu.
Głodzenie zachodzi, gdy co najmniej jeden proces oczekuje w nieskończoność na przydzielenie
pewnego zasobu (choć teoretycznie mógłby go otrzymać), podczas gdy inne procesy wymieniają się
tym zasobem.
Przykład
Obsługa na zasadzie stosu (last in, first servised).
Do zjawiska głodzenia procesów może doprowadzić niewłaściwie zorganizowany system priorytetów
procesów (współczynników liczbowych przyporządkowanych procesom, które wskazują, na ile
wykonywanie danego procesu jest pilne z punktu widzenia systemu operacyjnego). Jeśli przez cały
czas pojawiają się w systemie procesy o wysokich priorytetach, może się zdarzyć, że pewien proces
o niskim priorytecie nigdy nie będzie wybrany do wykonania. Metodą zapobiegania takiemu zjawisku
jest system priorytetów zmiennych w czasie - priorytet każdego oczekującego na wykonanie procesu
stopniowo rośnie, aż wreszcie w którymś momencie musi on uzyskać przydział procesora. Takie
rozwiązanie jest zastosowane na przykład w systemie Unix.
W języku matematycznej teorii zarządzania procesami własność systemu zapewniająca brak głodzenia
procesów oczekujących na przydział zasobów nazywana jest uczciwością (fairness). Wyróżniane są
różne rodzaje uczciwości w zależności od tego, czy proces zgłasza zapotrzebowanie na zasób przez
cały czas, czy też okresowo, i od tego, czy czas oczekiwania na przydział jest zależny od liczby innych
procesów ubiegających się o ten sam zasób.
W programowaniu procesów współbieżnych istotną rolę odgrywa pojęcie niepodzielności operacji.
W przypadku wykonywania sekcji krytycznej chcemy mieć zapewnione, że proces będzie mógł ją
wykonać jako całość, bez przerywania wykonania przez inne procesy. Jeśli operacja na zasobach
jest wykonywana przez pojedyncze wywołanie funkcji systemowej, to niepodzielność jej wykonania
jest gwarantowana przez system operacyjny (procesy wykonywane w trybie jądra systemu nie
podlegają wywłaszczaniu). Przykładami takich operacji są pojedyncze zapisy i odczyty do / z
kanału komunikacyjnego (na przykład kolejki komunikatów).
Gwarancji niepodzielności nie ma natomiast w przypadku operowania na pamięci wspólnej.
Pojedyncze instrukcje w językach wyższego poziomu, takie jak na przykład x = y + z ; w języku C,
gdzie x, y, z są nazwami zmiennych, którym zostały przyporządkowane lokaty w segmencie
pamięci wspólnej, są tłumaczone na cały ciąg rozkazów maszynowych, którego wykonywanie może
być przeplatane z wykonaniami innych procesów (być może również operujących na tych
zmiennych) w dowolny sposób.
Jeżeli mają być wykonywane operacje na pamięci wspólnej, lub bardziej złożone operacje na
dowolnych zasobach, programista musi sam zadbać o ich zabezpieczenie. Najprostszym mecha-
nizmem abstrakcyjnym służącym do tego celu są semafory. Podstawowym rodzajem semafora
jest semafor binarny, będący obiektem, którego jedyne pole może przyjmować tylko wartości
0 i 1, a jedyne operacje, jakie można na nim wykonać, to:
P (czekaj)
V (sygnalizuj)
Definicje tych operacji jest następująca:
P(S) - jeżeli S>0, to zmniejsz S o 1, w przeciwnym razie wstrzymaj wykonywanie procesu;
V(S) - jeżeli są jakieś procesy wstrzymane przez semafor S, to wznów jeden z nich, w przeciwnym
razie jeśli S=0, to zwiększ S o 1.
Uwaga.
1) Skutek próby otwarcia otwartego semafora binarnego zależy od implementacji. Dojście do
takiej sytuacji świadczy o błędzie w programie (i system operacyjny zazwyczaj reaguje
sygnalizacją błędu).
2) Same operacje na semaforach muszą być wykonywane niepodzielnie. W systemach z przeplo-
tem realizowane są jako funkcje systemowe, natomiast w sytuacji rzeczywistej równoległości
ich implementacja musi być wspierana przez odpowiedni sprzęt (procesory z niepodzielnymi
rozkazami typu test-and-set).
3) Niedeterminizm uruchamiania procesów czekających pod semaforem może podlegać różnym
ograniczeniom. Wyróżniamy na przykład semafor ze zbiorem oczekujących, gdzie wybór
procesu spośród oczekujących pod semaforem jest zupełnie przypadkowy, i semafor z kolejką
oczekujących, gdzie procesy są uwalniane spod semafora w takiej samej kolejności, w jakiej
do niego przybyły.
Jednym z klasycznych problemów programowania współbieżnego jest tak problem czytelników
i pisarzy. Problem ten jest abstrakcją problemu dostępu do wielodostępnej bazy danych. Każdy
proces aktualizujący dane (pisarz) musi mieć wyłączność dostępu do danych (czytelni), ale procesy,
które tylko czytają (czytelnicy), mogą pracować jednocześnie.
Problem ten jest możliwy, ale trudny do rozwiązania przy użyciu tak elementarnych narzędzi, jak
semafory. Typowe rozwiązania opierają się na wysokopoziomowych mechanizmach synchronizacji,
takich jak na przykład monitory lub regiony (dostępnych w językach wysokiego poziomu przeznaczo-
nych do programowania współbieżnego - na przykład w języku ADA).
Idea rozwiązania: (gwarantującego niemożliwość głodzenia zarówno czytelników, jak i pisarzy)
Przybywający pisarze ustawiani są w kolejkę prostą, przybywający czytelnicy gromadzeni są
w zbiorze. Mechanizm koordynujący wpuszcza na przemian pojedynczych pisarzy i cały zbiór
zgromadzonych czytelników. Wpuszczenie może mieć miejsce dopiero po opuszczeniu czytelni
przez poprzednich użytkowników / użytkownika. Jeżeli kolejka oczekujących pisarzy jest pusta,
a w czytelni przebywają czytelnicy, każdy nowo przybyły czytelnik jest od razu wpuszczany.
Jeśli w zbiorze nie oczekują żadni czytelnicy, kolejno przybywający pisarze są wpuszczani po
zakończeniu pobytu w czytelni przez poprzednika.
6. ZARZĄDZANIE PAMIĘCIĄ
Pamięć operacyjna z punktu widzenia systemu operacyjnego może być postrzegana jako bardzo duża
jednowymiarowa tablica, czyli struktura o dostępie swobodnym, która może być scharakteryzowana
przez:
1) zakres adresów (indeksów);
2) wielkość pojedynczej lokaty (jednostki adresowalnej).
0
1
2
MAX - 1
W prawie wszystkich współczesnych komputerach jednostką adresowalną jest bajt (8 bitów).
Uproszczony model bloku pamięci:
adres
w w
e y
j j
ś PAMIĘĆ ś
c c
i i
e e
zapis / odczyt
Jeżeli szyna adresowa ma n bitów, to może podawać 2 różnych adresów w pamięci. Podawane
adresy, będące liczbami z zakresu 0 * 2 - 1, to adresy fizyczne, określające rzeczywiste położenie
bajtu w pamięci. Sam zakres adresów nazywany jest fizyczną przestrzenią adresową.
Uwaga
Czas dostępu do pamięci jest jednym z najistotniejszych czynników wpływających na szybkość pracy
komputera. Ze względu na to, że wiele rozkazów procesora operuje na większych jednostkach pamięci,
niż 1 bajt, oraz na to, że kolejno wykonywane rozkazy często odnoszą się do sąsiednich lokat pamięci,
w czasie jednego dostępu do pamięci jest zwykle pobierana znacznie większa porcja, niż 1 bajt.
Procesory najdawniejszych komputerów w swoich instrukcjach operowały bezpośrednio na adresach
fizycznych. Było to związane z tym, że pojemności pamięci operacyjnych były małe, a systemy
operacyjne - jednozadaniowe (zatem program i jego dane mogły być umieszczone w z góry ustalonych
obszarach pamięci). Implementacja wielozadaniowości była związana z koniecznością umieszczania
wielu programów (oraz ich danych i stosów) w różnych obszarach pamięci jednocześnie, tak aby
mogły pracować współbieżnie i wzajemnie sobie nie przeszkadzały. Główny problem był związany
z trudnością przewidzenia, w jakim obszarze pamięci fizycznej program zostanie umieszczony w celu
jego wykonania.
Powyższy problem w gruncie rzeczy przeważnie nie dotyczy programistów, gdyż pisząc programy,
posługują się oni adresami symbolicznymi (nazwami zmiennych lub etykietami instrukcji
w programie). Programista powinien co najwyżej orientować się w ograniczeniach nałożonych na
dostępność zasobów (głównie pamięci) w systemie, aby jego program oraz dane nie przekroczyły
pewnej dopuszczalnej wielkości. Z punktu widzenia programu jest zatem dostępny pewien zakres
adresów, który może być różny od fizycznej przestrzeni adresowej (i używać swojej własnej
numeracji adresów), który nazywamy logiczną przestrzenią adresową.
Decyzja, w którym obszarze pamięci fizycznej zostanie umieszczony program, może być podjęta:
1) na etapie kompilacji programu, czyli przetwarzania jego postaci źródłowej, napisanej przez
programistę, na postać wykonywalną (binarną);
2) na etapie ładowania programu wykonywalnego z pamięci zewnętrznej do pamięci operacyjnej;
3) na etapie wykonywania programu.
W pierwszym przypadku mamy do czynienia z bezwzględnym (nieprzemieszczalnym) kodem
wykonywalnym, czyli takim, który nadaje się do wykonania tylko po umieszczeniu go w ściśle
określonym miejscu fizycznej przestrzeni adresowej. Dla najdawniejszych (jednozadaniowych)
systemów operacyjnych była to początkowo jedyna możliwość (przykład - programy typu COM
w systemie DOS), obecnie ma zastosowanie jedynie w programach inicjujących działanie komputera.
W drugim przypadku mamy do czynienia z programami przemieszczalnymi (relokowalnymi).
Program ładujący (loader), przepisując program binarny z pamięci zewnętrznej do określonego
miejsca w pamięci operacyjnej, wykonuje przekształcenia części adresowych przepisywanych
instrukcji tak, aby program mógł być prawidłowo wykonany (jest to relokacja statyczna).
Uwaga
Na listach rozkazów różnych procesorów mogą występować zarówno rozkazy korzystające z adresów
bezwzględnych (fizycznych), jak i rozkazy korzystające z adresów względnych (w takim przypadku
adres fizyczny jest obliczany na podstawie adresu względnego i zawartości rejestrów procesora).
W przypadku procesorów dysponujących wystarczająco bogatą listą rozkazów korzystających
z adresów względnych, zadanie loadera może zredukować się do odpowiedniego ustawienia
początkowych zawartości rejestrów procesora.
W trzecim przypadku program może być przemieszczany w pamięci operacyjnej w trakcie swojego
wykonywania (może to dotyczyć całości programu bądź niektórych jego fragmentów). W takim
przypadku wszystkie adresy w programie załadowanym do pamięci są traktowane jako adresy
logiczne, a proces ich przekształcania na adresy fizyczne musi odbywać się „na bieżąco” (musi to
być umożliwione przez architekturę komputera). Nazywamy to relokacją dynamiczną.
Przetwarzanie adresów logicznych na adresy fizyczne nazywa się wiązaniem (translacją) adresów.
W jakich sytuacjach fragmenty programów (lub całe programy) są przemieszczane w pamięci
operacyjnej w trakcie wykonania ?
1) W sytuacji, gdy całkowity rozmiar programu przekraczał cały dostępny obszar pamięci fizycznej,
stosowano niegdyś nakładkowanie. Polegało to na umieszczeniu najpierw w pamięci najwcześniej
wykonywanego fragmentu programu, potem zaś usunięciu go stamtąd (w całości lub w części)
i wpisaniu na jego miejsce kolejnego fragmentu (nakładki). Odpowiedzialność za prawidłowość
nakładkowania spoczywała na programiście. Nakładkowanie było trudnym zadaniem, gdyż
wymagało od programisty dużej wiedzy technicznej - znajomości wewnętrznej reprezentacji
danych i wielkości zajmowanego obszaru przez program i jego dane, oraz umiejętności właściwego
przekazania sterowania pomiędzy kolejnymi nakładkami.
2) Jeśli procesy czekają na dysku w kolejce do załadowania do pamięci operacyjnej i wykonania,
system operacyjny może uznać za słuszne czasowe przeniesienie z pamięci na dysk jakiegoś
niezbyt pilnego procesu (na przykład i tak oczekującego na transmisję danych) i wpisanie na jego
miejsce bardziej pilnego procesu z dysku. Postępowanie takie nazywane jest wymianą.
Wymieniony proces może po pewnym czasie powrócić do pamięci, ale już w inne miejsce.
3) Procesy w pamięci w zależności od swojego przebiegu wykonania mogą chcieć korzystać z różnych
funkcji bibliotecznych. W najprostszym przypadku biblioteka funkcji, której użycie przewiduje
programista, jest dołączana do programu już na etapie kompilacji i ładowana do pamięci razem
z nim (biblioteka łączona statycznie). Ze względu na oszczędność pamięci wskazane jest
ładowanie do pamięci funkcji bibliotecznych dopiero wtedy, gdy w trakcie wykonywania programu
okaże się, że rzeczywiście będą potrzebne. Biblioteki, które coś takiego umożliwiają, nazywane są
bibliotekami łączonymi dynamicznie (dynamic linked library).
Obsługą wywołania przez program funkcji z biblioteki łączonej dynamicznie zajmuje się system
operacyjny. Przejmuje on wtedy sterowanie i sprawdza, czy dana biblioteka nie przebywa już gdzieś
w pamięci operacyjnej. Jeśli nie, sprowadza ją z dysku i umieszcza w wolnym miejscu pamięci,
a następnie przekazuje programowi wywołującemu informację o adresie wywoływanej funkcji.
W przypadku, gdy po pewnym czasie system stwierdzi deficyt miejsca w pamięci, może (niepotrzebną
już) bibliotekę ponownie usunąć z pamięci.
W pewnej chwili pracy komputera zawartość jego pamięci operacyjnej może się przedstawiać
następująco:
0
dziury
MAX - 1
Obszary w pamięci, które w danej chwili nie są wykorzystane, nazywamy dziurami.
W przypadku ładowania do pamięci nowego programu / fragmentu programu (lub programu, który
wcześniej uległ czasowemu przeniesieniu na dysk), istotnym problemem jest wybór najwłaściwszej
dziury, w której można go umieścić. Kryterium jakości dokonanego wyboru jest związane z liczbą
procesów, które można jednocześnie przechowywać w pamięci (im więcej, tym lepiej). Ponieważ
system operacyjny nie wie z góry dokładnie, kiedy, ile i jak dużych procesów zostanie zgłoszonych
do wykonania, musi kierować się wcześniejszymi statystykami i aktualną zawartością kolejki
procesów. W gruncie rzeczy stworzenie optymalnego algorytmu gospodarowania pamięcią przez
system operacyjny nie jest możliwe, a w praktyce główną rolę odgrywają badania empiryczne
(oparte o eksperymenty).
Ze względu na szybkość realizacji, algorytmy wyboru dziury mają na ogół prostą postać (na
przykład „pierwsza wolna”, „najmniejsza wystarczająco duża”, „największa” itp.). System
operacyjny musi dysponować pełną informacją o aktualnie wykorzystanych i niewykorzystanych
obszarach pamięci, żeby realizować jedną z tych strategii.
Dynamiczne wiązanie adresów w instrukcjach musi być wspierane sprzętowo. Programy relokowalne
dynamicznie po wprowadzeniu ich do pamięci zawierają w dalszym ciągu adresy logiczne, które są
przeliczane na adresy fizyczne dopiero w chwili pobierania danej instrukcji programu do wykonania
przez procesor. Jedną z najprostszych koncepcji jest wykorzystanie rejestrów służących do ochrony
przydzielonych fragmentów pamięci, czyli rejestru bazowego i granicznego. Jeśli adresy logiczne
w programie wykorzystują zakres od 0 do k-1, to (po sprawdzeniu, czy nie przekraczają zawartości
rejestru granicznego) są po prostu sumowane z zawartością rejestru bazowego. W ten sposób następuje
odwzorowanie spójnego zakresu [ 0, k - 1 ] na zakres [ baza, baza + k - 1 ].
Stosowanie tak prostej metody translacji adresów ma następujące wady:
1) umiejscowienie danych i stosu nie jest niezależne od umiejscowienia programu (zatem nie można
wykorzystać kilku oddzielnych dziur);
2) nie jest możliwe współdzielenie fragmentu pamięci przez kilka procesów (przy zapewnieniu
jednocześnie właściwej ochrony pamięci).
Większe możliwości gospodarowania pamięcią stwarza koncepcja adresowania deskryptorowego
(opisowego). W tym trybie adresowania adres logiczny traktowany jest jako dwuczęściowy: jedna
część jest numerem pewnej pozycji w tablicy deskryptorów (opisów), która zawiera informacje nie
tylko o adresie bazowym, ale również o ochronie (prawach dostępu) do danego fragmentu pamięci.
Druga część (przesunięcie) jest sumowana z uzyskanym z tablicy deskryptorów adresem bazowym.
Adresowanie deskryptorowe umożliwia zarówno lepsze zagospodarowywanie dziur, jak i stosowanie
selektywnej ochrony różnych fragmentów pamięci przy zachowaniu możliwości ich współdzielenia.
Musi być jednak wspierane przez odpowiednio zaprojektowany sprzęt, aby z powodu swojej
komplikacji nie spowolniło istotnie pracy procesora. Najtrudniejszą przeszkodą do pokonania jest
czas wyszukiwania informacji w tablicy deskryptorów, która może być bardzo duża (czasem nawet
nie jest w całości przechowywana w pamięci).
W zależności od sposobu podejścia do wyróżniania fragmentów pamięci opisywanych w oddzielnych
pozycjach tablicy opisów, powyższą metodę adresowania nazywamy segmentacją lub stronicowaniem,
a same fragmenty segmentami lub stronami. Mechanizmy segmentacji i stronicowania mogą być
nałożone na siebie (w sensie złożenia dwóch funkcji), dając segmentację stronicowaną.
Segmentacja jest związana z logiczną strukturą programu. Fragmenty pamięci opisywane w tablicy
są nazywane segmentami i zazwyczaj mają różne wielkości. Typowym postępowaniem systemu
operacyjnego jest przydzielenie oddzielnych segmentów dla kodu programu, dla danych i stosu, jak
również dynamiczne przydzielanie i odbieranie procesom segmentów pamięci dzielonej. Jest też
możliwa zmiana wielkości segmentu w trakcie jego wykorzystywania.
Ponieważ segmenty mają zmienną wielkość, dla każdego segmentu trzeba przechowywać informację
zarówno o adresie bazowym, jak i o wielkości (czyli zakresie adresów logicznych).
Ciekawą możliwość stwarza przydzielenie różnych deskryptorów jednemu i temu samemu blokowi
pamięci fizycznej. Umożliwia to operowanie w jednym programie na kilku „wcieleniach” jednej
i tej samej zmiennej w pamięci, „zapętlenie” przestrzeni adresowej itp.
Jeżeli segment jest segmentem pamięci dzielonej, musi mieć (podobnie, jak współdzielone pliki),
licznik dowiązań. Każde przyłączenie takiego segmentu do logicznej przestrzeni adresowej pewnego
procesu zwiększa licznik o 1, zaś odłączenie - zmniejsza o 1. Fizyczna likwidacja segmentu (zamiana
na dziurę) może nastąpić dopiero wtedy, gdy licznik dowiązań będzie zawierał 0 (nikt nie korzysta).
Stronicowanie bazuje na podziale całej fizycznej przestrzeni adresowej na nieduże fragmenty
jednakowej wielkości zwane stronami. Niektóre procesory umożliwiają określanie rozmiaru strony
(w trakcie konfiguracji systemu operacyjnego). Obecnie typową wielkością strony jest 4KB (4096
bajtów). Podobnie, jak w przypadku segmentacji, adres logiczny jest rozkładany na dwie części:
numer strony w tablicy stron i przesunięcie (w obrębie strony). Numer strony przy użyciu tablicy jest
przekodowywany na adres (a dokładnie, jego bardziej znaczącą część) odpowiedniego fragmentu
pamięci fizycznej. Uzyskana część adresu jest następnie składana z bitami przesunięcia, tworząc
pełny adres fizyczny. Stronicowanie wiąże się z rezygnacją ze spójności fizycznego obrazu spójnej
logicznej przestrzeni adresowej.
Mechanizm stronicowania, podobnie jak segmentacji, umożliwia kontrolę praw dostępu procesów do
lokat w pamięci. Wyróżniane są prawa dostępu w sensie zapisu, odczytu, bądź wykonywania (rwx).
Uwaga
W literaturze często stronami nazywane są odpowiednie zakresy adresów logicznych, zaś
odpowiadające im zakresy adresów fizycznych - ramkami.
Zjawisko występowania „niezagospodarowanej” przestrzeni w pamięci w pewnej liczbie oddzielnych
fragmentów (dziur) nazywane jest fragmentacją pamięci (pojęcie to odnosi się zarówno do pamięci
operacyjnej, jak i do pamięci zewnętrznych). Zjawisko fragmentacji jest niekorzystne, gdyż utrudnia
gospodarowanie pamięcią (może na przykład okazać się, że żadna z dziur nie ma wystarczającej
wielkości, żeby pomieścić dany obiekt, choć suma wszystkich dziur byłaby wystarczająca).
Dopóki możliwe jest znajdywanie dziur wystarczającej wielkości, system stara się stosować jeden
z algorytmów wyboru. W przypadku, gdy odpowiednich dziur zaczyna brakować, można próbować
przeprowadzić upakowanie pamięci (w przypadku pamięci zewnętrznej nazywane zazwyczaj
defragmentacją). Polega to na odpowiednim poprzesuwaniu zapisanych obiektów w pamięci tak, aby
uzyskać jak największą komasację dziur.
Upakowanie pamięci operacyjnej przeprowadza system operacyjny (jeśli sprzęt umożliwia relokację
dynamiczną). Algorytmy upakowania zwykle stanowią kompromis pomiędzy ścisłością upakowania
i czasem jego wykonywania. Defragmentację dysku wykonują zazwyczaj programy wykonywane na
poziomie użytkownika. Mogą być uruchamiane przez użytkownika lub automatycznie przez system
operacyjny (i wykonywane „w tle” z niewysokim priorytetem).
W przypadku stosowania segmentacji mamy na ogół do czynienia z sytuacją, gdy niezagospodarowane
fragmenty pamięci występują na zewnątrz segmentów (w postaci dziur). Sytuację taką nazywamy
fragmentacją zewnętrzną.
W przypadku, gdy niezagospodarowane fragmenty pamięci są małe, nie opłaca się utrzymywać
w systemie informacji o ich istnieniu - lepiej jest włączyć je do przestrzeni adresowej zagospodarowa-
nych fragmentów. Mamy wtedy do czynienia z fragmentacją wewnętrzną. Fragmentacja wewnętrzna
jest charakterystyczna dla stronicowania - z każdą jednostką logiczną (kodem programu, blokiem
danych itp.) związane jest niewykorzystanie średnio pół strony. W przypadku stronicowania nie
występuje natomiast fragmentacja zewnętrzna, gdyż każda ramka w pamięci fizycznej może być
niezależnie zagospodarowana (co jest związane z rezygnacją z konieczności zachowywania spójności
obrazu fizycznego przestrzeni adresowej programu).
Jak z tego widać, wielkość strony jest wynikiem kompromisu pomiędzy jakością upakowania
informacji w pamięci, a czasem jej wyszukiwania - zmniejszanie rozmiaru strony powoduje
zmniejszenie średniej fragmentacji, ale jednocześnie powoduje rozrost tablicy stron (a co za tym
idzie, wydłużenie czasu wyszukiwania opisów stron).
Segmentacja stronicowana uważana jest za najdogodniejsze narzędzie gospodarowania pamięcią, ale
jednocześnie jest najtrudniejsza do zaimplementowania. W takim przypadku każdy segment dysponuje
swoją własną tablicą stron. Adres logiczny pobrany z instrukcji jest traktowany jako trzyczęściowy:
jedna z jego części wskazuje pozycję w (globalnej) tablicy segmentów, która teraz zamiast informacji
o adresie bazowym i wielkości segmentu zawiera informację o adresie bazowym tablicy stron
segmentu i liczbie jej pozycji. Pozostałe dwie części adresu są traktowane tak, jak w przypadku
„czystego” stronicowania, to jest jako numer pozycji w tablicy stron i przesunięcie na stronie.
Proces obliczania adresu fizyczne go jest w tym przypadku bardziej złożony i czasochłonny, niż
w przypadku samej tylko segmentacji lub samego tylko stronicowania. Dodatkowym problemem jest
wielkość tablicy segmentów i tablic stron, które w przypadku przechowywania ich w pamięci
operacyjnej same podlegają stronicowaniu. Aby przyspieszyć translację adresów, procesor może
przechowywać część informacji związanej z aktualnie wykonywanym procesem w szybkiej podręcznej
pamięci asocjacyjnej (skojarzeniowej).
7. KONCEPCJA PAMIĘCI WIRTUALNEJ
Systemy operacyjne i wykonywane pod ich nadzorem programy są zazwyczaj w stanie wykorzystać
dużo większą przestrzeń adresową, niż umożliwia to fizyczna pamięć operacyjna wbudowana
w komputer. Ponadto może występować duża różnica pomiędzy możliwościami adresowania
wynikającymi z formatu rozkazów procesora (i wielkości jego rejestrów), a możliwościami
wynikającymi z szerokości szyny adresowej.
W przypadku procesorów Intel, tryb rzeczywisty adresowania (wykorzystywany przez system
operacyjny DOS) umożliwia zaadresowanie jedynie około 1MB pamięci:
Jeden segment: 64 KB (ustalona wielkość).
1 MB + Przesunięcie pomiędzy początkami kolejnych segmentów: 16 B.
64 KB - Adresowanie: segment : offset , gdzie zarówno numer segmentu,
16 B jak i wielkość offsetu (przesunięcie względem początku segmentu)
są liczbami 16-bitowymi, a cały adres - 20-bitowy (w procesorach
80286 i późniejszych można obsługiwać nadmiar na 21-szym bicie).
Procesor 80286 pracujący w trybie chronionym (stosujący deskryptorowy tryb obliczania adresu
fizycznego, umożliwiający ochronę pamięci), ma możliwość zaadresowania 16 MB pamięci fizycznej
(choć jego sposób obliczania adresu teoretycznie umożliwiałby zaadresowanie 1 GB pamięci).
Procesory o numerach 80386 i wyższych mogą zaadresować 4 GB pamięci fizycznej (choć
teoretycznie mogłyby adresować nawet do 64 TB pamięci).
1 KB = 2 B = 1024 B 1 MB = 2 B (ponad 1 milion bajtów)
1 GB = 2 B (ponad 1 miliard bajtów) 1 TB = 2 B (ponad 1 bilion bajtów)
Koncepcja pamięci wirtualnej sprowadza się do symulowania większej ilości pamięci operacyjnej
przy użyciu szybkiej pamięci dyskowej. Może być zrealizowana zarówno w oparciu o stronicowanie,
jak i o segmentację (stronicowanie jest dużo dogodniejszym narzędziem). Korzystanie przez system
z pamięci wirtualnej nie powinno być dostrzegane przez programy wykorzystujące logiczną przestrzeń
adresową. Realizacja pamięci wirtualnej musi być wspierana przez odpowiedni sprzęt.
Aby móc zaimplementować pamięć wirtualną, należy wydzielić na dysku odpowiednio dużą partycję
przeznaczoną na przestrzeń wymiany (swap file). W przestrzeni wymiany przechowywane są
fragmenty logicznego obrazu pamięci, które aktualnie nie mieszczą się w pamięci operacyjnej.
Translacja adresów jest teraz realizowana dwustopniowo:
1) przy użyciu rejestrów procesora i tablic deskryptorów następuje przeliczenie adresu logicznego na
adres liniowy, który może odpowiadać pewnemu adresowi w fizycznej pamięci operacyjnej, bądź
być adresem zewnętrznym (pojęcie pamięci wirtualnej w gruncie rzeczy odpowiada zakresowi
adresów liniowych, który może być dużo większy od zainstalowanej pamięci fizycznej).
2) w przypadku, gdy adres liniowy jest adresem zewnętrznym, następuje przerwanie nazywane brakiem
jednostki (missing item), w wyniku którego wykonywany jest podprogram sprowadzenia z dysku do
pamięci operacyjnej brakującej jednostki (strony lub segmentu), a następnie (po zaktualizowaniu
wpisów w tablicy deskryptorów) ostateczne obliczenie adresu fizycznego.
W sytuacji, gdy w pamięci fizycznej brakuje miejsca na wpisanie jednostki sprowadzonej z dysku,
system musi podjąć decyzję, jaką jednostkę czasowo przepisać z pamięci operacyjnej na dysk.
Ogólnie, wymiana jednostek nie musi być wykonywana bardzo często dzięki zasadzie lokalności
odniesień (stwierdzającej rzadkość skoków na duże odległości w logicznej przestrzeni adresowej).
Wydajność działania systemu jest jednak w dużym stopniu zależna od doboru właściwego algorytmu
wymian.
Jeśli kompilator programów napisanych w pewnym języku został zaprojektowany pod kątem pracy
w systemie z wymianą, przypada mu ważna rola dostosowania logicznej struktury skompilowanego
programu do jednostek wymiany. Jest bardzo wskazane, na przykład, żeby duże struktury danych,
takie jak duże tablice, były przechowywane w jak najmniejszej liczbie jednostek. Jeśli program używa
podprogramów, najkorzystniej jest, jeśli zajmują one oddzielne jednostki.
Istotną rzeczą jest odseparowanie kodu programu (oraz ewentualnie używanych stałych) od obszaru
wykorzystywanego przez zmienne programu. Jednostki zajmowane przez kod i stałe przeznaczone są
tylko do odczytu, zatem przy czasowym usuwaniu ich z pamięci nie jest potrzebna aktualizacja zapisu
na dysku (cały czas przebywają tam w tym samym miejscu), co znacznie przyspiesza wykonywanie
procesu.
Aby system operacyjny wiedział, czy zachodzi konieczność aktualizacji zapisu w przestrzeni
wymiany na dysku w przypadku usuwania jednostki z pamięci operacyjnej, czy też nie, w opisach
jednostek umieszczane są bity modyfikacji, nazywane też bitami zabrudzenia (dirty bit).
W przypadku, gdy zawartość jednostki nie podlega modyfikacji, wartość tego bitu wynosi 0.
Jeśli w jednostce został zmieniony choćby jeden bajt, bit modyfikacji jest ustawiany na 1.
Jednostki zawierające kod i stałe mają przez cały czas wyzerowany bit modyfikacji. W przypadku
jednostek zawierających wartości zmiennych może się przez przypadek również zdarzyć, że od
chwili sprowadzenia ich do pamięci operacyjnej przez pewien czas będą używane wyłącznie do
odczytu - w tym czasie ich bit modyfikacji pozostaje wyzerowany, co przyspiesza ewentualną
wymianę.
Uwaga
Dostęp systemu operacyjnego do przestrzeni wymiany na dysku jest na ogół dużo szybszy, niż
dostęp do systemu plików, gdyż korzysta z dużo prostszych mechanizmów (może nie stosować
buforowania ani skomplikowanych algorytmów wyszukiwania miejsca zapisu / odczytu).
Najczęściej stosowanym sposobem implementacji pamięci wirtualnej jest stronicowanie na żądanie
(demand paging). W tym przypadku niektóre strony procesu (jego kodu i danych) mogą przebywać
w pamięci operacyjnej, a niektóre na dysku. System operacyjny prowadzi „leniwą” politykę
sprowadzania stron do pamięci - sprowadza je dopiero wtedy, gdy okażą się potrzebne („na żądanie
procesu”). Problemy, jakie trzeba rozstrzygnąć, to:
1) ile ramek wstępnie przydzielać każdemu procesowi ?
2) jaką politykę wymiany stron prowadzić, gdy nie ma już wolnych ramek w pamięci ?
Algorytmy, które rozwiązują te problemy, nazywane są odpowiednio algorytmami wstępnego
przydziału i algorytmami zastępowania. Jeśli na początku działania procesy nie mają
przydzielanych żadnych ramek, mówimy o czystym stronicowaniu na żądanie.
Wstępny przydział ramek nie musi być taki sam dla wszystkich procesów - może zależeć od
wielkości jego programu, od priorytetu, logicznej struktury lub danych na temat jego wcześniejszych
wykonań.
Algorytm zastępowania może być algorytmem zastępowania lokalnego lub algorytmem zastępowania
globalnego. W pierwszym przypadku każdy proces ma liczbę ramek przydzieloną na stałe i w jej
granicach operuje wymieniając swoje strony w miarę własnych potrzeb (stosując na przykład liczniki
użycia ramek, system priorytetów ramek i inne kryteria). W drugim przypadku procesy mogą
rywalizować o ramki - algorytm wymiany musi pełnić rolę arbitra i brać pod uwagę na przykład
priorytety procesów, wielkość programów czy logiczną strukturę (podobnie, jak algorytm przydziału
wstępnego).
W przypadku niewłaściwego doboru algorytmów w systemie mogą występować różne niekorzystne
zjawiska. Przykładowo, może się zdarzyć, że globalny algorytm wymiany stron przydzieli pewnemu
niskopriorytetowemu procesowi bardzo małą liczbę ramek. W takiej sytuacji prawdopodobnie byłoby
najkorzystniejsze czasowe zawieszenie takiego procesu i wznowienie go dopiero po zmniejszeniu się
obciążenia systemu. Jeśli jednak w takich warunkach proces będzie nadal się wykonywał, będzie musiał
co chwila wymieniać ramki, i łączny czas dokonywania tych wymian może przekroczyć łączny czas
jego efektywnej pracy - takie zjawisko nazywamy szamotaniem się procesu (migotaniem).
Fizyczna realizacja stronicowania na żądanie opiera się na istnieniu w każdej pozycji tablicy stron
(w każdym opisie strony) bitu poprawności odniesienia, który informuje, czy dana strona przebywa
aktualnie w pamięci operacyjnej (adres jest adresem wewnętrznym), czy też przebywa na dysku
(adres jest adresem zewnętrznym). W tym drugim przypadku żądanie dostępu do strony generuje
przerwanie (page fault). Po sprowadzeniu strony do pamięci (co ewentualnie może być związane z
wymianą) system aktualizuje tablicę stron i kontynuuje wykonywanie przerwanego procesu.
Realizacja segmentacji na żądanie wygląda podobnie - również oparta jest na istnieniu bitów
poprawności odniesienia w deskryptorach segmentów. Jest jednak algorytmicznie dużo trudniejsza
(a zatem powolniejsza) ze względu na zmienną wielkość dziur w pamięci i ewentualną potrzebę ich
okresowej komasacji.
Segmentację na żądanie wykorzystywały systemy dedykowane procesorowi Intel 80286 - na
przykład OS / 2.
Czynnikiem, jaki musi być uwzględniony w przypadku algorytmów wymiany jednostek pamięci, jest
możliwość współdzielenia tych jednostek. Odnosi się to do:
1) współdzielenia przez procesy wykonywane na tym samym procesorze głównym;
2) współdzielenia przez procesy wykonywane na różnych procesorach fizycznych w systemie
wieloprocesorowym;
3) współdzielenia przez proces oraz transmisję danych wykonywaną przez autonomiczne (działające
asynchronicznie) urządzenie zewnętrzne.
Z oczywistych względów nie można usunąć z pamięci jednostki przed zakończeniem transmisji
danych. W przypadku współdzielenia fragmentu pamięci przez różne procesy można usunąć ten
fragment dopiero po zmniejszeniu się stanu jego licznika dowiązań do zera. Uwzględnienie takiej
sytuacji jest szczególnie trudne w systemie wieloprocesorowym - gospodarka pamięcią dzieloną
w takim systemie musi być gospodarką scentralizowaną.
Mechanizmem umożliwiającym zabezpieczenie strony przed przedwczesnym usunięciem jej
z pamięci operacyjnej może być bit blokowania umieszczony w opisie strony. Ustawienie wartości
tego bitu na 1 stanowi informację dla systemu operacyjnego, że strona czasowo nie powinna podlegać
usuwaniu w ramach algorytmu wymiany.
Stosowanie bitu blokowania może być przydatne zarówno w sytuacji oczekiwania na zakończenie
transmisji danych czy współdzielenia strony, jak też w sytuacji, kiedy strona została dopiero co
sprowadzona do pamięci przez jakiś proces i jeszcze nie była ani razu użyta. Jeżeli proces ma niski
priorytet, mogłoby się zdarzyć, że inny, wysokopriorytetowy proces będzie usiłował pozbawić go
od razu miejsca w pamięci. Ponieważ sprowadzenie strony do pamięci jest czasochłonne, należy
zadbać o to, żeby choć przez pewien czas była ona wykorzystana.
Procesy użytkowników mogą z „egoistycznych pobudek” starać się nadużywać mechanizmu
blokowania stron w pamięci. Z tego powodu systemy operacyjne często traktują ustawienie bitu
blokowania jako „blokowanie zalecane”, nie „blokowanie obowiązkowe” i w sytuacji dużego
spiętrzenia wysokopriorytetowych procesów mogą same podejmować ostateczną decyzję co do
trzymania strony w pamięci lub też jej usunięcia.
Korzyści z implementacji pamięci wirtualnej:
1) podobnie, jak w przypadku nakładkowania, w pamięci mogą być przechowywane jedynie
fragmenty kodów programów, a nie całe kody;
2) programiści tworzący bardzo duże programy nie muszą wnikać w organizację wymiany ich
fragmentów pomiędzy pamięcią operacyjną a pamięcią zewnętrzną;
3) można równolegle wykonywać więcej procesów, niż wynika to z ograniczeń narzucanych przez
rozmiar pamięci operacyjnej, co polepsza wykorzystanie sprzętu i zwiększa wydajność systemu.
Procesor Intel 80286 umożliwiał realizację pamięci wirtualnej tylko w oparciu o mechanizm
segmentacji. Procesory o numerach 80386 i wyższych umozliwiają implementację pamięci wirtualnej
zarówno w oparciu o segmentację, jak i stronicowanie, przy czym zdecydowanie zalecany jest ten
drugi sposób.
8. SYSTEMY PLIKÓW
Poza pamięcią operacyjną (pamięcią główną) komputery są wyposażone w pamięć zewnętrzną
(pamięć pomocniczą), która jest realizowana w oparciu o nośnik magnetyczny lub optyczny.
Programy aktualnie wykonywane (oraz ich dane i stosy), a przynajmniej aktualnie wykorzystywane
fragmenty muszą przebywać w pamięci operacyjnej, gdyż tylko stamtąd procesor może bezpośrednio
pobierać rozkazy do wykonywania i tylko tam może bezpośrednio odczytywać lub zapisywać dane
(mówimy, że pamięć operacyjna jest pamięcią o dostępie bezpośrednim).
Do pamięci zewnętrznej procesor ma dostęp za pośrednictwem układu sterującego danym urządzeniem
(kontrolera), któremu może podawać rozkazy zapisu lub odczytu całych bloków danych (zatem nie
jest możliwe zapisywanie lub odczytywanie pojedynczych bajtów bez ponoszenia kosztów związanych
z transmisją całego bloku). Dostęp do informacji zapisanej w pamięci zewnętrznej jest wielokrotnie
wolniejszy, niż do informacji zapisanej w pamięci operacyjnej.
W związku z blokowym charakterem transmisji i zapisu danych, w pamięci zewnętrznej, podobnie
jak w pamięci operacyjnej, mamy do czynienia ze zjawiskiem fragmentacji.
Przyczyny stosowania pamięci zewnętrznych:
1) ze względów technologicznych mogą mieć dużo większą pojemność, niż aktualnie produkowane
układy pamięci operacyjnej;
2) są dużo tańsze od pamięci operacyjnej (w tym sensie, że jeden bajt pamięci zewnętrznej kosztuje
dużo mniej, niż jeden bajt pamięci operacyjnej);
3) pamięć zewnętrzna jest pamięcią trwałą - w przypadku wyłączenia zasilania cała zawartość
pamięci operacyjnej zostaje utracona, a zawartość pamięci zewnętrznej nie.
Pamięci zewnętrzne są produkowane w postaci dysków stałych (zwanych powszechnie twardymi
dyskami (hard disc)) oraz dysków wymiennych (dyskietek, płytek CD). Rozgraniczenie pomiędzy
dyskami stałymi i wymiennymi jest dość zatarte, gdyż twarde dyski mogą być umieszczane w obudo-
wach umożliwiających ich łatwą wymianę i przenoszenie do innego komputera. Pamięci służące do
okresowego zapisywania bardzo dużych ilości danych ze względów bezpieczeństwa (backup) mają
często postać taśmy magnetycznej. W komputerach często jest instalowanych kilka różnych
urządzeń pamięci zewnętrznej.
Pomijając przypadek przestrzeni wymiany omówiony na poprzednim wykładzie, zapis w pamięci
zewnętrznej odbywa się w logicznie wyodrębnionych jednostkach o zmiennej wielkości, zwanych
plikami (file). Pliki są obiektami logicznymi, które są „najbardziej widoczne” dla użytkowników
systemów komputerowych - większość użytkowników patrzy na pracę komputera przez pryzmat
operacji wykonywanych na pojedynczych plikach, bądź też ich zbiorach.
Ze względu na dużą liczbę plików przechowywanych we współczesnych komputerach ich zbiorowi
jest zwykle narzucana pewna struktura - pliki są pogrupowane w katalogi (directory) zwane również
folderami. W zależności od systemu operacyjnego cała pamięć zewnętrzna, jaką dysponuje komputer
(być może zorganizowana w postaci kilku oddzielnych urządzeń fizycznych) może być postrzegana
przez użytkownika jako jedna duża, wspólna przestrzeń służąca do przechowywania plików, bądź też
może być podzielona na kilka urządzeń logicznych (partycji) (partition), przy czym podział na
urządzenia logiczne wcale nie musi odzwierciedlać podziału na urządzenia fizyczne.
W poszczególnych partycjach przechowywane są odrębne struktury katalogów. Jedna z partycji
zwykle przeznaczana jest na przestrzeń wymiany (do której dostęp jest zorganizowany za pomocą
innych procedur, niż dostęp do plików, i która nie jest bezpośrednio widoczna dla użytkownika).
Z punktu widzenia interfejsu użytkownika plik jest najmniejszą jednostką pamięci zewnętrznej, na
której użytkownik może wykonywać operacje. W plikach mogą być przechowywane programy
wykonywalne (lub ich fragmenty), lub dane. Formalnie, każdy plik może być postrzegany jako zbiór
danych przeznaczonych do wykorzystania przez konkretny program (lub grupę programów).
Program wykonywalny (binarny) jest przeznaczony do pobrania i załadowania do pamięci przez
program ładujący (loader), plik zawierający program źródłowy (nieskompilowany) stanowi dane dla
przetwarzającego go kompilatora, plik zawierający zapis tekstu lub obrazu stanowi dane dla edytora
tekstowego lub graficznego (lub programu odtwarzającego) itd.
Zapis informacji w plikach musi mieć ściśle określoną strukturę (format), znaną programowi, który
będzie tę informację wykorzystywał. Szczególnymi przypadkami plików są:
- plik wykonywalny (executable file), przeznaczony do załadowania do pamięci operacyjnej
i interpretacji jako ciągu rozkazów dla procesora;
- plik tekstowy, zawierający ciąg bajtów traktowany jako ciąg znaków ASCII, w tym znaczników
końca linii (występujących nie rzadziej, niż określona liczba pozycji) i znacznik końca pliku.
Niektóre programy mogą traktować pliki jako „bezpostaciowe”, czyli ciągi bajtów o zupełnie
dowolnej strukturze.
Z każdym plikiem związany jest pewien zbiór jego atrybutów (zwany też czasem metryką pliku).
Atrybuty są podstawowymi informacjami na temat własności pliku i sposobów jego użytkowania.
Różne systemy plików stosują różne atrybuty, ale przeważnie są to takie informacje, jak:
- nazwa pliku;
- typ pliku;
- umiejscowienie w fizycznej pamięci zewnetrznej;
- rozmiar pliku;
- prawa dostępu (czy i kto może zapisywać, odczytywać, wykonywać ...);
- identyfikator właściciela pliku;
- data i czas utworzenia pliku, ostatniej modyfikacji i ostatniego dostępu do pliku.
Informacje na temat właściciela dotyczą tylko plików w wielodostępnych systemach operacyjnych.
W takim przypadku odmienne prawa dostępu mogą być wyznaczone dla właściciela pliku, odmienne
dla grupy jego współpracowników, a jeszcze inne dla pozostałych użytkowników systemu. Prawa
dostępu nadaje i zmienia właściciel pliku.
Plik jako obiekt logiczny wywodzi się z abstrakcji taśmy magnetycznej, a w związku z tym zbiór
typowych funkcji systemowych operujących na plikach odzwierciedla w pewnym stopniu fizyczne
operacje związane z obsługą taśmy. Plik należy zatem wyobrażać sobie jako ciąg lokat o jednakowej
wielkości, zakończony znacznikiem końca pliku. Wzdłuż pliku posuwa się wskaźnik bieżącego
położenia w pliku (wskaźnik pliku, odpowiadający fizycznej głowicy zapisująco-odczytującej).
eof (end of file)
Tradycyjną metodą dostępu do danych w pliku jest dostęp sekwencyjny, czyli zapisywanie lub
odczytywanie danych w pliku w kolejności zgodnej z ich umiejscowieniem (od początku do końca
pliku). Od chwili, gdy zaczęto implementować pliki w pamięci dyskowej, pojawiła się możliwość
„przeskakiwania” wskaźnika pliku z jednego miejsca w drugie - ten tryb dostępu, podobnie jak
w przypadku pamięci operacyjnej, nazwano dostępem swobodnym.
Typowe czynności wykonywane przez funkcje systemowe operujące na plikach to:
- utworzenie pliku (początkowo pustego, czyli wyznaczenie mu jedynie miejsca w pamięci
fizycznej i zapisanie atrybutów);
- otwarcie pliku (wpisanie informacji do systemowej tablicy plików otwartych, przydzielenie bufora
w pamięci operacyjnej, ustawienie wskaźnika na początku lub na końcu pliku);
- pisanie do pliku (zapis ciągu bajtów poczynając od bieżącej pozycji w pliku, ustawienie wskaźnika
na kolejnej pozycji po ostatniej zapisanej);
- odczyt z pliku (odczyt ciągu bajtów poczynając od bieżącej pozycji w pliku, ustawienie wskaźnika
na kolejnej pozycji po ostatniej odczytanej);
- zmiana położenia bieżącego w pliku (tylko przeniesienie wskaźnika w inne miejsce);
- zmiana atrybutów pliku (wywoływanie tej funkcji zwykle podlega pewnym ograniczeniom);
- zamknięcie pliku (przepisanie zawartości bufora do pamięci zewnętrznej i usunięcie wpisu
w tablicy plików otwartych);
- usunięcie pliku (zwolnienie miejsca zajmowanego przez plik w pamięci zewnętrznej i usunięcie
metryki pliku).
W systemach umożliwiających współbieżną pracę wielu użytkowników, prawidłowe zorganizowanie
ich dostępu do plików jest rzeczą o wiele trudniejszą, niż w systemach jednozadaniowych. Ponieważ
system plików jest wspólny dla wszystkich użytkowników, poszczególne pliki mogą być używane
przez wiele procesów jednocześnie. Wiąże się to z koniecznością zaprojektowania funkcji systemo-
wych tak, aby zapewnić logiczny i spójny obraz danych w pliku wszystkim procesom. W szczegól-
ności implementacja powinna uwzględniać:
- możliwość niezależnego otwierania i zamykania pliku przez każdy proces. Każdy plik powinien
posiadać licznik otwarć, który zwiększa się o 1 przy każdym otwarciu przez proces, a zmniejsza
o 1 przy każdym zamknięciu. Plik może być fizycznie usunięty tylko wtedy, gdy jego licznik
otwarć zawiera zero;
- możliwość niezależnego czytania i pisania przez każdy proces. Każdy proces powinien posiadać
swój własny wskaźnik pliku i móc go przemieszczać niezależnie od innych procesów. Operacje
czytania i pisania powinny być zaimplementowane jako operacje niepodzielne, aby zapewnić
logiczną spójność danych. Skutek każdego dokonanego przez jeden proces zapisu w pliku powinien
być natychmiast widoczny dla wszystkich innych procesów czytających ten plik.
W niektórych zastosowaniach (na przykład w implementacjach wielodostępnych baz danych) procesy
mogą potrzebować wykonywania bardziej złożonych operacji na współdzielonych plikach, niż tylko
takich, które mogą być zrealizowane przez pojedyncze wywołania funkcji systemowych (w teorii baz
danych takie operacje nazywane są transakcjami). Aby zapewnić niepodzielność wykonywania
transakcji, powinny być zaimplementowane systemowe mechanizmy blokowania (locking) całych
plików, bądź poszczególnych rekordów zapisanych w danym pliku.
Ponieważ operacje zapisu i odczytu do / z pliku przez procesy są w rzeczywistości zaimplementowane
jako operacje na buforach plików w pamięci, do implementacji blokowania mogą być użyte
odpowiednie mechanizmy ochrony fragmentów pamięci (omówione na poprzednich wykładach).
Uwaga
Same pliki mogą też służyć jako specyficzne mechanizmy koordynacji procesów. Jednym
z najstarszych i najdłużej wykorzystywanych prostych mechanizmów jest tak zwany plik zamkowy
(lockfile). Plik zamkowy jest pustym plikiem, czasowo tworzonym w wybranym i znanym procesom
katalogu. Działa on jak prosty semafor - fakt jego istnienia informuje procesy o tym, że dany zasób
jest właśnie zajęty przez pewien proces, zaś jeśli go nie ma, oznacza to, że zasób jest wolny.
Pierwotnie pliki tworzyły w pamięci zewnętrznej niezorganizowany zbiór. Gdy liczby plików
w systemach stały się bardzo duże, pojawiła się potrzeba narzucenia ich zbiorowi pewnej struktury,
aby łatwiej było wyszukiwać poszczególne pliki i wykonywać na nich operacje. Tak powstały
katalogi, będące w istocie specjalnym rodzajem plików, zawierających informacje o innych plikach
i przeznaczonych do operowania na nich za pomocą innych poleceń systemowych, niż na zwykłych
plikach.
Z punktu widzenia użytkownika, katalogi zawierają wykazy nazw plików wraz ze związanymi z nimi
informacjami (wielkość, położenie w pamięci zewnętrznej itp.) - w rzeczywistości informacje te są
zazwyczaj przechowywane w innym miejscu, a katalogi zawierają jedynie adresy tych miejsc.
Początkowo katalogi były jednopoziomowe (czyli stanowiły uporzadkowaną listę wszystkich plików),
potem pojawiły się dwupoziomowe (na przykład: poziom pierwszy - identyfikatory uzytkowników,
poziom drugi - pliki pogrupowane w zbiory należące do poszczególnych użytkowników). Obecnie
prawie wyłącznie spotyka się wielopoziomowe struktury katalogów zorganizowane w postaci
drzewa lub grafu acyklicznego.
Przykładowa organizacja drzewa katalogów:
GŁÓWNY
SYSTEM BINARNE TEKSTOWE UŻYTKOWNICY
STEROWNIKI ....... abc.bin xx.bin ... info.txt instr.txt .... ALA JAN OLA
ZADANIA PROGRAMY
zad1.txt zad2.txt ... pierwszy.pas .....
Katalog najwyższego poziomu (główny katalog systemu plików na danym urządzeniu logicznym)
nazywany jest korzeniem drzewa katalogów. Może zawierać zarówno katalogi niższego poziomu
(podkatalogi), jak i pliki. Podobnie katalogi niższego poziomu również mogą zawierać zarówno
podkatalogi, jak i pliki, itd.
W każdej chwili sesji pracy użytkownika z systemem operacyjnym użytkownik ma przyporządkowany
katalog bieżący (current directory), do którego zawartości przez domniemanie odnoszą się polecenia
użytkownika. Użytkownik może (wydając odpowiednie polecenie) zmienić swój katalog bieżący na
dowolny inny (w granicach posiadanych praw dostępu).
Nazwy plików i podkatalogów w obrębie jednego katalogu muszą być unikalne, natomiast w różnych
katalogach mogą występować takie same nazwy. Użytkownik, chcąc wydać polecenie dotyczące
obiektu umieszczonego w innym katalogu, niż jego katalog bieżący, musi nazwę tego obiektu
poprzedzić ścieżką dostępu. Ścieżki dostępu dzielą się na bezwzględne (podające położenie danego
obiektu względem korzenia drzewa katalogów) i względne (podające położenie obiektu względem
bieżącego katalogu użytkownika wydającego polecenie).
Przykład
Przyjmując strukturę drzewa katalogów z poprzedniego slajdu i zakładając, że katalogiem bieżącym
jest ALA, możemy stwierdzić, że bezwzględna ścieżka dostępu do pliku zad1. txt składa się z ciągu
katalogów GŁÓWNY, UŻYTKOWNICY, JAN, ZADANIA , zaś bezwzględna -
ALA, UŻYTKOWNICY, JAN, ZADANIA.
GŁÓWNY
SYSTEM BINARNE TEKSTOWE UŻYTKOWNICY
STEROWNIKI ....... abc.bin xx.bin ... info.txt instr.txt .... ALA JAN OLA
ZADANIA PROGRAMY
zad1.txt zad2.txt ... pierwszy.pas .....
- katalog bieżący
- bezwzględna ścieżka dostępu
- względna ścieżka dostępu
Uwaga
Nazwa pliku poprzedzona bezwzględną ścieżką dostępu (tak zwana pełna nazwa ścieżkowa pliku)
stanowi jednoznaczny identyfikator pliku na danym urządzeniu logicznym.
Graf acykliczny stanowi uogólnienie drzewa. Na ogół, podobnie jak drzewo, posiada jeden
wyróżniony wierzchołek (korzeń), ale od korzenia do innych węzłów drzewa może prowadzić już
więcej, niż jedna ścieżka.
katalog
plik
W tego rodzaju strukturze jeden i ten sam plik może figurować w więcej, niż jednym katalogu.
Podobnie, pewien katalog może mieć więcej nadkatalogów, niż tylko jeden. Struktura grafu
acyklicznego jest bardziej elastyczna, niż struktura drzewa, lecz jednocześnie jej implementacja
stwarza wiele istotnych problemów:
- usunięcie lub przemieszczenie pliku lub katalogu wymaga konsekwentnego usunięcia lub zmiany
wszystkich dowiązań;
- algorytmy przeszukiwania całej takiej struktury są bardziej skomplikowane, niż w przypadku
drzewa;
- nieumiejętne tworzenie dowiązań do katalogów może spowodować powstanie cyklu (pętli)
w grafie, co może wiązać się nawet z koniecznością reinstalacji całego systemu plików (dlatego
w systemie Unix prawo tworzenia dowiązań do katalogów ma jedynie administrator systemu).
W Uniksie poza tak zwanymi dowiązaniami twardymi istnieją też dowiązania miękkie, których
tworzenie nie powoduje zmian w rzeczywistej strukturze katalogów, i które nie stwarzają wyżej
opisanego zagrożenia.
Systemy plików mogą być implementowane w pamięci zewnętrznej na różne sposoby. Logiczny
obraz dysku jest ciągiem bloków o jednakowej wielkości, które są ponumerowane kolejno, czyli
adresy ich tworzą liniową przestrzeń adresową (adresy liniowe na dysku są przetwarzane na
kilkuskładnikowe adresy fizyczne, określające numer powierzchni itd.).
Najprostszym sposobem przydziału miejsca na plik jest przydział ciągły, czyli przydział odpowiedniej
liczby bloków o kolejnych adresach logicznych. Taka implementacja wiąże się z niedogodnościami
podobnymi do tych, jakie związane są z niestronicowaną pamięcią operacyjną - fragmentacja
zewnętrzna i konieczność ewidencjonowania dziur i stosowania algorytmów wyboru.
Wady tej nie ma przydział listowy miejsca na dysku, gdzie ciąg przydzielonych bloków stanowi
listę jednostronnie wiązaną (czyli strukturę, gdzie na końcu każdego bloku przechowywana jest
informacja o położeniu następnego bloku lub znacznik końca listy). Implementacja taka ma inną wadę -
praktycznie wyklucza dostęp swobodny do plików (a dostęp sekwencyjny też nieco spowalnia, wskutek
możliwości chaotycznego rozrzucenia bloków jednego pliku po całym dysku).
Przydział indeksowy polega na skupieniu całej informacji o położeniu kolejnych bloków należących
do pliku w specjalnym bloku indeksowym. Jest to rozwiązanie analogiczne do stronicowania pamięci
operacyjnej (przy czym blok indeksowy pełni rolę tablicy stron). Pozwala ono uniknąć fragmentacji
zewnętrznej i ułatwia dostęp swobodny, ale również wiąże się z pewnymi problemami implementa-
cyjnymi:
- krótkie pliki (a takich jest zwykle w systemie bardzo dużo) marnują dużo miejsca w swoich blokach
indeksowych;
- jeśli plik jest bardzo długi, pojedynczy blok indeksowy może być dla niego niewystarczający,
w takim przypadku należy utworzyć listę wiązaną złożoną z kilku bloków indeksowych (co od razu
spowalnia dostęp).
W praktyce w systemach plików często są stosowane mieszane metody implementacji.
9. SYSTEMY OPERACYJNE DOS I WINDOWS
Systemy operacyjne wytwarzane przez firmę Microsoft bazują na procesorach Intel i kompatybilnych
z nimi. Wyjątkiem od tej reguły jest system Windows NT, który posiada emulatory listy rozkazów
32-bitowych procesorów Intel dla niektórych innych procesorów (w tym procesorów o architekturze
RISC). Windows NT są zaprojektowane pod kątem zastosowań profesjonalnych w zakładach pracy,
wszystkie pozostałe produkty (wcześniejsze i późniejsze) są zasadniczo przeznaczone do zastosowań
amatorskich lub w niedużych, kilkuosobowych firmach.
Początkowe wersje systemów operacyjnych (w latach 80-tych) powstawały przy współpracy z firmą
IBM. Najwcześniejszy produkt - DOS (Disk Operating System), w wersji MS-DOS 1.00 powstał
w 1981 r. i był przeznaczony do pracy w komputerach zawierających procesor Intel 8086 i nie
zawierających dysku twardego. Kolejne wersje DOS-a pojawiały się aż do początków lat 90-tych,
osiągając numery 6.xx . Cały czas były to systemy jednozadaniowe, przeznaczone do wykonywania
programów na komputerach klasy PC i wykorzystujących tryb rzeczywisty pracy procesorów Intel.
Możliwości trybu chronionego udostępniane przez kolejne procesory Intel mogły być pod DOS-em
wykorzystywane jedynie „niejawnie”. DOS mógł współpracować z prostym protokołem sieciowym.
Najwcześniejsza wersja systemu DOS dysponowała prostym, jednopoziomowym systemem plików
wzorowanym na wykorzystywanym we wcześniejszym systemie operacyjnym CP/M. W późniejszych
wersjach pojawiła się hierarchiczna, wielopoziomowa struktura systemu plików wzorowana na rozwią-
zaniu istniejącym w systemie Unix. Pojawiła się też możliwość wydzielania dysków logicznych,
zwanych również strefami i oznaczanych pojedynczymi literami alfabetu (A: , B: , C: , ... ).
Kolejnymi „zdobyczami” systemu DOS (również wzorowanymi na rozwiązaniach systemu Unix) były
zestandaryzowane strumienie danych i możliwość ich przekierowywania oraz przetwarzanie potokowe
(ale tylko w wersji sekwencyjnej).
Zasadniczym interfejsem użytkownika systemu DOS był zawsze interfejs tekstowy, udostępniający
zestaw komend wykonujących typowe czynności na plikach i katalogach (dir, cd, type, copy, ... ).
Po pewnym czasie został on wzbogacony o komendy sterujące przydatne przy tworzeniu prostych
plików wsadowych (odpowiedników uniksowych skryptów) - for, if, goto, shift oraz call.
Dopiero na poczatku lat 90-tych do zestawu sterowników urządzeń DOS-a dodano sterownik myszy
umożliwiający obsługę prostego interfejsu graficznego (podobnego do nakładki Norton Commander).
Ponieważ DOS był oparty na trybie rzeczywistym pracy procesorów Intel, jego obszar danych
i programów systemowych w pamięci operacyjnej nie był w żaden sposób chroniony - użytkownik
mógł w każdej chwili dokonać odczytu lub zapisu w obszarze systemowym i dowolnie zmodyfikować
lub uniemożliwić działanie systemu (co powodowało konieczność jego restartu).
System DOS rozpoczynając działanie korzystał ze zbioru podstawowych funkcji obsługujących
urządzenia zewnętrzne zapisanych w pamięci stałej ROM, następnie uzupełniał go o zbiór własnych
funkcji systemowych zapisywanych w pamięci RAM. Większa część funkcji systemowych była
oficjalnie udostępniana programistom w postaci interfejsów do języków Asembler, Basic, a nieco
później również Pascal i C (za podstawowy język był uważany Basic, którego interpreter był
dołączany do każdej wersji DOS-a).
Niektóre istotne funkcje systemowe nie były oficjalnie opublikowane i udokumentowane przez firmę
Microsoft (pod pozorem przewidywanych ich zmian w kolejnych wersjach systemu), choć wiele firm
tworzących oprogramowanie znało je i wykorzystywało. Istniały też nieudokumentowane struktury
danych systemowych (na przykład Lista List - LL).
Mapa obszaru pamięci wykorzystywanego przez system DOS w trybie rzeczywistym:
Adres pola Długość pola Zawartość
00000 - 003FF 1 KB Tablica wektorów przerwań ROM BIOS
00400 - 004FF 256 B Obszar roboczy BIOS Pamięć
00500 - 005FF 256 B Obszar roboczy DOS konwencjonalna
0F420 - 9FFFF Programy systemowe i użytkowe 640 KB
A0000 - FFFFF Pamięć ekranu kart graficznych, Pamięć górna
udostępniona też dla innych sterowników 384 KB
Powyżej adresu 1 MB pamięć może być wykorzystywana przez procesory pracujące w trybie
chronionym. Istnieje biblioteka funkcji systemowych DOS umożliwiająca wymianę bloków pamięci
umieszczonych w obszarze powyżej adresu 1 MB z blokami umieszczonymi poniżej tego adresu
(wiąże się to z chwilowym przełączaniem procesora do trybu chronionego i z powrotem w sposób
niewidoczny dla programów pracujących w trybie rzeczywistym), co udostępnia programom
DOS-owym coś w rodzaju bardzo szybkiej pamięci zewnętrznej, na przykład tak zwany RAM-disk.
Pierwsze graficzne interfejsy użytkownika oparte na systemie okien i umożliwiające obsługę przy
użyciu wskaźnika (myszy) były opracowane przez firmę Xerox w 1981 r. i przeznaczone dla terminali
dużych komputerów. Komputery osobiste były w tym czasie jeszcze zbyt ubogie sprzętowo, aby
umożliwiać takie rozwiązania. Pierwszym systemem mikrokomputerowym wyposażonym w interfejs
graficzny był Macintosh (1984 r.). W tym czasie Microsoft równolegle prowadził własne prace nad
najwcześniejszą wersją systemu Windows (Windows 1.0 pojawił się w 1985 r. i miał interfejs
graficzny, ale wykorzystywał tylko tryb rzeczywisty 8086) oraz, we współpracy z IBM, nad systemem
operacyjnym OS / 2 (pojawił się w 1987 r. i wykorzystywał tryb chroniony 80286, ale pierwotnie miał
tylko interfejs tekstowy, a graficzny uzyskał dopiero rok później).
Kolejne wersje systemu Windows pojawiające się pod koniec lat 80-tych wykorzystywały już 16-bitowy
tryb adresowania procesorów Intel. Pierwszą wersją, która osiągnęła znaczący sukces komercyjny, była
wersja 3.0 (pojawiła się w 1990 r.). W tym samym roku doszło do rozwiązania współpracy pomiędzy
IBM (który przejął system OS / 2) i Microsoftem (który dalej rozwijał system Windows). Windows 3.1
pojawiły się w 1992 r. i zawierały między innymi ulepszone sterowniki urządzeń multimedialnych
oraz ulepszone programy biurowe (dysponujące różnymi zestawami czcionek i mogące wymieniać
obiekty pomiędzy sobą).
Wersje 3.x systemu Windows wykorzystywały 16-bitowy tryb adresowania oferowany przez procesory
Intel 80286 i 80386, dodatkowo od wersji 3.1 w górę mogły wykorzystywać mechanizm stronicowania
pamięci udostępniany przez Intel 80386.
Algorytm zarządzania procesami w systemach Windows 3.x był algorytmem niewywłaszczeniowym.
Programy użytkowe dostarczane przez producenta wraz systemem były tworzone w taki sposób, aby
dobrowolnie przekazywały procesor innym procesom po upłynięciu określonego czasu, natomiast
programy tworzone przez użytkowników miały możliwość „zawłaszczania” procesora na dowolnie
długi czas.
Wszystkie systemy Windows dostarczają dość bogaty interfejs programisty API (Application Program
Interface), który poza podstawowymi funkcjami systemowymi udostępnia też funkcje obsługujące
elementy interfejsu graficznego - przesuwalnych okien o zmiennych rozmiarach, menu rozwijalnych,
przycisków, okien dialogowych itp. Dostarczają też tekstowy interfejs użytkownika, który wzorowany
jest na pierwotnym interfejsie DOS-a i może być uruchamiany zarówno w trybie pełnoekranowym
(czyli trybie tekstowym sterownika karty graficznej), jak również w wielu oknach tekstowych
symulowanych w trybie graficznym.
Procesory Intel o numerach 80286 i wyższych w ramach zapewniania kompatybilności udostępniają
tryb rzeczywisty jako jeden ze swoich trybów pracy. Oznacza to, że programy użytkowe przeznaczone
do wykonywania na procesorze 8086 mogą być wykonywane również na procesorach o wyższych
numerach. Procesory te udostępniają również tak zwany tryb wirtualny 8086, w którym wiele
programów przeznaczonych dla trybu rzeczywistego może być współbieżnie (z podziałem czasu)
wykonywanych w oddzielnych przestrzeniach adresowych o pojemnościach 1 MB.
Własność tę wykorzystują systemy Windows, które umożliwiają wykonywanie (w wielu oknach)
programów DOS-owych. Utrudnienie stanowi fakt, że niektóre programy DOS-owe nie zawsze
korzystają z funkcji systemowych komunikując się z urządzeniami zewnętrznymi, ale, w imię
przyspieszenia działania, bezpośrednio wykonują operacje na fizycznych portach i lokatach pamięci
operacyjnej wykorzystywanych przez system. Takie programy nie mogą być uruchamiane pod
systememami Windows, gdyż systemy te (jak wszystkie przeznaczone do pracy w trybie chronionym)
zapewniają ochronę pamięci i sterowników urządzeń zewnętrznych.
Windows NT w wersji 3.1 pojawił się w 1993 r. i był pierwszym spośród systemów Windows, który
wykorzystywał 32-bitowy tryb adresowania udostępniany przez procesory Intel o numerach 80386
i wyższych. Był też wyposażony w emulatory procesorów intelowskich dla niektórych innych
procesorów. Posiadał interfejs graficzny Windows 3.x. Z założenia był przeznaczony do pracy
w sieciach lokalnych niedużych firm i miał mniejsze wymagania sprzętowe, niż system Unix. Dużo
rozwiązań jest „żywcem” przeniesionych z systemu Unix do Windows NT, również większa część
kodu jądra została napisana w języku C (utworzonym specjalnie dla Uniksa) oraz C++. Jednocześnie
wiele wewnętrznych struktur jądra systemu Windows i funkcji systemowych jest źle udokumentowa-
nych (lub w ogóle nieudokumentowanych).
W 1995 r. pojawił się Windows 95 - pierwszy 32-bitowy system Windows przeznaczony dla
komputerów osobistych. Został on wyposażony w nowy interfejs graficzny użytkownika (ale oparty
na podobnych do dotychczasowych wywołaniach funkcji systemowych, co umożliwia wykonywanie
pod nim programów „okienkowych” napisanych dla Windows 3.x). Interfejs ten został przejęty przez
kolejną wersję Windows NT - 4.0, oraz wszystkie kolejne wersje Windows przeznaczone dla
komputerów osobistych (98, 2000, Millenium, XP, ... ).
Wszystkie wersje systemu Windows poczynając od Windows NT 3.1 oraz Windows 95 mają 32-
bitowy interfejs programisty Win32 API, ale umożliwiają też wykonywanie programów napisanych
przy użyciu interfejsu DOS-owego i interfejsu 16-bitowego Win API (w takich granicach, w jakich
pozwalają wymogi bezpieczeństwa - nie jest możliwe przełamywanie ochrony pamięci i portów oraz
zawłaszczanie całego czasu procesora). W związku z poszerzeniem możliwości adresowania
zwiększyły się też możliwości obsługi dużych plików na dysku i dysków o większej pojemności.
Pomimo reorganizacji systemu plików nowe wersje Windows (poza NT) mogą obsługiwać również
starsze systemy plików na wydzielonych dyskach logicznych. Pewnym problemem stała się zmiana
DOS-owego interfejsu użytkownika tak, aby mógł obsługiwać dłuższe nazwy i rozszerzenia nazw
plików (pod DOS-em nazwa mogła być co najwyżej 8-znakowa, a rozszerzenie - co najwyżej
3-znakowe).
Wszystkie 32-bitowe systemy Windows implementują podział czasu i dysponują wywłaszczeniowym
algorytmem szeregowania procesów. Gospodarowanie pamięcią operacyjną oparte jest na mechanizmie
segmentacji stronicowanej.
Jedną z różnic organizacyjnych pomiędzy Windows w wersjach 16-bitowych i Windows w wersjach
32-bitowych jest to, że te pierwsze przechowywały parametry uruchamiania programów użytkowych
w oddzielnych plikach konfiguracyjnych (rozszerzenie ini) związanych z poszczególnymi programami,
natomiast te drugie przechowują ustawienia we wspólnym obiekcie nazywanym Rejestrem. Jest to
związane z faktem, że Windows 32-bitowe rozróżniają swoich użytkowników (którzy muszą mieć
przydzielone nazwy i hasła, i logować się na początku sesji pracy, a wylogowywać się na końcu).
Użytkownicy mogą mieć swoje indywidualne preferencje (wygląd ekranu, kolory, czcionki itp.)
które system zapamiętuje i uwzględnia przy logowaniu i uruchamianiu programów użytkowych.
Windows mogą korzystać z prostego protokołu komunikacyjnego Microsoft Networking dla sieci
lokalnych (jest on protokołem nietrasowalnym i nie obsługuje systemu adresów logicznych). Protokół
ten umożliwia wzajemne udostępnianie plików i urządzeń zewnętrznych w obrębie sieci.
Dla wszystkich systemów operacyjnych firmy Microsoft zostały skonstruowane programy obsługujące
protokół internetowy IP, jak również niektóre protokoły wyższego poziomu oparte na IP. Nawet w trybie
rzeczywistym możliwe jest korzystanie (w trybie tekstowym) z usług ftp i telnet, zaś pod Windows -
z przeglądarek internetowych oraz graficznych interfejsów ftp.
Systemy Windows NT są wytwarzane w wersjach Server (serwer) i Workstation (stacja robocza).
Serwery NT instalowane są na sprzęcie o większych możliwościach i na ogół przeznaczone są do
dostarczania usług sieciowych (serwer stron domowych, serwer ftp, serwer poczty elektronicznej itp.).
W przeciwieństwie do systemów uniksowych, serwery Windows NT (do wersji 4.0) nie umożliwiają
otwierania zdalnych sesji pracy użytkownikom nieuprzywilejowanym (zatem są przez nich postrzegane
głównie jako serwery plików).
Systemy Windows NT charakteryzują się większym stopniem niezawodności i bezpieczeństwa od
systemów Windows przeznaczonych dla indywidualnych komputerów PC (poza 2000 i XP). Dotyczy to
zarówno odporności na ataki w sieci komputerowej, jak i ochrony przed wadliwym oprogramowaniem
i przypadkowym zniszczeniem danych (na przykład utratą części danych na dysku w przypadku awarii).
Struktura systemów Windows NT jest warstwowa i wyraźnie izoluje warstwę zależną od sprzętu
(rodzaju procesora) od warstw wyższych (operujących na obiektach logicznych). W celu umożliwienia
wykonywania programów przeznaczonych dla innych (kompatybilnych) systemów operacyjnych,
w strukturze Windows NT wyodrębniono tak zwane podsystemy środowiskowe, emulujące
środowiska OS / 2, Windows 16-bitowych, DOS-a i środowisko zgodne ze standardem POSIX.
Wszystkie wyżej wymienione podsystemy współpracują z podsystemem środowiskowym Win32,
który jest najniższą warstwą wykonującą się w trybie użytkownika. Podsystem ten komunikuje się
z wykonującym się w trybie uprzywilejowanym egzekutorem, który składa się z modułów o nazwach:
- zarządca wejścia - wyjścia ;
- zarządca obiektów ;
- monitor bezpieczeństwa odwołań ;
- zarządca procesów ;
- zarządca pamięci wirtualnej ;
- udogodnienie wywoływania procedur lokalnych .
Pozostałymi (poza egzekutorem) częściami składowymi systemu wykonywanymi w trybie uprzywile-
jowanym są:
- jądro systemu ;
- warstwa abstrakcji sprzętu (Hardware Abstraction Layer, HAL).
Warstwa abstrakcji sprzętu umożliwia współpracę jądra z różnymi rodzajami procesorów (w wersji
4.0 z układami zawierającymi nie więcej, niż 8 procesorów).
Jądro systemu Windows NT jest tak zwanym mikrojądrem, czyli zawiera jedynie najprostsze,
najbardziej podstawowe kody funkcji systemowych (wszystkie pozostałe są już realizowane jako
procedury w wyższych warstwach systemu i nie mają zagwarantowanej niepodzielności wykonania).
Strony przechowujące kod i dane jądra nigdy nie są usuwane z pamięci operacyjnej, a procesy jądra
nigdy nie podlegają wywłaszczaniu.
Jądro ma organizację obiektową, co oznacza, że ze strukturami danych jądra (atrybutami) są
jednoznacznie związane zbiory funkcji (metod) operujących na nich. Wyróżniane są dwa rodzaje
obiektów jądra:
- obiekty dyspozytora ;
- obiekty sterujące .
Wśród obiektów jądra Windows NT są zarówno procesy (ciężkie), jak i wątki. Każdy proces
dysponuje pewną przestrzenią adresową, w której wykonuje się pewna liczba (jeden lub więcej)
wątków. Wątki mają priorytety będące liczbami naturalnymi z zakresu 0 - 31. Priorytety z zakresu
16 - 31 określają klasę czasu rzeczywistego wątków, a priorytety z zakresu 0 - 15 - klasę zmienną
wątków (choć system nie gwarantuje wątkom czasu rzeczywistego żadnych limitów czasowych).
Obiekty dyspozytora są bezpośrednio związane z synchronizacją i zmianą przydziału procesora. Są to:
- wątki (obiekty będące podstawowymi jednostkami szeregowania i wykonania);
- zdarzenia (obiekty odnotowujące fakt wystąpienia zdarzenia asynchronicznego);
- semafory, muteksy i mutanty (obiekty służące do synchronizacji wątków);
- czasomierze (obiekty mierzące czas wykonywania procedur i przerywające je w razie potrzeby).
Do obiektów sterujących zaliczane są:
- procesy (obiekty zawierające informacje o właścicielu, priorytecie, przydziale pamięci itp.);
- przerwania (obiekty kojarzące rodzaje zdarzeń z obsługującymi je procedurami);
- obiekt stanu zasilania (odnotowuje, czy wystąpiła awaria) i obiekt informujący o zasilaniu (wywołuje
procedurę obsługi w przypadku wystąpienia awarii);
- obiekty profilujące (mierzące czas wykonania wybranych fragmentów kodu - do celów
diagnostycznych);
- asynchroniczne wywołania procedur (programowe przerwania wykonań wątków).
Algorytm szeregowania wątków stosowany przez jądro systemu NT przywiązuje dość dużą wagę do
sprawności wątków interakcyjnych, czyli takich, które obsługują procesy bezpośredniego komuniko-
wania się użytkownika z systemem. Z tego powodu priorytety takich wątków oczekujących w kolejce
do wykonania przyrastają szybciej, niż na przykład priorytety wątków obsługujących komunikację
z urządzeniami dyskowymi lub sieciowymi.
Priorytet wątku zawieszonego wskutek upłynięcia przydzielonego kwantu czasu zostaje obniżony do
poziomu priorytetu podstawowego (związanego z jego procesem). Priorytety wątków klasy czasu
rzeczywistego nie ulegają zmianie. Każdy wątek klasy czasu rzeczywistego umieszczony w kolejce
wątków gotowych do wykonania powoduje wywłaszczenie wykonywanego wątku klasy zmiennej.
W systemach wieloprocesorowych wątki mogą oczekiwać na przydział dowolnego procesora lub
wybranego procesora. Jeżeli w pewnym momencie pewien procesor nie ma żadnego wątku, który
mógłby wykonywać, dyspozytor przydziela mu do wykonania tak zwany wątek bezczynny (idle).
Zarządzanie pamięcią stronicowaną bazuje na dwupoziomowej strukturze:
Katalog tablic stron procesu
Tablica stron nr 0 Tablica stron nr 1 Tablica stron nr 1024
Strona Strona Strona Strona Strona
4 KB 4 KB 4 KB 4 KB 4 KB
Zarówno katalog, jak i pojedyncza tablica stron mogą zawierać do 1024 wpisów.
Strona może przebywać w pamięci operacyjnej lub w pliku na dysku. Procesy uprzywilejowane
mają prawo zabronić usuwania wybranych stron z pamięci do pliku. Strony posiadają atrybuty, które
informują o stopniu ich ochrony (na przykład read-only).
Procesy mogą współdzielić obiekty pamięci, w których mogą mieć odwzorowane fragmenty swoich
przestrzeni adresowych.
Rdzennym systemem plików systemów operacyjnych Windows NT jest NTFS. System jest logicznie
podzielony na tomy (volume). Każdy tom zawiera hierarchiczną (drzewiastą) strukturę katalogów.
Jednostką przydziału miejsca na dysku są nie pojedyncze sektory, lecz całe ciągi przyległych sektorów
nazywane gronami (cluster). Wielkość gron jest ustalana na etapie konfiguracji systemu (mogą być
przyjęte wielkości domyślne).
System plików na dysku jest obsługiwany przez program FtDisk, którego założeniem jest uzyskanie
dużej odporności na skutki awarii systemu. Stosuje on kilka wariantów (skomplikowanego) zapisu
na dysku przy uwzględnieniu pewnej nadmiarowości, która w razie awarii pozwala odtworzyć
przynajmniej część zapisu z pewnego momentu przed awarią. W skrajnym przypadku program ten
stosuje zapis lustrzany (mirroring) dublujący zapis każdej informacji przy użyciu dwóch niezależnych
sterowników.
System NTFS posiada również wbudowaną możliwość kompresji danych w poszczególnych plikach
w celu zaoszczędzenia miejsca na dysku.
10. SIECIOWY SYSTEM OPERACYJNY NOVELL NETWARE
Systemy Novell NetWare są serwerami plików zaprojektowanymi pod kątem obsługi użytkowników
sieci lokalnej pracujących przy komputerach wyposażonych w systemy operacyjne firmy Microsoft
(DOS, Windows w dowolnej wersji), ale mogącymi również współpracować z programami klienckimi
dla systemów OS / 2, Macintosh i Unix (programy te nie są dostarczane przez firmę Novell wraz
z oprogramowaniem serwera i muszą być zakupione oddzielnie).
Wszystkie wersje systemów NetWare umożliwiają dostarczanie nie tylko usług dostępu do sieciowego
systemu plików, ale również podstawowych usług internetowych, takich jak FTP, DNS, DHCP czy
udostępnianie stron domowych (www).
Systemy te stanowią w pewnym stopniu konkurencję dla systemów Windows NT Server, gdyż ceny
są porównywalne, a pod wieloma względami oba serwery są funkcjonalnie równoważne. Według
danych z 1998 r., serwery NetWare stanowiły około 38 procent wszystkich serwerów na świecie,
serwery zgodne z systemem Unix - około 21 procent, zaś serwery Windows NT - około 16 procent.
Firma Novell poza serwerem NetWare dostarcza programy klientów Novell Client (zarówno w wersji
16-bitowej, przeznaczonej dla DOS-a i Windows 3.*, jak i w wersji 32-bitowej, przeznaczonej dla
wszystkich nowszych systemów Windows). Firma Microsoft również dostarcza własne programy
klienckie przeznaczone do komunikowania się z serwerami NetWare dołączone do oprogramowania
systemów Windows 32-bitowych, ale nie są one w stanie wykorzystać wszystkich możliwości
nowszych wersji serwerów NetWare.
Dla serwerów NetWare do wersji 4.* standardowym protokołem sieciowym służącym do komuniko-
wania się z klientami jest protokół IPX. Jest to protokół przeznaczony dla sieci lokalnych, nietraso-
walny (bazuje na systemie adresów fizycznych urządzeń sieciowych). Serwery NetWare w wersjach
5.* mogą komunikować się zarówno przy użyciu protokołu IPX, jak i przy użyciu protokołu
internetowego IP, przy czym ten drugi jest dla nich protokołem domyślnym. Wykorzystanie IP
umożliwia (jeśli administrator sieci przydzieli takie uprawnienia i odpowiednio skonfiguruje serwer)
zdalny dostęp (spoza sieci lokalnej) klientów do sieciowego systemu plików.
Systemy operacyjne Novell NetWare są systemami wielozadaniowymi z wywłaszczaniem, natomiast
nie oferują wielodostępu (użytkownicy nie mogą otwierać na nich sesji pracy). Administrator systemu
komunikuje się z nim z konsoli serwera, która dysponuje zarówno interfejsem tekstowym (zestaw
poleceń jest inny, niż zestaw poleceń systemów firmy Microsoft), jak i interfejsem graficznym. Jeśli
w sieci lokalnej jest wiele serwerów Netware, administrator może pracować przy którymkolwiek
z nich. W przypadku nowszych wersji serwera Netware (mogących komunikować się przy użyciu
protokołu IP), administrator może również zdalnie zarządzać całą siecią.
Nowsze wersje systemów Netware mają większe wymagania sprzętowe, niż starsze wersje, ale też
oferują dużo większe możliwości. Są w stanie wykorzystywać wieloprocesorowe płyty główne
komputerów i posiadają funkcje wyrównywania obciążenia procesorów serwerów. System plików
NSS korzysta z 64-bitowego systemu adresowania, w związku z czym górne ograniczenie rozmiaru
pliku wynosi 8 TB (takie samo jest górne ograniczenie rozmiaru jednego dysku logicznego),
a maksymalna liczba dysków logicznych - 256.
Wewnętrzna baza danych systemów Netware 5.* oparta jest na systemie Oracle.
Wersje systemu NetWare do 3.* były serwerocentryczne, co oznacza, że poszczególne serwery
w sieci lokalnej były obiektami logicznymi dostrzegalnymi dla użytkowników tej sieci - mieli oni
konta na konkretnych serwerach i dostęp do kont jednego użytkownika na różnych serwerach był
zabezpieczany oddzielnymi mechanizmami autoryzacji.
Nowsze wersje NetWare (od 4.*) są określane jako sieciocentryczne, co oznacza, że użytkownik sieci
lokalnej nie musi być świadomy liczby i nazw serwerów w tej sieci - posługując się jednym kontem
(i jednym hasłem) uzyskuje on dostęp do całego rozproszonego systemu plików, fizycznie rozmiesz-
czonego na różnych dyskach różnych serwerów.
Wersje sieciocentryczne NetWare operują na rozmaitych rodzajach obiektów logicznych (ponad 20
klas obiektów). Wszystkie obiekty są zarejestrowane w jednej wspólnej rozproszonej bazie danych
o nazwie Katalog NDS (Novell Directory Services). Katalog ten jest wykorzystywany przez funkcje
serwera Netware nazywane usługami katalogowymi. Taka organizacja systemu znacznie ułatwia
jego administrowanie - wyróżnianie grup użytkowników, przydzielanie uprawnień itp.
Starsze wersje NetWare używały oddzielnych baz danych (segregatorów) na poszczególnych serwerach.
Ogólny podział klas obiektów Novell NetWare:
- obiekt korzeń [Root];
- obiekty pojemniki (container), nazywane też kontenerami;
- obiekty liście (leaf).
Katalog NDS ma organizację drzewa, które posiada korzeń (jest on zawsze oznaczany jako [Root],
tworzony w czasie instalacji systemu i nie może być usuniety ani zmienić nazwy), węzły wewnętrzne
(pojemniki) oraz węzły końcowe (liście).
[Root]
pojemniki
liście
Uwaga
Katalog NDS jest strukturą organizującą różne rodzaje obiektów systemowych i nie powinien być
mylony z żadnym z systemów plików (które też mają drzewiastą organizację).
Rodzaje obiektów w dużym stopniu zostały zaprojektowane pod kątem wykorzystania w typowych
firmach.
Przykłady klas obiektów - pojemników:
- [Root] ;
- Country (kraj) - stosowane są dwuliterowe oznaczenia według standardu X.500 - na przykład
US, FR, PL, DE, RU, ... ( jeśli jest, musi wystąpić bezpośrednio pod [Root] );
- Locality (mniejsza jednostka administracyjna - stan, region, województwo, ...);
- Organization (firma, instytucja, organizacja, ...);
- Organizational Unit (jednostka organizacyjna - dział, filia, wydział, ...).
Przykłady klas obiektów - liści:
- User (użytkownik) - osoba posiadająca konto na serwerze;
- Group (grupa) - grupa użytkowników (zazwyczaj wykonujących wspólne zadanie i posiadających
jednakowe uprawnienia);
- NetWare Server - pojedynczy serwer pracujący we wspólnej sieci NetWare;
- Volume (tom) - system plików na jednym z dysków logicznych jednego z serwerów;
- Printer (drukarka udostępniona w sieci).
Uwaga
Wszystkie wyżej wymienione klasy obiektów są widoczne dla administratora systemu, który ich
nazwami posługuje się w swoich poleceniach dla serwera. Użytkownicy nie muszą być świadomi całej
struktury Katalogu NDS.
Każdy obiekt w Katalogu NDS posiada swój zbiór właściwości (właściwy dla jego klasy). Niektóre
właściwości są wymagane, niektóre mogą być opcjonalne.
Przykład
Właściwościami wymaganymi obiektu klasy User są: Login Name (nazwa konta) i Last Name (nazwisko).
Właściwościami opcjonalnymi są: Title (tytuł) i Telephone Number (numer telefonu).
Większość właściwości jest jednowartościowa (każdy obiekt może mieć przyporządkowaną tylko jedną
wartość takiej właściwości), ale niektóre mogą być wielowartościowe (obiekt może mieć całą listę
właściwości takiego rodzaju - na przykład pojedynczy użytkownik może mieć przyporządkowaną całą listę
numerów telefonów).
Poszczególne obiekty w Katalogu NDS posiadają swoje nazwy. Podobnie, jak w systemach plików,
wszystkie obiekty w obrębie jednego pojemnika muszą mieć nazwy unikalne, natomiast w różnych
pojemnikach nazwy mogą się powtarzać. Pojęciem analogicznym do ścieżki dostępu do pliku (lub
katalogu) jest pojęcie kontekstu w Katalogu NDS. Podobnie, jak w systemach plików wyróżniamy
względne oraz bezwzględne nazwy ścieżkowe obiektów (nazwy poprzedzone względnymi lub
bezwzględnymi ścieżkami dostępu, które jednoznacznie identyfikują obiekt w obrębie całego systemu
plików), w przypadku Katalogu NDS mówimy o bezwzględnych nazwach jednoznacznych (krótko:
nazwach jednoznacznych) i względnych nazwach jednoznacznych.
Notacja kontekstów i nazw jednoznacznych jest, niestety, dość odmienna od zapisu stosowanego
w przypadku systemów plików (co bywa mylące).
Poszczególne elementy zapisu kontekstu (nazwy kolejnych pojemników na ścieżce) są wymieniane
w odwrotnej kolejności, niż w przypadku systemów plików (czyli poczynając od pojemnika najniższe-
go poziomu), z pominięciem [Root], i są oddzielone kropkami.
Przykład ELBLAG . WARMINSKO_MAZURSKIE . PL
Nazwy jednoznaczne obiektów zaczynają się od kropki, po której następuje nazwa obiektu, a po
kolejnej kropce - kontekst.
Przykład
. KOWALSKI . PRACOWNICY . PWSZ . EDUKACJA . PL
Względne nazwy jednoznaczne określane są względem kontekstu bieżącego. Zaczynają się od
nazwy obiektu, po której następuje kropka i ciąg nazw kolejnych pojemników - od pojemnika, w
którym bezpośrednio jest umieszczony dany obiekt, aż do wspólnego pojemnika kontekstu tego
obiektu i kontekstu bieżącego wyłącznie, po którym (na końcu) umieszczonych jest tyle kropek, ile
poziomów do góry trzeba przejść z kontekstu bieżącego.
Przykład
Jeśli kontekstem bieżącym jest
STUDENCI . PWSZ . EDUKACJA . PL
to względną nazwą jednoznaczną obiektu podanego w poprzednim przykładzie będzie
KOWALSKI . PRACOWNICY .
Jednymi z podstawowych czynności wykonywanych w czasie pracy z systemami sieciowymi są
operacje na uprawnieniach (rights). W systemie Novell NetWare większość obiektów może otrzymać
uprawnienia do większości innych obiektów. Obiekt, który otrzymał jakieś uprawnienia, nazywany
jest powiernikiem (trustee). Powiernicy są przeważnie obiektami z następujących klas:
- [Root] ;
- Organization;
- Organizational Unit;
- Organizational Role;
- Group;
- User;
- [Public] (umożliwia nadawanie uprawnień wszystkim użytkownikom sieci, również niezalogo-
wanym, co z kolei może umożliwić im zalogowanie się).
W systemie Netware inne rodzaje uprawnień związane są z dostępem do pojedynczych plików lub
katalogów, a inne z dostępem do obiektów w Katalogu NDS.
Rodzaje uprawnień do plików i katalogów:
- Read [R] - prawo do odczytu zawartości pliku;
- Write [W] - prawo do zapisu do istniejącego pliku;
- Create [C] - prawo do tworzenia nowego pliku lub podkatalogu w katalogu;
- Erase [E] - prawo do usuwania istniejącego pliku lub katalogu;
- Modify [M] - prawo do zmiany nazwy i atrybutów pliku;
- File Scan [F] - prawo do przeglądania zawartości katalogu;
- Access Control [A] - prawo do nadawania powyższych uprawnień do pliku lub katalogu innym
obiektom ;
- Supervisor [S] - wszystkie prawa.
Uwaga
Obiekt mający tylko uprawnienie A do pewnego pliku lub katalogu nie może nadawać innym
obiektom prawa A do tego pliku (katalogu).
Jeżeli obiekt posiada uprawnienia do pewnego katalogu, to domyślnie posiada te same uprawnienia do
wszystkich podkatalogów tego katalogu, wszystkich podkatalogów tych podkatalogów, i tak dalej -
takie przenoszenie uprawnień na niższe poziomy w drzewie katalogów nazywane jest dziedziczeniem
(inheritance) uprawnień. Z każdym katalogiem jest związany filtr dziedziczenia uprawnień
(Inherited Rights Filter, IRF), w starszych wersjach NetWare nazywany maską dziedziczenia
uprawnień (Inherited Rights Mask, IRM), który może ograniczać możliwości dziedziczenia uprawnień
z nadkatalogu. Domyślny filtr pozwala na dziedziczenie wszystkich uprawnień.
Uprawnienia obiektu do katalogu mogą być uprawnieniami nadanymi bezpośrednio lub prawami
odziedziczonymi z nadkatalogu. Ostateczny zestaw uprawnień do katalogu jest obliczany następująco:
(prawa nadane bezpośrednio) * (prawa odziedziczone * IRF)
gdzie symbole * i * mają interpretację operacji bitowych. Obliczony zestaw praw nazywany jest
zestawem praw efektywnych (effective rights).
Uwaga
Uprawnienie S nigdy nie podlega filtrowaniu przez IRF.
Innym rodzajem zabezpieczeń plików i katalogów są ich atrybuty (attribute). Zbiór rodzajów
atrybutów stosowany w systemie NetWare jest nadzbiorem zbioru rodzajów atrybutów stosowanych
w systemach firmy Microsoft. Niektóre atrybuty ustawiane są przez właściciela pliku (katalogu) lub
administratora, niektóre zaś są ustawiane automatycznie przez system - te drugie nazywane są
znacznikami statusu (status flag) . Niektóre atrybuty są specyficzne dla plików, niektóre dla
katalogów, a niektóre mogą być stosowane do obu rodzajów obiektów.
Przykłady
A (Archieve) - znacznik statusu informujący, że zawartość pliku zmieniła się (w związku z czym
może wymagać zrobienia kopii zapasowej);
H (Hidden) - atrybut powodujący, że dany plik lub katalog będzie traktowany jako ukryty (domyślnie
jego nazwa nie będzie wyświetlana w spisie nazw);
P (Purge) - atrybut uniemożliwiający odzyskanie pliku lub katalogu po jego skasowaniu;
Ro (Read only) - atrybut zabezpieczający plik przed usunięciem, zmianą zawartości i zmianą nazwy
(przesłania działanie uprawnień Write, Erase i Modify);
X (Execute only) - atrybut ustawiany przez administratora, uniemożliwia wykorzystanie pliku do
czegokolwiek innego, niż wykonanie (np. uniemożliwia jego skopiowanie).
W starszych wersjach systemu Netware użytkownicy systemu plików operowali na uprawnieniach
przy użyciu poleceń trybu tekstowego:
RIGHTS nazwa - wyświetlenie swoich uprawnień do danego pliku lub katalogu
GRANT uprawnienia FOR nazwa TO użytkownik - przyznanie praw wybranemu użytkownikowi
do danego pliku lub katalogu
REVOKE uprawnienia FOR nazwa TO użytkownik - odbiera przyznane uprawnienia
ALLOW nazwa uprawnienia - ustawia maskę dziedziczenia praw dla katalogu o podanej nazwie
Do odebrania użytkownikowi wszystkich praw do pliku lub katalogu może też posłużyć polecenie:
GRANT NO RIGHTS FOR nazwa TO użytkownik
W nowszych wersjach systemu Netware do operowania na uprawnieniach służą programy posiadające
interfejs graficzny (na przykład program Filer).
System uprawnień związany z obiektami w Katalogu NDS ma podobny charakter, jak system upraw-
nień do plików i katalogów, ale jest bardziej skomplikowany. Uprawnienia te ogólnie dzielimy na
uprawnienia do obiektów i uprawnienia do właściwości. Podział ten jest odzwierciedleniem ogólnej
idei obiektowości w oprogramowaniu: uprawnienia do obiektów są związane z czynnościami, jakie
można wykonywać na całych obiektach (metodami), zaś uprawnienia do właściwości - z dostępem do
poszczególnych właściwości (pól) obiektu.
Rodzaje uprawnień do obiektów:
Browse - prawo do dostrzegania danego obiektu przy przeglądaniu Katalogu NDS;
Create - prawo do tworzenia nowych obiektów wewnątrz danego obiektu (tylko dla pojemników);
Delete - prawo do usunięcia danego obiektu;
Rename - prawo do zmiany nazwy danego obiektu;
Inheritable - prawo do przekazywania w dziedzictwie uprawnień do obiektów wewnątrz danego
obiektu (tylko dla pojemników, w wersjach 5.* NetWare);
Supervisor - wszystkie powyższe prawa oraz wszystkie niżej wymienione prawa do właściwości
danego obiektu.
Rodzaje uprawnień do właściwości:
Read - pozwala odczytać wartość danej właściwości;
Write - pozwala dodawać, usuwać i modyfikować wartości właściwości;
Compare - pozwala porównywać wartość właściwości z wartością zadaną (implikowane przez Read);
Add Self - pozwala dodawać, usuwać i modyfikować swoje własne właściwości (implikowane przez
Write);
Inheritable - pozwala przekazywać w dziedzictwie prawa do własciwości obiektów wewnątrz
danego obiektu (tylko dla pojemników, w wersjach 5.* NetWare);
Supervisor - wszystkie prawa do danej właściwości.
Uwaga
Jedną z właściwości każdego obiektu w Katalogu NDS jest lista praw dostępu (Access Control List).
Należy zachować dużą ostrożność w przyznawaniu uprawnień do tej właściwości.
Podobnie, jak z każdym katalogiem, z każdym obiektem w Katalogu NDS związany jest filtr
dziedziczenia uprawnień. Filtr ten wpływa zarówno na uprawnienia do danego obiektu, jak i na
uprawnienia do jego właściwości. W odróżnieniu od filtrów dla katalogów, filtry dla obiektów są
w stanie zablokować przekazywanie uprawnień Supervisor.
Stosowanie praw (przefiltrowanych przez IRF) do obiektów zawartych w danym obiekcie-pojemniku
nazywamy dziedziczeniem, natomiast przekazywanie praw posiadanych przez pewien obiekt-
pojemnik zawartym w nim obiektom nazywamy odpowiedniością pośrednią. Należy pamiętać, że
prawa wynikające z odpowiedniości pośredniej nie są filtrowane przez IRF danego obiektu-pojemnika.
Każdy użytkownik (czyli obiekt klasy User) może ponadto otrzymać uprawnienia w drodze
odpowiedniości bezpośredniej. Może to nastąpić na trzy sposoby:
- jeśli użytkownik jest właścicielem obiektu klasy Organizational Role, otrzymuje wszystkie prawa
tego obiektu;
- jeśli użytkownik należy do obiektu klasy Group, otrzymuje wszystkie prawa tego obiektu;
- użytkownik otrzymuje wszystkie prawa obiektów wpisanych na listę w jego własności Security
Equal To.
Obliczanie praw efektywnych w Katalogu NDS jest bardziej skomplikowane, niż w systemie plików.
Odbywa się według następujących reguł:
- jeżeli prawa do jakiegoś obiektu lub właściwości zostały przypisane bezpośrednio, tylko one są
obowiązujące;
- jeżeli nie ma praw przypisanych bezpośrednio, prawa efektywne obliczamy nastepująco:
(prawa odziedziczone * IRF) * (prawa z odpowiedniości bezpośredniej) * (prawa
z odpowiedniości pośredniej)
Ze względu na stopień komplikacji obliczania efektywnych praw w Katalogu NDS zalecane jest
sprawdzanie prawidłowości ich wyznaczenia przy pomocy programów narzędziowych.
11. HISTORIA ROZWOJU I OGÓLNA CHARAKTERYSTYKA SYSTEMU UNIX
System operacyjny Unix w swojej najwcześniejszej wersji powstał w 1969 roku w Bell Telephone
Laboratories - jednostce badawczej, której firmą macierzystą była telekomunikacyjna firma AT&T.
Na jego powstanie miały wpływ prace nad projektem systemu operacyjnego Multics prowadzone od
1965 roku wspólnie przez Bell Laboratories, General Electric Company i Massachusetts Institute of
Technology (Bell Labs odstąpiły od tego projektu w 1969 roku). Multics z założenia miał być systemem
wieloprocesowym i wielodostępnym, oferującym swoim użytkownikom dużą moc obliczeniową i dużą
pojemność pamięci, jak również łatwość współdzielenia danych.
Najwcześniejsza wersja Uniksa była napisana przez K. Thompsona dla maszyny PDP-7. Do pracy nad
systemem przyłączyli się D. Ritchie (biorący uprzednio udział w projektowaniu systemu Multics), nieco
później B. Kernighan i inni pracownicy Bell Labs. Thompson i Ritchie opracowali pierwotny system
plików Uniksa i nieduży zbiór prostych funkcji systemowych.
Pomysłodawcą nazwy Unix był B. Kernighan. Nazwa ta przez długie lata była zastrzeżonym znakiem
towarowym firmy AT&T, nabytym dopiero w latach 90-tych przez firmę Novell.
Istotnym momentem w rozwoju systemu Unix było opracowanie przez D. Ritchi'ego języka C (pod
wpływem opracowanego przez K. Thompsona języka B) i napisanie w 1973 roku na nowo całego
jądra systemu w tym języku. Spowodowało to, że konstrukcja systemu stała się przejrzysta i łatwo
przenośna na wszystkie komputery, dla których opracowano kompilator języka C oraz sterowniki
urządzeń zewnętrznych udostępniające wirtualny obraz sprzętu oczekiwany przez jądro systemu
Unix (tak zwaną maszynę wirtualną Uniksa). Od 1973 roku Unix stał się nierozerwalnie związany
z językiem C - kompilator C jest dostępny w każdej wersji Uniksa i wszystkie uniksowe biblioteki
systemowe mają interfejsy programisty w języku C.
Na początku lat 70-tych firma AT&T, nie mając prawa handlowania produktami komputerowymi,
przekazała licencje systemu Unix innym firmom komercyjnym i uniwersytetom amerykańskim.
Spowodowało to ogromną popularyzację systemów uniksowych i rozwój prac nad ich ulepszaniem
w wielu ośrodkach uniwersyteckich. Największy wpływ na dalszy rozwój Uniksa miały prace
prowadzone na Uniwersytecie Kalifornijskim w Berkeley - głównie prace związane z pamięcią
wirtualną i mechanizmem stronicowania. Spowodowały one zainteresowanie rządu amerykańskiego
projektem i wybranie Uniksa jako standardowego systemu operacyjnego dla potrzeb rządowych.
Prace nad systemem Unix w Berkeley były finansowane przez rządową agencję DARPA (Defense
Advanced Research Projects Agency), w dużym stopniu w związku z zapotrzebowaniem rządu na
opracowanie i implementację protokołów sieci Internet pozwalających na komunikację pomiędzy
lokalnymi sieciami komputerowymi opartymi na różnym sprzęcie i różnych protokołach warstwy
łącza. Spowodowało to szybki rozwój systemów uniksowych jako sieciowych systemów operacyjnych,
jak również rozwój samego Internetu, pełniącego początkowo rolę sieci służącej do celów militarnych
i akademickich w Stanach Zjednoczonych.
Wersje źródłowe systemu Unix były udostępniane przez AT&T bez żadnego oprogramowania użytko-
wego. Wiele firm produkowało swoje własne oprogramowanie dla systemów uniksowych i sprzedawało
swoje produkty pod rozmaitymi nazwami kojarzącymi się z nazwą Unix, na przykład Ultrix, Xenix itp.
Nawet duże firmy prowadzące własne prace badawcze, takie jak Hewlett-Packard, DEC czy IBM
decydowały się na implementacje systemów zgodnych z systemem Unix.
Na przełomie lat 70-tych i 80-tych firma AT&T zdecydowała się na wyprodukowanie własnej komer-
cyjnej wersji Uniksa - pierwsza taka wersja pojawiła się w 1982 roku pod nazwą Unix System III.
Różne wersje Uniksów rozpowszechniane przez różne firmy i uczelnie były ze sobą zgodne z punktu
widzenia zwykłego użytkownika, ale nie było pełnej zgodności pomiędzy ich interfejsami programisty
(nie oferowały jednakowych zbiorów funkcji systemowych). Przy porównywaniu ich własności
typowymi „punktami odniesienia” były najbardziej rozpowszechnione wersje systemów z AT&T
(System III, a od roku 1983 System V) oraz z Berkeley (BSD - Berkeley Software Distribution).
Pierwszą szeroko rozpowszechnioną wersją Uniksa z Berkeley była wersja 4.2 BSD.
Ostatnią wersją utworzoną w Bell Labs była SVR4 (System V Release 4), zaś na Uniwersytecie
w Berkeley - 4.4 BSD. Spośród innych wersji Uniksa najszerzej rozpowszechnionych na świecie
należy wymienić system Solaris firmy SUN Microsystems. Istnieją również systemy operacyjne,
które oferują nie-uniksowy interfejs użytkownika, ale są w dużym stopniu zgodne z Uniksem pod
względem wewnętrznej konstrukcji - typowym przykładem jest system Windows NT firmy Microsoft.
Różnorodność i rozpowszechnienie systemów uniksowych spowodowały podjęcie prac nad międzynaro-
dową standaryzacją Uniksa. Standard taki został opracowany przez organizację IEEE pod nazwą
POSIX (Portable Operating Systems Interface). Standaryzacji uległ także język C (ANSI C).
Rodziną systemów operacyjnych zgodnych z systemem Unix są też rozpowszechniane (w niektórych
wersjach nieodpłatnie) systemy Linux. Najwcześniejsza wersja Linuksa została napisana w 1991 roku
przez Linusa Torvaldsa (studenta z Finlandii) i udostępniona w Internecie. Była przeznaczona dla
procesora Intel 80386. Prace nad Linuksem spontanicznie podjęła międzynarodowa społeczność
informatyków w celu utworzenia pełnowartościowego, dostępnego dla wielu różnych rodzajów sprzętu
i niezależnego od firm komercyjnych systemu operacyjnego zgodnego z normą POSIX. Obecnie Linux
jest rozpowszechniany zgodnie z regułami GPL (General Public Licence) określonymi przez międzyna-
rodową organizację FSF (Free Software Foundation). Reguły te pozwalają kopiować i rozpowszechniać
programy podlegające licencji GPL i nakazują udostępnianie kodu źródłowego (nie tylko kodu
binarnego) każdego takiego programu.
Obecnie istnieje wiele różnych odmian systemu Linux, przy czym każda z nich pojawia się w wielu
coraz to nowszych wersjach. Najczęściej spotykane odmiany to Red Hat, Debian i Slackware.
Uwaga
Z Linuksem może również współpracować oprogramowanie komercyjne (nie podlegające licencji GPL).
Ogólne założenia projektowe systemów uniksowych:
- jak największa prostota, przejrzystość i hierarchizacja konstrukcji (nawet kosztem optymalizacji
czasowej i pamięciowej);
- prosty i spójny logicznie interfejs użytkownika dostarczający wszystkich podstawowych usług;
- dostarczenie narzędzi ułatwiających tworzenie bardziej złożonych programów na bazie prostszych
programów;
- korzystanie z hierarchicznego systemu plików, w którym logicznym obrazem zarówno plików, jak
i urządzeń zewnętrznych są niesformatowane strumienie bajtów;
- wielodostęp i wieloprocesowość (przy czym każdy użytkownik może współbieżnie wykonywać
wiele procesów);
- ochrona plików i procesów poszczególnych użytkowników;
- ukrycie przed użytkownikami szczegółów sprzętu komputerowego, na którym funkcjonuje system,
co umożliwia tworzenie oprogramowania łatwo przenośnego do innych systemów uniksowych.
Ogólny diagram jądra systemu Unix ( według [M. Bach] ):
Programy użytkowników
Poziom użytkownika Biblioteki
Poziom jądra
Interfejs funkcji systemowych
Podsystem Komunikacja
plików międzyprocesowa
Podsystem Przydział
Pamięć sterowania procesora
buforowa procesami Zarządzanie
pamięcią
Znakowe Blokowe
Programy obsługi urzadzeń
Sterowniki sprzętu
Poziom jądra
Poziom sprzętu
Sprzęt
Z punktu widzenia użytkownika system umożliwia mu komunikację poprzez urządzenie zewnętrzne
zwane terminalem. Unix powstawał w czasach, gdy podstawowym rodzajem terminala był dalekopis -
urządzenie elektryczne posiadające klawiaturę i umożliwiające drukowanie linii tekstu na wysuwającej
się wstędze papieru. Urządzenie takie posiadało bardzo ubogie możliwości korekty napisanego tekstu
i nie umożliwiało żadnego innego trybu pracy nad tekstem, niż kolejnymi wierszami. Klasyczny
terminal uniksowy (w obecnych czasach traktowany jako abstrakcyjne urządzenie symulowane przez
zestaw złożony z klawiatury i monitora ekranowego wyświetlającego tekst) oznaczony jest symbolem
vt100.
W miarę ulepszania sprzętu komputerowego komplikacji ulegał również logiczny obraz terminali
użytkowników. Terminal tekstowy vt102 obsługuje klawiaturę o większej liczbie przycisków
funkcyjnych i umożliwia bardziej zaawansowaną edycję tekstu. Obecnie dostępnych jest wiele rodzajów
terminali, z których prawdopodobnie najpopularniejszym i udostępniającym najbardziej typowe funkcje
trybu tekstowego współczesnego sprzętu (przesuwanie kursora we wszystkich kierunkach, różne kolory
znaków i tła, rozszerzony kod ASCII itp.) jest terminal ansi.
Unix dość wcześnie (w latach 80-tych) został wyposażony w możliwość obsługi terminali graficznych
(X terminal) udostępniających graficzny interfejs użytkownika (Graphical User Interface - GUI).
X terminal składa się z klawiatury, wskaźnika (myszy) i monitora ekranowego wyświetlającego
informację w graficznym trybie okienkowym. Pierwszy uniksowy interfejs graficzny został
opracowany w MIT przy wspólpracy z firmą DEC.
W klasycznych systemach uniksowych terminal tekstowy był terminalem nieinteligentnym, czyli
urządzeniem obsługującym jedynie prosty protokół komunikacji z komputerem (mainframe) i nie
umożliwiającym żadnego lokalnego przetwarzania. Obecnie zazwyczaj bezpośrednio z komputerem
połączona jest jedynie konsola operatorska (console), a użytkownicy współpracują z systemem
przy pomocy graficznych stacji roboczych, będących samodzielnymi komputerami połączonymi
z systemem uniksowym poprzez sieć lokalną i komunikującymi się z nim przy użyciu protokołu
graficznego (X protocol).
Uwaga
Graficzny tryb pracy umożliwia otwieranie wielu okien tekstowych (pseudoterminali) jednocześnie
na ekranie jednego terminala graficznego.
Jednym z najważniejszych programów systemowych jest interpreter poleceń, przyjmujący i wyko-
nujący polecenia użytkownika w trybie tekstowym. Uniksowe interpretery poleceń znane są pod nazwą
powłok (shell). Istnieje kilka rodzajów powłok, wzajemnie zgodnych między sobą w zakresie podsta-
wowych poleceń:
- shell Bourne'a (sh) - najstarszy, uważany za klasyczny, o najuboższych możliwościach;
- C shell (csh) - powstał w Berkeley, w większym stopniu przypomina język programowania C;
- shell Korna (ksh) - shell początkowo najczęściej używany w komercyjnych wersjach Uniksa.
W związku z opracowaniem systemu Linux, w ramach projektu GNU powstała nowa wersja shella
Bourne'a o nazwie bash (Bourne Again Shell). Wersja ta zaadaptowała większość najkorzystniejszych
cech wcześniej istniejących powłok i obecnie jest prawdopodobnie najbardziej rozpowszechniona na
świecie.
Poszczególne powłoki posiadają swoje charakterystyczne znaki zgłoszenia (prompt), po których łatwo
jest je rozpoznać. Znakiem zgłoszenia bash'a jest znak $ .
Zbiór poleceń dowolnej powłoki może nie tylko służyć do interaktywnej pracy z systemem, ale
również służyć jako interpretowany język programowania. Oznacza to, że użytkownik ma możliwość
tworzenia plików tekstowych zwanych skryptami (script), które zawierają ciągi poleceń powłoki
wykonywane po uruchomieniu skryptu jako kolejne instrukcje programu. Skrypty nie podlegają
kompilacji (tak jak na przykład programy w C), ale kolejne polecenia są z nich wybierane i oddzielnie
interpretowane przez system.
Polecenia wydawane przez użytkownika mogą być poleceniami wewnętrznymi shella, czyli polecenia-
mi realizowanymi przez sam program powłoki, lub poleceniami zewnętrznymi, czyli oddzielnymi
programami wykonywalnymi, których wykonanie interpreter jedynie inicjuje. Jednym z założeń
projektowych Uniksa jest zatarcie różnicy (z punktu widzenia użytkownika) pomiędzy wywołaniami
poleceń wewnętrznych, wywołaniami programów skompilowanych (binarnych) i wywołaniami
skryptów, co ułatwia szybkie konstruowanie większych programów z pewnej liczby mniejszych.
Użytkownik ma możliwość zmiany powłoki w trakcie pracy na inną, wywołując jej nazwę (na przykład
ksh). Zakończenie pracy z daną powłoką i powrót do poprzedniej uzyskujemy podając polecenie exit.
W przypadku skryptu również istnieje możliwość określenia, która powłoka powinna go wykonać.
Jednym z rodzajów obiektów abstrakcyjnych, na których operują systemy uniksowe, są użytkownicy
(user). Użytkownicy są zarejestrowani w systemie, mając przyporządkowane nazwy (login name)
i hasła (password). Wśród użytkowników wyróżniony jest użytkownik uprzywilejowany, którego
nazwą zawsze jest root. Użytkownik uprzywilejowany ma szczególne prawa - ma dostęp do wszystkich
zasobów systemowych, w szczególności do plików innych użytkowników i do przestrzeni adresowych
uruchamianych przez nich procesów. Użytkownik ten pełni rolę administratora systemu.
Użytkownicy, chcąc współpracować z systemem uniksowym, muszą otworzyć na nim sesję pracy.
W tym celu muszą zgłosić się do systemu (zalogować), podając swoją nazwę i hasło. System przepro-
wadza autoryzację użytkownika, sprawdzając odpowiednią pozycję w pliku haseł. W przypadku
pomyślnym przydziela zgłoszonemu użytkownikowi terminal i uruchamia związany z danym termina-
lem proces interpretera komend (jest to tak zwany shell zgłoszony). Proces ten w pierwszej kolejności
wykonuje swój skrypt startowy, ustalający różne szczegóły związane ze sposobem współpracy
z użytkownikiem (na przykład tworzący ścieżki dostępu do częściej używanych katalogów lub
rozbudowujący znak zgłoszenia).
Sesja pracy kończy się w przypadku podania przez użytkownika polecenia exit shellowi zgłoszonemu.
Klasycznym trybem otwierania sesji jest logowanie się w trybie tekstowym, przebiegające według
wyżej opisanego scenariusza. Użytkownik ma prawo otwierania wielu sesji jednocześnie (chyba, że
administrator systemu wprowadzi ograniczenie liczby sesji), przy czym może je otwierać zarówno
z różnych terminali fizycznych, jak też (jeśli oprogramowanie lokalnego komputera na to pozwala),
z jednego komputera - na przykład w wielu oknach tekstowych otwartych w trybie graficznym, lub
w pełnoekranowym trybie tekstowym - wtedy do przełączania pomiędzy sesjami służą odpowiednie
kombinacje kluczy.
Współczesne systemy uniksowe umożliwiają również otwieranie graficznej sesji pracy - w takim
przypadku oprogramowanie stacji roboczej użytkownika musi zawierać program X serwera, który
komunikuje się z komputerem uniksowym przy użyciu X protokołu. Logowanie odbywa się w trybie
graficznym, przy czym X serwer „zagarnia” na czas pracy cały ekran monitora stacji roboczej
i przekształca ją w uniksowy terminal graficzny. Odpowiednikiem shella zgłoszonego jest wtedy
zarządca sesji (session manager).
Uwaga
Zazwyczaj możliwe jest również zalogowanie się do systemu uniksowego w trybie tekstowym, a nas-
tępnie wykorzystywanie X serwera jedynie do odbioru wyjścia graficznego programów uniksowych.
Programy uniksowe generujące wyjście graficzne wyświetlają je na terminalu graficznym wyspecy-
fikowanym przez zmienną środowiska DISPLAY (jeśli sam program nie definiuje swojego terminala).
Zawartość tej zmiennej:
adres_sieciowy_komputera : numer_terminala
Adres sieciowy jest nazwą (adresem) w protokole IP lub innym protokole, na którym oparty jest
protokół graficzny. Numer terminala jest zazwyczaj zerem (chyba, że komputer obsługuje wiele
zestawów do pracy w grafice). Adres sieciowy może być pominięty w przypadku pracy bezpośrednio
na konsoli komputera uniksowego.
W przypadku zalogowania się w trybie graficznym zmienna DISPLAY jest jest ustawiana automatycz-
nie. W przypadku zalogowania się w trybie tekstowym może ona być ustawiona przez skrypt startowy
lub „ręcznie”. Użytkownik ma możliwość przyporządkowania zmiennej DISPLAY innego adresu
terminala graficznego, niż swój własny, a tym samym skierowania wyjścia graficznego swojego
programu do innego komputera (jeśli użytkownik tamtego komputera wyrazi na to zgodę odpowiednim
poleceniem).
Ważną cechą wszystkich systemów uniksowych jest utrzymywanie informacji wbudowanej (manual)
zawierającej opisy wszystkich programów zainstalowanych w systemie oraz wszystkich funkcji syste-
mowych. Informacja ta jest precyzyjniejsza, niż w innych systemach operacyjnych, ma bardziej
techniczny charakter i ściśle określony format. Jest zorganizowana w postaci kilku tomów, przy czym
pierwszy z nich zawiera opisy standardowych poleceń systemowych (polecenia wewnętrzne są często
zgrupowane w jednym opisie programu interpretera komend), a dalsze - opisy funkcji systemowych
i programów niestandardowych.
Polecenie wyświetlenia informacji:
man program
W przypadku, gdy polecenie (program) i funkcja systemowa mają tę samą nazwę, należy podać numer
tomu, na przykład
man read
wyświetli opis polecenia systemowego read, natomiast
man 2 read
wyświetli opis funkcji systemowej read.
Opisy poleceń podają:
- nazwę polecenia;
- składnię jego wywołania, w tym wykaz możliwych opcji;
- liczbę i rodzaj argumentów;
- zwracaną wartość;
- opis działania;
- ewentualny wpływ na zmienne środowiska i zależność od zmiennych środowiska.
Funkcje systemowe są opisane zgodnie z ich interfejsem w języku C. W opisie podane są następujące
elementy:
- nazwa funkcji;
- nazwa pliku nagłówkowego biblioteki, w której jest umieszczona funkcja;
- typ wyniku;
- liczba, kolejność i typy argumentów funkcji;
- opis działania;
- wykaz możliwych sytuacji błędnych wraz z ewentualnymi wartościami zmiennej globalnej errno.
Do każdego systemu uniksowego dołączona jest pewna liczba standardowych programów, których
nazwy i działanie (z punktu widzenia użytkownika) są takie same we wszystkich wersjach uniksów.
Poza wspomnianymi wcześniej powłokami (wywoływanymi przez podane polecenia, przykładowo csh)
i kompilatorem języka C (wywoływanym przez polecenie cc) w każdym systemie uniksowym występuje
edytor tekstu vi (jego interfejs jest uznawany przez wielu użytkowników za prymitywny i niewygodny,
ale posiada on zaskakująco szerokie możliwości edycji).
We współczesnych systemach uniksowych poza standardowymi programami działającymi w trybie
tekstowym dostępnych jest zwykle bardzo dużo programów użytkowych o interfejsie graficznym -
edytory tekstowe działające w trybie graficznym, edytory graficzne, przeglądarki internetowe, gry
komputerowe, odtwarzacze muzyki i filmów itd. - nie są one jednak zestandaryzowane, a ich obecność
w systemie zależy od decyzji administratora i posiadanych zasobów.
12. SYSTEM PLIKÓW SYSTEMU OPERACYJNEGO UNIX
Logiczny obraz systemu plików z punktu widzenia użytkownika Uniksa jest jednym grafem
acyklicznym o korzeniu oznaczonym znakiem / . Ten logiczny obraz nie jest zależny od
umiejscowienia plików na konkretnych urządzeniach fizycznych - dyskach twardych (ich partycjach),
dyskietkach itp., a nawet na urządzeniach innych komputerów w sieci - administrator systemu dokonuje
montowania (mount) poszczególnych fizycznych systemów plików w jednym dużym grafie, wspólnym
dla wszystkich użytkowników systemu, który może być widziany jako sieciowy system plików.
Oznacza to, że interfejs użytkownika nie pozwala mu na stwierdzenie, gdzie (na którym komputerze
w sieci, na którym jego urządzeniu) przechowywane są jego pliki. Oznacza to również, że zwykły
użytkownik nie ma uprawnień do posługiwania się dyskietkami i płytkami CD w systemach uniksowych
i w razie potrzeby musi zwrócić się z taką sprawą do administratora. Typowym sposobem ominięcia
tego problemu jest przesyłanie plików pomiędzy indywidualnym komputerem użytkownika a jego
serwerem uniksowym przy użyciu protokołu ftp lub innego równoważnego.
W systemie plików przechowywane są zarówno pliki zawierające dane i oprogramowanie systemowe,
jak i pliki należące do poszczególnych użytkowników. Katalogi najwyższego poziomu mają zwykle
standardowe nazwy - poniżej naszkicowany jest fragment typowego grafu:
/
bin dev etc home lib tmp ..................
bash cat .... tty1 .... passwd .... anna jan ..... libcurses libXt ...... ...............
Katalogi domowe poszczególnych użytkowników są umieszczane w katalogu home (czasem user).
W pozostałych katalogach umieszczone są pliki systemowe, na przykład w bin programy binarne
(skompilowane), w dev pliki specjalne urządzeń (device), w lib biblioteki funkcji (library) itd.
W różnych systemach uniksowych ogólne struktury grafów katalogów są podobne, ale w szczegółach
mogą się różnić.
Zgodnie z przyjętą filozofią systemu Unix, poza zwykłymi plikami w grafie katalogów są też
umieszczone pliki specjalne będące logicznymi obrazami urządzeń zewnętrznych (na przykład
terminali), środków komunikacji międzyprocesowej (kanałów komunikacyjnych) i innych obiektów.
W interfejsie użytkownika (na przykład w poleceniu ls -l ) wyróżnione są następujące rodzaje plików
(i przyporządkowane tym rodzajom symbole):
oznacza plik zwykły (ordinary file)
d oznacza katalog (directory)
l oznacza dowiązanie symboliczne (miękkie) (symbolic link)
c oznacza urządzenie znakowe (character device)
b oznacza urządzenie blokowe (block device)
n oznacza urządzenie sieciowe (network device)
s oznacza gniazdo (socket)
p oznacza łącze nazwane (named pipe)
Poza rodzajem plik uniksowy posiada wiele innych atrybutów, które są uwidocznione w interfejsie
użytkownika. Ważnymi atrybutami są prawa dostępu wyświetlane przez polecenie ls -l
w następującym formacie:
* * * * * * * * * *
u g o
rodzaj pliku
u - prawa właściciela (user)
(według podanych oznaczeń)
g - prawa grupy (group)
o - prawa innych użytkowników (other)
Każdy plik ma swojego właściciela (użytkownika, którego proces utworzył dany plik). Użytkownik
może należeć do jednej lub więcej grup użytkowników (z których jedna jest wyróżniona dla każdego
użytkownika jako jego grupa główna). Grupy są z kolei podzbiorami wszystkich użytkowników
danego systemu uniksowego.
Uwaga
Prawa właściciela, prawa jego grupy i prawa innych użytkowników stanowią oddzielne, niezależnie
ustalane zbiory atrybutów (na przykład właściciel może odebrać sobie prawa, ale pozostawić je grupie).
W powyższym 10-znakowym zestawieniu pierwszy znak określa rodzaj pliku, natomiast ciąg
pozostałych znaków jest traktowany jako trzy trójki, odpowiednio określające prawa dostępu dla
właściciela pliku, jego grupy i pozostałych użytkowników. W każdej trójce na każdej pozycji może
wystąpić litera (prawa nadane) lub znak - (prawa odebrane).
Na pierwszej pozycji:
r prawo do czytania (read)
Na drugiej pozycji:
w prawo do pisania (write)
Na trzeciej pozycji:
x prawo do wykonywania (execute)
s
S interpretacja zależna
t od położenia w ciągu
T
Ostatni rodzaj praw związany jest z uruchamianiem procesów na podstawie pliku wykonywalnego.
Proces może posiadać prawa użytkownika, który go uruchomił, lub właściciela pliku wykonywalnego
(nie musi to być ten sam użytkownik). Podobnie może posiadać prawa grupy użytkownika, który go
uruchomił, lub grupy właściciela pliku (mieć nadany identyfikator grupy właściciela).
Uwaga
Nie wszystkie rodzaje praw odnoszą się do wszystkich rodzajów plików, ponadto w przypadku
niektórych rodzajów mają odmienną interpretację. W szczególności w przypadku katalogów:
- prawo do czytania oznacza prawo do przeglądania zawartości katalogu (np. wyświetlenia jej na
ekranie terminala);
- prawo do pisania oznacza prawo do zmiany zawartości katalogu (tworzenia i usuwania w nim plików
i podkatalogów);
- prawo do wykonywania oznacza prawo do przeszukiwania („wejścia” do danego katalogu).
W przypadku programu, który jest przeznaczony do częstego wykonywania, jego tekst (zawartość pliku)
może być pozostawiony w pamięci operacyjnej, aby zaoszczędzić czasu na ponownym ładowaniu
programu z pliku. Atrybut pliku, który to ustala, nazywany jest bitem lepkości (sticky bit).
Zestawienie interpretacji atrybutów x, s, S, t, T w zależności od położenia w ciągu:
* * x * * * * * * prawo właściciela do wykonywania (brak ustanowienia ID właściciela dla
grupy i dla innych)
* * s * * * * * * prawo właściciela do wykonywania (ustanowienie ID właściciela dla grupy i dla
innych)
* * S * * * * * * brak prawa właściciela do wykonywania (ustanowienie ID właściciela dla grupy
i dla innych)
* * * * * x * * * prawo grupy do wykonywania (brak ustanowienia ID grupy dla innych)
* * * * * s * * * prawo grupy do wykonywania (ustanowienie ID grupy dla innych)
* * * * * S * * * brak prawa grupy do wykonywania (ustanowienie ID grupy dla innych)
* * * * * * * * x prawo do wykonywania dla innych (bez ustawienia bitu lepkości)
* * * * * * * * t prawo do wykonywania dla innych (i ustawienie bitu lepkości)
* * * * * * * * T brak prawa do wykonywania dla innych (i ustawienie bitu lepkości)
Szczególną postacią grafu acyklicznego jest drzewo. W drzewie każdy katalog (poza korzeniem)
ma dokładnie jeden nadkatalog i każdy plik należy do dokładnie jednego katalogu. Ogólnie, w grafie
acyklicznym nie musi to zachodzić - można tworzyć dodatkowe dowiązania zarówno plików, jak
i katalogów do katalogów innych, niż te, w których już są. W interfejsie użytkownika służy do tego
polecenie ln .
Uwaga
Zwykły użytkownik systemu ma jedynie prawo tworzenia dowiązań do plików. Prawo tworzenia
dowiązań do katalogów ma jedynie administrator systemu, ze względu na niebezpieczeństwo
zapętlenia w ten sposób grafu katalogów (co może wiązać się z nieodwracalnym uszkodzeniem
systemu plików).
Jeśli plik (lub katalog) jest dowiązany w kilku miejscach, to jego usunięcie w jednym miejscu nie
powoduje jego fizycznego skasowania, a jedynie usunięcie dowiązania do tego miejsca. Dopiero
usunięcie ostatniego istniejącego dowiązania powoduje fizyczne usunięcie z systemu plików.
Wyżej omówione dowiązania noszą nazwę dowiązań twardych (hard link). Ich tworzenie i usuwanie
wiąże się bezpośrednio ze zmianą informacji zapisanej w systemie na temat danego pliku. Użytkownik
ma prawo tworzenia również dowiązań symbolicznych (symbolic link). W interfejsie użytkownika są
one uwidocznione w podobny sposób, jak dowiązania twarde, ale w rzeczywistości są jedynie małymi
plikami zawierającymi informację na temat usytuowania pliku, do którego zostało stworzone takie
dowiązanie.
Z punktu widzenia użytkownika najbardziej istotną różnicą pomiędzy dowiązaniami twardymi
a miękkimi (symbolicznymi) jest to, że dowiązanie twarde może być utworzone tylko do istniejącego
pliku lub katalogu (system musi sprawdzić jego istnienie, modyfikując zapis w odpowiedniej
strukturze systemowej), zaś dowiązanie miękkie jest tylko plikiem z zapisaną informacją „na życzenie
użytkownika”, nie weryfikowaną przez system operacyjny. Odpowiednio, usunięcie wszystkich
twardych dowiązań do danego pliku spowoduje jego fizyczne skasowanie, bez względu na to, czy
gdzieś istnieją jakieś dowiązania symboliczne do tego pliku.
Łącza nazwane (kolejki FIFO) są specjalnym rodzajem plików przeznaczonych do umożliwiania
komunikacji międzyprocesowej. Mogą one być traktowane jako „pliki z nietrwałą zawartością”
w tym sensie, że każdy odczyt porcji informacji z łącza przez pewien proces jednocześnie usuwa tę
informację z łącza. Pojedyncze łącze umożliwia komunikację jednokierunkową - jeżeli chcemy
ustanowić komunikację dwukierunkową pomiędzy dwoma procesami, musimy użyć do tego pary łącz.
Łącza wymuszają synchronizację dwóch komunikujących się procesów - proces, który otworzył łącze
do zapisu, zostaje zawieszony aż do otwarcia łącza przez inny proces do odczytu (i na odwrót).
Podobnie proces, który wywołał funkcję zapisu do łącza, zostaje zawieszony aż do wywołania przez
drugi proces funkcji odczytu z łącza (i na odwrót).
Łącza nazwane są uwidocznione dla użytkownika w systemie plików jako pliki specjalne
o wielkości 0. Do ich utworzenia służy polecenie systemowe mkfifo. Poza łączami nazwanymi Unix
dysponuje również łączami nienazwanymi (bezimiennymi), które nie są uwidocznione dla
użytkownika. Są to tymczasowe pliki nietrwałe (a właściwie ich bufory), które są tworzone w katalogu
bieżącym na czas przekazywania strumienia danych z jednego procesu do drugiego wskutek użycia
operatora | .
Gniazda (socket) są alternatywnym medium komunikacji międzyprocesowej, charakterystycznym dla
Uniksów BSD. Zostały zaprojektowane jako obiekty interfejsu do protokołów komunikacyjnych
(w szczególności do protokołu internetowego IP). Mogą służyć zarówno do komunikacji wewnętrznej
(między procesami w tym samym systemie uniksowym), jak i do komunikacji pomiędzy procesami
na różnych komputerach.
W katalogu /dev umieszczone są pliki specjalne będące logicznymi obrazami urządzeń fizycznych -
terminali użytkowników, urządzeń dyskowych, drukarek i innych. Sposób ich obsługi zależy od
konkretnych programów obsługi (sterowników) tych urządzeń. Tekstowe terminale użytkowników
traktowane są jako pliki tekstowe, do których (jeśli prawa dostępu na to pozwalają) można bezpośrednio
pisać, i z których można bezpośrednio czytać. Związana z tym jest możliwość przesyłania sobie
wzajemnie komunikatów przez użytkowników systemu uniksowego, jak również możliwość
„bronienia się” przed otrzymywaniem niepożądanych komunikatów.
Uwaga
Administrator systemu zawsze ma możliwość przesłania komunikatu dowolnym użytkownikom.
Wewnętrzna organizacja systemu plików
Z każdym plikiem w systemie skojarzona jest systemowa struktura danych zawierająca informacje na
temat tego pliku, nazywana jego węzłem indeksowym (index node) lub, krótko i-węzłem (i-node).
I-węzeł zawiera następujące informacje:
- identyfikator właściciela pliku;
- typ pliku;
- prawa dostępu;
- czasy: ostatniego dostępu, ostatniej modyfikacji pliku i ostatniej modyfikacji i-węzła;
- liczba twardych dowiązań;
- tablica adresów bloków dyskowych, w których zapisana jest zawartość pliku;
- rozmiar pliku.
Tablica wszystkich i-węzłów zapisana jest na dysku, a jej kopia - w pamięci operacyjnej. Kopie
i-węzłów zawierają dodatkowo informacje o założeniu blokady na plik przez pewien pewien proces,
o modyfikacji i-węzła od czasu skopiowania do pamięci i o tym, czy plik jest punktem montowania.
Jeśli jakikolwiek proces chce czytać dane z pliku lub zapisywać do niego dane, musi najpierw ten plik
otworzyć, a po wykonaniu na nim operacji zamknąć go. W systemie przechowywane są w związku
z tym następujące struktury danych:
- struktura globalna jądra systemu nazywana tablicą plików, w której każda pozycja odpowiada
jednemu otwarciu pliku (zatem w przypadku, gdy dwa różne procesy otworzą ten sam plik, w tablicy
będą zapisane dwie pozycje);
- dla każdego procesu w jego indywidualnej strukturze danych przechowywana jest jego tablica
deskryptorów plików, która zawiera indeksy (numery pozycji) w globalnej tablicy plików związane
z otwarciami plików dokonanymi przez ten proces.
Tablice deskryptorów Tablica plików Tablica i-węzłów
plików (po jednej na proces) (globalna) (globalna)
Uwaga
Z powyższego sposobu zapisu informacji o plikach otwartych wynika, że w każdej chwili zarówno
liczba plików otwartych przez pojedynczy proces, jak i łączna liczba wszystkich plików otwartych
w systemie jest ograniczona. Maksymalne wielkości tych liczb są ustalane przez administratora
w czasie konfiguracji systemu operacyjnego.
Każdy proces w momencie swojego uruchomienia ma automatycznie otwarte trzy deskryptory plików:
0 - standardowe wejście (standard input);
1 - standardowe wyjście (standard output);
2 - standardowe wyjście błędów (standard error output).
Przez domniemanie te trzy deskryptory są związane z terminalem właściciela procesu (może to być
zmienione przez przekierowania) i służą odpowiednio do czytania danych, wyświetlania wyników
i wyświetlania komunikatów o błędach. Użytkownicy mogą rozdzielić mieszające się na ekranie
terminala strumienie wyjściowe: wyników i błędów, poprzez przekierowanie ich do dwóch różnych
plików.
Jedną z najważniejszych informacji zapisanych w każdej pozycji tablicy plików jest aktualne
położenie wskaźnika pliku. Ponieważ różne pozycje w tablicy plików mogą wskazywać na ten sam
i-węzeł, a ponadto w tablicach deskryptorów procesów mogą występować jednakowe deskryptory,
stwarza to bogactwo możliwości współpracy wielu procesów za pośrednictwem współdzielonych
plików. Jest możliwe zarówno niezależne przemieszczanie swoich wskaźników po jednym pliku przez
dwa procesy (w typowym zastosowaniu jeden z nich zapisuje dane, a drugi asynchronicznie te dane
odczytuje), jak też korzystanie (zwykle przez grupę spokrewnionych procesów) ze wspólnego
deskryptora, co umożliwia im na przykład naprzemienne zapisywanie danych do wspólnego pliku.
W systemach uniksowych procesy potomne dziedziczą po swoich procesach rodzicielskich między
innymi wszystkie deskryptory otwartych plików. Oznacza to, że strumienie wejściowe i wyjściowe
tych procesów będą mieszały się (jest zatem możliwe zjawisko wzajemnego „podkradania sobie”
danych przez proces rodzicielski i potomny) i programista powinien zadbać, aby w jego programach
nie występowały zjawiska hazardu (niedeterminizmu).
13. ZARZĄDZANIE PROCESAMI W SYSTEMIE OPERACYJNYM UNIX
Z punktu widzenia organizacji systemu Unix procesy są obiektami logicznymi posiadającymi swoje
atrybuty (zapisane w odpowiednich systemowych strukturach danych), na których można wykonywać
operacje przy użyciu odpowiednich funkcji systemowych.
Najważniejsze atrybuty procesów:
- identyfikator procesu PID (Process ID)
- identyfikator procesu rodzicielskiego PPID (Parent Process ID)
- rzeczywisty identyfikator użytkownika UID (User ID)
- efektywny identyfikator użytkownika EUID (Effective User ID)
- rzeczywisty identyfikator grupy użytkowników GID (Group ID)
- efektywny identyfikator grupy użytkowników EGID (Effective Group ID)
identyfikator grupy procesów PGRP (Process Group)
Proces może mieć swój terminal sterujący, z którym może komunikować się, pobierając z niego
dane (na przykład polecenia użytkownika) i wyświetlając wyniki. Jednym z atrybutów procesu jest
wskaźnik do struktury terminala - systemowej struktury danych związanej z danym terminalem.
Struktura terminala zawiera między innymi bieżący identyfikator grupy terminala (Current
Terminal Process Group) - nie ma ustalonego skrótu, na potrzeby wykładu przyjmijmy CTPGRP.
Jeśli proces nie ma swojego terminala sterującego, jego wskaźnik ma wartość NULL.
W większości przypadków procesy użytkowników mają swój terminal sterujący (ten, z którego
zostały uruchomione), natomiast procesy systemowe nie mają swojego terminala (wykonują one
swoją pracę przez cały czas, niezależnie od tego, czy nadzorca systemu jest aktualnie zalogowany).
Procesy systemowe, mające za zadanie dostarczanie określonych usług na rzecz innych procesów,
nazywane są często serwerami tych usług lub demonami (daemon).
Zwykły użytkownik systemu również ma możliwość uruchamiania procesów „pozbawionych
terminala sterującego”, które mogą być wykonywane nawet po zakończeniu przez użytkownika sesji
pracy i wyłączeniu terminala (na przykład mogą wykonywać przez całą noc skomplikowane
obliczenia i zapisywać wyniki do pliku).
Proces interpretatora komend, który zostaje uruchomiony w wyniku zalogowania się do systemu,
nazywany jest shellem zgłoszonym. Na początku pracy jest on procesem sterującym terminala,
z którego nastąpiło zalogowanie, czyli przywódcą grupy terminala (zachodzi dla niego
PID = PGRP = CTPGRP).
Proces nazywamy pierwszoplanowym (foreground process), jeżeli jego PGRP jest równy CTPGRP
wskazywanego przez niego terminala. W przeciwnym przypadku proces nazywamy drugoplanowym,
lub wykonującym się w tle (background process). Procesy pierwszoplanowe mogą odczytywać znaki
z terminala i zapisywać znaki do terminala (to jest pliku specjalnego odpowiadającego terminalowi).
Procesy drugoplanowe nie mogą odczytywać znaków z terminala (są zawieszane przy próbie czytania),
natomiast zazwyczaj mogą zapisywać znaki do terminala (jest to zależne od systemu i aktualnych
ustawień w strukturze terminala).
Uwaga.
Zazwyczaj w każdej chwili istnieje dokładnie jeden proces pierwszoplanowy terminala.
Pojedynczą komendę lub pojedynczy potok komend uruchomiony z linii poleceń shella nazywamy
pracą (job). Shell ma własność sterowania pracami (job control), jeżeli posiada możliwość zmiany
CTPGRP terminala, z którego został uruchomiony.
Polecenia:
komenda - uruchamia proces pierwszoplanowy;
komenda & - uruchamia proces drugoplanowy
(komenda &) - uruchamia proces drugoplanowy korzystając z podshella
Ctrl-Z - zawiesza proces pierwszoplanowy
Ctrl-Y - zawiesza proces pierwszoplanowy w chwili rozpoczęcia czytania
bg praca (lub praca &) - uruchamia zawieszony proces w tle
fg praca (lub praca ) - uruchamia zawieszony proces na pierwszym planie
kill -9 PID - powoduje zakończenie procesu (działającego lub zawieszonego)
Uwaga. Można uruchomić kilka procesów jednocześnie z jednej linii poleceń (pisząc na przykład
komenda_1 & komenda_2 & ).
Do uzyskiwania informacji o aktualnie wykonywanych procesach (numerach, właścicielach, stanach
procesów itp.) służy polecenie zewnętrzne ps. W zależności od podanych opcji może wyświetlać
jedynie informacje o aktywnych procesach lub o wszystkich (też zawieszonych), informacje jedynie
o procesach użytkownika wydającego polecenie, lub o wszystkich procesach w systemie (ta ostatnia
opcja zwykle nie jest udostępniana dla użytkowników nieuprzywilejowanych).
Uwaga
Użytkownik może badać stan swoich procesów związanych z bieżącą sesją przy użyciu interpretera
komend bieżącej sesji (w takim przypadku proces interpretera musi być pierwszoplanowy, a wszystkie
inne procesy drugoplanowe lub zawieszone), lub też otwierając nową sesję (i uruchamiając w ten
sposób nowy proces interpretera) i z niej sprawdzając stan procesów pierwszej sesji. Jest to możliwe,
bo użytkownik jest właścicielem wszystkich procesów zarówno pierwszej, jak i drugiej sesji.
Drugi sposób jest lepszy, jeśli z jakichś powodów nie jest wskazane czasowe przerywanie pracy
procesu pierwszoplanowego pierwszej sesji.
Pełny diagram stanów procesu (według [ M. Bach ] ):
Wykonywany w trybie użytkownika
wywołanie
funkcji systemowej, powrót do trybu
przerwanie przerwanie powrót użytkownika
i powrót wywłaszczenie
Wykonywany w trybie jądra
wyjście Wywłaszczony
Zombie ponowne
zasypia uszeregowanie
Gotowy do wykonania w pamięci
Uśpiony budzi się
w pamięci dość pamięci
usunięcie usunięcie sprowadzenie Utworzony
z pamięci z pamięci do pamięci fork
Uśpiony budzi się za mało pamięci
poza pamięcią Gotowy do wykonania poza pamięcią
Uwaga. Powyższy diagram nie uwzględnia mechanizmu stronicowania pamięci.
Jądro utrzymuje globalną strukturę nazywaną tablicą procesów, której zajęte pozycje odpowiadają
identyfikatorom procesów aktualnie istniejących w systemie (typowa wielkość tablicy procesów -
32768 pozycje). Każda zajęta pozycja zawiera między innymi:
- pole stanu procesu;
- identyfikator właściciela procesu (UID);
- wskaźnik do tablicy segmentów procesu;
- wskaźnik do u-obszaru (u-area, user area) procesu. Tablica segmentów
U-obszar
Tablica segmentów
procesu
Tablica procesów Pamięć operacyjna
Tablica procesów jest strukturą globalną, do której dostęp ma jedynie jądro systemu. Z każdym
wykonywanym procesem związany jest jego u-obszar, do którego dostęp ma zarówno jądro, jak
i sam proces (gdy wykonywany jest w trybie jądra). U-obszar zawiera między innymi:
- wskaźnik do odpowiadającej mu pozycji w tablicy procesów;
- deskryptory plików otwartych przez dany proces;
- nazwę bieżącego katalogu;
- argumenty aktualnego wywołania funkcji systemowej, zwracane wartości i kod błędu.
Z każdym wykonywanym procesem związana jest też tablica segmentów procesu, która zawiera
opisy segmentów przydzielonych procesowi (kodu, danych, stosu, pamięci wspólnej) wraz ze
wskaźnikami do odpowiednich pozycji w globalnej tablicy segmentów. Tablica segmentów,
utrzymywana przez jądro segmentów, zawiera z kolei deskryptory wszystkich istniejących segmentów
w pamięci operacyjnej. Taki dwustopniowy system adresowania segmentów w pamięci umożliwia
współdzielenie segmentów przez wiele procesów, przy jednoczesnym zachowaniu możliwości ich
ochrony (na przykład jeden proces może mieć prawo do zapisu w danym segmencie, a pozostałe tylko
do odczytu).
Każdy proces, poza procesem rozpoczynającym pracę systemu, powstaje w wyniku utworzenia przez
inny proces. Proces uruchamiający jądro systemu jest tworzony „bezpośrednio” i otrzymuje numer 0.
Jest on procesem wykonywanym w trybie jądra. Po uruchomieniu systemu przyjmuje on rolę procesu
wymiany. Tworzy on proces init o numerze 1, który jest procesem wykonywanym w trybie użytkow-
nika, i który zajmuje się dalej tworzeniem procesów potomnych. Init jest zatem przodkiem wszystkich
procesów w systemie poza procesem 0.
Każdy proces ma prawo tworzyć swoje procesy potomne (ich liczba jest ograniczona pojemnością
tablicy procesów oraz, ewentualnie, maksymalną liczbą procesów należących do jednego użytkownika).
W klasycznych systemach uniksowych proces potomny powstawał zawsze wskutek wywołania przez
proces rodzicielski funkcji systemowej fork (obecnie takich funkcji jest więcej, co jest między innymi
związane z implementacją wątków, czyli procesów lekkich). Utworzony proces potomny jest wpisywany
do tablicy procesów na kolejnej wolnej pozycji (modulo rozmiar tablicy), której numer staje się jego
identyfikatorem. Każdy proces „pamięta” identyfikator swojego procesu rodzicielskiego, natomiast
spośród identyfikatorów procesów potomnych pamięta jedynie ostatni (najpóżniej przydzielony).
Każdy nowo utworzony proces dziedziczy identyfikator grupy procesów (PGRP) po swoim procesie
rodzicielskim. Może w tej grupie pozostać lub wywołać funkcję systemową setpgrp, która powoduje
zmianę jego dotychczasowego PGRP na wartość jego identyfikatora (PID). Proces, który wywołał
setpgrp, staje się przywódcą grupy procesów.
Proces potomny dziedziczy po swoim procesie rodzicielskim większość jego atrybutów - UID, GID,
deskryptory przydzielonych zasobów systemowych (w szczególności deskryptory otwartych plików)
i zbiór zmiennych środowiska. Dziedziczy również segment kodu programu oraz otrzymuje kopie
segmentu danych i stosu. Ponieważ zazwyczaj celem utworzenia nowego procesu jest powierzenie
mu wykonywania innego programu, niż wykonuje jego proces rodzicielski, proces potomny powinien
wywołać funkcję systemową exec, której argumentem jest nazwa pliku z innym programem.
Proces potomny, kończąc działanie (wywołując funkcję systemową exit) przesyła informację o tym
do swojego procesu rodzicielskiego, przekazując mu jednocześnie swój kod zakończenia (exit code),
będący niedużą liczbą całkowitą (0 w przypadku traktowanym jako pomyślny). Jeśli proces rodziciel-
ski zakończył działanie przed zakończeniem wszystkich jego procesów potomnych, automatycznie
„przybranym rodzicem” dla niezakończonych procesów potomnych staje się proces init.
Priorytet procesu, decydujący o częstości wybierania go do wykonywania przez procesor, zależny jest
od kilku wielkości, między innymi od współczynnika nice (nazywanego też poziomem uprzejmości
procesu) umieszczonego w odpowiadającej danemu procesowi pozycji tablicy procesów.
Współczynnik ten może mieć wartość z zakresu 0 ... 39 , przy czym niższa wartość współczynnika
oznacza podwyższenie priorytetu.
Standardowa wartość nice wynosi 20 - wartość tę dziedziczą między innymi wszystkie shelle
zgłoszone pośrednio od procesu init (przez domniemanie każdy proces dziedziczy wartość nice po
swoim procesie rodzicielskim). Użytkownicy nieuprzywilejowani mogą uruchamiać procesy ze
współczynnikiem nice równym 20 lub więcej (do jego zwiększania służy polecenie systemowe nice).
Tylko nadzorca systemu może uruchamiać procesy z dowolnym współczynnikiem nice.
Uwaga
W dawniejszych systemach uniksowych wartość nice mogła być zmieniana jedynie przez proces,
którego ta wartość dotyczyła i nawet nadzorca systemu nie mógł wpływać na priorytety procesów
użytkowników. Obecnie możliwość taka istnieje (polecenie renice).
Procesy mogą korzystać z pomiaru czasu prowadzonego przez system i ustalać pory wykonywania
różnych czynności (na przykład tworzenia nowych procesów, wysyłania komunikatów itd.) poprzez
zapisywanie ich w tablicach crontab (związanych z poszczególnymi użytkownikami systemu).
Tablice te są sukcesywnie przeglądane przez proces demona zegarowego cron, który w podanych
porach inicjuje wykonywanie podanych komend.
Do operowania na zawartości tablicy crontab służy polecenie o takiej samej nazwie. Innymi
poleceniami związanymi z pomiarem czasu i wykonywaniem pewnych czynności w ustalonych
porach są polecenia at oraz leave.
Polecenia związane z pomiarem czasu ogólnie są uważane za polecenia niebezpieczne z punktu
widzenia systemu operacyjnego (nie są starannie opracowane pod kątem zabezpieczenia przed
włamaniami). W systemach uniksowych są zazwyczaj przechowywane listy użytkowników (ustalane
przez administratora), którzy albo a) mają prawo do wykonywania konkretnych poleceń związanych
z pomiarem czasu, albo b) mają zakaz wykonywania konkretnych poleceń.
14. KOMUNIKACJA MIĘDZYPROCESOWA W SYSTEMIE OPERACYJNYM UNIX
Poniższa seria slajdów jest w trakcie opracowywania.
W systemie Unix istnieje wiele środków komunikacji międzyprocesowej - zarówno przeznaczonych
do komunikacji między procesami na tym samym komputerze (zaprojektowanych glownie w AT&T),
jak i przeznaczonych do komunikacji w sieci komputerowej (gniazda BSD zaprojektowane
w Berkeley). Wszystkie te środki dostępne są w postaci struktur danych i obsługujących je funkcji
systemowych w interfejsie programisty w języku C. Najprostsze środki, takie jak przekazanie kodu
zakończenia procesu potomnego procesowi rodzicielskiemu, współdzielenie plików oraz łącza
(nazwane i nienazwane) dostępne są również poprzez polecenia powłoki (shella).
Proste mechanizmy koordynacji procesów w shellu:
1) polecenia exit i wait
exit liczba - kończy działanie procesu i przesyła jednobajtową liczbę (kod wyjścia)
do jego procesu rodzicielskiego
wait praca - powoduje zawieszenie procesu w oczekiwaniu na zakończenie jego procesu
potomnego i odbiera jego kod wyjścia
2) polecenia kill i trap
kill sygnał praca - powoduje wysłanie sygnału do procesu (z tej samej grupy)
trap komenda sygnał - powoduje przechwycenie przysłanego sygnału (jeśli to możliwe)
i wykonanie komendy
3) łącza nienazwane (bezimienne)
komenda1 | komenda2 | ... | komendan - potok
(może być utworzony przy założeniu, że komendy korzystają ze standardowego wejścia/wyjścia)
4) łącza nazwane (kolejki FIFO)
mkfifo nazwa (lub mknod nazwa p) - tworzy kolejkę FIFO
Zapis/odczyt oraz usunięcie odbywają się dla kolejki FIFO tak samo, jak dla zwykłego pliku
(mogą to robić wszystkie procesy, które mają odpowiednie prawa dostępu do kolejki). Musi
zachodzić synchronizacja operacji zapisu i odczytu.
Uwaga.
1) Łącza są realizowane jako bufory plików i mają ograniczoną pojemność. Mogą być widziane jako
„pliki nietrwałe” - odczyt porcji informacji z łącza usuwa ją jednocześnie z łącza.
2) Łącza nazwane są uwidocznione w systemie plików jako szczególny rodzaj plików (o wielkości 0).
Łącza nienazwane nie są uwidocznione (istnieją tylko w czasie wykonywania potoku).
Funkcje operujące na identyfikatorach.
Każdy proces poza procesem o numerze 0 powstaje wskutek utworzenia przez inny proces. Numery
procesów są liczbami naturalnymi przydzielanymi rosnąco modulo rozmiar tablicy procesów (zwykle
32 K) z pominięciem numerów aktualnie używanych. Każdy proces pamięta swój PID i PPID, ale
nie zapamiętuje w sposób automatyczny identyfikatorów tworzonych potomków (programista może
spowodować przechowywanie ich w zmiennych). Jeśli proces kończy działanie wcześniej, niż jego
(niektóre) procesy potomne, wszystkie procesy potomne otrzymują PPID=1 (jest to PID procesu Init)
i kontynuują działanie.
int getpid(void); - zwraca PID procesu
int getppid(void); - zwraca PPID procesu
int getpgrp(void); - zwraca PGRP procesu
int setpgrp(void); - odłącza proces od dotychczasowej grupy i ustanawia go przywódcą
nowej grupy (PGRP = PID)
Uwaga. Istnieją też odpowiednie funkcje dla identyfikatorów użytkowników i ich grup.
Funkcje związane z tworzeniem i kończeniem procesów.
Tworzenie nowego procesu:
int fork(void); zwraca -1 w przypadku niepowodzenia (na przykład brak zasobów)
zwraca 0 utworzonemu procesowi potomnemu
zwraca PID utworzonego potomka procesowi rodzicielskiemu
Wykonanie funkcji fork przez jądro systemu wiąże się z szeregiem skomplikowanych czynności
(przydział zasobów, wpisanie do tablicy procesów, kopiowanie środowiska itp.) i jest czasochłonne.
Segment instrukcji nie jest kopiowany, segment danych jest zwykle kopiowany dopiero w przypadku
próby dokonania zapisu przez nowy proces.
Zamiana kontekstu procesu:
Funkcja systemowa exec ma sześć interfejsów w języku C (różniących się sposobem przekazywania
parametrów i zmiennych środowiska). Jej zadaniem jest zamiana kontekstu procesu (przy zachowaniu
tożsamości procesu), to jest spowodowanie, żeby proces zaczął wykonywać inny program.
int execl (char *ścieżka, char *arg0, char *arg1, ... , char *argn, NULL);
ścieżka - pełna nazwa ścieżkowa pliku z nowym programem;
arg0 - powtórzona sama nazwa pliku z nowym programem;
arg1 ... argn - lista parametrów dla nowego programu zakończona znakiem pustym (NULL).
int execv (char *ścieżka, char *argv [ ] );
int execle (...);
int execve (...);
int execlp (...);
int execvp (...);
Funkcje fork i exec zazwyczaj współpracują ze sobą.
Kończenie wykonywania procesu:
void exit (int kod);
Kończy działanie procesu, wysyła sygnał do procesu rodzicielskiego oraz jednobajtowy kod wyjścia.
Oczekiwanie na zakończenie działania potomka:
int wait (int *wsk);
Zawiesza proces w oczekiwaniu na zakończenie któregokolwiek procesu potomnego. Zwraca PID
zakończonego potomka lub -1 w przypadku błędu. wsk zwraca dwa bajty:
- jeśli prawy bajt ma wartość 0, to lewy bajt zwraca kod wyjścia potomka;
- jeśli prawy bajt ma wartość niezerową, to określa, jaki sygnał spowodował zakończenie potomka,
oraz czy nastąpił zrzut pamięci do pliku core.
Uwaga. Obecnie istnieje też funkcja pozwalająca czekać na zakończenie określonego potomka.
Funkcje związane z operowaniem na sygnałach.
Wysłanie sygnału:
int kill (int pid, int sig);
Umożliwia wysłanie określonego sygnału do określonego procesu / grupy procesów.
Przechwycenie sygnału:
void (*signal (int sig, void (*func) (int))) (int);
Umożliwia przechwycenie określonego sygnału (jeśli to możliwe) i wykonanie wskazanej funkcji
obsługi.
Polecenia shella kill i trap są obudowami funkcji systemowych kill i signal.
Funkcje związane z operowaniem na łączach nienazwanych.
Pierwotnie łącza nienazwane mogły być używane jedynie jako jednokierunkowe:
P Q
zapis odczyt
kolejka prosta
Funkcja tworząca łącze:
int pipe (int fd [2] );
fd [0] - deskryptor pliku służący do odczytu z łącza
fd [1] - deskryptor pliku służący do zapisu do łącza
Do zapisów / odczytów stosujemy funkcje systemowe write i read (są wykonywane niepodzielnie).
Łącze ma pojemność zależną od ustawień systemowych (co najmniej pół KB, zazwyczaj 4 KB).
Zazwyczaj bezpośrednio po wywołaniu funkcji pipe wywoływana jest funkcja fork (proces potomny
dziedziczy deskryptory plików), a następnie, w zależności od zamierzonego kierunku przesyłania,
zamykane są niepotrzebne deskryptory (po jednym w każdym procesie).
...
pipe (fd);
if (fork ( ) = = 0) fd [1] fd [1]
{
close (fd [0] );
...
} fd [0] łącze fd [0]
else
{ proces proces
close (fd [1] ); potomny rodzicielski
...
}
Główną wadą łącz nienazwanych jest to, że mogą łączyć tylko procesy spokrewnione (zazwyczaj
pary rodzic - potomek, ale mogą też być dziadek - wnuk, dwóch potomków itp.).
W nowszych wersjach Unixa łącza są implementowane jako dwukierunkowe (full duplex).
W starszych mogły być tylko jednokierunkowe (half-duplex) - chcąc uzyskać łączność
dwukierunkową należało skorzystać z dwóch par deskryptorów i dwukrotnie wywołać funkcję pipe.
Uwaga.
1) W przypadku próby odczytu z pustego łącza lub próby zapisu do pełnego łącza procesy są czasowo
zawieszane.
2) W przypadku łącz dwukierunkowych może być potrzebna synchronizacja operacji zapisu i odczytu
po obu stronach łącza (na przykład za pomocą semaforów).
3) Na zakończenie działania programu należy pozamykać wszystkie otwarte deskryptory.
Funkcje związane z operowaniem na łączach nazwanych (FIFO).
Łącza nazwane są uwidoczniane w systemie plików jako specjalny rodzaj plików o zerowym
rozmiarze. Mogą być tworzone i usuwane zarówno w programach, jak i przy użyciu komend shella.
Z łączami nazwanymi mogą współpracować dowolne procesy (niekoniecznie spokrewnione), które
posiadają odpowiednie prawa dostępu.
Funkcja tworząca kolejkę FIFO:
int mknod (const char *ścieżka, int tryb);
ścieżka - pełna nazwa ścieżkowa kolejki FIFO
tryb - słowo trybu, którego bity informują między innymi o prawach dostępu do kolejki
Przed użyciem łącze nazwane musi być otwarte (open), a przed zakończeniem wykonywania programu
zamknięte (close) przez każdy proces współpracujący z łączem. Jest wymuszona synchronizacja
otwarcia łącza do zapisu i otwarcia łącza do odczytu przez dwa procesy chcące korzystać z łącza.
Samo korzystanie z łącza wygląda podobnie, jak w przypadku łącz nienazwanych (funkcje write i read).
11. PAKIET IPC
Narzędzia z pakietu IPC (InterProcess Communication) służą do koordynacji procesów wykonywa-
nych na jednym komputerze (nie są przeznaczone do komunikacji sieciowej). W skład tego pakietu
wchodzą biblioteki funkcji obsługujących kolejki komunikatów (message queue), pamięć dzieloną
(shared memory) i semafory (semaphore). Z wszystkimi trzema rodzajami obiektów związane są
odpowiednie struktury danych tworzone przez jądro systemu, do których dostęp jest możliwy jedynie
poprzez wywoływanie przeznaczonych do tego funkcji systemowych.
Zestawienie głównych funkcji pakietu IPC (wg W.R. Stevensa):
Kolejki komun. Semafory Pamięć dziel.
Plik nagłówkowy < sys/msg.h > < sys/sem.h> < sys/shm.h>
Funkcja systemowa tworzenia lub otwierania msgget semget shmget
Funkcja systemowa operacji sterujących msgctl semctl shmctl
Funkcje systemowe przesyłania msgsnd semop shmat
msgrcv shmdt
Ze względu na to, że struktury kontrolne obiektów pakietu IPC są przechowywane w jądrze systemu
i mogą być widoczne dla wszystkich procesów, muszą mieć klucze unikalne w obrębie całego systemu.
Zalecane jest stosowanie funkcji ftok generującej unikalne klucze na podstawie ścieżek dostępu do
plików z programami wykonywanymi przez procesy tworzące obiekty pakietu IPC. Dopuszczalny
zakres kluczy zależy od ustawień systemowych - odpowiada mu typ key_t zdefiniowany w nagłówku
< sys/types.h >.
Kolejki komunikatów
Kolejki komunikatów nie są kolejkami prostymi (komunikaty mogą być z nich wybierane w innej
kolejności, niż zostały umieszczone). Komunikaty posiadają pewną strukturę (nie są tylko ciągami
bajtów, jak w przypadku łącz):
struct msgbuf { long mtype ;
char mtext[1] ; }
Uwaga. Typ char zawartości komunikatu jest zdefiniowany tylko pro forma - może być rzutowany.
int msgget (key_t klucz, int flagi);
Zwraca: identyfikator kolejki w przypadku sukcesu ;
-1 w przypadku błędu.
Flagi określają prawa dostępu, oraz czy ma być zwrócony błąd, jeśli kolejka o danym kluczu już istnieje.
Działanie: tworzy kolejkę o podanym kluczu, jeśli taka kolejka jeszcze nie istnieje.
int msgsnd (int ident, struct msgbuf *kom, int rozmiar, int flagi) ;
Zwraca: 0 w przypadku sukcesu ;
-1 w przypadku błędu.
ident - identyfikator kolejki (zwrócony przez msgget)
kom - wskaźnik do struktury przechowującej typ komunikatu i sam komunikat (bufora komunikatu)
rozmiar - rozmiar komunikatu „netto” (nie licząc typu)
flagi - 0 lub IPC_NOWAIT (decydują, czy w sytuacji przepełnienia kolejki proces ma być zawieszony)
Działanie: wstawia komunikat wraz z podanym typem na koniec kolejki.
int msgrcv (int ident; struct msgbuf *kom, int rozmiar, long mtype, int flagi);
Zwraca: liczbę faktycznie pobranych bajtów z kolejki w przypadku sukcesu ;
-1 w przypadku błędu.
ident - identyfikator kolejki
kom - wskaźnik do bufora komunikatu
rozmiar - rozmiar struktury komunikatu (nie licząc typu)
mtype - typ komunikatu, jaki chcemy pobrać z kolejki (może być 0)
flagi - można ustawić IPC_NOWAIT i / lub MSG_NOERROR (powoduje odpowiednie zachowanie,
jeśli komunikat jest większy, niż przewiduje rozmiar)
Działanie: pobiera z kolejki najdawniej wstawiony komunikat o danym typie (jeśli istnieje), zaś
jeśli został podany typ 0, pobiera najdawniej wstawiony komunikat (o dowolnym typie).
int msgctl (int ident, int polecenie, struct msgqid_ds *struktura);
Zwraca: 0 w przypadku sukcesu ;
-1 w przypadku błędu.
ident - identyfikator kolejki
polecenie - kod czynności do wykonania na strukturze kontrolnej kolejki
struktura - wskaźnik do bufora struktury kontrolnej
Działanie: może wykonywać mnóstwo różnych czynności (w tym również takich, które mogą
pozbawić programistę kontroli nad kolejką) - zmieniać prawa dostępu, odczytywać
informacje o ostatnio wykonanej operacji na kolejce itp. Najczęściej jest wykorzystywana
do usunięcia kolejki:
msgctl (ident, IPC_RMID, 0);
Kolejki komunikatów są narzędziem bardziej skomplikowanym w użyciu, a jednocześnie oferującym
bogatsze możliwości, niż kolejki FIFO. W typowym zastosowaniu - implementacji par programów
typu klient / serwer - stosując kolejki FIFO musimy otworzyć oddzielną kolejkę dla serwera i oddzielne
dla wszystkich klientów (gdyż nie ma możliwości testowania danych umieszczonych w jednej kolejce
dla wielu odbiorców).
Dane od klientów powinny na początku zawierać informację o swojej długości oraz adres zwrotny
(to jest adres kolejki odbiorczej klienta).
W przypadku stosowania kolejek komunikatów wystarczają dwie takie kolejki (związane z serwerem),
gdyż umożliwiają demultipleksowanie odpowiedzi serwera do różnych klientów.
Klienci muszą mieć unikalne adresy kodowane jako typy komunikatów. Odpowiedzi serwera są
opatrywane tymi samymi typami, jakie miały przysyłane przez klientów pytania - klienci mogą je
selektywnie wybierać z kolejki zwrotnej.
Uwaga. Jest również możliwe rozwiązanie przy użyciu tylko jednej kolejki (dla pytań i odpowiedzi).
Pamięć dzielona
Poza własnym segmentem danych przydzielonym w momencie utworzenia, proces może mieć
przydzielony dynamicznie (w trakcie wykonywania) jeden lub więcej segmentów pamięci z ogólnych
zasobów systemowych. Takie segmenty są dołączane do przestrzeni adresowej procesu i można na
nich operować bezpośrednio (na przykład wykonując operacje przypisania). Jeśli programista ustanowi
odpowiednie prawa dostępu, segmenty takie mogą być niezależnie przydzielane wielu procesom
jednocześnie. Komunikowanie się przez pamięć dzieloną jest zdecydowanie najszybszym sposobem
komunikowania się procesów (choć najtrudniejszym do synchronizacji).
Uwaga.
1) Podobnie jak w przypadku plików i dowiązań do nich, segment pamięci dzielonej jest zwracany do
puli wolnych zasobów systemowych dopiero wtedy, gdy ostatni z użytkujących go procesów
zrzeknie się jego dalszego używania (odłączy go od swojej przestrzeni adresowej).
2) Nie ma możliwości operowania w segmencie pamięci dzielonej inaczej, niż za pomocą zmiennych
dynamicznych (wskaźników). W gruncie rzeczy segmenty pamięci dzielonej są widziane przez
programy jako dodatkowe (współdzielone) sterty.
3) Jeden i ten sam segment pamięci dzielonej może być dołączony do przestrzeni adresowej procesu
w wielu różnych miejscach. W ten sposób możemy dysponować wieloma kopiami jednej i tej
samej zmiennej, zmiana wartości jednej kopii jest natychmiast widoczna w pozostałych miejscach.
4) Proces potomny dziedziczy utworzony (i przyłączony) segment pamięci dzielonej.
int shmget (key_t klucz, int rozmiar, int flagi);
Zwraca: identyfikator segmentu w przypadku sukcesu;
-1 w przypadku błędu.
klucz, flagi - pełnią podobną rolę, jak dla kolejek komunikatów
rozmiar - rozmiar tworzonego segmentu w bajtach (argument nieistotny, jeśli segment już istnieje)
Działanie: tworzy nową pozycję w tablicy segmentów, jeśli segment wcześniej nie istniał.
int shmat (int ident, char *adres, int flagi);
Zwraca: wskaźnik do miejsca, gdzie rzeczywiście został dołączony segment, w przypadku sukcesu;
-1 w przypadku błędu.
ident - identyfikator segmentu (zwrócony przez funkcję shmget)
adres - wskaźnik do miejsca, gdzie programista proponuje dołączyć segment (może być 0)
flagi - rozmaite role ( na przykład mogą nakazać dołączenie segmentu tylko do odczytu)
Działanie: dołącza segment (i zwiększa jego licznik dowiązań o 1) pod podanym adresem (w miarę
możności) jeśli adres jest większy od 0, zaś pod adresem wybranym przez system, jeśli
podany adres jest równy 0 (najczęściej stosowane i najbardziej zalecane postępowanie).
int shmdt (char *adres);
Zwraca: 0 w przypadku sukcesu;
-1 w przypadku błędu.
adres - adres, pod którym był dołączony (przez funkcję shmat) segment pamięci dzielonej
Działanie: segment jest odłączany od przestrzeni adresowej procesu, a jego licznik dowiązań zmniej-
szany o 1. Jeżeli stan licznika dowiązań zmniejszył się w wyniku tego do 0, a segment był
oznaczony do usunięcia, w tym momencie następuje jego usunięcie z tablicy segmentów.
int shmctl (int ident, int polecenie, struct shmid_ds *struktura);
Zwraca: 0 w przypadku sukcesu;
-1 w przypadku błędu.
ident - identyfikator segmentu (zwrócony przez funkcję shmget)
polecenie - kod polecenia do wykonania na strukturze zarządzającej segmentem
struktura - wskaźnik do bufora struktury zarządzającej segmentem
Działanie: podobnie, jak w przypadku kolejek komunikatów, może wykonywać wiele różnych
czynności, a najczęsciej wykonywaną jest oznaczenie segmentu do usunięcia:
shmctl (ident, IPC_RMID, 0);
Uwaga. Zalecane jest odłączenie segmentu (wykonanie funkcji shmdt) przed oznaczeniem segmentu
do usunięcia.
Semafory
Semafory są uważane za najbardziej skomplikowane w użyciu obiekty pakietu IPC. Ich najbardziej
typowym zastosowaniem jest synchronizacja dostępu różnych procesów do zmiennych w pamięci
dzielonej. Semafory zaimplementowane w pakiecie IPC są podobne do semaforów Agerwali.
Występują nie jako pojedyncze obiekty, ale jako elementy tablic semaforów, na których można
wykonywać jednocześnie (niepodzielne) operacje. Maksymalny rozmiar tablicy semaforów zależy od
ustawień systemowych. Zakres wartości przyjmowanych przez pojedynczy semafor jest zakresem
wartości typu ushort (czyli od 0 do 255).
int semget (key_t klucz, int liczbasem, int flagi);
Zwraca: identyfikator tablicy semaforów w przypadku sukcesu;
-1 w przypadku błędu.
klucz, flagi - jak dla kolejek komunikatów i pamięci dzielonej
liczbasem - liczba semaforów w tablicy (argument nieistotny, jeśli tablica już istnieje)
Działanie: tworzy nową tablicę semaforów, jeśli wcześniej nie istniała.
int semop (int ident, struct sembuf *oper, unsigned liczbaoper);
gdzie struct sembuf
{ ushort sem_num; numer semafora w tablicy
short sem_op; operacja na semaforze
short sem_flg; } flagi operacji
Zwraca: 0 w przypadku sukcesu;
-1 w przypadku błędu.
ident - identyfikator tablicy semaforów (zwrócony przez funkcję semget)
oper - wskaźnik do początku tablicy operacji (tablicy struktur sembuf)
liczbaoper - liczba elementów w tablicy wskazywanej przez oper
Działanie: system wykonuje niepodzielnie wszystkie operacje nakazane w tablicy struktur wskazywanej
przez oper - albo nie wykonuje żadnej, jeśli choć jedna z nich jest w danej chwili niemożliwa.
Pojedyncza operacja na pojedynczym semaforze wygląda następująco:
- jeżeli wartość sem_op jest dodatnia, wartość semafora zwiększa się o nią (zatem, w przeciwieństwie
do tego, co jest podane w klasycznej definicji semafora, wartość semafora może wzrosnąć o więcej,
niż 1), a jednocześnie jest budzona odpowiednia liczba procesów śpiących pod tym semaforem (jeśli
są takie);
- jeżeli wartość sem_op jest ujemna, wartość semafora odpowiednio się zmniejsza, jeśli to możliwe,
a jeśli niemożliwe, zmniejszenie nie jest wykonywane, a proces albo zasypia czekając na zaistnienie
takiej możliwości, albo od razu następuje powrót z funkcji z błędem (w zależności od ustawienia
flagi IPC_NOWAIT);
- jeżeli wartość sem_op wynosi zero, proces zasypia, jeśli wartość semafora nie jest zerem (lub wraca
od razu z błędem, jeśli jest ustawiona flaga IPC_NOWAIT) i budzi się dopiero po osiągnięciu
wartości zero przez semafor.
int semctl (int ident, int numer, int polecenie, union semun argument);
gdzie union semun
{ int val; do ustawienia wartości pojedynczego semafora
struct semid_ds *buf; bufor struktury zarządzającej tablicą semaforów
ushort *array; wskaźnik do tablicy ustawień wartości całej tablicy sem.
struct seminfo *__buf; specyficzne dla Linuxa, używane przez
void *__pad; } jądro systemu operacyjnego
Zwraca: liczbę dodatnią będącą wynikiem wykonania polecenia - w przypadku sukcesu
-1 w przypadku błędu
ident - identyfikator tablicy semaforów (zwrócony przez funkcję semget)
numer - numer semafora w tablicy (istotny w przypadku, gdy polecenie dotyczy pojedynczego semafora)
polecenie - kod polecenia do wykonania
argument - argument jednego z typów wchodzących w skład unii, zależnego od polecenia
Działanie: może wykonywać mnóstwo różnych czynności (najwięcej z wszystkich funkcji IPC) na
pojedynczych semaforach, na całej ich tablicy lub na strukturze zarządzającej. Najczęściej
używanymi poleceniami są:
IPC_RMID usunięcie tablicy semaforów
GETALL odczytanie wartości wszystkich semaforów w tablicy
SETALL nadanie wartości wszystkim semaforom w tablicy
GETVAL odczytanie wartości pojedynczego semafora
SETVAL nadanie wartości pojedynczemu semaforowi
Uwaga. Operacje nadania lub odczytania wartości semaforów nie wiążą się z możliwością wstrzymania
procesu wykonującego daną operację.