Systemy Operacyjne

Michał ‘mina86’ Nazarewicz | 16 czerwca 2008

Coś z serii „ja już zaliczyłem na pięć, 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; }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 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.
  • 19 — 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 zero.
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 zera.
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 równy zero to uruchamiany jest y.
x || y — jak wyżej, tylko y uruchamiany jest, gdy x zwraca status różny od zero.
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.
  1. wewnątrz jeden proces,
  2. proces poza SK nie może blokować innego procesu pragnącego wejść do SK,
  3. 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 waitnotify 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 waitnotify 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.