I'm Multitasking! (not sure why :) )

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Cjmovie

I'm Multitasking! (not sure why :) )

Post by Cjmovie »

Well, I considered it a longshot, but I made my kernel multi-task in 10 minutes :).

I simply installed a new IRQ handler for the PIT and set it to go at 100 hz.

Then, inside the IRQ handler, it switches what task it's on by changing values in the register state passed to it (as a pointer in the stack). That way, when I return to the ASM part of the Interrupt, it POPs the values into the real registers.

Anyways, as I'm guessing I'll have to ask a question to pertain, I've also the question I was going to ask about it halfway through:

It's multitasking fine for a few seconds (actually, in bochs, it's a lot less. Don't see it with your eyes). Then it pauses for around 1/4 a minute, and resets (guessing a triple fault, cause maybe by lack of stack space?).

The only thing is that I never get an exception from the CPU, real or bochs. What could cause that pause, and what would triple fault without any exception? I know I'm on a longshot here without any code to show, but if you need it, feel free to ask. Again, I'm asking for general help.
JoeKayzA

Re:I'm Multitasking! (not sure why :) )

Post by JoeKayzA »

Hmm..

The pause could be a recursive loop, causing a stack overflow...and then triple fault. Is the stack in a place where it could overwrite parts of your IDT or page tables? (the latter one seems more likely to be the case) If so, try putting a guard page directly below stack space, then you can intercept this issue (and this is a good idea anyway in kernel mode)

Always check (and print) the new esp before you return to the next task, ensuring it is in a valid location. Check this out, if it doesn't help, some code would be interesting.

cheers Joe
proxy

Re:I'm Multitasking! (not sure why :) )

Post by proxy »

there is a problem with your multitasking code. When trying to figure out stack swapping i tried just what you are talking about and unfortunately it doesn't pan out in the long run. The problem is that while 7 of the general purpose registers will have your new state and CS/EIP/EFLAGS will too. ESP will not be correct for your new task. If you read the intel docs on a popa esp on the stack is NOT popped into the esp register (as this would make the popa fail further pops). So now the problem is that you are running code from task B with a stack from task A. If either of them have local vars on the stack...well consider them buh-by.

The way that i do it is really simple. My asm ISR handler calls a c/c++ function which does the actual work. This c/c++ function will return the esp of the next task. Then when i return to the asm stub, the next code loads eax into esp thus switching to the next tasks stack. Then as you planned all the general purpose registers look as they should for the new task.

It's super easy once you get your brain around it...until then well it's a little confusing, so good luck and I hope this helped.

proxy
JoeKayzA

Re:I'm Multitasking! (not sure why :) )

Post by JoeKayzA »

proxy wrote: The problem is that while 7 of the general purpose registers will have your new state and CS/EIP/EFLAGS will too. ESP will not be correct for your new task. If you read the intel docs on a popa esp on the stack is NOT popped into the esp register (as this would make the popa fail further pops). So now the problem is that you are running code from task B with a stack from task A. If either of them have local vars on the stack...well consider them buh-by.
Eh - I don't really understand: Assuming you are pusha'ing task A's state to task A's stack (and save ESP seperately then), then switch stack to task B (that means: replace ESP with task B's saved ESP), and then popa the state again, you have task B's state as well as it's valid ESP. I can't see a problem here.. ???

cheers Joe
proxy

Re:I'm Multitasking! (not sure why :) )

Post by proxy »

JoeKayzA, what you described sound fine, but it NOT the same as Cjmovie described, read it carefully...
Then, inside the IRQ handler, it switches what task it's on by changing values in the register state passed to it (as a pointer in the stack). That way, when I return to the ASM part of the Interrupt, it POPs the values into the real registers.
he is not changing esp, he is merely changing the saved register values on the stack.

think of it like this: (this is psuedo code so not precise)

Code: Select all

int_00:
push es
push ds
push fs
push gs
pusha

call int_handler

popa
pop gs
pop fs
pop ds
pop es

iret

/*----------c code -----------*/

voit int_handler(context c) {
/* call scheduler */
c.eax = next->eax;
c.ebx = next->ebx;
c.ecx = next->ecx;
c.edx = next->edx;

/*etc etc*/
}

from what i read in his oriingal question, he is modifying the saved register state and NOT swapping tasks.

proxy
JoeKayzA

Re:I'm Multitasking! (not sure why :) )

Post by JoeKayzA »

@proxy: You are right, I guess I'm already too accomodated to stack-based task state saving and switching.... ::)

cheers Joe
Cjmovie

Re:I'm Multitasking! (not sure why :) )

Post by Cjmovie »

To be exact, I am pushing all the registers in the ISR stub. Then a pointer to that area of the stack is pushed, which can be accessed by C code like a pointer to a structure. Then, after it changes the values in the stack, it returns to the ISR. The ISR then perfoms POPS for all the registers it earlier pushed, only the C code has changed them. That way the pop's actually 'install' the new task. If you wish to see code, you may below. It is actually super simple :).

Also, I would like to add that I tested it on my my working machine. It works perfectly, and ran for minutes without problem. I ended up turning it off, as I could see no trouble. So it appears that either my original *real* testbed is messed up or my new computer is more forgiving.

TaskSwitch.c:

Code: Select all

TaskState NextTask;
TaskState TaskList[2];
int CurrentTask = 0;

void SetTask(int a, TaskState b){
 TaskList[a] = b;
}

void TaskSwitcher(RegStateExt* r){
 if(CurrentTask == 0)CurrentTask = 1;
 else CurrentTask = 0;
 
 memcpy((UCHAR*)r, (UCHAR*)&TaskList[CurrentTask], 64);
}

void StartMultitask(){
 IRQ_InstallHandler(0, TaskSwitcher);
}
This is the 'main.c' (really, it's Kernel.c). KernelEntry is where control is passed to it.

Code: Select all

void BeepThis(){
 for(;;)PrintString("* TASK1! *");
}

void BeepThat(){
 for(;;)PrintString("+ TASK2! +");
}

TaskState BeepThisTask = {
 0x10, 0x10, 0x10, 0x10,
 0x00, 0x00, 0x00, 0x100000, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00,
 (UINT)BeepThis, 0x08, 0x00,
};

TaskState BeepThatTask = {
 0x10, 0x10, 0x10, 0x10,
 0x00, 0x00, 0x00, 0x200000, 0x00, 0x00, 0x00, 0x00,
 0x00, 0x00,
 (UINT)BeepThat, 0x08, 0x00,
};

void KernelEntry(LoaderInfoBlock* argv){
 int i, x;
 int NextMsgLine = 0;
 char a;
 
 SetSystemMemory(16);
 SetSystemBitmap(0x7E0000, 0xFFFE0000);
 
 GDT_Init();
 GDT_Install();
 
 IDT_Install();
 ISR_Install();
 IRQ_Install();
 
 PIT_Install();
 Keyboard_Install();
 
 EnableInterrupts();
 
 ClearScreenTo(0x00);
 
 PrintString("--DEMI OS--  (Version 0.0.5b)\n");
 PrintString("   + Memory Page Manager\n");
 PrintString("   + GDT\n");
 PrintString("   + IDT\n");
 PrintString("   + ISR\n");
 PrintString("   + IRQ\n");
 PrintString("   + Default Module: Console(CGA)\n\n");
 
 PrintString("Time it took to get to here: 0x");
 PrintHex(PIT_GetTime());
 
 SetTask(0, BeepThisTask);
 SetTask(1, BeepThatTask);
 StartMultitask();
 
 for(;;);
}
(Note: Some non-pertaining code has been ommited.)

ISR stub:

Code: Select all

;Common stub for all ISRs. Pushes all registers, then pointer to that structure
IsrStub:
 pushad    ;Push this stuff, in opposite order of memory (stack grows DOWN!)
 push ds
 push es
 push fs
 push gs
 
 mov ax, 0x10 ;Kernel Data Segment!
 mov ds, ax ;Restore data segment
 mov es, ax
 mov fs, ax
 mov gs, ax
 
 mov eax, esp ;Get stack pointer
 push eax     ;Push it. Pointer to structure formed in memory above
 
 call _IsrEntry    ;Call C handler
 
 pop eax   ;Pop back the standard registers
 pop gs
 pop fs
 pop es
 pop ds
 popad
 add esp, 8 ;Clean up pushed error code and ISR number
 iret       ;Return to interrupted whatever
Cjmovie

Re:I'm Multitasking! (not sure why :) )

Post by Cjmovie »

Almost forgot:

Code: Select all

typedef struct{
 unsigned int gs, fs, es, ds;
 unsigned int edi, esi, ebp, esp, ebx, edx, ecx, eax;
 unsigned int IdNum, ErrorCode;
 unsigned int eip, cs, eflags, useresp, ss;
} __attribute__ ((packed)) RegStateExt;
And:

Code: Select all

typedef RegStateExt TaskState;
proxy

Re:I'm Multitasking! (not sure why :) )

Post by proxy »

Cjmovie, thank you for confirming my suspisions about your technique. But it is still broken for the reasons I gave. the popa instruction will NOT load the esp from your state. According to the intel docs, a popa is fuctionally equivalent to this:

Code: Select all

pop edi
pop esi
pop ebp
add $4, esp
pop ebx
pop edx
pop ecx
pop eax
do you see the problem now? you never load a new esp for your new task, which will firstly give you bad data for any stack variables in your tasks, and will in the long run lead the stack overflows since all your "threads" are sharing the same stack effectivly.

proxy
Cjmovie

Re:I'm Multitasking! (not sure why :) )

Post by Cjmovie »

Oooh, ya....

Intel doc's sometimes can be hard to read when your tired :).

Oh well, it's an easy fix at least! I just need to add one more push and change my structure around. I'll let you guys know how it turns out.

And I think I know why it worked on one real computer: It seemed to be timed, somehow, just perfect. Each time it finished a call to "PrintString" it switch. That way the stack was in essence 'clean' at the task switch, meaning it could do whatever. I noticed that the tasks never wrote characters right in between each other, in otherwords:

"*TASK1*+TASK2+*TASK1*+TASK2+"
instead of:
"*TAS+TASKK1**2++Task2Tas" etc.
Cjmovie

Re:I'm Multitasking! (not sure why :) )

Post by Cjmovie »

Cjmovie wrote: Oooh, ya....

Intel doc's sometimes can be hard to read when your tired :).

Oh well, it's an easy fix at least! I just need to add one more push and change my structure around. I'll let you guys know how it turns out.

And I think I know why it worked on one real computer: It seemed to be timed, somehow, just perfect. Each time it finished a call to "PrintString" it switch. That way the stack was in essence 'clean' at the task switch, meaning it could do whatever. I noticed that the tasks never wrote characters right in between each other, in otherwords:

"*TASK1*+TASK2+*TASK1*+TASK2+"
instead of:
"*TAS+TASKK1**2++Task2Tas" etc.
Ya, right! Not so easy, is it me? lol :)

The main problem is that ESP has to be the LAST to be popped, so that the rest of the POPs don't pop on the wrong stack. But the processor, on returning from the interrupt (IRET) will do more pops. So I guess back to the 'drawing board'...

Time to get a TSS!
proxy

Re:I'm Multitasking! (not sure why :) )

Post by proxy »

no no no, you are going about it all wrong. it really is simple.

Basically you want something like this (keep in mind psuedo code so non critical details may be omited)

Code: Select all

irq_00:
push %ds
push %es
push %fs
push %gs
pusha

mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs

push %esp /*this allows the function to return the oriiginal esp if neccessary */

call IsrEntry  /* this will return the stack kernel esp of the new task */

add $4, %esp /* counter that push esp */

mov %eax, %esp /* set esp to what _IsrEntry returned */

/* at this point we are running on the NEW esp, pop all the other registers */

popa
pop %gs
pop %fs
pop %es
pop %ds

add $8, %esp
iret
and the C code would look something like this:

Code: Select all

uint32_t IsrEntry(uint32_t old_esp) {
    Thread *oldThread = current;    
    Thread *newThread = pickNextThread();
    if(oldThread != newThread) {
        /* update TSS's esp0 entry for new thread */
        /* maybe update cr3 if neccessary */
        return newThread->esp0;
    } else {
        return oldThread->esp0;
    }
}
see, this way when you want to create a new thread, you simply allocate it's kernel stack and push what looks like your iret frame and you just add it to the scheduler.


a lot simpler than it sounds right?

proxy
distantvoices
Member
Member
Posts: 1600
Joined: Wed Oct 18, 2006 11:59 am
Location: Vienna/Austria
Contact:

Re:I'm Multitasking! (not sure why :) )

Post by distantvoices »

It's as proxy says: dead simple and straight forward once one's got the brain wrapped around.

Cjmovie: Don't modify registers in saved away stacks: simply switch the stackpointer to the new esp0 stack. Registers of the saved state are simply popped off by the isr stub, and at last you're back in your good old esp3 stack, eh?

look at this pseudo code, it describes what proxy says:

taskA ... running
Interrupt/schedule
isr handler called by stub returns TaskB.esp0 (that's stuffed into eax)
isrstub sets the new esp0 value by moving eax to esp
pop pop pop (we want popcorn) all the registers.
IRET -> pops off eflags,cs,eip,esp,ss
taskB ... running

Mark that with this method it doesn't matter to which task you return. you can as well resume taskA again.

stay safe.
... the osdever formerly known as beyond infinity ...
BlueillusionOS iso image
Cjmovie

Re:I'm Multitasking! (not sure why :) )

Post by Cjmovie »

Ya, and if I follow, To create a task:

Allocate stack space for process
add SizeOfStack to that pointer (pointer is at start, stack grows down)
subtract 4 (size of one push, as we are on edge of next memory page if we don't)
Then do
*StackPointer-- = ThisValueThatWillNeedToBePopped.
etc. for each that will have to be popped.
And then just treat them all the same when task-switching


:)
Cjmovie

Re:I'm Multitasking! (not sure why :) )

Post by Cjmovie »

Still having a little trouble. Here's my code:

Code: Select all

typedef struct{
 UINT esp0;
 UINT esp3;
} __attribute__ ((packed)) Task;

extern void ASM_TaskStub();

Task TaskList[25];
int CurrentTask = -1;

void MT_ScheduleThread(int a, void (*thread)()){
 UINT *Stack;
 
 TaskList[a].esp0 = (UINT)AllocPage()+4096; //Stack for when kernel is called
 TaskList[a].esp3 = (UINT)AllocPage()+4096; //Stack for process itself
 
 TaskList[a].esp0--;              //Offset kernel stack into actual 4kb page
 
 TaskList[a].esp3--;              //Offset user/app stack into actual 4kb page
 Stack = (UINT*)TaskList[a].esp3; //Get stack pointer for process
 
 //IRET
 *Stack-- = 0x0202;       //EFLAGS
 *Stack-- = 0x08;         //CS
 *Stack-- = (UINT)thread; //EIP
 
 //PUSHa / POPa
 *Stack-- = 0; //EAX
 *Stack-- = 0; //ECX
 *Stack-- = 0; //EDX
 *Stack-- = 0; //EBX
 *Stack-- = 0; //Temp/Unused
 *Stack-- = 0; //EBP
 *Stack-- = 0; //ESI
 *Stack-- = 0; //EDI
 
 //PUSH ds, es, fs, gs / POP gs, fs, es, ds
 *Stack-- = 0x10; //DS
 *Stack-- = 0x10; //ES
 *Stack-- = 0x10; //FS
 *Stack-- = 0x10; //GS
 
 TaskList[a].esp3 = (UINT)Stack; //Update stack position in process info struct
}

UINT MT_SwitchTask(UINT esp){
 if(CurrentTask != -1)
  TaskList[CurrentTask].esp3 = esp;
 else
  CurrentTask = 0;
 
 CurrentTask++;
 if(CurrentTask > 1)CurrentTask = 0;
 
 return TaskList[CurrentTask].esp3;
}

void MT_Start(){
 IDT_InstallHandler(32, (UINT)ASM_TaskStub, 0x08, 0);
} /*32 is mapped to IRQ 0 */
ASM code (NASM syntax, not GAS):

Code: Select all

global _ASM_TaskStub
extern _MT_SwitchTask

;When called by an IRQ from processor, allows task switches
_ASM_TaskStub:
 pushad              ;Push all general purpose registers
 
 push ds             ;Push data segment
 push es             ;Push data segment 'e'
 push fs             ;Push data segment 'f'
 push gs             ;Push data segment 'g'
 
 mov eax, esp        ;Get current stack pointer
 push eax            ;Push it as argument to C function
 
 call _MT_SwitchTask ;Call 'C code' task scheduler
 
 add esp, 4          ;Clean up pushed argument to 'C code'
 
 mov esp, eax        ;Set new task's stack pointer. (EAX from C code return value)
 
 pop gs              ;Pop off data segment 'g'
 pop fs              ;Pop off data segment 'f'
 pop es              ;Pop off data segment 'e'
 pop ds              ;Pop off data segmemt
 
 popad               ;Pop all general purpose registers
 iret                ;Interrupt Return. Installs EIP etc. of new task
I'm getting from bochs:
fetch_raw_descriptor: LDTR.valid=0
Usually, as I find, that means invalid combination of pushes and pops. So, I'm guessing it has to do with me setting up the stack in the MT_ScheduleThread(). If in the C part of the switcher I just return the current ESP, it's fine (stays in kernel code).

Also, Both are ring0, so the CPU isn't pushing UserESP or SS.
Post Reply