W systemie operacyjnym szybkość dostępu do danych zgromadzonych na urządzeniach pamięci masowej odgrywa bardzo ważną rolę. W większości są to urządzenia blokowe (przede wszystkim twarde dyski oraz napędy CD-ROM, DVD, stacje dyskietek). Umożliwiają one swobodny dostęp do dowolnych danych na nich zgromadzonych. Jednakże operacje we/wy (odczytu i zapisu) są dosyć kosztowne czasowo (szczególnie dla urządzeń o wymiennych nośnikach). Dlatego też stosuje się różne mechanizmy minimalizacji rzeczywistej ilości operacji wejścia/wyjścia (dostępu do fizycznych bloków na urządzeniu). Jednym z takich rozwiązań jest podręczna pamięć buforowa.
Jest to wydzielony fragment pamięci fizycznej (to jest podstawowy warunek - bufory nie mogą być tymczasowo przerzucone na dysk w przypadku niedoboru pamięci w systemie), w którym odwzorowuje się bloki urządzeń w postaci buforów bloków. W tym mechanizmie przechwytywane jest każde żądanie dostępu do fizycznego bloku urządzenia i najpierw żądany blok (w operacji odczytu bądź zapisu) jest kopiowany do pamięci buforowej. Cechą charakterystyczną tej metody jest opóźniony zapis. Każda modyfikacja bloku odbywa się w pamięci buforowej, a zmodyfikowany blok jest zapisywany do urządzenia dopiero po upływie pewnego ustalonego czasu bądź w razie innej potrzeby (np. braku miejsca na nowe bufory).
Rys. 2.1 Miejsce podręcznej pamięci buforowej w systemie plików
W warstwowym modelu systemu plików Linuksa podręczna pamięć buforowa znajduje się w pomiędzy warstwą poszczególnych systemów plików a sterownikami urządzeń blokowych. Wszystkie żądania odczytu i zapisu bloków do urządzeń fizycznych przechodzą przez podręczną pamięć buforową, która dla każdego czytanego i zapisywanego bloku urządzenia tworzy bufor w pamięci fizycznej komputera.
Rys. 2.2 Schemat algorytmu obsługi żądania odczytu bloku
Gdy proces żąda odczytu jakiegoś bloku urządzenia (wywołując jakąś funkcję systemową odczytu pliku) to najpierw sprawdzana jest podręczna pamięć buforowa, czy nie zawiera kopii tego bloku. Jeśli bufor tego bloku jest w pamięci i jest on aktualny, to nie następuje żadna operacja odczytu bloku urządzenia, a zamiast tego dane z bufora są kopiowane do przestrzeni adresowej użytkownika. Dopiero w przypadku, gdy takiego buforu nie ma, albo jest, ale nie jest aktualny (jego dane nie zgadzają się z tymi na urządzeniu), wywoływana jest operacja we/wy dla tego urządzenia.
Gdy zaś proces żąda zapisu jakiegoś bloku urządzenia (wywołując jakąś funkcję systemową zapisu pliku) to najpierw również sprawdzana jest podręczna pamięć buforowa, czy nie zawiera kopii tego bloku. Jeśli bufor tego bloku jest w pamięci, to nie następuje żadna operacja odczytu bloku urządzenia, a zamiast tego dane są zapisywane do tego buforu i przeznaczony zostaje on do późniejszego zapisu. Dopiero w przypadku, gdy takiego buforu nie ma, wywoływana jest operacja odczytu tego bloku z urządzenia, tworzony jest dla niego bufor, dane zostają zapisane do buforu i zostaje on przeznaczony do późniejszego zapisu.
By efektywnie zarządzać pamięcią buforową bufory są zorganizowane w odpowiednie struktury danych pozwalające na szybkie ich odnajdywanie i wykonywanie odpowiednich czynności (np. przeznaczenie wybranych buforów do zapisu). Należą do nich: tablica z haszowaniem, listy LRU i listy wolnych buforów.
Każdy blok urządzenia przechowywany jest w postaci buforu składającego się z dwóch części:
Rys 2.4 Schemat odwzorowania bloku urządzenia w pamięci buforowej
Nagłówki bufora (struktura buffer_head) znajdują się w taflowej pamięci podręcznej (tzw. SLAB cache), a blok z danymi (tablica typu char) na specjalnie rezerwowanych stronach pamięci.
Nagłówek bufora zawiera dane o samym buforze (m.in. numer urządzenia i bloku, rozmiar bloku), jak i informacje pomocnicze służące do organizacji i zarządzania buforami (m.in. dowiązania do różnych list, stan buforu, czas kiedy ma być zapisany). Wykaz i omówienie jego wszystkich pól zawarte są w dalszej części opracowania.
Każdy blok jest identyfikowany przez numer bloku i numer urządzenia. Ta reprezentacja bloku w pamięci buforowej jest jednoznaczna, gdyż w danej chwili istnieje w pamięci tylko jeden bufor identyfikowany daną parą tych wartości.
By szybko odnaleźć przydzielony bufor dla określonego bloku używa się w tym celu tablicy hash_table[] z rozwiązywaniem kolizji metodą łańcuchową, tzn. elementy, dla których obliczone są te same indeksy w tablicy, połączone są w listę dwukierunkową. Do wyznaczenia indeksu takiej listy używa się funkcji haszującej (podana niżej przy szczegółach implementacji), która używa numeru urządzenia i numeru bloku. Po obliczeniu wartości funkcji haszującej dla poszukiwanego buforu, należy jeszcze przejrzeć po kolej listę znajdującą się pod obliczonym indeksem, w celu jego znalezienia. Rozmiar tej tablicy jest obliczany przy inicjalizacji pamięci buforowej, podczas startu systemu w procesie init() na podstawie ilości stron pamięci fizycznej.
W tablicy tej znajdują się tylko te nagłówki buforów, które obsługują przydzielone bufory.
Rys. 2.5 Fragment przykładowej tablicy hash_table[]
Są to dwukierunkowe listy cykliczne nagłówków używanych buforów.
W obecnej wersji jądra są cztery takie listy - każda zawiera inny typ buforów:
Typ | Zawartość |
---|---|
BUF_CLEAN | bufory aktualne |
BUF_LOCKED | bufory zablokowane (skierowane do zapisu) |
BUF_DIRTY | bufory zmodyfikowane, jeszcze nie skierowane do zapisu |
BUF_PROTECTED | chronione - ciągle obecne bufory Ramdisku |
* Zmiana w wersji 2.4.7 *
W stosunku do poprzedniej wersji jądra (2.2.x) zwiększono liczbę list LRU - dodano nowy typ listy LRU - BUF_PROTECTED.
Początki wszystkich list są umieszczone w globalnej tablicy list LRU lru_list[] indeksowanej typami list.
Rys. 2.6 Przykładowa tablica list LRU lru_list[]
Listy LRU realizują algorytm LRU.
Jego szczegóły oraz analizę można znaleźć w książce Silberschatza "Podstawy systemów operacyjnych". Dla krótkiego przypomnienia jest to algorytm zastępowania buforów w pamięci, który do oszacowania najbliższej przyszłości używa niedawnej przeszłości. Przy braku wolnych buforów zastępowane są najdawniej użyte. Potrzebuje on jakiegoś sposobu segregowania czasowego bloków przy odwoływaniu się do nich.
W Linuksie segregowanie czasowe jest zaimplementowane przez kolejkowanie buforów na listach LRU.
Co ciekawe w praktyce korzysta się z algorytmu LRU tylko przy zapisywaniu buforów zmodyfikowanych (z listy BUF_DIRTY). Zatem tak naprawdę pozostałe listy nie są potrzebne (poza tym, że segregują bufory według ich typów).
Są to dwukierunkowe listy cykliczne zawierające, jak wskazuje nazwa, nagłówki wolnych buforów.
Każda pojedyncza lista zawiera tylko wolne bufory jednej określonej wielkości bloku. W obecnej wersji jądra dopuszczalnych jest tylko 7 różnych wielkości bloków:
Początki wszystkich 7 list są umieszczone w globalnej tablicy list wolnych buforów free_list[] indeksowanej kolejnymi pozycjami wielkości bloków. Do przeliczania rozmiaru bloku na indeks w tablicy wolnych buforów służy specjalne makro.
Rys 2.7 Fragment przykładowej tablicy list wolnych buforów free_list[]
W momencie przydzielania nowego buforu o danym rozmiarze jest on usuwany z odpowiedniej listy buforów wolnych i wstawiany do tablicy z haszowaniem i odpowiedniej listu LRU.
Przechowuje bufory, które nie są już wykorzystywane. Stanowią one rezerwę, która jest wykorzystywana w przypadku braku odpowiednich buforów na liście wolnych buforów. Gdy liczba buforów nieużywanych przekroczy ustalony limit, to pamięć przez nie zajmowana jest zwalniana.
W danej chwili bufor znajduje się:
Gdy bufor jest zwalniany, usuwany jest z tablicy z haszowaniem i listy LRU i dołączany do odpowiedniej listy wolnych buforów.
W celu zapewnienia, że bufor zmodyfikowany zostanie zapisany, jak i odśmiecania pamięci buforowej ze starych buforów w razie zapotrzebowania na nowe bufory w Linuksie używa się:
Jest to demon zrealizowany jako wątek jądra, uruchamiany przy inicjalizacji systemu w procesie init (init/main.c).
Zajmuje się on kompleksową obsługą listy zmodyfikowanych buforów. W nieskończonej pętli zapisuje określoną parametrem liczbę buforów zmodyfikowanych, jeśli dodatkowo po tym zabiegu, stosunek liczby zmodyfikowanych buforów do ich całkowitej liczby, jest zbyt duży, to ponownie "wyrzuca" do urządzenia następną porcję buforów, a w przeciwnym przypadku przełącza się w stan uśpienia. Może być dodatkowo obudzony w razie niedoboru wolnych buforów, albo potrzeby zmniejszenia pamięci buforowej.
Jego parametry mogą być modyfikowane podczas działania.
Ten demon pracuje jako drugi wątek jądra związany z zarządzaniem pamięcią buforową. Podobnie jak bdlfush, uruchamiany jest przy starcie systemu. W nieskończonej pętli, co pewien okres czasu (podany jako parametr dla funkcji bflush), budzi się i synchronizuje stare bufory (dokładniej: zapisuje bloki specjalne, i-węzły oraz zmodyfikowane bufory, dla których minął czas opóźnienia).
Jest to funkcja do pozyskiwania wolnych buforów.
Przenosi bufor do właściwej listy LRU.
Podstawowa funkcja realizująca mechanizm podręcznej pamięci buforowej. Zwraca ona bufor dla określonego parametrami bloku urządzenia. To ona próbuje najpierw zlokalizować bufor żądanego bloku w pamięci. Gdy to się nie udaje, to przydziela nowy bufor o odpowiednim rozmiarze z listy wolnych buforów, wstawia go do tablicy z haszowaniem i odpowiedniej listy LRU. W przypadku braku odpowiedniego wolnego buforu próbuje go pozyskać (za pomocą refill_freelist()).
Funkcja zwraca bufor zawierający żądany blok. Używa do tego celu pamięci buforowej - jeśli znajdzie w niej odpowiadający blokowi aktualny bufor, to go zwraca, w przeciwnym przypadku odczytuje blok do bufora (obecnego w pamięci bądź nowoprzydzielonego) za pomocą funkcji ll_rw_block() (Low Level Read/Write Block - z pliku drivers/block/ll_rw_blk.c).
Uwaga: w poprzedniej wersji jądra Linuksa istniała funkcja breada() (Block Read Ahead), czytająca kilka kolejnych bloków z wyprzedzeniem. Obecnie jej już nie ma, ale sam mechanizm pozostał. Jest on nadal dostępny m.in. w funkcji obsługi żądania operacji blokowej dla urządzenia: ll_rw_block() z parametrem READA. Poza tym mechanizm ten realizują funkcje odczytu plików w konkretnych systemach plików.
Funkcja zwalnia blok: przydzielony mu bufor musi by zapisany na urządzeniu i zwolniony.
struct buffer_head { /* Pierwsza linia pamięci cache: */ struct buffer_head *b_next; /* dowiązanie na liście w tablicy z haszowaniem */ unsigned long b_blocknr; /* numer bloku */ unsigned short b_size; /* rozmiar bloku */ unsigned short b_list; /* typ listy LRU, na której znajduje się ten bufor */ kdev_t b_dev; /* numer urządzenia (B_FREE = bufor wolny) */ atomic_t b_count; /* liczba korzystających z buforu */ kdev_t b_rdev; /* numer rzeczywistego urządzenie */ unsigned long b_state; /* mapa bitowa stanu buforu */ unsigned long b_flushtime; /* czas kiedy zmodyfikowany bufor powinien zostać zapisany */ struct buffer_head *b_next_free;/* dowiązania na dwukierunkowych listach */ struct buffer_head *b_prev_free;/* LRU/wolnych buforów */ struct buffer_head *b_this_page;/* cykliczna lista buforów na jednej stronie */ struct buffer_head *b_reqnext; /* kolejka żądań operacji blokowych */ struct buffer_head **b_pprev; /* dwukierunkowa lista w tablicy z haszowaniem */ char *b_data; /* wskaźnik do bloku danych */ struct page *b_page; /* strona, na którą zmapowany jest ten nagłówek */ void (*b_end_io)(struct buffer_head *bh, int uptodate); /* obsługa zakończenia operacji we/wy */ void *b_private; /* zarezerwowane dla funkcji b_end_io */ unsigned long b_rsector; /* rzeczywista lokacja buforu na dysku (numer sektora) */ wait_queue_head_t b_wait; /* kolejka oczekujących na dostęp do buforu gdy jest zablokowany */ struct inode *b_inode; /* struktura i-węzła, do której należy blok buforu */ struct list_head b_inode_buffers; /* dwukierunkowa lista zmodyfikowanyh buforów dla danego i-węzła */ };
Uwaga:
Struktura nagłówka buforu jest tak zorganizowana, by najczęściej używane pola znajdowały się w jednej linii pamięci cache (16 bajtów). Ma to na celu zwiększenie szybkości (szczególnie odczuwalne na procesorach 32-bitowych).
* Zmiana w wersji 2.4.7 *
Określają m.in. do której listy LRU ma trafić bufor.
Nazwa bitu | Znaczenie |
---|---|
BH_Uptodate | 1 jeśli bufor odpowiada blokowi na dysku |
BH_Dirty | 1 jeśli bufor jest zmodyfikowany i musi być zapisany |
BH_Lock | 1 jeśli bufor jest zablokowany (np. przeznaczony do zapisu) |
BH_Req | 0 jeśli bufor został unieważniony |
BH_Mapped | 1 jeśli jest odwzorowany na dysk (tzn. ma odpowiadający mu fizyczny blok) |
BH_New | 1 jeśli bufor jest nowy i jeszcze nie zapisany |
BH_Protected | 1 jeśli bufor jest chroniony |
BH_PrivateStart | pierwszy wolny bit do prywatnych oznaczeń dla innych celów |
* Zmiana w wersji 2.4.7 *
Podawane jako parametr dla demona bdflush w unii jego parametrów (fs/buffer.c):
union bdflush_param { struct { ... int age_buffer; /* Czas dla zwykłego buforu, który musi upłynąć zanim zostanie zapisany */ ... } b_un; unsigned int data[N_PARAM]; } bdf_prm;Standardowo ustawiony jest na 0.3 s.
Zadeklarowany przedział dopuszczalnych wartości: [0.01 s - 60 s]
* Zmiana w wersji 2.4.7 *
Wywoływana jest po zakończeniu asynchronicznej transmisji dyskowej w celu aktualizacji nagłówków buforów dla odczytanych/zapisanych bloków.
gdzie:
MAX_BUF_PER_PAGE = (PAGE_CACHE_SIZE / 512)
hashfn(dev, block) = (((dev SHL (bh_hash_shift - 6)) XOR (dev SHL (bh_hash_shift - 9))) XOR ((block SHL (bh_hash_shift - 6)) XOR (block SHR 13) XOR (block SHL (bh_hash_shift - 12))))
dev - numer urządzenia
blok - numer bloku
bh_hash_shift - przesunięcie bitowe obliczane przy inicjalizacji pamięci buforowej
a SHL b - przesunięcie bitów w liczbie a o b pozycji w lewo
a SHR b - przesunięcie bitów w liczbie a o b pozycji w prawo
a odczytanie właściwej pozycji tablicy z haszowaniem odbywa się następująco:
hash(dev, block) = hash_table[(hashfn(HASHDEV(dev), block) AND bh_hash_mask)]
gdzie:
bh_hash_mask jest o 1 jeden mniejsza od rozmiaru tablicy z haszowaniem (obliczanego przy inicjalizacji pamięci buforowej)
* Zmiana w wersji 2.4.7 *
W jądrze 2.2.x funkcja haszująca miała postać:
hashfn(dev, blok) = (((unsigned)(HASHDEV(dev) XOR block)) AND bh_hash_mask)
a odczytanie właściwej pozycji tablicy z haszowaniem odbywało się bezpośrednio:
hash(dev, block) = hash_table[hashfn(dev, block)]
nr_hash = (ROZMIAR_STRONY * 2^order) / sizeof(struct buffer_head *)
gdzie:
order - wykładnik największej potęgi 2 mniejszej od liczby stron pamięci
5.1 Zalety:
5.2 Wady: