Oznaczenia w tekście
[i386] poświęcone wyłącznie procesorom intel z serii 80x86 i zgodnym, od 386 wzwyż.Segmentacja była dawniej podstawowym mechanizmem podziału pamięci na spójne podobszary - bloki kodu, danych, stosu. Pozwalało to na defragmentację, to jest przesuwanie segmentów, aby w miejsce wielu małych wolnych obszarów pojawił się jeden duży spójny. Adresy bowiem przechowywało się względem początku segmentu, który mógł być już płynny. Jednak takie podejście do problemu fragmentacji pamięci jest dosyć czasochłonne i skomplikowane, szczególnie gdy chciałoby się nie używane fragmenty segmentów zachować na dysk w celu uczynienia miejsca na nowe - to jest już w segmentacji dosyć trudne i czasochłonne.
Łatwiej jest myśleć o pamięci w porcjach (stronach), które każdy proces
dostaje w miarę potrzeby, zorganizowanych tak, że ze swego punktu widzenia
ma do czynienia ze spójnym fragmentem pamięci, o co dba sprzęt, który strony
o byle jakiej kolejności fizycznej układa tak, aby miały określony porządek.
Dodatkowo uzyskujemy wygodną metodę podziału pamięci na fragmenty, dla
których możemy jakoś badać stopień wykorzystania, i odpowiednio zamrażać
je na dysku w celu zwolnienia pamięci dla fragmentów często lub aktualnie
potrzebnych. Jednak ponieważ podział ma małą rozdzielczość, to wymagany
jest tu trochę inny sposób kompilacji lub pisania kodu/projektowania danych
- tak by w efekcie zgrupować obszary używane przez dany fragment programu
w spójnym bloku (lokalność odwołań).
Jednak metodologia taka nie jest obecnie szeroko praktykowana - może poza
projektowaniem standardowych bibliotek procedur.
W tym momencie segmentacja nie jest już mechanizmem defragmentacji, a jedynie ewentulalnie ochrony i dostępu do pewnych obszarów danych. (które to mechanizmy mogłyby być znacznie uproszczone do postaci nie wymagającej odwoływania się do pojęcia segmentu, co ma jednak miejsce w procesorach serii 386/486/PENTIUM/PPRO/?, zapewne ze względów kompatybilnościowo/hist[e]orycznych.)
Jaka jest zatem relacja między segmentacją a stronicowaniem? Z punktu widzenia adresu: segment wpływa na pierwotny adres wirtualny - dodając do niego liniowy adres początku segmentu, który potem obrabia mechanizm stronicowania.
Segmentacja jest:
Dlaczego więc w ogóle wymyślono segmentację projektując procesor 286 - chociaż stronicowanie było już wtedy znane i stosowane przez innych producentów procesorów ? Segmentacja, choć mechanizm trudny w programowaniu i wykorzystaniu, umożliwiał przydzielanie pamięci bardzo małymi elementami, a nie tylko stronami, które mają wielkość 4KB w przypadku [i386], przez co umożliwiło wykorzystanie go w systemie z małą ilością pamięci (np. 1MB). Oczywiście kosztem czasu poświęconego na defragmentację pamięci. Dlatego przy dzisiejszych pojemnościach pamięci segmentacja jest znacznie lepszym rozwiązaniem.
Segmentacja jednak, jeżeli już jest, to może się przydać - jako mechanizm ochrony (szczególnie, gdy nie zadbano o inne, lepsze) - globalnie ustala priorytety dla grupy stron, a nie tylko dla konkretnych stron. Jednak to można sprzętowo rozwiązać lepiej, stosując poziom uprzywilejowania np. dla danej tablicy stron, nie używając jeszcze jednego dodatkowego mechanizmu przeliczania adresu.
Generalnie - mechanizmu sprzętowego warto używać tylko wtedy, gdy jest udogodnieniem bądź usprawnieniem. Ani jedno, ani drugie nie ma jednak miejsca, przynajmniej w odniesieniu do [i386].
Osobiście nie polecałbym stosowania czystego Linuxa do zastosowań wymagających bezpieczeństwa, ochrony czy niezawodności bez jego głębokiego poznania. System ten prawidłowo skonfigurowany umożliwia osiągnięcie bardzo dużego poziomu bezpieczeństwa, jednak nie należy liczyć na to, że samo hasło Linux sprawi że twój system będzie bezpieczny. Trwają już prace nad jego rozszerzeniami np. odpowiednie moduły np. do budowania zapór ogniowych (ang. firewall).
Oczywiście ustalmy jedno - DOS czy Window$95 są w tym względzie znacząco z tyłu, a W$ NT wcale nie jest bardziej bezpieczne. Systemy te, pomijając samą kwestię jakości wykonania przez programistów, nie zostały od początku pomyślane jako systemy z mechanizmami zabezpieczeń, tylko jako systemy dla użytkownika prywatnego. Elementy bezpieczeństwa próbuje się wprowadzić metodą serii poprawek jako odpowiedź na zapotrzebowanie rynku oprogramowania systemowego
Podkreślę na wstępie - Linux w założeniu ma być najprostszą i przejrzystą
implementacją Unixa.
Często można spotkać opinię że sam Unix nie jest systemem
bezpiecznym! (w porównaniu z DOS-em może i wypada lepiej, ale o tym ostatnim
lepiej nie wspominać)
Często taka opinia wynika jednak z niepełnej wiedzy o systemie.
Wszystkie standardowe mechanizmy ochrony Unixa można z powodzeniem realizować na poziomie ochrony stron (bez używania pojęcia segmentu), z jednym wyjątkiem: potrzebne jest jeszcze bezpieczne przechodzenie w tryb nadzorcy i z powrotem - podczas wołania funkcji systemowych. Musi być wogóle realizowalny podział system - użytkownik, aby była mowa o bezpieczeństwie - co jednak nie jest konieczne do samej pracy systemu).
Ma to swoje odbicie w Linuxie: segmentacja została tu właściwie zdegenerowana do niezbędnego minimum wymaganego przez dany sprzęt to jest w przypadku [i386]:Omówię teraz szerzej te elementy, jednak przed przejściem do szczegółów potrzebny jest wstęp i krótka nota historyczna:
IDT (interrupt descriptor table) tablica deskryptorów przerwań, szerzej powinna być opisana przy przełączaniu procesów
GDT (global descriptor table) globalna tablica deskryptorów segmentów
- segmenty jądra, także innych procesów
[@]
Obecnie każdy proces ma swoje segmenty opisane w pewnych polach GDT - wspólnych dla
wszystkich procesów, oddzielnie dla danych, oddzielnie dla kodu.
W GDT poza tym znajdują się deskryptory segmentów TSS.
Podobnie jak każdy proces dwa segmenty ma też jądro, ale są to dwa inne segmenty w tablicy GDT, jako że jądro ma inne priorytety (poziom 0 w polach DPL), oraz inny obszar przestrzeni adresowej nie nachodzący na obszar procesów - 3GB w górę. GDT[1]:kod jądra, GDT[2]:dane jądra.
Tablica LDT opisana niżej jest zgodnie z nazwą lokalna, co dodatkowo oznacza, że aby zaistniała, musi być wpisana jako segment do tablicy globalnej (pierwotnego segmentu) GDT.
LDT (local/lokalna, reszta jw.) - nota historyczna: W tych tablicach były kiedyś deskryptory segmentów dla każdego procesu oddzielnie, ale ponieważ każdy proces ma zwykle tylko po dwa segmenty, i to zawsze takie same, to nie ma po co robić wielu identycznych segmentów na tablice LDT, z których każda wyglądała tak samo: LDT[0]:null, LDT[1]:kod, LDT[2]:dane/stos
przypis: Obecnie tej tablicy używają programy, które chcą mieć segmenty ponadstandardowe - jak np. emulatory Wine czy DOSemu - ale one pracują w trybie root-a, i robią te sztuczki raczej poza jądrem - UWAGA! jednak przełączanie musi wtedy uwzględnić zmianę LDT? może to jest w TSS?
O rozmiarze i obszarze dostępnej pamięci decyduje zatem ilość i miejsce przydzielonych stron.
dokładny opis deskryptora segmentu oraz pozostałe segmenty czyli to co znajduje się w tablicach GDT i ew. w LDT (tłumaczenie z KHG):
Oto deskryptor segmentu opisujący każdy segment w systemie. Są deskryptory regularne i systemowe. Dziwny format wynika z zachowania zgodności z 286. Rozmiar: 8 bajtów (64 bity).
63-54 | 55 | 54 | 53 | 52 | 51-48 | 47 | 46 | 45 | 44-40 | 39-16 | 15-0 |
---|---|---|---|---|---|---|---|---|---|---|---|
Base 31-24 |
G | D | R | U | Limit 19-16 | P | DPL | S | TYPE | Segment Base 23-0 |
Segment Limit 15-0 |
Wyjaśnienie:
R | zastrzeżone (0) |
DPL | 0 znaczy jądro, 3 znaczy użytkownik |
G | 1 to 4K rozdzielczość (Zawsze ustawione w Linux-ie) |
D | 1 oznacza odwołania 32-bitowe |
U | definiowalne przez programistę |
P | 1 - obecny w pamięci fizycznej |
S | 0 - segment systemowy , 1 - normalny segment kodu lub danych. |
Type | Wiele różnych możliwości, znaczenie zależy też od wartości S |
deskryptory systemowe w Linux-ie :
TSS: P=1, DPL=0, S=0, type=9, limit = 231, miejsce dla 1 tss_struct
(patrz TSS).
LDT: P=1, DPL=0, S=0, type=2, limit = 23, miejsce dla 3 deskryptorów segmentów.
Pole base jest ustawiane podczas fork(). Dla każdego procesu Tworzone
są TSS i LDT (obecnie LDT już nie, poza szczególnymi programami).
deskryptory regularne/standardowe Linux-a dla jądra: (head.S)
kod: P=1, DPL=0, S=1, G=1, D=1, type=a, base=0xc0000000, limit=0x3ffff
dane: P=1, DPL=0, S=1, G=1, D=1, type=2, base=0xc0000000, limit=0x3ffff
[@]Obecnie poniższe deskryptory są w GDT:
LDT dla task[0] (proces 0?) zawiera: (sched.h)
code: P=1, DPL=3, S=1, G=1, D=1, type=a, base=0xc0000000, limit=0x9f
data: P=1, DPL=3, S=1, G=1, D=1, type=2, base=0xc0000000, limit=0x9f
Domyślne LDT dla pozostałych procesów: (exec())
code: P=1, DPL=3, S=1, G=1, D=1, type=a, base=0, limit= 0xbffff
data: P=1, DPL=3, S=1, G=1, D=1, type=2, base=0, limit= 0xbffff
Z całą segmentacją związane są wydzielone rejestry IDTR, GDTR, LDTR i TR, jednak o nich nie będę pisał
Być może dla procesów, których exec ma ustawioną flagę SetUID, a właściciel jest root-em DPL=0, choć raczej nie - uprawnienia root-a to trochę co innego niż tryb pracy nadzorcy/jądra(ale to już jest działka chłopców od procesów).
Podsumowując - liniowy adres nie jest właściwie modyfikowany przez mechanizm segmentujący.
Mapa pamięci fizycznej przed rozpoczęciem jakichkolwiek procesów. Kolumna po lewej podaje adres startowy obszaru, adresy pisane pochyłą czcionką są przybliżone. Środkowa kolumna to nazwa obszaru(ów). Po prawej są podane odpowiednie procedury, zmienne lub objaśnienie roli obszaru.
0x110000 | WOLNE | memory_end or high_memory |
mem_map | mem_init() | |
inode_table | inode_init() | |
device data | device_init()* | |
0x100000 | dalsze pg_tables | paging_init() |
0x0A0000 | ZAREZERWOWANE | |
0x060000 | WOLNE | |
low_memory_start | ||
0x006000 | kod i dane jądra | |
floppy_track_buffer | ||
bad_pg_table bad_page |
używane przez page_fault_handler-y do eleganckiego zabijania procesów gdy brak pamięci. | |
0x002000 | pg0 | Pierwsza tablica stron jądra. |
0x001000 | swapper_pg_dir | Katalog (tablic?) stron jądra. |
0x000000 | strona null(zerowa) |
*inicjacje urządzeń żądające pamięci to(main.c): profil_buffer, con_init, psaux_init, rd_init, scsi_dev_init.
Obszary nie zaznaczone jako WOLNE są ZAREZERWOWANE. (mem_init). ZAREZERWOWANE strony przynależą do jądra i nie są nigdy zwalniane lub zrzucane(swapowane).
0xc0000000 | Niewidzialne jądro | zarezerwowane |
inicjalny stos | ||
miejsce na rozrost stosu | 4 strony | |
0x60000000 | biblioteki dzielone | |
brk | nie użyte | |
pamięć malloc-a | ||
end_data | niezainicjalizowane dane | |
end_code | zainicjalizowane dane | |
0x00000000 | tekst |
Zarówno segment kodu jak i danych rozciągają się od 0x00 do 3 GB.
Stos procesu jest jak widać po drugiej stronie w segmencie danych - poniżej kodu jądra (od 0x C000 0000 w dół wirtualnie).
Ochrona w ramach całego obszaru segmentu jest realizowana przez mechanizm stronicowania. Sam segment chroni tylko przed odwołaniem poza granicę 3Gb.
przypis:Warto zwrócić uwagę, że wszystkie segmenty pamięci dzielonej, obszary kodu, danych i stosu zawarte są w jednym, dużym segmencie, podwojonym z wymogów sprzętu - tzn. segment kodu, segment danych. Właściwy podział realizuje struktura vm_area_struct, o której szerzej niżej.
TSS (task swap(lub state) segment) - przełączanie procesów odbywa to
się przez (jawny?) skok do danego TSS.
Nie wykonuje się! wtedy zawarty tam kod
(bo tak zawsze można interpretować zawartość pamięci, np. te zamrożone dane o
procesie właśnie w TSS), tylko makroprocedura procesora zachowuje w bieżącym TSS
aktualny stan procesora , a nowy odtwarza z danych, do których skoczył,
w szczególności podstawowy dla stronicowania rejestr
CR3 czyli adres katalogu stron, oraz adres lokalnej
tablicy deskryptorów segmentów LDT, który obecnie z reguły nie
jest używany. Czas tej operacji wg. intel military i486 family spec.,
str. 87, linia 2 wynosi 10 us dla procesora i486DX33 - nie jest to mało w
stosunku do czasu najprostszej, 1-taktowej operacji: dla 33 MHz to jest 30ns,
(około 300 takich operacji można wykonać
zatem nie należy przełączania robić częściej, niż jest to potrzebne. Jeśli się
nie mylę, to w Linux-ie częstotliwość przełączania jest rzędu 10ms, to jest 1000x
więcej niż 10us, zatem straty powodowane przełączaniem to około 10us+epsilon/10ms
, a więc rzędu promili (0,1%), co jest do zaakceptowania. Pomijam tu jednak milczeniem
pewną delikatną kwestię wydajnościową zwiazaną z pamięcią cache.
Deskryptory TSS kolejnych procesów znajdują się obecnie w tablicy GDT
W tablicy GDT są to odpowiednio deskryptory o dwóch kolejnych indeksach (zapewne 4 i 5, choć to ulega zmianom), wspólne dla wszystkich procesów - bowiem to, co się naprawdę zmienia, to odpowiedni rejestr (CR3) pamiętany w TSS pokazujący adres tablicy stron (a dokładniej katalogu stron, jak dowiemy się przy opisie stronicowania).
Na temat szczegolow patrz pod Sprzetowe deskryptory.
Jak wyzej - tez dwa deksryptory, z tym ze inne indeksy w tablicy. Nie jest to tez tak ciekawe - jadro jest jedno - zatem nie dziwne, ze ma po jednym segmencie - danych i kodu.
Na temat szczegolow patrz pod Sprzetowe deskryptory.
Skok do konkretnej procedury systemowej
Tylko ogólnie: rejestry segmentowe ds i es zawierają selektor segmentu kodu jądra (w skrócie: wskazują na GDT[1], patrz wyżej GDT), a rejestr segmentowy fs zawiera selektor segmentu kodu wołającego procesu (GDT[4]?). Następnie wołane jest odpowiednie dla funkcji przerwanie.
Po pierwsze - nie robi się to tak prosto, jak tu zgrubsza podaję, bo nie można sobie ot tak zaglądać do segmentów jądra. Coś dzieje się z tablicami stron jądra i wołającego procesu, oraz jak rozstrzygane jest, czy proces może wywołać daną funkcję - to już są skomplikowane szczegóły.
3. Współdzielenie
Odbywa się raczej na poziomie stronicowania, oraz programowo - przez vm_area_struct (zarządzanie blokami SHM i wyzn. spójnych obszarów pamięci).
Jest to drzewo spójnych obszarów w pamięci wirtualnej, standardowo około
6, jednak łatwo może ich się zrobić dużo, zatem zastosowano wydajną strukturę
danych.
Jest to bowiem drzewo avl, służy głównie do wyznaczania wolnych spójnych
obszarów podczas tworzenia nowych segmentów(niesprzętowych) pamięci dzielonej.
Ogólnie jest zorganizowane jako drzewo avl ze względu na początkowy adres.
Wszystkie operacje dostępu/reorganizacji znajdują się w pliku mmap.c.
Także pewne operacje w ramach różnych bloków wykonują się inaczej (nie
ma na przykład odczytu z wyprzedzeniem do obszarów współdzielonych), co
jest także kontrolowane przez tą strukturę.
Jest to zorganizowane tak, że każdy blok vm_area_struct tego drzewa ma
listę operacji, różnie wyglądającą w zależności od typu bloku.
Wszelkie operacje rezerwacji/zwalniania odbywają się kaskadowo właśnie od vm_area_struct poprzez katalogi tablic/tablice (których elementy pokazują na fizyczne strony). Organizacja stronicowania jest opisana tu. Nie ma tu podziału na sprzętowe segmenty.
Jeszcze raz o bezpieczenstwie - ograniczenia wynikające z ochrony na poziomie stron: - tylko zakaz zapisu, nie można zakazać wykonania ani odczytu z danej strony. (Zatem można napisać program, który przeczytałby sam siebie, pomimo, że byłby nie do odczytania - warunek - trzeba w nim zawrzeć własny kod, który to zrealizuje - dlatego piszę NAPISAĆ, zatem raczej nieprzydatne do włamań... (a i tak nie za bardzo))
Rejestr CR3 po wykonaniu przełączenia zadań zostaje wczytany z danego
segmentu TSS, w tym rejestrze znajduje się właśnie adres katalogu tablicy
stron danego procesu. deskryptory segmentow TSS sa w tablicy GDT
(kazdy proces oddzielnie).
W tablicy GDT sa jeszcze 4 istotne deksryptory - dwa dla jadra (kod,dane),
dwa wspolne dla wszystkich procesow (kod dane).
W GDT moga sie znalezc jescze nadprogramowo w wyjatkowych sytuacjach wpisy
deskrytpory segmentow przeznaczonych na LDT czyli lokalna tablice
deksryptorow, w przypadku programow typu WINE (emulator W3.11/W95) czy DOSEMU
(wykonawca DOS).
Wlasciwa obrobka pamieci procesow najczęściej zaczyna się dopiero na poziomie stronicowania (poza np. Wine/DOSemu - jw.) - procesy mają bowiem różne katalogi tablic/tablice/strony fizyczne, być może częsciowo wspólne (kod lub SHM). Jednak to jest poza mechanizmem segmentacji (twórcy linuxa, jak już zaznaczyłem, pominęli milczeniem rozbudowane możliwości ochrony/rekalkulacji adresu na poziomie segmentacji, a zatem i ja pomijam je w swym opisie.)
przypis: Mogą one być ze względu na ilość (4 warstwy ochrony z kontrolą przejść między warstwami) zapewne przydatne do konstrukcji systemów opartych o ideę mikrojądra (jednak idea ta ze względu na malejącą cenę pamięci staje się nieaktualna, z drugiej strony wygodniej jest podzielić system na kilka niezależnych warstw coraz bardziej niezależnych od sprzętu)
mm/memory.c
mm/vm_area???.c
arch/i386/kernel/entry.S
arch/i386/kernel/*.S
- ??? --- !!! patrz stronicowanie !!!
>
include/asmi386/mm.h
include/asmi386/shed.h
include/asmi386/pgtable.h
Co, gdy ten sam adres - kodu i danych?
Takiej sytuacji chyba nie ma. Zgodnie z KHG - wirtualne wartości adresów kodu i danych są rozłączne.