Wojciech Dudek
listopad 2001
Zwykłe procesy w Linuxie używające adresów, w rzeczywistości używają adresów logicznych. Gdy proces odwołuje się do jakiejś komórki pamięci, to adres logiczny jest tłumaczony sprzętowo (przez procesor) na adres fizyczny. Proces translacja zależy od architektury procesora.
W architekturze i386 adres logiczny, w wyniku procesu zwanego segmentacją, jest tłumaczony na adres liniowy. Następnie wynik podlega kolejnej translacji zwanej stronicowaniem ostatecznie otrzymując adres fizyczny.
W procesorach RISC-owych nie ma segmnetacji, zatem adres logiczny jest równoznaczny adresowi liniowemu.
Programiści Linux'a stwierdzili, że jeżeli mamy stronicowanie to segmentacja jest nadmiarowa. Z tego powodu Linux używa segmentacji w bardzo ograniczonym zakresie, a dokładniej tylko wtedy kiedy musi. W szczególności wszystkie procesy używają tych samych segmentów.
Ponieważ Linux koncentruje swoje wysiłki na stronicowaniu, nie będziemy omawiać segmentacji. Ogólny proces stronicownia jest opisany w [Siber 99], wiec niewtajemniczonych odsyłam do tej pozycji. My skoncentrujemy się na stronicowaniu w Linux'ie.
Dla architektury 32-bitowej wystarczy stronicowanie 2-poziomowe. W i386 mamy 10 bitów na poziom I, 10-bitów na II poziom i 12 bitów przesunięcia w obrębie jednej strony. Jednak gdy mamy do czynienia z architektura 64-bitową to nie wystarcza. Stosuje się wtedy stronicowanie 3-poziomowe. W 64-bitowych procesorach Alpha strony są wielkości 8KB, czyli 13 bitów na offset i 10 bitów na każdy stopień stronicowania. Zostaje jeszcze 21 bitów których się nie używa. Do wykorzystania jest zatem 43 bity (Kto potrzebuje więcej niż 8192 TB?).
By umożliwić działanie także archtekturom 64-bitowym, Linux uogólnia stronicowanie do modelu trzypoziomowego. Model taki prezentuje obrazek:
W specjalnym rejestrze procesora jest załadowany numer ramki, w którym znajduje się Globalny Katalog Stron (ang. PGD - Page Global Directory). Najstarsze bity GLOBAL_DIR wskazują na pewną pozycje w Globalnym Katalogu. Znajduje się tam między innymi PFN dla Pośredniego Katalogu (ang. PMD - Page Middle Directory). Podobnie jest z drugim poziomem i trzecim. Z kolei w Tablicy Stron (ang. PTE - Page Table Entry) mamy ostateczny numer ramki, a nasz adres fizyczny składa się z bitów numeru ramki i przesunięcia (OFFSET). Każdy proces ma własna niezależną przestrzeń adresową i co za tym idzie własny Globalny Katalog Stron.
Stronicowanie wygląda trochę inaczej na różnych procesorach, zatem które bity odpowiadają za którą część musi być zdefiniowane osobno dla każdego procesora. W szczególność tam gdzie jest tylko stronicowanie dwupoziomowe, Pośrednia Tablica Stron składa się z 0 bitów (czyli jest pomijana).
Pozycje w tablicy stron i w katalogach stron zawierają oprócz PFN, pewne atrybuty ramki o zadanym PFN, które określają czy ramka jest dostępna, czy była używana, czy zawartość się zmieniła i jakie ma prawa dostępu. Może się tez zdarzyć , że ramka została usunięta do pliku wymiany, wtedy zamiast PFN przetrzymywana jest lokalizacja ramki na dysku.
Jeżeli podczas stronicowania procesor wykryje jakąś nieprawidłowość (czy to przekroczenie uprawnienia, czy próba odwołania do nieprawidłowej ramki) jest zgłaszany wyjątek strony. Co się wtedy dzieje opiszę w 3 rozdziale.
Trzeba sobie zdać sprawę, że stronicowanie jest dosyć drogie. Aby odwołać się do jakiegoś adresu musimy sięgnąć 3 do 4 razy do pamięci. Dlatego w procesorze umieszcza się specjalną pamięć zwaną buforami translacji adresów (ang. Translation Lookaside Buffers TLB), która przyspiesza tłumaczenie adresów liniowych. (Jak działa taka pamięć można przeczytać w: [Siber 99].) Jądro musi się także zając obsługa tej pamięci, kiedy dochodzi do modyfikacji którejś z pozycji w tablicy stron.
Pośrednie Katalogu Stron nie ma, jest tylko PGD i Tablica Stron. Pozycje w tych tablicach są takie same i zawierają następujące pola:
Linux używa pięciu atrybutów stron, które są realizowane za pomocą różnych bitów ochrony. Oto one:
W plikach: includesm-i386/page.h, includesm-i386/pgtable.h includesm-i386/pgtable-2level.h są zdefiniowane wszystkie definicje, funkcje i makra potrzebne do obsługi tablicy stron. Z najważniejszych należy wymienić:
W procesorach Pentium Pro i wyższych, jest możliwość korzystania z prawdziwego stronicowania trzypoziomowego. Linux wykorzystuje to odpowiednie definiując makra, stałe i funkcje w pliku pgtable-3level.h
Pamiętajmy, że w systemie mogą występować takie kawałki pamięci, które nie mogą być wykorzystane przez procesy nawet przez jądro. Są to obszary BIOS'u czy okno pamięci video. Występują też ramki do których nie ma dostępu mechanizm DMA. Na te wszystkie ograniczenia architektury system musi zwracać uwagę.
typedef struct page { struct list_head list; struct address_space *mapping; unsigned long index; struct page *next_hash; atomic_t count; unsigned long flags; struct list_head lru; unsigned long age; wait_queue_head_t wait; struct page **pprev_hash; struct buffer_head * buffers; void *virtual; struct zone_struct *zone; } mem_map_t;
Najważniejsze pola tej struktury to:
Jeżeli ramka jest przydzielona jako bufor, to buffers opisuje listę cykliczną w jakiej znajduje się ramka(, w przeciwnym razie NULL).
Ostatecznie ramka może być używana przez allokator płytowy (Slab) wtedy jest nanizany na listę list używaną przez ten allokator. Ramka tez może być zwolniona , wtedy jest nanizana na listę wolnych ramek.
Podczas operacji swapowania zawartości ramki flaga PG_locked jest ustawiona. Wszystkie zadania czekają na kolejce wiat gdy ramka jest zablokowana.
Bity opisujące flagi ramki są następujące:
PG_locked | blokuje ramke |
PG_error | pojawił się błąd we/wy przy swapowaniu tej ramki |
PG_referenced | z ramki korzystano za pośrednictwem tablicy przemieszczania pamięci podręcznej stron |
PG_uptodate | mówi czy zawartość ramki jest prawidłowa, ustawiana po prawidłowej operacji we/wy |
PG_dirty | strona jest brudna (nie używana) |
PG_active PG_inactive_dirty | |
PG_inactive_clean | aktywna, nie aktywna brudna, nie aktywna czysta (używane przez allokatora) |
PG_swap_cache | znajduje się w pamięci podręcznej wymiany |
PG_highmem | ramki z tą flaga nie są ciągle mapowane w przestrzeń wirtualną jądra |
PG_skip | jest używany w architekturze sparcparc64 do pomijania przestrzeni adresowej |
PG_reserved | jest ustawiony dla specjalnych ramek, które nigdy nie mogą być swapowane. Niektóre nawet mogą nie istnieć |
Przydział pamięci na tablice ramek (lub deskryptorów ramek) następuje w funkcji free_area_init_node(), w wczesnej fazie inicjacji jądra. Funkcja ta wywołuje free_area_init_core(), liczy ilość ramek we wszystkich strefach, znajduje dobre miejsce dla tablicy, ustawia pg_dat_t contig_page_data (informujące o węzłach pamięci), rezerwuje całą pamięć, czyści kolejki ramek i bitmapy pamięci.
Zawartość strony może nie znajdować w ramce (zawartość jest w pliku wymiany), albo strona jest nieprawidłowa, ewentualnie przekroczono uprawnienia dostępu. Kiedy jednostka stronicowania w procesorze wykryje taką sytuacje procesor podnosi wyjątek błędu strony. Wyjątek ten (jak zresztą wszystkie inne) obsługuje system operacyjny. Jego zadaniem jest najpierw stwierdzenie dlaczego nastąpił sytuacja wyjątkowa, czy to było rzeczywiste naruszenie bezpieczeństwa, czy to tylko skutek polityki oddalania w czasie przydziału pamięci (leniwość).
Natomiast jeżeli mamy dostęp do kontekstu użytkownika, sprawdzamy obszar wirtualnej przestrzeni adresowej pamięci procesu który spowodował błąd. Jeżeli adres nie należy do obszaru procesu to Segmentation fault - proces umiera. Jeżeli przekroczyliśmy stos to jest rozszerzany. Jeżeli piszemy lub czytamy a nie mamy na to zezwolenia to też wysyłamy sygnał SIGSEGV. Jeżeli jednak błąd powstał wskutek leniwej polityki to odpalana jest wysokopoziomowa procedura handle_mm_fault() (linux/mm/memory.c). Tam próbuje się przydzielić nową ramkę albo sprowadzić ją do pamięci.
Jeżeli ramka nie jest obecna to mamy do czynienia z stronicowaniem na żądanie (page demanding). Testowane jest czy strona była wykorzystywana wcześniej (pte_none). Jeżeli tak to ramkę mamy w swap'ie - sprowadzamy ramkę do_swap_page() 3. W przeciwnym wypadku wykonywana jest funkcja do_no_page(), a ona w zależności czy próbowaliśmy pisać lub czytać, allokuje nową (czystą) ramkę albo podstawia ramkę ZERO_PAGE wypełnioną zerami4.
Jeżeli ramka jest obecna (zostało tylko możliwość, że chcemy pisać) to
wykonywane jest tak zwane kopiowanie na żądanie (copy on
write). Obsługuje to funkcja do_wp_page() przydzielając nową
ramkę pamięci. Jeżeli próbowaliśmy pisać do ZERO_PAGE to ramka jest
wypełniana zerami, jeżeli nie a dla strony to kopiujemy zawartość odwoływanej ramki
do nowej, ostatecznie ustawiamy prawo do pisania.
W trakcie całej tej skomplikowanej procedury sprawdzane są
kilkakrotnie atrybuty strony do której się odwoływaliśmy i ustawiane
dla nowy pozycji w PTE.
This document was generated using the LaTeX2HTML translator Version 99.2beta8 (1.46)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.