Pamięć

Algorytm tłumaczenia adresu (Intel)

Radomir Małaczek

Streszczenie:

Algorytm tłumaczenia adresu służy do translacji adresu logicznego (używanego w mechanizmach pamięci wirtualnej) na adres fizyczny (oznaczający konkretne komórki pamięci). Linux do tej translacji używa algorytmu segmentacji i trójpoziomowego stronicowania.

Wstęp

Algorytm tłumaczenia adresu odbywa się w dwóch fazach:

Zarówno segmentacja i stronicowanie realizowane są przez sprzętowe jednostki. Dotychczas w jądrach serii 2.2 mechanizm stronicowania dla procesorów serii i386 był redukowany od dwupoziomowego - dla procesorów 32 bitowych, zdolnych adresować maksymalnie 4GB pamięci mechanizm dwupoziomowy był całkiem wystarczający. Trójpoziomowy mechanizm używany jest tylko dla procesorów 64 bitowych. Jednak w najnowszych procesorach Intela pojawiło się rozszerzenie zwane PAE (Physical Address Extension) umożliwiające zaadresowanie max. 64 GB pamięci. Przy takiej adresacji pamięci procesor korzysta z trójpoziomowego mechanizmu stronicowania. Jądra serii 2.4 wykorzystują już to rozszerzenie procesora.

Segmentacja

Mechanizmy segmentacji i stronicowania są trochę nadmiarowe, twórcy Linuxa postanowili położyć nacisk na mechanizmy stronicowania, używając segmentacji w bardzo ograniczonym zakresie -- głównie do ochrony pamięci.

W Linuxie zdefiniowane jest tylko kilka segmentów. Wszystkie one przechowywane są w Globalnej Tablicy Deksryptorów (Global Description Table). Każdy proces może posiadać własną tablicę deskryptorów (jeśli zechce). Istotną zmianą w stosunku do jąder serii 2.2 jest to, że w GDT nie są przechowywane ani TSS (Task Segment Struct), ani LDT (Local Description Table) procesów. Nakładało to istotne ograniczenie na liczbę jednoczesnych procesów. W obecnych jądrach w GDT przechowywane są TSS aktualnie wykonywanego procesu na każdym z procesorów (to oczywiście przekłada się na ograniczenie na liczbę obsługiwanych procesorów, ale jeszcze długo nie będzie ono tak istotne jak poprzednie ograniczenie). Ciekawym rozwiązaniem jest zostawianie dwóch wolnych pozycji między poszczególnymi wpisami TSS i LDT każdego z procesorów. Dzięki takiemu podejściu informacja dla każdego procesora wyrównana jest do 32 bajtów, więc tylko ile ma linia pamięci podręcznej procesora. Lista deskryptorów przechowywanych w GDT znajduje się w tabeli [*].

Adres logiczny składa się z dwóch części: segmentu i offsetu (przesunięcia).

segment : offset
selektor segmentu przesunięcie względem początku segmentu
16 bitów 32 bity

Możliwymi selektorami segmentu mogą być rejestry: cs, ss, ds, es, fs, gs. Rejestr cs przechowuje segment kodu, ss - segment stosu, ds - segment danych statycznych i zewnętrznych. Pozostałe segmenty mogą być dowolnie wykorzystywane przez programistę. W tabeli [*] opisane jest znaczenie poszczególnych bitów selektora.


Tabela: Opis selektora segmentu
Indeks TI RPL
13 bitów 1 bit 2 bity

Indeks Pozycja w tablicy GDT lub LDT (w zależności od bitu TI).
TI Gdy wyzerowany, indeks oznacza pozycję w tablicy GDT, gdy ustawiony -- w tablicy LDT
RPL Requestor Privilege Level


Każdy segment jest reprezentowany przez deskryptor segmentu. Adres tablicy tych deskryptorów (GDT) trzymana jest w 32bitowym rejestrze procesora gdtr. Tabeli GDT zajmuje w pamięci jedną stronę (4KB), a rozmiar jednego deskryptora to 8 bajtów (64 bity). Tabele LDT działają podobnie. Deskryptory przechowywane przez nie mają identyczny format, a tabela zajmuje 4KB pamięci. Opis pojedynczego wpisu w tych tabelach (deskryptor segmentu) opisany jest w tabeli [*].


Tabela: Zawartość Global Description Table w Linuksie (opis z desc.h i kernel/head.S)
Pozycja Zawrtość
0 pusta - takie adresowanie zwróci błąd
1 nie używana
2 Segment kodu jądra
3 Segment danych jądra
4 Segment kodu użytkownika, współdzielony przez procesy w trybie użytkownika
5 Segment danych użytkownika, współdzielony przez procesy w trybie użytkownika
6-7 nie używane
8-11 strony APM BIOS
12 TSS (Segment statusu zadania) pierwszego procesora
13 LDT (Local Description Table) pierwszego procesora
14-15 nieużywane
16-19 TSS+LDT drugiego procesora (+2 nie używane)
20-23 TSS+LDT trzeciego procesora (+2 nie używane)
.... itd. NR_CPUS razy (dla SMP)



Tabela: Opis deskryptora segmentu
Bity Nazwa Opis pola
56-63 24-31 BASE Adres liniowy pierwszego bajta segmentu
55 G Bit ziarnistości. Określa czy rozmiar segmentu liczony jest w bajtach (gdy wyzerowany) czy w stronach 4096 bajtowych (gdy ustawiony)
54 B/D
53 zawsze 0
52 AVL nie używane przez Linuksa
48-51 16-19 LIMIT rozmiar segmentu
47 zawsze 1
45-46 DPL Descriptor Privilege Level, używane do ograniczenia dostępu do segmentu
44 S jeśli ustawiona - segment jest segmentem użytkownika, gdy wyzerowana - segment jest segmentem systemowym
40-43 TYPE Typ segmentu (LDT, TSS, code, data, itp.)
32-39 16-23 BASE
16-31 0-15 BASE
0-15 0-15 LIMIT


Aby przyspieszyć mechanizm segmentacji, za każdym razem gdy zmieniamy zawartość selektora segmentu deskryptor segmentu jest automatycznie ładowany do odpowiedniego nieprogramowalnego rejestru procesora.

Algorytm segmentacji

  1. Mnożymy indeks z selektora przez 8 (ponieważ rozmiar jednego deskryptora segmentu to 8 bajtów) i dodajemy do zawartości rejestru gdtr lub ldtr (w zależności od pola TI selektora).
  2. Pod otrzymanym adresem odczytujemy pole BASE z danego deskryptora segmentu i do otrzymanej wartości dodajemy przesunięcie z adresu logicznego. Otrzymana wartość jest adresem liniowym.

Wady segmentacji

Mechanizm segmentacji nie jest mocno wykorzystywany przez Linuksa. Mało tego, wspominałem o rozszerzeniu PAE, które pozwala adresować 64GB pamięci na nowych procesorach Intela. Przy wyłączonym mechanizmie stronicowania, nie uda nam się zaadresować pamięci powyżej 4GB. Już ten fakt pokazuje, że segmentacja nie jest wystarczającym mechanizmem przy projektowaniu nowoczesnego systemu operacyjnego.

Mechanizm segmentacji może różnić się na różnych architekturach, przez co nie jest zbyt przenośnym mechanizmem. Na niektórych procesorach segmentacja sprzętowa właściwie nie istnieje. Praktycznie każdy procesor wspierający mechanizm segmentacji inaczej ją implementuje. Stronicowanie dzięki swej prostocie jest zrealizowane praktycznie tak samo na różnych architekturach.

Stronicowanie

Linux ma zaimplementowany niezależny od platformy trójpoziomowy mechanizm stronicowania. Jednak architektura procesorów rodziny i386 do niedawna pozwalała zaadresować tylko 4GB pamięci. Mechanizm trójpoziomowy jest w takim przypadku zbyteczny -- sprzętowa jednostka adresowania w takich procesorach realizuje mechanizm dwupoziomowy. Implementacja stronicowania w Linuksie dla tych procesorów ignoruje po prostu środkowy poziom.

Niedawno, w procesorach Pentium Pro i lepszych, wprowadzono rozszerzenie zwane PAE (Phisical Address Extension), które pozwala na zaadresowanie na tych procesorach 64GB pamięci RAM. Rozszerzenie to dotyczy tylko adresów fizycznych, adres liniowy wciąż jest 32 bitowy, przez co tylko 4GB pamięci mogą być ,,stale odwzorowane'' przez jądro. Dodatkowo -- możliwe jest uzywanie bloków stronicowych w wielkości 2MB.

Dla takich procesorów zmienił się również mechanizm stronicowania. Dla tych procesorów używa się trójpoziomowego stronicowania. Linux wspiera to rozszerzenie dopiero od wersji jądra 2.4.

Warto odnotować, że ani AMD K5, ani AMD K6 nie obsługuje tego rozszerzenia. Dopiero procesor AMD Athlon posiada PAE.

W procesorach Pentium (i AMD K5) wprowadzono inne ciekawe rozszerzenie -- PSE (Page Size Extension). Rozszerzenie to (przy nie używanym PAE) pozwala na stosowanie bloków stronicowych o wielkości nie tylko 4KB, ale też 4MB.

W Linuksie zdefiniowano trzy rodzaje tablic stronicowania:

PGD
Globalny Katalog Stron
PMD
Pośredni Katalog Stron
PTE
Katalog Stron

Adres tablicy PGD przechowywany jest w rejestrze cr3, który zachowywany jest w TSS procesu1. Dzięki temu każdy proces posiada własny globalny katalog stron.

Stronicowanie dwupoziomowe

Rysunek: Stronicowanie dwupoziomowe (jak w jądrach 2.2).
\includegraphics[width=\textwidth]{paging32-2.eps}

W stronicowaniu dwupoziomowym zrezygnowano z Pośredniego Katalogu Stron (PMD).

Oto wycinek pliku pgtable-2level.h:

/* Od którego bitu zaczyna się indeks z Page Global Directory */
#define PGDIR_SHIFT     22
/* Liczba pozycji w Page Global Directory */
#define PTRS_PER_PGD    1024

/* Od którego bitu zaczyna się indeks PMD (jak widać równy PGDIR_SHIFT) */
#define PMD_SHIFT       22
/* Rozmiar Page Middle Directory */
#define PTRS_PER_PMD    1

/* Liczba pozycji w Tablicy Stron */
#define PTRS_PER_PTE    1024
Adres liniowy interpretowany jest w następujący sposób:

Dla stron 4KB:
bity
31-22 Pozycja w PGD (Globalnym Katalogu Stron), 10 bitów
21-12 Pozycja w PTE (Tablicy Stron), 10 bitów
11-0 Przesunięcie w stronie

Dla stron 4MB:
bity
31-22 Pozycja w PGD (Globalnym Katalogu Stron), 10 bitów
21-0 Przesunięcie w stronie

Opis wpisów w tabelach stron i zawartość rejestru cr3 w tableli [*].


Tabela: Struktury dla mechanizmu 2-poziomowego
31-21 20-12

Rejestr cr3
PDBR bity 31-121

Wpis w PGD
Adres tabeli PMD (31-12)
Deskr. strony 4MB (PGD) Adres strony 4MB (31-22) *
Deskr. strony 4KB (PTE) Adres strony 4KB (31-12)


11-9 8 7 6 5 4 3 2 1 0

Rejestr cr3
0 0 0 0 0 PCD PWT 0 0 0

Deskr. strony 4MB (PGD)
AVL * PS2 D A PCD PWT U W P

Wpis w PGD
AVL * PS3 * A PCD PWT U W P

Deskr. strony 4KB (PTE)
AVL * * * A PCD PWT U W P
1 PDBR, Page Directory Base Pointer, w Linuksie adres GDT
2 Page Size, musi być równe 1
3 Page Size, musi być równe 0


Algorytm stronicowania dwupoziomowego

  1. Odczytujemy adres odpowiedniej tablicy stron z globalnego katalogu stron (adres GDT wskazuje rejestr cr3).
  2. Gdy deskryptor z tablicy GDT ma wyzerowany bit PS (Page Size):
    1. Z tablicy stron pod pozycją z adresu liniowego odczytujemy adres fizyczny 4KB strony.
    1. Gdy deskryptor z tablicy GDT ma ustawiony bit PS (Page Size), adres odczytany z tabeli GDT jest adresem fizycznym 2MB strony.
  3. Do otrzymanego adresu fizycznego dodajemy przesunięcie.

Stronicowanie trójpoziomowe

Rysunek: Stronicowanie, gdy procesor obsługuje PAE (adres fizyczny 36 bitowy).
\includegraphics[width=\textwidth]{paging36-2.eps}

Oto wycinek pliku pgtable-3level.h:

/* PGDIR_SHIFT determines what a top-level page table entry can map */
#define PGDIR_SHIFT     30
#define PTRS_PER_PGD    4
  
/* PMD_SHIFT determines the size of the area a middle-level page table can map */
#define PMD_SHIFT       21
#define PTRS_PER_PMD    512
     
/* entries per page directory level */
#define PTRS_PER_PTE    512

Tu interpretacja adresu liniowego jest następująca:

Strony 4KB:
bity
30-31 Pozycja w PGD (Globalnym Katalogu Stron), 2 bity
21-29 Pozycja w PMD (Pośrednim Katalogu Stron), 9 bitów
12-20 Pozycja w PTE (Tablicy Stron), 9 bitów
0-11 Przesunięcie w stronie

Strony 2MB:
bity
30-31 Pozycja w PGD (Globalnym Katalogu Stron), 2 bity
21-29 Pozycja w PMD (Pośrednim Katalogu Stron), 9 bitów
0-20 Przesunięcie w stronie

Deskryptory tablicśtron przechowywane w PGD, PMD i PTE różnią się od przypadku stronicowania dwupoziomowego. Różnica polega na tym, że adres bazowy kolejnej tablicy czy strony podawany jest jako adres 36-bitowy (konkretnie tylko 24 strasze bity, ponieważ adresy tablic i same strony są wyrównane do 4 KB). Format takiego deskryptora opisany jest w tabeli [*].


Tabela: Struktury dla stronicowania 3-poziomowego (PAE)
63-36 35-32 31-21 20-12

Rejestr cr3
Nie dotyczy PDBR bity 31-121

Wpis w PGD
* Adres tabeli PMD (35-12)

Deskr. strony 2MB (PGD)
* Adres strony 2MB (35-21) *

Wpis w PMD
* Adres tabeli PTE (35-12)

Deskr. strony 4KB (PTE)
* Adres strony 4KB (35-12)


11-9 8 7 6 5 4 3 2 1 0

Rejestr cr3
PDBR bity (11-5)2 PCD PWT Nie używane

Wpis w PGD
AVL * * * * PCD PWT * * P

Deskr. strony 2MB (PGD)
AVL * PS3 D A PCD PWT U W P

Wpis w PMD
AVL * PS4 * A PCD PWT U W P

Deskr. strony 4KB (PTE)
AVL G * * A PCD PWT U W P
1 PDBR, Page Directory Base Pointer, w Linuksie adres GDT
2 Pozycja tabeli PGD może być wyrównana do 32 bajtów
3 Page Size, musi być równe 1
4 Page Size, musi być równe 0


Oczywistą zauważalną różnicą jest zmiana rozmiaru deskryptora. Dla architektur wspierających PAE (i oczywiście ustawionym bicie używania PAE) wszystkie katalogi i tablice stron są teraz 64 bitowe.

Szybki rzut oka na plik linux/include/page.h tylko potwierdza ten fakt:

...
#if CONFIG_X86_PAE
typedef struct { unsigned long pte_low, pte_high; } pte_t;
typedef struct { unsigned long long pmd; } pmd_t;
typedef struct { unsigned long long pgd; } pgd_t;
#define pte_val(x)      ((x).pte_low | ((unsigned long long)(x).pte_high << 32))
#else
typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x)      ((x).pte_low)
#endif
...

Adres Globalnego Katalogu Stron, dotąd przechowywany w cr3, również uległ nieznacznej zmianie.

Algorytm stronicowania trójpoziomowego

  1. Odczytujemy adres odpowiedniego pośredniego katalogu stron z globalnego katalogu stron (adres GDT wskazuje rejestr cr3).
  2. Gdy deskryptor z tablicy GDT ma wyzerowany bit PS (Page Size):
    1. Z pośredniego katalogu stron odczytujemy (na odpowiedniej pozycji) adres tablicy stron.
    2. Z tablicy stron pod pozycją z adresu liniowego odczytujemy adres fizyczny 4KB strony.
  3. Gdy deskryptor z tablicy GDT ma ustawiony bit PS (Page Size):
    1. Z pośredniego katalogu stron odczytujemy (na odpowiedniej pozycji) adres fizyczny 2MB strony.
  4. Do otrzymanego adresu fizycznego dodajemy przesunięcie.

Spis Literatury

1
Daniel P. Bovet & Marco Cesati: Linux Kernel, Wydawnictwo ReadMe, 2001

2
Intel, Intel Architecture Software Developer's Manual, vol. 3: System Programming, 1999.

3
Źródła systemu Linux w wersji 2.4.7



Footnotes

... procesu1
patrz include/asm-i386/processor.h


Radomir Małaczek