Valid HTML 4.01!

Zarządzanie pamięcią

Spis treści


A jak to wszystko wygląda w Linuksie ...


Organizacja katalogów stron jądra i użytkownika

Proces użytkownika widzi (przez segmenty) spójną przestrzeń adresową 0-4 GB. Górny gigabajt (3-4 GB) jest widoczny tylko w trybie jądra. W trybie użytkownika dane są rozmieszczane w zakresie 0-3 GB.

Sposób organizacji tablic służących do stronicowania jest w Linuksie bardzo podobny do organizacji adresowania pamięci przez procesory z rodziny x86 (powyżej 386, w trybie wirtualnym).

Każdy proces ma swój własny katalog tablic stron (rozmiaru jednej ramki - tak jak i tablica stron). Wskaźnik do niego znajduje sie w strukturze mm_struct pod nazwą pgd. Przy zmianie kontekstu Linux dba o to, by załadować go do odpowiedniego rejestru procesora.

Przesunięcie w adresie liniowym jest równocześnie przesunięciem względem początku wyznaczonej ramki.

Procesor z rodziny x86 znajduje adres danej strony przez wskaźnik podwójnie pośredni. Linux implementuje organizacje tablic zawierajacych adresy ramek trójpoziomowo (od wersji 2.6.11 są już cztery poziomy). Poprzez odpowiednie dyrektywy #define środkowa tablica jest traktowana jako pojedynczy wpis w katalogu tablic stron. W tej sytuacji procedury dotyczace środkowych tablic często tylko wywołują te dotyczące tablic trzeciego poziomu. Nie wystarczy jednak zmienić stałych, aby system mógł działać na procesorze o potrójnie pośrednim odwołaniu do pamięci, ponieważ wiele procedur zakłada, że środkowe tablice nie istnieją, np. funkcja alokująca pamięć na tę tablicę jest pusta.


Tablice stron

Ostatnia tablica stron (czyli trzecia) zawiera adresy ramek (o ile są rezydentne w pamięci). Ponieważ adres ramki jest wielokrotnością 4KB, więc najmłodsze 12 bitów ma zawsze wartość zero. Linux wykorzystuje 7 z nich do przechowywania dodatkowych informacji o stronie.

Pola adres są jednobajtowe, najmłodsze z nich ma 4 najmłodsze bity zerowe. Dużymi literami oznaczono bity informacyjne strony. Mają one dokładnie te same nazwy i układ, co bity obsługiwane sprzętowo w procesorach z rodziny x86.

Pozycja w tablicy stron
adres adres adres 4MDIRTYACCESSED PCDPWTUSERRWPRESENT=1

Opis poszczególnych bitów:

PRESENT
Kiedy procesor odwołuje się do danej strony i bit ten jest ustawiony na 1 oznacza to, że strona znajduje się w pamięci. Pola adres zawierają wtedy prawidłowy adres ramki w pamięci fizycznej. Jeśli przy odwołaniu do pamięci bit ten jest równy zero, to następuje błąd braku strony, a wszystkie pozostałe bity mają inne znaczenie, związane z obsługą błędu (określają położenie strony na urządzeniu wymiany).

RW
Ustawiony na 1 oznacza pozwolenie na zapis na danej stronie. Bit ten służy do organizacji współdzielenia stron. Ustawiony na 1, pozwala na zapis bezpośredni na stronę, a gdy jest równy zero, to przy zapisie jest wywoływany błąd ochrony strony.

USER
Bit mówiący o poziomie ochrony: użytkowy/systemowy.

ACCESSED
Linux zakłada, ze bit ten jest sprzętowo ustawiany na 1 w momencie odwołania do strony i wykorzystuje ten fakt do postarzania stron.

DIRTY
Ustawiany sprzętowo na 1 przy zapisie na stronę. Ułatwia wymianę stron.

4M
Oznacza rozmiar ramki wielkości 4MB.

Ponadto cały element tablicy stron może mieć wartość 0. Linux zakłada, że odwołując się do takiej strony procesor wywołuje procedurę obsługi błędu braku strony. Linux przydziela fizyczną ramkę procesowi dopiero w momencie odwołania się do niej. Zero jest początkową wartością, na jaką ustawiane są zwykle wpisy w katalogu i tablicach stron.

Interesujacy jest fakt, ze alokując nową pamięć dla procesu jądro dokonuje tego tylko logicznie - poprzez wpisy w tablicy stron i tablicy segmentów. Dla jądra natomiast pamięć jest alokowana od razu - znajdowana jest ramka w pamięci fizycznej. System nie wierzy użytkownikowi i stara się optymalizować jego ewentualne nadmierne żądania.

Alokowanie pamięci dla procesu odbywa się przez rozszerzenie segmentu, (funkcja sys_brk). Natomiast uzyskiwanie pamięci fizycznej jest realizowane w systemie zarządzania ramkami.

Funkcja get_free_page uzyskuje ramkę od wspomnianego modułu, zeruje ją i przekazuje jej adres liniowy. Stąd wniosek, że program dostaje nową pamięć wyzerowaną.

Funkcja pgd_alloc() tworzy nowy katalog stron, czyli dostaje ramkę, wypełnia ją zerami, a przestrzeń 3-4 GB odwzorowuje na pamięć jądra. Zera w katalogu stron oraz w tablicy stron są traktowane jako ramka alokowana przy pierwszym dostępie. Górny gigabajt jest przy przepisywaniu ustawiany na systemowy.

Dzięki temu, że tablice stron segmentu jądra są dla wszystkich procesów identyczne, każdy proces w trybie systemowym ma do czynienia z tym samym segmentem jądra. W segmencie jądra adresy logiczne są adresami fizycznymi przesuniętymi o 3 GB (z wyjątkiem wirtualnych obszarów pamięci odwzorowywanych za pomocą funkcji vmalloc()).


Pamięć wirtualna procesu

Każda struktura task_struct posiada pole mm będące wskaźnikiem do struktury mm_struct opisującej przestrzeń adresową procesu.


struct mm_struct {
    struct vm_area_struct * mmap;
    struct rb_root mm_rb;
    struct vm_area_struct * mmap_cache;
    pgd_t * pgd;
    atomic_t mm_users; 
    atomic_t mm_count; 
    int map_count; 
    struct rw_semaphore mmap_sem;
    spinlock_t page_table_lock;
    struct list_head mmlist; 

    unsigned long start_code, end_code; 
    unsigned long start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;
    unsigned long rss, total_vm, locked_vm;
    unsigned long def_flags;
    .....
};

mmap: lista liniowa VMA należących do tej przestrzeni adresowej posortowana wg adresów,
mm_rb: wskaźnik do korzenia drzewa RB (red-black); z drzewa korzysta się wówczas, gdy liczba VMAs wzrośnie powyżej pewnej granicy,
mmap_cache: wskaźnik do ostatnio używanego VMA,
pgd: Page Global Directory, czyli katalog stron procesu,
mm_users: liczba procesów współdzielących tę strukturę,
map_count: liczba VMAs,
mmap_sem: semafor do ochrony struktury,
start_code, end_code: adres początku (końca) sekcji kodu,
start_data, end_data: adres początku (końca) sekcji danych,
start_brk, brk: adres początku (końca) obszaru sterty,
total_vm: całkowita liczba stron używanych przez proces,

przestrzen adresowa procesu

Przestrzeń adresowa procesu składa się z (zazwyczaj) wielu rozłącznych spójnych obszarów pamięci. Każdy z nich jest opisany strukturą vm_area_struct.


struct vm_area_struct {
    struct mm_struct * vm_mm; 
    unsigned long vm_start;   
    unsigned long vm_end;     
    struct vm_area_struct *vm_next;
    pgprot_t vm_page_prot;  
    unsigned long vm_flags;
    struct rb_node vm_rb;
    struct vm_operations_struct * vm_ops;
    unsigned long vm_pgoff; 
    struct file * vm_file;  
     ......
};

vm_mm: przestrzeń adresowa, do której należy ten obszar VMA,
vm_start: adres startowy VMA,
vm_end: adres pierwszego bajtu ZA końcem VMA,
vm_next: następne VMA na liście,
vm_page_prot: prawa dostępu do tego VMA,
vm_rb: drzewo RB,
vm_pgoff: jeśli obszar należy do zamapowanego pliku, to jest to przesunięcie w pliku (w liczbie stron),
vm_file: jeśli obszar należy do zamapowanego pliku, to jest to wskaźnik do tego pliku,
vm_ops: zbiór wskaźników do funkcji realizujących operacje na tym VMA:


struct vm_operations_struct {
      void (*open)(struct vm_area_struct * area);
      void (*close)(struct vm_area_struct * area);
      struct page * (*nopage)(struct vm_area_struct * area, 
                     unsigned long address, int *type);
      int (*populate)(struct vm_area_struct * area, unsigned long address, 
         unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);

Na zbiór tych obszarów są nałożone dwie struktury:

  1. lista jednokierunkowa, posortowana po adresach początkowych,

  2. drzewo czerwono-czarne, również utworzone ze względu na adresy początkowe.

Lista jednokierunkowa służy do liniowego przeszukiwania pamięci w celu przejrzenia kolejnych stron przydzielonych procesowi. Drzewo czerwono-czarne jest przeznaczone do szybkiego wyszukiwania obszaru pamięci zawierającego podany adres (ta operacja jest wykonywana często, np. przy każdym błędzie braku strony). Ponadto w strukturze mm_struct znajduje się pole mmap_cache pełniące rolę "pamięci podręcznej" - pamięta się w nim dowiązanie do ostatnio używanego obszaru vm_area_struct.

Czy rzeczywiście użycie takiej algorytmicznie złożonej struktury danych jaką są drzewa czerwono-czarne było tu niezbędne? Nowy obszar pamięci wirtualnej procesu jest tworzony przy każdym powiększeniu się procesu (np. w wyniku wywołania funkcji systemowej brk), a także przy odwzorowywaniu pliku do pamięci wirtualnej. Liczba takich obszarów dla jednego procesu zazwyczaj oscyluje w granicach 6, choć w pewnych sytuacjach może osiągnąć 3000.

Każdy obszar pamięci wirtualnej może mieć przypisany zbiór operacji właściwych dla stron tego obszaru przy zapisywaniu ich na dysk, odczytywaniu, obsłudze błędów strony itp. Przy wykonywaniu takich operacji najpierw sprawdza się, czy odpowiednia funkcja jest zdefiniowana, a jeżeli tak, to się ją wykonuje. Jeżeli nie, to wykonuje się operację domyślną. W praktyce specjalne operacje są zdefiniowane dla stron pamięci dzielonej i dla obrazów plików dyskowych.


Tworzenie przestrzeni adresowej procesu - algorytm fork

Wydawać by się mogło, że naturalnym sposobem powielania procesu jest następujący schemat: kopiuje się obszar danych procesu macierzystego, a obszar kodu jest przez oba procesy, macierzysty i potomny, współdzielony. Taki algorytm jednak jest bardzo kosztowny w przypadku dłuższych programów, które w danej chwili mogą mieć część pamięci wirtualnej na dysku. W starszych systemach uniksowych istniały dwie funkcje: fork() i vfork(). Funkcja fork() działała tak jak opisano powyżej, a vfork() był wywoływany tylko wtedy, gdy następną instrukcją potomka było wywołanie exec(). W drugim przypadku nie kopiowano pamięci wirtualnej.

W Linuksie ten problem doczekał się lepszego rozwiązania. Stosuje się tu technikę zwaną copy on write, czyli kopiowanie przy zapisie, co oznacza, że wirtualna pamięć będzie skopiowana jedynie wtedy, gdy jeden z dwóch procesów będzie próbował coś zapisać. Jedynie pamięć służąca tylko do odczytu, np. kod programu, zawsze będzie dzielona.

Algorytm fork jest realizowany przez funkcję systemową sys_fork zdefiniowaną w pliku arch/i386/kernel/process.c:


asmlinkage int sys_fork(struct pt_regs regs)
{
        return do_fork(SIGCHLD, regs.esp, &regs, 0, ...);
}

Funkcja do_fork jest zdefiniowana w pliku kernel/fork.c:


int do_fork(unsigned long clone_flags, unsigned long stack_start,
            struct pt_regs *regs, unsigned long stack_size, ...)

Na początku tworzy się nowy identyfikator procesu (get_pid()) oraz nową strukturę task_struct (wywołanie funkcji alloc_task_struct()), po czym kopiuje się kolejne zasoby procesu, czyli deskryptory otwartych plików (copy_files()), kontekst systemu plików (copy_fs()), procedury obsługi sygnałów (copy_sighand()), struktury opisujące pamięć wirtualną procesu macierzystego (copy_mm()).

Do_fork() wywołuje się z parametrami, z których interesuje nas głównie parametr clone_flags. Ciekawe flagi to m.in. CLONE_FILES, CLONE_SIGHAND, CLONE_VM, które określają czy dany zasób ma być kopiowany, czy klonowany (czyli właściwie współdzielony przez oba procesy). I tak np. jeśli wywołano do_fork() z ustawioną flagą CLONE_FILES, to zwiększa się jedynie licznik odwołań do struktury files, która będzie współdzielona.

Oto zbiór wszystkich flag:


#define CSIGNAL         0x000000ff  \
        /* signal mask to be sent at exit */
#define CLONE_VM        0x00000100  \
        /* set if VM shared between processes */
#define CLONE_FS        0x00000200  \
        /* set if fs info shared between processes */
#define CLONE_FILES     0x00000400  \
        /* set if open files shared between processes */
#define CLONE_SIGHAND   0x00000800  \
        /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE    0x00002000  \
        /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK     0x00004000  \
        /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT    0x00008000  \
        /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD    0x00010000  \
        /* Same thread group? */

Podobnie jest z flagą CLONE_VM. Do_fork() wywołuje funkcję copy_mm() z tym samym parametrem clone_flags. Jeśli flaga CLONE_VM jest ustawiona, to funkcja ogranicza się do zwiększenia licznika odwołań do struktury mm_struct i skopiowania wskaźnika do tej struktury. W tej sytuacji oba procesy mają prawo zapisu do tej samej pamięci. Jest to wykorzystywane przy implementacji wątków.

Standardowo funkcja systemowa sys_fork() wywołuje do_fork() z ustawioną tylko flagą SIGCLD, która określa, jaki sygnał zostanie wysłany do procesu macierzystego po zakończeniu działalności przez potomka. W tym więc przypadku, funkcja copy_mm jest wywoływana bez ustawionej flagi CLONE_VM i wykonuje następujące czynności: tworzy nową strukturę opisującą pamięć wirtualną procesu (struktura mm_struct przydzielana przez funkcję allocate_mm()); a następnie tworzy nowy katalog stron (wywołuje funkcję mm_init(), które z kolei wywołuje pgd_alloc()), wypełnia go zerami, a przestrzeń 3-4 GB odwzorowuje na pamięć jądra. Zera w katalogu stron oraz w tablicy stron są traktowane jako strona alokowana przy pierwszym dostępie.

Następnie copy_mm() kopiuje cała listę vm_area_struct z procesu macierzystego do procesu potomnego (funkcja dup_mmap). Nie jest to jednak samo skopiowanie, muszą być wykonane oprócz tego także inne czynności.

W przypadku, gdy obszar pochodzi z odwzorowania i-węzła zwiększa się jedynie licznik odwołań do tego obszaru, a sam obszar dodaje się do struktury obszarów dzielonych. W przypadku, gdy mamy do czynienia z normalnym obszarem, wywołuje się funkcję copy_page_range(). Po pierwsze sprawdza ona, czy możemy pisać w obszarze i czy nie jest on dzielony. Następnie w pętli dla każdej strony należącej do obszaru zeruje się flagę RW, czyli zakaz zapisu na tej stronie. Jest to robione jednak jedynie wtedy, gdy obszar, do którego należy strona, nie jest dzielony i można w nim pisać. Następnie zwiększa się licznik odwołań do strony.

W takiej sytuacji, jeśli jeden z procesów będzie chciał zapisać coś do takiej strony, zostanie zgłoszony błąd ochrony strony. System sprawdzi, czy w strukturze vm_area_struct, do której jest przypisana strona, jest ustawiona flaga do zapisu i jeśli tak, to strona zostanie skopiowana. W przypadku obszaru kodu zwiększa się tylko licznik odwołań do pamięci, przez co jest ona zwalniana dopiero po zakończeniu pracy przez oba procesy.

Oprócz funkcji sys_fork() są jeszcze dostępne dwie inne funkcje systemowe: sys_clone i sys_vfork.


asmlinkage int sys_clone(struct pt_regs regs)
{
    unsigned long clone_flags;
    unsigned long newsp;

    clone_flags = regs.ebx;
    newsp = regs.ecx;
    if (!newsp)
            newsp = regs.esp;
    return do_fork(clone_flags, newsp, &regs, 0, ...);
}

asmlinkage int sys_vfork(struct pt_regs regs)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 
                   regs.esp, &regs, 0, ...);
}

Janina Mincer-Daszkiewicz