Alokacja i dealokacja pamięci dla jądra (mm/slab.c)

Jednymi z najczęstszych operacji wykonywanych przez jądro systemu operacyjnego są przydział i zawalnianie pamięci, dlatego potrzebny jest do tego zadania jak najbardziej efektywny aprat. Ponieważ alokator oparty na alogrytmie bliźniaków nie zapewnia wystarczającej wydajności, dlatego w Linuxie (począwszy od wersji 2.2) używany jest zapożyczony z SunOS'a alokator płytowy (ang. Slab Allocator), zwany też taflowym.

Idea jego działania opiera się na spostrzeżeniu, że obiekty alokowane przez jądro często są tego samego typu (a co za tym idzie, rozmiarów), co umożliwia grupowanie ich razem i powtórne używanie zainicjowanych obiektów, dzięki czemu zyskujemy czas który trzeba byłoby poświęcić na powtórne ich zainicjowanie. Szczegóły implementacji alokatora płytowego zapewniają także efektywne wykorzystanie sprzętowej pamięci podręcznej procesora. Drugą ważną korzyścią ze stosowania alokatora płytowego jest znaczące zmieniejszenie fragmentacji wewnętrznej.

Głowną jednostką zarządzania pamięcią przez alokator płytowy są połączone w listę cache (pamięci podręczne),  zawierające obiekty tego samego typu. Cache jest złożony z tzw. płyt (ang. slab) będących fragmentami pamięci składającymi się z kolejnych stron, gdzie liczba tych stron jest potęgą 2. Przy inicjowaniu jądra tworzone są cache dla różnych rodzajów danych, np. dla deskryptorów cache, dla deskryptorów plików, itp.
Istnieje poza nimi lista cache dla obiektów różnych rozmiarów, od 32 bajtów do 128 kilobajtów, dzięki czemu można łatwo wyszukać odpowiedni cache dla obiektów danego rozmiaru (przechowywane są w struturze cache_sizes).

Płyty służą bezpośrednio do przechowywania obiektów, które są połączone w listę dwukierunkową.
Każda płyta ma wskaźnik do następnej, są one połączone w listę o nastepującym uporządkowaniu: najpierw pełne, potem zapełnione częściowo a na końcu puste.

Typem danych opisującym cache jest kmem_cache_t, najważniejsze jego pola to:

Ważną cechą alokatora płytowego jest jednorazowę wywołanie konstrktora obiektu, następne obiekty w jego miejscu nie są juz konstruowane. Jednak w systemie Linux konstruktor (i destruktor) ustawiane są i tak zawsze na NULL (dla większenia wydajności), więc nie można zaobserwowac korzyści z tego podejścia.

W celu lepszego wykorzystania sprzętowej pamięci podręcznej procesora stosuje się technikę zwaną kolorowaniem.
Polega ona na stosowaniu, przy alokowaniu obiektów w kolejnych płytach, zmiennego przesunięcia obiektów względem początku płyty. Kolor płyty pomnożony przez colour_off se struktury cache'a oznacza właśnie wyżej wspomniane przesunięcie. Maksymalny kolor zależy od ilości wolnego miejsca na płytach (pole free). Kolor jest zwiększany cyklicznie dla kolejnych płyt.

Drugą ważną strukturą jest slab_t o ważnych polach

Każdy obiekt przechowywany w cache także ma swój deskryptor, przechowywane są w nim między innymi
dowiązania do następnego wolnego obiektu ...

(!!! TODO !!!)

Opis działania funkcji kmalloc() i kfree()

Do przydzielania pamięci dla jądra służy funkcja kmalloc. Funkcja ta znajduje cache o najmniejszym rozmiarze nie większym od size po czym wywołuje wewnętrzną funkcję __kmem_cache_alloc z cache wskazanym przez element z cache_sizes, i zwraca oddany przez nią wynik.

Parametr flags może przyjmowac wartości:

  • GFP_USER - alokowanie pamięci na potrzeby użytkownika, w trakcie alokowania proces może zostać uśpiony
  • GFP_KERNEL - alokowanie dla jądra, może zostać uśpiony
  • GFP_ATOMIC - nie może zostać uśpiony, używane w procedurach obsługi przerwań
  • Funkcja ta, przy zablokowanych przerwaniach wykonuje makro kmem_cache_alloc_one, aż uda się przydzielić pamięć dla obiektu.
    Jeżeli, w trakcie przydzielania, wolne obiekty w cache zostały przydzielone komu innemu po prostu próbuje przydzielić jeszcze raz, po zwiększeniu cache, czyli stworzeniu nowej płyty.
    Jeżeli nie uda się powiększyć cache to zwraca NULL.
  • kmem_cache_alloc_one(cachep) (makro!)
  • Przydziela pamięć dla obiektu na pierwszej niepustej płycie w cache, a jeśli nie ma takiej to skacze (goto) do instrukcji zwiększenia cache.
    W celu przydzielenia pamięci wykonuje kmem_cache_alloc_one_tail
  • static inline void* kmem_cache_alloc_one_tail(kmem_cache_t *cachep, slab_t *slabp)
  • Tu wreszcie wykonuje się właściwe przydzielenie pamięci.
    Zwiększany jest licznik używanych obiektów w płycie, po czym zwracany jest wskaźnik do pierwszego (na liście) wolnego nieużywanego miejsca na niej (z uwzględenieniem przesunięcia o slabp->s_mem).
    Jeżeli płyta została po tym zapełniona, to wskaźnik na pierwszą niepełną płytę cache jest przesuwany na kolejną (slabp->list.next).

    Niestety w przypadku SMP procedura ta nieco się komplikuje.

    Jeżeli jądro jest skompilowane dla SMP, to oprócz obiektów trzymanych w głównym cache każde CPU posiada bufor do tymaczasowgo trzymania stworzonych obiektów. Rozmiar tego bufora zależy od rozmiaru obiektów w cache (pole objsize), przy czym im większy obiekt tym mniej jest ich tam trzymanych, a dla obiektów większych niż PAGE_SIZE rozmiar bufora wynosi 0.
    W strukturze kmem_cache_t znajduje się tablica cpudata zawierająca elementy typu cpucache_t dla każdego CPU. Pola te zawierają wartość avail - wole miejsce w buforze i limit - graniczenie opisane powyżej.

    W przypadku SMP w kmem_cache_alloc nie jest wywoływane bezpośrednio kmem-cache_alloc_one, tylko nastepujące czynności (przerwania wyłączone):
     

    1. jeżeli nie ma przydzielonegu bufora dla aktualnego procesora, to wywołaj kmem_cache_alloc_one
    2. w przeciwnym przypadku jeżeli jest miejce w buforze (avail > 0), to (uwaga!) pzrydzielane jest miejsce z niego
    3. w przeciwnym przypadku wywoływana jest funkcja kmem_cache_alloc_batch, która używając kmem_cache_alloc_one_tail przenosi cachep->batchcount (które jest ustawiane na połowę limitu bufora dla procesora) obiektów do głównego cache, zwiększając przy tym avail, po czym przydziela miejsce w buforze dla aktualnego procesora.


    Do zwalniania pamięci wykorzystuję się funkcję kfree
     

  • void kfree (const void*objp)

  •  

     

    Funkcja zwalniająca poprzednio zaalokowaną pamięć. Należy uważać, żeby zwalniać tylko pamięć przydzieloną przez kmalloc, inaczej działanie jej jest niekreślone.

    Przebieg działanie jest nastepujący:

    1. na zablokowanych przerwaniach z pomocą makra GET_PAGE_CACHE odnajduje cache do którego należy strona zajmowana przez obiekt
    2. wywołuje __kmem_cache_free dla znalezionego cache i danego obiektu
    Wywołana w niej została funckja
     
  • static inline void kmem_cache_free_one(kmem_cache_t *cachep, void *objp)
    1. z pomocą makra GET_PAGE_SLAB sprawdza w której płycie zawarty jest obiekt
    2. uwzględniając rozmiar obiektu w cache, rozmiar płyt i przesunięcie obiektów względem początku płyty oblicza numer obiektu w płycie
    3. wstawia na początek listy wolnych obiektów obiekt o wyliczonym numerze
    4. jeśli trzeba, przenosi odpowiednio deskryptor płyty w liście desktyptorów tak, aby zachowana była kolejność puste-częsciowe-pełne


    I znowu sutuacja dla SMP komplikuje się.
    Zmienia się przebieg działania funckji __kmem_cache_free, dla SMP działa nastepująco

    1. jeżeli nie ma bufora obiektów przydzielonego do aktualnego procesora, to używając funkcji free_block (dostającej jako parametry listę obiektów, i ich ilość) zwalnia podany obiekt
    2. w.p.p. sprawdza, czy avail < limit, jeżeli tak, to wstawia do bufora dla aktywnego procesora wskaźnik do obiektu (uwaga, usunięcie nastąpi z opóźnieniem)
    3. jeżeli nie ma wolnego miejsca w buforze, to korzystając z free_block zwalnia drugą połowę (czyli batchcount) zawartości bufora obiektów przydzielonego do aktywnego procesora

    Obsługa urządzeń wymiany (mm/swapfile.c, mm/page_io.c)


    W systemie Linux dyskowa pamięć pomocnicza może być realizowana na dwa sposoby

    Obsługa pamięci wymiany w pliku daje większą elastyczność, gdyż nie trzeba modyfikować tablicy partycji by przydzielić dodatkowe urządzenie, ale jest wolniejsza z powodu możliwej fragmentacji - rozmiar pliku wymiany jest stały, ale może nie zajmować on spójnego obszaru dysku.
    Poza tym wydajność zmniejsza też narzut spowodowany przez korzystanie z pośredniego systemu plików.

    Tych wad nie ma urządenie blokowe, służy ono wyłącznie do obsługi pamięci wymiany i zapewnia szybki, chociaż i tak wiele razy wolniejszy od dostepu do pamięci opracyjnej RAM (rzędu 100+ razy wolniej), bezpośredni dostęp.
    Pamięć wymiany jest więc (konieczną) ostatecznoscią i nie powinno się liczyć na nią jeśli najwazniejsza jest
    wydajność, chociaż dzięki zrzuceniu nieużywanych danych na dysk, a także zabezpieczeniu przed brakiem wolnej pamięci uzywanie jest przyności wymierne korzyści.

    W systemie może być aktywne więcej niż jedno urządznie wymiany, jednak ich ilość jest ograniczona przez stałą MAX_SWAP_FILES (linux/swap.h, w linuxie 2.4.7 równą domyślnie 8).

    Informacja o urządzeniach wymiany zawarta jest w tablicy swap_info o elementach typu swap_info_struct.

    Ważne pola tego typu to:

    Ilość urządzeń jest pamiętana w zmiennej nr_swapfiles, dodatkowo w strukturze swap_list trzymane są zmienne
    head - początek listy i next - następny do wykorzystania.
    W zmiennej least_priority pamiętany jest jest najmnijeszy aktualny priorytet, inicjowana jest ona na wartość 0.
    Urządzenia połącznone są w listę priorytetową według pola prio.

    Na początku każdego urządzenia znajduje się nagłówek określający rodzaj obszaru wymiany, etykietę urządzenia, wersję, ilość złych stron i ich tablicę.

    Opis działania funkcji:

    Przydzielanie urzadzenia:
     
  • asmlinkage long sys_swapon(const char* specialfile, int swap_flags)

  • Funkcja ta przyłącza urządzanie o zadanej nawie (np. "/dev/hda2", "/swap.file"). Aby można było przyłączyć urządzenie, musi byc ono wcześnie zainicjowane z przestrzeni użytkownika programem "mkswap".

    Flags może mieć maski:

    Działanie:
    1. sprawdza przywileje
    2. ustawia odpowiedni priorytet i inicjuje strukturę_swap_info_struct (przy tym, jeśli nie ma ustawionej flagi SWAP_FLAG_PREFER, ustawia priorytet na wartość zmiennej least_priority i zmnijesza ją)
    3. znajduje i-węzeł odpowiadający podanej nazwie
    4. jeżeli znaleziono urządzenie blokowe to
      1. ustawia na nim rozmiar bloku równy rozmiarze strony
      2. sprawdza, czy można na nim zapisywać
      3. sprawdza, czy podane urządzanie nie zostało jeszcze przyłącznone
    5. jeżeli jest zwykłym plikiem
      1. sprawdza, czy podane urządzanie nie zostało jeszcze przyłącznone
    6. ustawia odpowiednio rozmiar urządzenia
    7. na podstawie typu obszaru wymiany (versja 1, lub 2) odpowiednio ustawia pola struktury swap_info_struct
    8. po sprawdzniu dodatkowych warunków dodaje urządzenie wymiany, po zablokowniu spinlocka swaplock
    Przy inicjowaniu mapy urządzenia pomija strony zaznaczone jako złe.

    Do odłączenia przyłącznonego urządzenia służy funkcja

    Jej działanie jest nastepujące:
     
    1. sprawdza przywileje
    2. blokuje jądro (lock_kernel)
    3. po znalezieniu i-węzła urządzenia szuka go w liście swap_list
    4. blokuje dostep do listy urządzneń (swap_list_lock) listę
    5. usuwa urządzenie z listy
    6. po odblokowaniu listy urządzeń próbuje zwolnić podane urządzenie poprzez funkcję try_to_unuse, która przenosi strony z obszaru wymiany do pamięci RAM. Funkcja ta ewentualnie wywołuje schedule, gdyż operacja ta może trwać długo, co prz zablokowawanym jądrze spowodwało by zastój.
    7. jeśli zwróci ona błąd, tzn, nie udało się zaalokować wszystkich stron z obszaru wymiany, to wstawia spowrotem urządzenie na listę (blokując ją)
    8. usuwa urządznie z tablicy
    9. odblokowuje jądro


    W celu zalokowania pamięci w obszarze wymiany używamy funckji:
    (swp_entry ma jedno pole - unsigned long val, linux/shmem_fs.h)

    Przebieg jej działania jest taki:
    1. jeżeli count (liczb dowiązań do strony) jest większe, niż SWAP_MAP_MAX (linux/swap.h, równe 0x7fff), to zwraca błąd
    2. wywołuje swap_list_lock, czyli spin_lock(&swaplock)
    3. jeżeli nie ma urządzeń wymiany, lub nie ma wolnych stron zwraca wynik oznaczający brak pamięci
    4. dla kolejnych urządzeń na liście, począwszy od swap_list.next, jeśli można na nie zapisywać próbuje, po zablokowaniu ich spinlocka przydzielić miejsce na danym urządzeniu z pomocą funkcji scan_swap_map
    5. po przydzieleniu, jeśli nastepne na liści urządzenie ma taki sam priorytet, to przesuwa swap_list.next na nie, jesli nie to ustawia swap_list.next na swap_list.head jeśli tenże ma różny priorytet, dzięki czemu najpierw będą używane urządzenia z początku kolejki
    6. odblokowuje listę i zwraca wynik
    Jeżeli nie uda się przydzielić miejsca, nie zwraca błędu tylko liczbę 0 w polu val.
    Zdefiniwana jest także funkcja get_swap_page(void), która wywołuje __get_swap_page z liczbą 1. Funkcja scan_swap_map próbuje zoptymalizować rozkład stron w obszarze wymiany tak, aby zminimalizować czas dostępu do stron.
    Alokuje ona SWAPFILE_CLUSTER (swapfile.c, aktualnie 256) stron kolejno po sobie.
    Jeżeli si->cluster = 0, czyli już zaalokowano cały klaster, to szuka pustego klastra (ciągłego pustego obszaru o długości SWAPFILE_CLUSTER). Jeżeli i to się nie uda, to alokuje szukając wolnej strony.

    Operacją przeciwną jest swap_free (i __swap_free)

    Funkcja ta sprawdza najpierw poprawność wywołania (czy istnije urzadzenie, czy jest używane, czy entry jest poprawne).
    Jeśli wszytstko się zgadza, a wartość w swap_map odpowiadająca zwalnianej stronie jest nie mniejsza niz count, to odejmuje od tej wartości count, po czym, jeżeli nie zostały żadne dowiązania, to zwiększa nr_swap_pages.

    Do zapisu i odczytu z urządzenia wymiany uzywana jest zdefinowana w mm/page_io.c funkcja

    Jest ona tylko opakowaniem dla bardziej złożonej funkcji Funkcja ta po sprawdzeniu, czy spełnione są wszytkie konieczne warunki (poprawne urządzenie odpowiadające
    entry, poprawny plik urządzenia wymiany, jeśli jest plikiem) zaznacza, jeśli odczytujemy, że strona nie jest już aktualna, a jeśli zapisujemy że powinien być zmniejszony licznik odwołań do niej.
    Później wykonuje funckję brw, która zajmuje się już bezpośrednim zapisem, lub odczytem stron.
      Filip Kaliński (fk181140@zodiac.mimyw.edu.pl)