Ośmiobitowa pamięć wirtualna

Liczba odsłon: 188

Mechanizmy pamięci wirtualnej kojarzą się wyłącznie z nowoczesnymi, 32-bitowymi mikroprocesorami. W rzeczywistości już niektóre późne konstrukcje 16-bitowe (na przykład Intel 80286 i Zilog Z8000) wprowadzały pewne możliwości wirtualizacji pamięci i oferowały co najmniej dwa tryby pracy (zwykły i uprzywilejowany). Te możliwości są całkowicie wystarczające do stworzenia systemu operacyjnego ściśle nadzorującego pracę aplikacji (systemem takim był choćby 16-bitowy Microsoft OS/2 stworzony dla mikroprocesora 80286).

Co jednak wielu osobom może wydać się zupełnym szaleństwem, w większość tych funkcji można wyposażyć nawet najprostsze ośmiobitowe mikroprocesory, znane ze starych domowych komputerków. Za obsługę pamięci wirtualnej odpowiada bowiem osobny blok funkcjonalny zwany MMU (ang. Memory Management Unit). We wszystkich obecnie produkowanych procesorach przeznaczonych na rynek konsumencki MMU stanowi zintegrowaną część składową, tylko w starszych konstrukcjach (choćby w przypadku Z8000) pojawiając się w postaci odrębnego układu scalonego. Niezbyt skomplikowanego układu scalonego, należy dodać, i zaprojektowanie takiego układu leży w zakresie możliwości wielu elektroników.

Rozważania nad możliwością wyposażenia mikroprocesora ośmiobitowego w układ zarządzania pamięcią należy rozpocząć od przypomnienia sobie zasad działania mechanizmów pamięci wirtualnej. Wirtualizacja pamięci polega zatem na rozróżnieniu adresów komórek pamięci generowanych przez program od ich adresów przekazywanych układom scalonym pamięci RAM. Mówiąc bardziej obrazowo, program generuje jakiś adres (wirtualny), moduł MMU zamienia go w zupełnie inny adres na podstawie przekazanych mu przez system operacyjny informacji o ułożeniu pamięci i przekazuje ten adres (już nie wirtualny, a fizyczny) układom pamięci. Zazwyczaj cała sztuka odbywa się we wnętrzu mikroprocesora i na jego magistrali adresowej pojawia się od razu adres fizyczny, nic nie stoi jednak na przeszkodzie, by pojawił się na niej jeszcze adres wirtualny, przerabiany na fizyczny dopiero w osobnym układzie pośredniczącym w transmisji pomiędzy procesorem a pamięcią.

Na potrzeby tego artykułu przyjmijmy, że mamy do czynienia z dowolnym ośmiobitowym mikroprocesorem posiadającym 16-bitową magistralę adresową (a więc zdolnym zaadresować do 64 KiB pamięci), osobne linie R oraz W sygnalizujące chęć zapisu lub odczytu danych oraz linię MEM/IO wybierającą obszar adresowy, którego tyczyć się ma operacja transmisji (pamięć operacyjna lub przestrzeń adresowa urządzeń wejścia-wyjścia).

Mikroprocesor

Aby w ogóle możliwe było tłumaczenie adresów wirtualnych na nowe adresy fizyczne o innej wartości, należy podzielić pamięć operacyjną na jednostki organizacji. Intel projektując układ 80286 wybrał elastyczny mechanizm segmentacji, w którym programista określa liczbę, początek w pamięci fizycznej i rozmiar w bajtach każdego używanego obszaru z osobna. Po latach okazało się, że teoretycznie mniej elastyczny, lecz za to prostszy mechanizm stronicowania cieszy się większą popularnością i większość układów 32-bitowych (nawet własny układ 80386 Intela) może korzystać ze stronicowania podczas gdy niewiele modeli obsługuje jakąś formę segmentacji. My mamy zamiar zaprojektować coś nowoczesnego, proponuję zatem podzielić pamięć o pojemności maksymalnej 256 KiB na strony po 4 KiB każda (taki rozmiar stron pamięci, a dokładniej ramek pamięci, jest nieformalnym standardem w świecie mikroelektroniki).

Widzisz już chyba pierwszą ciekawostkę: mamy zamiar podłączyć do starego układu obsługującego 64 KiB pamięci czterokrotnie więcej, niż jest on w stanie zaadresować. Normalnie stanowiłoby to problem, jednak wirtualne zarządzanie pamięcią pozwoli korzystać z całej pojemności pamięci — choć oczywiście na raz widoczne będzie co najwyżej 64 KiB. Przełączanie fragmentów pamięci tak, aby 8-bitowy procesor mógł obsłużyć 128 KiB lub więcej pamięci nie jest zresztą nowym pomysłem: takie sztuczki stosowano w wielu mikrokomputerach domowych, spośród których wystarczy wymienić Atari 130XE, Amstrada CPC 6128 czy Amstrada PCW 8512. Przełączanie bloków było też chętnie stosowanym sposobem pełnego wykorzystania pamięci RAM o pojemności 64 KiB: wyłączając ROM i włączając w zamian bloki RAM można było używać całego obszaru RAM na raz. Dosyć jednak tych dygresji.

Efektem jest następujący podział adresów pamięci:

Widać wyraźnie, że skoro procesor może wygenerować w ramach adresu wirtualnego jedynie 16 możliwych numerów stron pamięci od 0 do 15, układ MMU musi zawierać 16 rejestrów o pojemności 6 bitów każdy, by zamieniać adresy stron pamięci wirtualnej na adresy ramek pamięci fizycznej. Wyposażymy go jeszcze w moduł interfejsu umożliwiający wykonanie na rozkaz mikroprocesora jednej tylko operacji: podstawienia pod stronicę pamięci wirtualnej o numerze x (0—15) ramki pamięci fizycznej o numerze y (0—63). Całość będzie wyglądała następująco:

System pamięci wirtualnej

Podłączamy teraz do MMU 128 KiB pamięci RAM i tyleż pamięci ROM (gdzieś w końcu musi znaleźć się system operacyjny lub przynajmniej program ładujący go z pamięci masowej...) i stajemy przed zadaniem określenia pod jakimi adresami wirtualnymi powinny się znajdować jakie elementy systemu operacyjnego i aplikacji. Mógłby to być następujący podział:

Podział pamięci fizycznej na ramki
Podział 256 KiB pamięci fizycznej (ROM oraz RAM) na ramki po 4 KiB każda oraz adresacja fizyczna bloków pamięci
 
Podział przestrzeni adresowej
Podział 64 KiB wirtualnej przestrzeni adresowej mikroprocesora na strony po 4 KiB każda

Zerową stronę pamięci w wirtualnej przestrzeni adresowej każdego procesu zarezerwowałem na potrzeby systemu operacyjnego, gdyż większość mikroprocesorów trzyma w pierwszych kilkudziesięciu bajtach pamięci istotne dla systemu operacyjnego informacje, na przykład wektory obsługi przerwań lub dane konfiguracyjne. Dwanaście kolejnych stron przeznaczonych jest do dowolnego wykorzystania przez aplikacje, zaś trzy ostatnie przekazałem systemowi operacyjnemu na jego kod.

Trzy strony (razem 12 KiB) mogą wydawać się o wiele zbyt małym obszarem biorąc pod uwagę, że nasz projekt mikrokomputera zawiera 128 KiB (a więc 32 ramki) pamięci ROM przechowującej wszystkie procedury systemowe. Niech jednak tylko pierwsza z trzech zarezerwowanych stron zawiera non-stop tę samą ramkę ROM, a pozostałe są przełączane w zależności od potrzeb. Jest potrzebna procedura znajdująca się w ramce piątej pamięci ROM? Niech strona E pamięci wirtualnej wskazuje na ramkę 5 ROM, a pod przynależnymi tej stronie adresami komórek pamięci $E000—$F000 pojawi się kod z wycinka pamięci ROM. Kolejne odwołanie do systemu operacyjnego wymaga obszerniejszej funkcji zapisanej w ramkach 7 i 8 pamięci ROM? Dwie operacje zapisu do rejestrów MMU wystarczą, by w stronicach E i F (adresy od $E000 do $FFFF) pojawiły się w mgnieniu oka dane z potrzebnych ramek ROM! Wszystko oczywiście bez żadnego kopiowania: po prostu MMU w momencie odwołania się do określonych komórek pamięci wirtualnej przekierowuje operację w określony w jego rejestrach obszar pamięci fizycznej.

Zmiana adresacji wirtualnej
Przeprogramowanie układu MMU pozwala mikroprocesorowi pod tymi samymi adresami wirtualnymi „widzieć” różne ramki pamięci i wykorzystywać je zamiennie. Dzięki temu w ramach tylko 12 KiB przestrzeni adresowej można wywoływać podprogramy zapisane w 128 KiB pamięci ROM.

Ten sam mechanizm podstawiania można wykorzystać w celu uruchamiania jednocześnie kilku programów i przełączania się między nimi. Załóżmy, że system operacyjny zapisany w pamięci ROM umożliwia uruchomienie kilku programów (z których aktywny jest tylko jeden) i przełączanie się pomiędzy nimi za pomocą odpowiedniej kombinacji klawiszy. Nacisnięcie tej kombinacji powoduje skok do kodu systemu operacyjnego w ramach obsługi przerwania klawiatury, rozpoznanie naciśniętej kombinacji i włączenie w obszar adresowy stronic E i F funkcji obsługi przełączania aktywnej aplikacji. Funkcja ta zapamiętuje gdzieś w pamięci operacyjnej informację o tym, której stronie pamięci wirtualnej z zakresu od 1 do C (przynależnego aplikacjom) odpowiada która ramka pamięci fizycznej (aby móc przywrócić potem stan aplikacji), po czym zapisuje w rejestrach MMU odczytane z pamięci takie same informacje o procesie, do którego ma nastąpić przełączenie działania. Nastepnie wystarczy tylko przywrócić zawartości rejestrów i wskaźnika stosu (również zapisane w pamięci obok tabeli translacji stron na ramki) i wrócić z poziomu systemu operacyjnego do aplikacji. Przed przerwaniem wykonywała się aplikacja A wykorzystując w dowolnym zakresie przysługujących jej 12 stron pamięci wirtualnej, po powrocie z przerwania działa już aplikacja B obsługująca pamięć w dowolny inny sposób i – co najciekawsze – nie mająca fizycznego dostępu do pamięci aplikacji A bowiem ramki tej pamięci nie są włączone w obszar adresowy procesora i dopóki nie zmieni się zapisów w rejestrach MMU nie ma sposobu, by się do nich dostać i odczytać lub zmodyfikować obce dane!

Można pójść jeszcze dalej i zacząć oszczędzać pamięć za pomocą kreatywnego wykorzystania stronicowania. Wyobraź sobie program wymagający 5 ramek pamięci na swój kod (20 KiB), 1 ramki na stos (4 KiB) i 6 ramek na dane (24 KiB). Razem zajmuje on 12 ramek (czyli wszystko, co ma do dyspozycji), a to odpowiada 48 KiB pamięci. Jeżeli teraz uruchomisz dwie kopie tego samego programu, zajmą one w sumie 24 ramki, a więc 96 KiB pamięci — prawie wszystko, co masz do dyspozycji!

Tak jednak być nie musi: przecież w obu przypadkach kod programu jest ten sam, zmieniają sie jedynie dane. Sprytny system operacyjny może wykorzystać ten fakt, by kod programu zapisać w pamięci tylko jednokrotnie w 5 ramkach, a następnie te 5 ramek włączać w wirtualną przestrzeń adresową każdej kopii tego programu. W efekcie raz uruchomiony program zajmie tak samo jak poprzednio 48 KiB pamięci, jednak druga (i każda kolejna) kopia będzie wymagała jedynie dodatkowych 28 KiB pamięci na stos i dane. W 128 KiB pamięci mieszczą się teraz trzy kopie programu i niewiele brakuje, by zmieściła się jeszcze czwarta.

Takie proste włączanie stron pamięci jednego procesu w wirtualną przestrzeń adresową innego procesu może dać jeszcze jedną korzyść: współdzielenie danych. Jeżeli jeden proces zapisze coś w ramce pamięci dostępnej dla innych procesów, po przełączeniu aktywnego procesu przez użytkownika druga kopia programu (lub inny program) może odczytać zapisane tam wcześniej dane i wykorzystać je lub zmodyfikować. W ten sposób tworzy się mechanizmy wymiany danych takie jak potoki (ang. streams, pipes) lub współdzielone bloki pamięci (ang. shared memory area).

Coraz większym problemem staje się jednak zabezpieczenie pamięci przed nieautoryzowanym dostępem lub modyfikacją. Co prawda w naszym przykładzie aplikacje nie mogą uszkodzić kodu systemu operacyjnego (jest on przechowywany w pamięci ROM), mogą jednak zniszczyć swój własny kod (co w przypadku współdzielenia go między wieloma kopiami aplikacji spowoduje „wylot” wszystkich z nich) lub dane przekazane im wyłącznie do odczytu. Wrażliwa na ataki będzie też strona pamięci o numerze 0, którą zarezerwowałem na potrzeby systemu operacyjnego. Trzeba coś z tym zrobić.

Zacznijmy od możliwości wskazania, czy strona pamięci ma być dostępna do odczytu i do zapisu. W tym celu do każdego z szesnastu rejestrów MMU opisujących szesnaście dostępnych w przestrzeni adresowej stron dodajmy dwa bity. Jeden z nich będzie określał, czy stronę można czytać, a drugi — czy można zapisywać. W stanie 00 strona będzie całkowicie niedostępna (co pozwoli przyznawać aplikacjom tylko tyle stron, ile naprawdę potrzebując, resztę zaznaczając jako niedostępne), w stanie 01 będzie ją można czytać, w stanie 10 tylko zapisywać (mało to przydatne), a w stanie 11 będzie ona w pełni dostępna do zapisu i do odczytu.

System operacyjny będzie nieco bardziej skomplikowany, teraz jednak strony pamięci zawierające kod programu będą oznaczane trybem dostępu 11 tylko na czas ładowania programu do pamięci, po czym będzie można oznaczyć je jako 01, przez co procesor będzie mógł je odczytywać, jednak próby zapisu będą ignorowane lub zgłaszane przerwaniem. Próba odczytania strony w ogóle niedostępnej spowoduje zwrócenie jakiejś bezpiecznej wartości (najlepiej 00h lub kodu instrukcji typu NOP, nic nie robiącej) i również zgłoszenie procesorowi przerwania. Od tego momentu programy nie mogą uszkodzić własnego kodu i mogą chronić udostępniane innym procesom dane przed modyfikacją (o ile sobie tego życzą, oczywiście), jednak nadal strony pamięci należące do systemu operacyjnego dostępne są do zapisu dla wszystkich.

Kolejnym krokiem powinno być zatem wprowadzenie drugiego trybu pracy o wyższych uprawnieniach, tak zwanego trybu uprzywilejowanego (nazywanego też trybem jądra). Do rejestrów MMU dodajemy jeszcze dwa bity pełniące identyczną rolę jak te dodane przed chwilą, jednak w odniesieniu do trybu uprzywilejowanego; poprzednie dwa będą się tyczyły zwykłego trybu pracy (nazywanego trybem aplikacji lub trybem użytkownika). Teraz system operacyjny może ustawić dla trybu użytkowniku prawo dostępu do strony systemowej o wartości 01, co uniemożliwi aplikacji zmodyfikowanie danych, pozostawiając dla trybu jądra prawo dostępu o wartości 11, dopuszczające wprowadzanie zmian. Jak jednak rozróżnić te tryby?

Nowoczesne mikroprocesory posiadają wbudowane układy rozdzielające oba tryby i nawet potrafią blokować wykonywanie z poziomu trybu użytkownika rozkazów potencjalnie destabilizujących system operacyjny (na przykład zmieniających konfigurację pamięci). Mikroprocesory 8-bitowe nie obsługują oczywiście takiego podziału, musimy go zatem nieudolnie udawać. Niech w MMU znajdzie się jeszcze jeden rejestr, jednobitowy tym razem, ustawiany w stan zero w czasie, gdy procesor ma wykonywać kod systemu operacyjnego i w stan jeden w czasie, gdy wykonywany jest kod aplikacji. W zależności od stanu tego bitu układ MMU analizuje pierwszą lub drugą parę bitów określających prawa dostępu do strony pamięci: jeżeli procesor znajduje się w trybie uprzywilejowanym, może otrzymać szersze prawa dostępu do strony pamięci niż w trybie aplikacji.

Realizacja praw dostępu do pamięci
Realizacja praw dostępu do pamięci
W pierwszym przypadku układ MMU znajduje się w trybie uprzywilejowanym (USER=0), dlatego prawa dostępu do strony pamięci pobierane są z bitów SW (ang. supervisor write) i SR (ang. supervisor read) deskryptora strony. Zapis jest dozwolony (SW=1), zatem operacja jest przeprowadzana.
W drugim przypadku układ MMU znajduje się w trybie zwykłym (USER=1), dlatego prawa dostępu do strony pamięci pobierane są z bitów UW (ang. user write) i UR (ang. user read). Zapis jest zabroniony (UW=0), zatem operacja jest anulowana a stan linii W zmieniany.

Nic jednak nie przełączy automatycznie trybu pracy procesora z jednego w drugi i dlatego sam system musi po rozpoczęciu działania przez jedną z jego funkcji przełączać MMU w tryb nadzorowany i vice versa, przy powrocie do kodu aplikacji ponownie wpisywać jedynkę w rejestr trybu ograniczając uprawnienia dostępu do pamięci. Będzie to działać całkiem sprawnie z jednym wyjątkiem: ponieważ operacja przełączenia trybu pracy musi być wykonana przez system oczywiście jeszcze w momencie pracy w trybie użytkownika, potencjalnie aplikacja może zrobić to samo nadając sobie bardzo wysokie uprawnienia. Taką cenę płacimy za rozszerzanie możliwości prostego mikroprocesora.

Teraz dysponujemy już projektem naprawdę działającego ośmiobitowego systemu mikroprocesorowego z mechanizmem pamięci wirtualnej realizowanym przez stronicowanie. Krótko mówiąc — ZX Spectrum z możliwościami Pentium! Możemy zatem zaszaleć jeszcze bardziej: dodajmy jeszcze jeden bit w każdym z rejestrów opisujących strony pamięci (te rejestry nazywają się fachowo deskryptorami stron, a blok 16 rejestrów opisujących całą przestrzeń adresową to tablica deskryptorów stron). Ten jeden bit o nazwie PRESENT będzie ustawiony na wartość jeden jeżeli strona będzie obecna w pamięci lub niedostępna, zaś na wartość zero jeżeli strona zostanie usunięta do pliku wymiany. W momencie przełączania aktywnego zadania system operacyjny może zwolnić nieco pamięci RAM przenosząc deaktywowane strony pamięci do pliku wymiany i kasując bit PRESENT w ich deskryptorach; z kolei deskryptory stron pamięci aktywowanego zadania muszą zostać przeglądnięte i jeżeli któraś z nich będzie nieobecna, musi zostać wczytana do pamięci zanim zacznie być wykonywany kod aktywowanego programu. W efekcie mimo posiadania w projektowanym przez nas komputerze jedynie 128 KiB pamięci RAM można używać wielu programów zajmujących łącznie setki kilobajtów pamięci na dane — nieaktywne zadania będą przenoszone na dyskietkę, a zwolniona przez to pamięć stanie się dostępna dla włączanego programu w ciągu kilku sekund potrzebnych na dokonanie zapisu i odczytu stron pamięci.

Nie możemy niestety w pełni zautomatyzować procesu wymiany stron między pamięcią fizyczną i plikiem wymiany tak, jak jest to zrobione w nowoczesnych mikroprocesorach 32-bitowych. Spowodowane jest to faktem, że gdyby nawet MMU mogło zareagować przerwaniem na próbę odwołania się do komórki pamięci leżącej w stronie nieobecnej w pamięci fizycznej, ośmiobitowy procesor obsłużyłby je dopiero po zakończeniu wykonywania rozkazu odwołującego się do tej komórki! Tymczasem możliwość załadowania strony do pamięci fizycznej w ramach przerwania i kontynuowania realizacji przerwanego brakiem danych rozkazu wymaga, by przerwanie wstrzymywało wykonanie rozkazu, a po jego obsłużeniu procesor był w stanie powtórnie wykonać ten sam rozkaz, tym razem dysponując już potrzebnymi stronami pamięci. Procesory od początku projektowane z myślą o mechanizmie pamięci wirtualnej mają taką możliwość, podczas gdy stare mikroprocesory ośmiobitowe obsługują przerwania tylko w przerwie między cyklami rozkazowymi i nie dopuszczają możliwości, że potrzebnego bloku pamięci „chwilowo nie ma”.

Z tego samego powodu niemożliwe jest zrealizowanie obsługi mechanizmu page on demand, polegającego na wczytywaniu zawartości stron pamięci z dysku dopiero w momencie pierwszego odwołania się procesora do komórek danej strony. Gdyby zatem nawet nasz miniaturowy system operacyjny został wyposażony w obsługę plików odwzorowywanych w pamięci, pliki te musiały by być ładowane w całości już w momencie definiowania odwzorowania, co z dzisiejszego punktu widzenia jest marnowaniem czasu i pamięci (poza przypadkami, gdy program odwołuje się do całej zawartości używanego pliku). Odwzorowywanie plików w pamięci wymagałoby też rozszerzenia funkcjonalności MMU: dodatkowy bit deskryptorów stron pamięci (na przykład o nazwie MODIFIED lub DIRTY) przechowywałby informację, czy dana strona pamięci została zmodyfikowana i w związku z tym czy jej zawartość powinna być zapisana na dysku. Układ MMU sam by ustawiał ten bit na wartość jeden w momencie przechwycenia na magistrali operacji zapisu danych w konkretnej stronicy. Warto dodać, że taki mechanizm usprawniłby też wymianę stron z plikiem wymiany na dysku, gdyż system mógłby wykrywać strony danych które uległy modyfikacji od momentu aktywowania programu i w momencie przełączania zadań nie powtarzać operacji zapisywania ich w pliku wymiany.

Aktualizacja bitu DIRTY
W momencie przechwycenia dozwolonej operacji zapisu do pamięci układ MMU automatycznie zmienia stan bitu DIRTY deskryptora strony, której operacja dotyczyła, na 1.

A teraz zaszalejmy już całkiem i dodajmy do naszego projektu... drugi mikroprocesor! Układ MMU musi zostać wyposażony w dwie odrębne magistrale lokalne (czyli połączone magistrale adresowa, danych i sterująca), po jednej dla każdego procesora, oraz w mechanizm arbitrażu wstrzymujący jeden procesor w momencie, gdy drugi wykonuje operacje na pamięci lub komunikuje się z urządzeniami. Przydać się może również rejestr zawierający numer procesora chwilowo monopolizującego cały komputer: dzięki niemu system operacyjny może odciąć wszystkie procesory poza jednym od dostępu do pamięci (będą wstrzymywane do momentu zwolnienia magistrali), wykonać sekwencję operacji o charakterze atomowym (niepodzielnym) i zwolnić blokadę, unikając w ten sposób niepożądanego współzawodnictwa z innymi procesorami.

W takim układzie wieloprocesorowym wykorzystanie pamięci staje się jeszcze bardziej ekonomiczne. Układ MMU musi co prawda dysponować po jednym zestawie deskryptorów stron na każdy podłączony procesor, jednak oba mikroprocesory mogą pracować równolegle i wykonywać całkowicie niezależnie ten sam lub zupełnie inne programy. Oba mogą też w swoich przestrzeniach adresowych używać różnych lub tych samych stron pamięci. Nawet system operacyjny może na obu znajdować się w różnym stanie konfiguracji pamięci, na przykład na procesorze pierwszym w dwóch ostatnich stronach pamięci o adresach E i F mogą znajdować się ramki 3 i 5 pamięci ROM, zaś drugi procesor w tych samych stronach pamięci wirtualnej może „widzieć” ramki 7 i 10 pamięci ROM, wykonując zupełnie inną funkcję systemu operacyjnego.

System wieloprocesorowy
System wieloprocesorowy. Każdy z mikroprocesorów może być chwilowo wstrzymany sygnałem WAIT (na przykład w momencie, gdy drugi dokonuje operacji na pamięci). Rejestr EXCLUSIVE może zawierać numer procesora chwilowo roszczącego sobie prawo do wyłącznego dostępu do pamięci i urządzeń. Co najważniejsze, każdy procesor dysponuje własną tabelą deskryptorów stron i może odwoływać się do tych samych lub różnych ramek z różnymi uprawnieniami, realizując różne procesy. Dla każdego procesora musi też być utrzymywany osobny rejestr stanu, bowiem jeden procesor może znajdować się w trybie uprzywilejowanym, a drugi w trybie użytkownika.

Osobną kwestią, słabo już związaną z tematyką tego artykułu, jest obsługa przerwań w systemie wieloprocesorowym. Jeżeli założymy, że jest to system symetrycznie wieloprocesorowy (SMP, ang. Symmetrical Multi-Processing), czyli taki, w którym wszystkie mikroprocesory są równoważne i nie mają z góry przydzielonego zakresu wykonywanych zadań, każdy z układów może odebrać przerwanie, jednak tylko jeden z nich powinien je obsłużyć. Wymaga to dodania oprócz układu MMU również programowalnego kontrolera przerwań (PIC, ang. Programmable Interrupt Controller), który we współpracy z systemem operacyjnym będzie rozdzielał przerwania pochodzące od różnych urządzeń między procesory starając się równomiernie obciążać je tym obowiązkiem.

Podsumowanie

Tak oto dotarliśmy do końca procesu projektowania, uzyskując mikrokomputer z kilkoma ośmiobitowymi procesorami korzystającymi ze wspólnej pamięci operacyjnej (stałej i o dostępie swobodnym) za pośrednictwem mechanizmu pamięci wirtualnej obsługującego stronicowanie, wymiatanie stron do pliku wymiany, wykrywanie modyfikacji danych w pamięci i ograniczanie praw dostępu do pamięci w zależności od trybu pracy procesora (zwykłego lub uprzywilejowanego).

Możesz się spytać: po co to wszystko? Nikt o zdrowych zmysłach nie będzie przecież dzisiaj budował komputera z tak prymitywnymi procesorami, nawet, jeżeli miałyby to być mikrokontrolery sterujące procesem produkcyjnym. Celem tego artykułu nie jest jednak zaprezentowanie gotowego rozwiązania sprzętowego na bazie archaicznego sprzętu, a pokazanie idei i zasad działania mechanizmu pamięci wirtualnej na konkretnym przykładzie. Pokazanie, że tak naprawdę mechanizm ten oparty jest o kilka bardzo prostych układów sprzętowych posługujących się głównie bogatym zestawem rejestrów programowanych bezpośrednio przez system operacyjny, od którego „sprytu” zależy, czy wykorzysta funkcje oferowane przez ten prosty sprzęt i czy zrobi to w najszerszym możliwym zakresie.

Nowoczesne mikroprocesory całą potrzebną elektronikę mają zintegrowaną w swoim wnętrzu. Deskryptory stron opisujące olbrzymie, wielogigabajtowe wirtualne przestrzenie adresowe przechowywane są nie w kilku rejestrach, a w pamięci fizycznej (zajmują jej często wiele megabajtów) i buforowane są dla szybszego dostępu w tablicy translacji wewnątrz procesora (TLB, ang. Translation Look-ahead Buffer). Możliwość wznawiania wykonywania rozkazu niemożliwego do wykonania z powodu braku potrzebnej stronicy pamięci pozwala na ładowanie brakujących stron z dysku w momencie odwołania się do nich, a nie „na zapas” w momencie aktywowania zadania. Z kolei duża szybkość pracy pozwala myśleć nie o prostym przełączaniu zadań za pomocą kombinacji klawiszy, a o regularnym przełączaniu ich w celu stworzenia iluzji jednoczesnej realizacji (wielozadaniowość przez wywłaszczenie, ang. preemptible multitasking). W prezentowanym projekcie świadomie zrezygnowałem z takiej funkcji, gdyż ośmiobitowe procesory pracujące z częstotliwością kilku megaherców więcej czasu spędzałyby na przełączaniu procesów niż na wykonywaniu ich kodu.

Najważniejsze, abyś zapamiętał, że pamięć wirtualna to prosty układ elektroniczny nie mający wiele wspólnego z dyskiem twardym i „udawaniem” pamięci za jego pomocą. Obsługę pamięci wirtualnej, a więc wiązania adresów używanych przez program z innymi adresami właściwych bloków większej pamięci fizycznej można dodać nawet do najprostszych mikroprocesorów, prawie magicznie zwiększając ich możliwości o funkcje dostępne wydawałoby się tylko w 32-bitowych i 64-bitowych „monstrach” znajdujących się dzisiaj w każdym komputerze domowym klasy PC. Właśnie takim prostym układom zawdzięczamy stabilną pracę nowoczesnych systemów operacyjnych, możliwość współdziałania różnych aplikacji ze sobą i wymieniania danych pomiędzy nimi oraz łatwe łączenie różnych modułów bibliotecznych w jeden duży program.


Procesory miałem co prawda 10 lat temu, ale nawet bardzo dobry wykładowca AGH dr Wiśniewski nie wyłożył trybu wirtualnego tak w prosty i zrozumiały sposób dla mnie ja Ty to zrobiłeś. :-)
Bardzo dobry artykuł. Czasem przychodzi mi na myśl, że ośmiobitowce mogłyby być znacznie mocniejsze, gdyby projektanci i programiści dysponowali obecnym doświadczeniem.
Mimo to rozwijanie ośmiobitowców nie miało już większego sensu ;) Jak widać z obecnych doświadczeń, nawet architektura 32-bitowa nie wystarcza do niektórych zastosowań. A szczególnie, gdy oprogramowanie jest nadęte i niezoptymalizowane.
"Nikt o zdrowych zmysłach nie będzie przecież dzisiaj budował komputera z tak prymitywnymi procesorami" – oj, znam paru takich, co są gotowi się porwać. :)