OSDev.org

The Place to Start for Operating System Developers
It is currently Fri Apr 19, 2024 4:23 pm

All times are UTC - 6 hours




Post new topic Reply to topic  [ 7 posts ] 
Author Message
 Post subject: Implementing a Thread Yield
PostPosted: Sat Jun 23, 2007 4:03 pm 
Offline
Member
Member
User avatar

Joined: Sat Apr 21, 2007 7:21 pm
Posts: 127
Basically, if I am stuck sitting somewhere in a loop waiting for an event or something, I would like to call a thread yield function.

This guys purpose is to emulate a task switch that occurs when a timer event is fired.

So I have this code below that is my timer handler:
Code:
irq0:

pusha          ;Push all standard registers

push ds        ;Push segment d

push es        ;Push segmetn e

push fs        ; ''

push gs        ; ''



mov eax, 0x10  ;Get kernel data segment

mov ds, eax    ;Put it in the data segment registers

mov es, eax

mov fs, eax

mov gs, eax



push esp       ;Push pointer to all the stuff we just pushed

call TaskSwitch ;Call C code



mov esp, eax   ;Replace the stack with what the C code gave us



mov al, 0x20   ;Port number AND command number to Acknowledge IRQ

out 0x20, al     ;Acknowledge IRQ, so we keep getting interrupts



pop gs         ;Put the data segments back

pop fs

pop es

pop ds



popa           ;Put the standard registers back



;We didn't push an error code or IRQ number, so we don't have to edit esp now



iret           ;Interrupt-Return



And this is wonderfully handled by my task switcher:
Code:
typedef struct{        //Simple structure for a thread
  unsigned int esp0;   //Stack for kernel
  unsigned int esp3;   //Stack for process
  unsigned long thread_id;
} Thread;

typedef struct _ThreadQueue ThreadQueue;
struct _ThreadQueue
{
   ThreadQueue *prev;   
   ThreadQueue *next;
   Thread *thread;
};

ThreadQueue *ReadyQueue;

unsigned int TaskSwitch(unsigned int OldEsp)
{
   OSTicks++;
   ThreadQueue *exec,*m_blk;
     
     exec = ReadyQueue;
     
     //First time into task switcher... the OldEsp is Thrown Away (Old kernel thread)
     if (CurrentTask == -1)
     {
        CurrentTask = ReadyQueue->thread->thread_id;
        return ReadyQueue->thread->esp0;
     }
          
   //If there is only one task to switch
     if (ReadyQueue->next == 0)
     {
        ReadyQueue->thread->esp0 = OldEsp;
        CurrentTask = ReadyQueue->thread->thread_id;
        return ReadyQueue->thread->esp0;   
     }
     
     //This case is for > 1 Thread
     ReadyQueue = ReadyQueue->next;

   for (m_blk = ReadyQueue; m_blk != 0; m_blk = m_blk->next)
   {
      if (m_blk->thread->thread_id == CurrentTask)
      {
         m_blk->thread->esp0 = OldEsp;
      }
      
      if (m_blk->next == 0)
      {
         m_blk->next = exec;
         exec->prev = m_blk;
         exec->next = 0;
      }
   }

     CurrentTask = exec->thread->thread_id;
     return exec->thread->esp0;   
}


Basically I want a way that I can get to the switcher without sitting around for an interrupt.

I tried an obvious
Code:
extern TaskSwitch

YieldThread:
pusha          ;Push all standard registers
push ds        ;Push segment d
push es        ;Push segmetn e
push fs        ; ''
push gs        ; ''

mov eax, 0x10  ;Get kernel data segment
mov ds, eax    ;Put it in the data segment registers
mov es, eax
mov fs, eax
mov gs, eax

push esp       ;Push pointer to all the stuff we just pushed
call TaskSwitch ;Call C code

mov esp, eax   ;Replace the stack with what the C code gave us

pop gs         ;Put the data segments back
pop fs
pop es
pop ds

popa           ;Put the standard registers back

iret
;iret           ;Interrupt-Return


Both with an iret and just a ret both give me a GPF...
And I am not really shore where to go from here...

Thanks,
Rich


Top
 Profile  
 
 Post subject:
PostPosted: Sat Jun 23, 2007 4:13 pm 
Offline
Member
Member
User avatar

Joined: Wed Sep 28, 2005 11:00 pm
Posts: 85
You can't use "iret" in the code you've shown. The IRET instruction will pop eip, cs, and eflags from the stack, so the reason for the GPF is most likely due to invalid values.

EDIT: I've noticed that you do a 'push esp' before you call TaskSwitch, but you don't deallocate it after you've used it. Maybe that also has something to do with the problem?


Top
 Profile  
 
 Post subject:
PostPosted: Sat Jun 23, 2007 4:36 pm 
Offline
Member
Member
User avatar

Joined: Sat Apr 21, 2007 7:21 pm
Posts: 127
The stack is properly formatted with the right values.
Also TaskSwitch returns a new esp for the thread its about to start.

-Rich


Top
 Profile  
 
 Post subject:
PostPosted: Sat Jun 23, 2007 5:21 pm 
Offline
Member
Member
User avatar

Joined: Sat Jan 15, 2005 12:00 am
Posts: 8561
Location: At his keyboard!
Hi,

All code that follows your task switch function must be identical for all callers.

This means that in your IRQ handler, this code:

Code:
    mov esp, eax   ;Replace the stack with what the C code gave us
    mov al, 0x20   ;Port number AND command number to Acknowledge IRQ
    out 0x20, al     ;Acknowledge IRQ, so we keep getting interrupts
    pop gs         ;Put the data segments back
    pop fs
    pop es
    pop ds
    popa           ;Put the standard registers back
    iret           ;Interrupt-Return


Must be identical to the following code from your "yeild()" :

Code:
    mov esp, eax   ;Replace the stack with what the C code gave us
    pop gs         ;Put the data segments back
    pop fs
    pop es
    pop ds
    popa           ;Put the standard registers back
    iret


This is because a task that enters one function might leave through a completely different function. For example, if TaskA is pre-empted by your timer IRQ and TaskB starts running, and if TaskB calls "YieldThread()" and switches back to TaskA, then taskA will return from "YieldThread()" and not from the IRQ handler. The opposite can also happen - a thread that calls yield could return from the timer IRQ handler.

Note: In this case I really do mean "identical" - that EOI in the timer IRQ handler should be before the call to the "TaskSwitch()" function, not after.

In addition, the way you return must match the way you call the code. You can't do "call YieldThread" and then return with an IRET, and an IRQ handler can't try to leave with a RET.

To fix this I'd change ESP in the "TaskSwitch()" function, because the "TaskSwitch()" function always returns with a RET. I'd also make sure interrupts are disabled before calling the "TaskSwitch()" function.


Cheers,

Brendan

_________________
For all things; perfection is, and will always remain, impossible to achieve in practice. However; by striving for perfection we create things that are as perfect as practically possible. Let the pursuit of perfection be our guide.


Top
 Profile  
 
 Post subject:
PostPosted: Sat Jun 23, 2007 5:33 pm 
Offline
Member
Member
User avatar

Joined: Sat Apr 21, 2007 7:21 pm
Posts: 127
But thats the thing, my YieldThread code is a cut and paste of the ISR0 handler... the only difference is that I don't out 0x20,0x20.

The TaskSwitch Takes the Old Thread ESP0 and stores it. It then takes the next threads ESP0 and returns it.

Thats how the ISR0 works... it grabs all of the registers... and pushes them onto the stack. The task switcher switches the stack and returns a new stack and the values are then poped off...

However... for the life me I cannot get it to work in terms of yeilding... the actual ISR switching works great.

Also Ive been told 55x times that killing interrupts in a task switcher is a bad idea.

Thanks

-Rich


Top
 Profile  
 
 Post subject:
PostPosted: Sat Jun 23, 2007 7:31 pm 
Offline
Member
Member
User avatar

Joined: Tue Nov 09, 2004 12:00 am
Posts: 843
Location: United States
As Brendan said:
Brendan wrote:
Note: In this case I really do mean "identical" - that EOI in the timer IRQ handler should be before the call to the "TaskSwitch()" function, not after.


It would seem more natural for the two different code paths to converge, right? I mean a yield is essentially exactly the same as a thread switch. I would feel more comfortable knowing I only had to update one function instead of two.

Such that the code path from a hardware driven thread switch is from IRQ zero, while the yield is from another IRQ. And both call the same final function which would be to switch threads?

[HW Interrupt]-->[Timer]-->[Scheduler]-->[Thread Switch]
[SW Interrupt]-->[Scheduler]->[Thread Switch]

Trying to depict the different divisions the control flows through, while contrasting the two code paths converging.

If your thread switching code currently works then so should the yield.

_________________
Projects


Top
 Profile  
 
 Post subject:
PostPosted: Sat Jun 23, 2007 11:56 pm 
Offline
Member
Member
User avatar

Joined: Sat Jan 15, 2005 12:00 am
Posts: 8561
Location: At his keyboard!
Hi,

astrocrep wrote:
But thats the thing, my YieldThread code is a cut and paste of the ISR0 handler... the only difference is that I don't out 0x20,0x20.


Is your "yieldThread()" a software interrupt, or do you use "call yieldThread"? If it's not an interrupt handler, then that'd be another difference...

astrocrep wrote:
Also Ive been told 55x times that killing interrupts in a task switcher is a bad idea.


IMHO it's extremely unlikely that your task switcher is fully re-entrant. If a task calls "YieldThread()", and while it's in the middle of running "TaskSwitch()" it's preempted by the timer IRQ, what happens? When you're only doing one task switch every N ms this won't happen and you won't get problems (even if "TaskSwitch()" isn't re-entrant at all, because you can't get 2 or more timer IRQs at the same time), but when other code starts causing task switches too...

So, how do you make "TaskSwitch()" fully re-entrant? You can't use a spinlock or a mutex, as that would just cause deadlocks. There are ways around this though (e.g. completely skip the task switch if some other code is already in the middle of doing a task switch), however even then it can be very "fragile"....

Basically, during the task switch there's no single point in time where the task switch occurs. For e.g. your code might change a "currentTaskID" variable, then load a new CR3, then save FPU/MMX/SSE state, then load new FPU/MMX/SSE state, then load a new stack, then load general registers, etc. An IRQ can occur at any point while your switching from one task to another. Because of this all IRQ handlers would need to handle the case where half the CPU's state is from the old thread and half the CPU's state is for the new thread. It's very easy to forget this while writing an IRQ handler and end up with code that works 99.9% of the time. Trust me, this is much much worse than code that doesn't work at all - for the other 0.1% of the time other unrelated tasks might crash randomly due to the exact timing of the IRQ/s and other things, and when it does you'll typically have no way to figure out which piece/s of code caused the problem, and also won't be sure if anything you change actually fixed it or not.

I completely agree that disabling interrupts for a relatively long period of time is a bad idea. If you make your "TaskSwitch()" fast, then you wouldn't be disabling interrupts for a relatively long period of time, and therefore it wouldn't be a bad idea... :)


Cheers,

Brendan

_________________
For all things; perfection is, and will always remain, impossible to achieve in practice. However; by striving for perfection we create things that are as perfect as practically possible. Let the pursuit of perfection be our guide.


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 7 posts ] 

All times are UTC - 6 hours


Who is online

Users browsing this forum: Bing [Bot] and 164 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group