Segmentacja – nielubiana siostra stronicowania

Co to?

Segmentacja, to obok stronicowania, popularny sposób organizacji pamięci. W kwestii samej pamięci nic się nie zmieniło – nadal rozważamy liniową przestrzeń, której komórki posiadają adresy (dla przypomnienia – adres do konkretnej komórki pamięci nazywamy adresem fizycznym). Mechanizm segentacji dzieli liniową pamięć na segmenty – segmentem, mówiąc najprościej, jest pewien (najczęściej ciągły) obszar pamięci. W taki sposób do odwołania się do naszej „płaskiej”, jednowymiarowej pamięci potrzebujemy dwuwymiarowego adresu (adres logicznego – czyli pewnego, jednoznacznego odpowiednika adresu fizycznego) – jest to nazwa (najczęściej numer) segmentu, oraz pozycja wewnątrz tego segmentu. Przeanalizujmy poniższy rysunek:

Pamięć adresowana jest od 0x0000 do 0xFFFF. Zawiera ona 3 segmenty o numerach (nazwach) 0, 1 i 2. Jak widzimy w każdym segmencie adresowanie zaczyna się od 0x0000, a kończy się na pewnym adresie, nazywanym popularnie limitem, który jednocześnie wyznacza rozmiar segmentu. Teraz, aby odwołać się do elementu 1 o adresie 0x3000 należy podać adres segmentu czyli 1 oraz odpowiednie przesunięcie wewnątrz tego segmentu, czyli 0x3000 (takie samo jak adres fizyczny, gdyż segment zaczyna się na adresie 0x0000) – ostatecznie otrzymujemy adres (1, 0x3000) (notacja: (segment, przesunięcie wewnątrzsegmentowe)), dla elementu 2 o adresie fizycznym 0xA123 otrzymujemy w posegmentowanej pamięci adres (0, 0x2124), natomiast element 3 o adresie fizycznym 0xFFFF ma adres logiczny (2, 0x1FFF). Wszystko powinno być już teraz jasne. Trzeba przyznać, że dosyć to udziwnione – liniowa pamięć jest wygodna, po co więc używać dwuwymiarowego odpowiednika i w ogóle skąd się te segmenty biorą?

Zalety

Segmentację opracowano w celu umożliwienia podziału programu i danych na logicznie niezależne przestrzenie adresowe oraz aby wprowadzić łatwy mechanizm bezpieczeństwa i współdzielenia.

Segmentacja jest widoczna w modelu programowym – programista może swodobnie definiować nowe, własne segmenty – w ten sposób dostajemy logiczny, elegancki podział informacji. Przykładowe segmenty to: kod, dane, stałe, bss (statycznie alokowane dane), stos itd. To, co się w nich znajdzie zależy tak naprawdę od inwencji programisty.

Segmentacja wprowadza sprytne zabezpiecznie programów przed niebezpiecznymi błędami typu przepełnienie bufora, gdyż poszczególne segmenty mogą być oznaczone jako kod lub jako dane. Próba „wykonania” segmentu z danymi lub nadpisania załadowanego kodu zakończy się błędem. Ponadto segmenty mogą posiadać zabezpiecznia ze względu na poziom uprawnień – tj. czy to kod/dane systemu operacyjnego, sterowników, czy użytkownika.

Kolejną zaletą jest łatwość relokacji danych. Wyobraźmy sobie, że chcemy mieć tablicę o zmiennym rozmiarze – taką do której możemy „doalokować” pewną liczbę bajtów. W pewnym momencie, przy zmianie długości może się okazać, że bezpośrednio po naszej tablicy znajdują się inne, potrzebne dane. Musimy wtedy zaalokować naszą nową, większą tablicę w innym, odpowiednio dużym miejscu – a co się z tym wiąże – zmienić jej adres. Korzystając z segmentacji możemy umieścić taką dynamiczną tablicę w odpowiednim segmencie i w miarę potrzeb zwiększać jego limit. Gdy okaże się, że rozszerzający się segment zacznie zachodzić na inny, możemy go wtedy relokować w inne miejsce, ale tym razem adres się nie zmieni, gdyż segment nadal będzie miał ten sam numer, a elementy w jego wnętrzu to samo przesunięcie (choć oczywiście wiąże się to z kopiowaniem danych).

Ostatnią zaletą, o której warto wspomnieć to możliwość łatwego współdzielenia kodu oraz danych. Wystarczy w tym celu utworzyć odpowiednie, współdzielone segmenty i pozwolić na korzystanie z nich odpowiednim procesom. W taki sposób uruchomienie kilku instancji danego programu nie powoduje zapełnienia dostępnej pamięci wieloma identycznymi wersjami kodu.

Wady

Wady mechanizmu segmentacji są oczywiste. Segmentacja wymusza na programiście używania nieliniowej przestrzeni adresowej co może być kłopotliwe. Ponadto, tworzenie wielu segmentów, a nastepnie ich usuwanie może doprowadzić do powstawania „dziur” pomiędzy kolejnymi segmentami, które będą zbyt małe, aby pomieścić nowe segmenty. Takie zjawisko nazywamy szachownicowaniem lub (popularniej) zewnętrzną fragmentacją. Problem ten można rozwiązać za pomocą bardzo kosztownego kompaktowania – czyli upychania segmentów tak, aby następowały po sobie.

Realia

Architektura x86 posiada wsparcie dla segmentacji. Można powiedzieć nawet więcej – segmentacja działa zawsze i nie da się jej wyłączyć (w przeciwieństwie do stronicowania), czego większość informatyków jest nieświadoma. Sprytnym, programowym obejściem tego faktu jest utworzenie dwóch segmentów, rozciągających się na całą pamięć – kodu i danych. Będą one wtedy oczywiście na siebie zachodzić, ale niczemu to nie szkodzi. Tej techniki używają wszystkie nowoczesne systemy operacyjne w tym Linux, Windows oraz Mac OS X. Dlaczego segmentacja jest tak nielubiana? Cóż, widocznie wszyscy programiści kochają liniową przestrzeń adresową ;). Warto jednak zauważyć, że we wspomnianych systemach operacyjnych segmentacja realizowana jest niejako programowo – tj. istnieje możliwość tworzenia własnych segmentów (słówka kluczowe segment albo section w NASM) w assemblerze. Nie istnieje jednak szczególny powód aby to robić, dlatego z tej możliwości korzystają tylko kompilatory języków wyższego poziomu tak, aby ułożyć logicznie kod (w sekcje code, data, bss).

Jeśli interesują Was systemy korzystające z segmentacji, polecam poczytać o systemie MULTICS (z którego wywodzi się UNIX),  w którym mechanizmu tego użyto.