Pamięć
Jednym z zadań systemu operacyjnego jest zarządzanie zasobami – tzn. przydzielanie ich i odbieranie wykonywającym się programom. Pamięć, zaraz po czasie procesora, jest najważniejszym i zarazem najpopularniejszym zasobem, który używany jest przez procesy. Jak pokazuje historia, rozwój pamięci jest bardzo szybki – IBM 7094 z 1962 roku miał standardowo około 150 KiB pamięci, a sam komputer kosztował 3.5 mln USD. Na dzień dzisiejszy, przeciętny komputer ma 10000 razy więcej pamięci i kosztuje 10000 razy mniej :). Z drugiej strony, programy rozrastają się jeszcze szybciej niż dostępna pamięć – 1 MiB wystarczał kiedyś, żeby polecieć w kosmos, natomiast Windows Vista ledwo zipie na komputerze z 1 GiB pamięci ;).
Pamięci powinno być dużo – najlepiej nieskończenie dużo. Do tego jeszcze warto, aby była ona nieskończenie szybka, nieulotna i tania. Niestety – nie da się! W ramach rozwiązania tego problemu wprowadzono koncepcję hierarchii pamięci. Zgodnie z nią komputery wyposażone są w nieco bardzo szybkiej, małej i drogiej pamięci (rejestry, cache); w dużo szybkiej i niedrogiej pamięci (RAM) oraz w ogromnie dużo, bardzo wolnej, ale bardzo taniej pamięci (HDD, DVD itd.). Do wygodnej pracy z taką hierarchią niezbędne jest stworzenie pewnej abstrakcji, która pozwalałaby nie przejmować się tym, czy operujemy na cache’u, RAM-ie, czy wczytujemy wymiecione programy z dysku – ale o tym za chwilę, na razie zajmijmy się najprostszym przypadkiem.
Brak abstrakcji
W tym modelu zarządzania pamięcią, każdy wykonywający się program operuje bezpośrednio na pamięci fizycznej (RAM) – wykonanie instrukcji mov [0x9000], 24 spowoduje wpisanie wartości 24 pod adres 0x9000. Wydaje się to intuicyjnie oczywiste, jednak pociąga to za sobą wiele skomplikowanych problemów. Przede wszystkim ciężko tu zapewnić ochronę – procesy mogą odczytywać i zapisywać pamięć, która nie należy do nich. Do tego, każdy proces musi wiedzieć gdzie został załadowany tak aby instrukcje odwoływania się do pamięci były poprawne. Każdy proces musi również znać i przestrzegać limitów pamięci, które raczej powinny być sztywne. Wszystkie te problemy starano się jakoś rozwiązać i tak np. wprowadzono podział pamięci na pewne zakresy, które przypisywane były procesom poprzez odpowiednie znaczniki. Inny sposób zakłada natomiast wymiatanie całej pamięci na dysk, wybranie kolejnego procesu do wykonania, a następnie wczytanie całej jego pamięci z dysku – jest to rozwiązania wyjątkowo nieefektywne. Dokładniejsze studium przypadku ujawnia wiele kolejnych problemów – np. alokacja dużej tablicy (ciągłej!) w pofragmentowanej przestrzeni może się okazać niemożliwe – potrzebny jest wtedy mechanizm relokacji. Na wiele problemów związanych z brakiem abstrakcji znajdziemy pewne (lepsze lub gorsze) remedium, niestety niektórych rzeczy obejść się nie uda – np. potrzeby powiększenia przestrzeni adresowej ponad dostępna pamięć fizyczną. Na koniec warto jednak dodać, iż całkowity brak abstrakcji na pamięci jest wciąż popularny i uzasadniony – w systemach wbudowanych. Procesy działające w takich systemach są zwykle małe i zaufane – nie potrzebna jest im szczególna ochrona, czy wygoda programowania.
Pamięć wirtualna
Wirtualny – słowo często używane w informatyce, nie zawsze poprawnie, nie zawsze świadomie :). Ciekawy tego co mówi na ten temat słownik języka polskiego – sprawdziłem i zdziwiłem się co nie miara. Nic więc dziwnego, że dezinformacja się szerzy :D. Mówiąc wymijająco (ale za to prawdziwie) – to co znaczy wirtualny – zależy od kontekstu :). Wracając jednak do tematu…
Pamięć wirtualna to mechanizm zapewniający wykonywającemu się programowi dostęp do pełnej, niezależnej od innych procesów przestrzeni adresowej. Przestrzeń ta w szczególności nie musi odpowiadać pamięci fizycznej (co jest sytuacją typową) – może być nieciągła, pofragmentowana i przechowywana na nośnikach trwałych (dysku). Zachodzi więc pewne mapowanie adresu wirtualnego (na którym operuje proces) na adres w pamięci fizycznej. To podejście rozwiązuje nam wszystkie problemy związane z ochroną (oddzielne przestrzenie adresowe = brak możliwości „wchodzenia sobie w drogę”), ładowaniem do pamięci (procesy mogą być np. ładowane pod stały adres), nieciągłością przy alokacji tablic (sprawę załatwia mapowanie – ciągły obszar mapowany jest na nieciągły, który był aktualnie dostępny). Nie ma rzeczy idealnych, więc łatwo się domyślić, że muszą być jakieś wady. Tak jest i tym razem. Pamięć wirtualna znacząco komplikuję budowę architektur (potrzebna jest jednostka zarządzania pamięcią), a także powodują pewien narzut związany z pośredniością, co może mieć znaczenie np. w systemach czasu rzeczywistego.
Dla uzupełnienia dodam, że pamięć wirtualną buduje się w oparciu o pamięć RAM i urządzenia dyskowe. Wspomniany wcześniej cache jest w zasadzie dla programisty i systemu operacyjnego przezroczysty (podsystemy architektury decydują o tym, co ma być cache’owane), a rejestrami zajmują się kompilatory i programiści assemblera (i tak nie przydałyby się w budowaniu pamięci wirtualnej).
Praktycznie
Praktycznie rzecz ujmując pamięć wirtualna jest jak najbardziej pożądana (oprócz wspomnianych systemów czasu rzeczywistego) i w zasadzie nie spotyka się w dużych komputerach jej braku. Pamięć wirtualną realizować można na dwa sposoby – za pomocą segmentacji oraz za pomocą stronicowania (o którym w następnym wpisie), z czego tak jak pisałem, ta druga metoda jest zdecydowanie popularniejsza.