Laborator 8 - Drivere de sisteme de fișiere (Linux) partea 1

Obiectivele laboratorului

  • Dobândirea de cunoștințe legate de VFS și înțelegerea conceptelor de 'inode', 'dentry', 'file', superbloc și block de date.
  • Înțelegerea procesului de montare a unui sistem de fișiere în cadrul VFS.
  • Cunoștințe legate de diversele suporturi posibile pentru sisteme de fișiere și înțelegerea diferențelor dintre driverele pentru sisteme de fișiere cu suport fizic (pe disc) și sisteme de fișiere fără suport fizic.

Cuvinte cheie

  • VFS
  • superbloc
  • inode
  • dentry
  • file
  • mount/kill_sb/sb
  • buffer_head

Materiale ajutătoare

Virtual Filesystem

Virtual Filesystem (cunoscut și sub prescurtarea VFS) este o componentă a nucleului care se ocupă de tratarea tuturor apelurilor de sistem legate de fișiere și sisteme de fișiere. VFS este o interfață generică între utilizator și un sistem de fișiere particular. Acest lucru simplifică implementarea sistemelor de fișiere și oferă o integrare facilă a mai multor sisteme de fișiere. În acest fel, implementarea unui sistem de fișiere este realizată prin folosirea API-ului pus la dispoziție de VFS iar părțile generice de comunicație cu dispozitivul hardware și subsistemul de I/O sunt rezolvate de VFS.

Din punct de vedere funcțional sistemele de fișiere pot fi grupate în:

  • sisteme de fișiere pentru disc (ext3, ext4, xfs, fat, ntfs, etc.)
  • sisteme de fișiere pentru rețea (nfs, smbfs/cifs, ncp, etc.)
  • sisteme de fișiere virtuale (procfs, sysfs, sockfs, pipefs, etc.)

O instanță de nucleu Linux va folosi VFS pentru ierarhia (de tip arbore) de directoare și fișiere. Un nou sistem de fișiere va fi adăugat ca un subarbore a VFS prin operațiunea de montare. Fiecare sistem de fișiere este de obicei montat de pe mediul pentru care a fost construit (de pe un dispozitiv de tip bloc, de pe rețea etc.). În particular însă, VFS-ul poate folosi drept dispozitiv de tip bloc virtual un fișier normal, deci se pot monta sisteme de fișiere pentru disc peste fișiere normale. Astfel, se pot crea stive de sisteme de fișiere.

Ideea de bază a VFS-ului este de a oferi un singur model de fișier, care să poată reprezenta fișierele din orice sistem de fișiere. Driver-ul de sistem de fișiere este responsabil pentru aducerea la numitorul comun. Astfel se poate crea o singură structură de directoare care conține întreg sistemul. Va exista un sistem de fișiere care va fi rădăcina, restul fiind montate în diverse directoare ale acestuia.

Modelul general al sistemului de fișiere

Modelul general al sistemului de fișiere, la care trebuie să se reducă orice sistem de fișiere implementat, este format din mai multe entități cu rol bine definit: superbloc, inode, file și dentry. Aceste entități sunt metadatele sistemului de fișiere (conțin informații despre date sau despre alte metadate).

Entitățile modelului interacționează cu ajutorul unor subsisteme ale VFS sau ale nucleului: cache-ul de dentry-uri, cache-ul de inode-uri, buffer cache-ul. Fiecare entitate este tratată ca un obiect: are o structură de date asociată și un pointer la o tabelă de metode. Inducerea unui comportament particular al fiecărei componente este făcut prin înlocuirea metodelor asociate.

superbloc

Superblocul stochează informațiile necesare unui sistem de fișiere montat:

  • zonele de inode-uri, de blocuri
  • dimensiunea blocului sistemului de fișiere
  • lungimea maximă a numelor fișierelor
  • dimensiunea maximă a fișierelor
  • locația inode-ului rădăcină

Localizare:

  • În cazul sistemelor de fișiere pentru discuri, superblocul are un corespondent în primul bloc al acestora (Filesystem Control Block).
  • În VFS, toate superbloc-urile sistemelor de fișiere sunt reținute într-o listă de structuri struct super_block și metodele în struct super_operations

inode

Inode-ul (index node) menține informații despre un fișier în sensul general (abstractizare): fișier obișnuit (regular file), director, fișier special (pipe, fifo), dispozitiv de tip bloc, dispozitiv de tip caracter, link, sau orice poate fi abstractizat ca fișier.

Un inode menține informații precum:

  • tipul fișierului;
  • dimensiunea fișierului;
  • drepturile de acces;
  • timpul de acces sau modificare;
  • poziționarea datelor pe disc (pointeri către blocurile de pe disc ce conțin date).

În general, inode-ul nu deține numele fișierului. Numele este reținut de entitatea dentry. Astfel, un inode poate avea mai multe nume (hardlink-uri).

Localizare:

  • La fel ca și superblocul, inode-ul are un corespondent pe disc. inode-urile de pe disc sunt, în general, grupate într-o zonă specializată (zonă de inode-uri), separată de zona de blocuri de date; în unele sisteme de fișiere, structurile echivalente inode-urilor sunt răspândite în structura sistemului de fișiere ( FAT);
  • ca entitate VFS, inode-ul este reprezentat de structura struct inode și de operațiile cu aceasta definite în structura struct inode_operations.

Fiecare inode este în general identificat de un număr. Pe Linux, argumentul -i la comanda ls precizează numărului inode-ului asociat fișierului:

razvan@valhalla:~/school/2008-2009/so2/wiki$ ls -i
1277956 lab10.wiki  1277962 lab9.wikibak  1277964 replace_lxr.sh
1277954 lab9.wiki   1277958 link.txt      1277955 tema4.wiki

file

File este componenta din modelul general al sistemului de fișiere care se apropie cel mai mult de utilizator. Structura există doar ca entitate VFS în memorie și nu are corespondent fizic pe disc.

În vreme ce inode-ul abstractizează un fișier situat pe disc, file-ul abtractizează un fișier deschis. Din punctul de vedere al procesului, entitatea file abstractizează fișierul. Din punctul de vedere al implementării sistemului de fișiere, însă, inode-ul este entitatea care abstractizează fișierul.

Structura file menține informații precum:

  • poziția cursorului de fișier (file pointer);
  • drepturile de deschidere ale fișierului;
  • pointer către inode-ul asociat (eventual indexul acestuia).

Localizare:

dentry

Dentry (directory entry) realizează asocierea între un inode și numele fișierului.

În general o structură dentry conține două câmpuri:

  • un întreg care identifică inode-ul;
  • un șir de caractere reprezentând numele acestuia.

dentry reprezintă o componentă specifică dintr-o cale, care poate fi un director sau un fișier. Spre exemplu, pentru calea /bin/vi, vor fi create obiecte dentry pentru /, bin și vi (un total de 3 obiecte dentry).

  • dentry are un corespondent pe disc, dar corespondența nu este directă deoarece fiecare sistem de fișiere păstrează dentry-urile într-un mod specific
  • în VFS, entitatea dentry este reprezentată de structura struct dentry și de operațiile cu aceasta definite în structura struct dentry_operations.

Înregistrarea și deînregistrarea sistemelor de fișiere

În versiunea actuală, kernel-ul Linux are suport pentru un număr în jur de 50 de sisteme de fișiere, dintre care:

Pe un singur sistem, însă, este puțin probabil să existe mai mult de 5-6 sisteme de fișiere. Din acest motiv, sistemele de fișiere (sau, mai corect, tipurile de sisteme de fișiere) sunt implementate ca module și pot fi încărcate sau descărcate oricând.

Pentru a putea încărca / descărca în mod dinamic un modul de sistem de fișiere este necesar un API de înregistrare / deînregistrare a tipului sistemului de fișiere în / din sistem. Structura care descrie un anumit sistem de fișiere este struct file_system_type:

#include <linux/fs.h>
 
struct file_system_type {
         const char *name;
         int fs_flags;
         struct dentry *(*mount) (struct file_system_type *, int,
                                   const char *, void *);
         void (*kill_sb) (struct super_block *);
         struct module *owner;
         struct file_system_type * next;
         struct list_head fs_supers;
         struct lock_class_key s_lock_key;
         struct lock_class_key s_umount_key;
         //...
};
  • name este șirul de caractere prin care numele va identifica un sistem de fișiere (argumentul dat la mount -t).
  • owner este THIS_MODULE pentru sisteme de fișiere implementate în module, și NULL dacă sunt scrise direct în kernel.
  • Funcția mount citește superblocul de pe disc în memorie la încărcarea sistemului de fișiere. Funcția este proprie fiecărui sistem de fișiere. Pentru mai multe detalii, citiți Secțiunea Funcțiile mount kill_sb .
  • Funcția kill_sb eliberează superblocul din memorie, citiți Secțiunea Funcțiile mount kill_sb .
  • fs_flags precizează flag-urile cu care trebuie montat sistemul de fișiere. Un exemplu de flag este FS_REQUIRES_DEV care precizează VFS-ului că sistemul de fișiere are nevoie de un disc (nu este un sistem de fișiere virtual).
  • fs_supers este o listă ce conține toate superblocurile asociate acestui sistem de fișiere. Dat fiind că același tip de sistem de fișiere poate fi montat de mai multe ori, pentru fiecare mount va exista un superbloc separat.

Înregistrarea unui sistem de fișiere în sistem se realizează, în general, în funcția de inițializare a modulului. Pentru înregistrare, programatorul va trebui să

  1. inițializeze o structură de tipul struct file_system_type cu numele, flag-urile, funcția care implementează operația de citire a superblocului și referința la structura ce identifică modulul curent
  2. apeleze funcția register_filesystem.

La descărcarea modulului trebuie să se deînregistreze sistemul de fișiere prin apelarea funcției unregister_filesystem.

Un exemplu de înregistrare / deînregistrare a unui sistem de fișiere virtual se găsește în codul pentru ramfs:

static struct file_system_type ramfs_fs_type = {
        .name           = "ramfs",
        .mount         = ramfs_mount,
        .kill_sb        = kill_litter_super,
};
 
static int __init init_ramfs_fs(void)
{
        return register_filesystem(&ramfs_fs_type);
}
 
static void __exit exit_ramfs_fs(void)
{
        unregister_filesystem(&ramfs_fs_type);
}

Funcțiile mount, kill_sb

La montarea sistemului de fișiere, nucleul apelează funcția mount definită în cadrul structurii struct file_system_type. Funcția face un set de inițializări și returnează un dentry (structura struct dentry) ce reprezinta directorul punctului de mount. De obicei, mount este o funcție simplă care apelează una din funcțiile:

  • mount_bdev, care montează un sistem de fișiere aflat pe un device de tip bloc
  • mount_single, care montează un sistem de fișiere care partajează o instanță între toate operațiile de montare
  • mount_nodev, care montează un sistem de fișiere ce nu se afla pe un device fizic
  • mount_pseudo, o funcție ajutătoare pentru pseudo-sistemele de fișiere (sockfs, pipefs, în general sisteme de fișiere care nu pot fi montate)

Aceste funcții primesc ca parametru un pointer spre o funcție fill_super care va fi apelată după inițializarea superblocului pentru terminarea inițializării acestuia de către driver. Un exemplu de o astfel de funcție găsiți în secțiunea fill_super.

La demontare nucleul apelează kill_sb, care face operații de tip cleanup și apelează una din funcțiile:

  • kill_block_super, care demontează un sistem de fișiere aflat pe un device de tip bloc
  • kill_anon_super, care demontează un sistem de fișiere virtual (informația este generată în momentul în care este cerută)
  • kill_litter_super, care demontează un sistem de fișiere ce nu se afla pe un device fizic (menține informația în memorie)

Un exemplu pentru un sistem de fișiere fără suport pe disc este funcția ramfs_mount din sistemul de fișiere ramfs:

struct dentry *ramfs_mount(struct file_system_type *fs_type,
        int flags, const char *dev_name, void *data)
{
        return mount_nodev(fs_type, flags, data, ramfs_fill_super);
}

Un exemplu pentru un sistem de fișiere pentru disc este funcția minix_mount din sistemul de fișiere minix:

struct dentry *minix_mount(struct file_system_type *fs_type,
        int flags, const char *dev_name, void *data)
{
         return mount_bdev(fs_type, flags, dev_name, data, minix_fill_super);
}

Superblocul în VFS

Superblocul există atât ca entitate fizică (entitate pe disc) cât și ca entitate VFS (în cadrul structurii struct super_block). Superblocul conține numai metainformație și este folosit pentru scrierea și citirea de metainformații de pe disc (inode-uri, directory entries). Un superbloc (și implicit structura struct super_block) va conține informații despre dispozitivul utilizat, lista de inode-uri, pointer-ul la inode-ul rădăcină al sistemului de fișiere și un pointer la operațiile de superbloc.

Structura struct super_block

O parte din definiția structurii struct super_block este prezentată mai jos:

struct super_block {
        //...
        dev_t                   s_dev;              /* identifier */
        unsigned long           s_blocksize;        /* block size in bytes */
        unsigned char           s_blocksize_bits;   /* block size in bits */
        unsigned char           s_dirt;             /* dirty flag */
        unsigned long long      s_maxbytes;         /* max file size */
        struct file_system_type *s_type;            /* filesystem type */
        struct super_operations *s_op;              /* superblock methods */
        //...
        unsigned long           s_flags;            /* mount flags */
        unsigned long           s_magic;            /* filesystem’s magic number */
        struct dentry           *s_root;            /* directory mount point */
        //...
        char                    s_id[32];           /* informational name */
        void                    *s_fs_info;         /* filesystem private info */
};

Superblocul memorează informația globală pentru o instanță a unui sistem de fișiere:

  • dispozitivul fizic pe care rezidă
  • dimensiunea blocului
  • dimensiunea maximă a unui fișier
  • tipul sistemului de fișiere
  • operațiile pe care le suportă
  • numărul magic (identifică sistemul de fișiere)
  • dentry-ul directorului rădăcină

În plus, un pointer generic (void *) memorează date private sistemului de fișiere. Superblocul poate fi privit ca un obiect abstract căruia îi sunt adăugate date proprii în momentul în care există o implementare concretă.

Operațiile pe superbloc

Operațiile pe superbloc sunt descrise de structura struct super_operations:

struct super_operations {
       //...
       int (*write_inode) (struct inode *, struct writeback_control *wbc);
       struct inode *(*alloc_inode)(struct super_block *sb);
       void (*destroy_inode)(struct inode *);
 
       void (*put_super) (struct super_block *);
       int (*statfs) (struct dentry *, struct kstatfs *);
       int (*remount_fs) (struct super_block *sb, int *flags, char *data);
       //...
};

Câmpurile structurii sunt pointeri de funcții cu următoarele semnificații:

  • write_inode, alloc_inode, destroy_inode scrie, alocă, respectiv eliberează resurse asociate unui inode și sunt descrise în Laboratorul 09;
  • put_super este apelată în momentul eliberării superblocului, la umount; în cadrul acestei funcții trebuie eliberate orice resurse (în general memorie) din datele private ale sistemului de fișiere, dacă există memorie alocată;
  • remount_fs este apelată atunci când nucleul a detectat că se încearcă o remontare (flag-ul de montare MS_REMOUNTM); cel mai adesea aici trebuie să se detecteze dacă se încearcă o trecere read-only → read-write sau viceversa; acest lucru se poate face simplu pentru că se pot accesa flag-urile vechi (în sb→s_flags) cât și cele noi (în flags); data e un pointer către datele trimise de mount ce reprezintă opțiuni specifice sistemului de fișiere;
  • statfs se apelează atunci când se face un apel de sistem statfs (încercați stat –f sau df); în cazul acestui apel trebuie completate câmpurile structurii struct kstatfs, aşa cum se face, de exemplu, în funcţia ext4_statfs.

Funcția fill_super

După cum s-a specificat, funcția fill_super este apelată pentru terminarea inițializării superblocului. Această inițializare presupune completarea câmpurilor structurii struct super_block și inițializarea inode-ului rădăcină.

Un exemplu de implementare este funcția ramfs_fill_super apelată pentru inițializarea câmpurile din superbloc care au mai rămas de inițializat:

#include <linux/pagemap.h>
 
#define RAMFS_MAGIC     0x858458f6
 
static const struct super_operations ramfs_ops = {
        .statfs         = simple_statfs,
        .drop_inode     = generic_drop_inode,
        .show_options   = generic_show_options,
};
 
static int ramfs_fill_super(struct super_block *sb, void *data, int silent)
{
        struct ramfs_fs_info *fsi;
        struct inode *inode;
        int err;
 
        save_mount_options(sb, data);
 
        fsi = kzalloc(sizeof(struct ramfs_fs_info), GFP_KERNEL);
        sb->s_fs_info = fsi;
        if (!fsi)
                return -ENOMEM;
 
        err = ramfs_parse_options(data, &fsi->mount_opts);
        if (err)
                return err;
 
        sb->s_maxbytes          = MAX_LFS_FILESIZE;
        sb->s_blocksize         = PAGE_SIZE;
        sb->s_blocksize_bits    = PAGE_SHIFT;
        sb->s_magic             = RAMFS_MAGIC;
        sb->s_op                = &ramfs_ops;
        sb->s_time_gran         = 1;
 
        inode = ramfs_get_inode(sb, NULL, S_IFDIR | fsi->mount_opts.mode, 0);
        sb->s_root = d_make_root(inode);
        if (!sb->s_root)
                return -ENOMEM;
 
        return 0;
}

În kernel sunt disponibile funcții generice pentru implementarea operațiilor cu structurile sistemelor de fișiere. Funcțiile generic_drop_inode și simple_statfs, folosite în codul de mai sus, sunt astfel de funcții și pot fi folosite în implementarea driverelor, daca funcționalitatea acestora este suficientă.

În mare, funcția ramfs_fill_super din codul de mai sus setează câteva câmpuri din superbloc, apoi citește inode-ul rădăcină și alocă dentry-ul rădăcină. Citirea inode-ului radăcină se face în funcția ramfs_get_inode, și constă din alocarea unui nou inode folosind new_inode și inițializarea acestuia. Pentru eliberarea inode-ului se folosește iput, iar pentru alocarea dentry-ului rădăcină d_make_root.

Un exemplu de implementare pentru un sistem de fișiere pentru disc este funcția minix_fill_super din sistemul de fișiere minix. Funcționalitatea pentru sistemul de fișiere pentru disc este similară cu cea a sistemului de fișiere virtual, cu deosebirea folosirii buffer cache-ului. De asemenea, sistemul de fișiere minix păstrează date private de forma struct minix_sb_info. Mare parte a acestei funcții se ocupa cu inițializarea acestor date private (care nu sunt incluse în extrasul de cod de mai sus, pentru claritate). Datele private sunt alocate folosind funcția kzalloc și sunt păstrate în câmpul s_fs_info al superblocului.

Funcțiile VFS-ului primesc de obicei ca parametru superblocul, un inode sau/și un dentry, care conțin un pointer către superbloc, astfel încât aceste date private pot fi accesate ușor.

Buffer cache-ul

Buffer cache-ul este un subsistem în kernel care se ocupă caching-ul (atât la citire cât și la scriere) blocurilor de pe dispozitivele de tip bloc. Entitatea de bază cu care lucrează buffer cache-ul este struct buffer_head. Câmpurile mai importante din această structură sunt:

  • b_data, pointer către o zonă din memorie de unde au fost citite sau unde trebuie scrise date
  • b_size, dimensiunea buffer-ului
  • b_bdev, device-ul cu care se lucrează
  • b_blocknr, numărul blocului de pe device care a fost încărcat sau trebuie să fie salvat pe disc
  • b_state, starea buffer-ului

Există câteva funcții importante ce lucrează cu astfel de structuri:

  • __bread: citește un bloc cu numărul dat și de dimensiune dată într-o structură buffer_head; în caz de succes întoarce un pointer către buffer_head, altfel întoarce NULL;
  • sb_bread: face același lucru ca și funcția precedentă, dimensiunea blocului de citit fiind luată din superbloc, la fel și dispozitivul de pe care se face citirea;
  • mark_buffer_dirty: marchează buffer-ul dirty (setează bitul BH_Dirty); buffer-ul va fi scris pe disc la un moment ulterior de timp (din când în când kernel thread-ul bdflush se trezește și scrie buffere pe disc);
  • brelse: eliberează memoria folosită de buffer, după ce în prealabil a scris buffer-ul pe disc daca era nevoie;
  • map_bh: asociază buffer-head-ul cu sectorul corespunzător.

Funcții și macro-uri folositoare

Superblocul conține de obicei o hartă a blocurilor ocupate (de inode-uri, dentry, date) sub forma unui bitmap (vector de biți). Pentru lucrul cu aceste hărți se recomandă folosirea următoarelor funcții:

  • find_first_zero_bit, pentru găsirea primului bit zero dintr-o zonă de memorie. Parametrul size semnifică numărul de biți al zonei în care se face căutarea;
  • test_and_set_bit, pentru setarea unui bit și obținerea vechii valori;
  • test_and_clear_bit, pentru ștergerea unui bit și obținerea vechii valori;
  • test_and_change_bit, pentru inversarea valorii unui bit și obținerea vechii valori.

Pentru verificarea tipului unui inode se pot folosi următoarele macrodefiniții:

  • S_ISDIR(inode→i_mode), pentru a verifica dacă inode-ul este un director;
  • S_ISREG(inode→i_mode), pentru a verifica dacă inode-ul este un fișier clasic (nu de tip link sau device).

Resurse utile

  1. Robert Love – Linux Kernel Development, Second Edition – Chapter 12. The Virtual Filesystem
  2. Understanding the Linux Kernel, 3rd edition - Chapter 12. The Virtual Filesystem
so2/laboratoare/lab08.txt · Last modified: 2017/04/11 23:53 by codrut.grosu2703
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0