赞
踩
对应书P409 9.2节
进程有资源即页表,进程的所有线程公用进程的页表
这次试验创造的是一个内核线程,所以它使用的内存空间是内核内存池,1MB到2MB的页目录和页表。
整个流程如下:
定义了PCB结构体,中断栈结构体(本次实验只是留了个位置,可不管),线程栈结构体
其中,PCB中重要的成员变量便是线程名,线程栈指针
线程栈结构体重要的成员变量便是线程需要执行的函数的指针。
因为,如果切换到该线程,线程应该知道要执行哪个函数
#ifndef __THREAD_THREAD_H #define __THREAD_THREAD_H #include "stdint.h" typedef void thread_func(void*); /* 进程或线程的状态 */ enum task_status { TASK_RUNNING, TASK_READY, TASK_BLOCKED, TASK_WAITING, TASK_HANGING, TASK_DIED }; /*********** 中断栈intr_stack *********** * 此结构用于中断发生时保护程序(线程或进程)的上下文环境: * 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文 * 寄存器, intr_exit中的出栈操作是此结构的逆操作 * 此栈在线程自己的内核栈中位置固定,所在页的最顶端 ********************************************/ struct intr_stack { uint32_t vec_no; // kernel.S 宏VECTOR中push %1压入的中断号 uint32_t edi; uint32_t esi; uint32_t ebp; uint32_t esp_dummy; // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略 uint32_t ebx; uint32_t edx; uint32_t ecx; uint32_t eax; uint32_t gs; uint32_t fs; uint32_t es; uint32_t ds; /* 以下由cpu从低特权级进入高特权级时压入 */ uint32_t err_code; // err_code会被压入在eip之后 void (*eip) (void); uint32_t cs; uint32_t eflags; void* esp; uint32_t ss; }; /*********** 线程栈thread_stack *********** * 线程自己的栈,用于存储线程中待执行的函数 * 此结构在线程自己的内核栈中位置不固定, * 用在switch_to时保存线程环境。 * 实际位置取决于实际运行情况。 ******************************************/ struct thread_stack { uint32_t ebp; uint32_t ebx; uint32_t edi; uint32_t esi; /* 线程第一次执行时,eip指向待调用的函数kernel_thread 其它时候,eip是指向switch_to的返回地址*/ void (*eip) (thread_func* func, void* func_arg); /***** 以下仅供第一次被调度上cpu时使用 ****/ /* 参数unused_ret只为占位置充数为返回地址 */ void (*unused_retaddr); thread_func* function; // 由Kernel_thread所调用的函数名 void* func_arg; // 由Kernel_thread所调用的函数所需的参数 }; /* 进程或线程的pcb,程序控制块 */ struct task_struct { uint32_t* self_kstack; // 各内核线程都用自己的内核栈 enum task_status status; char name[16]; uint8_t priority; uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出 }; void thread_create(struct task_struct* pthread, thread_func function, void* func_arg); void init_thread(struct task_struct* pthread, char* name, int prio); struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg); #endif
intr_stack:中断栈,cpu执行中断入口程序时就按此结构顺序压栈保护上下文,位于PCB所在页最高地址。
thread_stack:线程栈,用于保存待运行的函数,地址紧接着中断栈
task_struct:PCB结构
#include "thread.h" #include "stdint.h" #include "string.h" #include "global.h" #include "memory.h" /* 由kernel_thread去执行function(func_arg) */ static void kernel_thread(thread_func* function, void* func_arg) { function(func_arg); } /* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */ void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) { /* 先预留中断使用栈的空间,可见thread.h中定义的结构 */ pthread->self_kstack -= sizeof(struct intr_stack); /* 再留出线程栈空间,可见thread.h中定义 */ pthread->self_kstack -= sizeof(struct thread_stack); struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; kthread_stack->eip = kernel_thread; kthread_stack->function = function; kthread_stack->func_arg = func_arg; kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0; } /* 初始化线程基本信息 */ void init_thread(struct task_struct* pthread, char* name, int prio) { memset(pthread, 0, sizeof(*pthread)); strcpy(pthread->name, name); pthread->status = TASK_RUNNING; /* self_kstack是线程自己在内核态下使用的栈顶地址 */ pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); pthread->priority = prio; pthread->stack_magic = 0x19870916; // 自定义的魔数 } /* 创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg) */ struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) { /* pcb都位于内核空间,包括用户进程的pcb也是在内核空间 */ struct task_struct* thread = get_kernel_pages(1); init_thread(thread, name, prio); thread_create(thread, function, func_arg); asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; \ ret ": : "g" (thread->self_kstack) :"memory"); return thread; }
thread_start:在内核池中申请了一页物理页来用作PCB,然后调用了 init_thread将PCB里的成员变量初始化,其中self_kstack保存了PCB所在页最高地址。之后调用了thread_create,在最高地址处预留了中断栈的空间,然后又填写了线程栈结构体中的成员变量,eip被赋值为kernel_thread,最后一句ret指令执行kernel_thread函数,从而执行function

其中最难理解的无疑是下面这句内联汇编,要弄懂,就必须结合线程栈结构
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; \ ret ": : "g" (thread->self_kstack) :"memory"); struct thread_stack { uint32_t ebp; uint32_t ebx; uint32_t edi; uint32_t esi; /* 线程第一次执行时,eip指向待调用的函数kernel_thread 其它时候,eip是指向switch_to的返回地址*/ void (*eip) (thread_func* func, void* func_arg); /***** 以下仅供第一次被调度上cpu时使用 ****/ /* 参数unused_ret只为占位置充数为返回地址 */ void (*unused_retaddr); thread_func* function; // 由Kernel_thread所调用的函数名 void* func_arg; // 由Kernel_thread所调用的函数所需的参数 };
首先,线程栈的成员变量从第一个ebp到最后一个func_arg它们的地址是从低到高,所以thread->self_kstack指向的是ebp;
其次,c语言和其他高级语言语言编译器不同,但是都遵守ABI规则,这个规则其中的一条就是,主调函数调用被调函数,被调函数一定要保存ebp、ebx、edi、esi、esp。只要都遵守这个规则,应用程序就可以在任何操作系统上运行。同时,我们已经学过,c语言遵循cdecl函数调用约定。
最后,schedule函数是c代码,switch_to是汇编代码,本次实验线程要运行的函数void k_thread_a(void* arg)也是c语言
综上,如果某线程是被调度上cpu,其必将经历c函数schedule到switch_to汇编代码(先push保存五个ABI寄存器,再POP出五个ABI寄存器,再ret)到c函数(该线程需要运行的函数,本次实验是k_thread_a)。
而本次实验是第一次创建线程,没有schedule,故之模拟了后半段,只POP了五个原本就被初始成0的五个寄存器,然后ret执行线程第一次创建该执行的函数kernel_thread。
第二个难理解的点就是,当ret弹出eip后,esp指向的是无意义的占位符。
eip被赋值成kernel_thread后,其实要执行的是一段c函数
/* 由kernel_thread去执行function(func_arg) */
static void kernel_thread(thread_func* function, void* func_arg) {
function(func_arg);
}
此时栈指针指向占位符,其实就是为了模拟c语言正常语法调用函数kernel_thread(k_thread_a, "argA ")后的栈情况,让该函数可以顺利找到传入的参数function,func_arg
这个时候就要能想起c语言遵循的cdecl函数调用约定
主调者从右向左压入参数,主调者回收栈空间
汇编程序调用c函数,一定要记得这个约定,汇编程序要从右往左依次push所有参数,然后调用结束后,还要记得add esp。本次实验本质就是在第一次创建线程的时候,从汇编依靠ret调用了c函数,之所以没有push参数,是因为线程栈成员变量的设定,已经构造好栈了
c语言的正常语法调用是kernel_thread(k_thread_a, "argA ")
翻译成汇编,要遵行从右向左传参
即push "argA " ; push k_thread_a; call kernel_thread,其中call kernel_thread又等价于 push 返回地址; jmp kernel_thread
此时栈情况

这显然和ret后的栈情况是一致的,只不过返回地址是无意义的占位符,这不影响c去找到正确的参数。
#include "print.h" #include "init.h" #include "thread.h" void k_thread_a(void* ); int main(void) { put_str("I am kernel\n"); init_all(); thread_start("k_thread_a", 31, k_thread_a, "argA "); while(1); return 0; } void k_thread_a(void* arg){ char* para = arg; while(1){ put_str(para); } }
thread_start最后执行k_thread_a函数,循环输出“argA”
$(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h lib/stdint.h \
kernel/global.h lib/string.h lib/stdint.h kernel/debug.h \
lib/kernel/print.h kernel/memory.h \
lib/kernel/bitmap.h thread/thread.h
$(CC) $(CFLAGS) $< -o $@
还要增加OBJS
$(BUILD_DIR)/thread.o
也要增加
LIB = -I thread/

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。