3. Algorytmy, czyli jak to działa
Jądro wykonuje na Przestrzeni Adresowej procesu następujące wysokopoziomowe operacje:
- Tworzenie Przestrzeni Adresowej dla procesu
- Alokacja nowego zakresu adresów liniowych
- Zwolnienie pewnego zakresu adresów liniowych
- Usuwanie Przestrzeni Adresowej procesu
Zanim przejdziemy do ich omówienia, zajmiemy się grupą kilku niższych funkcji pomocniczych.
Pozwoli to na nabranie pewnego pojęcia o tym, jak wyglądają operacje wykonywane
na Przestrzeni Adresowej, oraz ułatwi zrozumienie bardziej specjalizowanych
funkcji.
2.1 Funkcje obsługi Przestrzeni Adresowej
-
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
Funkcja find_vma() znajduje pierwszy (reprezentujący najmniejsze adresy) Region Pamięci, dla którego addr < vm_end
- Sprawdź, czy mm->mmap_cache zawiera addr. Jeżeli tak - koniec. Sukces w ok. 35% przypadków.
- Wpp. przejdź drzewo AVL Regionów (lub listę, jeśli AVL nie istnieje) i znajdź właściwy Region
- Jeżeli udało się znaleźć, to zapamiętaj go w mm->mmap_cache
-
struct vm_area_struct * find_vma_prev(struct mm_struct * mm, unsigned long addr, struct vm_area_struct **pprev)
Funkcja find_vma_prev() robi to samo co find_vma(), z tym że zwraca również poprzednik znalezionego Regionu.
- Jeżeli nie istnieje drzewo AVL Regionów, to przeszukaj listę i koniec
- Wpp. przeszukaj drzewo znajdując również poprzednika
-
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm, unsigned long start_addr, unsigned long end_addr)
Funkcja find_vma_intersection() znajduje pierwszy Region przecinający domknięty przedział [ start_addr .. end_addr - 1 ]
- Sprawdź, czy find_vma(mm, start_addr) przecina się z zadanym przedziałem
-
unsigned long get_unmapped_area(unsigned long addr, unsigned long len)
Funkcja get_unmapped_area() znajduje pierwszy nieużywany przedział adresów o długości
len, zaczynający się na prawo od addr. Zwraca początek
znalezionego przedziału.
- Znajdź pierwszy Region kończący się na prawo od addr: find_vma(current->mm, addr)
- Jeżeli addr + len > PAGE_OFFSET (wyjeżdzamy na część przestrzeni przeznaczoną dla jądra), to porażka
- Jeżeli znaleziony Region zaczyna się dostatecznie daleko (lub nie istnieje), to sukces
- Wpp. przypisz koniec znalezionego Regionu na addr i znajdź kolejny Region.
-
void insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vmp)
Funkcja insert_vm_struct() dodaje strukturę vmp
do listy mm->mmap i ewentualnie do drzewa AVL (mm->mmap_avl).
Drzewo AVL jest tworzone wtedy, kiedy liczba Regionów Pamięci w Deskryptorze (mm->map_count)
osiągnie AVL_MIN_MAP_COUNT (include/linux/sched.h -> 32).
- Załóż blokadę na dodawanym Regionie vmp
- Jeżeli drzewo AVL nie istnieje, to dodaj Region vmp do posortowanej listy mm->mmap
- Wpp.
- wstaw vmp do drzewa AVL, wyznaczając jednocześnie jego sąsiadów (bezpośredniego poprzednika i następnika w porządku sortowania)
- W czasie stałym dodaj vmp do listy mm->mmap
- Jeżeli drzewo AVL nie istnieje, ale liczba regionów osiągnęła właśnie AVL_MIN_MAP_COUNT, to utwórz drzewo
- Jeżeli Region vmp zawiera odwzorowany w pamięci plik, funkcja insert_vm_struct wykonuje pewne dodatkowe czynnosci z tym związane
- Zdejmij blokadę
2.2 Operacje na Przestrzeni Adresowej
Przejdziemy teraz do omówienia implementacji poszczególnych wysokopoziomowych
operacji na przestrzeni adresowej procesu:
2.2.1 Alokacja i dealokacja zakresu adresów liniowych
Zaczniemy od operacji alokacji i dealokacji zakresu adresów liniowych.
Te dwie operacje realizowane są w całości przez funkcje
do_mmap() i do_munmap():
static inline unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset)
|
int do_munmap(struct mm_struct *mm, unsigned long addr, size_t len)
|
2.2.1.1 static inline unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset)
Funkcja do_mmap służy do alokowania nowego przedziału adresów
o zadanej długości len. Tworzy ona i inicjuje nowy Region Pamięci dla
bieżącego procesu.
Parametry:
- file i offset są używane, jeżeli nowy Region ma odwzorowywać plik.
- addr określa, od którego miejsca należy rozpocząć szukanie wolnego zakresu adresów
- len - wymagana długość alokowanego zakresu (zostanie zaokrąglona w górę do granicy strony)
- prot - wymagane prawa dostępu do stron alokowanego Regionu. Dozwolone wartości i
ich znaczenie to (por. tabela z pkt. 1.2.1):
PROT_READ | VM_READ |
PROT_WRITE | VM_WRITE |
PROT_EXEC | VM_EXEC |
PROT_NONE | Proces nie ma żadnych praw dostępu |
- flag określa pozostałe flagi alokowanego Regionu:
MAP_SHARED | Strony mogą być współdzielone. (VM_SHARED) |
MAP_PRIVATE | Dokonane przez proces zmiany muszą być widoczne tylko dla niego (zgaszona VM_SHARED) (p. "kopiowanie przy zapisie") |
MAP_FIXED | Początkowy adres alokowanego obszaru musi być równy addr |
MAP_ANONYMOUS | Z alokowanym regionem nie jest związany żaden plik |
MAP_GROWSDOWN | VM_GROWSDOWN |
MAP_DENYWRITE | VM_DENYWRITE |
MAP_EXECUTABLE | VM_EXECUTABLE |
MAP_LOCKED | VM_LOCKED |
MAP_NORESERVE | Nie sprawdzaj ilości dostępnej pamięci (p. opis algorytmu) |
Algorytm:
-
Sprawdź wartości parametrów:
-
Jeżeli addr + len lub po prostu len przekraczają
PAGE_OFFSET (zahaczają o część Przestrzeni Adresowej
przeznaczoną dla jądra), to porażka: -EINVAL
-
Jeżeli proces przekroczyłby dozwoloną liczbę przydzielonych Regionów
Pamięci: mm->map_count > max_map_count, to porażka: -ENOMEM
-
Jeżeli wypadkowe flagi dla Regionu Pamięci mm->def_flags
okreslają, że strony nowo alokowanych Regionów mają być zablokowane w
pamięci (VM_LOCKED), natomiast liczba zablokowanych przez
proces stron przekroczyłaby dozwoloną wartość okresloną przez pole
rlim Deskryptora Procesu
(current->rlim[RLIMIT_MEMLOCK].rlim_cur),
to porażka: -EAGAIN
-
Jeżeli nowo-alokowany Region ma odwzorowywać dany plik, to:
-
Jeżeli strony mają móc być współdzielone (MAP_SHARED), to:
-
Jeżeli proces ma móc zapisywać do Regionu, ale nie ma praw zapisu
do odwzorowywanego pliku, to porażka: -EACCES
-
Jeżeli plik można tylko appendować (dopisywać na koniec), ale
prawa zwykłego zapisu do niego również są włączone, to -EACCES
-
Jeżeli strony mają być prywatne (MAP_PRIVATE), ale proces
nie ma praw odczytu z pliku, to -EACCES
-
Zaalokuj nowy obszar:
-
Jeżeli nowo alokowany obszar ma być 'anonymous', tzn. nie ma być
odwzorowaniem pliku ani przestrzeni I/O jakiegoś urządzenia, to
spróbuj przedluzyć istniejący obszar (wyznaczany przez addr,
a więc nie dowolny), o ile jego prawa dostępu są zgodne. Jeśli to się
uda - zakończ.
(to jest wlaśnie owo lączenie sąsiadujących ze sobą
Regionów Pamięci, których prawa dostępu są zgodne)
-
W przeciwnym przypadku, lub gdy powyższe się nie powiedzie:
-
O ile flaga MAP_FIXED nie jest włączona we flags,
znajdź odpowiednio duży obszar (get_unmapped_area(addr, len)).
Nie ma -> -ENOMEM
-
Jeżeli zaś flaga MAP_FIXED była włączona we flags,
to spróbujemy użyć zadanej wartości addr jako pierwszego
adresu nowo alokowanego obszaru
-
Pobierz nowy Region Pamięci (struct vm_area_struct) z
Alokatora Plytowego:
kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL).
Nie udało się -> -ENOMEM
-
Zainicjalizuj Region tak, aby poprawnie opisywał nowo alokowany
obszar (zakresy, flagi)
-
Sprawdź, czy wszystko poszło dobrze:
-
Jeżeli nowo alokowany Region [ addr .. addr + len ] ma część
wspólną z jakimś istniejącym Regionem (mogło się tak zdarzyc w
przypadku użycia flagi MAP_FIXED), to porażka: zwolnij nowo
zaalokowany Region i zwróć -ENOMEM
-
Jeżeli wielkość (w stronach) Przestrzeni Adresowej procesu
(mm->total_vm) przekracza ograniczenie zapisane w Deskryptorze
Procesu (current->rlim[RLIMIT_AS].rlim_cur), to porażka:
zwolnij nowo zaalokowany Region i zwróć -ENOMEM
-
Jeżeli flaga MAP_NORESERVE nie była ustawiona we flags,
Region ma zawierać prywatne, zapisywalne strony, natomiast ilość wolnej
pamięci jest zbyt mała, to również porażka: zwolnij nowo zaalokowany
Region i zwróć -ENOMEM
-
Końcowe ustawienia:
-
Wstaw nowo zaalokowany Region na listę (drzewo AVL) Regionów w Deskryptorze
Pamięci bieżącego procesu (insert_vm_struct())
-
W przypadku MAP_LOCKED (strony maja byc zablokowane w pamięci)
fizycznie zaalokuj strony. Gdy flaga MAP_LOCKED jest zgaszona,
strony beda sukcesywnie alokowane w miare potrzeby
(p. "stronicowanie na żądanie")
-
Zwróć pierwszy adres z nowego Regionu Pamięci
2.2.1.2 int do_munmap(struct mm_struct *mm, unsigned long addr, size_t len)
Funkcja do_munmap służy do zwalniania zakresu adresów z Przestrzeni Adresowej procesu.
Parametry:
- mm - Deskryptor Pamięci procesu, z którego Przestrzeni Adresowej chcemy usunąc adresy
- addr - poczatek przedziału do usunięcia
- len - długość przedziału do usunięcia (zostanie zaokrąglona w góre do wielokrotności wielkości strony)
Uwaga: zwalniany zakres adresów zwykle nie odpowiada dokładnie pojedyńczemu Regionowi Pamięci. Może być zawarty w którymś Regionie, może też obejmować dowolną liczbę Regionów. Nie musi zaczynać się ani kończyć w żadnym z Regionów.
Algorytm:
-
Sprawdź wartości parametrów:
-
Jeżeli addr nie jest wielokrotnością PAGE_SIZE
(wielkości strony), to porażka: -EINVAL
-
Jeżeli zwalniany obszar zachodzi na część Przestrzeni Adresowej procesu
przeznaczoną dla jądra, to porażka: -EINVAL
-
Jeżeli zwalniany obszar po wyrównaniu do granic stron ma zerową długość,
to porażka: -EINVAL
-
Usuń (lub odpowiednio zmniejsz) Regiony Pamięci nakładające się na usuwany zakres adresów:
-
Znajdź najbardziej na lewo polożony Region, który nakłada się na usuwany
przedział (find_vma_prev())
-
Jeżeli nie ma takiego, to sukces: 0
-
Wpp., jeżeli usuwany przedział dzieli znaleziony Region na dwie części,
a liczba przydzielonych procesowi Regionów Pamięci osiągnęła swoje
dozwolone maksimum (max_map_count, standardowo 65536),
to (jako że nie możemy rozdzielić tego Regionu na dwa) porażka: -ENOMEM
-
Zaalokuj jedną nową strukturę Regionu Pamięci (struct vm_area_struct)
z Alokatora Płytowego (może być potrzebna do późniejszego usuwania
Regionów). Porażka: -ENOMEM
-
Utwórz listę Regionów Pamięci zachodzących na usuwany przedział. Lista
tworzona jest na wskaźnikach vm_next, wskazujących "do tyłu"
(na wcześniejsze Regiony)
-
Po kolei usuń Regiony z listy, zmniejszając przy tym map_count w Deskryptorze Pamięci procesu.
Przy każdym usuwanym Regionie:
-
Jeżeli Region odwzorowywał plik, to usuń go z listy Regionów
odwzorowujących ten plik zmniejszając liczbę dowiązań
-
Zwolnij bloki stronicowe należące do części wspólnej
bieżącego Regionu i usuwanego przedziału
-
Unieważnij pozycje usuniętych bloków w tablicy TLB
(Translation Lookaside Buffers)
-
Jeżeli bieżący Region nie jest w całości pokrywany przez usuwany
przedział, to (po usunięciu tegoż Regionu) wstaw z powrotem na
listę jego "zmniejszoną" wersję (insert_vm_struct()).
-
Jeżeli zaalokowana w pkt. 4 struktura vm_area_struct nie została
wykorzystana, to ją zwolnij.
-
Zwolnij tablice stron odpowiadające usuwanemu przedziałowi.
Oprócz mmap() istnieje również inna funkcja, sys_brk(),
służąca do alokowania wyłącznie obszarów 'anonymous', tj., jak już wspomnieliśmy,
nie związanych z żadnymi plikami ani urządzeniami. Służy ona do zwiększania
(lub zmniejszania) sterty procesu, i stanowi po prostu uproszczoną wersję
mmap(). Funkcja mmap() - dzięki parametrom
prot, flag, file i offset - ma znacznie szersze
zastosowanie: za jej pomocą można m.in. alokować pamięc przeznaczoną dla stosu
trybu użytkownika, dla przestrzeni I/O urzadzeń, dla pamięci współdzielonej
IPC oraz dla plików.
2.2.2 Tworzenie i usuwanie przestrzeni adresowej procesu
Uwaga: przed przeczytaniem tego punktu dobrze jest się zapoznać z techniką
"kopiowania przy zapisie".
2.2.2.1 Tworzenie przestrzeni adresowej procesu
Tworzenie przestrzeni adresowej dla nowo powstającego procesu realizowane jest
przez funkcję copy_mm z kernel/fork.c. Wywoływana jest ona przez
funkcje z rodziny fork: clone(), fork() i vfork(),
służące do tworzenia nowych procesów i wątków.
Dzięki technice kopiowania przy zapisie,
funkcja copy_mm nie musi robić zbyt wiele:
-
W przypadku tworzenia nowego wątku (wątki jednego procesu współdzielą swoją
przestrzeń adresową), copy_mm po prostu przypisuje mu Deskryptor
Pamięci (mm_struct) procesu
macierzystego, inkrementując przy tym licznik mm_users w tej strukturze.
-
W przypadku zaś tworzenia "prawdziwego" procesu copy_mm:
-
Powiela Deskryptor Pamięci
mm_struct
-
Powiela Lokalną Tablicę Deskryptorów procesu
2.2.2.2 Usuwanie przestrzeni adresowej procesu
Likwidacją przestrzeni adresowej procesu zajmuje się funkcja
exit_mm(), wywoływana przez funkcję exit() i
pokrewne:
-
Zawiadom rodzica jeśli czeka na wywołaniu vfork()
-
Zdekrementuj licznik mm_users w swoim Deskryptorze Pamięci
mm_struct,
-
Zwolnij Lokalną Tablicę Deskryptorów procesu
-
Zwolnij kolejne Regiony Pamięci "odlinkowując" je od ewentualnie
powiązanych z nimi i-node-ów
-
Zwolnij Globalny Katalog Stron procesu
-
Zwolnij sam Deskryptor Pamięci