A memóriakezelés áttekintése

A 32 bites x86 architektúrájú processzorok memória-kezelésének lehetőségei két részre oszthatóak: szegmentálás és lapozás.
 

  • A szegmentálás (segmention) elősegíti elkülöníteni az egyedi kód, adat és verem modulokat, részeket, így lehetőség van arra, hogy több program (avagy taszk) fusson egy processzoron anélkül, hogy egymással érintkezniük kellene vagy lehetne.

  • A szegmentálást nem kell külön bekapcsolni, ezt mindig használjuk védett módban, következéskép nincs státusz bit, mellyel le lehetne tiltani a szegmentálást. Csupán programozási módszerrel lehet elérni, hogy elnyomjuk a szegmentálásos modell működését.
  • A lapozás (paging) azt a lehetőséget biztosítja számunkra, hogy úgy hozzuk létre programunk, operációs rendszerünk környezetét, hogy a fizikai memória szűkössége esetén csak annyi információt tároljunk a fizikai memóriában, amennyi szükséges az adott program, rendszer futásához. Így a program azon memória-részei, ún. lapjai, amelyek szükségesek a futáshoz, a fizikai memóriában maradnak, a fölösleges, illetve az átmenetileg nem szükségesek pedig egy átmeneti tárolóra (pl. a háttértárolóra) kerülnek. Ha időközben az eltárolt lapok szükségessé válnának, a processzor támogatása révén meglehetősen gyorsasan visszalapozhatóak a fizikai memóriába. Ezt a memória-kezelést virtuális memória-kezelésnek is nevezzük, hisz több memóriát kezelhetünk, mint amennyi fizikailag jelen van gépünkben. A lapozást továbbá a különböző taszkok elkülönítésére is használjuk.

  • A lapozás nem automatikusan kapcsolódik be védett módban, ha szükségünk van rá, bekapcsolhatjuk megfelelő előkészületek után.

3.1. ábra: Szegmentálás és lapozás

Amint az ábra is mutatja a szegmentálás lehetőséget nyújt a processzor címezhető területének (lineáris címtartomány - linear address space) kisebb védett részekre, szegmensekre való osztásához. A szegmensek használhatóak kód, adat, verem vagy rendszer adatstruktúrák tárolására (úgymint a már tárgyalt TSS és LDT). Ha több program (taszk) fut a processzoron lehetőség van arra, hogy minden program a saját szegmens készletével dolgozzon. A processzor szigorúan ellenőrzi a szegmensek elhelyezkedését, méretét, nehogy egyik program a másik szegmensébe írhasson. Továbbá a szegmenseket elkülöníthetjük típusuk szerint. Adott típusú szegmensen csak adott művelet, parancs végezhető. Pl. kódszegmenst csak olvasni lehet írni nem.
Minden szegmens a processzor lineáris címtartományában helyezkedik el. Ahhoz, hogy egy adott szegmensben egy adott bájtot elérjünk logikai címet (ezt far-pointer-nek, távoli mutatónak is nevezik) kell használnunk. A logikai cím a fenti ábrán látható módon két részből a szegmens kiválasztóból és az ofszetből áll. A szegmens kiválasztó (pontosabban a leíró, amiről már szót ejtettünk) egy egyedi azonosító a szegmens számára. A szegmens kiválasztó végül is egy index a leíró táblázat (pl. a GDT) egy elemére a kívánt szegmens leíróra. Így a szegmensleíró meghatározza a szegmens méretét, a hozzáférési jogokat, a szegmens privilégium szintjét, a típusát és az első kezdő bájtot a lineáris címtartományon (ezt a szegmens kezdőcímének nevezzük). Az ofszet határozza meg a szegmensen belül az elérni kívánt bájt helyét. (Az ofszet simán hozzáadódik az előbb elmondott szegmens kezdőcímhez, így alkotva lineáris címet a processzor lineáris címtartományában.)
Ha a lapozás nem használt akkor a lineáris cím közvetlenül fizikai címet jelent a processzor számára (a lineáris cím konverzió nélkül fordítódik fizikaivá). A fizikai címtartomány szélességét, nagyságát a processzor címsín szélessége határozza meg. (Amekkora a legnagyobb megcímezhető bájt => címsínek száma. Ezt iPentium-ig 232-en, illetve iP6 családtól már 236-on.)
Részint a multitaszking miatt van szükség több memóriára, mint amennyi fizikailag elhelyezkedik a gépünkben, így a lineáris címtartományt, a memóriát úgymond "virtualizáljuk". (Fent már szóltunk erről.) A processzor virtuális memória-kezelésnek csak egy részét, a logikai kezelés feltételeit valósítja meg a lapozásos memóriakezelésen keresztül (A többit az operációs rendszernek kell elkészíteni).
A lapozás "virtuális memória" környezetet biztosít, ahol a nagy lineáris címtartomány bizonyos nagyságú fizikai memóriával (RAM, ROM) és háttértárolóval van megoldva. Az operációs rendszer fenntart egy lap címtárat (page-directory) és csomó lap táblázatot (page-table), amik egybefoglalják a lapokat (pages).
Amikor egy program (vagy taszk) hozzá akar férni egy címhez a lineáris címtartományban, akkor a processzor a lapcímtárat és lap táblázatot használja, hogy átfordítsa a lineáris címet a fizikaivá és teljesíthesse a kívánt műveletet - írást, vagy olvasást - a kívánt memória címen.
Ha az elérni kívánt címet tartalmazó lap nincs éppen bent a fizikai memóriában, akkor a processzor megszakítja a program (taszk) futását egy page-fault kivétel generálásával. Ekkor az operációs rendszer lekezeli a laphiba (page-fault) kivételt úgy, hogy beolvassa a fizikai memóriába a háttértárolóról a kívánt lapot. Ezek után már a program folytathatja a futását és elérheti a kívánt címet.
Akkor van a lapozásos memória-kezelés, a virtuális memória-kezelés helyesen megvalósítva az operációs rendszerben, ha az adott programnak (és a felhasználónak) teljesen észrevehetetlen annak létezése. 16 bites (pl. valós) programokat is be lehet lapozni (anélkül, hogy ők tudhatnának róla, hisz a 16 bites processzorokban még nincs is implementálva ez a lehetőség) akkor, ha virtuális-8086-os módban futnak. Ez még nagyobb biztonságot ad, mind az operációs rendszernek és a programnak.
 

Szegmensek használata

A 32 bites x86-os processzor architektúra által támogatott szegmentálásos mechanizmus eredménye képen sok fajta rendszer építhető fel. Kezdve a sík memória-modelltől (Basic Flat Model), mely csak minimális védelmet nyújt, egészen a sok, multiszegmens (Multi Segment) modellig, mely meglehetősen összetett, robosztus alkalmazások és operációs rendszerek memória modelljére használatos nagy megbízhatósága által. Az alábbiakban a lehetséges szegmentált memória modelleket tekintjük működés, felhasználhatóság, illetve egyéb szempontok alapján.
 

A sík modell

Ez a legegyszerűbb memória modell, mely az operációs rendszer és programok számára egy 'nem szegmentált', folytonos címtartományt biztosít. Ezért e modellnek legtalálóbb neve a "nem szegmentált modell" lehet. Ez a modell nagymértékben (szinte teljesen) elrejti a szegmentálást a rendszerprogramozók elől, így szinte megkerülhető a szegmentálás. Ez természetesen csak a szegmentálás "eltompítása", nem kikapcsolása!
Ahhoz, hogy ezt a memória modellt használjuk legalább két szegmens leírót kell létrehoznunk: egyet, mely a kódszegmensre és egyet, mely az adatszegmensre mutat. (Lásd az alábbi ábrát.) Ahogy mindkét szegmens az egész lineáris címtartományra van leképezve, úgy mindkét szegmens leírónak azonosan 0 a kezdő címe, és szegmens határuk (a szegmens bájtban mért hossza) is azonosan 4 GBájt. Annak ellenére, hogy a szegmens határt 4 GBájtra állítjuk, ha olyan címet címezünk meg, ahol nincs fizikailag memória, nem kapunk általános védelmi-hibát (#GP).
A ROM (EPROM) általában a fizikai címtartomány tetején helyezkedik el, mert a processzor reszet után a működését az FFFF_FFF0h címen kezdi. A RAM (DRAM) a címtartomány alsó részén helyezkedik el, mert reszet után a DS 0-ra van állítva. (Az utolsó adatok nyílván a valós címzésre értendőek, de ezekről pontosabban, bővebben beszélünk a processzor inicializálása témakörben.)

3.2. ábra: Sík modell

Védett sík modell

A védett sík modell hasonló a nem szegmentált modellhez annyi különbséggel, hogy a szegmens határ csak azon címtartományokat foglalja magába, ahol fizikailag is létezik memória. (Lásd az alábbi ábrát.) Általános védelmi hibát (#GP - general-protection error) generál a processzor amennyiben nem létező memória címtartományhoz kívánunk hozzáférni. Ez a modell minimális hardver védelmet ad bizonyos program hibák ellen.

3.3. ábra: Védett sík modell

Ezt a modellt sokkal összetettebb módon is használhatjuk, pl. ha a lapozást (bővebben: 3.6-os alfejezet) párosítjuk ezen védett sík modellel. Így lehetővé válik, pl. a felhasználói kód és a supervisor (kernel) kód elválasztása. Négy szegmenst kell definiálni: kód- és adatszegmenst a felhasználói privilégium szintre (3-as privilégium szint, a legalacsonyabb jogokkal) és úgyszintén kód- illetve adatszegmenst a supervisor privilégium szintre (kernel, az operációs rendszer magja, 0-s, legprivilegizáltabb privilégium szint). Általában ezek a szegmensek átlapolódnak egymáson a 0-s címtől kezdődően a lineáris címtartományban. Így ez a sima szegmentálásos modell egyszerű lapozással párosítva megvédheti az operációs rendszert a felhasználói programoktól úgy, hogy minden taszknak külön lapozó egységet (lapcímtárat) hoz létre (Mivel a TSS-ben a CR3-at átadjuk, így minden taszk saját lapcímtárral rendelkezhet.). Ezáltal nem csak az operációs rendszer kernel-jét védjük meg a felhasználói programoktól, hanem a taszkokat is megvédjük egymástól, így az egyik taszk összeomlása nem vezethet más taszkok illetve az operációs rendszer összeomlásához. Ezt a memória modellt (a védett sík modellt illetve a lapozást) sok fejlett operációs rendszer használja.
 

Multiszegmens modell

A multiszegmens modell, mint az alábbi ábra is mutatja teljesen kihasználja a szegmentálás lehetőségeit megvalósítva a teljes hardveres védelmét a kód-, illetve adatstruktúrák és a programok, illetve taszkok számára. Itt minden programnak (vagy taszknak) saját szegmensleíró táblázata van a saját szegmenseivel. A szegmensek vagy kizárólagosan csak az őt birtokló program által, vagy programok (taszkok) között megosztva érhető el. Az összes szegmenshez, illetve a futtató környezethez (~operációs rendszer) való hozzáférést a processzor kontrollálja.

3.4. ábra: Multiszegmens modell

A hozzáférés vizsgálat (access check) nem csak a szegmensen kívül történő illegális műveleteket (pl. írás a szegmensen kívül, a szegmens határt átlépve) hanem a szegmensen belüli nem támogatott műveleteket is letiltja. Pl. a kódszegmensek csak olvasható szegmensek, így letiltottuk (maga a processzor) az ezekbe történő írást, ha írni kívánnánk a kódszegmensbe kivétel generálódik. A hozzáférési jogok megállapítása és létrehozása úgyszintén bizonyos védelmi szinteket biztosít a különböző szintű programoknak, taszkoknak, pl. megvédi az operációs rendszert az illetéktelen alkalmazás hozzáférésétől.
 

A lapozás és a szegmentálás

Lapozást bármelyik fent említett szegmens modellel együtt használhatjuk. A processzor által létrehozott lapozó mechanizmus a lineáris címtartományt lapkora osztja (ezekbe helyezhetjük el szegmenseinket). Ezt jól mutatja be a 3.1-es ábra. Majd ezek a lineáris címtartománybeli lapok a fizikai memóriába lesznek betérképezve (mappolva). A lapozásos mechanizmus rengeteg lap szintű védelmi lehetőséget hordoz magában, amit vagy a szegmentálás szegmens védelmével, avagy a helyett használhatunk. Pl. két szintű supervisor kódot is létrehozhatunk a másodikat is megvédve a felhasználói programoktól.
 

A fizikai címtartomány

Védett módban a 32 bites x86-os processzorok 4 GBájtnyi fizikai címtartományt biztosítanak illetve ennyi érhető el. (Ez 232-en bájt.) Ez a legnagyobb cím, ami a processzor címbuszán megcímezhető. Ez egy sima, egybefüggő (fizikai szinten nem szegmentált) címtartomány egészen a 0 kezdő címtől, az FFFFFFFFh címig. Ez a fizikai címtartomány állhat csak olvasható, olvasható / írható memóriából, illetve memóriába ágyazott I/O-ból is.
Az Intel P6 családtól kezdődően a processzorok támogatják a fizikai cím kiterjesztést a 236-on bájtra avagy 64 GBájtra. Ez a processzor 36 bites címsínének köszönhető, így a legnagyobb címezhető bájt címe: FFFFFFFFFh. Ezt a szolgáltatást a CR4-ben található "fizikai címzés kiterjeszése" (PAE - Phisical Address Extension) flag (CR4, 5. bit) egyre állításával állíthatjuk be.
 

A logikai és a lineáris címek

Védett módban, a rendszer szintű műveleteknél a processzor két szintű címfordítást használ, hogy megkapja a fizikai címet: logikai címfordítás és lineáris címtartomány lapozása.
Még a legkisebb mértékű szegmentálás alkalmazásakor is a processzor által elérhető címtartomány minden bájtja logikai címmel érhető el. A logikai cím áll egy 16 bites szegmens kiválasztóból (szelektor) és egy 32 bites ofszetből, amint az alábbi ábra is mutatja. A szegmens kiválasztó megadja azt a szegmenst, amelyikben a keresett bájt található, és az ofszet pedig a szegmens kezdő címéhez képest a relatív elhelyezkedését, a címét.
A processzor minden logikai címet lineáris címmé alakit. A lineáris cím egy 32 bites cím a processzor lineáris címtartományában. Ahogy a fizikai címtartomány is, úgy a lineáris címtartomány is egybefüggő (nem szegmentált), 232 bájt cím tartománnyal, kezdve 0-tól egészen az FFFFFFFFh-ig. A lineáris címtartomány (nem úgy, mint a logikai) tartalmazza az összes szegmenst illetve rendszer táblázatot (leíró táblák, stb.), amit a processzor definiált.
A logikai címből lineáris címre történő címfordítás esetén a processzor az alábbiakat teszi:

  1. A szegmens kiválasztó által megadott index (a kiválasztó alsó 3 bitjét kivéve az egész maga az index) segítségével a processzor kiolvassa a GDT vagy LDT-ben elhelyezkedő kívánt leírót (a szegmens leíró tartalmát). Ez csak akkor történik meg, ha új kiválasztót (szelektort) töltünk egy szegmens regiszterbe. Ekkor töltődik a szegmens regiszter rejtett részébe a leíró információi.
  2. Megvizsgálja a szegmens leírót, hogy meggyőződjön a hozzáférési jogokról, a szegmens méretéről hogy a kívánt cím a szegmensen belül esik -e, illetve a fent említett jogok alapján elérhető -e.
  3. A szegmens kezdő címéhez (ami a szegmens leíróban található) adja az ofszetet így képezvén a lineáris címet.

3.5. ábra: Címfordítás: logikai címből, lineáris címmé

Amennyiben a lapozás nem használt a processzor a lineáris címet közvetlenül fizikai címként használja (avagy a processzor címbuszán a "lineáris cím megy ki"). Amennyiben használt a lapozó egység, akkor a lineáris címből egy második címfordítás szükséges a fizikai cím kiszámításához.
 

Szegmens kiválasztók

A szegmens kiválasztó egy 16 bites azonosító minden szegmens számára. (3.6-os ábra) A szegmens regiszterben megadandó szegmens kiválasztó nem közvetlenül a szegmensre mutat (ahogy valós címzésű módban), hanem egy szegmens leíróra, amelyik definiálja a szegmenst. A szegmens kiválasztó az alábbi részekből áll:

  • Index: (a 3. bittől a 15-ig.) Ez az index rész választja ki a szegmens leírót a GDT vagy LDT-ből. Mivel 13 biten tároljuk az indexet így 213-on, 8192 db indexünk pontosabban leírónk lehet. A processzor az index értékét 8-cal szorozza (mivel ennyi bájtból áll egy leíró hossza) és hozzáadja a GDT vagy LDT kezdőcíméhez, ami a GDTR vagy LDTR regiszterben található. (A nyolccal való szorzást nem kell fizikailag a processzornak elvégeznie, hiszen hárommal balra van eltolva az index értéke, ami nyolccal való szorzást jelent. Ugyanis a 0-2 biteken más, nem az index bitei vannak. Így csak az alsó 3 bitet kell kinullázni és hozzá is lehet adni a GDT illetve LDT kezdőcíméhez.)
  • Tábla kijelölő (TI - Table Indicator) flag (2. bit): Kiválasztja, mely leíró táblázatot kívánjuk használni:
- ha üres - nulla ezen bit, akkor a GDT-ből választódik ki a leíró az index által,
- ha bekapcsolt - 1 ezen bit, akkor pedig az aktuális LDT-re értendő az index.

3.6. ábra: Szegmens kiválasztó

  • Igényelt privilégium szint (RPL - Requested Privilege Level) (a 0. és 1 bitek): Meghatározza a kiválasztók privilégium szintjét. Már láttuk, hogy a privilégium szint 0 és 3 között lehet, ahol a 0 a legnagyobb privilégiummal rendelkező szint (legprivilegizáltabb). Az RPL és CPL-ről bővebben, a védelmi szintek tárgyalásakor beszélünk.
Az első leíró a GDT-ben nem használt a processzor által. Az a szegmens kiválasztó, mely ezen 0. leíróra mutat a GDT-ben (0 index, TI = 0) úgynevezett "null szegmens kiválasztóként" szokták használni (null referencia). A processzor nem generál kivételt, ha a szegmens regiszterbe null kiválasztót töltünk (kivéve a CS és SS-t). Abban az esetben viszont már generál kivételt, ha null kiválasztóval feltöltött szegmens regiszterrel akarunk a memóriához hozzáférni. Null kiválasztót használhatunk a nem használt szegmens regiszterek feltöltésére, a processzor is reszet után ezzel tölti fel a szegmens regisztereket. A CS vagy SS regiszterbe null kiválasztó töltése általános-védelmi hibát (#GP) eredményez.
A szegmens kiválasztók elérhetőek a felhasználói programok számára úgy, mint egy pointer része, de értéküket reprezentáló leíróikat csak a link editorok vagy link betöltök - a kernel része - módosíthatják. (A felhasználói szintű programok nem.)
 
Szegmens regiszterek

Hogy csökkentsük a címfordításra elhasznált időt és a kód bonyolultságát a 32 bites x86-os architektúrájú processzor 6 szegmens kiválasztó regisztert tartalmaz. (3.7-es ábra) Mindegyik szegmens regiszternek speciális feladata van. Így legalább az alábbi 3 regiszter használt mindegyik program által: kódszegmens (CS), adatszegmens (DS) és veremszegmens (SS). Ezen regisztereknek a rájuk jellemző kiválasztókat kell tartalmazniuk, melyeknek érvényesnek kell lenniük. A processzor további három adatszegmens regisztert támogat, melyekkel további szegmensek választhatóak ki az aktuális program vagy taszk számára, ezek: ES, FS, GS.
Ha egy program egy szegmenst el akar érni, akkor először is az egyik szegmens regiszterbe valós kiválasztót, a kívánt szegmens kiválasztóját kell töltenie. Így annak ellenére, hogy több ezer (2 * 8192) szegmenst definiálhatunk egyszerre lévén, hogy csak 6 szegmens regiszter van csak 6-ot használhatunk. Ha az adott haton kívül továbbiakat kell elérnünk akkor a szegmens regiszterekben a kiválasztókat váltogatva tehetjük meg.

3.7. ábra: Szegmens regiszterek

Minden szegmens regiszternek van egy "látható" és egy "rejtett" része (erről már szót ejtettünk). A rejtett szegmens regiszter részt néha "leíró cache-nek" (descriptor cache) vagy "árnyék regiszternek" (shadow register) is nevezik. Amikor egy szegmens kiválasztót a szegmens regiszter "látható" részébe töltünk (ezek a fent elmondott CS, … GS regiszterek), a processzor automatikusan betölti a szegmens regiszter "rejtett részébe" a szegmens kezdő címét, a szegmens méretét (szegmens határ) és a szegmenshez a hozzáférési jogokat. Ez az információ eltárolás teszi lehetővé a processzor számára, hogy plusz órajel ciklus nélkül történhessen meg a memória hozzáféréskor a cím transzformáció. (Gondoljunk bele, hogy az LDT-n keresztüli címzéshez a cache nélkül + 3 órajel ciklus kellene.) Multiprocesszoros rendszerekben, amikor ugyanahhoz a leíró táblához kívánnak hozzáférni, az operációs rendszernek kell módosítania, újratöltenie a szegmens regisztert, amikor a leíró tábla módosul. Ha ez nem valósul meg, akkor a szegmens regiszter rejtett része nem frissül fel, így a processzor a leíró cache-ben (a szegmens regiszter rejtett részében) a régi adatokkal dolgozik, annak ellenére, hogy a memóriában, a leíró táblázatban már új adatok vannak.
Két fajta utasítás típus létezik, hogy a szegmens regiszterbe kiválasztót adjunk meg:

  1. Direkt utasítás, úgy mint a: MOV, POP, LDS, LES, LSS, LGS és LFG utasítások. Ezek explicit módod férnek a szegmens regiszterekhez.
  2. Implicit módon az alábbi távoli mutatóval rendelkező (far pointer-es) utasítások használhatóak: CALL, JMP és RET utasítások illetve az IRET, INTn, INO, INT3 utasítások. Ezek az utasítások a CS és néha más regiszterek értékét is megváltoztatják működésük elemeként.
A MOV utasítással a szegmens kiválasztók (a szelektor látható) részét általános regiszterben is eltárolhatjuk.