Memory management -=-=-=-=-=-=-=-=-=- The human mind ordinarily operates at only ten percent of its capacity -- the rest is overhead for the operating system. Správa paměti vychází z toho, že v rozumném OS je neúnosné, aby jednotlivé procesy si navzájem videly do paměti (nebo do paměti jádra) a nebo dokonce tam mohli něco měnit. Proto OS většinou pro proces vytváří iluzi, že na počítači běží sám. Proces potom má k dispozici například následující rozložení paměti: začátek adresovatelného prostoru konec prostoru v v +-------------------------+-------------------------------------------------+ | (read only) | (read write) (nic) (read write) | | Kód procesu | halda ------> díra <----------stack| | | | +-------------------------+-------------------------------------------------+ ^ ^ brk stack pointer Proces tedy je v paměti sám. Má ji k dispozici celou (tedy 2GB na Intelu). Halda a zásobník většinou rostou proti sobě a tak jsou v principu neomezené. Veprostřed je díra. Při zápisu nebo čtení proces spadne, protože paměť tam prostě není. Pokud chce zvětšit haldu nebo zásobnik, posune stack pointer, nebo brk a OS mu kus paměti přidá (tu napřed pečlive snuluje, aby proces náhodou nedostal nějaká tajná data). Virtuální paměť Pokud dojde fyzická ram, většina OS začne swapovat na disk. To je celkem jednoduché - OS si vede statistiky o používanosti jednotlivých stránek paměti, vybere ty méně používané a uloží je na disk. Pokud proces chce na stránku přistupovat, OS mu ji opět nahraje z disku. Ale i zde je co měnit. Samotné algoritmy na určení co swapovat ven jsou složité. Klasický UNIX dělal paging (odložení celého procesu na disk) a swaping (odložení jenom některých stránek). Linux dělá jenom swaping. Existuje mnoho algoritmů na určení přístupových patternů, tedy nějakých informací o tom, jak který proces paměť využívá apod. Základní rozdíl ale je mezi synchroním a asycnroním swapováním. Vstup a výstup většinou funguje tak, že jádro nečeká na pomalý disk až práci dodělá a mezi tím může normálně souštět procesy. Pokud je ale swapování synchroní (aktivované v tom okamžiku, kdy proces chce nějakou paměť, ale jádro ji nemá), proces nemůže fungovat. Pokud není žádný jiný proces schopný běhu, nezbývá než čekat. Staré jádra Linuxu měly synchroní swapování. Často se stávalo, že došla paměť, disk řádil jak divý, nic nešlo ale systém hrdě hlásil, že má 99% volného času. A byla to pravda, jenom nebyl schopen tento čas nijak využít. Nyní v systému běží swapovací daemon. Ten se aktivuje v situaci, kdy počet volných stránek klesne pod určené minimum a začne swapovat na pozadí. Bežící procesy tedy čekat nemusí. Horší situace je při nahrávání stránek zpět. Ale i zde lze situaci zlepšit pomocí přístupových patternů a readaheadu. Účinou metodou je i swap cache. Ta funguje vpodstatě opačně, než klasická cache. Pokud se nějaká stránka nahraje zpět z disku do paměti, na disku se nezruší až do té doby, dokud ji program nezmění. Pokud se mezitím ma znova odswapovad, neukládá se a použije se stará pozice. Další šikovné rozšíření je to, že v případě, že v paměti je kopie nějakého souboru na disku (třeba spustitelný kód programu), swapování ven se provede tak, že se paměť prostě zahodí a při swapování zpět se zase nahraje z disku. I toto má Linux implementované. Zpožděné přidělování paměti Často se stává, že proces si sice řekne pro paměť ale nakonec ji nepotřebuje. I to ale lze vyřešit. Pokud proces zažádá o paměť pomocí sbrk, paměť nedostane, pouze systém si někde poznačí, že ji v budoucnosti dostat má. A v případě, že proces na paměť začne přistupovat, paměť se mu přidělí. A tak se často stává, že procesy v paměti zabírají několikanásobně méně místa, než si o sobě myslí. Podobný systém lze použít při vznikání procesu. V UNIXu vzniká každý proces pomocí volání fork. To proces rozdělí na dva identické. Pokud chcete sputit jiný program, napřed proces forkem rozdělíte na dva a potom jeden z nich pomocí volání exec spustí vlastní program (exec proste vymění kód procesu - po zkončení programu se už stary program nevrání). To způsobovalo hodně zpomalení. Například pokud máte desetimegový program, co se rozhodne vypsat adresář, napřed se 10MB paměti zdvojí a pak se vymění za malý ls. To rozhodně není nejlepší nápad. Jedno řešení je zavédst volání spawn (jako v DOSu, Windows apod.), které prostě sputí program bez nutnosti forku. Jiné řešení použili ve SystemuV, kde zavedli vfork - což je fork, který nerozdvojuje paměť a tak tedy oba procesy chvíli paměť sdílí. Nejlepší řešení ale je to, že při forku paměť fyzicky nerozdvojíte, jenom označíte, že se v budoucnosti paměť má rozdvojit. A tak oba procesy paměť sdílí. Ale v okamžiku, kdy jeden do paměti zapíše, vyvolá se přerušení a OS danou stránku (pouze 4KB) rozdvojí. A tak se rozdvojí opravdu jenom to co je nezbytně nutné a fork je opravdu rychlý. Sdílení paměti Sdílení paměti je šikovný prostředek pro komunikaci. Například kreslící program může pustit nějaký filtr, který bude přistupovat přímo do obrázku v paměti kreslícího programu. Jiné části paměti kreslícího programu mít přístupné nebude. Toto lze velmi jednodušše udělat přes mapování stránek. Sdílená paměť má ale ještě jedno zajímavé využití. Pokud v systému běží více kopíí jednoho programu (což je velmi časté například u interpretru příkazů), mohou sdílet kód. To je také důvod, proč spustitelný kód v UNIXu je read only. Sdílené knihovny versus dynamicky linkované Bežně program používá mnoho knihoven. Ty jsou celkem velké a hlavně u malých programů často větší než program sám. Zalinkovávání knihoven přímo do programu (jako v DOSu) je tedy zbytečné mrháním místem na disku. Proto vznikly DLL knihovny. Ty jsou ve speciálním souboru a teprve při startu programu se zalinkují. To ušetří místo na disku ale zpomalí start programu (zalinkovávání není nejjednodušší). Aby se knihovny nemusely složitě linkovat, většinou se kompilují tak, aby byly nezávislé na pozici v paměti. Navíc mají tabulku odskoků, přes kterou skáčou na zalinkované funkce. Při startu programu se potom všechny knihovny nahrajou do paměti a udělá se resolving - vyplní se tabulky. Potom není nutné patchovat přímo kód knihoven a je možně (za cenu docela maláho zpomalení) sdílet knihovny v paměti mezi programy. Jiné řešení jsou klasické sdílené knihovny. Ty vyházejí z jednoduchého nápadu. V paměti programu se vynechá místo například mezi zásobníkem a koncem adresovatelného prostoru na knihovny. Každá knihovna se potom zkompiluje pro nějakou pozici. Tyto knihovny se potom přidají do programu po zpuštění a program je může normálně volat tak, jako kdyby s nimi byl slinkován. Toto má výhodu v tom, že start programu je mnohem rychlejší (není nutné dělat žádné linkování). Podobný mechanizmus používají staré a.out programy v Linuxu a dalších OS. Přináší to ale potíže s tím, že když zkompilujete dvě knihovny pro stejné místo v paměti, kolidují spolu a není možné je použít zároveň. Také to přináší zbytečné plýtvání adresovatelným prostorem. Pokud uděláte novou verzi knihovny, adresy se změní a je nutné všechny programy překompilovat. Formát ELF v Linuxu to proto řeší ještě jinak. Přívá stejný mechanizmus jako DLL knihovny ale nedělá resolving. Používa tzv. late-binding. Při startu programu se žádný resolving nedělá. Všechny odkazy v tabulce se vyplní tak, aby skákaly na jednu funkci (dinamický linker) Pokud program nějakou funkci zavolá, skočí se do linkeru. Ten zjistí, jakou funkci se program pokouší spustit, najde patřičnou sdílenou knihovnu (ani knihovny se nezavádí na začátku ale až v případě prvního použití) a nahradí odskok zavoláním správné funkce. Podruhé se už dynamický linker nevolá a zavolá se rovnou daná funkce. Start je mnohem rychleší, neresolvují se zbytečně všechny funkce, jenom ty potřebné a navíc, pokud program sice používá nějakou knihovnu, která na disku není, spadne až v době, kdy ji poprvé zavolá. To se stát nemusí v případě, že to je nějaký driver apod. Toto řešení se ukazuje jako nejpoužitelnější kompromis mezi rychlostí a flexibilitou. Linkování je stále velmi rychlé a přitou je možné bezpečně měnit verze knihoven. Page demand loading Další problém je zavádění programu. Bežný postup, kdy se program prostě zavede do paměti a spustí se ukazuje neefektivní. Programy často nevyužijí celý svůj kód (třeba obsahují ovladač, který není vůbec třeba) a musí čekat dokud nejsou zavedeni do paměti. To lze obejít tím, že se zavede pouze začátek programu a ten se zpustí. Pokud program skočí do části, která se zatím nenahrála, dohraje se. To způsobí, že se nahrají jen ty části programu, které jsou třeba. Na druhou stranu to ale přináší zbytečné seekování po disku, protože program se většinou neprovádí lineárně. Tomu ale docela účině zabraňuje readahead, který čte stránky dopředu. Protože běží asynchroně protgram se dokonce může nahrávat v době, kdy se už provádí. To že všechny tyto techniky jsou dohromady poměrně účiné snad ukáže příklad: PID USER PRI NI SIZE RSS SHARE STAT LIB %CPU %MEM CTIME COMMAND 217 root 0 0 480 160 120 S 0 0.0 1.0 0:00 /bin/login Ten říká, že proces login sice požádal celkově o 480KB paměti ale dostal pouze 160 a sdíli 120KB s jinými programy. Mmap Možná si teď říkáte, že memorymanagement je velmi komplikovaný. Častečně to je pravda, ale všechny tyto funkce lze velmi elegantně implementovat pomocí funkce mmap. Ta navíc přináší možnost, aby programy použily tyto schopnosti i pro jiné účely. Funkce mmap umožňuje "namapovat" libovolnou část souboru na libovolnou pozici v paměti s libovolnými přistupovými právy (read, write, exec a kombinace). Je také možné mapovat "anonymní" stránky - tedy novou paměť a mapovat fyzickou paměť (přes "soubor" /dev/mem, který fyzickou paměť obsahuje). Navíc je možné sdílet mapování (pokud si více procesů namapuje stejný soubor, bude pouze jednou v paměti a mohou tak tedy komunikovat). Pomocí mmap lze implementovat všechny popsané věci - spustitelný soubor se napřed namapuje na začátek paměti, knihovny se zase mapují doprostřed, posouvání brk je vlastně zvětšování anonymní namapované paměti. Navíc ale procesy můžou svoji paměť rozdělit jinak (to využívá například DOSemu), pokud na to mají práva, mohou si namapovat videoram kamkoliv do svého adresovatelného prostoru, nadělat si do své paměti díry, spřístupnit svůj kód pro zápis apod. Mmap je právě jedna z věcí, které se velmi těžko implementují na mikrojádrových systémech. Většina popsaných služeb potřebuje mnoho spolupráce mezi jednotlivýmí částmi systému, která většinou není v mikrojádrovém OS možná. výheň