OS Programming

Wielozadaniowość z TSS

Na wstępie chciałem napisać, że jest to mój pierwszy art i że informacje tutaj podane pochodzą w dużej mierze z mojego doświadczenia.  

1. Co to jest TSS.


TSS to skrót od Task State Segment. Informacje o segmencie TSS tak jak każdym innym zapisujemy w GDT. Procesor zapisuje w nim stan aktualnie wykonywanego procesu. TSS zawiera praktycznie wszystkie rejestry procesora oraz kilka innych rzeczy. Struktury tej używa się głównie, aby zaprogramować wielozadaniowość. Procesor korzysta z zawartych w niej danych np. przy przejściach między różnymi poziomami uprzywilejowania. Struktura TSS przedstawia się następująco (kod w C):
typedef struct {
        unsigned long   backlink,
                esp0,
                ss0,
                esp1,
                ss1,
                esp2,
                ss2,
                cr3,
                eip,
                eflags,
                eax,
                ecx,
                edx,
                ebx,
                esp,
                ebp,
                esi,
                edi,
                es,
                cs,
                ss,
                ds,
                fs,
                gs,
                ldt,
                bmoffset;
} tss_t;

Teraz opisze pokolei pola:

backlink


Pole to wskazuje to na ostatnio użyty TSS. Jest ono ustawiane przez procesor przy skuku to TSS'a, gdy flaga NT (Nested Task) jest zapalona. Jeżeli teraz wywołamy instrukcję iret procesor skoczy to TSS'a używając pola backlink

esp0, ss0


Informacje o stosie dla poziomu uprzywilejowania 0. Są one używane gdy nastąpi przejście na poziom 0 z niższego (np. 3 -> 0). Jest to wykorzystywane np. gdy pracujemy na poziomie 3 i nadejdzie przerwanie, to stos zostanie załadowany z esp0 i ss0.

esp1, ss1, esp2, ss2


Podobnie jak esp0 i ss0. Ktoś może się zastanawiać czemu nie ma ss3 i esp3. Odpowiedź jest prosta: nie ma nizszego poziomu niż 3, nie można przejść na 3 poziom jako na wyższy, więc w procesorze nie zaimplementowano esp3 i ss3

cr3


Adres aktualnego katalogu stron.

eip,eflags,eax,ecx,edx,ebx,esp,ebp,esi,edi


To się chyba już każdy domyśli :P

ldt


Numer selektora LDT w GDT.

bmoffset


Zawiera wskaźnik do tzw. I/O permission bitmap. UWAGA!!! Nie podajemy fizycznego położenia w pamięci tylko Offset względem początku TSS'a. Jeżeli nie używamy I/O permission bitmap ustawiamy offset większy niż rozmiar segmentu TSS ustawiony w GDT.

2. Deskryptor TSS


Teraz zajmijmy się desktyptorrem TSS w tablicy GDT. Jego format przedstawia się nastepująco:
tss_descriptor.png
  • B - Busy flag: flaga informująca procesor czy dany TSS jest aktualnie zajęty (zadanie jest aktywne). Procesor ustawia tą flagę gdy załadujemy task register instrukcja tr lub gdy skoczymy to TSS'a
  • BASE - Adres segmentu TSS w pamięci.
  • DPL - Poziom uprzywilejowania.
  • G - Granularność (czy rozmiar segmentu pomnożyć przez 4096).
  • LIMIT - Limit (rozmiar) segmentu.
  • P - Czy segment jest dostępny.
  • TYPE - Typ segmentu.

3. Task Register


Przechowuje 16-bitowy numer selektora TSS w GDT. Procesor używa tych informacji aby ustalić aktualny TSS. Do ładowania/pobierania wartości tr służą dwie procedury:
  • ltr <selektor> - ładuje nową wartość selektora do tr. Może być wywoływana tylko z ring0.
  • str - zapisuje wartość tr. Może być wywoływana z KAŻDEGO poziomu uprzywilejowania.

4. Sposoby przełączania zadań


Istnieją 4 sposoby na przełączenie zadania:
  • Bierzące zadanie wywołuje jmp lub call do deskryptora TSS w GDT.
  • Bierzące zadanie wywołuje jmp lub call do Task-Gate w GDT.
  • Wywołanie przerwania którego wektor wskazuje na task-gate i idt.
  • Wywołanie iret gdy flaga NT w rejestrze eflags jest ustwaiona

Jak procesor przełącza zadanie


  • Procesor otrzymuje selektor TSS dla nowego zadania jako adres JMP/CALL lub z poja backlink (jeśli flaga NT ustawiona i wywołane IRET)
  • Sprawdza czy bierzące zadanie może przełączyć zadania.
  • Sprawdza czy w nowym deskryptorze TSS jest ustwaiona flaga Present, oraz czy rozmier jest poprawny.
  • Sprawdza czy zadanie jest dostępne (dla JMP/CALL/INT) lub zajęte (dla IRET).
  • Sprawdza poprawność wszystkich rejestrów segmentowych.
  • Jeśli zmiana była zainicjowana przez JMP lub IRET, procesor czyści flagę Busy w bierzącym TSS'ie. Jeśli zmiania została zainicjowana przez CALL/INT to flaga BUSY jest ustawiana.
  • Jeśli zmiana została zainicjowana przez IRET procesor czyści flagę NT w zapisanym rejestrze eflags. Jeśli natomiast przłączenie było zainicjowane przez JMP/CALL flaga NT zostaje niezmieniona.
  • Procesor zapisuje stan zadania do TSS oraz wykonaną z poprzednim kroku kopię eflags.
  • Jeśli przełączenie zadań zostało zainicjowane przez CALL/INT procesor ustwaia flage NT w rejestrze
  • Jesli zmiana została zapoczątkowana przez CALL/JMP/INT to przecosor ustwia Busy Flag.
  • Procesor ładuje task register
  • TSS jest załadowany do procesora, dotyczy to CR3, LDTR, EFLAGS, EIP, deskryptorów rejestrów segmentowych oraz ogólnego przeznaczenia.
  • Rejestry segmentowe są ładowane.
  • Rozpoczyna się wykonywanie nowego zadania...

5. Implementacja TSS'ów we własnym OS'ie


Dość teori, czas na praktyke :P. Napiszemy tutaj prosty scheduler, przy użyciu TSS'ów.

1. Informacje o o procesach

Chcąc nie chcąc, gdzieś musimy trzymać informacje o uruchomionych procesach. My wykorzystamy tablicę, której każdy wpis będzie strukturą z informacjami o procesie. A więc na początek może jakiś plik nagłówkowy (sched.h)
#ifndef __SCHED_H
#define __SCHED_H
 
/* Struktura TSS */
typedef struct {
        unsigned long backlink,
                esp0,
                ss0,
                esp1,
                ss1,
                esp2,
                ss2,
                cr3,
                eip,
                eflags,
                eax,
                ecx,
                edx,
                ebx,
                esp,
                ebp,
                esi,
                edi,
                es,
                cs,
                ss,
                ds,
                fs,
                gs,
                ldt,
                bmoffset;
} tss_t __attribute__ ((packed));
 
/* Struktura procesu */
typedef struct {
                char status; //-1 - Free; 0 - runable; 1 - stoped
                tss_t * tss; //Wskaźnik na TSS
                unsigned char tss_id; //Numer TSS'a
                unsigned char next_proc; //ID nastepnego zadania
                char name[32]; //Nazwa
} process_t;
 
/* Maksymalna ilość zadań */
#define MAX_TASKS 128
/* Numer pierwszego wpisu TSS w GDT */
#define FIRST_TSS 5
 
#endif

Zakładamy że TSS'y będą zaczynać się od FIRST_TSS i będą aż do MAX_TASKS + FIRST_TSS
Teraz przydała by się jakaś procedura umożliwiająca instalację TSS'a w GDT. Oto i ona:
void setup_ldt_seg(unsigned long index, unsigned base)
{
 dword limit = sizeof(gdt_desc) * 3;
 gdt[index].base_0_15 = (unsigned long)(base)&0x0000FFFF;
 gdt[index].limit = 0x67;
 gdt[index].base_16_23 = ((unsigned long)(base)&0x00FF0000)>>16;
 gdt[index].dpl_type = 0x82;
 gdt[index].gav_lim = ((limit & 0xF0000) >> 16) | 0x40;
 gdt[index].base_24_31 = ((unsigned long)(base)&0xFF000000)>>24;
}

Korzysta ona ze tablicy struktur, będącej GDT systemu:
typedef struct  {
  unsigned short limit;
  unsigned short base_0_15;
  unsigned char base_16_23;
  unsigned char dpl_type;
  unsigned char gav_lim;
  unsigned char base_24_31;
} gdt_desc;
 
extern gdt_desc gdt[255];


Możemy już dodawać deskryptory TSS do systemu. Możemy więc zacząć pisać naszego sched'a. Zacznijmy od zadeklarowania kilku zmiennych:
/* Tablica procesów */
process_t tasks[MAX_TASKS];
/* TSS'y */
tss_t tasks_tss[MAX_TASKS];
/* Aktualne zadanie */
unsigned char current_task = 0;

Tablica tasks_tss przechowuje TSS dla każdego zadania, natomiast tablica tasks: informacje o zadaniach. Zmienna current_task wskazuje na aktualne zadanie.
Napiszmy więc tablice, która zainicjuje TSS'y, doda je do GDT, utworzy proces kernel'a, czyli krótko mówiąc zainicjalizuje schedulera.
void init_sched(void)
{
 /* procedure get_* zwracają wartośći poszczególnych rejestrów */
 tasks_tss[0].cr3 = get_cr3();
 tasks_tss[0].esp0 = get_esp();
 tasks_tss[0].ss = get_ss();
 tasks_tss[0].esp = get_esp();
 tasks_tss[0].cs = get_cs();
 tasks_tss[0].ds = get_ds();
 tasks_tss[0].es = get_es();
 tasks_tss[0].fs = get_fs();
 tasks_tss[0].gs = get_gs();
 
 setup_tss_seg(FIRST_TSS, &tasks_tss[0]); //TSS dla kernel'a
 
 //Ustawiamy sobie odrazu wszystkie TSS'y
 //i ustawiamy zadania jako wolne
 int i;
 
 for (i=1;i<MAX_TASKS;i++)
 {
  setup_tss_seg(FIRST_TSS + i , &tasks_tss[i]); //Instalujemy w GDT
  tasks[i].status = 0; //Status jako wolne
  tasks[i].tss_id = i; //Numerek w tasks_tss
 }
 
 //Ustawiamy dane o procesie kernel'a
 tasks[0].status = 1; //Uruchomiony
 StrCpy((char *)&tasks[0].name, "KERNEL TASK"); //Nazwa
 tasks[0].tss = &tasks_tss[0]; //Adres na TSS
 tasks[0].tss_id = 0; //selektor tss dla kernel'a
 __asm__ __volatile__ ("ltr %w0"::"r"((FIRST_TSS + tasks[0].tss_id) * 8)); //Wykonujemy ltr <selektor_tss_kernel'a>
                                                                           //ktory zaladuje rejestr tr
}


Mamy sched'a gotowego do pracy. Teraz napiszemy procedure która umożliwi znajdowanie zadania do uruchomienia, oraz przełączenie zadania. A więc do dzieła:
/* Zwraca:
   pid zadania do uruchomienia lub -1 gdy nie ma nic do roboty
*/
 
int schedule(void)
{
 int i; 
 /* Pobieramy nastepne zadanie do uruchomienia */
 i = current_task;
 while (1)
 {
  i = tasks[i].next_proc;
 
  if (tasks[i].status != 0) //Czy zdolne do pracy?
  {
   return -1;
  }
 
  if (i == current_task) //Czy znalezione jest aktualnym
  {
   return -1;
  }
  else
  {
   return i; //Zwróć pid zdania
  }
 }
}

Procedurę, która pobierze nam pid zadania. Teraz trzeba ją jakoś wywoływać. Najprościej i jednocześnie najlepiej jest podpiąć się pod przerwanie zegarowe. Piszemy więc w assemblerze kod obsługu przerwania zegarowego (NASM)
GLOBAL _timer_handler
_timer_handler:
        push gs ;Rejestry segmentowe na stos
        push fs
        push es
        push ds
        pusha
        mov ax,0x10 ;Załaduj wartość dla kernel'a
        mov ds,ax
        mov es,ax
        mov al,0x60 
        out 0x20,al ;EOI
EXTERN _do_irq0       ;Odwołanie do funkcji w C
        call _do_irq0 ;którą na chwile napiszemy
        popa
        pop ds
        pop es
        pop fs
        pop gs
        iret ;wracamy

W tej procedurze odwołujemy się do funkcji do_irq0 w C. Oto i ona:
unsigned short do_irq0(void)
{
 int pid;
 pid = schedule(); //Pobieramy pid nowego zadania
 
 if (pid == -1) //Jesli -1 nie zmieniamy
    return 0;
 
 current_task = pid; //Ustawiamy current_task
 current_task = pid; //Ustawiamy current_task
 DWORD sel[2];
 sel[0] = 0x0;
 sel[1] = (FIRST_TSS + proc_tab[tmp].tss_id) * 8;
 __asm__ __volatile__ ( "ljmp *(%0)": :"g" ((DWORD *)&sel) ); //Skaczemy do zadania
 return 1; //i zwracamy selektor do skoku (nr w GDT * 8)
}

Mamy teraz działające przełączanie zadań. Jednak nie możemy jeszcze dodawać zadań do tablicy procesów. Napiszemy zatem procedury które to umożliwią:
/* Zwraca pierwszy wolny pid w tablicy */
unsigned char get_free_pid(void)
{
 unsigned char i;
 for(i=0;i<MAX_TASKS;i++)
 {
  if (tasks[i].status == -1)
     return i;
 }
 
 return 0;
}
 
/* Dodaje proces do tablicy procesów */
byte add_task(byte pid, void * entry, dword * kstack, dword * ustack, byte user_mode)
{
 if (pid == 0)
 {
  return 0;
 }
 
 //Konfigurujemy TSS
 tasks[pid].tss = &tasks_tss[pid];
 tss_t * tss = tasks[pid].tss;
 
 tss->esp0 = (dword)kstack;
 tss->ss0 = get_ss();
 tss->cr3 = get_cr3();
 tss->eip = (dword)entry;
 
 tss->eflags = 0x202; //Przerwania są włączone
 tss->esp = (dword)ustack;
 tss->bmoffset = sizeof(tss_t); //Nie używamy bitmapy
 tss->ldt = 0;
 
 /* procuje w ring 3 czy w ring 0*/
 if (user_mode)
 {
  tss->cs = 3 * 8 + 3;
  tss->ds = 4 * 8 + 3;
  tss->ss = tss->fs = tss->gs = tss->ds;
 }
 else
 {
  tss->cs = 0x08;
  tss->ds = 0x10;
  tss->ss = tss->fs = tss->gs = tss->ds;
 }
 
 tasks[pid].status = 0;
 return pid;
}
 
/* Funkcja pomocnicza: znajduje ostatnie zadanie w proc tab */
int get_last_task(void)
{
 int i = 0; //Zaczynamy od kernel'a
 while (tasks[i].next_proc != 0)
 {
  i = tasks[i].next_proc;
 }
 
 return i;
}
 
/* Dodaje proces do tablicy procesów */
byte add_proc(void * entry, dword size, char * name)
{
 dword * UStack = allocate_pages(1); //Alokujemy stos poziomu ring 3
 dword * KStack = allocate_pages(1); //Alokujemy stos poziomu ring 0
 //Dodajemy proces
 byte pid = get_free_pid();
 tasks[pid].status = 1; //Zajmij wpis
 int prev_task = get_last_task();
 tasks[prev_task].next_proc = pid;
 tasks[pid].next_proc = 0;
 
 StrCpy(&tasks[pid].name, (char *)name);
 return add_task(pid, entry, kstack , ustack, true);
}

I to by było na tyle. Prosze wybaczyć wszelkie błedy i przeoczenia. Zaprezentowane procedury pochcdzą wprost z mojego OS'a, dlatego mogą odwoływac się do funkcji, których nie ma standardowo.