Innehåll

Virtuellt minne

Virtuellt minne
Virtuellt minne, om MMU:n
Virtuellt minne, om swap

 


03/01-01 | Janne Johansson | icepic@64bits.se


Virtuellt minne

If you can't make it, fake it.

Virtuellt minne, swapping, växlingsfil med mera är ord man ofta råkar ut för på sitt OS vad det gäller RAM och minne, men vad betyder de?

Hela idén med virtuellt minne kommer sig av en krets, en del av CPU:n som kallas MMU. Det står för Memory Management Unit och är kortfattat en bit kisel som i realtid kan ligga och lyssna på addressbussens signaler från CPU:n och agera på olika sätt beroende på vad som far förbi, och i vissa fall skriva om alla adresser on-the-fly och i andra fall helt stoppa instruktionen som försökte göra en operation mot en viss minnesaddress.

Det kanske inte verkar så nyttigt, men det har en stor mängd olika användningsområden. Ett av dem är minnesskydd, då man med MMU:n kan skrivskydda väl valda delar av internminnet, så att inga program får skriva där. Det är ett enormt stort steg för att få multiuser-säkerhet på en dator, eftersom man utan minnesskydd inte kan hindra någon från att förändra andra personers programkod. Kan man inte hindra det så är det i princip omöjligt att hindra en buse från att ta över maskinen om han kör på den.

Men det är inte bara för säkerhet och minnesskydd som den är bra, den visade sig även kunna användas för att fejka minne med hjälp av disk och få långt bättre minnesutnyttjande i allmänhet.

Det gäller att kunna skilja på just virtuellt minne och swap, eftersom de ofta blandas ihop. Jag kommer gå igenom skillnaderna ganska djupt, men det är ok om du inte är med på allt. Mot slutet blir det mindre råa tekniska detaljer, så sluta inte läsa bara för att det verkar invecklat i mitten av artikeln.

För att förklara hur virtuellt minne går till måste vi gå in lite djupare i minneshantering och mer specifikt om hur programladdning fungerar.

Minneshantering med C64 och Amiga

Om vi tar ett enkelt exempel på hur programladdning går till från C64:an först:

Alla program som laddas in på normalt vis hamnar på addressen $0800 och framåt. Således kan de skrivas så att hopp i programkoden hoppar till $0854 om det är där destinationen kommer hamna. Att det är just där vet man redan när man skriver eller kompilerar programmet, eftersom man kan räkna från $0800 och fram till destinationen, och på så sätt få fram addressen.

Det här är ju enkelt och bra, men det har en extrem nackdel. Man kan bara ha ett program i taget. Även om man enkelt kan ladda in ett annat program i minnet så måste man lägga det på en annan address, och då kommer hoppen och referenserna i det andra programmet att peka helt galet och hoppa in i det första programmet eftersom alla program förväntar sig att hamna på samma startpunkt. För C64:an var det inte ett problem, men det ställdes inte riktigt samma krav på den som på ett modernt OS.

Tar man ett lite mer avancerat OS än C64:an, som AmigaOS, så har det ett lite annorlunda trick för att kunna ha fler program inladdade samtidigt. Under AmigaOS gör man som så att man låter alla program tro att de kommer hamna på address 0 och framåt, även om man vet att det inte är sant. För att då kunna ladda in programmet vart som helst i minnet ser man till att göra en lista som medföljer programmet som visar på vilka ställen som adresserna är "fel", och när OS:et har bestämt sig för vart den ska lägga programmet, adderar den på startaddressen på de platser där det behövs. Så om programmet har referenser till hopp eller data på vad den tror är address $2000, $4000 och $6000 och programmet laddas in på addressen $10000 så kommer $10000 att adderas på dessa tre referenser och de blir då $12000, $14000 och $16000 respektive.

Så långt låter ju allt bra. Man kan göra program som görs om runtime för att passa den address de verkligen hamnar på. Men om man då laddar ett $10000 bytes långt program (64k) på address $10000, ett likadant på $20000 (nästa lediga plats) och ett tredje på $30000 och man sen avslutar programmet på $20000 (program 2) så kommer man få en lucka i minnet som bara går att använda för program som är $10000 bytes eller mindre. Man har alltså fragmenterat minnet. Men till skillnad från en floppy eller HD kan man inte bara defragmentera genom att flytta runt datat, eftersom de referenser som gjorts har "satt sig" i programmet. Inte bara de referenser som följde med programmet vid inladdning, utan även referenser som har uppkommit vid körning. En reboot blir det enda som kan ordna upp det problemet om man inte lyckas avsluta de två programmen runt om.

En till skillnad mellan AmigaOS/C64 och system med virtuellt minne (och minnesskydd) är att de skyddade systemen inte tillåter att programmen ändrar sig i själva koden, s.k. självmodifierande kod. Ibland kan självmodifierande kod vara praktiskt, men oftast är det ett otyg som dessutom gör debuggning än svårare.

Om man bestämmer sig för att programmen inte ska tillåtas vara självmodifierande så vinner man en hel del olika saker. Den första vinsten är att man kan låta MMU:n göra de delar av programmet som är kod skrivskyddade. På så sätt kan man undvika och upptäcka alla buggar som uppkommer av att programmen felaktigt skriver och läser från minne som de inte alls har med att göra. På C64 och AmigaOS har man ingen som helst möjlighet att upptäcka det förrän det man skrivit över behövs av ett program och det i sin tur kraschar eller fryser.

Man kan också dela samma kod mellan flera instanser av samma program, så om man startar 10 stycken kommandotolkar samtidigt så kan de allihopa köra på exakt samma kodsnuttar, även om det data de använder sig av måste vara unika mellan de 10 körningarna. Det ger ju en extra vinst i tid bara av att inte behöva ladda in koden igen, bortsett från att det spar minne.

Det finns ytterligare fler fördelar, men de kräver att vi går in ännu mer på hur en MMU arbetar.

MMU i närbild

En MMU har som tidigare sagt möjligheten att dels skydda minne genom att läs- och/eller skrivskydda olika delar av minnet. Dessa delar kallas ofta "page" och är normalt sett mellan 4 och 8k. Den precisionen är oftast alldeles tillräcklig för att få en balans mellan effektivitet och minnesåtgång. MMU:n har en tabell över alla använda "pages" och vilka regler som gäller för dessa.

Ett enkelt exempel på hur vissa delar blir mycket enklare när man använder den funktionen hos en MMU är den stack som alla program har. Det är en sorts temporärarea i minnet som programmet kan bruka sig av för att lägga undan data som inte behöver existera under hela programkörningen. När man kör komplexa program eller gör rekursiva anrop så kommer stacken att växa. (eller rättare sagt, mer av den kommer att förbrukas) På C64 var stacken delad mellan alla program och på modiga 256 bytes, vilket är katastrofalt lite med dagens mått mätt, men det verkade räcka för allt som man använde den till just då.

Under AmigaOS fick man en stack per program åtminstone, men storleken var nästintill fast. Normalt sett fick man 4k, men det gick att göra en global inställning i OS:et så att alla program startade efter ändringen fick den nya större storleken.

Problemet där var att man inte visste om den började ta slut. Skulle den ta slut kommer programmet definitivt att bete sig skumt och riskerade att ta med sig hela systemet ner. Inte bra. Man kunde ju inte heller sätta alla stackar till 100k eftersom det skulle gå åt 100k*(antal program), och det är inte särskilt effektivt.

Har man däremot en MMU så kan man ge programmet en eller flera "pages" till sin stack och sen låta den page som är direkt efter dessa tillhöra dig, men ändå vara skriv- och lässkyddad. Då kommer man få en "fault" när stacken går över gränsen, men det blir en kontrollerad krasch. Det OS:et gör då är att ta bort skrivskyddet, flytta den skyddade page:n ytterligare ett steg bort, och sen återstarta programmet vid den instruktion man fångade. På det sättet kan alla program ha mer eller mindre exakt så mycket stack som de behöver, utan att användaren måste rota runt, eller att det går åt extremt med minne.

En annan fördel man får vad det gäller programladdning är att man kan ladda programmet allteftersom. Om man har en jätteeditor som är på 20 Meg, och startar den med "fet-editor --version" för att bara få ut versionsnummret skulle man bli gråhårig av att vänta på att 20 Meg laddas in för att programmet ska skriva ut en rad som säger att den har version 1.2.3 och sen avsluta.

Det man kan göra är att man laddar in en eller ett par pages från starten av filen, och sen kör man igång binären. Allteftersom den kör igenom programmet så kommer man komma in i den area som hittils inte laddats in, varpå OS:et laddar en eller ett par pages till, och så fortsätter man tills man antingen har laddat hela programmet, eller tills man avslutat det. På så sätt kan ett 20 Meg stort program bara behöva ladda in ett tiotal kilobyte från disk och ändå köras igång, skriva ut sin versionssträng och avsluta som vanligt. Man behöver inte ens skriva programmen på nåt speciellt sätt för att det ska bli så här, det bara funkar av sig självt.

Den andra funktionen som en MMU har är att skriva om adresser.

Det gör att man kan få tillbaka de fördelar och enkelhet som C64:ans modell hade, samtidigt som man undviker de problem som AmigaOS hade med minnesfragmentering.

Det man gör med MMU:n är att man låter alla program skrivas för att köras från samma address, men sen laddar man in dem någon annanstans och via MMU:n mappar om de fysiska adresserna till de virtuella. Alla program får varsin virtuell rymd som gäller just när de kör och när OS:et växlar mellan olika program så byts MMU:ns omvandlingstabell ut också.

Oftast låter man en eller två pages vara oanvända i starten av det virtuella minnet (av debugskäl bl.a) så att alla program laddas in som om de låg på t.ex address $2000. Genom hela programmet så sitter det minnesreferenser som baserar sig på att programmet faktiskt ligger där. Men OS:et laddar i själva verket in programmet där det just nu finns plats i det fysiska minnet och lägger in en omvandligsregel i MMU:n som ordnar allt.

Är programmet inladdat på den fysiska addressen $123000 och vill hoppa till START+$1000 (vilket programmet tror är $3000) så kommer MMU:n skriva om den addressen på addressbussen till $124000 och så kommer hoppet hamna rätt iallafall.

Det verkar ju inte initialt som om det vore nån större vinst med det här, mer än att man kan lura programmen lite. Men finessen kommer när man inser att varje page som mappas från en fysisk address till en virtuell inte behöver följa samma regler som programmet. Ett programm som är $2000 bytes långt tror att det är laddat mellan $2000 till $4000 och kräver då att minnet ligger i rad. Det motsvarar då 2 fysiska pages i MMU:n., men dessa 2 pages behöver inte vara i rad alls.


  Nästa sida »

 




03/01-01 | Janne Johansson | icepic@64bits.se

Diskutera denna artikeln i vårt forum!