Kernel 2.6

Prezentacja przedstawiająca nowe cechy jądra systemu Linux o numerze 2.6.

Autorzy: Maciej Cielecki, Konrad Durnoga, Adam Fuksa i Jędrzej Fulara.

(zaleca się oglądanie rysunków z ustawioną 24-bitową głębią barw)


Spis tresci

  1. Wstęp
  2. Asynchroniczne Wejście-Wyjście (AIO)
  3. Blokowe operacje wejścia/wyjścia
  4. Wywłaszczalne jądro
  5. Taktowanie zegara
  6. NUMA - Non-Uniform Memory Access
  7. Hyper-Threading
  8. NPTL & NGPT
  9. Scheduler
  10. Wywołania systemowe
  11. Zarządzanie informacjami o urządzeniach
  12. Moduły
  13. RMAP - Odwrotne odwzorowanie stron
  14. Systemy plików
  15. LVM2 i Device-mapper
  16. Wydajność
  17. Czego zabrakło w 2.6?
  18. Błędy
  19. Inne
  20. Bibliografia
  21. Zakończenie

Wstęp

Linux bardzo się zmienił w wersji 2.6. Poniżej przedstawiamy wybrane przez nas zmiany, które wybraliśmy jako wyjątkowo interesjuące.

Zgodnie z życzeniem pani JMD, stworzyliśmy wiele diagramów przedstawiających różne procesy w nowym jądrze.


Asynchroniczne Wejście-Wyjście (AIO)

AIO pozwala nawet pojedynczemu wątkowi aplikacji na wykonywanie operacji I/O jednocześnie z dowolnym innym przetwarzaniem. Jest to zrealizowane poprzez interfejs pozwalający na wysyłanie jednego lub więcej żądań operacji I/O w jednym wywołaniu systemowym bez potrzeby czekania na zakończenie tych operacji. Takie żądania zawsze powiązane są z jakimś kontekstem.

long sys_io_submit(aio_context_t ctx_id, long nr, struct iocb __user * __user *iocbpp)

Oddzielny interfejs jest odpowiedzialny za zbieranie zakończonych operacji związanych z danym kontekstem.

long sys_io_getevents(aio_context_t ctx_id, long min_nr, long nr,
	struct io_event __user *events, struct timespec __user *timeout)

Poniższe funkcje służą do tworzenia i kasowania kontekstu

long sys_io_setup(unsigned nr_events, aio_context_t __user *ctxp)
long sys_io_destroy(aio_context_t ctx)

Można także spróbować anulować asynchroniczą operację I/O

long sys_io_cancel(aio_context_t ctx_id, struct iocb __user *iocb, struct io_event __user *result)

AIO znajduje zastosowanie w aplikacjach, które wykonują bardzo dużo operacji I/O, np

AIO w linuxie jest niezgodne z POSIX, ale za to jest wydajniejsze. Obsługa AIO została dołączona do jądra 2.6. Istnieją także patche na jądro 2.4.

Źródła:


Blokowe operacje wejścia/wyjścia

W jądrze 2.6 zaimplementowano od nowa mechanizm blokowego I/O. Twórcy postawili przed sobą następujące zadania:

Do tej pory do obsługi żądań używano struktury buffer_head. Nie pozwalała ona na realizację wyżej wymienionych postulatów. Na przykład jedno duże żądanie było zawsze rozbijane na wiele małych. Powodowało to duży narzut pamięci związany z tworzeniem dużej ilości struktur buffer_head (po jednym dla każdego kawałka).

Obecnie przyjęte rozwiązanie o wiele efektywniej gospodaruje pamięcią. Wprowadzono nową strukturę Bio (zdefiniwana w inlude/linux/bio.h).

struct bio {
         sector_t                bi_sector;
         struct bio              *bi_next;       
         struct block_device     *bi_bdev;
         unsigned long           bi_flags;       
         unsigned long           bi_rw;          
 
         unsigned short          bi_vcnt;        /* ilosc bio_vec */
         unsigned short          bi_idx;         /* aktualny indeks w tablicy wektorow */

         unsigned short          bi_phys_segments;
 
         unsigned short          bi_hw_segments;
 
         unsigned int            bi_size;        

         unsigned int            bi_hw_front_size;
         unsigned int            bi_hw_back_size;
 
         unsigned int            bi_max_vecs;    
 
         struct bio_vec          *bi_io_vec;     /* tablica bio_vec */
 
         bio_end_io_t            *bi_end_io;
         atomic_t                bi_cnt;
 
         void                    *bi_private;
 
         bio_destructor_t        *bi_destructor;
};

Struktura bio zawiera tablicę struktur bio_vec. Bio_vec to krotka, złożona ze stony, offsetu na stronie, od którego rozpoczyna się bufor i długości tego bufora:

struct bio_vec {
         struct page     *bv_page;
         unsigned int    bv_len;
         unsigned int    bv_offset;
};

Tablica bi_io_vec może być współdzielona przez różne struktury bio. Umożliwia to zrealizowanie wymagania dotyczącego równoległego odczytu z niezależnych źródeł (np. macierze dyskowe RAID).


Wywłaszczalne jądro

W dotychczasowych wersjach Linuksa procesy działające w trybie jądra (w tym również procesy użytkownika, które znalazły się w trybie jądra przy wywołaniu funkcji systemowych) nie mogły zostać wywłaszczone przez inny proces. Mogły one działać dopóty, dopóki same dobrowolnie nie zrzekły się procesora (np. dopóki nie powróciły z wywołania z funkcji systemowej bądź nie zasnęły w oczekiwaniu na jakiś zasób). Czas jaki procesy mogły pozostawać w trybie jądra, a tym samym blokować dostęp do procesora, nie był ograniczony przez żadną arbitralnie ustaloną wartość. Mogło to powodować, że proces przebywał w przestrzeni jądra nawet kilkaset milisekund. Z punktu widzenia niektórych aplikacji (na przykład multimedialnych) wymagających częstego (co kilka / kilkanaście milisekund) dostępu do procesora tak długie oczekiwanie jest nieakceptowalne. Rozwiązaniem tego problemu jest wprowadzony w jądrze 2.6 mechanizm wywłaszczania jądra (preemptible kernel). Obserwujemy obecnie tendencję do przekształcania Linuxa w system w pełni Real-Time'owy, czyli taki, w którym każde przerwanie zostanie obsłużone w z góry narzuconym czasie.

W jądrze 2.6 możliwe jest wywłaszczenie każdego wątku, w tym również działającego w trybie jądra, w (prawie) dowolnym momencie. Takie podejście jest źródłem pewnych trudności: należy zapewnić spójność wszystkich danych w krytycznych momentach.

Na przyład taki kod mógłby mieć niepożądany efekt:

int arr[NR_CPUS];
arr[smp_processor_id()] = i;
/* tutaj następuje wywłaszczenie */
j = arr[smp_processor_id()];   /* i oraz j nie muszą być takie same - proces mógł wrócić na inny procesor */

Istnieją więc momenty, w których wywłaszczenie NIE MOŻE nastąpić. Należy więc newralgiczne pukty kodu chronić - używając specjalnych locków (preempt_enable(), preempt_disable()).

#define preempt_disable() \
 do { \
         inc_preempt_count(); \
         barrier(); \
 } while (0)

#define preempt_enable() \
 do { \
         preempt_enable_no_resched(); \
         preempt_check_resched(); \
 } while (0)

Czasami preempt_enable() i preempt_disable() są wołane implicite. Na przykład w funkcjach obsługujących wirujące blokady.

#define spin_lock_irqsave(lock, flags) \
 do { \
         local_irq_save(flags); \
         preempt_disable(); \
         _raw_spin_lock(lock); \
 } while (0) 

#define spin_unlock_irqrestore(lock, flags) \
 do { \
         _raw_spin_unlock(lock); \
         local_irq_restore(flags); \
         preempt_enable(); \
 } while (0)

Proces działający w trybie jądra można więc wywłaszczyć, gdy nie ma on założonego żadnego locka.

Aby uczynić jądro wywłaszczalnym, dodano do task_struct licznik preempt_count. Na początku jego wartość wynosi zero. Przy każdym założeniu locka jego wartość jest zwiększana, a przy zdjęciu - zmniejszana o jeden.

Gdy licznik wynosi zero, można proces wywłaszczyć. Przy powrocie z przerwania do przestrzeni jądra, sprawdzane są licznik preemt_count oraz flaga zwracana przez need_resched(). Jeśli licznik jest wyzerowany a flaga ustawiona, to oznacza, że oczekuje jakiś gotowy do wykonania proces o wyższym priorytecie oraz jądro jest w stanie stabilnym. Można przekazać sterowanie. Jeśli jednak licznik jest różny od zera, oznacza to, że nie można wywołać funkcji schedulera. Sterowanie jest przekazywane z powrotem do tego samego procesu.

Wywłaszczenie jądra może również nastąpić explicite: jeśli proces znajdujący się w jądrze zasypia lub wywoła funkcję schedule(). W tym wypadku nie jest sprawdzana wartość licznika preempt_count. Zakłada się, że proces wołający schedule() wie co robi.

A więc, podsumowując - jądro może być wywłaszczone, gdy:


Taktowanie zegara

Jądro umożliwia dynamiczne ("w locie") zmienianie szybkości procesora. Możliwy jest zarówno odczyt jak i zapis do pliku /proc/cpufreq.

Wspierane są między innymi technologie: Intel SpeedStep, Itel Xeon, AMD PowerNow K6, AMD Elan czy VIA Cyrix Longhaul

Powyżej opisana właściwość jest szczególnie istotna w oszczędnym gospodarowaniu energią, co z kolei ma kluczowe znaczenie dla komputerów przenośnych.

Źródło: www.brodo.de/cpufreq


NUMA - Non-Uniform Memory Access

NUMA jest architekturą wieloprocesorową zaprojektowaną w celu pokonania ograniczeń architektury SMP (Symmetric Multi-Processing). W SMP wszystkie dostępy do pamięci odbywały się po tej samej, wspólnej dla wszystkich CPU szynie. Spełniało to swoje zadanie dla relatywnie małej ilości procesorów. Jednak wraz ze wzrostem ich liczby (dziesiątki lub setki), CPU zaczynają współzawodniczyć o dostęp do wspólnej szyny, która staje się wówczas wąskim gardłem całego systemu. NUMA rozwiązuje ten problem poprzez ograniczenie liczby procesorów korzystających z tej samej szyny.

W architekturze NUMA cały system podzielony jest na tak zwane węzły. Jeden węzeł obejmuje bloki pamięci, procesory i urządzenia wejścia - wyjścia, które są fizycznie podpięte do jednej szyny pamięci. Pamięć znajdująca się w tym samym węźle co procesor jest jego pamięcią lokalną (ang. local), zaś ta z innych węzłów - zdalną (ang. remote). Jakkolwiek możliwy jest dostęp zarówno do jednej jak i do drugiej pamięci, to jednak odwołanie do pamięci zdalnej jest dużo mniej efektywne niż do lokalnej.

Jądro 2.6 udostępnia programowe wsparcie dla obsługi architektury NUMA. "Planista przydziału procesora" stara się nie przydzielać procesu do wykonania na procesory z różnych węzłów. Co więcej, zarządzanie pamięcią zorganizowane jest tak, by proces nie musiał sięgać do pamięci z innych węzłów.

Dodatkowo jądro stara się równoważyć obciążenie różnych węzłów. Wykorzystywane są do tego następujące funkcje (kernel/sched.c)

static int find_busiest_node(int this_node)
static int sched_best_cpu(struct task_struct *p)
void sched_balance_exec(void)
static void balance_node(runqueue_t *this_rq, int idle, int this_cpu)

W jądrze 2.6.5 równoważenie to było wykonywane co 400 ms.

Przykładowe architektury NUMA:


Hyper-Threading

Firma Intel w linii procesorów P4 wprowadziła technologię Hyper-Threading. Jest to sprzętowe symulowanie na jednym fizycznym procesorze kilku procesorów logicznych. Technologia ta przyspiesza wykonywanie aplikacji wielowątkowych.

Jądro 2.6 wspiera obsługę procesorów z technologią HT: rozpoznaje zarówno procesory fizyczne jak i logiczne. (Patrz kernel/cpu/intel.c oraz kernel/sched.c)


NPTL & NGPT

Obecnie rozwijane są dwa konkurencyjne projekty obsługi wątków zgodne ze standardem POSIX. Są to NGPT (New Generation POSIX Threads) oraz NPTL (Native POSIX Thread Library).

Rozważane są różne modele obsługi wątków.

Do tworzenia procesów wielowątkowych używana jest funkcja clone(). Powoduje ona jednak nieprzenośność kodu.

Realizowana przez IBM oraz Intel biblioteka NGPT jest oparta na hybrydowym modelu M:N. Uznaje się, że model ten jest najlepszą drogą do osiągnięcia optymalnej wydajności. Wymaga on jednak utrzymywania dwóch specjalnych schedulerów: jednego w przestrzeni jądra, drugiego w przestrzeni użytkownika. Obsługa sygnałów staje się jednak bardzo skomplikowana.

Ulrich Drepper wraz z Ingo Molnarem realizują alternatywny projekt NPTL. Bazuje on na modelu 1:1. Zaletą tego podejścia jest jego prostota. Jest tylko jeden scheduler, obsługa sygnałów leży w gestii kernela. Nie występują również komplikacje związane z blokowaniem się wątków, czego nie można powiedzieć o NGPT. Zasadnicza część NPTL-a jest zaimplementowana jako biblioteka z przestrzeni użytkownika, włączona do biblioteki Gnu C.

Porównanie wydajności NGPT i NPTL:

Źródło: www.onlamp.com


Scheduler

W jądrze 2.6 największe zmiany w porównaniu do wersji 2.4 dotyczą schedulera. Zastosowano całkowicie nowy algorytm szeregowania procesów. Dzięki temu planista działa w czasie stałym. Autorem nowego schedulera jest Ingo Molnar.

Scheduler w starych wersjach jądra Linuksa miał wiele wad. Algorytm planujący przydział procesora różnym procesom działał w czasie liniowym. Było to spowodowane przeliczaniem priorytetów i przyznawaniem kwantów czasu wszystkim procesom dopiero po upływie całej epoki. Poza tym system utrzymywał jedną, globalną kolejkę procesów gotowych. W przypadku komputerów wieloprocesorowych było to rozwiązanie bardzo nieoptymalne: wszystkie procesory czekały, aż na ostatnim zakończy się praca. Poza tym nie było specjalnego wsparcia dla procesów interaktywnych, co miało negatywny wpływ na wydajność większości aplikacji. Wszystko to skłoniło programistów Linuksa do opracowania nowego, bardziej wydajnego planisty.

Cele stawiane przed nowym schedulerem:

Wszystkie te cele udało się osiągnąć.

Główne idee algorytmu zaproponowanego przez Ingo Molnara

Do tej pory była utrzymywana jedna globalna kolejka procesów gotowych. W nowym schedulerze do każdego procesora przyporządkowana jest oddzielna, każdy gotowy do wykonania proces znajduje się w dokładnie jednej takiej kolejce. Umożliwia to niezależne planowanie przydziału czasu procesora dla każdego CPU. Zapewnia to skalowalność architektur wieloprocesorowych. Kolejki te są następującego typu:

struct runqueue {
    spinlock_t     lock;        	/* spin lock which protects this runqueue */
    unsigned long    nr_running;     	/* number of runnable tasks */
    unsigned long    nr_switches;     	/* number of contextswitches */
    unsigned long    expired_timestamp;  /* time of last array swap */
    unsigned long    nr_uninterruptible; /* number of tasks in uinterruptible sleep */
    struct task_struct *curr;        /* this processor's currently running task */
    struct task_struct *idle;        /* this processor's idle task */
    struct mm_struct  *prev_mm;      /* mm_struct of last running task  */
    struct prio_array  *active;       /* pointer to the active priority array */
    struct prio_array  *expired;      /* pointer to the expired priority array */
    struct prio_array  arrays[2];      /* the actual priority arrays */
    int         prev_cpu_load[NR_CPUS];/* load on each processor */
    struct task_struct *migration_thread;  /* the migration thread on this processor */
    struct list_head  migration_queue;   /* the migration queue for this processor */
    atomic_t      nr_iowait;      /* number of tasks waiting on I/O */
}

Ponieważ struktura runqueue jest jedną z ważniejszych dla schedulera struktur danych, to udostępnione są do jej obsługi następujące makra:

Najważniejszymi polami runqueue są tablice active i expired.

Active i Expired są typu prio_array:

struct prio_array {
    int        nr_active;      /* number of tasks */ 
    unsigned long   bitmap[BITMAP_SIZE]; /* priority bitmap */
    struct list_head queue[MAX_PRIO];   /* priority queues */
};

To właśnie tablice typu prio_array zapewniają stały czas działania. Zawierają one osobną kolejkę procesów dla każdego priorytetu (tablica queue). Dodatkowo utrzymywana jest bitmapa, pozwalająca na szybkie ustalenie najwyższego priorytetu oczekujących procesów (funkcja sched_find_first_bit()).

Różnych priorytetów jest 140. Najbardziej uprzywilejowany jest proces o priorytecie równym zero. Priorytety od 0 do 99 odpowiadają procesom klasy RT zaś od 100 do 139 pozostałym procesom. Procesy klasy RT są szeregowane zgodnie ze strategią FIFO lub Round Robin. Natomiast pozostałe - jedynie zgodnie z Round Robin.

Przydział kwantu czasu

W wielu systemach operacyjnych (w tym również w starszych wersjach Linuxa) przydzielanie nowego kwantu czasu następuje po całej epoce.

struct task_struct *p;

for_each_task(p)
	p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);

Prowadzi to do schedulera działającego w czasie liniowym.

W nowym Linuxie zamiast tego wprowadzono dla każdego procesora wspomniane już tablice Active i Expired. Pierwsza z nich zawiera procesy, które nie wyczerpały jeszcze swojego kwantu czasu. Druga - przez analogię - te, które swój czas już wykorzystały. W momencie, gdy proces jest przenoszony do Expired, automatycznie jest wyliczany dla niego nowy priorytet i nowy kwant czasu.

Warto zauważyć, że procesy klasy RT nigdy nie są wsadzane do kolejki Expired. Gdy wszystkie procesy znajdą się w Expired, następuje proste przepięcie wskaźników.

struct prio_array array = rq->active;
if (!array->nr_active) {
	rq->active = rq->expired;
	rq->expired = array;
}

Zastępuje to pętlę występującą we wcześniejszych wersjach jądra.

Wyznaczanie priorytetu dynamicznego

Każdy proces ma przyznaną przez użytkownika wartość nice (z przedziału -19 do 20). Wartość ta jest podstawą obliczenia tak zwanego priorytetu statycznego. (NICE_TO_PRIO(nice))

#define MAX_USER_RT_PRIO        100
#define MAX_RT_PRIO             MAX_USER_RT_PRIO
#define NICE_TO_PRIO(nice)      (MAX_RT_PRIO + (nice) + 20)

Obliczona w ten sposób wartość przechowywana jest w polu static_prio w strukturze task_struct.

Priorytet dynamiczny jest funkcją priorytetu statycznego i poziomu interaktywności. Procesy interaktywne są "nagradzane". Wartość ich priorytetu dynamicznego może być zmniejszona o 5, za to procesom obliczeniowym (obciążającym procesor) grozi "kara" - zwiększenie wartości priorytetu o (co najwyżej) 5. Oczywiście wartość priorytetu "zwykłego" procesu nie może wyjść poza przedział [100..139].

Wartość priorytetu dynamicznego jest zwracana przez funkcję effective_prio().

Wyznaczanie poziomu interaktywności

Głównym wyznacznikiem interaktywności procesu jest ilość czasu, jaki spędza on na operacjach wejścia / wyjścia. Mierzy się więc, ile czasu proces "śpi". Im większą część czasu śpi, tym jest bardziej interaktywny (oczekuje na jakąś akcję ze strony użytkownika). Do implementacji tej heurystyki system przechowuje w strukturze task_struct pole sleep_avg, mówiące o tym, ile czasu proces przespał. Na podstawie tej wartości oraz MAX_SLEEP_AVG wyliczany jest bonus (od -5 do 5).

Wyrównywanie obciążenia procesorów

Nowy scheduler sprawdza, czy procesory są obciążone równomiernie. Jeśli istnieje procesor obciążony o 25% bardziej niż aktualny procesor, to część zadań z niego jest przenoszonych na aktualne CPU (funkcja load_balance). W pierwszej kolejności przenoszone są procesy z tablicy Expired (nie mają one już najprawdopodobniej danych w cache'u procesora).

Struktura task_struct


Wywołania systemowe

W dotychczasowych wersjach jądra systemu Linux do wywołania funkcji systemowej używano zawsze instrukcji int 0x80 w celu wygenerowania przerwania programowego. Dla niektórych architektur (np Intel P4) było to mało wydajne. Obecnie wprowadzono obsługę mechanizmu sysenter do przejścia z przestrzeni użytkownika do przestrzeni jądra. Na przykład dla procesora Pentium 4 uzyskano dwukrotny wzrost szybkości omawianej operacji. Nowy mechanizm wymaga jednak zarówno wsparcia sprzętowego (Pentium II i wyższe, oraz Athlon AMD) jak również programowego (aktualna biblioteka glibc). Używanie sysenter z więcej niż 5 parametrami jest jednak uciążliwe.


Zarządzanie informacjami o urządzeniach

W dotychczasowych jądrach Linuksa nie istniał wystarczająco ogólny model urządzenia. W jądrze 2.6 wprowadzono nowy, w pełni zunifikowany mechanizm. Jest on oparty na strukturze kobject.

Struktura kobject

Kobject jest prostą lecz uniwersalną reprezentacją danych związanych z każdym obiektem występującym w systemie. Gdyby Linux implementowany był w języku obiektowym, to większość innych klas dziedziczyłoby z kobjectu.

Struktura kobject zawiera takie atrybuty, których prawdopodobnie mogłaby potrzebować większość obiektów:

Prawie wszystkie obiekty modelu urządzeń posiadają głęboko zaszyty kobject.

Struktura kobject zdefiniowana jest w pliku linux/kobject.h:

struct kobject {
        char                    * k_name;
        char                    name[KOBJ_NAME_LEN];
        atomic_t                refcount;
        struct list_head        entry;
        struct kobject          * parent;
        struct kset             * kset;
        struct kobj_type        * ktype;
        struct dentry           * dentry;
};

Inicjowanie struktury kobject odbywa się za pomocą funkcji kobject_init():

void kobject_init(struct kobject *kobj);

Użytkownik musi przynajmniej ustawić nazwę. Można do tego wykorzystać funkcję:

int kobject_set_name(struct kobject *kobj, const char *format, ...);

Kset

Struktura Kset grupuje kobjecty identycznego typu. W systemie sysfs każdy kset należy do określonego podsystemu (ale do jednego podsystemu może należeć wiele kset-ów). Struktura ta odpowiada między innymi za to, jak system odpowiada na zdarzenia generowane przez hotplug.

Hotplug

Hotplug jest technologią pozwalającą użytkownikowi na dodawanie i usuwanie urządzeń podczas pracy komputera.

Sysfs

Sysfs jest wirtualnym systemem plików, który umożliwia reprezentację urządzeń z punktu widzenia użytkownika. Należy podkreślić, iż model urządzenia i sysfs są logicznie różnymi obiektami. Z reguły wirtualny system sysfs montowany jest w katalogu /sys.

Przykład urządzenia (IDE):

/sys/devices/pci0/00:11.1/ide0/0.0

Programiści sterowników urządzeń nie muszą ręcznie tworzyć wpisów w sysfs; są one generowane automatycznie przez model urządzenia.

Kobjecty a sysfs

Do dodania wpisu do sysfs służy funkcja kobject_add():

int kobject_add(struct kobject *kobj);

Do usuwania wpisu wykorzystuje się funkcję kobject_del():

void kobject_del(struct kobject *kobj);

Moduły

W jądrze 2.6 zmieniła się również obsługa modułów. Poniżej wspominamy o kilku najbardziej widocznych z punktu widzenia programisty różnicach:


RMAP - Odwrotne odwzorowanie stron

RMAP - Odwrotne odwzorowanie stron

Głównym autorem rmapa jest Rik van Riel.

Przed wprowadzeniem do podsystemu pamięci wirtualnej mechanizmu rmap zwolnienie ramki było operacją kłopotliwą. Ponieważ ramka mogła być współdzielone przez wiele procesów (np. ramki z kodem bibliotek), kiedy kernel chciał zwolnić ramkę, musiał przeszukać tablice stron wszystkich procesów.

Kernel wiedział które ramki należą do danego procesu, ale nie wiedział (bezpośrednio) które procesu używaja danej ramki.

Rmap (reverse mapping VM) rozwiązuje problem wiążąc z każdą ramką listę adresów elementów tablic stron (Page Table Entry Chain - pte_chain). W przypadku gdy odwołanie do ramki jest tylko z jednej tablicy stron, zamiast listy adresów elementów tablic stron trzymany jest bezpośredni adres do elementu tablicy.

Wadą tego rozwiązania jest pamięciożerność. Przykładowo, jeśli komputer ma 128MB RAM, a ramka ma rozmiar 4096B, to mamy 32768 ramek. Jeśli z każdą ramką zwiążemy jeden łańcuch (12 bajtów), to zużyjemy 400kB pamięci. Czyli sporo.

Należy jeszcze wspomnieć, że w przypadku gdy nasz system ma wystarczająco dużo pamięci RAM i nie swapuje, mechanizm rmap jest raczej nieużywany.

Jednak w przypadku typowych komputerów zalety rmapa przeważają nad wadami.

Wsparcie dla architektur bez MMU

Do głównej gałęzi jądra 2.6 dodano wsparcie dla komputerów z procesorami bez jednostki zarządzającej pamięcią (MMU). Kod ten jest rozwijany głównie przez Projekt uCLinux, umożliwia uruchamianie linuxa na prostych procesorach i mikrokontrolerach używanych np. w palmtopach/handheldach, przenośnych terminalach systemów telefonii komórkowej (komórkach) czy w cyfrowych odbiornikach telewizji satelitarnej.

Procesory bez MMU są mniejsze i tańsze, jednak, co oczywiste, nie izolują programów i kernela (zamiast segmentation fault - katastrofa).


Systemy plików

Rozszerzone atrybuty i ACLe

Linux Kernel 2.6 dla każdego pliku oprócz standardowych atrybutów (czasy modyfikacji, właściciel, grupa, uprawnienia w stylu rwxr-xr-x/755) może przechowywać dodatkowe atrybuty.

Przykładowym wykorzystaniem tej możliwości jest implementacja systemu ACL (Access Control List) czyli rozszerzonych atrybutów dostępu (możemy nie tylko określić jakie uprawnienia do pliku powinien mieć właściciel, grupa i reszta świata, ale też konkretni użytkownicy albo grupy).

ACLe są używane w systemach NT Microsoftu, jednak z różnych względów (nie tylko technicznych) są dużo mniej popularne w systemach unixowych.

ReiserFS - nowy system plików

Ojcem ReiserFSa jest Hans Reiser

ReiserFS (czasem zwany Reiser3) to jeden z pierwszych systemów plików z kronikowaniem dla Linuxa.

Kronikowanie (journaling) zapewnia atomowość operacji na systemie plików (nigdy więcej fsck, które dla duzych systemów plików ext2 mogą trwać godzinami).

Za zwiększenie bezpieczeństwa danych płacimy szybkością (trzeba uaktualniać kronikę) i przestrzenią dyskową (kronika zajmuje miejsce).

W ReiserFS tylko operacje na metadanych systemu plików są atomowe. Pełnej atomowości nie można było uzyskać - api linuxa nie obsługiwało pojęcia transakcji.

ReiserFS do przechowywania obiektów używa b-drzew, znany jest ze swojej szybkości i efektywnego przechowywania i dostępu do dużej liczby małych plików, nawet tylko w jednym katalogu.

Namesys prowadzi prace nad nowym systemem plików - Reiser4. Naprawde warto wejść na strone i przeczytać o R4, poniżej ładne obrazki dla zachęty.

R4 jest podobno najszybszym systemem plików, zamiast b-drzew wykorzystuje tańczące drzewa (jeszcze szybsze). Obsługuje pluginy (można łatwo zaimplementować szyfrowanie/kompresje w locie).


LVM2 i Device-mapper

Device-mapper to nowa część jądra Linuksa wspierająca tzw logical volume menagement, czyli zarządzanie woluminami logicznymi.

Został stworzony na potrzeby LVM2 i EVMS. W jądrze 2.4 istniał LVM1, nie używający Device-mappera. Nowa wersja jest znacznie lepiej zaprojektowana. Ponieważ LVM2 jest wstecznie kompatybilny z formatem LVM1, więc zdecydowano się na całkowite usunięcie starej wersji.

LVM2 pozwala na wysoko poziomowe zarządzanie przestrzenią dyskową w systemie. W przeciwieństwie do zwykłego podejścia "dysków i partycji", pozwla na szybkie i łatwe rozszerzanie i przenoszenie woluminów. Możliwe jest także zdefiniowanie grup woluminów pozwalających administratorowi systemu na łatwe i intuicyjne obracanie się w świecie nazw, które mu coś mówią. Przeciwieńswtem tego są zwykle używane nazwy fizycznych dysków takich jak "hda" czy "sdb".

EVMS także służy do zarządzania woluminami.

W wypadku gdy chcemy używać Device-mappera, przydadzą nam się:

Strona Device-mappera: sources.redhat.com/dm/


Wydajność

Szybkość działania niektórych operacji w 2.6 na tle innych kerneli.

Konfiguracja

stan na: październik 2003

komputer: 900 MHz P3, 256 MB RAM

kernele:

Testy

Czas działania socket(), czyli: allokacja pamięci (co jest szybkie) + znalezienie nieużywanego deskryptora (co troche trwa).

Bind na nieużywany port, mało istotne dla serwerów WWW, bardziej dla serwerów proxy. (freebsd i 2.4 schowane pod wykresem 2.6)

Mapowanie kolejnych stron z 200MB pliku, odczyt pierwszego bajtu z każdej strony.

Więcej wykresów i porównań: źródło


Czego zabrakło w 2.6?

Symbol sys_call_table

Funkcjonalność opisaną w zadaniu 4 można osiągnąć między innymi tak:

To drugie zostało utrudnione w 2.6 bo symbol sys_call_table nie jest już eksportowany.

Jak znaleźć sys_call_table? Jest kilka sposobów.

Dość skuteczną metodą (działa w 2.4/2.6 + uml) jest:

void *scan_sys_open_sys_close(void)
{
  unsigned long *ptr = (unsigned long *)&init_mm;
  unsigned long offset;
  unsigned long datalen = 16384;

  for (offset=0; offset >datalen; ptr++,offset++) 
      if (ptr[0] == (unsigned long)&sys_close &&
    	ptr[__NR_open - __NR_close] == (unsigned long)&sys_open) 
	  return ptr - __NR_close;
  return NULL;
}

Gdy już mamy adres sys_call_table można:

if (sct = scan_sys_open_sys_close()) {
	blokujkernel();
	stary_open = sct[__NR_open];
	sct[__NR_open] = nasz_open;
	odblokujkernel();
}

kHTTPd

W 2.6 nie jest już wbudowany serwer WWW. Funkcjonalność która umożliwiała szybsze działanie została udostępniona zwykłym programom, zaś kHTTPd został usunięty.

Na starszych kernelach kHTTPd był szybszy niz apache.

Oś X - liczba współbieżnych zapytań do serwera WWW

Oś Y - liczba przetworzonych zapytań na sekundę [wydajność].


Błędy

Niestety 2.6 nie różni się na tyle od swoich poprzedników by nie mieć błędów.

Oto list sprzed roku o ciekawej dziurze wykorzystującej sytuację wyścigu

 
Date: Wed, 19 Mar 2003 20:22:45 +0100 (CET)
From: Andrzej Szombierski <qq@kuku.eu.org>

To: bugtraq@securityfocus.com


Hello

There are many discussions (on slashdot for example) on the recent linux
ptrace (& kmod) bug. I'll try to clarify what is this all about.

It's a local root vulnerability. It's exploitable only if:
1. the kernel is built with modules and kernel module loader enabled
 and
2. /proc/sys/kernel/modprobe contains the path to some valid executable
 and
3. ptrace() calls are not blocked

These conditions are met on most standard linux distros.

Ok now how it works:
When a process requests a feature which is in a module, the kernel spawns
a child process, sets its euid and egid to 0 and calls execve("/sbin/modprobe")
The problem is that before the euid change the child process can be
attached to with ptrace(). Game over, the user can insert any code into a
process which will be run with the superuser privileges.

Solutions/workarounds:
- patch the kernel
 or
- disable kmod/modules
 or
- install a ptrace-blocking module
 or
- set /proc/sys/kernel/modprobe to /any/bogus/file

A word about 2.5. kernels - these are not vulnerable because the kernel
thread spawning code has been rewritten so that the modprobe process is
spawned from keventd, it never runs with non-root uid, so it can't be
ptraced by any non-root user.

Sample exploit here (ix86-only):
http://august.v-lo.krakow.pl/~anszom/km3.c

-- : Andrzej Szombierski : anszom@v-lo.krakow.pl : qq@kuku.eu.org :
: anszom@bezkitu.com ::: radio bez kitu <=> http://bezkitu.com :

A oto informacja o nowym poważnym błędzie sprzed tygodnia (znowu wyścig)

Synopsis:  Linux kernel uselib() privilege elevation
Product:   Linux kernel
Version:   2.2 all versions, 2.4 up to and including 2.4.29-pre3, 2.6 up 
           to and including 2.6.10
Vendor:    http://www.kernel.org/
URL:       http://isec.pl/vulnerabilities/isec-0021-uselib.txt

CVE:       CAN-2004-1235
Author:    Paul Starzetz <ihaquer@isec.pl>
Date:      Jan 07, 2005


Issue:
======

Locally  exploitable  flaws  have  been found in the Linux binary format
loaders'  uselib()  functions  that  allow  local  users  to  gain  root
privileges.

Inne


Bibliografia

Przy tworzeniu prezentacji korzystaliśmy z następujących źródeł:


Zakończenie

Dziękujemy za uwagę.