Valid HTML 4.01!

Zarządzanie plikami - 1

Spis treści

Ciekawe odsyłacze


Wirtualny system plików (VFS)

VFS

Linux potrafi obsługiwać wiele różnych systemów plików (ściślej: formatów systemów plików). Wirtualny system plików przy dostępie do danych w różnych formatach używa zunifikowanego interfejsu, dzięki czemu z poziomu użytkownika dodanie obsługi nowego formatu danych jest stosunkowo proste.

Taka koncepcja umożliwia implementowanie obsługi zarówno takich formatów danych, które są zapisane na fizycznym nośniku (jak etx2), takich, które są dostępne za pośrednictwem sieci (jak nfs), jak i takich, które są tworzone dynamicznie, na żądanie użytkownika (jak proc).


Interfejs systemu plików

W systemie operacyjnym Linux procesy odwołują się do plików za pomocą dobrze zdefiniowanego zbioru funkcji systemowych. Zawiera on zarówno funkcje obsługujące istniejące pliki, a wśród nich open(), read(), write(), lseek() i close(), jak i funkcje służące do tworzenia nowych plików, jak creat(). Obejmuje on również funkcje wykorzystywane przy implementacji potoków: pipe() i dup().

Pierwszym krokiem, jaki musi wykonać proces, by uzyskać dostęp do danych istniejącego pliku, jest wywołanie funkcji open(). Jeśli wykona się ona pomyślnie, to przekazuje procesowi deskryptor pliku, za pomocą którego może on wykonywać na pliku inne operacje, takie jak czytanie (read()) i pisanie (write()). Z kolei jeśli dany plik nie istnieje, to proces może go utworzyć za pomocą funkcji creat(), która, tak jak open(), przekazuje w przypadku sukcesu deskryptor pliku. Funkcje read() i write() zapewniają sekwencyjny dostęp do danych. W przypadku, gdy zachodzi konieczność dostępu bezpośredniego (niesekwencyjnego), proces może skorzystać z funkcji lseek(), która umożliwia zmianę bieżącej pozycji w pliku.

Procesy spokrewnione (jak np. przodek i potomek) mogą komunikować się ze sobą za pomocą łączy komunikacyjnych (ang. pipe). Do ich tworzenia służy funkcja pipe(), która przekazuje dwa deskryptory pliku: do czytania i do pisania. Proces potomny (stworzony za pomocą funkcji fork()) dziedziczy powyższe deskryptory od procesu macierzystego, dzięki czemu możliwy jest przepływ danych od jednego z procesów do drugiego. Funkcja pipe() w połączeniu z funkcją dup(), która kopiuje dany deskryptor do pierwszej wolnej pozycji w tablicy deskryptorów, umożliwia zrealizowanie potoków, takich jak w interpretatorze poleceń, które łączą standardowe wyjście jednego procesu ze standardowym wejściem innego.

Gdy proces przestaje korzystać z pliku lub łącza, to może je zamknąć wywołując funkcję close() z odpowiednim deskryptorem. Zwalnia tym samym pozycję w tablicy deskryptorów odpowiadającą zamykanemu plikowi.

Opisane funkcje systemowe mają swoje odpowiedniki w bibliotece standardowych funkcji wejścia-wyjścia. Funkcje standardowe tworzą interfejs wysokiego poziomu między procesem a jądrem, umożliwiający korzystanie z takich udogodnień jak buforowanie, czy sformatowane wyjście. Jednakże przy pisaniu programów użytkowych często zachodzi potrzeba bezpośredniego odwoływania się do funkcji systemowych.


Tworzenie pliku

Funkcja creat() służy do tworzenia pliku o podanej nazwie. Jest utrzymana jedynie w celu zachowania zgodności z poprzednimi wersjami Linuksa. Jej działanie sprowadza się do wywołania funkcji systemowej open() z odpowiednimi parametrami:


asmlinkage long sys_creat(const char *pathname, int mode)
    return sys_open(pathname, O_CREAT | O_WRONLY | O_TRUNC, mode);

Otwieranie pliku

Funkcja systemowa open()służy do otwierania pliku. Jej wywołanie jest pierwszą czynnością, jaką musi wykonać proces chcący uzyskać dostęp do informacji znajdujących się w pliku. Wynikiem działania funkcji open() jest liczba naturalna zwana deskryptorem pliku. W ramach jednego procesu jest ona wykorzystywana jako identyfikator pliku; dwa otwarte przez proces pliki mają różne deskryptory. Jeden plik może zostać przez proces otwarty dwa razy i ma wtedy dwa różne deskryptory.


asmlinkage long sys_open(const char * filename, int flags, int mode)

Deskryptor pliku jest indeksem w tablicy deskryptorów plików (nazywanej także tablicą deskryptorów otwartych plików).

Pierwszą czynnością jest wczytanie nazwy ścieżkowej pliku z przestrzeni adresowej procesu do przestrzeni jądra.

Następnie znajduje się wolne miejsce w tablicy deskryptorów plików. Wybierany jest deskryptor o najmniejszym wolnym numerze, zostanie on zwolniony, jeśli operacja otwarcia pliku się nie powiedzie.

Przegląda się krok po kroku nazwę ścieżkową w celu dotarcia do odpowiadającego plikowi i-węzła, który należy otworzyć (i-węzeł pełni rolę metryczki pliku). Podczas znajdowania i-węzła korzysta się z informacji o atrybutach otwarcia.

Wykonanie funkcji może potoczyć się wielorako, w zależności od tego, jaki jest cel otwarcia pliku, czyli jakie były parametry otwarcia.

Jeśli plik ma zostać stworzony, to jądro sprawdza, czy taki plik już istnieje i w razie czego przekazuje błąd. Błąd pojawia się także wtedy, gdy proces wywołujący nie posiada praw dostępu.

Jeśli prawa są właściwe, to jest pobierany nowy, wolny i-węzeł. Podczas znajdowania nowego i-węzła zakłada się blokadę na i-węzeł katalogu macierzystego, by można było dołączyć do niego nowy i-węzeł. W sytuacji, gdy nastąpiła próba otwarcia do czytania katalogu lub pliku z systemu plików przeznaczonego tylko do odczytu, jest generowany błąd.

Jeśli plik jest otwierany z zamiarem obcięcia, to jest wywoływana funkcja zwalniająca jego bloki.

Jeśli plik ma zostać otwarty do pisania, to podejmuje się próbę uzyskania prawa do pisania w pliku. W przypadku niepowodzenia i-węzeł jest zwalniany, a funkcja przekazuje błąd.

Tworzy się i wypełnia pozycję w tablicy plików (zwanej też tablicą otwartych plików) związanej z danym systemem plików. Będzie z niej można dotrzeć do i-węzła pliku. Ustawia się w niej także bieżącą pozycję w pliku.

Teraz wywołuje się tę część funkcji otwierania pliku, która zależy od konkretnego systemu plików. W systemie plików EXT2 ta część jest pusta. Wreszcie tablica deskryptorów, w miejscu uprzednio wybranym, jest wypełniana wskaźnikiem do wybranej pozycji w tablicy plików, a indeks miejsca, w którym jest wypełniana zostaje przekazany jako wynik działania funkcji.


Czytanie z pliku

Funkcja systemowa read() służy do czytania danych z otwartego pliku. Ma ona postać:


asmlinkage ssize_t sys_read(unsigned int fd, char * buf, size_t count)

gdzie fd jest deskryptorem otwartego pliku, buf jest adresem struktury danych w procesie użytkownika, do której wczytujemy dane, natomiast count określa liczbę bajtów, jaką chce wczytać użytkownik. Jeżeli funkcja wykona się pomyślnie, to jako wynik przekaże rzeczywistą liczbę wczytanych bajtów lub 0 w przypadku napotkania końca pliku.

Funkcja rozpoczyna czytanie od bieżącej pozycji w pliku, a po jego zakończeniu zwiększa bieżącą pozycję w pliku o liczbę przeczytanych bajtów. W przypadku stwierdzenia odczytu sekwencyjnego wykorzystuje czytanie z wyprzedzeniem, co znacznie zwiększa wydajność systemu.

Funkcja read(), podobnie jak funkcja write(), jest niezależna od zainstalowanego rodzaju systemu plików. Zrealizowana jest ona na dwóch poziomach: najpierw wykonuje się część wspólną dla wszystkich systemów plików (sprawdzenie praw dostępu oraz poprawności argumentów funkcji), potem następuje wywołanie funkcji właściwej dla danego systemu plików. Ta przynależna konkretnemu systemowi funkcja realizuje odczyt danych z pliku.

Informacja o funkcjach, które realizują operacje na plikach dla danego systemu plików znajdują się w strukturze file_operations. Do struktury tej prowadzi wskaźnik z każdego pliku w tablicy plików. Dla systemu EXT2 funkcją służącą do czytania danych z pliku jest generic_file_read():


ssize_t generic_file_read(struct file * filp, char * buf, 
                          size_t count, loff_t *ppos)

gdzie filp jest pozycją w tablicy plików odpowiadającą danemu plikowi, buf jest adresem struktury danych w procesie użytkownika, do której wczytujemy dane, natomiast count określa liczbę bajtów, jaką chce wczytać użytkownik.

Z kolei generic_file_read() wywołuje do_generic_file_read(), która wykonuje większość pracy.

Jeśli jądro stwierdzi, że pozycja, od której proces chce rozpocząć czytanie danego pliku znajduje się wewnątrz obszaru, który poprzednio był wczytywany z wyprzedzeniem, przyjmuje, że plik jest czytany sekwencyjnie, czyli że powinno być zainicjowane czytanie z wyprzedzeniem danego pliku. W przeciwnym razie jądro zaznacza, że czytanie z wyprzedzeniem nie odbędzie się.

Jądro sprawdza, ile danych może być wczytanych z wyprzedzeniem. Rozmiar danych może się okazać zbyt mały, by warto było inicjować czytanie z wyprzedzeniem. Rozmiar danych do wczytania musi należeć do ustalonego przedziału.

Następnie jądro w pętli wykonuje opisane dalej czynności, aż do zakończenia czytania.

Jądro szuka w pamięci podręcznej stron strony, która zawiera dane z pliku. Może się zdarzyć, że jądro nie znajdzie w pamięci operacyjnej takiej strony - nawet jeśli plik był czytany z wyprzedzeniem, odpowiednia strona mogła zostać usunięta z pamięci. Jądro rezerwuje wówczas wolną ramkę w pamięci i zleca wypełnienie ramki danymi z odpowiadającego jej bloku na dysku.

Jeśli ramka jest wypełniona aktualnymi danymi, bądź jej zawartość jest właśnie aktualizowana, to jądro inicjuje wczytanie odpowiedniej liczby stron z wyprzedzeniem. Następnie czeka, aż na danej ramce zostaną zakończone wszystkie zainicjowane wcześniej operacje wejścia-wyjścia i zostanie ona zablokowana. Jeśli okaże się, że odblokowana ramka zawiera nieaktualne dane, to jądro synchronicznie wypełnia ją danymi z odpowiadającego jej bloku na dysku. Teraz pozostaje jeszcze skopiować dane z odpowiedniego fragmentu ramki do przestrzeni adresowej procesu.


Pisanie do pliku

Funkcja systemowa write() służy do pisania danych do otwartego pliku. Ma ona postać:


asmlinkage ssize_t sys_write(unsigned int fd, const char * buf, size_t count)

Funkcja write składa się z dwóch części: niezależnej i zależnej od systemu plików. Część niezależna sprawdza, czy proces ma prawo do pisania w pliku, czy proces ma prawo dostępu do obszaru pamięci, do którego adres przekazał jako parametr wywołania.

Teraz już wywołuje się funkcję właściwą dla konkretnego systemu plików; w przypadku ext2 jest do generic_file_write().

Opuszcza się semafor na i-węźle w celu wykluczenia jednoczesnych zapisów do pliku.

Jądro sprawdza, czy system plików nie jest przeznaczony tylko do odczytu; jeśli tak, to przekazuje błąd.

Jeśli plik został otwarty do dopisywania, to znacznik bieżącej pozycji w pliku jest ustawiany na koniec pliku, w przeciwnym wypadku zapis będzie dokonany od pozycji określonej przez znacznik zapisany w tablicy plików.

Następnie jest obliczany numer bloku, który będzie zapisywany, a który odpowiada wcześniej ustawionemu znacznikowi bieżącej pozycji w pliku, oraz miejsce w tym bloku, od którego rozpocznie się pisanie.

Pisanie do pliku, tak jak czytanie, odbywa się poprzez podręczną pamięć buforową stron. W przypadku pisania pełnych stron sprawa jest prostsza. Jeśli jednak wypisuje się jedynie fragment strony, to najpierw trzeba całą stronę wczytać do pamięci podręcznej, zmodyfikować i dopiero wówczas przeznaczyć do zapisu.

Na końcu jest modyfikowany czas ostatniej modyfikacji pliku i czas ostatniej modyfikacji i-węzła (ustawiane są na czas bieżący). Modyfikowana jest także długość pliku zapisana w i-węźle. Jeśli bieżąca pozycja w pliku jest większa niż zapisana w i-węźle wartość, to zapis trzeba zaktualizować tzn. ustawić długość pliku na równą bieżącej pozycji w pliku. Po zaznaczeniu i-węzła jako brudnego i zwolnienia blokady nałożonej na i-węzeł, funkcja kończy działanie.


Zamykanie pliku

W systemie Linux do zamknięcia otwartego pliku służy funkcja systemowa close(). Ma ona postać:


asmlinkage long sys_close(unsigned int fd)

gdzie argument fd określa deskryptor otwartego pliku. Funkcja close() zwalnia deskryptor, dzięki czemu może on być powtórnie przydzielony. Jeśli licznik odwołań do pozycji w tablicy plików związanej z tym deskryptorem jest większy niż 1, to zmniejsza się licznik i operacja się kończy. Jeśli licznik odwołań jest równy 1, to jest zwalniana pozycja w tablicy plików oraz wywoływany algorytm iput() dla odpowiadającego jej i-węzła z tablicy i-węzłów. W przypadku gdy inne procesy odwołują się do tego i-węzła, to jest zmniejszany licznik odwołań do niego. W przeciwnym razie algorytm iput() zwalnia i-węzeł z pamięci i, jeśli to konieczne, uaktualnia jego zawartość na dysku.


Struktury danych VFS

VFS reprezentuje podejście obiektowe do systemu plików. Podstawowe obiekty, zapewniające jednolity interfejs do systemów plików o różnych formatach, to:

  1. Struktura files:

    Przechowuje lokalne dla procesu informacje o deskryptorach otwartych plików i sposobie obsługi plików. Ten obiekt istnieje jedynie w pamięci operacyjnej.

  2. Struktura file:

    Przechowuje informacje o związku procesu z otwartym plikiem. Ten obiekt istnieje jedynie w pamięci operacyjnej.

  3. Struktura dentry:

    Przechowuje informacje o związku między pozycją w katalogu a plikiem. Każdy dyskowy system plików przechowuje tę informację na dysku we właściwy sobie sposób.

  4. I-węzeł:

    Metryczka pliku zawierająca wszystkie podstawowe informacje o pliku. Dla dyskowych systemów plików obiekt ten ma swój odpowiednik w postaci bloku kontrolnego pliku, przechowywanego na dysku. Z każdym i-węzłem jest związany numer i-węzła, który jednoznacznie identyfikuje plik w systemie plików.

  5. Struktura address_space:

    Główna struktura danych podręcznej pamięci buforowej stron (ang. page cache). Odwzorowuje wszystkie strony jednego pliku (czyli obraz przestrzeni adresowej tego pliku w pamięci operacyjnej) w bloki dyskowe (czyli obraz przestrzeni adresowej tego pliku na dysku).

  6. Superblok:

    Przechowuje informacje o zamontowanym systemie plików. Dla dyskowych systemów plików obiekt ten ma swój odpowiednik w postaci bloku kontrolnego systemu plików, przechowywanego na dysku.

Rysunek ilustruje sytuację, w której trzy różne procesy otworzyły ten sam plik, dwa z nich przy użyciu tego samego twardego dowiązania (ang. hard link). Każdy z procesów sięga za pomocą prywatnego deskryptora pliku do własnej struktury file. Wystarczają dwie struktury dentry, po jednej dla każdego twardego dowiązania. Obie struktury dentry wskazują na ten sam i-węzeł, który identyfikuje ten sam superblok i za jego pomocą ten sam fizyczny plik na dysku.

Obiekty VFS

Rysunek: Obiekty VFS (źródło: Bovet, Ceasti, Linux Kernel)

Ze względów efektywnościowych część struktur przechowywanych na dysku jest czasowo utrzymywana w pamięciach podręcznych w pamięci operacyjnej. Linux utrzymuje:

W dalszej części tego oraz podczas dwóch kolejnych wykładów będziemy przyglądać się wykorzystaniu tych struktur w jądrze Linuksa.


Struktury danych procesu z informacjami o plikach

Oto pola w metryczce procesu (task_struct) opisujące związek procesu z plikami:


   struct fs_struct *fs;       /* inf. o systemie plików */
   struct files_struct *files; /* inf. o otwartych plikach */

Struktura fs_struct

Główne pola struktury fs_struct to dowiązanie do opisu bieżącego katalogu procesu (pwd) oraz korzenia systemu plików (root) . Dzięki tym polom proces zna swój kontekst w systemie plików.


struct fs_struct {
     atomic_t count;
     int umask;
     struct dentry * root, * pwd;
     .....
};

Pole count określa liczbę procesów współdzielących tę samą strukturę fs_struct (liczba ta wzrasta podczas wywołania funkcji do_fork() z ustawioną flagą CLONE_FS). Obsługę tego pola ilustruje funkcja copy_fs() wywoływana wewnątrz do_fork().


static inline int copy_fs(unsigned long clone_flags, struct task_struct * tsk)
{
     if (clone_flags & CLONE_FS) {
         atomic_inc(&current->fs->count);
         return 0;
     }
     tsk->fs = __copy_fs_struct(current->fs);
     if (!tsk->fs)
         return -ENOMEM;
     return 0;
}

Struktura files_struct

Struktura files_struct zawiera tablicę indeksowaną liczbami naturalnymi, które odpowiadają deskryptorom otwartych plików. Wartością pozycji w tej tablicy jest dowiązanie do globalnej (w ramach systemu plików) tablicy otwartych plików.


#define BITS_PER_LONG 32
#define NR_OPEN_DEFAULT BITS_PER_LONG

struct embedded_fd_set {
        unsigned long fds_bits[1];
};
struct fdtable {
        unsigned int max_fds;
        int max_fdset;
        struct file ** fd;      /* current fd array */
        fd_set *close_on_exec;
        fd_set *open_fds;
        struct fdtable *next;
};
struct files_struct {
    atomic_t count;
    struct fdtable *fdt;
    struct fdtable fdtab;
    spinlock file_lock;     
    int next_fd;
    struct embedded_fd_set close_on_exec_init;
    struct embedded_fd_set open_fds_init;
    struct file * fd_array[NR_OPEN_DEFAULT];
};

Pole count określa liczbę procesów współdzielących tę samą strukturę files_struct (liczba ta wzrasta podczas wywołania funkcji do_fork() z ustawioną flagą CLONE_FILES).

Podczas tworzenia procesu alokuje się dla niego wstępnie fd_array[NR_OPEN_DEFAULT] na pierwsze 32 otwierane pliki (w razie potrzeby ta tablica jest rozszerzana o kolejne pozycje - na bieżącą tablicę deskryptorów wskazuje pole fd). Pole open_fds jest maską bitową, w której ustawia się bit podczas przydzielania nowego deskryptora oraz zeruje go podczas zwalniania deskryptora. Pole next_fd podpowiada gdzie warto zacząć szukać kolejnego wolnego deskryptora. Ponieważ struktura danych fd_set zawiera 1024 bity, więc tyle maksymalnie otwartych plików może na raz posiadać proces.

Dzięki wywołaniom systemowym dup() i fork() różne deskryptory mogą się odnosić do tego samego otwartego pliku, czyli różne elementy tablicy mogą wskazywać na tę samą pozycję w tablicy otwartych plików.


Struktura file

Struktura file opisuje jedną pozycję w tablicy otwartych plików. Każde wywołanie funkcji open() powoduje przydzielenie dla otwieranego pliku nowej pozycji w tej tablicy. Pozycje mogą być współdzielone (pole f_count większe od jeden) - dochodzi do tego w wyniku użycia funkcji dup() lub fork().


struct file {
    struct list_head        fu_list;
    struct dentry           *f_dentry;
    const struct file_operations  *f_op;
    atomic_t                f_count;
    unsigned int            f_flags;
    mode_t                  f_mode;
    loff_t                  f_pos;
    ....

Pole fu_list pozwala na dowiązanie struktury do jednej (i tylko jednej) z list: listy wszystkich otwartych plików w danym systemie plików (identyfikowanym poprzez swój superblok) lub listy nieużywanych struktur.

Pole f_dentry zawiera dowiązanie do struktury dentry tego pliku, która jest tworzona podczas odwzorowywania nazwy ścieżkowej pliku w numer i-węzła.

Pole f_mode przechowuje informacje o trybie, w jakim otwarto plik.

Pole f_pos zawiera wskaźnik bieżącej pozycji w pliku. W obecnej wersji dla i386 ma długość 64 bity (skorelowane z maksymalnym dopuszczalnym rozmiarem pliku).

Pole f_op zawiera dowiązanie do tablicy wskaźników do metod, jakie mogą być wykonywane na tym pliku. Pole to uzyskuje wartość od inode->i_fop (por. wykład o tablicach rozdzielczych urządzeń).


struct file_operations {
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *);
    int (*fsync) (struct file *, struct dentry *, int datasync);
    int (*lock) (struct file *, int, struct file_lock *);
    ....
};

I-węzeł

I-węzeł, czyli metryczka pliku, jest ściągany do pamięci operacyjnej podczas otwierania pliku i rozszerzany o dodatkowe pola (np. wskaźniki i numer i-węzła).

TypNazwaOpis
unsigned long i_ino numer i-węzła w dyskowej tablicy i-węzłów
umode_t i_mode typ pliku: zwykły(IS_REG), katalog(IS_DIR), łącze nazwane(IS_FIFO), specjalny, znakowy, 0 gdy wolny; 
także prawa dostępu do pliku dla wszystkich, grupy użytkownika oraz jego samego
nlink_t i_nlink liczba (twardych) dowiązań do pliku
uid_t i_uid identyfikator właściciela pliku
gid_t i_gid identyfikator grupy właściciela pliku
loff_t i_size rozmiar pliku w bajtach (64 bity, czyli 2^64)
time_t i_ctime czas utworzenia pliku
time_t i_atime czas ostatniego dostępu do pliku
time_t i_mtime czas ostatniej modyfikacji pliku
unsigned long  i_blocks liczba bloków dyskowych zajmowanych przez plik
unsigned short i_bytes liczba bajtów w ostatnim bloku pliku
unsigned long i_blksize rozmiar bloków do przesyłania danych dyskowych (inicjowane na PAGE_SIZE)
dev_t i_rdev wskazuje na rzeczywiste urządzenie - tak jak jest zapisany na dysku
struct super_block *i_sb wskaźnik na superblok urządzenia, do którego należy i-węzeł
unsigned int i_flags tutaj trzymane są flagi określające sposób korzystania z  i-węzła, jak również pliku, który wyznacza (m.in. S_WRITE - do zapisu...)
atomic_t i_count liczba wykonanych open() na pliku; wartość zero oznacza, że struktura i-węzła jest wolna i może w razie potrzeby być wykorzystywana przez inne procesy
struct mutex i_mutex semafor binarny do zakładania blokady
struct file_lock *i_flock struktura przechowująca informacje o blokadach w pliku
struct list_head i_list wskaźniki utrzymujące listę i-węzłów w różnych stanach
struct list_head i_sb_list wskaźniki utrzymujące listę i-węzłów jednego superbloku
struct list_head i_hash wskaźniki łączące i-węzeł w listę z innymi znajdującymi się w tablicy haszującej pod tym samym numerem
struct list_head i_dentry Lista wszystkich struktur dentry odnoszących się do tego i-węzła
struct pipe_inode_info i_pipe używane gdy i-węzeł opisuje łącze komunikacyjne
struct block_device *i_bdev wskaźnik do podprogramu obsługi urządzenia blokowego
struct cdev *i_cdev wskaźnik do podprogramu obsługi urządzenia znakowego
struct inode_operations *i_op zestaw operacji na i-węzłach (takich, jak tworzenie nowego), ściśle związanych z wykorzystywanym systemem plików - np. EXT2
const struct file_operations *i_fop zestaw operacji na plikach
struct address_space *i_mapping Wskaźnik do związanego z tym plikiem obiektu address_space
struct address_space *i_data obiekt address_space tego pliku

Każdy i-węzeł znajduje się na jednej z trzech listy podwójnie wiązanych (wykorzystuje się w tym celu pole i_list):

  1. globalna dwukierunkowa lista inode_in_use, zawierająca wszystkie używane i-węzły (i_count>0 i i_nlink>0). Nowo przydzielone i-węzły są dodawane do tej listy:

    
       list_add(&inode->i_list, &inode_in_use);
    

  2. globalna dwukierunkowa lista inode_unused, która zawiera i-węzły z i_count=0;

    
       list_add(&inode->i_list, &inode_unused);
    

  3. dla każdego superbloku dwukierunkowa lista sb->s_dirty, która zawiera poprawne i-węzły z i_count>0, n_link>0 i i_state & I_DIRTY. Ta lista umożliwa sprawne uaktualnianie kopii i-węzłów na dysku;

    
       list_add(&inode->i_list, &sb->s_dirty);
    

Ponadto każdy i-węzeł znajduje się na dwukierunkowej liście wszystkich i-węzłów swojego superbloku (pochodzących z tego samego systemu plików). Na listę wskazuje pole s_inodes superbloku, a do tworzenia dowiązań służy pole i_sb_list i-węzła.

Wreszcie każdy i-węzeł znajduje się w tablicy haszującej list dwukierunkowych inode_hashtable. Każdy i-węzeł jest identyfikowany poprzez swój numer oraz numer urządzenia, na którym się znajduje. Tablica haszująca umożliwia szybki dostęp do i-węzła w pamięci: oblicza się dla niego wartość funkcji haszującej i wyszukuje go na odpowiedniej liście w tablicy.

Każdy i-węzeł może się na raz znajdować tylko w tablicy haszującej, tylko w jednym systemie plików oraz na jednej i tylko jednej spośród list inode_inuse, inode_unused i sb->s_dirty.

W pamięci może się znaleźć co najwyżej jedna kopia danego i-węzła dyskowego.


Alokacja pliku na dysku w systemie ext2

Użytkownik widzi plik jako ciągły strumień bajtów o określonej długości. Gdyby plik na dysku miał również strukturę ciągłą, tzn. zajmował następujące po sobie bloki, to dostęp do wszystkich bajtów pliku byłby bardzo prosty: w i-węźle wystarczyłoby pamiętać adres początku pliku i jego długość. Jednak system operacyjny musi uwzględnić możliwość dynamicznych zmian rozmiaru pliku. Opisana strategia nie pozwala na taką elastyczność systemu plików i powoduje dużą fragmentację wolnego miejsca na dysku. Jądro pozwala na rozproszenie bloków pliku na dysku. Ten sposób przydziału miejsca komplikuje zadanie lokalizacji danych. Utrzymywanie liniowej listy bloków składających się na plik byłoby złym rozwiązaniem - trzeba by narzucić duże ograniczenie na rozmiar pliku bądź dopuszczać i-węzły o zmiennej wielkości. Również obsługa liniowej listy bloków byłaby bardzo nieefektywna.

W Linuksie przyjęto następujące rozwiązanie. W i-węźle przechowuje się niewielkę tablicę numerów bloków składających się na plik. Istotą pomysłu jest wyróżnienie czterech typów adresów przechowywanych w tej tablicy:

bezpośredni
jest to adres bloku z danymi
pojedynczy pośredni
jest to adres bloku, który zawiera listę adresów bezpośrednich
podwójny pośredni
jest to adres bloku, który zawiera listę adresów bloków pojedynczych pośrednich
potrójny pośredni
jest to adres bloku, który zawiera listę adresów bloków podwójnych pośrednich

Blok indeksowy

Niektóre numery bloków mogą być równe zero. Oznacza to, że na pewnej przestrzeni pliku nie zostało nic zapisane (sytuacja taka jest możliwa dzięki funkcji lseek()).

W systemie plików EXT2 tablica z adresami w i-węźle ma nazwę:

Tablica ta ma (w systemie EXT2) rozmiar równy 15. Jest w niej 12 pozycji z adresami bloków bezpośrednich i po jednej pozycji dla bloku pojedynczego, podwójnego i potrójnego pośredniego.


Tłumaczenie numeru logicznego bloku na numer fizyczny (algorytm bmap)

Zadaniem algorytmu bmap jest przekształcanie logicznego numeru bloku w pliku na fizyczny numer bloku na dysku. W systemie plików EXT2 implementacja algorytmu bmap znajduje się w funkcji ext2_bmap().


static int ext2_bmap(struct address_space *mapping, sector_t block)

Pierwszy argument funkcji pozwala dotrzeć do i-węzła pliku. Drugim argumentem jest logiczny numer bloku.

Uproszczona implementacja algorytmu bmap będzie treścią ćwiczeń nr 7.


Przykład

Załóżmy, że w bloku mieszczą się 4 adresy: addr_per_block = 4, zatem addr_per_block_bits = 2. Przypuśćmy, ze układ bloków pewnego pliku jest taki, jak na rysunku poniżej.

Przyklad

Powiedzmy, że chcemy odczytać blok o numerze (logicznym) 22.

block = 22
Ponieważ 22 >= 12, więc blok nie leży wśród bloków bezpośrednich.
block = 22 - 12 = 10
Sprawdzamy, że 10 >= addr_per_block, czyli musimy rozpatrywać blok podwójny pośredni.
block = 10 - addr_per_block = 10 - 4 = 6
Sprawdzamy, że 6 < 16 (addr_per_block do kwadratu), czyli szukany blok jest w strefie podwójnej pośredniości.

Pobieramy adres bloku podwójnie pośredniego w i-węźle: 185. Wczytujemy blok 185. Mamy 6 > > 2 = 1, czyli szukany blok leży w drugim bloku pojedynczym pośrednim (z zapisanych w bloku o numerze 185). Pobieramy numer tego bloku; jest to 114.

Wczytujemy blok 114. Numer interesującego nas bloku jest na pozycji 6 & (addr_per_block - 1) = 6 & 3 = 2. Zatem numer fizyczny bloku o numerze logicznym 22 to 600.


Janina Mincer-Daszkiewicz