Starting a multitasking conversation
Greetings.
I apologize for the fact that this post was so late, but it was not possible to write earlier. In this issue, we’ll start talking about multitasking for our system.
To begin with, we will solve one important question: what multitasking will we implement? There is hardware, there is multitasking software ...
At first it seems that the hardware is better, after all, Intel clearly tried to make this mechanism “fly”, but there are pitfalls here. Firstly, it is slower. How? Ask Intel engineers. Secondly, everyone probably already read that for hardware multitasking we should use TSS (Task-State Segment), whose descriptors are stored in GDT, which can accommodate ... (drum roll) ... 8192 descriptors. It may seem that this is enough, but on the server (yes, yes, we will dream) this may not be enough. For us, in principle, this is not important, but we will do it honestly - software multitasking.
In this issue, I propose to consider only the mechanism for switching tasks.
Now let's talk about what we need to do.
1) Come up with some kind of replacement TSS.
2) Decide how to implement the address space for processes.
3) Think over task switching.
Let's implement preemptive multitasking, i.e. we will do this: according to the timer tick (which defaults every 18.2 times per second), we will switch tasks. Instead of TSS, you can enter a structure into which the state of the process will be saved. The address space for each process is static (you have to start somewhere, right?). That is, roughly speaking, we take a piece of RAM and divide it into N equal parts.
Now you can start implementation.
First, let's introduce a replacement TSS; Let it be proudly called TSS_struct and look as follows: Now we implement the function that we will call at each tick of the timer.
We need to keep the state of the old task, find another task, load a new one. To mark TSS_structs we will use 10 bytes, where each bit will denote 1 task. It is also worth considering 2 situations that will affect the stack:
1) The privilege level does not change;
2) It changes;
In the first case, the stack will look like this: In the second one: Note that only those registers that are simply necessary to execute the interrupt handler change their values. In this issue, I propose to consider only Ring0. Ring (1 | 2 | 3) we have not yet considered, and the behavior there will be different, so we will limit ourselves.
Before we start writing the code, let me tell you where there are slippery places where I personally stopped, and often for a long time.
1) The most elementary: we incorrectly specify the return address that we push onto the stack.
2) Do not set IF flag (Interrupt Flag) in EFLAGS. That is, masked interrupts are prohibited, and you can forget about switching tasks.
3) It is important not to make a mistake in the search function of the next task. Despite its simplicity, you can breed beetles there. Personally, I drove it separately through Olga, so that the result was guaranteed to be true.
4) If you decide not to jump from procedure to procedure, but to act calmly and calmly through calls, then do not forget about the stack! In general, the stack, in my opinion, is the most slippery place in this business.
Now consider a shortened context switching procedure. Why abbreviated? We need to show multitasking, so I won’t write a lot of code here now. We have a demo.
Okay, let's start with the fact that we need to describe a new task. Since it is executed in Ring0, let us leave the stack and the value of all segment registers alone. Just put the data to return. This is just a demonstration! This should not bear the proud name create_task at all. Just put the values for the boot procedure and set the bit in the bitmap of the occupied TSS_structs. So: The 20h selector is used here. I have this area for storing TSS_structs. Yet. Why do we set the 6th bit? And here's the catch. Code that is already being executed should also become .... task. Therefore, you need to mark this 7th bit:
Now let's look at the procedures for saving and switching contexts, searching for a new task.
Everything is simple and easy here: we calculate the TSS_struct address by the task number, look for a new one, read the data from its TSS_struct and jump to the new task.
So, from the PIT handler we jump to the task switching procedure - task_switch: Where are the variables for storage: We will need to calculate the address in the future. So let's arrange it as a procedure. Now consider the procedure for saving context. Banality - on the offsets we put the values. All. The following function searches for the next victim in the bitmap and returns its address in the EDI. Now all that remains is context loading.
Here it is worth paying attention. As we remember, during interruption, 3 values are stored in the stack (in this case, communal) (privilege level does not change). Before the transition, we push the 'coordinates' of the new task onto the stack, transfer control to the PIT handler and make iretd. Our torment is over? Nearly. In the context of the new task, you need to do sti to enable interrupts. That's all. And you were afraid!
The handler for the timer will take the following form: Now we introduce the tasks. Remember, before that we have the jmp $ completion code? Now you can put an increase in the character there. Visually and quickly. And the task that we mentioned before can be represented in the form Now we put it all together and enjoy the result. So. Here's the introductory article and it's over.
Implemented a semblance of multitasking. The 'tasks' do not have their own stack, their segments and local descriptor tables ... There is still much work to be done. Here are the topics for future releases. I apologize for my English ('bysi' = 'busy'; it’s easy to fix the code everywhere for a long time).
I apologize for the fact that this post was so late, but it was not possible to write earlier. In this issue, we’ll start talking about multitasking for our system.
To begin with, we will solve one important question: what multitasking will we implement? There is hardware, there is multitasking software ...
At first it seems that the hardware is better, after all, Intel clearly tried to make this mechanism “fly”, but there are pitfalls here. Firstly, it is slower. How? Ask Intel engineers. Secondly, everyone probably already read that for hardware multitasking we should use TSS (Task-State Segment), whose descriptors are stored in GDT, which can accommodate ... (drum roll) ... 8192 descriptors. It may seem that this is enough, but on the server (yes, yes, we will dream) this may not be enough. For us, in principle, this is not important, but we will do it honestly - software multitasking.
In this issue, I propose to consider only the mechanism for switching tasks.
Now let's talk about what we need to do.
1) Come up with some kind of replacement TSS.
2) Decide how to implement the address space for processes.
3) Think over task switching.
Let's implement preemptive multitasking, i.e. we will do this: according to the timer tick (which defaults every 18.2 times per second), we will switch tasks. Instead of TSS, you can enter a structure into which the state of the process will be saved. The address space for each process is static (you have to start somewhere, right?). That is, roughly speaking, we take a piece of RAM and divide it into N equal parts.
Now you can start implementation.
First, let's introduce a replacement TSS; Let it be proudly called TSS_struct and look as follows: Now we implement the function that we will call at each tick of the timer.
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
We need to keep the state of the old task, find another task, load a new one. To mark TSS_structs we will use 10 bytes, where each bit will denote 1 task. It is also worth considering 2 situations that will affect the stack:
1) The privilege level does not change;
2) It changes;
In the first case, the stack will look like this: In the second one: Note that only those registers that are simply necessary to execute the interrupt handler change their values. In this issue, I propose to consider only Ring0. Ring (1 | 2 | 3) we have not yet considered, and the behavior there will be different, so we will limit ourselves.
;|EFLAGS
;|CS
;|EIP <---- ESP указывает сюда
;V
;|SS
;|ESP
;|EFLAGS
;|CS
;|EIP <---- ESP указывает сюда
;V
Before we start writing the code, let me tell you where there are slippery places where I personally stopped, and often for a long time.
1) The most elementary: we incorrectly specify the return address that we push onto the stack.
2) Do not set IF flag (Interrupt Flag) in EFLAGS. That is, masked interrupts are prohibited, and you can forget about switching tasks.
3) It is important not to make a mistake in the search function of the next task. Despite its simplicity, you can breed beetles there. Personally, I drove it separately through Olga, so that the result was guaranteed to be true.
4) If you decide not to jump from procedure to procedure, but to act calmly and calmly through calls, then do not forget about the stack! In general, the stack, in my opinion, is the most slippery place in this business.
Now consider a shortened context switching procedure. Why abbreviated? We need to show multitasking, so I won’t write a lot of code here now. We have a demo.
Okay, let's start with the fact that we need to describe a new task. Since it is executed in Ring0, let us leave the stack and the value of all segment registers alone. Just put the data to return. This is just a demonstration! This should not bear the proud name create_task at all. Just put the values for the boot procedure and set the bit in the bitmap of the occupied TSS_structs. So: The 20h selector is used here. I have this area for storing TSS_structs. Yet. Why do we set the 6th bit? And here's the catch. Code that is already being executed should also become .... task. Therefore, you need to mark this 7th bit:
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
bts word [bysi_TSS_map],7
Now let's look at the procedures for saving and switching contexts, searching for a new task.
Everything is simple and easy here: we calculate the TSS_struct address by the task number, look for a new one, read the data from its TSS_struct and jump to the new task.
So, from the PIT handler we jump to the task switching procedure - task_switch: Where are the variables for storage: We will need to calculate the address in the future. So let's arrange it as a procedure. Now consider the procedure for saving context. Banality - on the offsets we put the values. All. The following function searches for the next victim in the bitmap and returns its address in the EDI. Now all that remains is context loading.
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
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
Here it is worth paying attention. As we remember, during interruption, 3 values are stored in the stack (in this case, communal) (privilege level does not change). Before the transition, we push the 'coordinates' of the new task onto the stack, transfer control to the PIT handler and make iretd. Our torment is over? Nearly. In the context of the new task, you need to do sti to enable interrupts. That's all. And you were afraid!
The handler for the timer will take the following form: Now we introduce the tasks. Remember, before that we have the jmp $ completion code? Now you can put an increase in the character there. Visually and quickly. And the task that we mentioned before can be represented in the form Now we put it all together and enjoy the result. So. Here's the introductory article and it's over.
timer:
;........
.s_t:
push ax ; шлём EOI
mov al,20h
out 20h,al
out 0a0h,al
pop ax
iretd ; прыгаем на задачу
task:
sti
inc byte [gs:0]
jmp task
Implemented a semblance of multitasking. The 'tasks' do not have their own stack, their segments and local descriptor tables ... There is still much work to be done. Here are the topics for future releases. I apologize for my English ('bysi' = 'busy'; it’s easy to fix the code everywhere for a long time).