来看Main中的下一个函数Init_Scheduler
Init_Scheduler函数开启了系统的多任务机制。
位于./src/geekos/kthread.c
void Init_Scheduler(void) { struct Kernel_Thread* mainThread = (struct Kernel_Thread *) KERN_THREAD_OBJ; /* * Create initial kernel thread context object and stack, * and make them current. */ Init_Thread(mainThread, (void *) KERN_STACK, PRIORITY_NORMAL, true); g_currentThread = mainThread; Add_To_Back_Of_All_Thread_List(&s_allThreadList, mainThread); /* * Create the idle thread. */ /*Print("starting idle thread\n");*/ Start_Kernel_Thread(Idle, 0, PRIORITY_IDLE, true); /* * Create the reaper thread. */ /*Print("starting reaper thread\n");*/ Start_Kernel_Thread(Reaper, 0, PRIORITY_NORMAL, true); }
/* * Kernel thread context data structure. * NOTE: there is assembly code in lowlevel.asm that depends * on the offsets of the fields in this struct, so if you change * the layout, make sure everything gets updated. */ struct Kernel_Thread { ulong_t esp; /* offset 0 */ volatile ulong_t numTicks; /* offset 4 */ int priority; DEFINE_LINK(Thread_Queue, Kernel_Thread); void* stackPage; struct User_Context* userContext; struct Kernel_Thread* owner; int refCount; /* These fields are used to implement the Join() function */ bool alive; struct Thread_Queue joinQueue; int exitCode; /* The kernel thread id; also used as process id */ int pid; /* Link fields for list of all threads in the system. */ DEFINE_LINK(All_Thread_List, Kernel_Thread); /* Array of MAX_TLOCAL_KEYS pointers to thread-local data. */ #define MAX_TLOCAL_KEYS 128 const void* tlocalData[MAX_TLOCAL_KEYS]; };
看Init_Scheduler中的下一句Init_Thread
位于./src/geekos/kthread.c
/* * Initialize a new Kernel_Thread. */ static void Init_Thread(struct Kernel_Thread* kthread, void* stackPage, int priority, bool detached) { static int nextFreePid = 1; struct Kernel_Thread* owner = detached ? (struct Kernel_Thread*)0 : g_currentThread; memset(kthread, '\0', sizeof(*kthread)); kthread->stackPage = stackPage; kthread->esp = ((ulong_t) kthread->stackPage) + PAGE_SIZE;//指向进程堆栈底部的指针 kthread->numTicks = 0; kthread->priority = priority; kthread->userContext = 0; kthread->owner = owner; /* * The thread has an implicit self-reference. * If the thread is not detached, then its owner * also has a reference to it. */ kthread->refCount = detached ? 1 : 2; kthread->alive = true; Clear_Thread_Queue(&kthread->joinQueue); kthread->pid = nextFreePid++; }
下一句
g_currentThread = mainThread
把mainThread赋给全局变量g_currentThread,表示此线程当前在运行
Add_To_Back_Of_All_Thread_List(&s_allThreadList, mainThread)将此结构体链入到结构体链表中
再看下一句
Start_Kernel_Thread(Idle, 0, PRIORITY_IDLE, true)
和Start_Kernel_Thread(Reaper, 0, PRIORITY_NORMAL, true)
启动了Idle和Reaper两个线程,这两个内核线程会在后面详细说明。
Start_Kernel_Thread位于./src/geekos/kthread.c
/* * Start a kernel-mode-only thread, using given function as its body * and passing given argument as its parameter. Returns pointer * to the new thread if successful, null otherwise. * * startFunc - is the function to be called by the new thread * arg - is a paramter to pass to the new function * priority - the priority of this thread (use PRIORITY_NORMAL) for * most things * detached - use false for kernel threads */ struct Kernel_Thread* Start_Kernel_Thread( Thread_Start_Func startFunc, ulong_t arg, int priority, bool detached ) { struct Kernel_Thread* kthread = Create_Thread(priority, detached); if (kthread != 0) { /* * Create the initial context for the thread to make * it schedulable. */ Setup_Kernel_Thread(kthread, startFunc, arg); /* Atomically put the thread on the run queue. */ Make_Runnable_Atomic(kthread); } return kthread; }
Create_Thread用来为线程分配栈空间,初始化线程结构;Setup_Kernel_Thread用来将此线程的上下文压入堆栈中,压入的上下文是一些初始值;
Make_Runnable_Atomic用来将此线程链入到运行队列中。
先看Create_Thread函数
位于/src/geekos/kthread.c
/* * Create a new raw thread object. * Returns a null pointer if there isn't enough memory. */ static struct Kernel_Thread* Create_Thread(int priority, bool detached) { struct Kernel_Thread* kthread; void* stackPage = 0; /* * For now, just allocate one page each for the thread context * object and the thread's stack. */ kthread = Alloc_Page(); if (kthread != 0) stackPage = Alloc_Page(); /* Make sure that the memory allocations succeeded. */ if (kthread == 0) return 0; if (stackPage == 0) { Free_Page(kthread); return 0; } /*Print("New thread @ %x, stack @ %x\n", kthread, stackPage); */ /* * Initialize the stack pointer of the new thread * and accounting info */ Init_Thread(kthread, stackPage, priority, detached); /* Add to the list of all threads in the system. */ Add_To_Back_Of_All_Thread_List(&s_allThreadList, kthread); return kthread; }
Create_Thread函数首先用Alloc_Page分配了一个页的线程结构体和一个页的堆栈,这两个物理页就是一个线程的家当了。
Alloc_Page会在系统可用页链表中搜索一个可用页,然后返回这个页的首地址。
然后调用Init_Thread初始化线程,并将此线程结构链入到链表s_allThreadList中,s_allThreadList是系统中所有线程的链表,包括可运行的线程和休眠线程。Init_Thread在前面初始化mainthread时已经看到了。
/* * Set up the initial context for a kernel-mode-only thread. */ static void Setup_Kernel_Thread( struct Kernel_Thread* kthread, Thread_Start_Func startFunc, ulong_t arg) { /* * Push the argument to the thread start function, and the * return address (the Shutdown_Thread function, so the thread will * go away cleanly when the start function returns). */ Push(kthread, arg); Push(kthread, (ulong_t) &Shutdown_Thread); /* Push the address of the start function. */ Push(kthread, (ulong_t) startFunc);//用户指定的函数 /* * To make the thread schedulable, we need to make it look * like it was suspended by an interrupt. This means pushing * an "eflags, cs, eip" sequence onto the stack, * as well as int num, error code, saved registers, etc. */ /* * The EFLAGS register will have all bits clear. * The important constraint is that we want to have the IF * bit clear, so that interrupts are disabled when the * thread starts. */ Push(kthread, 0UL); /* EFLAGS */ /* * As the "return address" specifying where the new thread will * start executing, use the Launch_Thread() function. */ Push(kthread, KERNEL_CS); Push(kthread, (ulong_t) &Launch_Thread);//线程被初次调度运行时,首先运行的是Launch_Thread函数 /* Push fake error code and interrupt number. */ Push(kthread, 0); Push(kthread, 0); /* Push initial values for general-purpose registers. */ Push_General_Registers(kthread);//首次这里压入的通用寄存器值全为0 /* * Push values for saved segment registers. * Only the ds and es registers will contain valid selectors. * The fs and gs registers are not used by any instruction * generated by gcc. */ Push(kthread, KERNEL_DS); /* ds */ Push(kthread, KERNEL_DS); /* es */ Push(kthread, 0); /* fs */ Push(kthread, 0); /* gs */ }
可以看到Setup_Kernel_Thread向堆栈中压入了线程的个方面的信息,包括线程参数,线程启动函数和退出函数,各寄存器信息等。
注意这里入栈的参数先后顺序是事先约定的。
Push(kthread,0UL)开始到最后这些压入的参数是用于还原现场的。
极其要注意首先入栈的那几个参数,这是理解线程执行的关键。
当切换到线程的时候,先还原上下文,然后跳到Launch_Thread去执行,Launch_Thread执行完后,最后一句ret指令,系统从栈中弹出函数地址startFunc继续执行,
startFunc执行完后,最后一句汇编ret再弹出函数Shutdown_Thread结束线程。
怎么理解这种用压入函数地址来调用函数的方式呢。
可以用如下代码来理解
... call Shutdown_Thread ... Shutdown_Thread: call startFunc ... ... ret startFunc: call Launch_Thread ... ... ret Launch_Thread ... ... ... ret
这样,栈中就会连续的压入Shutdown_Thread,startFunc,Launch_Thread的入口地址,依次执行Launch_Thread,startFunc,Shutdown_Thread,然后在ret中依次返回。
执行ret,CPU就会从栈中取出4B载入到eip中执行。
/* * Push a dword value on the stack of given thread. * We use this to set up some context for the thread before * we make it runnable. */ static __inline__ void Push(struct Kernel_Thread* kthread, ulong_t value) { kthread->esp -= 4; *((ulong_t *) kthread->esp) = value; }
回到Start_Kernel_Thread中看最后一句Make_Runnable_Atomic(kthread),将线程链路到可运行队列中,由于可运行队列是全局可见的,所以这里加了临界保护。
位于./src/geekos/kthread.c
/* * Atomically make a thread runnable. * Assumes interrupts are currently enabled. */ void Make_Runnable_Atomic(struct Kernel_Thread* kthread) { Disable_Interrupts(); Make_Runnable(kthread); Enable_Interrupts(); }
/* * Add given thread to the run queue, so that it * may be scheduled. Must be called with interrupts disabled! */ void Make_Runnable(struct Kernel_Thread* kthread) { KASSERT(!Interrupts_Enabled()); Enqueue_Thread(&s_runQueue, kthread); }
至此Init_Scheduler()函数结束。
可以看到它初始化了内核主线程,以及两个Idle和Reaper两个内核线程。
还有一些具体细节会在下次添加。
Idle是系统初始化时产生的线程,其优先级为PRIORITY_IDLE,一直位于系统的可运行队列中(因为是while(1)循环),是系统中最低的级别,也就是说,只有系统运行队列中没有其他的可运行进程了,才会调度Idle运行。
位于./src/geekos/kthread.c
/* * This is the body of the idle thread. Its job is to preserve * the invariant that a runnable thread always exists, * i.e., the run queue is never empty. */ static void Idle(ulong_t arg) { while (true) Yield(); }
/* * Voluntarily give up the CPU to another thread. * Does nothing if no other threads are ready to run. */ void Yield(void) { Disable_Interrupts(); Make_Runnable(g_currentThread); Schedule(); Enable_Interrupts(); }
/* * Add given thread to the run queue, so that it * may be scheduled. Must be called with interrupts disabled! */ void Make_Runnable(struct Kernel_Thread* kthread) { KASSERT(!Interrupts_Enabled()); Enqueue_Thread(&s_runQueue, kthread); }
/* * Queue of runnable threads. */ static struct Thread_Queue s_runQueue;
static __inline__ void Enqueue_Thread(struct Thread_Queue *queue, struct Kernel_Thread *kthread) { Add_To_Back_Of_Thread_Queue(queue, kthread); }
/* * Schedule a thread that is waiting to run. * Must be called with interrupts off! * The current thread should already have been placed * on whatever queue is appropriate (i.e., either the * run queue if it is still runnable, or a wait queue * if it is waiting for an event to occur). */ void Schedule(void) { struct Kernel_Thread* runnable; /* Make sure interrupts really are disabled */ KASSERT(!Interrupts_Enabled()); /* Preemption should not be disabled. */ KASSERT(!g_preemptionDisabled); /* Get next thread to run from the run queue */ runnable = Get_Next_Runnable(); /* * Activate the new thread, saving the context of the current thread. * Eventually, this thread will get re-activated and Switch_To_Thread() * will "return", and then Schedule() will return to wherever * it was called from. */ Switch_To_Thread(runnable); }
/* * Get the next runnable thread from the run queue. * This is the scheduler. */ struct Kernel_Thread* Get_Next_Runnable(void) { struct Kernel_Thread* best = 0; best = Find_Best(&s_runQueue); KASSERT(best != 0); Remove_Thread(&s_runQueue, best); /* * Print("Scheduling %x\n", best); */ return best; }
/* * Find the best (highest priority) thread in given * thread queue. Returns null if queue is empty. */ static __inline__ struct Kernel_Thread* Find_Best(struct Thread_Queue* queue) { /* Pick the highest priority thread */ struct Kernel_Thread *kthread = queue->head, *best = 0; while (kthread != 0) { if (best == 0 || kthread->priority > best->priority) best = kthread; kthread = Get_Next_In_Thread_Queue(kthread); } return best; }
static __inline__ void Remove_Thread(struct Thread_Queue *queue, struct Kernel_Thread *kthread) { Remove_From_Thread_Queue(queue, kthread); }
; ---------------------------------------------------------------------- ; Switch_To_Thread() ; Save context of currently executing thread, and activate ; the thread whose context object is passed as a parameter. ; ; Parameter: ; - ptr to Kernel_Thread whose state should be restored and made active ; ; Notes: ; Called with interrupts disabled. ; This must be kept up to date with definition of Kernel_Thread ; struct, in kthread.h. ; ---------------------------------------------------------------------- align 16 Switch_To_Thread://栈中存储着函数的第一个参数(欲运行的线程结构体) ; Modify the stack to allow a later return via an iret instruction. ; We start with a stack that looks like this: ; ; thread_ptr ; esp --> return addr ; ; We change it to look like this: ; ; thread_ptr ; eflags ; cs ; esp --> return addr //调整栈状态,因为将来系统会在其他的线程上下文中使用iret切换回来。iret需要从栈中弹出eflag、cs、return addr(线程运行地址) push eax ; save eax mov eax, [esp+4] ; get return address,得到返回地址存到eax中,也就是Schedule中Switch_To_Thread函数执行完后的下条指令地址 mov [esp-4], eax ; move return addr down 8 bytes from orig loc,将返回地址向下移8B add esp, 8 ; move stack ptr up pushfd ; put eflags where return address was mov eax, [esp-4] ; restore saved value of eax push dword KERNEL_CS ; push cs selector sub esp, 4 ; point stack ptr at return address,这句运行结束后,堆栈状态就是上面的图示。这里压入的线程参数会在下次 ; Push fake error code and interrupt number,压入错误码,和中断向量,这里是线程切换,无需理会中断向量,所以中断向量设为0即可。 push dword 0 push dword 0 ; Save general purpose registers. Save_Registers ; Save stack pointer in the thread context struct (at offset 0).g_currentThread指向当前执行的线程,这里就是得到Idle这个线程的结构体指针,并把esp赋给Idle线程结构体 mov eax, [g_currentThread] mov [eax+0], esp ; Clear numTicks field in thread context, since this ; thread is being suspended.Idle线程结构体中的numTicks变量置0,因为Idle将被挂起 mov [eax+4], dword 0 ; Load the pointer to the new thread context into eax. ; We skip over the Interrupt_State struct on the stack to ; get the parameter.将要运行的线程的结构体指针赋给eax mov eax, [esp+INTERRUPT_STATE_SIZE] ; Make the new thread current, and switch to its stack.将线程设置为当前运行线程,并切换esp到新的堆栈。 mov [g_currentThread], eax mov esp, [eax+0] ; Restore general purpose and segment registers, and clear interrupt ; number and error code.从新的esp栈中弹出数据,在这里还原新进程的上下文。 Restore_Registers ; We'll return to the place where the thread was ; executing last.切换到新进程了,这里是极其核心,极其关键的一步!!!注意iret和ret的区别,iret还还原了寄存器cs和eflag。 iret
/* * This struct reflects the contents of the stack when * a C interrupt handler function is called. * It must be kept up to date with the code in "lowlevel.asm". */ struct Interrupt_State { /* * The register contents at the time of the exception. * We save these explicitly. */ uint_t gs; uint_t fs; uint_t es; uint_t ds; uint_t ebp; uint_t edi; uint_t esi; uint_t edx; uint_t ecx; uint_t ebx; uint_t eax; /* * We explicitly push the interrupt number. * This makes it easy for the handler function to determine * which interrupt occurred. */ uint_t intNum; /* * This may be pushed by the processor; if not, we push * a dummy error code, so the stack layout is the same * for every type of interrupt. */ uint_t errorCode; /* These are always pushed on the stack by the processor. */ uint_t eip; uint_t cs; uint_t eflags; };
至此Idle线程完毕。。
这里要说明一下这里的线程切换函数Switch_To_Thread和通用中断处理函数Handle_Interrupt的区别和联系。
都在./src/geekos/lowlevel.asm中
它们的共同点都是会将当前进程的上下文保存。
具体过程:
Handle_Interrupt过程:
保护被中断进程的上下文---->从栈中取出中断向量---->根据中断向量取出中断处理函数地址并调用中断处理函数---->中断处理函数结束,返回到Handle_Interrupt---->判断是否需要调度新进程---->需要则调度新进程,调用Make_Runnable得到新进程的上下文并恢复执行;不需要调度则恢复被中断进程的上下文,继续执行被中断进程。
Switch_To_Thread过程:
保护当前进程的上下文---->从栈中取出欲运行进程的结构体---->切换到欲运行进程堆栈,恢复其上下文---->跳到欲运行进程执行。
Handle_Interrupt中使用了三个系统全局变量:g_currentThread、g_preemptionDisabled、g_needReschedule。
Switch_To_Thread中只使用了一个系统全局变量:g_currentThread
Handle_Interrupt是由于外部中断触发,有特权级的变换。
Switch_To_Thread是在某任务A中调用函数Schedule触发,无特权级变换。
再来看另外一个内核线程Reaper,Reaper线程用于回收进程退出时的内存。
位于/src/geekos/kthread.c中
/* * The reaper thread. Its job is to de-allocate memory * used by threads which have finished. */ static void Reaper(ulong_t arg) { struct Kernel_Thread *kthread; Disable_Interrupts(); while (true) { /* See if there are any threads needing disposal. s_graveyardQueue是等待回收内存的队列*/ if ((kthread = s_graveyardQueue.head) == 0) { /* Graveyard is empty, so wait for a thread to die. */ Wait(&s_reaperWaitQueue); } else { /* Make the graveyard queue empty.清空队列,下面要回收队列中的所有内存 */ Clear_Thread_Queue(&s_graveyardQueue); /* * Now we can re-enable interrupts, since we * have removed all the threads needing disposal. */ Enable_Interrupts(); Yield(); /* allow other threads to run? 因为Reaper线程永远不会退出,所以要给其他线程运行的机会,这里没有Yield的话系统就死机了*/ /* Dispose of the dead threads.遍历整个链表 */ while (kthread != 0) { struct Kernel_Thread* next = Get_Next_In_Thread_Queue(kthread); #if 0 Print("Reaper: disposing of thread @ %x, stack @ %x\n", kthread, kthread->stackPage); #endif Destroy_Thread(kthread);//清除线程的栈空间,以及线程结构体,将线程从系统所有线程队列中删除 kthread = next; } /* * Disable interrupts again, since we're going to * do another iteration. */ Disable_Interrupts(); } } }
/* * Wait on given wait queue. * Must be called with interrupts disabled! * Note that the function will return with interrupts * disabled. This is desirable, because it allows us to * atomically test a condition that can be affected by an interrupt * and wait for it to be satisfied (if necessary). * See the Wait_For_Key() function in keyboard.c * for an example. */ void Wait(struct Thread_Queue* waitQueue) { struct Kernel_Thread* current = g_currentThread; KASSERT(!Interrupts_Enabled()); /* Add the thread to the wait queue. */ Enqueue_Thread(waitQueue, current); /* Find another thread to run. */ Schedule(); } /* * Wake up all threads waiting on given wait queue. * Must be called with interrupts disabled! * See Keyboard_Interrupt_Handler() function in keyboard.c * for an example. */ void Wake_Up(struct Thread_Queue* waitQueue) { struct Kernel_Thread *kthread = waitQueue->head, *next; KASSERT(!Interrupts_Enabled()); /* * Walk throught the list of threads in the wait queue, * transferring each one to the run queue. */ while (kthread != 0) { next = Get_Next_In_Thread_Queue(kthread); Make_Runnable(kthread); kthread = next; } /* The wait queue is now empty. */ Clear_Thread_Queue(waitQueue); }
线程进入s_graveyardQueue就是进入了墓地,等待着最终的躯壳的消散。
在Setup_Kernel_Thread中我们压入了线程退出函数Shutdown_Thread,线程运行完用户执行的函数时候,就会运行Shutdown_Thread
Shutdown_Thread内就是一个Exit(0)函数。
结合Exit(0)函数来看,就能明白Reaper线程的功能了。
/* * Exit the current thread. * Calling this function initiates a context switch. */ void Exit(int exitCode) { struct Kernel_Thread* current = g_currentThread; if (Interrupts_Enabled()) Disable_Interrupts(); /* Thread is dead */ current->exitCode = exitCode; current->alive = false; /* Clean up any thread-local memory */ Tlocal_Exit(g_currentThread); /* Notify the thread's owner, if any */ Wake_Up(¤t->joinQueue); /* Remove the thread's implicit reference to itself. */ Detach_Thread(g_currentThread); /* * Schedule a new thread. * Since the old thread wasn't placed on any * thread queue, it won't get scheduled again. */ Schedule(); /* Shouldn't get here */ KASSERT(false); }
/* * Hand given thread to the reaper for destruction. * Must be called with interrupts disabled! */ static void Reap_Thread(struct Kernel_Thread* kthread) { KASSERT(!Interrupts_Enabled()); Enqueue_Thread(&s_graveyardQueue, kthread); Wake_Up(&s_reaperWaitQueue); } /* * Called when a reference to the thread is broken. */ static void Detach_Thread(struct Kernel_Thread* kthread) { KASSERT(!Interrupts_Enabled()); KASSERT(kthread->refCount > 0); --kthread->refCount; if (kthread->refCount == 0) { Reap_Thread(kthread); } }