Wielozadaniowość z TSS
milyges
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): ```cpp 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 backlinkesp0, 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 ss3cr3
Adres aktualnego katalogu stron.eip,eflags,eax,ecx,edx,ebx,esp,ebp,esi,edi
To się chyba już każdy domyśli :Pldt
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: * 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 ustwaionaJak 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.