Uruchamianie systemu i niskopoziomowe mechanizmy obsługi procesów.


Spis treści

  1. Uruchamianie systemu
    1. Struktura MBR i tablic partycji dysku systemowego
    2. Obraz jądra i fazy ładowania obrazu
    3. Programy ładujące dla Linuksa
    4. Przebieg budowy struktur jądra i tworzenia pierwszego procesu
  2. Mechanizm przerwań procesora Intel 386
  3. Osługa przerwań i wywołań systemowych w Linuksie
  4. Niskopoziomowa obsługa błędów braku i ochrony strony
    1. Wsparcie sprzętowe
    2. Realizacja w Linuksie
  5. Przełączanie kontekstu
    1. Wsparcie sprzętowe
    2. Realizacja w Linuksie
  6. Mechanizm DMA
  7. Zegary w Linuksie
    1. Liczenie czasu
    2. Przerwania zegarowe i ich obsługa
    3. Planowanie zdarzeń
  8. Jak zaimplementować własną funkcję systemową

1. Uruchamianie systemu


1.1 Struktura Master Boot Recordu i tablicy partycji

Po uruchomieniu komputera i wykonaniu wstępnych testów BIOS ładuje zawartość pierwszego sektora urządzenia do pamięci operacyjnej pod adres 0x7c00

Struktura Master Boot Recordu:

Struktura tablicy partycji:


struct partition {
	unsigned char boot_ind;		/* 0x80 - aktywna */
	unsigned char head;		/* ścieżka początkowa */
	unsigned char sector;		/* sektor początkowy */
	unsigned char cyl;		/* cylinder początkowy */
	unsigned char sys_ind;		/* Typ partycji */
	unsigned char end_head;		/* ścieżka końcowa */
	unsigned char end_sector;	/* sektor końcowy */
	unsigned char end_cyl;		/* cylinder końcowy */
	unsigned int start_sect;	/* sektor początkowy
					   partycji */
	unsigned int nr_sects;		/* liczba sektorów wchodzących
					   w skład partycji */
}

Przykładowa organizacja dysku twardego:

Podstawowe zasady jakim podlegają partycje na dysku twardym:

 

początek następny spis treści

1.2 Obraz jądra


Tworzenie jądra

Katalogi ze źródłami:


Kompilacja:
# make config lub # make menuconfig lub # make xconfig - konfiguracja
# make depend - zależności dla kompilatora
# make clean - czyszczenie po poprzedniej kompilacji
# make zImage lub # make bzImage - kompilacja jądra
# make modules - kompilacja modułów
# make modules install - instalacja modułów

Instalowanie jądra (czasami robione automatycznie przez make) polega na przegraniu go z katalogu /arch/i386/boot do podkatalogu /boot w głównym katalogu, utworzeniu odpowiedniego wpisu w pliku /etc/lilo.conf i uruchomieniu programu lilo.

 

początek następny spis treści

Duże jądro (BIG Kernel):

Specyfika ładowania jądra przez program ładujący powoduje, że teoretycznie nie powinno ono zajmować więcej niż 448 KB, gdyż tyle miejsca jest dla niego przeznaczone (od adresu 0x90000 będzie zaczynał się setup. Tymczasem, mimo kompresji, większość współczesnych jąder po kompilacji zajmuje więcej. W związku z tym możliwe jest skompilowanie jądra jako dużego (ang. BIG). Tak utworzone jądro różni się od zwykłego tylko nazwami plików pośrednich i ostatecznego obrazu (mają na początku dodaną literę 'b'). Informacja o tym, że jądro jest duże jest też przekazywana do kodu jądra (przez dodanie do opcji kompilatora gcc deklaracji __BIG_KERNEL__). W ten sposób kod jądra może rozpoznać, że ma się załadować ,,wyżej'', powyżej granicy 1 MB.

 

początek następny spis treści

Obraz jądra:

bootsect jest obrazem sektora ładującego system z dyskietki. Zadaniem umieszczonego tam kodu jest po prostu załadowanie do pamięci kolejnych ścieżek z dyskietki, zawierających dalszą część jądra. Taka struktura pliku jądra pozwala na bardzo proste tworzenie dyskietki ładującej, przez skopiowanie obrazu jądra do fizycznego urządzenia, np:
# dd if=/boot/vmlinuz of=/dev/fd0

setup powstaje z kompilacji dwóch plików: arch/i386/boot/setup.S oraz arch/i386/boot/video.S. Zawiera inicjację sprzętu (w tym karty graficznej), a w szczególności przełącza procesor w tryb chroniony.

head.o ma za zadanie przygotować środowisko i wywołać pierwszą części kodu jądra napisaną w C - procedurę dekompresującą jądro umieszczoną w misc.o.

piggy.o - skompresowana część jądra.

 

początek następny spis treści

Skompresowana część jądra:

Składowe pliku piggy.o są oddzielnie kompilowane, złączane za pomocą programu objcopy, kompresowane programem gzip i łączone linkerem z plikami head.o i misc.o - powstaje vmlinux.out (lub bvmlinux.out w przypadku dużego jądra).

piggy.o zawiera jeszcze część kodu inicjującego. Jest to min. head.o (powstały z arch/i386/kernel/head.S, odpowiedzialny za ostateczne przygotowanie środowiska dla kodu w C, oraz kod utworzony po kompilacji podkatalogu init, zawierający funkcję wejściową jądra start_kernel() i tworzenie pierwszego procesu. Dalej zaś znajduje się już reszta kodu jądra.

Ostatnim etapem tworzenia obrazu jądra jest uruchomienie programu build (powstałego z kompilacji arch/i386/boot/tools/build.c. Program ten ustawia jeden za drugim pliki bootsect, setup i vmlinux.out (bvmlinux.out) i tworzy obraz jądra: arch/i386/boot/zImage (bzImage).

 

początek następny spis treści

Ładowanie obrazu jądra:

Ładowanie jądra z dyskietki:

Ładowanie jądra z dysku twardego:

Konieczność uaktualnienia pliku mapy jest jednym z powodów, dla których po skompilowaniu jądra trzeba uruchomić lilo.

 

początek następny spis treści

Inicjalizacja sprzętu (setup):

setup jest odpowiedzialny za uzyskanie od BIOS-u podstawowych informacji o sprzęcie, które zapisywane są pod adresy 0x90000-0x901FF - tam, gdzie poprzednio znajdował się bootsect.

Przełączenie w tryb chroniony:

 

początek następny spis treści

Dekompresja jądra (head.o):


Kod dekompresujący korzysta z kilku funkcji w C, np. tych wykonujących rzeczywisty algorytm dekompresji PKZIP metody 8, zamieszczonych w lib/inflate.c. W arch/i386/boot/compressed/misc.c zaimplementowane są np. własne funkcje malloc(), free() i puts(), wykorzystywana do wypisania komunikatu "Uncompressing Linux...", a po skończonej dekompresji "Ok, booting the kernel."

 

początek następny spis treści

Dekompresja zwykłego jądra:

 

początek następny spis treści

Dekompresja dużego jądra:

 

początek następny spis treści

Przygotowanie środowiska dla kodu w C (head.o):

Stronicowanie pamięci:

Inne:


 

początek następny spis treści

1.3 Programy ładujące dla Linuksa


Programy ładujące dla Linuksa - LILO

Plan wystąpienia

Pliki wchodzace w skład pakietu LILO

Lilo składa się z dwóch części: programu instalującego plik mapy, oraz plików odczytywanych podczas startu systemu.


Pliki, po modyfikacji których należy uruchomić /sbin/lilo
Pliki, których nie należy modyfikować, ale można relokować
Plik, /etc/lilo.conf

/etc/lilo.conf jest plikiem konfiguracyjnym LILO i można go dostosowywać do własnych potrzeb

Opcje globalne:

    boot=boot_device    - urządzenie gdzie zapisany zostanie sektor
                          ładujący, domyślnie root
    default=name        - obraz ładowany domyślnie, jeśli nie ma to
                          pierwszy w pliku
    install=boot_sector	- instaluje wybrany plik w bootsektorze,
                          domyślnie /boot/boot.b
    map=map_file
    message=message_file
    time_out=czas       - czas po jakim ładowany jest domyślny obraz
    verbose=level       - poziom szczegółowości raportowania: 0-5

Opcje dla konkretnych obrazów ładowalnych:

    label=name
    password=password

Opcje tylko dla obrazów jąder Linuksa:

        image=file_name
        read-only             - montuje system plików root tylko do odczytu
        read-write
        root=root_device      - określa urządzenie, które ma być
                                montowane jako root

Opcje tylko dla innych systemów operacyjnych:

        other=device            - urządzenie, z którego pobrany zostanie
                                  bootsector
        loader=chain_loader     - domyślny /boot/chain.b
        table=device            - określa urządzenie, z którego pobrana
                                  zostanie tablica partycji

Schemat ładowania systemu

 

początek następny spis treści

Programy ładujące dla Linuksa - Loadlin

Obrazek ze schematem działania Loadlina

 

początek następny spis treści

1.4 Inicjalizacja struktur jądra i budowa pierwszego procesu.


Pierwszą uczciwą wywoływaną w jądrze funkcją jest start_kernel. Macza ona palce we wszystkim, dlatego niemożliwe jest streszczenie jej w kilku słowach. Pozostaje nudne wyliczanie czynności, które są kolejno wykonywane.

Zajmiemy się następującymi funkcjami z pliku init/main.c

Funkcja start_kernel()

Uwaga o pamięci:

Procedury inicjalizacyjne, przyjmują parametry memory_start,memory_end rezerwują na własne potrzeby pamięć z początku i zwracają zmodyfikowany wskaźnik memory_start.
printk(linux_banner);
Wypisanie linuksowego tekstu powitalnego.
setup_arch(&command_line, &memory_start,&memory_end);
  • pobieranie informacji o sprzęcie (procesor, płyta główna, BIOS) -- przeważnie odczytywane są informacje ustalone wcześniej na etapie bootowania (setup).
  • ustawiane są pola struktury init_task.mm.
  • odczytywanie rozmiaru pamięci Wykorzystywane są do tego informacje BIOS-u, lecz można je przeciążyć za pomocą linii poleceń.
  • Ustalanie rozmiaru initrd (czyli RAM-dysku, na którym montuje się korzeń systemu plików). Sprawdza się, czy obszar przeznaczony na RAM-dysk nie wystaje poza pamięć.
  • Rezerwowanie portów dla standardowych urządzeń (dma, timer, fpu). Wywoływana jest funkcja request_region.
  • ustawianie funkcji odpowiedzialnej za przełączanie konsol wirtualnyc (zmienna conswitchp).
paging_init(memory_start,memory_end);
Przygotowuje grunt pod stronicowanie. Katalog stron i tablice stron są ustawiane tak, że odwzorowują kolejne ramki pamięci fizycznej. Uwzględnia się to, że pierwsze 4MB zostały już zamapowane przez head.o. Strona odpowiadające adresowi wirtualnemu 0 jest odmapowywana, aby wyłapywać odwołania do NULL.
trap_init();
Ustawienie bramek dla przerwań, w tym bramki dla wywołań systemowych.
init_IRQ();
Inicjalizacja przerwań. Przypisywane są domyślne hadlery przerwaniom sprzętowym. Następnie ustawiane są bramki dla przerwań zewnętrznych (za wyjątkiem obsługującego wywołania systemowe).
sched_init();
Inicjalizuje tablicę procesów oraz tablicę haszującą dla PID-ów procesów. Instaluje też procedury obsługi przerwania zegarowego (ich dolne części) - funkcja init_bh.
time_init();
Pobiera z CMOS-u czas rzeczywisty i inicjalizuje nim strukturę xtime.
parse_options(command_line);
Przetwarza opcje podane w linii poleceń jądra.
console_init(memory_start,memory_end);
Inicjalizuje osługę konsoli. Ustawia domyślną dyscyplinę linii (tty_register_ldisc) oraz trukturę opisującą typ terminala. Następnie inicjalizuje konkretne terminale: wirtualne lub szeregowe.
init_modules();
Inicjalizuje obsługę modułów. Pola struktury kernel_module są przeważnie stałe, inicjalizowane jest jedynie pole nsyms.
kmem_cache_init(memory_start, memory_end);
Inicjalizuje pamięć podręczną dla jądra.
sti();
Włączenie przerwań.
calibrate_delay();
Wyznacza BOGO-Mipsy. Ten test polega na zmierzeniu szybkości procesora w kręceniu sie w pustej pętli. Wyznaczona wartość przydaje się niektórym sterownikom urządzeń.
Sprawdzenie czy nie został zamazany obraz RAM-dysku.
Mogło tak się stać, gdyż procedury wywoływane powyżej mogły zabrać ten obszar na swoje potrzeby.
mem_init(memory_start,memory_end);
Inicjalizuje tablicę mem_map (wywołując clear_bit). Zaznacza strony zajęte przez jądro (PG_reserved), strony możliwe do wykorzystania przez mechanizm DMA (PG_DMA). Następnie wypisuje informację statystyczną (ilość dostęj pamięci, w tym ilość pamięci zajętej prze jądro, dane, inicjalizację). Potem sprawdza się, czy procesor poprawnie interpretuje zabezpieczanie stron przed zapisem (nie robią tego modele 386 i niektóre dziwne 486).
kmem_cache_sizes_init();
Dalszy ciąg inicjalizacji pamięci podręcznej jądra.
proc_root_init();
Rejestrowanie funkcji obsługi dla systemu plików proc;
Rezerwacja pamięci podręcznej (przy pomocy kmem_cache_create) oraz inicjalizacja ewentualnych tablic haszujących dla:
  • systemu zarządzania procesami.
    uidcache_init();
    filescache_init();
  • systemu plików
    dcache_init();
  • systemu zarządzania pamięcią
    vma_init();
  • buforów systemowych
    buffer_init(memory_end-memory_start);
  • kolejki sygnałów
    signals_init();
  • systemu plików
    inode_init();
    file_table_init();
Uff! Łapiemy oddech i brniemy dalej...

 

początek następny spis treści


Funkcja init()

Jest to pierwsza funkcja, która staje się procesem z prawdziwego zdarzenia. Najważniejszą sprawą jest wywołanie procedury do_basic_setup(), która wykonuje następujące czynności: Po powrocie do funkcji init():

Na tym niezwykle pasjonujący ;-) proces uruchamiania systemu się kończy i rozpoczyna się nudna, codzienna praca.

 

początek następny spis treści

2. Mechanizm przerwań procesora Intel 386
3. Osługa przerwań i wywołań systemowych w Linuksie



Wprowadzenie

Obsługa przerwań w sposób naturalny mocno uzależniona jest od architektury komputera . Nas interesuje platforma sprzętowa oparta na jednym procesorze Intel 80386. Podstawowym źródłem informacji był dla nas kod jądra w wersji 2.2.12.

Rodzaje przerwań:


Mechanizm wołania funkcji systemowych

W tablicy funkcji systemowych sys_call_table znajdują się adresy podprogramów z przestrzeni jądra uruchamianego przez funkcje system_call() . Nazwa podprogramu to sys_ albo old_  plus  implementowana przez nią funkcja systemowa.


Wywołanie funkcji systemowej wygląda następująco:

Dostęp do danych przechowywanych w przestrzeni adresowej użytkownika (selektor jego segmentu danych jest w rejestrze FS) zapewniają m.in. makra get_user() i put_user() zdefiniowane w pliku arch/i386/kernel/segment.h. Ich wykonanie może spowodować konieczność ściągnięcia do pamięci strony, do której się odwołują.

Algorytm system_call()

Argument: numer wołanej funkcji systemowej
Wynik: ujemny numer błędu albo nieujemny kod powrotu
  {
    if(numer_funkcji > NR_syscalls)
      return(-ENOSYS);
    if(sys_call_table[numer_funkcji]==NULL)
      return(-ENOSYS);
    return((sys_call_table[numer_funkcji])());
  }

Algorytm ret_from_sys_call

  {
    if(intr_count!=0)                   // obsługujemy jakieś przerwanie
      return;                           // wróć do przerwanego procesu
    while(czekają funkcje "bottom half")
    {
      ++intr_count;
      do_bottom_half();
      --intr_count;
    }
    if(przerwanym procesem było jądro)
      return;
    if(ustawiona jest flaga need_resched)
    {
      schedule();
      goto ret_from_sys_call;
    }
    if(bieżący proces to task[0])       // do task[0] nie są wysyłane
      return;                           //   żadne sygnały
    if(czekają jakieś sygnały)
      do_signal();
  }
Funkcja do_bottom_half() opisana jest niżej

Obsługa wyjatków

Podprogramy obsługi wyjątków składają się z dwóch części: definiowany w pliku entry.s fragmentu kodu przechodzi do trybu jądra, woła funkcję o nazwie do_nazwa_wyjatku() zdefiniowaną zazwyczaj w pliku traps.c i wraca do przerwanego procesu, wykonując ret_from_sys_call.
Wiekszość podprogramow obsługi generowana jest przez makro DO_ERROR(), które woła funkcję force_sig() z odpowiednim numerem sygnału, po czym sprawdza, czy wyjątku nie spowodował kod jądra (jeśli tak, zatrzymuje system procedura die_if_kernel()).
 
Wyjątek Sygnał Nazwa podprogramu obsługi
0 SIGFPE  do_divide_error
3 SIGTRAP do_int3
4 SIGSEGV do_overflow
5 SIGSEGV do_bounds
7 SIGSEGV do_device_not_available
8 SIGSEGV do_double_fault
9 SIGFPE  do_coprocessor_segment_overrun
10 SIGSEGV do_invalid_TSS
11 SIGBUS  do_segment_not_present
12 SIGBUS  do_stack_segment
17 SIGSEGV do_alignment_check

Uwaga: wyjątek 9 obsługiwany jest w kontekście procesu, który ostatnio używał koprocesora (wszystkie pozostałe - w kontekście procesu bieżacęgo).

Poza tym:


Obsługa przerwań sprzetowych

Podstawowe informacje dotyczące przerwań sprzętowych:


Każde przerwanie opisane jest strukturą irq_desc_t "IRQ descriptor", która zawiera następujące pola:

typedef struct { unsigned int status; /* status linii IRQ (włączony, wyłączone itp.) */ struct hw_interrupt_type *handler; /* funkcje obsługi (zależne od typu sterownika)*/ struct irqaction *action; /* dane o zarejestrowanych funkcjach obsługi */ unsigned int depth; unsigned int unused[4]; } irq_desc_t;


Struktura:

struct hw_interrupt_type { const char * typename; void (*startup)(unsigned int irq); void (*shutdown)(unsigned int irq); void (*handle)(unsigned int irq, struct pt_regs * regs); /* obsługuje przerwanie */ void (*enable)(unsigned int irq); /* włącza przerwanie nr */ void (*disable)(unsigned int irq); /* wyłącza */ }; zawiera wskaźniki do funkcji obsługi przerwania dla konkretnego typu sterownika.


W pliku arch/i386/kernel/irq.c makro BUILD_IRQ() tworzy podprogram obsługujący każde przerwanie, który:

Uwaga: wyjątkiem jest przerwanie zegarowe, które obsługiwane jest w sposób specjalny.


Funkcja do_IRQ()


W strukturze hw_interrupt_type opisującej sterownik standartu 8259 PIC (jedyny obsługiwany przez linuksa) w polu handle znajduje się wskaźnik do funkcji do_8259A_IRQ()

Funkcja do_8259A_IRQ()


Rejestrowanie funkcji wołanych przez irq_desc[irq].handler->handle()

Aby irq_desc[irq].handler->handle() miały co robić, trzeba najpierw zarejestrować wołane przez nie funkcje. Wymienia teraz związane z tym zadaniem podprogramy:

Struktura struct irqaction, w której przechowywane są dane rejestrowanych funkcji, zdefiniowana jest w pliku include/linux/interrupt.h:

struct irqaction { void (*handler)(int, // numer obsługiwanego IRQ void *, // tu będzie irqaction.dev_id struct pt_regs *); // NULL lub rejestry CPU unsigned long flags; // flagi SA_* unsigned long mask; const char *name; // nazwa funkcji obsługi void *dev_id; // argument dla handler() struct irqaction *next; // następna zarejestrowana funkcja } W polu flags ustawiane są bity SA_* zdefiniowane w pliku include/asm-i386/signal.h.


Koncepcja "bottom halves"

Aby zminimalizować czas spędzony w do_IRQ(), projektanci sterowników urządzeń wymagających czasochłonnej obsługi powinni podzielić ją na dwie części. Szybka funkcja rejestrowana przez request_irq() jest wołana w chwili nadejścia przerwania. Wolniejsza funkcja realizująca drugą część obsługi wstawiana jest do tablicy bh_base; jej szybka siostra w razie potrzeby zaznacza (funkcja bh_mark()>), że należy dokończyć (przy włączonych przerwaniach) obsługę urządzenia.


Pliki zródłowe

 

początek następny spis treści

4. Niskopoziomowa obsługa błędów braku i ochrony strony


4.1 Wsparcie sprzętowe


Obsługa błędów braku i ochrony strony wymaga wsparcia sprzętowego. Wsparcie takie zapewniają procesory rodziny 386. Błąd niepowodzenia stronicowania może zostać wygenerowany w trakcie translacji adresu liniowego na fizyczny. Procesor generuje ten błąd jeśli podczas translacji nastąpi odwołanie do elementu katalogu lub tablicy stron zawierającego wyzerowane pewne bity.


Błędy przy translacji

 

Znaczenie bitów w elementach katalogu i tablicy stron.



Bity W i U są ignorowane przez procesor na poziomach uprzywilejowania CPL < 3. CPL (Current Privilege Level) oznacza aktualny poziom uprzywilejowania procesora. W trybie jądra CPL = 0, w trybie użytkownika CPL = 3.

Adres liniowy, przy dostępie do którego powstał błąd jest zapisywany do rejestru CR2. Oprócz tego na stos odkładany jest kod błędu znaczenie mają tylko trzy najmłodze bity :

 

początek następny spis treści

4.2 Realizacja w Linuksie


Zarys algorytmu obsługi "niepowodzenia" stronicowania


page_fault

Kod page_fault znajduje się w /arch/i386/kernel/entry.S.
Procedura ta jest podkładana po wyjątek 14. Jest ona napisana w asemblerze i jest bardzo prosta. Kiedy wystąpi błąd braku lub ochrony strony, page_fault wywołuje:
asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code) .

do_page_fault

Kod do_page_fault jest już napisany w C. Znajduje się on w /arch/i386/mm/fault.c.
Procedura ta sprawdza, czy mamy do czynienia z błędem braku lub ochrony strony z którym możemy sobie poradzić.

Algorytm do_page_fault



do_page_fault

Jeśli procedura stwierdzi, że adres dla którego został wygenerowany błąd strony należy do vma_area aktualnego procesu, wywołuje int handle_mm_fault(struct task_struct *tsk, struct vm_area_struct * vma, unsigned long address, int write_access). Na powyższym rysunku ścieżka sterowania odpowiadająca temu przypadkowi jest oznaczona grubą linią. W przeciwnym wypadku do procesu zwykle jest wysyłany odpowiedni sygnał.

 

początek następny spis treści

5. Przełączanie kontekstu


5.1 Wsparcie sprzętowe


Najważniejszą strukturą danych podczas przełączania kontekstu jest segment TSS (Task State Segment). Przechowuje on kontekst obecnie działającego procesu.
Segment TSS ma postać:

TSS
Offset Górne słowo Dolne słowo
00h zarezerwowane link
04h ESP0
08h zarezerwowane SS0
0Ch ESP1
10h zarezerwowane SS1
14h ESP2
18h zarezerwowane SS2
1Ch CR3
20h EIP
24h EFLAGS
28h EAX
2Ch ECX
30h EDX
34h EBX
38h ESP
3Ch EBP
40h ESI
44h EDI
48h zarezerwowane ES
4Ch zarezerwowane CS
50h zarezerwowane SS
54h zarezerwowane DS
58h zarezerwowane FS
5Ch zarezerwowane GS
60h zarezerwowane LDTR
64h offset mapy I/O (IOBP) zarezerwowane
68h Opcjonalne dane systemu
IOPB-20h Opcjonalna mapa przekierowania przerwań
IOPB Opcjonalna mapa dostępnych portów I/O

Specjalny rejestr procesora TR (Task Register) przechowuje selektor do aktywnego segmentu TSS. Na jego podstawie procesor dokonuje uaktualnienia danych w TSS.


Do załadowania nowego selektora do rejestru TR służy instrukcja LTR. Jej parametrem może być rejestr albo komórka pamięci.


Dla symetrii oprócz instrukcji LTR istnieje instrukcja STR. Umożliwia ona odczytanie selektora z rejestru TR.


Oprócz deskryptorów segmentów TSS w tablicy deskryptorów można tworzyć deskryptory bramy do zadań (Task Gate Descriptor). Każdy taki deskryptor zawiera selektor jednego deskryptora TSS.

W programie można wykonać daleki skok (zwykłe far jmp) pod selektor TSS, albo selektor deskryptora bramy, co spowoduje automatyczne zapamiętanie bieżącego kontekstu, odtworzenie kontekstu z TSS selektora do którego skaczemy oraz uruchomienie zadania które jest powiązane z tym TSS. Jest to odrobinę mylące, gdyż instrukcja jmp nie oznacza skoku pod nowy adres, ale wywołanie sprzętowej procedury przełączania zadań. Daje to gotowy mechanizm przełączania kontekstu. Czas takiego przełączania jest około 300 razy dłuższy od czasu zwykłego skoku.


 

początek następny spis treści

5.2 Realizacja w Linuksie


W skład kontekstu procesu wchodzą:

Jądro zachowuje kontekst gdy system otrzymuje przerwanie lub proces wywołuje funkcję systemową. Przełączanie kontekstu polega na zmianie aktualnie wykonywanego procesu. Przyczyną zmiany kontekstu może być zaśnięcie lub zakończenie się procesu, bądź wykorzystanie przydzielonego procesowi kwantu czasu.

W Linuksie wywołanie systemowe jest równoważne wygenerowniu odpowiedniego przerwania, natomiast przełączenie kontekstu może nastąpić tylko tuż przed powrotem z procedury obsługi przerwania. Jednak w trakcie wykonywania procedury obsługi przerwania może wystąpić przerwanie o wyższym priorytecie i kontekst procesu, który wykonuje się w trybie jądra musi być zachowany. Dlatego każdy proces posiada własną przestrzeń na stosie jądra, w której odkładane są poszczególne warstwy kontekstu.

Procesory i386 posiadają wsparcie dla przełączania kontekstu. Z każdym zadaniem związany jest specjalny segment (task state segment), który zawiera informacje o stanie danego procesu (rejestry, mapa portów). Wznowienie wykonywania procesu odbywa się przez skok do jego segmentu TSS. Jednak Linux nie wykorzystuje w pełni wsparcia sprzętowego. Wprawdzie korzysta z segmentów stanu zadania, lecz jądro zapamiętuje rejestry procesora i wykonuje skok do kodu nowego procesu. Umożliwia to lepszą kontrolę nad segmentami procesu i zapewnia większe bezpieczeństwo systemu.


Struktury jądra związane z przełączaniem kontekstu

1. Pola struktury task_struct [include/linux/sched.h].
2. Struktura thread_struct [include/asm-i386/processor.h].
3. Globalna tablica deskryptorów segmentów [include/asm-i386/desc.h].

Etapy przełączania kontekstu

1. Wygenerowane zostało przerwanie lub proces wywołał funkcję systemową.
2. Jeśli została wywołana funkcja schedule()i proces nie jest uprawniony do dalszego wykonywania się, to należy przełączyć kontekst.
3. Powrót z funkcji systemowej lub funkcji obsługi przerwania.
4. Uwagi.

 

początek następny spis treści

6. Mechanizm DMA



Wstęp

DMA służy do kopiowania danych z pamięci operacyjnej do urządzenia lub w stronę przeciwną bez anagażowania czasu procesora. Obsługa DMA w jądrze Linuxa jest bardzo silnie zależna od wykorzystywanej architektury (zwłaszcza magistrali systemowej).  

Przydzielanie bufora DMA

Bufor DMA musi znajdować się w ciągłym obszarze pamięci. Wynika to z tego, że zarówno magistrala ISA jak i PCI operują na fizycznych (a właściwie magistralowych) adresach pamięci. Do przeliczanie adresów wirtualnych na adresy jakich używa magistrala i z powrotem służą funkcje:

Istnieją też magistrale w których stosowane są inne rozwiązania (np. magistrala Sbus pracuje na adresach wirtualnych). Aby przydzielić bufor DMA musimy się posłużyć funkcją kmalloc lub get_free_pages. Dla magistrali ISA trzeba przy alokacji bufora określić priorytet GFP_KERNEL aby zagwarantować ograniczenia przez nią nakładane.

Obsługa DMA dla magistrali ISA

Oryginalny kontroler DMA dla magistrali ISA mógł obsługiwać tylko cztery kanały. Aktualnie używany sprzęt zawiera układ równoważny dwóm takim kontrolerom. Posiada więc on 8 kanałów. Kanał 4 lub inaczej kanal 0 drugiego kontrolera nie jest jednak dostępny dla urządzeń ISA służy bowiem do połączenia z pierwszym kontrolerem. Kanały 0--3 są kanałami 8 bitowymi natomiast 5--7 są 16 bitowe. Sterownik DMA posiada kilkanaście rejestrów sterujących jego działaniem. Dla nas istotne jest, że każdemu kanałowi DMA przypisane są trzy rejestry: adresowy, liczący oraz strony. Rejestr adresowy jest 16 bitowy, natomiast stron 8 bitowy. Ponieważ pierwszy sterownik dokonuje transmisji 8 bitowej, a drugi 16 bitowej podczas jednej operacji mogą one przesłac odpowiednio 64 i 128 kilobajtów danych. Rozmiar rejestru stron nakłada na magistralę ISA ograniczenie transmisji do dolnych 16 megabajtów pamięci. Ograniczenie to jest sprawdzane podczas przydzielania pamięci na bufor DMA jeśli określimy priorytet GFP_KERNEL.

Struktury danych dla kanałów DMA

struct dma_chan { 
	int lock;
	const char *device_id 
} ; 

struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS]

Pole lock struktury dma_chan określa czy kanał jest wolny czy przydzielony jakiemuś urządzeniu (w tym drugim przypadku nazwa tego urządzenia jest wskazywana przez device_id). Nazwa urządzenia jest wykorzystywana wyłącznie przez system plików proc. Tablica dma_chan_busy zawiera informacje o wszystkich kanałach DMA ( MAX_DMA_CHANNELS jest dla magistrali ISA zawsze rowne 8).

Przydzielanie i zwalnianie kanałów DMA

Zanim skorzystamy z kanału DMA jądro musi nam go przydzielić. Ponieważ przy o końcu transmisji DMA informuje nas zazwyczaj przerwanie je także musimy mieć przydzielone. Ustaliła się konwencja żeby najpierw żądać przerwania, a dopiero potem kanału DMA. Następujące funkcje służa do przydzielania i zwalniania kanałów:

Funkcje sterujące kontrolerem DMA

Obsługa DMA dla magistrali PCI

Implementacja DMA dla magistrali PCI jest dużo prostsza niż dla magistrali ISA. Urządzenie które chce czytać lub pisać do pamięci żada kontroli magistrali, a gdy ją otrzyma samo kontroluje sygnały elektryczne nią sterujące. Aby przesłać dane do lub z pamięci wystarczy więc przydzielić bufor DMA oraz zapisać informacje sterujące transmisją do rejestrów urządzenia. W przypadku magistrali PCI nie obowiązuje ograniczenie 16 dolnych magabajtów. Bufor DMA może się znajdować w dowolnym miejscu pamięci głównej.

 

początek następny spis treści

7. Zegary w Linuksie



7.1 Liczenie czasu

W linuksie czas jest liczony od uruchomienia systemu. Jednostka czasu jest zdefiniowana za pomocą makrodefinicji HZ. Dla systemów opartych na procesorze Alpha jest to 256 lub 1024, dla pozostałych platform 100 czyli 10 milisekund. Oznacza to, że co 10 milisekund generowane jest przez układ zegara czasu rzeczywistego tyknięcie, które następnie liczone jest przez przerwanie zegarowe. Aktualna liczba wygenerowanych tyknięć znajduje sie w globalnej zmiennej jiffies. Jeśli zainteresowani jesteśmy czasem rzeczywistym to znajduje się on w zmiennej xtime, jednak dokładność zapewniana przez tą zmienna wynosi także tylko 10 milisekund. Aby odczytać aktualny czas z większą dokładnością musimy skorzystać z zegara czasu rzeczywistego (robi to np. funkcja void do_gettimeofday(struct timeval *tv)).

 

początek następny spis treści

7.2 Przerwania zegarowe i ich obsługa

Ponieważ przerwania zegarowe wykonywane są dość często to czas ich wykonania powinien być jak najkrótszy. Z tego powodu procedura ich obsługi podzielona jest na połowę podrzędną i nadrzędną. Oto jak wygląda część nadrzędna:

void do_timer(struct pt_regs * regs)
 { 
	(*(unsigned long *)&jiffies)++;
	lost_ticks++; 
	mark_bh(TIMER_BH);
	if (!user_mode(regs)) 
		lost_ticks_system++; 
	if (tq_timer) 
		mark_bh(TQUEUE_BH);
 }

Jak widzimy zwiększa ona wartość zmiennej jiffies oraz zmiennych lost_ticks i lost_ticks_system. Zmienne te liczą liczbę przerwań jakie upłynęły od czasu ostatniego wywołania podrzędnej części procedury obsługi przerwania. Procedura do_timer zaznacza też jako aktywną swoją podrzędną połowę oraz kolejkę zadań TQUEUE_BH. Zadania wymagające większej ilości czasu wykonywane są przez część podrzędną obsługi przerwania zegarowego:

static void timer_bh(void) {
   update_times();
   run_old_timers(); 
   run_timer_list(); 
}

Funkcje run_old_timers oraz run_timer_list zajmują się uruchamianiem funkcji znajdująceych się na listach starych i nowych liczników czasu. Funkcja update_timers odpowiada za aktualizację zegarów w całym systemie oraz do aktualizacji czasów obecnego procesu. Jeśli upłynął kwant czasu bierzącego procesu to przy najbliższej okazji uruchamiany jest program szeregujący.

W systemie Linux można ograniczyć "zużycie procesora" przez proces. Robi się to za pomocą funkcji systemowej setrlimit. Przekroczenie granicy sprawdzane jest w funkcji update_times, a proces jest informowany przez sygnał SIGXCPU, albo przerywany przez SIGKILL.

psecs = (current -> stime + current -> utime) / HZ; 
if (psecs > current -> rlim[ RLIMIT_CPU ].rlim_cur) { 
   /* Wysyłaj SIGXCPU co sekundę */
   if (psecs * HZ == current -> stime +  current -> utime) 
      send_sig( SIGXCPU , current ,1);
   /* A SIGKILL, gdy zostanie przekroczone maksimum */
   if (psecs > current -> rlim [ RLIMIT_CPU ].rlim_max)
      send_sig( SIGKILL ,current ,1);

 

początek następny spis treści

7.3 Planowanie zdarzeń

W Linuksie istnieje możliwość odłożenia wykonania jakiegoś zadania na pózniej. Najważniejsze mechanizmy wykorzystywane w tym celu to kolejki zadań i liczniki czasu. O ile kolejki zadań umożliwiają nam uruchamianie zadań w bliżej nieokreslonej przyszłości to liczniki czasu pozwalają na określenie dokładnego czasu wykonania.

Kolejki zadań

Kolejki zadań są mocno związane z mechanizmem części podrzędnych procedur obsługi przerwań.

struct tq_struct { 
	struct tq_struct *next; 
	unsigned long sync; 
	void (*routine)(void *); 
	void *data; 
}; 

 

Predefiniowane kolejki zadań

Stare liczniki czasu

Stare liczniki czasu zostały w jądrze linuxa w celu zachowania kompatybilności. Odradza się ich stosowanie w nowych programach. Starych liczników czasu jest 32 z czego część ma już zdefiniowane znaczenie:

#define BLANK_TIMER 0 /* Screen-saver konsoli */ 
#define BEEP_TIMER 1 /* Głośnik konsoli */ 
#define RS_TIMER 2 /* Port RS-232 */ 
#define SWAP_TIMER 3 /* Wymiana stron */ 
#define BACKGR_TIMER 4 /* Żądanie wejścia/wyjścia */ 
#define HD_TIMER 5 /* Stary kontroler IDE */ 
#define FLOPPY_TIMER 6 /* Stacja dysków */ 
#define QIC02_TAPE_TIMER 7 /* Taśma QIC 02 */ 
#define MCD_TIMER 8 /* CDROM Mitsumi */ 
#define GSCD_TIMER 9 /* CDROM Goldstar */ 
#define COMTROL_TIMER 10 /* Comtrol serial */ 
#define DIGI_TIMER 11 /* Digi serial */ 
#define GDTH_TIMER 12 /* Kontroler Gdth scsi */
#define COPRO_TIMER 31 /* 387 błąd sprzętowy (w czasie bootowania) */ 

Liczniki składają się z funkcji oraz czasu w jakim ma ona zostać wykonana. Funkcja jest wykonywana tylko raz, jeśli chcesz ją uruchomić po raz drugi musisz ją ponownie zarejestrować w liczniku czasu.

struct timer_struct { 
	unsigned long expires; 
	void (*fn)(void); 
}; 
extern struct timer_struct timer_table[32]; 

Nowe liczniki czasu

Nowe liczniki czasu nie nakładają ograniczenia na ilość zarejestrowanych funkcji są bowiem zaimplementowane jako lista dwukierunkowa. Dają też możliwość przekazywania argumentów do uruchamianych funkcji (pole data). Zaleca się korzystanie z funkcji jądra w celu modyfikowania wartości pól struktury timer_list po wstawienie jej do listy.

struct timer_list { 
	struct timer_list *next;
	struct timer_list *prev; 
	unsigned long expires; 
	unsigned long data; 
	void (*function)(unsigned long);
 };

 

początek następny spis treści

8. Jak zaimplementować własną funkcję systemową





Mechanizm wywoływania funkcji systemowych

Wywołanie funkcji systemowej polega na wypełnieniu odpowiednich rejestrów i wywołaniu przerwania 0x80. Do tego celu zdefiniowane są specjalne makra, które zwalniają nas z obowiązku bezpośredniego pisania w asemblerze.


Kod funkcji systemowej


Zarejestrowanie nowego wywołania systemowego


Kompilacja


Jak używać

Przed użyciem funkcji systemowej deklarujemy:

    #include <linux/unistd.h>

    _syscall0(int,nazwa_funkcji)

Makro _syscall0 (0 oznacza liczbę parametrów) dzięki wpisowi do linux/unistd.h zna numer naszej funkcji i umieszcza go w rejestrze wywołując przerwanie 0x80. Procedura obsługi przerwania szuka w tablicy sys_call_table adresu funkcji pod pozycją przekazaną w rejestrze. Znajduje ten adres i tam skacze. Funkcje systemowe działają w trybie jądra i dzięki temu mają dostęp do wszystkich jego struktur i zmiennych.

 

początek spis treści