Do spisu treści
6.3.4 Przydział bloków dyskowych
Spis treści
W systemie Linux operacja alokacji bloków dyskowych realizowana jest przez funkcję systemową
ext2_new_block, której definicja znajduje się w pliku "balloc.c". Nagłówek tej funkcji
przedstawia się nastepująco:
int ext2_new_block (const struct inode *inode, unsigned long goal,
u32 *prealloc_count, u32 *prealloc_block, int err);
Parametr inode wskazuje na i-węzeł, dla którego przydzielamy blok, zaś parametr goal
podaje numer bloku, który chcielibyśmy zaalokować. Jeżeli nie będzie możliwa alokacja bloku o
tym numerze, funkcja ext2_new_block spróbuje przydzielić jakikolwiek
inny wolny blok.
Oto szczegółowy scenariusz działania:
Najpierw procedura
pobiera z podanego parametru inode wskaźnik do struktury opisującej super-blok bieżącej grupy
(tzn. grupy, do której należy podany i-węzeł) i blokuje ten super-blok. To blokowanie zapewnia, że żadne dwa procesy nie będą jednocześnie
próbować alokować bloków w tym filesystemie. Jest to konieczne,
ponieważ przydzielanie bloków wymaga modyfikacji struktur dyskowych
i ich odwzorowania w pamięci (w buforach). W szczególności często
modyfikowany jest sam super-blok.
W każdym filesystemie pewna liczba wolnych bloków zarezerwowana
jest dla administratora systemu. Gdyby nie było tego ograniczenia,
łatwo byłoby zablokować system i jedynym ratunkiem byłoby
zakończenie wszystkich zadań. Dlatego jeżeli liczba wolnych
bloków filesystemu zapisana super-bloku jest mniejsza niż
to minimum zarezerwowane dla administratora systemu, użytkownik
wywołujący procedurę nie jest administratorem systemu ani
nie ma odpowiednich uprawnień, to wykonanie procedury kończy
się z kodem błędu. W przeciwnym przypadku przechodzimy do
pierwszej fazy działania, w której próbuje się znaleźć wolny
blok w filesystemie.
Bloku zawsze próbujemy szukać najpier w bieżącej grupie,
dopiero potem szukamy w pozostałych. Poza tym, ponieważ sytuacja,
w której struktury opisujące stan filesystemu dość rzadko tracą
integralność, testowanie, czy jakaś grupa ma wolne bloki
rozpoczyna się zawsze od sprawdzenia licznika wolnych bloków
tej grupy. Można założyć, że jego wartość prawie zawsze będzie
zgodna z tym, co zawiera bitmapa.
Pobierany jest zatem deskryptor bieżącej grupy. Jeżeli licznik
wolnych bloków jest większy niz zero, to załadowana zostaje
bitmapa zajętości bloków tej grupy i sprawdzane jest, czy zadany
blok jest wolny. Jeśli jest, zostaje przydzielony, a jeśli nie,
próbuje się przydzielić jeden z następujących po nim kilkunastu
bloków, testując odpowiednie bity w bitmapie zajętości.
Liczba sprawdzanych tutaj bloków jest w zasadzie pseudolosowa,
ale ograniczona przez ilość bloków w grupie. Szukanie nowego
bloku do przydzielenia w pierwszej kolejności w najbliższym
sąsiedztwie podanego bloku ma zasadniczy sens dla prędkości
działania systemu plików. Właśnie temu, że podobne
przypadki szczególne uwzględniane są w algorytmie przydziału bloków,
system plików ext2 zawdzięcza wyjątkowo niski współczynnik
fragmentacji plików. Bloki danego pliku są prawie zawsze położone
blisko siebie i wczytywanie pliku jest szybkie.
Jeżeli nie udało się znaleźć wolnego bloku w najbliższym
sąsiedztwie, to najpierw przeszukujemy pozostałą, nie sprawdzoną
dotąd część bieżącej grupy. Mimo wszystko lepiej, aby blok
był w tej samej grupie, nawet, jeżeli do wolnego bloku w innej
grupie byłoby bliżej. Dzięki temu zwiększa się prawdopodobieństwo,
że wszystkie bloki pliku będą w tej samej grupie lub przynajmniej
w maksymalnie kilku, najlepiej sąsiadujących, dzięki temu
dane plików o i-węzłach z różnych grup są dodatkowo separowane od
siebie, co zwiększa stopień uporządkowania na dysku, a właśnie
taki cel przyświeca grupom w filesystemie.
W przypadku porażki poszukiwanie kontynuowane jest w
pozostałych grupach. Najpierw, poczynając od grupy następnej po
bieżącej, poszukuje się grupy, która ma jakieś wolne bloki
testując licznik wolnych bloków w deskryptorze. Tutaj wykorzystuje
się niski współczynnik awaryjności systemu ext2. Prawdopodobieństwo
tego, że gdzieś będą wolne bloki, a licznik będzie zero lub na
odwrót jest na tyle niskie, że można je zlekceważyć. Gdyby chcieć
testować po kolei wszystkie bitmapy, tracilibyśmy średnio o wiele
więcej.
Po znalezieniu takiej grupy ładowana jest jej bitmapa zajętości.
Następnie w pierwszej kolejności próbuje się znaleźć w tej grupie
ciąg co najmniej ośmiu następujących po sobie wolnych bloków
(cały wolny bajt w bitmapie). Czyni się tak po to, aby zwiększyć
szanse, że kolejne kilka bloków, które proces będzie chciał alokować,
będzie leżało tuż obok właśnie alokowanego. To jeden ze sposobów
unikania fragmentacji plików. Opłaca się to czynić, mimo, że czasem
konieczne jest powtórne przeszukanie bitmapy, jeśli się nie uda.
Procesy rzadko tworzą pliki o bardzo małych rozmiarach, dlatego
taki sposób postępowania nie powinien prowadzić do stopniowego
zrównywania rozmiarów wolnych, niezaalokowanych ciągów bloków,
choć widać, że warto byłoby wprowadzić dodatkowy parametr
sterujący, który mówiłby procedurze, czy alokowany plik jest
bardzo mały (wtedy nawet lepiej jest załatać nim małą dziurę)
czy raczej duży (wtedy dobrze jest szukać większych wolnych
ciągów bloków).
Jeżeli długi ciąg wolnych bloków nie zostanie znaleziony,
próbuje się znaleźć jakikolwiek wolny blok w tej grupie.
Jeżeli nie zostanie znaleziony, zgłoszony zostaje błąd systemowy
integralności informacji w deskryptorze (licznik wolnych bloków).
Jeśli zaś wolny blok zostanie znaleziony, to przechodzimy do
drugiej fazy działania.
Natomiast w przypadku, kiedy poszukiwanie całego wolnego bajtu
w bitmapie zakończyło się sukcesem, bitmapa przeszukiwana
jest od początku bajtu wstecz w celu zlokalizowania początku
tego wolnego obszaru. Skoro już znaleziono większy wolny
obszar, warto upychać dane od początku tego obszaru, żeby go
zbyt prędko nie sfragmentować. Przeszukiwanie, co łatwo zauwazyć,
wystarczy ograniczyć do siedmiu bloków w tył. Znalezione miejsce
jest miejscem przydziału nowego bloku.
Druga faza działania rozpoczyna się od sprawdzenia,
czy alokacja nowego bloku nie narusza nałożonej
na filesystem quoty. Jeżeli narusza, to wyjście z kodem błędu,
jeśli natomiast nie narusza, to w dalszej kolejności sprawdza się,
czy alokowany blok nie leży w obszarze systemowym. Jeżeli
nie leży, to podjęta zostaje próba ustawienia bitu zajętości
dla tego bloku w bitmapie zajętości. Ta próba w pewnych sytuacjach
może się nie powieść i wówczas cały algorytm, począwszy od początku
pierwszej fazy, wykonywany jest od nowa, do skutku, w pętli
nieskończonej. Nie jest jasne, co może być powodem niepowodzenia
takiej próby. Z uwagi na to, że algorytm przydziału bloków
zaczyna się od zablokowania super-bloku danego filesystemu,
a kończy jego odblokowaniem, nie jest możliwe, aby dwa procesy
wykonywały tę funkcję jednocześnie na rzecz tego samego filesystemu.
Po ustawieniu bitu zajętości rozpoczyna się trzecia faza,
w której dokonuje się opcjonalnej alokacji z wyprzedzeniem.
Chęć dokonania prealokacji deklaruje się za pośrednictwem parametru
prealloc_block. Próbuje się alokować dodatkowe bloki
spośród następujących bezpośrednio po pierwszym zaalokowanym,
ale maksymalnie osiem i tylko do końca bieżącej grupy
(tzn. nie więcej niż tyle, ile jest bloków w danej grupie
za bieżącym). Nie jest wcale oczywiste, że taki sztywny sposób
prealokacji ma sens. Funkcja nie przyjmuje żadnego parametru
sterującego, który wskazywałby liczbę bloków do prealokacji.
Jeżeli często tworzone są pliki o rozmiarach rzędu kilkunastu
bloków (czyli kilku kilobajtów, a przecież większość plików w
systemie jest takiego rozmiaru), a bloki są im przydzielane
sekwencyjnie, to średnio cztery bloki prealokowane są niepotrzebnie,
czyli około 20% długości pliku. Widać tutaj, że opisywany algorytm
można by usprawnić.
Prealokacja zaczyna się od próby wywołania alloc_block i,
jeżeli to się uda, próbuje się ustawić bit zajętości.
W przypadku, kiedy alokacja się uda, a ustawienie bitu zajętości nie,
dealokuje się blok. Po zakończeniu w parametrze prealloc_count
zwracana jest liczba prealokowanych w ten sposób bloków. Jest to
zwarty obszar, bo prealokacja kończy się po pierwszym niepowodzeniu.
Po fazie prealokacji następuje ostatnia, czwarta faza działania,
w której uaktualnia się struktury na dysku. W pierwszej kolejności
bitmapa bieżącej grupy przesłana zostaje na dysk. Jej bufor
oznaczony zostaje jako brudny i, jeśli jest to możliwe, wystartowana
zostaje transmisja asynchroniczna, a jeśli nie, to synchroniczna
i czeka się na jej zakończenie. Następnie
dodatkowo sprawdza się, czy przydzielone bloki nie są poza zakresem
i ewentualnie zgłaszany jest błąd systemowy. Nie jest jasne,
dlaczego ten warunek nie jest sprawdzany wcześniej.
Najprawdopodobniej sytuacja ta jest do tego stopnia nieprawdopodobna,
że minimalnie krótki czas zaoszczędzony na jej sprawdzaniu
procentuje średnio bardziej niż czas , który traci się, gdy ona
wystąpi.
Bufora przydzielonego dla załadowania bitmapy używa się na
załadowanie zadanego bloku funkcją getblk, zeruje się bufor
i oznacza jako brudny, do zapisania, po czym zwalnia się go
(faza inicjalizacji zerami nowego bloku). Podsystem obsługi
buforów zapisze go na dysk. Bufory bloku z deskryptorami grupy i
super-bloku także oznacza się jako brudne, do zapisania na dysk,
ponieważ ich zawartość zmieniła się. Ostatecznie zwalnia się
semafor super-bloku i kończy wykonanie procedury, zwracając numer
zaalokowanego bloku.
Powyższy opis można skonfrontować z oryginalnym kodem procedury
ext2_new_block() opatrzonym
komentarzami.
Bibliografia
- Pliki źródłowe Linuxa:
- linux/fs/ext2/balloc.c
- linux/include/linux/ext2_fs.h
- inode.h
- Projekt Linux
- Linux Kernel Hackers Guide
- Maurice J. Bach "Budowa systemu operacyjnego UNIX"
Autor: Krzysztof Ostrowski