RSS

Pamięć wirtualna

Liczba odsłon: 511

Wielu, nawet całkiem zaawansowanych użytkowników komputerów słysząc hasło „pamięć wirtualna” od razu odpowiada: „to miejsce na dysku twardym uzupełniające pamięć operacyjną”. Nic bardziej błędnego. Wirtualizacja pamięci może istnieć nawet na komputerach, które nie są w ogóle wyposażone w dysk twardy, a uzupełnianie braków pamięci fizycznej miejscem na dysku twardym jest tylko jedną ze specyficznych, opcjonalnych funkcji systemu pamięci wirtualnej.

Istnieje jedna cecha decydująca o „wirtualności” pamięci: możliwość separacji adresów logicznych generowanych przez programy od adresów fizycznych konkretnych komórek pamięci. Taka separacja może być przeprowadzona na wiele sposobów, z których najpopularniejsze dwa to segmentacja i stronicowanie. Warto podkreślić, że system pamięci wirtualnej musi funkcjonować w sposób całkowicie przezroczysty dla programów użytkownika; z konieczności jego funkcjonowanie musi być dostrzegalne jedynie dla systemu operacyjnego zarządzającego pamięcią (choć też nie dla wszystkich jego modułów).

Upraszczając jeszcze bardziej całą kwestię można stwierdzić, że program działający w systemie obsługującym pamięć wirtualną odwołuje się do pamięci „na ślepo”, znając tylko logiczne (wirtualne) adresy lokacji pamięci. Dopiero układ zarządzania pamięcią (MMU, ang. Memory Management Unit), przy współpracy z systemem operacyjnym przekształca adresy logiczne należące do danego procesu na konkretne adresy fizyczne właściwe dla pamięci operacyjnej. Warto pamiętać, że kilka procesów może używać tych samych adresów logicznych a mimo to odwoływać się do zupełnie różnych obszarów pamięci!

Segmentacja

Segmentacja pamięci polega na podzieleniu przez system operacyjny pamięci fizycznej na fragmenty o określonym początku, rozmiarze, atrybutach i identyfikatorze. System tworzy takie segmenty na żądanie aplikacji, przekazując jej jedynie identyfikatory nie pozwalające na odczytanie parametrów segmentów. Programy odwołują się zatem do kolejnych komórek pamięci w ramach należących do nich segmentów, nie wiedząc nic o tym, w jakie miejsca pamięci fizyczne trafiają ich odwołania. Procesy nie mają też prawa „widzieć” segmentów należących do innych programów — w czasie przekazywania kontroli procesowi system musi zablokować definicje segmentów należących do pozostałych procesów, przy każdym przełączeniu blokując segmenty wyłączanego programu i na nowo uaktywniając segmenty programu aktywowanego.

Segmenty mają swoje zalety. Największą jest prostota relokacji kodu i danych: ponieważ nie jest ważne gdzie w pamięci fizycznej zakotwiczony jest segment, program może odwoływać się do kolejnych słów pamięci w ramach segmentu zawsze licząc od zera do końca segmentu. Konieczne dodawanie adresów realizuje sprzętowo układ zarządzający pamięcią — co za ulga dla systemu operacyjnego!

Okazuje się jednak, że wady segmentacji przeważyły nad zaletami. Pierwsze implementacje segmentowanej pamięci wirtualnej narzucały dość poważne ograniczenia na rozmiary segmentów (na przykład 64 KiB), zmuszając programistów do dzielenia kodu programów oraz bloków danych w sposób nienaturalny i utrudniając tworzenie naprawdę dużych struktur danych. Nawet po zniesieniu tego ograniczenia okazało się, że nikt nie chce bawić się w dzielenie programu na logiczne części, a normalną pracę systemu i programu można osiągnąć znacznie prostszymi środkami. Podsumowując: segmentacja jest rozwiązaniem bardzo eleganckim, lecz na tyle kłopotliwym, że obecnie praktycznie się jej nie stosuje.

Stronicowanie

Stronicowanie pamięci (ang. paging) pozwala we względnie prosty sposób stworzyć aplikacjom iluzję posiadania przez nie (na własność!) płaskiej, liniowej przestrzeni adresowej rozpoczynającej się w komórce o adresie 0 i kończącej w ostatniej adresowanej przez mikroprocesor. Taka przestrzeń adresowa podzielona jest na fragmenty o określonym rozmiarze (najczęściej 4 KiB lub 8 KiB), zwane stronami (ang. page).

Ponieważ strony są tworem wirtualnym (tworzącym wirtualną przestrzeń adresową procesu), musi istnieć możliwość przechowywania gdzieś ich zawartości. W tym celu pamięć fizyczna również podzielona jest na fragmenty o takim samym rozmiarze co strona, zwane tym razem ramkami (ang. page frame). Jeżeli strona jest używana, jest skojarzona z konkretną ramką pamięci fizycznej przechowującą (buforującą) jej zawartość. Jeżeli strona jest zbędna, nie zajmuje ani jednej ramki pamięci fizycznej.

Stronicowanie pozwala w prosty sposób realizować ciekawe sztuczki. Kilka stron pamięci wirtualnej można skojarzyć z jedną i tą samą ramką pamięci fizycznej, współdzieląc w ten sposób dane między wieloma wirtualnymi przestrzeniami adresowymi (każdy proces dysponuje własną, więc jest to jedyny sposób wymiany danych między programami). Błędy zgłaszane w momencie odwołania do nieistniejącej strony mogą powodować przyznawanie procesowi większej ilości pamięci fizycznej, dzięki czemu program zajmuje jej dokładnie tyle, ile używa (a nie tyle, o ile poprosił — być może ze zbyt dużym zapasem).

Oczywiście tak jak w przypadku segmentacji cały opis wirtualnej, stronicowanej przestrzeni adresowej musi być zmieniany za każdym razem, gdy system operacyjny usypia jedno zadanie i wznawia inne. Tylko w ten sposób można zapewnić brak możliwości odczytywania i zmieniania danych należących do jednego procesu przez inny program.

Zarządzanie pamięcią wirtualną

Układ zarządzający pamięcią (lub sam mikroprocesor) potrzebuje szczegółowych informacji na temat segmentów i stron pamięci, którymi ma zarządzać. Informacje takie przechowuje w pamięci fizycznej i udostępnia układowi system operacyjny. Tylko on ma bezpośredni dostęp do pamięci fizycznej (z pominięciem mechanizmów pamięci wirtualnej) i „zna” jej podział na jednostki logiczne (segmenty lub stronice).

Rekordy informacji opisujących segmenty i strony nazywane są deskryptorami segmentów i stron (ang. segment descriptor, page descriptor) i gromadzone w tablicach deskryptorów (ang. descriptor table). Każdy proces działający w systemie dysponuje własnymi tablicami deskryptorów (wyłączny dostęp do nich ma system operacyjny) i za każdym razem, gdy moduł decyzyjny systemu nakazuje zmianę kontekstu wykonania kodu (czyli uśpienie jednego procesu i wznowienie wykonania drugiego) układ zarządzający pamięcią musi zostać przeprogramowany tak, by „zapomniał” o dotychczasowym podziale pamięci i „nauczył się” nowego, opisującego bloki pamięci należące do nowego procesu. W ten sposób pamięć jednego procesu jest chroniona przed ewentualnymi próbami dostępu ze strony drugiego procesu.

Zabezpieczenia

Wirtualizacja pamięci, dzięki konieczności opisywania każdego fragmentu pamięci używanego przez oprogramowanie, umożliwia nadawanie tym fragmentom poziomów uprzywilejowania. Dostęp do tak zabezpieczonej pamięci mogą mieć tylko instrukcje programu wykonywane z co najmniej takim samym (lub wyższym) poziomem uprzywilejowania. Nadając instrukcjom i fragmentom pamięci programu użytkownika niski poziom uprzywilejowania, a instrukcjom i fragmentom pamięci systemu operacyjnego poziom wysoki można w prosty sposób zapewnić współistnienie w każdym momencie programu i systemu, dostęp systemu do wszystkich informacji i brak możliwości zmodyfikowania lub przeglądania przez program bloków pamięci zawierających informacje systemowe.

Wymiana danych z pamięcią masową

Specjalnie nie wspominałem dotąd o współpracy z pamięcią masową, by jak najmocniej zatrzeć powiązania między pamięcią wirtualną a uzupełnianiem pamięci fizycznej miejscem na dysku twardym. Taka funkcjonalność, nazywana wymiataniem (ang. swapping) jest bowiem pewnym szczególnym przypadkiem wynikającym z faktu, że w deskryptorze każdego segmentu lub strony można ustawić flagę informującą, że segment lub strona nie istnieją w pamięci fizycznej. Przy próbie odwołania się do takiego fragmentu pamięci wirtualnej procesor zgłasza systemowi operacyjnemu błąd, a ten – zorientowawszy się w sytuacji – może załadować brakującą stronę z dysku twardego (ang. swap in), umieścić ją w pamięci fizycznej, zaktualizować deskryptor i wznowić wykonanie programu.

Pozbycie się danych z pamięci operacyjnej jest jeszcze prostsze: wystarczy zapisać zawartość segmentu lub strony (a dokładniej: odpowiadającej jej ramki) w pamięci masowej (na przykład na dysku twardym), po czym ustawić w deskryptorze flagę informującą, że segment lub strona przestały istnieć. Problemem jest jedynie ustalenie takiej strategii wyrzucania (ang. swap out) danych z pamięci fizycznej, by oddalane były tylko dane rzeczywiście chwilowo niepotrzebne: beznadziejny byłby system, który pozbywałby się danych tylko po to, by za chwilę znów musieć je wczytywać do pamięci.

Tutaj widać przewagę stronicowania nad segmentacją: w przypadu stronicowania wymiatanie danych obejmuje pojedyncze strony pamięci, a więc najczęściej 4 KiB lub 8 KiB danych w jednym kroku (oczywiście na raz można przenosić między pamięcią fizyczną a masową wiele stron); segmentacja wymusza wymiatanie całymi segmentami — jeżeli segment ma rozmiar jednego mebibajta, operacja przeniesienia go będzie trwała znacznie dłużej, a ponadto bardzo mało prawdopodobne jest, że naprawdę cały segment jest i będzie zbędny. Oczywiście, można połączyć ze sobą mechanizmy segmentacji i stronicowania i używać pierwszego do logicznego podziału pamięci, a drugiego do wymiatania danych do i z pamięci masowej — podnosi to jednak znacznie stopień skomplikowania systemu operacyjnego i oprogramowania, a zyski są wątpliwe. Dlatego najczęściej stosuje się tylko stronicowanie.

Przy okazji warto podkreślić różnicę między plikiem wymiany (ang. swap file), która to nazwa jest ogólna i może opisywać plik, w którym zapisane są obrazy segmentów lub stron, a plikiem stron (ang. paging file), który przechowuje wyłącznie obrazy niepotrzebnych chwilowo stron pamięci.

Pliki odwzorowywane w pamięci

Ograniczałem się dotychczas do opisywania sposobu wykorzystania pamięci wirtualnej, zarządzania nią i powiększania pojemności pamięci fizycznej przez usuwanie chwilowo niepotrzebnych fragmentów danych do pliku wymiany gdzieś w pamięci masowej. Zastosowanie mechanizmu umożliwiającego oznaczenie strony pamięci jako nieistniejącej jest jednak jeszcze szersze.

Wyobraź sobie sytuację, w której wczytujesz do pamięci wirtualnej procesu plik wykonywalny zawierający kod programu. Tworzone są strony pamięci mieszczące cały kod, a w czasie wczytywania — ramki pamięci przechowujące kolejne fragmenty tego kodu. Ładowanie trwa długo, gdyż trzeba wczytać wiele mebibajtów kodu (programy są obecnymi czasy dość obszerne...), a do tego już po paru minutach okazuje się, że większość kodu nie będzie potrzebna i może zostać spokojnie usunięta do pliku wymiany. Nie dość, że użytkownik stracił wiele sekund podczas uruchamiania programu, to jeszcze traci miejsce na dysku marnowane przez drugą kopię kodu programu (pierwsza jest w pliku wykonywalnym, a druga w pliku wymiany)!

Tymczasem można prościej. Niech wczytanie pliku wykonywalnego ogranicza się do przygotowania stron pamięci w liczbie wystarczającej do zmieszczenia całego kodu (jak poprzednio), jednak zamiast ładowania do nich kodu oznaczmy je jako nieistniejące. System rozpoczyna wykonywanie programu i BUUUM — procesor zgłasza nieistnienie strony. Funkcja obsługi błędu wie jednak o co chodzi, wczytuje fragment kodu z pliku i program „leci” dalej aż do końca strony, znów wylatując z takim samym błędem przy kolejnej stronie; powtarzamy procedurę i tak aż do znudzenia. Zalety takiego rozwiązania, zwanego wczytywaniem stron na żądanie (ang. page on demand) są oczywiste:

Opisane powyżej rozwiązanie nosi nazwę plików odwzorowanych w pamięci (ang. memory mapped files) i stosowane jest obecnie przez wszystkie systemy operacyjne nie tylko w stosunku do plików wykonywalnych, ale do wszystkich plików (o ile aplikacja życzy sobie odwoływać się do nich w ten oszczędny i bezpośredni w wykorzystaniu sposób). Dzięki niemu plik staje się jakby fragmentem pamięci operacyjnej (i faktycznie jest w niej buforowany, jednak wszystkie operacje zapisu powodują zaktualizowanie danych w pamięci masowej), a klasyczna operacja przydzielenia fragmentu pamięci może być stosowana wyłącznie w przypadku potrzeby przechowywania danych w sposób tymczasowy, bez konieczności zapisywania ich na dysku.

Łatwo też zauważyć, że do realizacji mechanizmu plików odwzorowanych w pamięci niezbędne jest stronicowanie. Segmentacja pozwoliłaby co najwyżej wczytać do pamięci cały segment, czyli efektywnie — cały plik (chyba, że operacja dotyczyłaby pliku wykonywalnego podzielonego już wewnętrznie na segmenty). Bez sensu.

Kopia na życzenie

Zdarza się czasem, że dwa programy muszą korzystać z tych samych danych źródłowych, mając jednak możliwość wprowadzania w nich zmian widocznych tylko dla nich samych, nie wpływających na kopię „widzianą” przez drugi proces. W klasycznym systemie bez pamięci wirtualnej jedynym sposobem rozwiązania takiego problemu jest wcześniejsze skopiowanie całego bloku danych i udostępnienie jednej kopii każdemu z procesów. Segmentacja pamięci znikomo ułatwia zadanie: należy stworzyć kopię całego segmentu i każdemu z procesów przekazać jedną z nich (choć obie mogą mieć w takim przypadku taki sam identyfikator, czyli selektor, ang. selector). W obu przypadkach operacja wymaga trzymania dwóch kopii bloku nawet, jeśli ewentualne zmiany dotyczą tylko kilku bajtów. Efektywne? Absolutnie nie.

Przy stronicowaniu zadanie jest proste: wystarczy oznaczyć wszystkie strony interesującego oba procesy bloku pamięci jako „tylko do odczytu” i pozwolić im dowolnie je wykorzystywać. Gdy któryś z procesów spróbuje dokonać zmiany, procesor zgłosi błąd, a system operacyjny natychmiast skopiuje modyfikowaną stronę pamięci (tylko jedną stronę!), wstawi tę kopię w przestrzeń adresową procesu modyfikującego jej zawartość zamiast „oryginału” i usunie z niej blokadę zapisu. Praca programu zostanie wznowiona, operacja zapisu słowa danych do pamięci dokończna, a blok danych rozrośnie się o jedną tylko zmodyfikowaną stronę. Oczywiście w momencie, gdy zmiany obejmą cały blok pamięci konieczne będzie i tak utrzymywanie dwóch pełnych kopii tego bloku, jednak z drugiej strony przy braku zmian programy nie zajmą nadmiarowo ani jednego bajtu! Wszystko to dzięki opisanemu wyżej mechanizmowi, zwanemu kopiowaniem w momencie zapisu (ang. copy on write).

Podsumowanie

Trudno sobie wyobrazić nowoczesny, wielozadaniowy, stabilny system operacyjny bez obsługi pamięci wirtualnej. I choć wiele osób przeklina plik wymiany i nagłe spowolnienia powodowane wczytywaniem wyrzuconych wcześniej ramek pamięci, brak tych wszystkich mechanizmów albo uniemożliwiłby ładowanie używanego obecnie oprogramowania i zbiorów danych (nie zmieściłyby się w pamięci), albo jeszcze bardziej ograniczył wydajność komputera, skupiającego swoją moc obliczeniową na przemieszczaniu i zwalnianiu bloków pamięci tak, by zadowolić potrzeby wszystkich procesów.


Bardzo ciekway artykuł, z tym, że ja jestem jeszcze zielony w tych kwestiach. Daltego też mam pytanie natury technicznej, co zrobić, gdy pokazuje się informaacja " za mało pamięci wirtualnej ..." taka informacja pokazuje mi się gdy gram w BF 2 w sieci i tylko wtedy, co powoduje, że gra się "tnie" i jest to niestety denerwujące. Co zrobić, żeby nie było więcej takich komunikatów.
Trzeba zwiększyć minimalny rozmiar pliku wymiany w Windows.
Najlepiej otwórz Menedżera zadań i obserwuj wykres zajętości pamięci. Jeżeli kiedykolwiek zaczyna docierać do górnej granicy skali, na pewno trzeba powiększyć minimalny rozmiar pliku wymiany.
Gratuluję i dziekuję za całkiem sporo przydatnych informacji.