Projekt: "Ćwiczenia z budowy systemu operacyjnego Linux wersja 2.4.7"
Obsługa tablicy ramek

Janusz Kuligowski, nr albumu 171815

Grudzień 2001

Wprowadzenie

W poniższym tekście przedstawiam podstawowe informacje dotyczące obsługi tablicy ramek. Tekst ten jest zatem wstępem do tematów zawartych w następnych rozdziałach.

Skupiam się w nim na wersji systemu dla procesorów architektury i386. O ile podstawowe pojęcia nie zależą od wybranej platformy sprzętowej (np. ramka, strona, pamięć wirtualna), o tyle pewne parametry są z nią ściśle związane.

Należy więc zauważyć, że dla omawianej rodziny procesorów przestrzeń adresowa jest ograniczona do 4 GB, ze względu na wykorzystywanie do zapisania adresu 32 bitów.

W źródłach termin "page" bywa używany zarówno w odniesieniu do ramek, jak i stron - zatem różne przeze mnie tłumaczenie przeze mnie słowa page nie wynika z niekonsekwencji, ale jest następstwem używania w polskiej terminologii odrębnych terminów - dla przypomnienia:

Stałe

Poznawanie obsługi tablicy ramek proponuję rozpocząć od pliku include/asm-i386/page.h. Zdefiniowane tam są między innymi stałe, określające wielkość ramki (a tym samym wielkość strony). Ponieważ w różnych sytuacjach zachodzi konieczność wykonywania operacji na bitach, zatem najpierw zdefiniowano rozmiar długości adresu w ramach strony:

#define PAGE_SHIFT 12

(12 bitów - czyli 4096 bajtów w jednej ramce)

a następnie liczbowo wielkość ramki:

#define PAGE_SIZE (1UL « PAGE_SHIFT)

$2^1$$^2$ - czyli 4096 bajtów w jednej ramce. O, tyle samo, co poprzednio! Czyli wielkość ramki = wielkości ramki. To dobrze.

Znajduje się tu także definicja stałej PAGE_MASK - maski bitowej, mającej jedynki na pozycjach oznaczających bity numeru strony i zera na pozycjach bitów przesunięcia (offsetu) - czyli adresu w obrębie strony:

#define PAGE_MASK ( (PAGE_SIZE-1)) </PRE> .

Przykład zastosowania tej maski w praktyce znajduje się już w tym pliku parę linijek niżej:

<PRE> #define PAGE_ALIGN(addr) (((addr)+PAGE_SIZE-1)&PAGE_MASK) )) </PRE>
- jest to makro ustawiające addr na początek nowej ramki; stała PAGE_MASK jest wykorzystana do wyzerowania bitów offsetu adresu.)

Zdefiniowanie stałej PAGE_SIZE na liczbę 12 oznacza, że pamięć dzielimy na ramki wielkości 4kB.

Kolejną ważną stałą jest:

#define __PAGE_OFFSET (0xC0000000)
#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
,

co jak widać ustala wielkość pamięci użytkownika na 3GB - zatem wirtualna przestrzeń adresowa jądra zajmuje tylko 1GB, co umożliwia wykorzystanie co najwyżej 950MB fizycznej pamięci.

Struktura page

Z każdą ramką ramką związana jest instancja typu struct page służąca przechowaniu informacji o operacjach wykonywanych na ramce - nie jesteśmy jednak w stanie stwierdzić, które zadania korzystają z ramki w konkretnym momencie.

Definicja

W pliku include/linux/mm.h znajduje się definicja tej struktury - przytoczę ją tutaj w całości, gdyż poznanie jej jest potrzebne do zrozumienia dalszej części kodu jądra:

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;


oraz bardzo ważna dla nas deklaracja:
/* The array of struct pages */ extern mem_map_t * mem_map ; /* Jest to globalna tablica ramek . */


A oto znaczenie pól struktury page:

list; /* wskaźniki listowe */
*mapping; /* wskaźnik I-węzła mapowanego pliku */
index; /* pozycja w mapowanym pliku */
*next_hash; /* następna ramka o tym samym kluczu w tablicy haszującej */
count; /* licznik odwołań */
flags; /* flagi bitowe */
lru; /* wskaźniki listy zwalniania, chronione przez pagemap_lru_lock */
age; /* licznik wieku strony w ramce */
wait; /* ramka zablokowana? ustaw się w kolejce... */
**pprev_hash; /* poprzednia ramka o tym samym kluczu w tablicy haszującej */
* buffers; /* wskaźnik na listę nagłówków buforów (jeżeli ramka jest wykorzystywana na bufory) */
virtual; /* wirtualny adres na potrzeby jądra */
*zone; /* strefa pamięci, w której ramka się znajduje */

Zostaje ona zainicjowana w pliku /mm/page_alloc.c w funkcji free_area_init_core(). Początkowo wszystkie ramki mają ustawioną flagę PG_reserved, zwalniane są dopiero w funkcji free_all_bootmem() z pliku /mm/bootmem.c. (W pliku tym znajdują się też inne funkcje używane przy starcie systemu.)

Dla wyjaśnienia - w pliku /include/linux/list.h znajduje się definicja struktury list_head:

struct list_head {
struct list_head *next, *prev;
};

Są to wskaźniki na następny i poprzedni element listy. Stosowanie takiej struktury uzasadniono możliwością lepszej optymalizacji kodu funkcji operujących na całych listach, niż przy zastosowaniu operacji na pojedynczych elementach listy.

Struktura zone

Niektóre maszyny (np. PeCety) wymagają podziału fizycznej pamięci na strefy. W PeCetach mamy trzy strefy: W pliku include/linux/mmzone.h znajduje opis się wykorzystywanej do realizacji tego podziału struktury zone_struct:

typedef struct zone_struct {

spinlock_t lock;
unsigned long free_pages;
unsigned long inactive_clean_pages;
unsigned long inactive_dirty_pages;
unsigned long pages_min, pages_low, pages_high;

struct list_head inactive_clean_list;
free_area_t free_area[MAX_ORDER];

struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
unsigned long zone_start_paddr;
unsigned long zone_start_mapnr;

char *name;
unsigned long size;
} zone_t;



/* Te pola są najczęściej używane: */

lock; /*
semaforek */
free_pages; /*
liczba wolnych ramek */
inactive_clean_pages; /*
liczba nieaktywnych, niezmodyfikowanych stron */
inactive_dirty_pages; /*
liczba nieaktywnych, zmodyfikowanych stron */
pages_min, pages_low, pages_high; /*
m.in. minimalna liczba wolnych ramek, wymagana w systemie */

/* Wolne obszary różnych rozmiarów: */
inactive_clean_list; /*
wskaźniki listowe - jak zwykle */
free_area[MAX_ORDER]; /*
tablica list wolnych obszarów (według rozmiaru) */

/* Pola do obsługi pamięci: */
*zone_pgdat;
*zone_mem_map; /*
ramki mieszczące się w strefie */
zone_start_paddr;
zone_start_mapnr;



Rodzaje stref są zdefiniowane kawałek niżej:

#define ZONE_DMA 0
#define ZONE_NORMAL 1
#define ZONE_HIGHMEM 2
#define MAX_NR_ZONES 3


Struktura ta (oraz jej zastosowanie) jest dokładnie opisane w rozdziale dotyczącym strategii buddy.

Wykorzystanie

Wracamy do include/linux/mm.h. Są w nim zdefiniowane makra, służące obsłudze licznika użycia - czyli pola count w strukturze page.

Co rozumiemy przez ,,użycie'' ramki (a dokładniej: znajdującej się w niej strony): Poza tym wiele procedur jądra zwiększa ten licznik przed istotnymi operacjami, aby uniemożliwić usunięcie zawartości tej strony na dysk.

A oto przykłady makr do obsługi tego licznika:

#define get_page(p) atomic_inc(&(p)->count) /* pobieramy */
#define put_page(p) __free_page(p) /* oddajemy */
#define put_page_testzero(p) atomic_dec_and_test(&(p)->count)
#define page_count(p) atomic_read(&(p)->count)
#define set_page_count(p,v) atomic_set(&(p)->count, v)


Wiele procesów może "widzieć" tę samą stronę (np. przy mapowaniu /dev/null wszystkie procesy widzą tę samą stronę pełną zer). Jeśli powyższa flaga nie jest ustawiona - wtedy page->count zawiera licznik odwołań do ramki:
page->count == 0 oznacza, że ramka jest wolna,
page->count == 1 oznacza, że ramka jest używana w dokładnie jednym celu (np. prywatne dane jednego procesu).

Ramka może być zarezerwowana przez funkcję kmalloc() lub kogokolwiek, kto wywoła funkcję __get_free_page(). W takim przypadku page->count wynosi co najmniej 1, a wszystkie pozostałe pola są nieużywane, ale powinny być ustawione na 0 lub NULL.
Obowiązek zarządzania tą ramką spoczywa wówczas na wywołującym tę funkcję.

Pozostałe ramki (tzw. ramki procesów) są w całości zarządzane przez systemową obsługę pamięci - w szczególności wszystkie operacje związane z wysyłaniem stron na dysk są obsługiwane przez jądro.

Ramka może być wykorzystywania do mapowania i-węzła. Wówczas page->mapping jest wskaźnikiem do i-węzła, a page->offset zawiera pozycję w pliku zawartości ramki.

Jeśli ramka ma dowiązane bufory, wtedy page->buffers zawiera cykliczna listę buforów.

Dla ramek zawierających i-węzły page->count zawiera liczbę dowiązań, plus 1 jeśli są bufory, plus 1 dla cache ramki. Wszystkie ramki należące do i-węzłów są powiązane w następujące listy: mapping->clean_pages, mapping->dirty_pages, mapping->locked_pages.

W systemie znajduje się globalna tablica page_hash_table (z kluczem będącym parą (inode, offset), zadeklarowana w pliku /include/linux/pagemap.h), umożliwiająca odnalezienie ramki, zawierającej szukany fragment pliku. W ramce takiej w polach page->next_hash i page->pprev_hash znajdują się wskaźniki do innych elementów o tej samej pozycji w tablicy haszującej.

Operacje wejścia-wyjścia, flagi

Wszystkie ramki należące do procesów mogą podlegac operacjom We/Wy, np: Na czas operacji wejścia/wyjścia ustawiana jest flaga PG_locked. Zadania oczekujące na zakończenie operacji czekają w kolejce page->wait. Flaga PG_uptodate mówi o tym, że zawartość ramki jest aktualna.

PG_reserved jest ustawiona dla tych szczególnych stron, które nie mogą być nigdy przeniesione na dysk - na przykład strony wykorzystywane na kod i struktury jądra.

Aby umożliwić wybór strony do wyswapowania na dysk ramki mapujące pliki używają bitu PG_referenced, który jest ustawiany zawsze wtedy, gdy system odwołuje się do ramki przez tablicę haszującą.

PG_error jest ustawiana w przypadku wystąpienia błędu wejścia/wyjścia przy obsłudze tej ramki.

Strony z ustawionym bitem PG_highmem nie są stale umieszczone w wirtualnej przestrzeni adresowej jądra - muszą byc "kmapped" (przyłączane) oddzielnie przed wykonaniem operacji wejścia/wyjścia.

Oto część flag:


#define PG_locked 0 /* Zablokowana. Nie dotykaj! */
#define PG_error 1 /* Był błąd We/Wy */
#define PG_referenced 2 /* Były odwołania */
#define PG_uptodate 3 /* Zgodna ze swoim odwzorowaniem na dysku */
#define PG_dirty 4 /* Była modyfikowana */
#define PG_active 6 /* Aktywna */
#define PG_slab 8 /* Wykorzystywana na pamięć jądra */
#define PG_inactive_clean 11 /* Nieaktywna, niemodyfikowana (może być odesłana na dysk) */

Aby ułatwić obsługe tych flag zdefiniowano następujące makra:

#define Page_Uptodate(page) test_bit(PG_uptodate, &(page)->flags)
#define SetPageUptodate(page) set_bit(PG_uptodate, &(page)->flags)
#define ClearPageUptodate(page) clear_bit(PG_uptodate, &(page)->flags)
#define PageDirty(page) test_bit(PG_dirty, &(page)->flags)
#define SetPageDirty(page) set_bit(PG_dirty, &(page)->flags)
#define ClearPageDirty(page) clear_bit(PG_dirty, &(page)->flags)
#define PageLocked(page) test_bit(PG_locked, &(page)->flags)
#define LockPage(page) set_bit(PG_locked, &(page)->flags)
#define TryLockPage(page) test_and_set_bit(PG_locked, &(page)->flags)
#define PageChecked(page) test_bit(PG_checked, &(page)->flags)
#define SetPageChecked(page) set_bit(PG_checked, &(page)->flags)

oraz funkcje:
extern void __set_page_dirty(struct page *);

static inline void set_page_dirty(struct page * page)
{
if (!test_and_set_bit(PG_dirty, &page->flags))
__set_page_dirty(page);
}


Jak widać - polegają one na wykonywaniu operacji bitowych. Makr takich jest jeszcze kilkadziesiąt - nię będę ich wszystkich wymieniać - zainteresowanym polecam lekturę źródeł.

Struktura address_space

Natomiast w pliku /include/linux/fs.h znajdujemy definicję struktury address_space (tego typu jest pole mapping w strukturze page):

struct address_space {
struct list_head clean_pages ;
struct list_head dirty_pages ;
struct list_head locked_pages ;
unsigned long nrpages ;
struct address_space_operations *a_ops ;
struct inode *host ;
struct vm_area_struct *i_mmap ;
struct vm_area_struct *i_mmap_shared ;
spinlock_t i_shared_lock ;
int gfp_mask ;
};


Oto opis jej pól:

clean_pages /* lista czystych stron */
dirty_pages /* lista zmodyfikowanych stron */
locked_pages /* lista zablokowanych stron */
nrpages /* liczba wszystkich stron */
*a_ops /* operacje, które można wykonywać na tej strukturze */
*host /* właścicel: inode, block_device */
*i_mmap /* lista prywatnych mapowań */
*i_mmap_shared /* lista współdzielonych mapowań */
i_shared_lock /* semafor */
gfp_mask /* sposób alokacji pamięci */

Pojawia się tutaj bardzo ciekawa struktura address_space_operations. Struktury z rodziny ,,struktur operacji'' (o nazwach: ..._operations) przywołują na myśl obiektowe podejście do programowania - gdyż są to wskaźniki do funkcji, mogących wykonywać operacje na polach struktury. Są one nawet nazywane przez autorów metodami :

struct address_space_operations {

int (*writepage)(struct page *);
int (*readpage)(struct file *, struct page *);
int (*sync_page)(struct page *);
int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
int (*bmap)(struct address_space *, long);
};


Jak widać są to typowe operacje wejścia/wyjścia. Sama struktura moze nie wygląda szczególnie wyjątkowo, niemniej jest to ciekawy pomysł na wprowadzenie elementów obiektowości, nie sądzicie?


Pożegnanie

Na tym kończę informacje o obsłudze ramek. Szczegóły wykorzystania wspomnianych przeze mnie struktur i funkcji zostaną omówione w dalszych rozdziałach prezentacji.


Dziękuję za lekturę!
Janusz Kuligowski



Janusz Kuligowski, nr albumu 171815