Table of Contents

Laborator 9 - Drivere de sisteme de fișiere (Linux) partea 2

Obiectivele laboratorului

Cuvinte cheie

Materiale ajutătoare

Inode-ul

Inode-ul este o componentă esențială a unui sistem de fișiere UNIX ( ext4, reiserfs) și, în același timp, o componentă importantă a VFS. Un inode este o metadată (deține informații despre informații). Un inode identifică în mod unic un fișier de pe disc și deține informații despre acesta (uid, gid, drepturi de acces, timpi de acces, pointeri la blocurile de date etc.). Un aspect important este faptul că un inode nu deține informații despre numele fișierului (acesta este reținut de structura struct dentry asociată).

Inode-ul referă un fișier de pe disc. Pentru referirea unui fișier deschis (asociat cu un descriptor de fișier din cadrul unui proces) se folosește structura struct file. Unui inode îi corespund zero sau mai multe structuri file (mai multe procese pot deschide același fișier, sau un proces poate deschide același fișier de mai multe ori).

Inode-ul există atât ca entitate VFS (în memorie), cât și ca entitate pe disc (pentru sistemele de fișiere UNIX, HFS, NTFS etc.). Inode-ul din VFS este reprezentat de structura struct inode. Ca și celelalte structuri din VFS, struct inode este o structură generică care acoperă opțiunile pentru toate tipurile de fișiere suportate, chiar și acele sisteme de fișiere care nu au o entitate pe disc asociată (cum este FAT).

Structura inode

Structura struct inode este unică pentru toate sistemele de fișiere. În general sistemele de fișiere dețin și informații particulare. Acestea sunt referite prin intermediul câmpului i_private al structurii. Convențional, structura care păstrează acele informații particulare este denumită fsname_inode_info, unde fsname reprezintă numele sistemului de fișiere. Spre exemplu, sistemele de fișiere minix și ext4 păstrează informațiile particulare în structurile struct minix_inode_info, respectiv struct ext4_inode_info.

Câteva din câmpurile importante ale structurii struct inode sunt:

Câteva funcții care pot fi utilizate în lucrul cu inode-uri:

Un inode creat cu new_inode() nu este în hash table, și în afară de cazul în care aveți motive serioase, trebuie să îl introduceți în hash table;

Operații cu inode-uri

Obținerea unui inode

Una din principalele operații cu inode-uri este obținerea unui inode (a unei structuri struct inode în VFS). Până la versiunea 2.6.24 a nucleului Linux, dezvoltatorul definea o funcție read_inode. Începând cu versiunea 2.6.25, dezvoltatorul trebuie să definească o funcție <fsname>_iget unde <fsname> este numele sistemului de fișiere. Această funcție este responsabilă cu aflarea inode-ului VFS în cazul în care acesta există sau crearea unui inode nou și completarea acestuia cu informațiile de pe disc.

În general, în cadrul funcției se va apela iget_locked pentru obținerea structurii struct inode din VFS. În cazul în care inode-ul este nou creat, atunci va trebui citit inode-ul de pe disc (folosind sb_bread) și completate informațiile utile.

Un exemplu de astfel de funcție este minix_iget:

static struct inode *V1_minix_iget(struct inode *inode)
{
	struct buffer_head * bh;
	struct minix_inode * raw_inode;
	struct minix_inode_info *minix_inode = minix_i(inode);
	int i;
 
	raw_inode = minix_V1_raw_inode(inode->i_sb, inode->i_ino, &bh);
	if (!raw_inode) {
		iget_failed(inode);
		return ERR_PTR(-EIO);
	...
}
 
struct inode *minix_iget(struct super_block *sb, unsigned long ino)
{
	struct inode *inode;
 
	inode = iget_locked(sb, ino);
	if (!inode)
		return ERR_PTR(-ENOMEM);
	if (!(inode->i_state & I_NEW))
		return inode;
 
	if (INODE_VERSION(inode) == MINIX_V1)
		return V1_minix_iget(inode);
    ...
}

În cadrul funcției minix_iget se obține inode-ul VFS folosind iget_locked. Dacă inode-ul este deja existent (nu este nou = nu este configurat flag-ul I_NEW) funcția se întoarce. Altfel, se apelează funcția V1_minix_iget care va citi inode-ul de pe disc folosind minix_V1_raw_inode și apoi va completa inode-ul VFS cu informațiile citite.

Superoperații

O bună parte din superoperații (componentele structurii struct super_operations, utilizate de superbloc) sunt folosite în lucrul cu inode-uri. În continuare sunt prezentate acestea:

Operații pentru inode - inode_operations

Operațiile pentru inode sunt descrise de structura struct inode_operations.

Inode-urile sunt de mai multe tipuri: fișier, director, fișier special (pipe, fifo), dispozitiv de tip bloc, dispozitiv de tip caracter, link etc. Din acest motiv, operațiile pe care trebuie un inode să le implementeze sunt diferite pentru fiecare tip de inode. Mai jos vor fi prezentate în detaliu operațiile implementate pentru un inode de tip fișier și un inode de tip director.

Operațiile unui inode sunt inițializate și accesate folosind câmpul i_op al structurii struct inode.

Structura file

Structura file corespunde unui fișier deschis de un proces și există doar în memorie, fiind asociată unui inode. Este entitatea din VFS cea mai apropiată de user-space; câmpurile structurii conțin informații familiare ale unui fișier din user-space (modul de acces, poziția în fișier, etc.), iar operațiile cu aceasta sunt apeluri de sistem cunoscute (read, write, etc.).

Operațiile pentru file sunt descrise de structura struct file_operations.

Pentru a inițializa operațiile pe file pentru un sistem de fișiere, se folosește câmpul i_fop al structurii struct inode. La deschiderea unui fișier, VFS-ul inițializează câmpul f_op al structurii struct file cu adresa din inode→i_fop, astfel încât apeluri de sistem ulterioare să folosească valoarea stocată în file→f_op.

Inode-urile de tip fișier

Pentru lucrul cu inode-ul trebuie completate câmpurile i_op și i_fop ale structurii inode. Tipul inode-ului determină operațiile pe care trebuie să le implementeze.

Operații asupra inode-urilor de tip fișier

În sistemul de fișiere minix, pentru operațiile pe un inode este definită structura minix_file_inode_operations, iar pentru operațiile pe file se definește structura minix_file_operations:

const struct file_operations minix_file_operations = {
         .llseek         = generic_file_llseek,
         .read_iter      = generic_file_read_iter,
         //...
         .write_iter     = generic_file_write_iter,
         //...
         .mmap           = generic_file_mmap,
         //...
};
 
const struct inode_operations minix_file_inode_operations = {
        .setattr        = minix_setattr,
        .getattr        = minix_getattr,
};
 
        //...
        if (S_ISREG(inode->i_mode)) {
                inode->i_op = &minix_file_inode_operations;
                inode->i_fop = &minix_file_operations;
        }
        //...

Funcțiile generic_file_llseek, generic_file_mmap, generic_file_read_iter și generic_file_write_iter sunt implementate în kernel.

Pentru sistemele de fișiere simple nu trebuie să se implementeze decât operația de trunchiere (apelul de sistem truncate). Deși inițial exita o operație dedicată, începând cu 3.14, operația a fost înglobată în setattr: dacă dimensiunea pasată este diferită de dimensiunea curentă a inode-ului atunci va trebui efectuată o operație de trunchiere. Un exemplu de implementare a acestei verificări este prezentă în funcția minix_setattr:

static int minix_setattr(struct dentry *dentry, struct iattr *attr)
{
        struct inode *inode = d_inode(dentry);
        int error;
 
        error = setattr_prepare(dentry, attr);
        if (error)
                return error;
 
        if ((attr->ia_valid & ATTR_SIZE) &&
            attr->ia_size != i_size_read(inode)) {
                error = inode_newsize_ok(inode, attr->ia_size);
                if (error)
                        return error;
 
                truncate_setsize(inode, attr->ia_size);
                minix_truncate(inode);
        }
 
        setattr_copy(inode, attr);
        mark_inode_dirty(inode);
        return 0;
}

Operația de trunchiere presupune:

Un exemplu de implementarea a operației de trunchiere este funcția minix_truncate din sistemul de fișiere minix.

Funcțiile generic_* sunt deja implementate!

Operații asupra spațiului de adresă

Între spațiul de adrese al unui proces și fișiere există o strânsă legătură: execuția programelor se face aproape exclusiv prin maparea fișierului în spațiul de adresă al procesului. Întrucât această abordare funcționează foarte bine și este destul de generală, poate fi folosită și în cazul apelurilor de sistem obișnuite cum ar fi read și write.

Structura care descrie spațiul de adresă este struct address_space, iar operațiile cu aceasta sunt descrise de structura struct address_space_operations. Pentru inițializarea operațiilor asupra spațiului de adresă, se completează câmpul inode->i_mapping->a_ops al inode-ului de tip fișier.

Un exemplu este structura minix_aops din sistemul de fișiere minix:

static const struct address_space_operations minix_aops = {
         .readpage = minix_readpage,
         .writepage = minix_writepage,
         .write_begin = minix_write_begin,
         .write_end = generic_write_end,
         .bmap = minix_bmap
};
 
//...
if (S_ISREG(inode->i_mode)) {
        inode->i_mapping->a_ops = &minix_aops;
}
//...

Funcția generic_write_end este deja implementată. Majoritatea funcțiilor specifice sunt foarte ușor de implementat, după cum urmează:

static int minix_writepage(struct page *page, struct writeback_control *wbc)
{
         return block_write_full_page(page, minix_get_block, wbc);
}
 
static int minix_readpage(struct file *file, struct page *page)
{
         return block_read_full_page(page,minix_get_block);
}
 
static void minix_write_failed(struct address_space *mapping, loff_t to)
{
        struct inode *inode = mapping->host;
 
        if (to > inode->i_size) {
                truncate_pagecache(inode, inode->i_size);
                minix_truncate(inode);
        }
}
 
static int minix_write_begin(struct file *file, struct address_space *mapping,
                        loff_t pos, unsigned len, unsigned flags,
                        struct page **pagep, void **fsdata)
{
        int ret;
 
        ret = block_write_begin(mapping, pos, len, flags, pagep,
                                minix_get_block);
        if (unlikely(ret))
                minix_write_failed(mapping, pos + len);
 
        return ret;
}
 
static sector_t minix_bmap(struct address_space *mapping, sector_t block)
{
         return generic_block_bmap(mapping,block,minix_get_block);
}

Tot ce mai trebuie făcut este să se implementeze minix_get_block, care trebuie să translateze un bloc al unui fișier într-un bloc de pe device. Dacă flag-ul create primit ca parametru este activat, trebuie alocat un nou bloc. În cazul în care se creează un bloc nou, trebuie marcată corespunzător harta de biți. Pentru a înștiința nucleul să nu mai citească blocul de pe disc, trebuie marcat bh cu set_buffer_new. Trebuie asociat buffer-ul cu blocul cerut prin funcția map_bh.

Structura dentry

Operațiile pe directoare folosesc structura struct dentry. Principala sarcină a acesteia este realizarea de legături între inode-uri și numele fișierelor. Câmpurile importante ale acestei structuri sunt prezentate mai jos:

struct dentry {
        //...
        struct inode             *d_inode;     /* associated inode */
        //...
        struct dentry            *d_parent;    /* dentry object of parent */
        struct qstr              d_name;       /* dentry name */
        //...
 
        struct dentry_operations *d_op;        /* dentry operations table */
        struct super_block       *d_sb;        /* superblock of file */
        void                     *d_fsdata;    /* filesystem-specific data */
        //...
};

Semnificațiile câmpurilor:

Operații cu dentry

Operațiile care se aplică cel mai adesea asupra dentry-urilor sunt:

Trebuie să se folosească d_instantiate și NU d_add pentru apelurile create, mkdir, mknod, rename, symlink.

Operații asupra inode-urilor de tip director

Operațiile de lucru cu inode-urile de tip director au un nivel de complexitate mai ridicat decât cele de tip fișier. Dezvoltatorul trebuie să definească operații pentru inode-uri și operații pentru file-uri. În minix, aceste operații sunt definite în structurile minix_dir_inode_operations, respectiv minix_dir_operations:

struct inode_operations minix_dir_inode_operations = {
      .create = minix_create,
      .lookup = minix_lookup,
      .link = minix_link,
      .unlink = minix_unlink,
      .symlink = minix_symlink,
      .mkdir = minix_mkdir,
      .rmdir = minix_rmdir,
      .mknod = minix_mknod,
      //...
};
 
struct file_operations minix_dir_operations = {
      .llseek = generic_file_llseek,
      .read = generic_read_dir,
      .iterate = minix_readdir,
      //...
};
 
        //...
	if (S_ISDIR(inode->i_mode)) {
		inode->i_op = &minix_dir_inode_operations;
		inode->i_fop = &minix_dir_operations;
		inode->i_mapping->a_ops = &minix_aops;
	}
       //...

Singura funcție deja implementată este generic_read_dir.

Funcțiile care implementează operațiile asupra inode-urilor de tip director sunt cele descrise mai jos.

Crearea unui inode

Funcția de creare de inode este indicată de câmpul create din structura inode_operations. În cazul minix este vorba de minix_create. Această funcție este apelată în urma apelurilor de sistem creat și open. O astfel de funcție realizează următoarele operații:

  1. Introduce în structura fizică a discului o nouă intrare; nu trebuie uitată actualizarea hărților de biți pe disc.
  2. Configurează drepturile de acces la cele primite ca parametru.
  3. Marchează inode-ul ca murdar (dirty) cu ajutorul funcției mark_inode_dirty.
  4. Instanțiază intrarea de director (dentry) cu ajutorul funcției d_instantiate.

Crearea unui director

Funcția de creare de director este indicată de câmpul mkdir din structura inode_operations. În cazul minix este vorba de minix_mkdir. Această funcție este apelată în urma apelului de sistem mkdir. O astfel de funcție realizează următoarele operații:

Funcția de creare de link (hard) este indicată de câmpul link din structura inode_operations. În cazul minix este vorba de minix_link. Această funcție este apelată în urma apelului de sistem link. O astfel de funcție realizează următoarele operații:

Funcția de creare de link (simbolic) este indicată de câmpul symlink din structura inode_operations. În cazul minix este vorba de minix_symlink. Operațiile care trebuiesc realizate sunt similare cu cele de la minix_link cu deosebirile date de faptul că se creează o legătură simbolică.

Funcția de ștergere a unui link (hard) este indicată de câmpul unlink din structura inode_operations. În cazul minix este vorba de minix_unlink. Această funcție este apelată în urma apelului de sistem unlink. O astfel de funcție realizează următoarele operații:

  1. Șterge din structura fizică a discului intrarea dată ca parametru.
  2. Decrementează contorul i_nlink al inode-ului către care puncta intrarea (altfel inode-ul nu va fi niciodată șters).

Ștergerea unui director

Funcția de ștergere a unui director este indicată de câmpul rmdir din structura inode_operations. În cazul minix este vorba de minix_rmdir. Această funcție este apelată în urma apelului de sistem rmdir. O astfel de funcție realizează următoarele operații:

  1. Realizează operațiile realizate de minix_unlink.
  2. Se asigură că directorul este gol; în caz contrar, se întoarce ENOTEMPTY.
  3. Șterge și blocul/blocurile de date aferente.

Căutarea unui inode într-un director

Funcția de căutare a unei intrări într-un director și de extragere a inode-ului acesteia este indicată de câmpul lookup din structura inode_operations. În cazul minix este vorba de minix_lookup. Această funcție este apelată indirect în momentul în care se doresc informații despre inode-ul aferent unei intrări dintr-un director. O astfel de funcție realizează următoarele operații:

  1. Caută în directorul indicat în dir intrarea cu numele dentry→d_name.name.
  2. În cazul în care intrarea este găsita se va întoarce NULL și se va asocia inode-ul cu numele, cu ajutorul funcției d_add.
  3. În caz contrar, se întoarce ERR_PTR.

Iterarea prin intrările într-un director

Funcția de iterare prin intrările unui director (de listare a conținutului directorului) de link (hard) este indicată de câmpul iterate din structura file_operations. În cazul minix este vorba de minix_readdir. Această funcție este apelată în urma apelului de sistem readdir.

Funcția întoarce fie toate intrările din director, fie doar o parte în momentul în care bufferul alocat pentru aceasta nu este disponibil. Un apel al funcției poate întoarce:

Funcția va fi apelată consecutiv până în momentul în care se citesc toate întrările disponibile. Funcția este apelată de cel puțin două ori.

Funcția realizează următoarele operații:

  1. Parcurge intrările (dentry-urile) din directorul curent.
  2. Pentru fiecare dentry citit se incrementează valoarea ctx→pos.
  3. Pentru fiecare dentry valid (un inode diferit de 0, de exemplu), apelează funcția dir_emit.
  4. Dacă funcția dir_emit întoarce o valoare diferită de zero înseamnă că bufferul din user space este umplut și funcția se va întoarce.

Argumentele funcției dir_emit sunt:

Operații pe bitmap-uri

În cazul lucrului cu sistemul de fișiere, sunt reținute informații de gestiune (ce bloc este liber sau ocupat, ce inode este liber sau ocupat) prin intermediul de hărți de biți (bitmap). Pentru aceasta avem adesea nevoie să folosim operații de lucru pe biți. Astfel de operații sunt:

Operațiile de lucru cu hărți de biți se găsesc în headerele din include/asm-generic/bitops în special în find.h și atomic.h. Funcții uzuale, cu denumirile indicând rolul lor, sunt:

Aceste funcții primesc de regulă adresa hărții de biți, eventual dimensiunea ei (în biți) și, la nevoie, indexul bitului care se dorește activat (set) sau dezactivat (clear).

Câteva exemple de folosire sunt indicate mai jos:

unsigned int map;
unsigned char array_map[NUM_BYTES];
size_t idx;
int changed;
 
/* Find first zero bit in 32 bit integer. */
idx = find_first_zero_bit(&map, 32);
printk (KERN_ALERT "The %zu-th bit is the first zero bit.\n", idx);
 
/* Find first one bit in NUM_BYTES bytes array. */
idx = find_first_bit(array_map, NUM_BYTES * 8);
printk (KERN_ALERT "The %zu-th bit is the first one bit.\n", idx);
 
/*
 * Clear the idx-th bit in integer.
 * It is assumed idx is less the number of bits in integer.
 */
clear_bit(idx, &map);
 
/*
 * Test and set the idx-th bit in array.
 * It is assumed idx is less the number of bits in array.
 */
changed = __test_and_set_bit(idx, &sbi->imap);
if (changed)
	printk(KERN_ALERT "%zu-th bit changed\n", idx);

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