System plików

Podręczna pamięć buforowa w systemie Linux (jądro 2.4.7)

Obsługa nagłówków buforów, list wolnych buforów i list LRU


Krzysztof Gołębiowski


kg189210@zodiac.mimuw.edu.pl

10.12.2001



1. Wprowadzenie

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.

1.1 Podręczna pamięć buforowa (buffer cache)

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).

1.2 Wymagania względem realizacji podręcznej pamięci buforowej

  1. Jednemu blokowi urządzenia odpowiada co najwyżej jeden bufor w pamięci.
  2. Bufory zmodyfikowane powinny zostać KIEDYŚ zapisane na urządzeniu.
  3. Stwierdzenie, czy bufor bloku znajduje się w pamięci powinno być możliwie najszybsze.
  4. Powinien istnieć mechanizm odśmiecania pamięci buforowej, tzn. usuwania z niej dawno nie używanych buforów w celu przydzielenia nowych.

2. Realizacja mechanizmu buforowania urządzeń blokowych w Linuksie

  • Oryginalnie implementacja podręcznej pamięci buforowej oparta była na koncepcji opisanej przez M. J. Bacha w książce "Budowa systemu operacyjnego UNIX" (1986)
  • Obecnie jest to oddzielny system buforowej pamięci podręcznej
  • Uzupełniany jest przez zorientowane na pliki buforowanie stron pamięci (od wersji 2.0.x)

    2.1 Miejsce podręcznej pamięci buforowej w systemie plików Linuksa


    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.

    2.2 Dynamiczny system pamięci buforowej

  • Podręczna pamięć buforowa wykorzystuje pewną część stron pamięci fizycznej nie używanej przez jądro i procesy. Obecnie ze względu na mniejsze znaczenie mechanizmu buforowania urządzeń blokowych zmniejszono początkowy obszar pamięci przeznaczonej na pulę buforów. W razie potrzeby przydzielenia dodatkowych buforów, próbuje się pozyskać dodatkowe strony pamięci na system podręcznej pamięci buforowej.

  • Przy wzroście zapotrzebowania na pamięć podstawową miejsce przeznaczone na buforowanie bloków może być częściowo zwolnione na rzecz jądra albo procesów.

    2.3 Algorytm obsługi żądania dostępu do bloku dyskowego

    2.3.1 Algorytm obsługi żądania odczytu bloku


    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.

    2.3.2 Algorytm obsługi żądania zapisu bloku


    Rys 2.3 Schemat algorytmu obsługi żądania zapisu bloku

    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.

    2.4 Organizacja buforów w pamięci

    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.

    2.4.1 Format przechowywania bloku:

    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.

    2.4.2 Identyfikacja buforu

    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.

    2.4.3 Tablice z haszowaniem

    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[]

    2.4.4 Listy LRU

    Są to dwukierunkowe listy cykliczne nagłówków używanych buforów.

  • Cechy list LRU:

  • Rodzaje list LRU:

    W obecnej wersji jądra są cztery takie listy - każda zawiera inny typ buforów:

    TypZawartość
    BUF_CLEANbufory aktualne
    BUF_LOCKEDbufory zablokowane (skierowane do zapisu)
    BUF_DIRTYbufory zmodyfikowane, jeszcze nie skierowane do zapisu
    BUF_PROTECTEDchronione - 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.

    2.4.5 Algorytm LRU (Least Recently Used - najdawniej używane)

    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).

    2.4.6 Listy wolnych buforów

    Są to dwukierunkowe listy cykliczne zawierające, jak wskazuje nazwa, nagłówki wolnych buforów.

  • oddzielne dla poszczególnych wielkości blokó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:

  • tylko potęgi liczby 2: 512, 1024, 2048, 4096, 8192, 16384, 32768 bajtó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.

    2.4.7 Lista nieużywanych buforów

    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.

    2.4.8 Umiejscowienie buforu w omówionych strukturach danych

    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.

    2.5 Mechanizm aktualizacji stanu pamięci buforowej

    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ę:

  • Bdflush

    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.

  • Update

    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).

  • refill_freelist(int size)

    Jest to funkcja do pozyskiwania wolnych buforów.

  • refile_buffer(struct buffer_head *bh)

    Przenosi bufor do właściwej listy LRU.

    2.6 Wykorzystanie mechanizmu pamięci buforowej

  • Get block - getblk(kdev_t dev, int block, int size)

    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()).

  • Block read - bread((kdev_t dev, int block, int size)

    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.

  • Block release - brelse(struct buffer_head *bh)

    Funkcja zwalnia blok: przydzielony mu bufor musi by zapisany na urządzeniu i zwolniony.

    3. Wybrane szczegóły implementacyjne

    3.1 Nagłówek buforu

    3.1.1 Definicja struktury buffer_head z pliku nagłówkowego include/linux/fs.h

    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 *

  • zmiana typu kolejki dla oczekujących na odblokowanie buforu
  • zmiana kolejności pól struktury
  • dodanie pól dowiązań do struktur i-węzła

    3.1.2 Dokładniejszy opis ważniejszych pól struktury buffer_head

  • Typ listy LRU

    Nowo zainicjalizowane bufory są typu BUF_CLEAN.

  • Bity stanu buforu

    Określają m.in. do której listy LRU ma trafić bufor.

    Nazwa bituZnaczenie
    BH_Uptodate1 jeśli bufor odpowiada blokowi na dysku
    BH_Dirty1 jeśli bufor jest zmodyfikowany i musi być zapisany
    BH_Lock1 jeśli bufor jest zablokowany (np. przeznaczony do zapisu)
    BH_Req0 jeśli bufor został unieważniony
    BH_Mapped1 jeśli jest odwzorowany na dysk (tzn. ma odpowiadający mu fizyczny blok)
    BH_New1 jeśli bufor jest nowy i jeszcze nie zapisany
    BH_Protected1 jeśli bufor jest chroniony
    BH_PrivateStartpierwszy wolny bit do prywatnych oznaczeń dla innych celów

    * Zmiana w wersji 2.4.7 *

  • dodano bity: BH_Mapped i BH_New

  • b_flushtime

  • w tyknięciach zegara systemowego (tzw. jiffies - 1/100 s)
  • dla nowego buforu ustawiany na 0
  • przy modyfikacji buforu ustawiany na aktualny czas systemowy powiększony o czas opóźnienia zapisu dla zwykłych buforów (jiffies + age_buffer).

    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 *

  • usunięcie pola age_super - czasu opóźnienia dla bloku struktury inode albo bloku specjalnego

  • Funkcja obsługi końca operacji wejścia/wyjścia

    Wywoływana jest po zakończeniu asynchronicznej transmisji dyskowej w celu aktualizacji nagłówków buforów dla odczytanych/zapisanych bloków.

    3.2 Ustalone parametry ilościowe dotyczące pamięci buforowej


    3.3 Funkcja haszująca

    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)]

    3.4 Inicjalizacja pamięci buforowej

    4. Podsumowanie zmian w jądrze 2.4.7

    5. Zalety i wady pamięci buforowej:

    5.1 Zalety:

    5.2 Wady:


    6. Bibliografia

    1. "Podstawy systemów operacyjnych" - A. Silberschatz, J. L. Peterson, P. B. Galvin, WNT 1993
    2. "Linux Kernel - Jądro systemu" - M. Beck i inni, MIKOM 1999
    3. Pliki źródłowe Linuksa (jądro 2.4.7):
      • include/linux/fs.h - struktury danych, różne deklaracje
      • fs/buffer.c - funkcje obsługi podręcznej pamięci buforowej
      • drivers/block/ll_rw_blk.c - funkcje odczytu i zapisu bloków
      • init/main.c - treść procesu init
    4. Opracowania studentów Wydziału Matematyki, Informatyki i Mechaniki UW:
      • Linux: Algorytmy + Struktury danych (2000/2001)
      • Linux Podręcznik (1999/2000)
      • Projekt Lab-Linux (1997/1998)
      • Projekt Linux (1996/1999)