Obsługa stronicowania w Linuksie 2.4

Autor: Andrzej Gąsienica-Samek

Wstęp

Zarządzanie pamięcią w Linuksie jest oparte o mechanizm stronicowania. Przypomnijmy podstawowe pojęcia:

Na początku chciałbym zwalczyć pewnien mit. Mit ten może być zakorzeniony w umysłach szczególnie tych osób, które miały kontakt z jądrem 2.2:

Maksymalny rozmiar wirtualnej przestrzeni adresowej
nie ma związku z maksymalnych rozmarem pamięci RAM

Procesory 32 bitowe posiadają 4 GB wirtualną przestrzeń adresową, która odwzorowuje część pamięci fizycznej. Jednak rozmiar wirtualnej przestrzeni adresowej nie ma bezpośredniego związku z rozmiarem pamięci, jaką procesor może obsługiwać. W obecnych czasach rozmiary pamięci RAM liczone w dziesiątkach gigabajtów robią wrażenie, ale już nie dziwią. Rozmiar maksymalnej obsługiwanej pamięci RAM zależy jedynie od liczby bitów jakie są przeznaczone na numer ramki we wpisie w tablicy stron. Jeśli liczba bitów jest mała, wtedy maksymalny rozmiar RAM jest znacznie mniejszy niż maksymalny rozmiar wirtualnej przestrzeni adresowej. Jeśli liczba bitów jest duża (np. 32), wtedy maksymalny rozmiar RAM jest znacznie większy i nawet na 32 bitowych architekturach może przekraczać 4 GB.

Ogólne zasady zarządzania pamięcią w Linuksie

Strefy pamięci

W Linuksie pamięć fizyczna została podzielona na 3 strefy:

Strefa pamięci opisywana jest przez strukturę zone_struct, której definicja znajduje się w pliku include/linux/mmzone.h. Jej główne pola to:

typedef struct zone_struct {
    spinlock_t lock;
    /* liczba wolnych ramek */
    unsigned long free_pages; 
    /* służą do utrzymywania odpowiedniego poziomu wolnych ramek */
    unsigned long pages_min, pages_low, pages_high;
    /* Struktury danych dla algorytmu bliźniaków
       realizującego przydział ramek ze strefy */
    free_area_t free_area[MAX_ORDER];
    ...
} zone_t;

Tablica wszystkich ramek

Każda ramka pamięci operacyjnej posiada opisującą ją strukturę page. Struktura ta opisuje obecny sposób wykorzystania ramki, zawierają również pola specyficzne dla każdego sposobu wykorzystania. Definicja struktury page znajduje się w pliku include/linux/mm.h:

typedef struct page {
    /* Przeznaczenie dowiązań jest zależne
       od obecnego sposobu wykorzystania ramki */
    struct list_head list;
    /* Licznik odwołań */
    atomic_t count;
    /* Flagi użycia */
    unsigned long flags;
    /* Lista czekających na odblokowanie strony */
    wait_queue_head_t wait;
    /* Adres ramki w przestrzeni adresowej jądra,
       NULL jeśli ramka strefy HighMem i nie odwzorowana */
    void *virtual;
    /* Strefa pamięci, do której należy ramka */
    struct zone_struct *zone;
    /* Kilka pól wykorzystywanych przy obsłudze
       plików i pamięci podręcznej */
    ...
} mem_map_t;

W pliku mm/memory.c znajduje się deklaracja tablicy mem_map, inicjowanej w trakcie startu systemu:

mem_map_t * mem_map;

Podstawowym polem struktury page jest count, czyli licznik użycia. Jest on zwiększany:

Dodstęp do tego pola jest realizowany za pomocą specjalnych makr zdefiniowanych razem ze strukturą, jak np. get_page(), put_page(), page_count().

Flagi flags mówią o stanie strony i składają się ze stałych PG_XXX. Dla każdej stałej jest zdefiniowane odpowiednie makro służące do testowania, np:

#define PG_locked 0 // strona zablokowana
#define PG_slab 8   // strona wykorzystywana przez alokator płytowy
#define PageLocked(page)        test_bit(PG_locked, &(page)->flags)
#define LockPage(page)          set_bit(PG_locked, &(page)->flags)
#define PageSlab(page)          test_bit(PG_slab, &(page)->flags)
#define PageSetSlab(page)       set_bit(PG_slab, &(page)->flags)

Obsługa tablicy stron

Linux stosuje trójpoziomowe stronicowanie składające się z trzech głównych struktur: globalnego katalogu stron, pośredniego katalogu stron i tablicy stron. Krótko omówię pliki nagłówkowe związane z obsługą struktur stronicowania. Większość makrodefinicji składa się z jednego wiersza i dlatego najlepszym sposobem na poznanie ich wszystkich jest zajrzenie do samego kodu. Następnie pokażę konkretny przykład znajdowania ramki (struktury page) na podstawie adresu liniowego, który bardzo dobrze obrazuje sposób posługiwania się tablicami stron.

Plik include/asm/page.h

W pliku tym zawarte są deklaracje dotyczące wielkości strony i definicje typów elementów globalnego katalogu stron, pośredniego katalogu stron i tablicy stron. Dla procesora 80386 są one następujące:

#define PAGE_SHIFT      12  // log rozmiaru strony
#define PAGE_SIZE       (1UL << PAGE_SHIFT) // rozmiar strony = 4096
typedef struct { unsigned long pte_low; } pte_t; // element tablicy stron
typedef struct { unsigned long pmd; } pmd_t; // element pośredniego katalogu
typedef struct { unsigned long pgd; } pgd_t; // element globalnego katalogu

Plik ten dostarcza również definicji stałej PAGE_OFFSET, która oznacza początek wirtualnej przestrzeni adresowej przeznaczonej dla jądra. Standardowo jest ona równa 3 GB. Stała ta jest bardzo istotna, gdyż struktury używane do stronicowania muszą znajdować się w części pamięci na stałe odwzorowywanej przez jądro. Dodatkowo musi istnieć prosty mechanizm przeliczania adresu logicznego tablicy stron, na adres fizyczny. Z tego względu pierwsza część przestrzeni adresowej powyżej PAGE_OFFSET odwzorowuje bezpośrednio strefy DMA i Normal. Dzięki temu przeliczanie z adresu logicznego na fizyczne jest, w tym przypadku, bardzo proste:

/* Przeliczenie adresu liniowego odwzorowującego w przestrzeni jądra
   strefę DMA lub Normal na adres fizyczny */
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
/* Przeliczenie w drugą stronę */
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))

Plik include/asm/pgtable.h

W pliku tym znajdują się makra opisujące bity ochrony stronicowania i wykonujące operacje na tablicach stron. Są to bardzo proste makra występujące w ogromnej ilości. Aby poczuć klimat tego pliku przytoczę tylko kilka z nich dla procesora 80386:

/* Tworzy element pte_t na podstawie wskaźnika do struktury page
   i praw dostępu */
#define mk_pte(page, pgprot)    __mk_pte((page) - mem_map, (pgprot))
/* Ustawienie pozycji tablicy stron */
#define set_pte(pteptr, pteval) (*(pteptr) = pteval)
/* Przesunięcie adresu dające pozycję w globalnym katalogu stron */
#define PGDIR_SHIFT     22
/* Liczba elementów w globalnym katalogu stron */
#define PTRS_PER_PGD    1024
/* Zwraca indeks wpisu opisującego address w globalnym katalogu stron */
#define pgd_index(address) ((address >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
/* Zwraca wskaźnik do elementu globalnego katalogu stron,
   odnoszącego się do adresu addess.
   Parametr mm jest deskryptorem pamięci procesu */
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))

Plik include/asm/pgalloc.h

Plik ten dostarcza deklaracji funkcji do przydzialania pamięci na tablice stron. Dostępne są funkcje przydzialające pamięć natychmiast (na potrzeby jądra) lub powoli (na potrzeby użytkownika). Jądro posiada specjalną pamięć podręczną zawierającą wolne ramki przeznaczone na tablice stron i katalogi stron. Funkcje zadeklarowane w tym pliku są zdefiniowane w pliku mm/memory.c. W tym pliku znajdują się również deklaracje funkcji służacych do czyszczenia buforów translacji procesora. Jest to niezbędne przy zmianie tablic stron.

/* Przydziela pamięć (zazwyczaj ramkę) na tablicę stron.
   Może powodować przełączenie kontekstu */
static inline pte_t *pte_alloc_one(struct mm_struct *mm,
                                   unsigned long address) {...}
/* Przydziela natychmiast pamięć na tablicę stron */
static inline pte_t *pte_alloc_one_fast(struct mm_struct *mm,
                                        unsigned long address) {...}
/* Zwalnia tablicę stron odkładając ją na listę */
extern __inline__ void pte_free_fast(pte_t *pte) {
        *(unsigned long *)pte = (unsigned long) pte_quicklist;
        pte_quicklist = (unsigned long *) pte;
        pgtable_cache_size++;
}
/* Zwalnia tablicę stron zwalniając ramkę */
extern __inline__ void pte_free_slow(pte_t *pte) {
        free_page((unsigned long)pte);
}
#define pte_free(pte)           pte_free_slow(pte)

Przykład korzystania z tablicy stron

Zgodnie z obietnicą przedstawię teraz przykład wykorzystania tablicy stron z pliku mm/memory.c. Następująca funkcja na podstawie adresu liniowego zwraca wskaźnik do struktury page reprezentującej ramkę zamapowaną w tym adresie. Jest używana w przypadku błędu obsługi strony. Dodatkowo wykonuje parę sprawdzeń:

static struct page * follow_page(unsigned long address, int write) {
        pgd_t *pgd;
        pmd_t *pmd;
        pte_t *ptep, pte;
    /* Znajdujemy wskaźnik do elementu w globalnym katalogu stron */
        pgd = pgd_offset(current->mm, address);
    /* Sprawdzamy czy ten element wskazuje na pośredni katalog stron */
        if (pgd_none(*pgd) || pgd_bad(*pgd))
                goto out;
    /* I wyciągamy wskaźnik do elementu w pośrednim katalogu */
        pmd = pmd_offset(pgd, address);
        /* Jesli wskazuje on na tablicę stronę */
        if (pmd_none(*pmd) || pmd_bad(*pmd))
                goto out;
    /* To wyciągamy wskaźnik do elementu tablicy stron */
        ptep = pte_offset(pmd, address);
    /* I sam element tablicy stron */
        pte = *ptep;
    /* Sprawdzenie czy strona znajduje się w pamięci */
        if (pte_present(pte)) {
            /* Jeśli potrzebujemy prawa zapisu, to sprawdźmy czy są.
                   Linux w specjalny sposób używa flagi dirty */
                if (!write ||
                    (pte_write(pte) && pte_dirty(pte)))
                        /* Obliczenie wskaźnika do struktury page 
                           na podstawie elementu strony */
                        return pte_page(pte);
        }
    /* A to przykład kodowania niestrukturalnego,
           nie posiadającego żadnego uzasadnienia */
out:
        return 0;
}

Obsługa strefy HighMem

Ramki ze strefy HighMem mogą być bez problemu przydzielane do przestrzeni użytkownika. W takim przypadku jądro uzyskuje możliwość dostępu do tych ramek, jeśli tylko działa na rzecz odpowiedniego procesu. W takim przypadku ramki ze strefy HighMem są zamapowene w tablicy stron procesu. Przydatna jest jednak możliwość skorzystania z pamięci HighMem również w innych przypadkach. Na przykład jądro wykorzystuje tą pamięć do obsługi buforów dyskowych. Aby było to możliwe jądro dostarcza odpowiednich funckji w pliku nagłówkowych include/asm/highmem.h:

/* Ta funkcja sprawdza, czy ramka jest zamapowana na stałe
   w przestrzeni adresowej jądra, jeśli nie to mapuje ramkę */
static inline void *kmap(struct page *page) {
        if (in_interrupt())
                BUG();
        if (page < highmem_start_page)
                return page_address(page);
        return kmap_high(page);
}
/* Wykonuje odmapowanie */
static inline void kunmap(struct page *page) {
        if (in_interrupt())
                BUG();
        if (page < highmem_start_page)
                return;
        kunmap_high(page);
}

Po zastosowaniu kmap pole virtual stuktury page zawiera wskaźnik, pod którym dostępna jest dana ramka w przestrzeni adresowej jądra. Po zakończeniu użycia należy wykonać kunmap. Definicje funkcji znajdują się w pliku mm/highmem.c.

Co było pierwsze...? Czyli skąd to wszystko się bierze

W pliku include/asm/pgtable.h znajduje się deklaracja funkcji paging_init(), która odpowiada za włączenie stronicowania. Jest ona zdefiniowana w arch/xxx/mm/init.c. Funkcja ta inicjuje struktury stref, początkowe tablice stron i włącza stronicowanie. Następnie wywołuje funkcję free_area_init() zadeklarowaną w include/kernel/mm.h, a zdefiniowaną w mm/page_alloc.c, w celu stworzenia tablicy mem_tab. W niedługim czasie wykonywana jest funkcja mem_init() z mm/memory.c, która inicjuje pozostałe struktury związane z obsługą pamięci (np. struktury algorytmu buddy) i informuje użytkownika o ilości dostępnej pamięci.

Koniec