Code:
//This is a complete task switcher for ZCPU, based on the WireOS5 task switcher
//It uses internal ZCPU timer to call thread switch procedure, which changes
//current thread running on the ZCPU. This allows several programs to run at
//once on a single CPU.
//
//Features:
// Supports single-CPU systems only right now
// Every thread has own stack (with variable size)
// Threads can have different timesplices (higher timesplice -> more priority)
// Threads can GIVE BACK some of the timesplice (using "nmiint 32")
// Own set of registers for every thread (NOT DEPENDANT ON OTHER THREADS)
// You can pause thread by setting timesplice to 0
// Works FAST, damn fast. Not much slower than hardware multithreading
// Supports critical sections, but needs some work
//
//TODO:
// Error handling (threads CAN crash, right now it'll mess up everything)
// More thread API (fork, delete, suspend, pause, etc)
// Handle the case when ALL threads are gone (NumThreads == 0)
// Various bollocks associated with memory and stuff (its all possible)
// Multi-CPU support (requires shared RAM, a lot of shared RAM)
// You need to check state of IF (interrupt flag), because threads may already
// be created when IF = 0 (and STI in the end of Thread_Create could cause
// erratic behaviour)
// Setting stack size properly in the Thread_Create (it might fail if you create it with thread with small stack, even though that stack isn't used up)
//Go and initialize the threads
jmp Main;
//== Thread parameters =========================================================
//These arrays store various parameters about all the threads running
ThreadStackPointer: alloc 16;
ThreadStackOffset: alloc 16;
ThreadStackSize: alloc 16;
ThreadTimeSlice: alloc 16;
alloc CurrentThread; //Index of thread that currently executes
alloc NumThreads; //Total number of threads
//==============================================================================
// Interrupt table
InterruptTable: alloc 512;
//==============================================================================
// Thread switcher
ContextSwitch:
//0. Disable interrupts, so we dont get thread switching while we are thread switching
cli;
mov DS,0; //Set this so we can access thread list and stuff stuff quickly
mov LS,#CurrentThread; //DS/LS would be reset by NMIRET anyway (it pops/pushes all registers)
//Purpose:
//DS = 0: we set it to 0 so we can access all those variables, even if thread set DS to something else
//LS = #CurrentThread: ZCPU computes real address of memory value like this:
// Address = Segment + Address
// We can cheat fast array access like this:
// Address = #CurrentThread + ThreadStackPointer
//1. Store old stack pointer (because it might have been changed by the thread)
mov LS:#ThreadStackPointer,ESP;
//2. Move on to next thread
inc #CurrentThread; mod #CurrentThread,#NumThreads;
mov LS,#CurrentThread; //Don't forget to update our fast array access cheat...
//3. Perform context switch
mov SS,LS:#ThreadStackOffset; //Stack segment
mov ESP,LS:#ThreadStackPointer; //Stack pointer
cpuset 9,LS:#ThreadStackSize; //Stack size
cpuset 65,LS:#ThreadTimeSlice; //Time slice
//CPUSET sets some internal registers which don't have names:
//9 is ESZ (stack size)
//65 is TimerRate (how fast timer fires)
//4. Reset interrupt timer (gives proper amount of instructions to every thread)
cpuget EAX,29; //Read instruction counter
add EAX,4; //Account for 4 instructions that follow (add,cpuset,sti,nmiret)
cpuset 66,EAX; //Write last timer fire time
//If you remove "add EAX,4" it will give threads 4 instructions less than it should
//Alternatively you should know that changing timer mode also resets
//last fire time (but changing fire rate does not)
//5. Restore interrupts
sti;
nmiret;
//=============================================================================
//=============================================================================
// Create new thread in same address space as host thread
//
// EAX: ptr to thread
// EBX: ptr to stack
// ECX: stack size
// EDX: time slice
//
alloc IFState;
Thread_Create:
//Store current interrupt flag state, so we dont accidently enable
//interrupts when not wanted (they MUST be turned off for this function, but
//they dont have to be turned off outside of it)
cpuget #IFState,32;
//Clear interrupt flag state/enter critical section
cli;
//Store current stack state
push ESI; push EDI; push EBP;
mov ESI,SS; mov EDI,ESP;
//Initialize thread state
mov ESP,ECX; sub ESP,1; //Stack pointer on top of the stack (ESP = Size - 1)
mov SS,EBX;
//Push all of the registers to stack
//Thread state is stored in two ways:
// If thread is running:
// Thread state is stored in CPU registers
// If thread is standby:
// Thread state is stored on stack (as NMI return information)
//
//So we just push all registers as zeroes here
push 0;//LS
push 0;//KS
push 0;//ES
push 0;//GS
push 0;//FS
push 0;//DS
push 0;//SS <-- Dont worry about stack ones, they are force-changed by task switcher
push 0;//CS
push 0;//EDI
push 0;//ESI
push 0;//ESP
push 0;//EBP
push 0;//EDX
push 0;//ECX
push 0;//EBX
push 0;//EAX
push 0; //CMPR
push EAX; //IP
//These two are not used by NMI interrupt, but instead they are for INT return
push 0;
push 0;
//Setup thread list entry
mov EBP,ThreadStackPointer; add EBP,#NumThreads;
mov #EBP,ESP;
mov EBP,ThreadStackOffset; add EBP,#NumThreads;
mov #EBP,SS;
mov EBP,ThreadStackSize; add EBP,#NumThreads;
mov #EBP,ECX;
mov EBP,ThreadTimeSlice; add EBP,#NumThreads;
mov #EBP,EDX;
inc #NumThreads;
//Restore caller thread state
mov SS,ESI; mov ESP,EDI;
pop EBP; pop EDI; pop ESI;
cpuset 32,#IFState; //Critical section end
ret
Main:
cli;
//Setup interrupt handler
lidtr InterruptTable;
mov EBP,InterruptTable;
add EBP,128; //Interrupt 32
mov #EBP,ContextSwitch;
inc EBP;
mov #EBP,0; //CS
inc EBP;
mov #EBP,0; //reserved
inc EBP;
mov #EBP,96; //NMI + enabled flags
//Enable extended mode
stef;
//Initialize thread list
mov #CurrentThread,0;
mov #NumThreads,0;
//Create threads
mov EAX,thread1;
mov EBX,thread1_stack;
mov ECX,8192;
mov EDX,16;
call Thread_Create;
mov EAX,thread2;
mov EBX,thread2_stack;
mov ECX,8192;
mov EDX,16;
call Thread_Create;
mov EAX,thread3;
mov EBX,thread3_stack;
mov ECX,4096;
mov EDX,16;
call Thread_Create;
mov EAX,thread4;
mov EBX,thread4_stack;
mov ECX,2048;
mov EDX,8; //How about a small timesplice?
call Thread_Create;
//Initialize hardware timer (WAY #1 - TIMER register way)
// cpuset 65,0.10; //fire 10 times per second
// cpuset 64,1;
//You should NOT use way #1, and here is why:
//TIMER register updates only every CPU tick
//Time step of TIMER register = time step of server frames
//That value is limited to about 50hz. Your task switching will
//execute certain (MOST LIKELY DIFFERENT ALL THE TIME) amount of operations,
//which depends on frequency of the CPU. It'll also probably give some threads
//two of these timeslices, not one, in case you set interval close to timer error
//
//The second way, on the other hand, will switch threads independantly from CPU
//ticks. In fact, this is the way you give threads time slices based on an
//instruction counter.
//
//How timer works:
//It fires NMI interrupt at specific rate (every X seconds, or every N instructions)
//It's pretty much same as if you wired pulser to NMI input of the CPU
//You can set what NMI it calls, rate, and you can control when timer
//will fire next.
//Initialize hardware timer (WAY #2 - TMR register way)
cpuset 65,32; //Set some big value so timer doesn't trigger yet
cpuset 64,2;
//Don't worry, timer wont fire yet anyway, we dont have interrupts enabled yet
//Enable interrupts and force context switch to the first thread
//Standby threads are holding their full state on their stacks
//We just switch our stack (original stack given us by the ZCPU) to
//one of the thread stacks. It already holds full thread state there,
//as if NMI interrupt was called.
//
//If we now call NMIRET, ZCPU will restore thread state, and begin
//execution
mov ESP,#ThreadStackPointer;
mov SS,#ThreadStackOffset;
cpuset 9,#ThreadStackSize;
cpuset 65,#ThreadTimeSlice;
sti; //NOTE: interrupts may only happen TWO instructions after NMI
nmiret; //Just look at this:
//
// sti;
// <--- interrupt >>>NEVER<<< happens here
// mov eax,0;
// <--- interrupt MIGHT happen here
// mov ebx,0;
// <--- might happen here too
// cli;
// <--- will NEVER happen here
//== Thread 1 =================================================================
thread1:
mov eax,0; //threads have own registers
thread1_loop:
add eax,3; //3 instructions in this loop :)
mov port0,eax;
jmp thread1_loop;
thread1_stack: alloc 8192;
//=============================================================================
//== Thread 2 =================================================================
thread2:
mov port1,124812948;
mov eax,0;
thread2_loop:
add eax,3;
mov port1,eax;
//idle; <--- DONT do this, you SHOULD NOT have "idle" in any children threads, or it'll make shit run slow yo
jmp thread2_loop;
thread2_stack: alloc 8192;
//=============================================================================
//== Thread 3 =================================================================
thread3:
mov eax,0;
thread3_loop:
add eax,3;
mov port2,eax;
nmiint 32; //PRETTY MUCH SAME AS "IDLE" OPCODE FOR MULTITHREADED SYSTEMS
//GIVES BACK THREAD TIME SPLICE TO OS
//We just call task switching interrupt before the timer does
jmp thread3_loop;
thread3_stack: alloc 4096;
//=============================================================================
//== Thread 4 =================================================================
thread4:
mov eax,0;
thread4_loop:
add eax,3;
mov port3,eax;
jmp thread4_loop;
thread4_stack: alloc 4096;
//=============================================================================
//=============================================================================
// What will you see in the demo output (to run this you only need a data port)
//
// Thread 1 and thread 2 run pretty much at same rate
// Thread 3 will run very slowly, because it gives back most of its timesplice
// Thread 4 will run approx twice slower than threads 1 and 2 (it has smaller timesplice) (I've decided to create a set of "Hello I'm ...." tutorials. They are pretty much just a thread with commented code in it)
Bookmarks