Coś z serii „ja już zaliczyłem na 5.0, a może komuś
się przyda”, czyli przygotowany przeze mnie plik
z odpowiedziami na pytania przygotowawcze do egzaminu
z przedmiotu Systemy operacyjne. Zapewne największa wartość
mają one dla studentów Elektroniki Politechniki Warszawskiej,
niemniej jednak może ktoś się tym zainteresuje.
Definicje podstawowe i obowiązkowe
- System operacyjny
-
Zbiór programów i procedur spełniających dwie podstawowe
funkcje:
- zarządzanie zasobami systemu komputerowego,
- tworzenie maszyny wirtualnej.
Zasobami systemu komputerowego są:
- zasoby sprzętowe:
- czas procesora,
- pamięć operacyjna,
- urządzenia zewnętrzne,
- inne komputery powiązane przez sieć itp;
- zasoby programowe:
- pliki,
- bufory,
- semafory,
- tablice systemowe itp.
Zarządzanie zasobem polega na:
- śledzeniu,
- przydzielaniu,
- odzyskiwaniu.
- Proces
- Wykonujący się program wraz z jego środowiskiem
obliczeniowym.
- Powłoka
- Interpreter poleceń uruchamiany standardowo po otwarciu sesji
użytkownika.
- Sekcja krytyczna
- Fragment programu, w którym występują instrukcje dostępu do
zasobów dzielonych. Instrukcje tworzące sekcję krytyczną muszą
być poprzedzone i zakończone operacjami realizującymi
wzajemne wykluczanie.
- Semafor
- Obiekt inicjowany nieujemną liczbą całkowitą, na której
zdefiniowane są dwie niepodzielne operacje up(sem) { ++sem;
} i down(sem) { while (!sem); --sem; }.
- Pamięć wirtualna
- System pamięci złożony z co najmniej dwóch rodzajów
pamięci: małej i szybkiej (np. pamięci operacyjnej) oraz
dużej, lecz wolnej (np. pamięci pomocniczej), a także
z dodatkowego sprzętu i oprogramowania umożliwiającego
automatyczne przenoszenie fragmentów pamięci z jednego rodzaju
pamięci do drugiego.
- To co jest na slajdach to jakiś bezsens. A jak mam
bezdyskowy router postawiony na Linuksie to co, nie ma tam pamięci
wirtualnej? Tak naprawdę w pamięci wirtualnej chodzi
o to, iż proces operuje na liniowej przestrzeni wirtualnej
przestrzeni adresowej, która nie musi odpowiadać liniowej
przestrzeni w pamięci fizycznej. Dodatkowo, jakieś obszary
pamięci mogą być niedostępne w pamięci fizycznej, a na
dysku czy innym pomocniczym medium (słyszałem, że pod Linuksem
udało się korzystać z pamięci karty graficznej jako
swapu).
Programowanie w języku powłoki
- Prawa dostępu do plików, bity SUID, SGID.
- rwxrwxrwx–po trzy bity na właściciela, grupę
i „innych” (w tej kolejności patrząc od
najbardziej znaczących bitów). Bity dla właściciela określają
jakie prawa ma właściciel pliku; bity dla grupy określają jakie
prawa mają użytkownicy wchodzący w skład grupy, do której należy
grupa, za wyjątkiem właściciela; bity dla "innych" określają jakie
prawa mają pozostali użytkownicy.
- Patrząc od bitów najbardziej znaczących rzeczone bity określają:
- r–prawo do odczytu (w kontekście katalogu
odczytu listy plików),
- w–prawo do zapisu (w kontekście katalogu
do tworzenia lub usuwania plików) i
- x–prawo do wykonywania
(a w kontekście katalogu do zmiany katalogu
bieżącego na dany katalog).
Aby móc dobrać się do jakiegokolwiek pliku lub katalogu trzeba
mieć prawo wykonywania do katalogu, w którym ten plik/katalog
się znajduje (i oczywiście działa to rekurencyjnie).
- SUID i SGID określane są dla plików wykonywalnych
i jeżeli są ustawione to w momencie uruchamiania programu
odpowiednio UID lub GID użytkownika ustawiany jest na taki jaki
jest odpowiednio właściciel lub grupa pliku. Uwaga! Jeżeli bity
te zostaną ustawione do pliku ze skryptem skrypt nie zostanie
uruchomiony z prawami właściciela. Wynika to z faktu,
że tak naprawdę to nie skrypt jest uruchamiany,
a interpreter. Na wyjściu polecenia ls bity SUID
i SGID widoczne są jako s zamiast
x w prawach użytkownika bądź grupy.
- Jest jeszcze sticky bit (polecenie ls wyświetla to jako
t zamiast x w uprawnieniach dla
„innych”), który dla katalogu określa, że pliki
w jego wnętrzu mogą być kasowane lub może być im zmieniana
nazwa jedynie przez właściciela danego pliku, katalogu lub super
użytkownika. Przydatne w katalogach typu /tmp.
- Główne zmienne powłoki.
-
- 0–komenda uruchamiająca skrypt.
- #–liczba argumentów programu lub funkcji.
- 1..9–argumenty skryptu lub funkcji.
- *–wszystkie argumenty skryptu/funkcji
rozwinięte do pojedynczego ciągu znaków.
- @–wszystkie argumenty skryptu/funkcji
rozwinięte do wielu ciągów znaków. Uwaga na użycie
cudzysłowów! Bez nich, $@ to to samo
co $*.
- $–PID procesu.
- ?–kod wyjścia ostatnio zakończonego polecenia.
- !–PID ostatnio wykonanego procesu w tle.
- IFS–internal fields separator; określa jakie
znaki mogą separować słowa.
- HOME–katalog domowy.
- SHELL–polecenie wywołujące powłokę.
- PATH–lista katalogów oddzielonych
dwukropkiem, w których wyszukuje się pliki wykonywalne.
- PS1–znak zachęty powłoki, gdy wpisujemy
komendy.
- PS2–znak zachęty, gdy kontynuujemy
komendę.
- TERM–rodzaj terminala.
- MAIL–skrzynka pocztowa, nie musi być wcale
ustawione.
- Polecenie powłoki read.
- Czyta linię ze standardowego wejścia dzieli ją
na n słów zgodnie z wartością
zmiennej IFS, gdzie n to liczba
argumentów i zapisuje te słowa do zmiennych podanych jako
argumenty. Ostatnie słowo może de facto nie być pojedynczym
słowem.
- Polecenie powłoki set.
- Ustawia argumenty wywołania programu/funkcji, do których
odwołujemy się przy pomocy $<cyferka>. Dzięki temu
poleceniu można robić zmienne lokalne w funkcjach, ale nie
tylko. W ogóle jest to fajowe polecenie. :P Ponadto, można
ustawiać różne opcje jeżeli argumenty zaczynają się od myślnika.
Jeżeli nie chcemy ustawiać opcji należy jeszcze dodać --
coby polecenie nie próbowało interpretować tego co podajemy jako
argumenty.
- Polecenie powłoki export.
- Powoduje, że zmienna/e którą/e podamy jako argument/y zostanie
„wyeksportowana” i będzie dostępna
w wywoływanych poleceniach jako zmienna środowiskowa.
Zmienne, które były zmiennymi środowiskowymi w momencie
włączenia skryptu nadal takimi pozostają (przykładami są HOME,
PATH itp.).
- Pętle for/while/do
w programowaniu powłoki.
- for <zmienna> in <lista-słów>; do
<polecenie>; done wykonuje to co jest wewnątrz dla
każdego słowa podanego w liście słów tak, że przy każdym
przebiegu pętli podana zmienna ma wartość kolejnego słowa.
- for <zmienna>; do <polecenie>; done tak jak
powyżej, gdy za listę słów poda się "$@".
- while <warunek>; do <polecenie>; done
wykonuje polecenie tak długo jak warunek zwraca kod błędu 0.
- while :; do <polecenie>; done taka miła pętla
nieskończona.
- Metaznaki powłoki.
- Zapewne chodzi o znaki *, ?, [
oraz \. Mają one znaczenie przy interpretowaniu poleceń,
gdyż są rozwijane w nazwę pliku lub nazwy plików. Gwiazdka
pasuje do dowolnego ciągu znaków (łącznie z pustym), znak
zapytania pasuje do dokładnie jednego znaku (ani gwiazdka ani znak
zapytania nie powodują rozwinięcia nazw zaczynających się od
kroki), otwierający nawias kwadratowy rozpoczyna grupę, która
pasuje do jednego z podanych w niej znaków
(np. [a-z], [abc], [a-z123]) lub pasuje
do znaków, w niej nie podanych jeżeli opis grupy zaczyna się
od wykrzyknika (tak, wykrzyknika, to że Twój bash akceptuje
znak ^ nie ma tu nic do rzeczy)
(np. [!a-z], [!abc], [!a-z123]).
Backslash powoduje, że dany znak traci swoje
„magiczne” znaczenie.
- Odczyt pliku /etc/passwd
z wykorzystaniem read/set/IFS.
while IFS=: read nick:pass:uid:gid:info:home:shell; do
echo "User $nick ($uid/$gid); home directory: $home; shell: $shell"
done </etc/passwd
- Wykorzystanie polecenia expr do operacji arytmetycznych
na zmiennych.
- i=`expr $i + 1`, i=`expr $i - 1`,
i=`expr $a \* $b`.
- Opisz rezultat wykonania poleceń i grup poleceń:
- x >/dev/null 2>&1–stdout i stderr
przekierowane do tego samego /dev/null, tzn. jest jeden
zduplikowany deskryptor pliku.
- x 2>&1 1>/dev/null–stderr przekierowane
do stdout, a potem stdout przekierowane
do /dev/null.
- x 2>/dev/nul 1>/dev/null–stderr
przekierowane do pliku nul w katalogu dev,
a stdout do /dev/null.
- x < y > z–stdin czytane
z pliku y, stdout pisane do pliku z.
- x & y ; z–x zostanie odpalone
w tle i zaraz po tym y. z zostanie uruchomione,
gdy działanie zakończy y.
- x || y–y wykonane
jeżeli x zwróci status różny od 0.
- find / foo.x 2>&1 1>/dev/null–przeszuka
cały dysk, a następnie jeszcze katalog/plik foo.x
w bieżącym katalogu. Komunikaty błędów wypisane do stdout
a wszystko inne wyrzucone do /dev/null.
- find / foo.x >/dev/null 2>&1–przeszuka
cały dysk, a następnie jeszcze katalog/plik foo.x
w bieżącym katalogu. Standardowe wyjście oraz komunikaty
błędów rzucone do /dev/null.
- Opisz precyzyjnie działanie poniższych komend:
- x > y–tworzony bądź zeruje jest
plik y do którego przekierowane jest standardowe wyjście
polecenia x.
- x | y–tworzony jest potok, do którego pisze
polecenie x, a czyta polecenie y.
Tzn. standardowe wyjście x jest przekierowane do potoku,
który jest przekierowany jako standardowe wejście polecenia
y.
- x < y–otwierany jest plik y
i jego zawartość jest wczytywana jako standardowe wejście
polecenia x.
- cat y | x–efekt taki sam jak powyżej, tyle że
zrealizowane to jest przez potok opisany dwa punkty wyżej.
- x & y–x uruchamiany jest w tle
i zaraz po uruchomieniu nim (nie musi się on kończyć)
uruchamiany jest y.
- x && y–uruchamiany jest x
i jeżeli zwraca status 0 to uruchamiany jest y.
- x || y–jak wyżej, tylko y uruchamiany
jest, gdy x zwraca status różny od 0.
- x ; y &–uruchamiany jest x,
a gdy się skończy uruchamiany jest y w tle.
- cat x > y <–błąd składni? Bez
tego < na końcu to polecenie cat wczytuje
zawartość pliku x i wypisuje je na standardowe
wyjście, które przekierowane jest do utworzonego lub zerowanego
pliku y.
Procesy i wątki:
- Współbieżność
- procesy nie muszą wykonywać się jeden po drugim.
- Równoległość
- procesy wykonują się jednocześnie.
- Rozproszoność
- procesy wykonują się no różnych maszynach.
- Porównanie procesów i wątków.
- Procesy mają osobną przestrzeń adresową, wątki nie. Procesy są
generalnie cięższe od wątków i przełączanie pomiędzy
procesami trwa dłużej niż pomiędzy wątkami.
- Porównanie wątków poziomu jądra i wątków poziomu
użytkownika.
- Jądro nie wie o istnieniu wątków poziomu użytkownika toteż,
na maszynie 1024 procesorowej i tak będzie wykonywał się
jeden wątek użytkownika, tymczasem wątki poziomu jądra mogą być
szeregowane przez jądro i wówczas wiele wątków tego samego
procesu może wykonywać się jednocześnie na kilku procesorach.
- Architektura wielowątkowa w systemie Solaris.
- Procesy.
- Wątki użytkownika–implementowane bibliotecznie
nierozróżnialne przez jądro.
- LWP (procesy lekkie)–rozróżniane przez jądro mogą
obsługiwać dowolną liczbę wątków poziomu użytkownika.
- Wątki jądra--podstawowy elementy szeregowanie rozmieszczane na
procesorach.
Wzajemne wykluczanie i synchronizacja.
- Wyścig i warunki wyścigu.
- sytuacja, w której co najmniej dwa procesy wykonują
operację na zasobach dzielonych, a ostateczny wynik zależy od
momentu realizacji.
- Warunki konieczne implementacji sekcji krytycznej.
-
- wewnątrz jeden proces,
- proces poza SK nie może blokować innego procesu pragnącego wejść do
SK,
- każdy proces oczekujący na wejście powinien się
doczekać.
- Idea wykorzystania instrukcji Test&SetLock (TSL) do realizacji
semafora binarnego.
register r = 1;
do {
TSL(r, semaphore)
} while (r);
- Albo jak ktoś woli pseudo-assembler:
mov reg, 1
loop_here: tsl reg, [semaphore]
jne reg, 0, loop_here
- Albo jak ktoś woli ix86 (w notacji AT&T):
mov eax, 1
loop_here: xchg eax, [semaphore]
or eax, eax
jnz loop_here
- Algorytm i opis idei poprawnego rozwiązania problemu pięciu
filozofów.
#define N 5
enum State { THINKING, HUNGRY, EATING } states[N] = { THINKING /* ... * };
semaphore sem[N] = { 0 /* ... * };
semaphore mutex = 1;
#define LEFT(n) (((n) + N - 1) % N)
#define RIGHT(n) (((n) + 1) % N)
void philosopher(int n);
void take_forks(int n);
void put_forks(int n);
void test(int n);
void philosopher(int n) {
for(;;){
think();
take_forks(n);
eat();
put_forks(n);
}
}
void take_forks(int n) {
down(mutex);
state[n] = HUNGRY; /* MUST BE BEFORE TEST BELOW */
test(n);
up(mutex);
down(sem[n]);
}
void put_forks(int n) {
down(mutex);
state[n] = THINKING; /* MUST BE BEFORE TESTS BELOW */
test(LEFT(n));
test(RIGHT(n));
up(mutex);
}
void test(int n) {
if (state[n] == HUNGRY &&
state[LEFT(n)] != EATING &&
state[RIGHT(n)] != EATING) {
state[n] = EATING;
up(sem[n]);
}
}
- Idea jest taka, że filozof może być w jednym z trzech
stanów: Myślący, Głodny lub Jedzący. I teraz
w momencie, gdy filozof staje się głodny to zmienia swój stan
na odpowiedni i sprawdza, czy jego sąsiedzi nie jedzą (jeżeli
nie to znaczy, że oba widelce ma wolne) i wówczas zmienia
swój stan na Jedzący i podnosi sam dla siebie semafor.
Semafor ten następnie opuszcza co powoduje albo natychmiastowe
przejście dalej (bo sam sobie go podniósł) albo czekanie, aż obaj
sąsiedzi nie będą jeść. Teraz, gdy filozof przestaje jeść to
zmienia swój stan na Myślący i sprawdza czy przez przypadek
jeden z jego sąsiadów nie może zacząć jeść i jeżeli może
to zmienia jego stan na Jedzący i podnosi dla niego semafor
(na którym zapewne ten filozof sobie smacznie śpi).
Przykładowe zadania projektowe
- Skrypt, który przyjmuje jako argument id użytkownika
i wypisuje na stdin nazwę użytkownika,
#! /bin/sh
while IFS=: read nick pass uid other; do
if [ X"$1" = X"$uid" ]; then
echo "$nick"
exit 0
fi
done
exit 1
- Skrypt zamieniający w nazwach wszystkich plików z bieżącego
katalogu litery z dużych na małe.
#! /bin/sh
for i in *; do
n=`printf '%s\n' "$i" | tr '[A-Z]' '[a-z]'`
if [ X"$i" != X"$n" ]; then
mv -f -- "$i" "$n"
fi
done
- Skrypt wypisujący przyjęte argumenty w odwrotnej
kolejności.
#! /bin/sh
OUTPUT=$1
while [ $# -gt 1 ]; do
shift
OUTPUT="$1 $OUTPUT"
done
printf '%s\n' "$OUTPUT"
- Algorytm czytelników i pisarzy preferujący
pisarzy–podać algorytm, opisać synchronizację, wyjaśnić
przeznaczenie poszczególnych zmiennych.
monitor {
unsigned rc = 0, wc = 0;
bool occupied_by_writer = false;
cond rc_zero, wc_zero, available;
reader_enter() {
while (wc) wait(wc_zero);
++rc;
}
reader_exit() { if (!--rc) notify(rc_zero); }
writer_enter() {
++wc;
while (rc) wait(rc_zero);
while (occupied_by_writer) wait(available);
occupied_by_writer = true;
}
writer_exit() {
occupied_by_writer = false;
if (!--wc) notify(wc_zero);
notigy(available);
}
};
- Implementacja monitora oraz funkcji wait
i notify jest oczywista, konkretnie dla monitora
dajemy jeden semafor mutex inicjowany jedynką i przy wejściu
do funkcji robimy na nim down, a przy
wyjściu up. Zmienne warunkowe natomiast to struktura
składająca się z licznika count zainicjowanego zerem i semafora
semaphore, wówczas funkcje wait
i notify wyglądają następująco:
wait(cond condition) {
++condition.count;
up(mutex);
down(condition.semapore);
down(mutex);
}
notify(cond condition) {
if (condition.count) {
--condition.count;
up(condition.semaphore);
}
}
- Napisać algorytm z wykorzystaniem do synchronizacji
semaforów z dostępnymi operacjami semaforowymi na jednym
semaforze (a więc nie wolno korzystać z atomowych
operacji wielosemaforowych) modelujący pracę statków
w następującej sytuacji:
- Z portu A do portu B cztery statki przewożą towar cyklicznie
pływając tam i z powrotem. Po drodze między A i B
jest jedna śluza, przez śluzę mogą jednocześnie przepływać co
najwyżej dwa statki (w tym samym bądź różnych kierunkach).
Ponadto, aby przepłynąć śluzę, statek musi być ciągnięty przez
(musi mieć przydzielony) holownik. Holowniki są dwa
i czekają na statki przy śluzie, na początku od strony portu
A, od której to statki rozpoczną swoje pierwsze przepłynięcie
przez śluzę. Holowniki po przepłynięciu śluzy pozostają na
drugiej stronie, nie wracają bez obciążenia. Aby holownik mógł
przeprowadzić statek przez śluzę, holownik musi znajdować się po
tej samej stronie śluzy co dopływający statek. Inaczej statek
musi czekać (proces ma zasnąć), aż inny statek płynący
z przeciwka przejdzie przez śluzę, a przy okazji na
właściwą stronę przemieści się jeden z holowników.
sem liczba_holownikow[2] = { 2, 0 };
int strona_holownika[2] = { 0, 0 };
sem mutex = 1;
void statek() {
int strona = 0;
for(;;){
int holowik;
rob_cos_po_stronie(strona);
plyn_do_sluzy_od_strony(strona);
down(liczba_holownikow[strona]);
down(mutex);
holownik = strona_holownika[1] == strona;
strona_holownika[holownik] = -1;
up(mutex);
plyn_na_druga_strone_z_holownikiem(strona, holownik);
strona = 1 - strona;
down(mutex);
/* Operacja przypisania pojedynczej zmiennej calkowitej jest
co prawda atomowa, ale nie mamy pewnosci czy na pewno tak
jest wiec lepiej dac mutex. */
strona_holownika[holownik] = strona;
up(mutex);
up(liczba_holownikow[strona]);
plyn_do_portu_po_stronie(strona);
}
}
- Gdyby było osiem to nic się nie stanie. Będzie to dzialać nawet dla
42 statków. Za to gdyby było więcej holowników to trzeba by użyć pętli
przy wybieraniu holownika.
Zarządzanie pamięcią
- Metody przydziału pamięci
-
- brak podziału–wolna przestrzeń adresowa w danej
chwili przydzielana jednemu procesowi użytkowemu.
Wieloprogramowanie można realizować przez wymiatanie
(ang. swapping),
- podział pamięci–wolna przestrzeń adresowa podzielona na części
przydzielane pojedynczym procesom użytkowym,
- wykorzystanie pamięci wirtualnej–istnieje jedna lub
wiele wirtualnych przestrzeni adresowych przydzielanych
procesom użytkowym, a mających w niewielkim stopniu
pokrycie w pamięci operacyjnej.
- Rodzaje fragmentacji, występowanie, metody
przeciwdziałania.
-
- wewnętrzna–zjawisko tworzenia niewykorzystywalnych,
choć przydzielonych w ramach pewnej struktury (partycja,
ramka), obszarów pamięci,
- zewnętrzna–zjawisko tworzenia niewykorzystywalnych,
nieprzydzielonych obszarów pamięci, zazwyczaj spowodowane
niedoskonałością działania organizacji alokacji pamięci
procesom użytkowym.
- Zapobiegać można poprzez:
- zwalnianie i scalanie,
- zagęszczanie i relokację oraz
- mechanizm stronicowania.
- Stronicowanie i segmentacja.
- Stronicowanie polega na podzieleniu pamięci wirtualnej procesu
na strony o zadanej wielkości i przechowywanie
fizycznych adresów początków tych stron w tablicy, w ten
sposób, że jeżeli proces odwołuje się do eNtego bajtu Ktej strony
w pamięci wirtualnej to na podstawie tablicy translacji
adresów tworzony jest adres fizyczny begin_page[K] + N.
- Segmentacja polega na tym, iż tworzonych jest wiele przestrzeni
adresowych, z których każda zaczyna się pod innym adresem
w pamięci fizycznej (jeżeli stronicowanie nie występuje) lub
wirtualnej (jeżeli stronicowanie występuje). Zasadniczo, jeżeli
proces odwołuje się do eNtego bajtu Ktego segmentu to na podstawie
deskryptora tego segmentu tworzony jest adres fizyczny/logiczny
begin_segment[K] + N.
- Zasadniczo strony na siebie nie mogą nachodzić (choć różne
strony różnych procesów mogą być odwzorowane na tą samą stronę
w pamięci fizycznej), mają stały rozmiar oraz ich kolejność
w pamięci wirtualnej w żaden sposób nie determinuje
kolejności w pamięci fizycznej, gdy tymczasem segmenty mogą
na siebie nachodzić, mają zmienne rozmiary oraz muszą być
przechowywane w postaci ciągłej w pamięci
fizycznej/logicznej.
- Odwrócone tablice stron.
- W normalnym podejściu tablic translacji adresów, tablice te
rosną w zależności od rozmiaru wirtualnej przestrzeni
adresowej, co może niepraktyczne. Przykładowo, mając 64-bitową
przestrzeń adresową (nie wiem czy takie architektury istnieją, ale
nieważne) trzeba by wprowadzić z pięć poziomów w tablicy
translacji co okazuje się dość niepraktyczne.
- Z tego powodu, zamiast trzymać tablicę strona logiczna
-> strona fizyczna trzyma się tablicę strona fizyczna ->
strona logiczna, której rozmiar jest proporcjonalny do rozmiaru
pamięci fizycznej. Wyszukiwanie w czymś takim powiązania
strony logicznej do strony fizycznej wymaga przeszukania całej
tablicy i dlatego stosuje się różne struktury pomocnicze
takie jak tablice mieszające i duże bufory.
- Opis translacji adresu w architekturze Pentium.
- Procesy adresują pamięć za pomocą 16-bitowego numeru segmentu
oraz 32-bitowego przesunięcia. Pierwsze trzy bity w numerze
segmentu są jakieś magiczne. Pierwsze dwa bity oznaczają poziom
ochrony, czy coś takiego, a trzeci czy opisu segmentu należy
szukać w GDT (Global Descriptor Table takiej samej dla wszystkich
procesów) czy LDT (Local Descriptor Table różnej dla każdego
procesu). W pierwszym kroku procesor wyciąga informacje
o segmencie, z których najistotniejszą jest początek
tego segmentu. Gdy ma już 32-bitowy początek segmentu sumuje go
z 32-bitowym przemieszczeniem w segmencie co daje adres
logiczny.
- Teraz rozpoczyna się stronicowanie, czyli bardziej znaczące 20
bity adresu logicznego oznaczają numer strony
i z (dwupoziomowej o ile dobrze pamiętam) tablicy
translacji adresów wyciągany jest adres danej strony w przestrzeni
fizycznej, a dokładnie bardziej znaczące 20 bity; pozostałe
12 bity są kopiowane bezpośrednio z adresu logicznego.
- Można to wszystko zapisać tak (uwaga! to tylko przykład, wcale
nie mówię, że to wygląda dokładnie tak, tzn. że trzeci bit
w numerze segmentu ma takie znaczenie, a nie odwrotne, czy że
właśnie w taki sposób szukany jest wpis w GDT/LDT; wszystko
rozdrobniłem, aby było bardziej czytelne):
uint32_t translate(uint16_t segment, uint32_t offset) {
/* segmentation */
bool global = segment & 4;
uint32_t *dt = global ? GDT : LDT;
uint32_t seg_start = dt[segment >> 3];
uint32_t logical = seg_start + offset;
/* paging */
uint32_t index1 = logical >> 22;
uint32_t index2 = (logical >> 12) & ((1 << 10) - 1);
uint32_t page_start = TAB[index1][index2];
return (page_start & ~((1 << 12) - 1)) | (logical & ((1 << 12) - 1));
}
- Poziomy ochrony w architekturze Pentium.
- Są cztery poziomy. Proces działający w danym poziomie nie
może odczytywać danych ani (bezpośrednio) wywoływać funkcji
znajdujących się w poziomach bardziej uprzywilejowanych. Aby
przejść do poziomu bardziej uprzywilejowanego proces musi albo
wykorzystać bramę (ang. call gate) albo wywołać przerwanie.
Wejście/wyjście.
- DMA–opis i przeznaczenie.
- Jest to mechanizm mający na celu umożliwienie transmisji danych
pomiędzy dwoma urządzeniami (np. dyskiem i pamięcią) bez
angażowania w to procesora. W trakcie, gdy DMA działa
procesor działa odrobinę wolniej (gdyż są jakieś czary mary,
dodatkowe cykle czy coś), ale w rezultacie wszystko działa
o wiele szybciej, gdyż procesor nie musi obsługiwać przerwań
związanych z transmisją danych, a wszakże samo
przełączanie kontekstu, gdy nastąpi przerwanie może być
długotrwałe.
- RAID 0
- Wiele dysków tworzy jeden dysk logicznych, brak redundancji.
Dane mogą być trzymane albo w paskach, gdzie poszczególne paski są
trzymane na kolejnych dyskach (np. jeżeli są dwa dyski to jeden ma
paski o numerach parzystych, a drugi nieparzystych) przy czym,
wymaga to, aby dyski były takie same (a dokładniej, jeżeli nie są
to marnuje się przestrzeń przynajmniej jednego z nich); albo
w ten sposób, że „konkatenuje” się wszystkie
dyski w ten sposób, że na każdym jest jakiś ciągły fragment
dysku logicznego.
- RAID 1
- Na wszystkich dyskach są dokładnie te same dane.
- RAID 2
- Jakieś coś dzikiego stosowane w dawnych czasach przez IBM
korzystające z kodów korekcyjnych.
- RAID 3
- Jeden z dysków jest dyskiem z danymi parzystości
i parzystość jest liczona na poziomie bajtów (?).
- RAID 4
- Tak jak RAID 3, tylko parzystość jest liczona na poziomie pasków
(w sumie nie wiem czym to się różni).
- RAID 5
- Tak jak 4, tyle że nie ma jednego dysku z parzystością tylko
dla każdego „poziomu” jest to cyklicznie kolejny
dysk.
- RAID 6
- Tak jak RAID 5 tylko są dwa dyski parzystości dzięki czemu jak
jeden dysk nam padnie można przejść na RAID5 i ciągle mieć
zabezpieczenie.
- RAID 0+1
- Na „dole” RAID 0, na
„górze” RAID 1.
- RAID 1+0
- Na „dole” RAID 1, na
„górze” RAID 0.
- RAID 10
- Po prostu RAID 1+0.
System plików
- Idea plików odwzorowywanych w pamięci.
- Spoko akcja ziom, polegająca na tym, iż jakiś obszar pamięci to tak
naprawdę zawartość pliku. Jeżeli OS wspiera ten mechanizm i go
odpowiednio poinstruujemy, to wie np. że w momencie zwalniania takiej
strony, zamiast wyrzucać ja do swapu może zapisać ją na dysk (tzn. no
może albo i nie, zależy jak to tam jest z współbieżnym dostępem do
pliku, ale pomińmy ten aspekt) i tak samo wie, że należy odczytać go
z tego pliku, a nie ze swapu. Tak samo, jeżeli proces zakończy
działanie czy wyrejestruje takie odwzorowanie to OS zapisuje zmiany
dokonane w pamięci do pliku.
- system plików w Unix V7 wraz z przykładem rozwiązania nazwy
/home/nowak/soi.txt
- Wszystko jest plikiem. Każdy plik ma iwęzeł. W iwęźle zapisane
są takie informacje jak rozmiar, atrybuty, czasy utworzenia,
modyfikacji itp. oraz wskaźniki na bloki danych. Katalog to plik,
w którym zapisane są pozycje katalogowe czyli lista nazw plików,
które się w nim znajdują, a dokładniej odwzorowanie
nazwa pliku<->numer iwęzła. W Unix V7 pozycja taka to
był 16-bitowy numer iwęzła oraz 14-znakowa nazwa pliku.
- Rozwinięcie nazwy /home/nowak/soi.txt:
Blok np. 1 Iwęzeł 67 Blok 24 iwęzeł 33 Blok 14
1 . rozmiar 24 . rozmiar 14 .
1 .. atrybutu 1 .. atrybuty 24 ..
46 dev bla bla bla 32 zenon bla bla bla [ 66 soi.txt ]
23 etc 24 [ 33 nowak ] 14 38 foo
[ 67 home ] . 12 kruk . 2 bar
. . . . .
. . . . .
. . .
- I już wiemy, że w iwęźle 66 są atrybuty, rozmiar, etc
interesującego nas pliku oraz wskazania na bloki danych zajmowane
przez ten plik.
- Oczywiście katalog może zajmować kilka bloków i dany plik może
znajdować się na którymś bloku z kolei.
- Idea dzienników transakcyjnych w systemach plików (ang. journaling).
- Idea jest taka, aby niezależnie od jakichkolwiek awarii
(np. nagłe wyłączenie prądu) system plików (a w różnych
wersjach dziennikowania również dane plików) pozostawały
w jednolitym stanie. Realizuje się to w ten sposób, że
zamiast zapisywać zmiany bezpośrednio w miejsce, którego one
dotyczą zapisuje się je gdzieś na boku i gdy to się powiedzie
zmienia się odpowiednie wskaźniki w systemie plików.
- Idea migawek w systemach plików (ang. snapshot).
- Jest to zapisanie stanu urządzenia takiego jaki jest
w danej chwili bez blokowania zapisu na to urządzenie
(tzn. zapewne zablokować trzeba, ale tylko na krótką chwilę, gdy
tworzona jest migawka, a to trwa raczej krótko).
W momencie tworzenia migawki dane nie są kopiowane
i zamiast tego, jeżeli na danym dysku jakiś proces próbuje
dokonać zmian to dany sektor/blok/jak-zwał-tak-zwał jest
zapisywany w jakiejś przestrzeni tymczasowej lub na odwrót,
tzn. w momencie modyfikacji dane z migawki kopiowane są
na obszar tymczasowy (a może nawet docelowy migawki),
a dane zmodyfikowane bezpośrednio na dysk.