Pull to refresh

Начинаем разговор о многозадачности

Reading time5 min
Views5K
Приветствую.
Извиняюсь за то, что этот пост так задержался, но не было возможности написать раньше. В этом выпуске начнём говорить о многозадачности для нашей системы.

Для начала решим один важный вопросец: какую многозадачность мы будем реализовывать? Есть аппаратная, есть software многозадачность…
Сначала кажется, что аппаратная лучше, ведь как никак Intel явно постаралась, чтобы этот механизм ‘летал’, но здесь есть подводные камни. Во-первых, это медленнее. Как? Спросите у инженеров Intel. Во-вторых, все, наверное, уже прочитали, что при аппаратной многозадачности мы должны использовать TSS (Task-State Segment), дескрипторы которых хранятся в GDT, которая может вместить … (барабанная дробь) … 8192 дескрипторов. Может показаться, что этого достаточно, но вот на сервере (да, да, помечтаем) этого может оказаться недостаточно. Нам, в принципе, это не важно, но мы сделаем на совесть – software multitasking.
В этом выпуске предлагаю рассмотреть только механизм для переключения задач.
Теперь давайте рассуждать, что нам надо сделать.
1) Придумать какую-нибудь замену TSS.
2) Решить, как реализовать адресное пространство для процессов.
3) Продумать переключение задач.
Давайте реализуем вытесняющую многозадачность т.е делать будем так: по тику таймера (который по дефолту срабатывает каждые 18.2 раза в секунду) будем производить переключение задач. Вместо TSS можно ввести структуру, в которую и будет сохраняться состояние процесса. Адресное пространство для каждого процесса статично (надо же с чего-нибудь начинать, правда?). То есть, грубо говоря, берем кусок оперативки и делим на N равных частей.
Теперь можно приступить к реализации.

Для начала давайте введём замену TSS; Пусть она гордо называется TSS_struct и выглядит следующим образом:

TSS_struct:
0: privilage level (0|3)
1: ESP (Ring0)
5: CR3
9: EIP
13: EFLAGS
17: EAX
21: ECX
25: EDX
29: EBX
33: ESP (Ring3)
37: EBP
41: ESI
45: EDI
49: ES
51: CS
53: SS
55: DS
57: FS
59: GS
61: LDT_selector
63-256 - free


Теперь реализуем функцию, которую будем вызывать при каждом тике таймера.
Нам нужно, чтобы сохранилось состояние старой задачи, отыскалась очередная задача, загрузилось новая. Для пометки TSS_struct’ов будем использовать 10 байт, где каждый бит будет обозначать 1 задачу. Так же стоит рассмотреть 2 ситуации, которые будут влиять на стек:
1) Уровень привилегий не меняется;
2) Меняется;
При первом варианте стек будет выглядеть так:

;|EFLAGS
;|CS
;|EIP <---- ESP указывает сюда
;V


Во втором так:
;|SS
;|ESP
;|EFLAGS
;|CS
;|EIP <---- ESP указывает сюда
;V


Заметим, что меняют значения только те регистры, которые просто необходимы для выполнения обработчика прерывания.
В этом выпуске предлагаю рассмотреть только Ring0. Ring(1|2|3) мы ещё не рассматривали, да и поведение там будет другим, так что мы ограничимся.
Перед тем, как начнём писать код, давайте я вам расскажу, где здесь скользкие места, на которых лично я стопорился, и, зачастую, надолго.
1) Самое элементарное: неправильно указываем адрес возврата, который пихаем в стек.
2) Не устанавливаем в EFLAGS флаг IF (Interrupt Flag). Т.е маскируемые прерывания запрещаются, и о переключении задач можно забыть.
3) Немаловажно не ошибиться в функции поиска следующей задачи. Несмотря на простоту там можно развести жуков. Лично я её прогонял отдельно через Ольгу, чтоб результат был гарантированно верным.
4) Если вы решили не прыгать с процедуры на процедуру, а чинно и спокойно действовать через call’ы, то не забывайте про стек! Вообще стек, по-моему, самое скользкое место в этом деле.
Теперь рассмотрим сокращённую процедуру переключения контекстов. Почему сокращённую? Нужно показать многозадачность, так что не будет писать здесь сейчас много кода. У нас демонстрационный пример.
Ладно, начнём с того, что нам нужно описать новую задачу. Так как она исполняется в Ring0, то стек и значение всех сегментных регистров оставим в покое. Просто положим данные для возврата. Это просто демонстрация! Это вообще не должно носить гордое имя create_task. Просто положим значения для процедуры загрузки да установим бит в битовой карте занятых TSS_struct’ов. Итак:

create_task:
mov ax,20h; - см. ниже
mov es,ax
mov [es:100h+9],dword task;EIP
pushfd ;EFLAGS в стек
pop eax
mov [es:100h+13],eax;EFLAGS
mov [es:100h+51],word 8h ;CS – Ring0, не забываем

bts word [bysi_TSS_map],6 ;Теперь пометим в карте
mov ax,10h;старый селектор на место
mov es,ax
ret


Здесь использован селектор 20h. У меня это область для хранения TSS_struct’ов. Ещё. Почему же устанавливаем 6-ой бит? А загвоздка вот в чём. Код, который уже исполняется должен тоже стать …. задачей. Потому нужно пометить этот 7-ой бит:

bts word [bysi_TSS_map],7


Теперь давайте рассмотрим процедуры сохранения и переключения контекстов, поиска новой задачи.
Здесь всё просто и легко: вычисляем адрес TSS_struct по номеру задачи, ищем новую, считываем из её TSS_struct данные и прыгаем на новую задачу.
Итак, из обработчика PIT’а прыгаем на процедуру переключения задач — task_switch:
task_switch:

mov [temp_1],eax
mov [temp_2],es

xor eax,eax
mov ax,20h
mov es,ax

call calculate_TSS_struct_address ;возвращает: EDI – начальный адрес
jmp store_context


Где переменные для хранения:

temp_1 dd 0;EAX
temp_2 dw 0;ES


Подсчёт адреса нам понадобится и в будущем. Так что оформим в виде процедуры.

calculate_TSS_struct_address:
push eax
push ebx
mov eax,[cur_task_num];в этой dword’овой переменной будем хранить номер тек. задачи
mov ebx,100h
mul ebx
pop ebx
mov edi,eax;EDI – начало TSS_struct
pop eax
ret


Теперь рассмотрим процедуру сохранения контекста. Банальщина – по смещениям кладём значения. Всё.

store_context:
;mov eax,cr3 ;каталоги страницы не трогаем
;mov [es:edi+5],eax

pushfd
pop eax
mov [es:edi+13],eax;EFLAGS

mov [es:edi+21],ecx
mov [es:edi+25],edx
mov [es:edi+29],ebx
mov [es:edi+37],ebp
mov [es:edi+41],esi
mov [es:edi+45],edi

mov ax,[temp_2]
mov [es:edi+49],ax;ES

mov eax,[temp_1]
mov [es:edi+17],eax
mov [es:edi+53],ss
mov [es:edi+55],ds
mov [es:edi+57],fs
mov [es:edi+59],gs
;sldt ax ;Локальных сегментов у нас тоже пока нет
;mov [es:edi+61],ax

pop eax
mov [es:edi+9],eax;EIP
; Чистим стек
pop ax
mov [es:edi+51],ax;CS

popfd ;EFLAGS

jmp find_next_task


Следующая функция ищет очередную жертву в битовом массиве и возвращает в EDI её адрес.

find_next_task:
xor edx,edx
mov eax,[cur_task_num]
mov ecx,8
div ecx
;EAX - номер байта
;EDX - 'перевёрнутый' номер бита
test edx,edx
jnz .norm

mov edi,7
jmp .step

.norm: mov edi,8
.step: sub edi,edx;real bit #
mov edx,edi
mov edi,eax

.cycle:
bt word [bysi_TSS_map+edi],dx
jc .found
cmp dx,0
je .inc_byte
dec dx
jmp .cycle

.inc_byte:
cmp edi,10; лично я ограничил кол-во процессов 800-ста штуками.
je .error
inc edi
mov dx,7
jmp .cycle

.found:
push edx
mov eax,8
mul edi
pop edx

mov di,7
sub di,dx
add eax,edi
mov [cur_task_num],eax
call calculate_TSS_struct_address
jmp load_context

.error:
mov dx,7;по новой в атаку.
xor edi,edi
jmp .cycle


Теперь осталась лишь загрузка контекста.

load_context:
;mov eax,[es:edi+5];CR3 - пока со страницами не разбираемся
;mov cr3,eax

;mov esp,[es:edi+1];стек у нас пока что системный
;mov ss,[es:edi+53]

mov ecx,[es:edi+21]
mov edx,[es:edi+25]
mov ebx,[es:edi+29]
mov ebp,[es:edi+37]
mov esi,[es:edi+41]
mov edi,[es:edi+45]

;mov ds,[es:edi+55]; мы же не клали сюда ничего. Так что пока ничего и не загружаем
;mov fs,[es:edi+57]
;mov gs,[es:edi+59]

;кладём 'координаты' новой задачи в стек (для iretd)
push dword [es:edi+13];EFLAGS
push word [es:edi+51] ;CS
push dword [es:edi+9];EIP

jmp timer.s_t


Здесь стоит обратить внимание. Как помним, при прерывании в стеке (в данном случае коммунальном) хранятся 3 значения (privilege level не меняется). Перед переходом мы кладём в стек 'координаты' новой задачи, передаём управление обработчику PIT'а и делаем iretd. Наши мучения закончены? Почти. В контексте новой задачи нужно сделать sti, чтобы разрешить прерывания. Вот и всё. А вы боялись!
Обработчик же для таймера примет следующий вид:

timer:
;........
.s_t:
push ax ; шлём EOI
mov al,20h
out 20h,al
out 0a0h,al
pop ax
iretd ; прыгаем на задачу


Теперь введём задачи.
Помните, до этого у нас код заканчиваля на jmp $?
Теперь туда можно поставить увеличение символа. Наглядно и быстро.
А task, который мы упоминали до этого можно представить в виде

task:
sti
inc byte [gs:0]
jmp task


Теперь собираем всё это вместе и любуемся на результат.

Итак. Вот вводная статья и закончилась.
Реализовали подобие многозадачности. У 'задач' нет своего стека, своих сегментов и локальных дескрипторных таблиц… Предстоит ещё много работы. Вот и темы для будущих выпусков. Извиняюсь за свой английский ('bysi' = 'busy'; просто в коде везде долго исправлять).
Tags:
Hubs:
+25
Comments18

Articles

Change theme settings