Špecifiká vývoja ovládačov 2
Tento článok nadväzuje na Pár faktov a mýtov o vývoji ovládačov a prináša ďalšiu várku zaujímavostí a špecifík vývoja ovládačov na platforme Windows. Tentoraz sa zameriame najmä na pamäť.
Procesy a vlákna
Pri programovaní bežné aplikácie vieme, že tá má svoj vlastný virtuálny adresný priestor, do ktorého jej ostatné aplikácie nemôžu priamo siahať. V žargóne operačného systému je tento adresný priestor (a ďalšie prostriedky, ako sú otvorené súbory) zakomponovaný do entity s názvom proces. Proces ďalej obsahuje vlákna (v základe jedno) zodpovedná za vykonávanie kódu umiestnenom v adresnom priestore. Ako vývojári aplikácie vieme, že nám patrí jeden proces, v ktorom môžeme mať jedno alebo viac vlákien vykonávajúcich nami požadované operácie.
Ako bolo povedané na konci minulého článku, ovládače fungujú veľmi podobne ako DLL knižnice. Nie sú izolované vo vlastnom virtuálnom adresnom priestore, ale všetky zdieľajú jeden priestor, ktorý je navyše namapovaný do adresových priestorov všetkých procesov. Bezpečnostné mechanizmy procesora (konkrétne jednotky zodpovedné za stránkovanie) zaisťujú, že aplikácia, hoci o nej vedia, nemôžu do tejto oblasti vykonávať žiadne prístupy (čítanie, zápis, vykonávanie inštrukcií). Opačne žiadne také tvrdenie neplatí - ovládače môžu pristupovať do celého adresového priestoru procesu, v ktorého kontexte sa práve nachádzajú.
Tým sa dostávame k otázke, v kontexte akého procesu (popr. Akého vlákna) vlastne ovládače vykonávajú svoj kód. Odpoveď je šalamúnsky: záleží na tom, kto potrebuje ich služieb. Napríklad ovládač monitorujúci prístupy do registra je volaný vždy v kontexte vlákna, ktoré sa snažia danú operáciu s registrom vykonať. Volanie funkcie, ktorú ovládač podstrčí systému, aby ho informoval o prácu s registrom, je súčasťou každej z mnohých druhov registrových operácií. Podobné pravidlá platia pre ovládače dozerajúci na otváranie a vytváranie súborov na disku.
Nie vždy je však situácia takto jednoduchá. Ovládače často spracúvajú prichádzajúce požiadavky asynchrónne - odovzdávajú je na spracovanie svojim vlastným vláknam, ktorý môžu komunikovať s ďalšími komponentmi jadra. Pre tieto komponenty potom často nie je možné dopátrať sa toho, v kontexte akého vlákna alebo procesu bol daný požiadavka vytvorený.
Nejasnosť kontexte procesu a vlákna vedie k programovaniu ovládačov tak, aby na týchto vlastnostiach nezáležalo. Vzhľadom k tomu, že oblasť vyhradená jadru sa v každom adresnom priestore nachádza na rovnakom mieste, ovládače iba nesmie siahať mimo tejto oblasti, pokiaľ si nie sú absolútne isté, v akom procese sa práve nachádzajú. A aj potom je rozumné pracovať len s dátami, ktorá im daný proces ukáže (odovzdaním adresy a veľkosti príslušného buffera), pretože štruktúru celého adresového priestoru nepoznajú (nevie napríklad, kde sa nachádza pamäť s alokovanými dátami).
Umiestnenie pamäti jadra vždy na rovnakom mieste adresového priestoru procesu ukazuje nasledujúci obrázok. Adresové priestory Prieskumníka Windows (Explorer.exe), Firefoxu (firefox.exe) a Poznámkový blok (notepad.exe) zdieľa svoju hornú časť (adresy 0x80000000-0xffffffff). Dolná časť svojho priestoru má každý proces sám pre seba.
Hoci by sa teda mohlo zdať, že prístup do ľubovoľného adresového priestoru (pretože ovládače naozaj môžu medzi adresovým priestory prechádzať podľa svojich prianí) znamená pre autorov ovládačov obrovskú moc, prakticky tejto výhody príliš nevyužívajú. Najmä ak hovoríme o ovládačoch, ktorých cieľom je stabilný beh na čo najväčšom množstve verzií Windows. Zvyčajne je totiž kontexty procesu a vlákna vôbec nezaujímajú.
Stránkovanie a nestránkovú pamäť
Podobne ako každá aplikácia, aj jadro disponuje haldou, z ktorej môžu ovládače alokovať pamäť, prípadne ju tam vracať, pokiaľ ju už nepotrebujú. Hlavný rozdiel spočíva vo fakte, že ovládače môžu alokovať z rôznych háld (poolov), pričom zásadné sú nasledujúce dva:
- Nestránkovú halda (nonpaged pool) obsahuje bloky, ktoré sa vždy budú nachádzať vo fyzickej pamäti, nikdy neodcestují na disk do stránkovacieho súboru. Najdôležitejšou vlastnosťou takto alokovanej pamäte nie je to, že jej čítanie a zápisy budú vždy rýchle (nebude treba ju hľadať na disku), ale jej prítomnosť aj za okolností, kedy sa do stránkovacieho súboru pozrieť nemožno. Príkladom takýchto okolností sú obslužné rutiny prerušenia, ktorý musí prebehnúť vždy čo možno najrýchlejšie a nemali by byť príliš zložité. Nestránkovú pamäť by mala byť využívaná iba v oprávnených prípadoch, pretože jej množstvo je obmedzené veľkosťou RAM počítača. Nepodieľa sa na ilúziu budované mechanizmy virtuálnej pamäte.
- Stránkovanie halda (paged pool) disponuje naopak bloky,
ktoré môžu do stránkovacieho súboru odcestovať kedykoľvek. V podstate sa
jedná o ekvivalent haldy bežných aplikácií spravované funkciami
malloc()
afree()
.
Aj aplikácia môže jadro systému inštruovať, aby s určitou
časťou jej adresového priestoru zaobchádzalo ako z nestránkovú haldou
(tzn. Neodďaľoval jej obsah na disk). K tomuto účelu slúži funkcia
VirtualLock()
.
Obsluha výnimiek
Vyvolávanie a ošetrovanie výnimiek určite patrí medzi obľúbené postupy nielen v C ++, ale aj vo vyšších programovacích jazykoch (Java, C #, Python...). Pre priaznivcov tohto štýlu programovania predstavuje prostredie jadra Windows veľké sklamanie - mechanizmus výnimiek nie je príliš podporovaný, rozhodne nie dostatočne pre C ++, v ktorom inak ovládača programovať možno.
Ako demonštruje nasledujúci kód, podpora výnimiek predsa len nie je
nulová. Namiesto slov try
a except
sa používajú
alternatívy __try
a __except
. V prípade, že chceme
výnimku vyhodiť, zavoláme funkciu ExRaisestatus()
.
__try { // . . . Normální běh . . . __except (EXCEPTION_EXECUTE_HANDLER) { // . . . Obsluha výjimky . . . }
Až na niekoľko funkcií je API jadra založené čisto na chybových kódoch - či určitá funkcia uspela, sa ovládač dozvie z jej návratovej hodnoty. Snáď jedinú výnimku tvorí prípad, keď ovládač reaguje na požiadavku aplikácie a potrebuje preniesť jej špecifikovaná dáta do pamäte jadra.
Keďže aplikácie nemôže čítať ani zapisovať do pamäti jadra, odovzdáva potrebné dáta (napríklad meno súboru, ktorý sa má otvoriť) tak, že jadru oznámi ich adresu a dĺžku. Jadro (alebo príslušný ovládač) potom musí z tejto adresy dáta prekopírovať mimo dosahu aplikácie, prípadne je zabezpečiť iným spôsobom. Aplikácia totiž môže v čase, keď jadro s jej dátami pracuje:
- meniť ich obsah,
- meniť oprávnenia príslušných pamäťových stránok (a tak zamedziť zápisu),
- pamäť uvoľniť, takže adresa odovzdaná jadru prestane byť platná.
Našťastie všetky tieto škaredé prípady je možné riešiť obsluhou
výnimiek, ktorá v tomto prípade funguje, ako má. Ak sa počas kopírovania
do pamäte jadra daný buffer stane neplatným, je vyvolaná výnimka
STATUS_ACCESS_VIOLATION
, ktorú jadro (alebo ovládač) zachytí a
obslúži. Ak sa však ovládač pokúsi pristúpiť na neplatnú adresu v
pamäti jadra, ani ten najlepší obsluha výnimiek ho nezachráni pred modrou
obrazovkou smrti.
Neexistuje žiadny spôsob, ako ovládač môže zistiť, či je
určitá adresa v pamäti jadra platná. Môže sa maximálne dozvedieť, či
prístup na takú adresu vyvolá výpadok stránky (funkcia
MmIsAddressValid()
). Výpadok stránky ale nemusí znamenať, že
je daná adresa neplatná; príslušná oblasť pamäte sa môže nachádzať v
stránkovacom súboru na disku. Tu uvedená funkcia navyše nezaručuje, či
daná adresa nevyvolá výpadok stránky hneď potom, čo vráti riadenie
volajúcemu.