Virtual File System (VFS)

Mikulas Patocka

Vytvoreno 9.1.1999 pro seminar Linux.

Obsah

Co je VFS?

Virtual File System navzdory nazvu neni filesystem. Je to rozhrani, pres ktere Linux pristupuje k filesystemum. Je to soubor funkci, ktere musime napsat (napr. vytvoreni souboru, smazani souboru) a ktere muzeme pouzit (treba cteni bloku z disku), abychom meli ovladac filesystemu. Pisu o tom proto, ze jsem napsal vlastni ovladac filesystemu HPFS, co umi i zapisovat (najit to muzete tady, do kernlu se to asi v nejblizsi dobe nedostane). Psal jsem to proto, ze mam OS/2 a HPFS je jedina moznost (FAT - zoufale neefektivni na vetsim disku, FAT32, NTFS - vyplody firmy Micro$hit(r), EXT2 - umi to jen Linux, existuje sice i driver EXT2 pro OS/2, ale ten je tak bugovity, ze je nepouzitelny - urcite je snazsi psat ovladac do Linuxu nez opravovat ovladac pro OS/2).

Pokud si myslite, ze po precteni tohoto textu budete umet delat ovladace filesystemu, tak si to myslite spatne. Tento text muze slouzit jen jako doplnkova informace, nejlepsi dokumentace se vzdy da najit v /usr/src/linux/fs/*.c, /usr/src/linux/fs/*/*.c a /usr/src/linux/include/linux/*.h. Pokud najdete, ze v tomto dokumentu pisi bludy, tak se radsi podivejte do zdrojaku, jak je to spravne :-)

Programovani v jadre

Pokud budeme psat nejaky ovladac, je vyhodne ho psat jako modul jadra, ponevadz pak nemusime rebootovat pocitac po kazde zmene. Moduly se pisou tak, ze v programu napiseme dve funkce: int init_module() a void cleanup_module(). Modul zkompilujeme, ale nelinkujeme. Pri nahrati modulu jadro zavola funkci init_module, ta vrati 0 - uspech, jinak neuspech. Pred odstranenim modulu se zavola funkce cleanup_module(). K nahrati a odstraneni modulu muzeme pouzit prikazy insmod a rmmod.

V jadre samozrejme nemuzeme pouzivat funkce standartni libc. Zde uvadim nektere obecne funkce, ktere se nam budou hodit:

int printk(char *fmt, ...)
Jako printf. Na zacatku textu muzeme uvest prioritu <0> az <7> (viz linux/include/linux/kernel.h). 0 znamena nejvyssi prioritu (oops, panic), <7> nejnizsi. Vetsinou se nastaveni priority nepouziva a vse se pise s defaultni prioritou <4>. Normalne se zpravy z printk pisou na konsoli, jinak si to muzeme menit daemony klogd a syslogd.
void *kmalloc(size_t size, int pri)
Alokace pameti. Prvni parametr je zrejmy, druhy urcuje prioritu danou konstantou GFP_XXX. Vetsinou se tam dava GFP_KERNEL, GFP_ATOMIC kuprikladu znamena, ze se nebude swapovat - pouziva se v obsluze preruseni nebo jinde, kdyz se proces nesmi blocknout. Pamet alokovana pomoci GFP_KERNEL se neodswapovava (ale swapuje se uzivatelska pamet, kdyz neni misto).
void kfree(void *ptr)
Uvolni pamet. Pokud na to zapomeneme, dojde k takzvanemu memory leaku. Pamet se porad alokuje a alokuje, swapuje to cim dal vic a vic ... a pak uz clovek nevydrzi to cekani a resetuje to.
void schedule()
Prepne proces. V jadre je kooperativni multitasking (t.j. proces nemuze byt z vyssi moci prepnut, leda ze sam odevzda rizeni pomoci teto funkce). Neni moc duvodu tuto funkci volat. Kdyz clovek zkouma, jak funguji jine funkce tak narazi prave na to, ze se pomoci teto funkce preda rizeni jinemu procesu treba kdyz proces ceka na data z disku.
void sleep_on(struct wait_queue **p)
Zpusobi, ze proces ceka na fronte p dokud ho nekdo neprobudi, a pripadne prepne na jiny proces, ktery chce bezet.
void wake_up(struct wait_queue **p)
Probudi procesy cekajici na dane fronte.
void lock_kernel(), void unlock_kernel()
Na 2.1 jadrech zamykaji jadro pri SMP. Ve 2.0 se mohl do jadra dostat pouze jeden procesor, v 2.1 jich tam muze byt vic a tak pouzivaji tyto funkce na zamykani. Podivame-li se vsak na vetsinu funkci sys_open, sys_read..., uvidime na zacatku lock_kernel() a na konci unlock_kernel(), takze si 2.1 jadro moc nepomohlo... Do kodu na ovladani filesystemu se tedy v zadnem pripade dva procesory dostat nemohou.
Samozrejme, ze toto nejsou vsechny funkce jadra, uplny seznam ziskame prikazem:
grep '^[a-z_].*(.*);' `find /usr/src/linux -name "*.h"` | less

Bufferova cache

Sprava bufferu byla v Linuxu delana s cilem minimalizovat kopirovani dat. Treba v msdosu je bezne zavolat funkci na cteni dat a jako parametr ji predat adresu, kam ma data ulozit. V Linuxu to funguje jinak: zavola se funkce a ta vrati adresu, kde jsou data ulozena. Vyhoda je zrejma - data se nekopiruji. Ma to ovsem i nevyhodu - kdyz chceme mit nekolik bloku po sobe v pameti, stejne je musime kopirovat :-( Obejit to jde snad nejak pres page cache, hrozne se mi do toho nechce.

Tak, zde jsou funkce:

struct buffer_head *bread(kdev_t dev, int block, int size)
Nacte blok do bufferu nebo vrati pointer na buffer, co je uz v cachi. Adresu dat urcuje polozka bh_data struktury buffer_head. dev je cislo zarizeni, block cislo bloku, size je velikost bloku. Radsi tim nezkousejte cist z jednoho zarizeni bloky ruzne velikosti - se zlou se potazete.
void brelse(struct buffer_head *bh)
Uvolni buffer (t.j. snizi jeho pocitadlo; pokud je pocitadlo na nule, buffer muze byt odstranen z pameti podle algoritmu LRU). Buffery ziskane pomoci bread by se _mely_ uvolnovat, jinak se zaflakava pamet.
void mark_buffer_dirty(struct buffer_head *bh, int pri)
Pokud jsme do bufferu zapisovali, je treba pouzit tuto funkci, aby o tom Linux vedel. Casem se buffer prepise na disk. Parametr pri je 0 pro datove bloky a 1 pro systemove bloky (bitmapy, inody, adresare). Systemove bloky jsou zapisovany driv. Cas je mozno nastavit v daemonovi bdflush. Pokud tento daemon nebezi, bloky se nezapisuji (leda pri nedostatku pameti).
struct buffer_head *getblk(kdev_t dev, int block, int size)
Podobne jako bread, ale necte blok. Oblibena je konstrukce bh=getblk(...);mark_buffer_uptodate(bh);, coz se pouziva treba pri vytvareni souboru (byla by blbost data cist, kdyz je pak kompletne prepiseme).
struct buffer_head *get_hash_table(kdev_t dev, int block, int size)
Jako getblk, ale pokud blok neni v cachi, tak vrati NULL.
void bforget(struct buffer_head *bh)
Jako brelse, ale smaze buffer z cache. Pri delete souboru je napriklad vhodne smazat vsechny jeho buffery. bh=get_hash_table(...);bforget(bh);
void ll_rw_block(int rw, int nr, struct buffer_head * bh[])
Zpusobi fyzicke clteni nebo zapis bloku. Nepouzivat, sic bude zbytecne hrkat disk.

Operace na superbloku

Takze pokud napiseme filesystem, musime jadru nejakym zpusobem sdelit, ze jsme filesystem napsali. To ucinime funkci register_filesystem, jako parametr se ji predava struktura file_system_type, ktera ma polozky: nazev, priznaky (dava se tam vetsinou FS_REQUIRES_DEV jako ze to potrebuje zarizeni), funkce read_super a pointer na dalsi filesystem, ktery nastavime NULL. Kdyz si pak filesystem nekdo namountuje, tak se zavola funkce read_super, ktera jako parametr dostane castecne vyplneny superblok, volby filesystemu (-o v prikazu mount) a posledni parametr silent, ktery znamena, ze se nema rvat, kdyz se filesystem nepodari namountovat. Funkce read_super ma za cil vyplnit superblok a nacist root inodu. V superbloku se musi nastavit poloka s_op, aby ukazovala na struktutu super_operations. Tato struktura obsahuje seznam dalsich funkci pro spravu filesystemu:
void (*read_inode) (struct inode *)
Nacte inodu. Inoda ma vyplneno jen cislo a zarizeni a cilem teto funkce je nacist ostatni polozky. U neinodoveho filesystemu si musime rozmyslet, co bude inoda. Inoda by mela obsahovat informace jako velikost souboru, datum, prava pristupu. Inoda neobsahuje jmeno souboru. Inoda by nemela menit cislo (i kdyz i to je mozne, kdyz uz to jinak nejde). Treba na FAT je vhodne za cislo inody povazovat fyzicke umisteni adresarove polozky na disku.
void (*write_inode) (struct inode *)
Zapise inodu. Na 2.0 kernelch je volana kdyz posledni proces uvolni inodu, na 2.1 je volana asynchronne nekdy pozdeji. Na 2.1 jsou s touto funkci celkem problemy, protoze se z ni nesmi zamykat dalsi struktury. Ja jsem to vyresil tak, ze ji nepouzivam.
void (*put_inode) (struct inode *)
Provede se pri iput (viz nize).
void (*delete_inode) (struct inode *)
Provede se pri smazani souboru nebo adresare.
int (*notify_change) (struct dentry *, struct iattr *)
Zavola se pri zmene velikosti, vlastnika nebo prav pristupu inody. Muze vratit chybu, pokud si filesystem mysli, ze by takova zmena nemela nastat. Treba na MSDOS FS je vhodne pomoci teto funkce zmenu vlastnika zakazat. Jinak se misto pointeru na tuto funkci muze dat NULL, pak Linux sam kontroluje, zda je zmena legalni, a ovladac filesystemu se o to nemusi starat. K fyzickemu zapsani zmen na disk se tato funkce nepouziva (vyjimku tvori moje HPFS, ktere ma extremne hnusnou strukturu a kde to jinak nejde), zapis inody je treba napsat do write_inode.
void (*put_super) (struct super_block *)
Odmountovani filesystemu.
void (*write_super) (struct super_block *)
Syncnuti filesystemu.
int (*statfs) (struct super_block *, struct statfs *, int)
Informace o filesystemu (volne misto, volne inody apod...).
int (*remount_fs) (struct super_block *, int *, char *)
Remountovani (mount -o remount).
void (*clear_inode) (struct inode *)
Vola se pri uvolnovani inody z cache.
void (*umount_begin) (struct super_block *)
Vola se pri umount.

Inody

Bufferova cache neni jedina. Existuje jeste druha (a v 2.1.1xx jadrech i treti) cache inodova. Funkce struct inode *iget(struct super_block *sb, int ino) vrati inodu z cache (pokud tam uz je) nebo ji do cache prida a nacte ji pomoci read_inode. Funkce void iput(struct inode *) dekrementuje pocitadlo a pokud je nulove, inoda muze byt uvolnena. Pokud je inoda dirty (flag inode->i_dirt), zavola se write_inode. Na 2.0 jadrech se write_inode vola rovnou pri iput, na 2.1 se vola asynchrone nekdy pozdeji (pozor na to: zrovna vcera jsem opravil chybu, ze to zarvalo, pokud se write_inode zavolalo pri mazani souboru).

Dale zde mame funkce clear_inode - vymaze inodu, mark_inode_dirty - oznaci inodu dirty (pouze v 2.1.1xx), na 2.0 se musi napsat inode->i_dirt = 1.

Dentry cache

Jadra 2.1.1xx disponuji dalsi cachi - je to cache na adresarove polozky (dentry). Ke kazde adresarove inode je pridelena tato cache. Pri vyhledani souboru se jadro nejdrive podiva do teto cache, pokud tam jmeno najde, vrati rovnou inodu asociovanou s timto jmenem; pokud ho tam nenajde, tak zavola funkci lookup (viz nize). Pokud ma nas filesystem nejake podivnosti pri praci se jmeny souboru (treba je case-insensitive), meli bychom si napsat aspon dve funkce: compare_dentry a hash_dentry - porovnani dvou jmen a zahashovani jmena. Tyto funkce ulozime do struktury dentry_operations a na kazde dentry nastavime pointer na tuto strukturu (viz filesystem AFS nebo moje HPFS).

Operace na inodach

Aby nas filesystem fungoval, musime napsat nejake funkce na bezne operace se soubory a adresari. Pointery na tyto funkce dame do struktur inode_operations a file_operations. Tyto funkce zde tedy popisu. Popis je vzhledem k 2.1 kernlu:
int (*create) (struct inode *,struct dentry *,int)
Vytvoru soubor. Inoda adresar, ve kterem se ma vytvorit, v dentry je nazev polozky. Poledni parametr jsou prava pristupu. Vlastnika a grupu noveho souboru je treba zjistit z current->fsuid a current->fsgid. (current je promenna ukazujici na aktualni proces).
int (*lookup) (struct inode *,struct dentry *)
Vyhleda polozku dentry v adresari inode.
int (*link) (struct dentry *,struct inode *,struct dentry *)
Udela hard link. Nic vic o tom nevim, protoze na hpfs hardlinky nejsou :-)
int (*unlink) (struct inode *,struct dentry *)
Smaze soubor.
int (*symlink) (struct inode *,struct dentry *,const char *)
Udela symlink. Posledni parametr urcuje, kam ma symlink ukazovat, prostredni parametr je nazev symlinku.
int (*mkdir) (struct inode *,struct dentry *,int)
Vytvori adresar.
int (*rmdir) (struct inode *,struct dentry *)
Smaze adresar. Pokud je adresar neprazdny, tak musi vratit chybu.
int (*mknod) (struct inode *,struct dentry *,int,int)
Jako create, az na to, ze nevytvari soubor, ale device, pipu nebo socket.
int (*rename) (struct inode *, struct dentry *, struct inode *, struct dentry *)
Prejmenovani nebo presunuti souboru nebo adresare. Pozor! Tato funkce musi kontrolovat, zda nepresouvame adresar do sebe sama.
int (*readlink) (struct dentry *, char *,int)
Nacte obsah symlinky.
struct dentry * (*follow_link) (struct dentry *, struct dentry *, unsigned int)
Otevre soubor tam, kam symlika ukazuje.
int (*readpage) (struct file *, struct page *)
Provadi asynchroni cteni stranky souboru. Asynchroni znamena, ze funkce posle pozadavek na i/o, ale hned se vrati a _neceka_, az pozadavek dobehne a stranka je nactena. Pomoci teto funkce delaji jine funkce readahead a kdybyste ji naprogramovali synchronne, tak by to vyrazne zpomalilo.
int (*writepage) (struct file *, struct page *)
Zapis stranky.
int (*bmap) (struct inode *,int)
Tato funkce je dost dulezita. Vraci, z jakych bloku se soubor sklada. Prvni parametr je inoda souboru, druhy je cislo bloku v souboru. Funkce ma vratit cislo fyzickeho bloku na disku, kde se blok souboru nachazi. Pokud napiseme tuto funkci, muzeme se pak uz vyhnout psani nekterych jinych a pouzit misto toho generic funkce jadra. Napriklad je pak mozne uvest funkci generic_readpage, ktera je zase pouzivana funkcemi generic_file_read a generic_file_mmap. Pouziti techto generic funkci je vyhodne, protoze jsou dobre optimalizovany a maji readahead. Kdybychom si napsali vlastni read a mmap, jiste by nedosahly takovych kvalit, jako generic funkce.
void (*truncate) (struct inode *)
Useknuti souboru. V inode->i_size je uz nova delka, nase funkce soubor na tuto delku zkrati. Neni jasne, proc ma typ void. Co kdyby nastala chyba?
int (*permission) (struct inode *, int)
int (*smap) (struct inode *,int)
int (*updatepage) (struct file *, struct page *, const char *, unsigned long, unsigned int, int)
int (*revalidate) (struct dentry *)
Tohle nevim. Ja tam mam NULL a funguje mi to :-)

Operace na souborech

A ted operace na souborech. Kazda inoda ma pointer na struct file_operations, coz je tabulka nasledujicich funkci. (otazkou je, jaka je vyhoda, ze jsou v linuxu operace rozdeleny na operace na souboru a na inode...)
loff_t (*llseek) (struct file *, loff_t, int)
Seek ... vetsinou NULL, coz znamena defaultni seek. Muzeme si tam dat treba neco, co zacne readaheadit na zvolenem offsetu, ale neverim, ze to zpusobi vyrazne zrychleni.
ssize_t (*read) (struct file *, char *, size_t, loff_t *)
Cteni souboru. Je dobre delat cteni pomoci funkce generic_file_read, protoze ta ma readahead.
ssize_t (*write) (struct file *, const char *, size_t, loff_t *)
Zapis do souboru. Pri zapisu na ci za konec se soubor prodlouzi.
int (*readdir) (struct file *, void *, filldir_t)
Cteni adresare. Cte pouze nazvy adresarovych polozek, nic vic. Pozice odkud mame cist je v file->f_pos. Poprve je to nula, pak si tuto polozku sami menime jak chceme. filldir_t je typ funkce, ktera se zavola pro kazdou adresarovou polozku. Tato funkce navratovou hodnotou rekne, zda mame cist dalsi polozku nebo skoncit. Po precteni vsech polozek poprve vratime hned 0 (bez chyby) a po dalsim zavolani teprv chybu. V 2.0 kernlech je bug, ze nektere filesystemy (treba msdos) to tak nedelaji. Zpusobi to treba nefunkcnost prikazu diff -r. Velmi neprijemna vlastnost teto funkce je, ze musi byt schopna pokracovat cteni i kdyz mezi jednotlivymi volanimi doslo k vytvareni nebo mazani polozek v adresari :-( Pritom musi pokracovat presne od toho mista, kde se pri minulem volani skoncilo (jinak nefunguje treba mazani adresaru v Midnight Commanderu). Na MSDOS FS nebo EXT2 to necini problemy, na HPFS to cini problemy znacne. Jediny zpusob, jak to vyresit je vest si z inody seznam ukazatelu na jeji soubory a pri zmene adresare vsechny tyto pozice updatovat (coz je v pripade stromove struktury adresare, jakou ma HPFS celkem vzrusujici).
unsigned int (*poll) (struct file *, struct poll_table_struct *)
Funkce poll. Vetsinou se zde dava NULL.
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long)
Taky NULL.
int (*mmap) (struct file *, struct vm_area_struct *)
Mmapovani souboru. Muzeme sem dat generic_file_mmap. generic_file_mmap ma jednu znacnou nevyhodu, ze cte primo sektory do pameti a nenuluje prostor v poslednim sektoru za koncem souboru. Na filesystemech, ktere tam drzi nuly (treba ext2) to nevadi, ale na msdosu to uz vadi. Nektere programy (treba mkdep.c z kernelu) vesele segfaultuji, pokud za koncem mmapovaneho souboru nejsou nuly. Jinak je to taky trochu bezpecnostni dira; ja myslim, ze uzivatelsky proces by _nemel_ mit pristup k nahodnym neinicializovanym datum.
int (*open) (struct inode *, struct file *)
Chceme delat nejakou cinnost pri otvirani souboru? Pokud ne, tak sem dame NULL.
int (*flush) (struct file *)
Flushnuti bufferu. Vetsinou NULL.
int (*release) (struct inode *, struct file *)
Zavreni souboru. Pokud pri nem nechceme nic delat, tak NULL.
int (*fsync) (struct file *, struct dentry *)
Fsync. Dame sem bud file_fsync, nebo jinou vlastorucne napsanou funkci. Pozor, pokud sem dame NULL, tak bude volani sys_fsync vracet chybu - to pusobi problemy treba v Netscapu (nejdou ukladat bookmarky).
int (*fasync) (int, struct file *, int)
Zase nejaky sync. NULL tam davat.
int (*check_media_change) (kdev_t dev)
Kontrola zmeny media v ovladaci filesystemu? No nevim...
int (*revalidate) (kdev_t dev)
Ani nevim, kdy je to volano - najdete si to sami pomoci grepu.
int (*lock) (struct file *, int, struct file_lock *)
Pri zamykani vetsinou taky nemusime delat nejake specialni akce.

Zamykani

Linux ma, jak znamo, multitasking. Takze se nam muze do naseho driveru dostat vic procesu. Pokud jeden proces treba chce cist z disku, tak se rizeni preda jinemu procesu, a ten muze klidne taky vlezt do nejake funkce VFS. Aby se dva procesy nesnazili soucasne modifikovat nejakou strukturu, je treba ji zamknout. Globalni veci (superblok, bitmapy) se zamykaji pomoci uz napsanych funkci lock_super a unlock_super. Pokud chceme zamykat i inody, musime si vetsinou vytvorit zamky vlastni (ty, co tam jsou jsou casto nepouzitelne - treba lookup se nekdy vola s otevrenym i->i_sem a nekdy se zavrenym - plati pro kernly 2.0, jak je to v 2.1 nevim). Ty zamky, co uz v kernlu jsou, nedovoli, aby vic procesu soucasne modifikovalo adresar nebo zapisovalo do souboru, ale bohuzel uz dovoli, aby se adresar nebo soubor soucasne modifikoval i cetl, coz je casto nezadouci, protoze se mohou nacist nesmyslne hodnoty. Zamek vytvorime bud tak, ze ke strukture, kterou chceme zamknout pridame dve polozky
int lock;
struct wait_queue queue;
Zamkneme pak pomoci while (lock) sleep_on(&queue); lock=1; a odemkneme lock=0; wake_up(&queue); Jak jste jiste postrehli, tento zamek selze na viceprocesorovych systemech. To ale nevadi, protoze do VFS se muze vzdy dostat jen jeden procesor. I funkce lock_super a unlock_super jsou napsany timto zpusobem. Podivame-li se na funkce sys_open, sys_read a dalsi, zjistime, ze na jejich zacatku je vzdy lock_kernel a na konci unlock_kernel.
Dalsi zpusob je pouziti semaforu (ty jsou SMP-safe). Do struktury pridame polozku struct semaphore sem = MUTEX; Zamkneme pomoci down(&sem) a odemkneme up(&sem).
Kdyz neco dvakrat zamkneme (treba down(&sem);down(&sem);), nastane takzvany deadlock - proces se navzdy zablokuje, nejde killnout (ani -9) a musime rebootovat. Stejne tak to muze spadnout, kdyz se dva procesy pokousi zamcit dve struktury v opacnem poradi - tj. proces 1 vykona down(&inode1->sem);down(&inode2->sem); a proces 2 down(&inode2->sem);down(&inode1->sem); Jiste si domyslite, co se stane. Ja jsem to obesel tak, ze inody zamykam podle cisla vzestupne.

Bugy

Na zaver zde uvedu nekolik chyb, co jsem v Linuxu nasel (ve verzi 2.0.33). Nemusime vsak kvuli nim podlehat hysterii, kuprikladu OS/2 ma bugy vetsi.

Struktura HPFS

Ponevadz EXT2 ci FAT filesystemy jiste kazdy zna, uvedu zde strukturu mene znameho filesystemu HPFS, na kterem bezi OS/2. Detailnejsi informace - viz hpfs.h. Bloky maji velikost 512b, ale nektere (bitmapy a dnody) maji 2048b. V sektoru 16 je superblok a v sektoru 17 spareblok. V nich jsou obecne informace, jake v superblocich zpravidla byvaji. Na disku jsou rozmisteny bitmapy - vetsinou tak, aby byly blizko datum, ale neni to nutne. Kazdy soubor nebo adresar ma fnodu (neco jako inoda na EXT2), ale na rozdil od inod muzou byt fnody kdekoliv na disku. Fnoda obsahuje v pripade souboru alokacni informace nebo (pokud ma soubor vic jak 8 fragmentu) pointer na alokacni b-strom. Na rozdil od EXT2 zde neni seznam vsech bloku souboru nalezicich, ale seznam ve tvaru zacatek-delka. V pripade adresare fnoda ukazuje na korenovou dnodu adresare, ktera obsahuje direnty (polozky adresare), kdyz zacne byt dirent moc, tak se seskupi nekolik dnod v b-strom setrideny podle jmen souboru. Vyhledani souboru v adresari ma tedy logaritmickou slozitost vzhledem k poctu polozek adresare. Direnty navic obsahuji i takove informace, jako delku, datum apod, takze kdyz dame v OS/2 'ls -la', staci pouze precist adresar a nemusi se tolik seekovat jako na EXT2. HPFS ma uprostred disku vyhrazeno nekolik MB na dnody - to snizuje (lepe receno ma snizovat, ale diky dementnimu umistovani fnod se tak nedeje) seekovani. Kdyz je tento prostor plny tak se dnody umistuji kdekoli. Kazda fnoda navic muze obsahovat extended attributy - dvojice (jmeno, hodnota). Kdokoli si tam muze zapsat, co chce. Vesinou jsou EA v fnode, kdyz jsou velke, tak se presunou do samostatneho sektoru, nebo jejich umisteni urcuje strom anod.

                         +------------+
                         | superblock |
                         +------------+
                               |
                               |
                               |
Korenovy adresar filesystemu:  V
                         +------------+     +---------------------+
                         | root fnode |---->| extended attributes |
                         +------------+     +---------------------+
                               |
                               |
                               V
                         +------------+  Polozky v jednom adresari jsou ve
                         | root dnode |  stromu, takze vyhledani souboru se
                         +------------+  deje v logaritmickem case
                          /    |    \
                         /     |     \
                        /      |      \
                       V       V       V
                    +-----+ +-----+ +-----+
                    |dnode| |dnode| |dnode|
                    +-----+ +-----+ +-----+
                     / | |   | | |   | | |
                    /                  |
                   /         ...       |  
   Podadresar:    V                    V  Soubor:
      +-----------------+  +--+  +------------+     +---------------------+
      | directory fnode |->|ea|  | file fnode |---->| extended attributes |
      +-----------------+  +--+  +------------+     +---------------------+
               |                   |  ...   |
               |                   V        V
               V              +-------+  +-------+
        +------------+        | anode |  | anode |
        | root dnode |        +-------+  +-------+
        +------------+         | | | |    | | | |
      a zase dalsi strom       V V V V    V V V V
                            strom anod, v jehoz listech je informace o
                            fyzickem umisteni bloku na disku; listy jsou
                            trojice (offset v souboru, sektor na disku, pocet),
                            tedy ne primy seznam bloku, jako na EXT2

Kdyz se nad tim zamyslime, zjistime, ze HPFS ma oproti EXT2 nektere dobre vlastnosti, ktere zpusobuji urychleni: Ma smysl se pokouset EXT2 o tyto featury vylepsit (nebo napsat nove HP-EXT3-FS)? Obavam se, ze asi ne. :-( Sotvaktery uzivatel bude riskovat ztratu dat na disku za drobne urychleni.