操作系统真象还原实验记录之实验十四:实现内核线程
对应书P409 9.2节
1.相关基础知识总结
进程有资源即页表,进程的所有线程公用进程的页表
这次试验创造的是一个内核线程,所以它使用的内存空间是内核内存池,1MB到2MB的页目录和页表。
整个流程如下:
定义了PCB结构体,中断栈结构体(本次实验只是留了个位置,可不管),线程栈结构体
其中,PCB中重要的成员变量便是线程名,线程栈指针
线程栈结构体重要的成员变量便是线程需要执行的函数的指针。
因为,如果切换到该线程,线程应该知道要执行哪个函数
2.实验代码
2.1thread.h
#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结构
2.2 thread.c
#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去找到正确的参数。
2.3 main.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”
2.4 makefile
$(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/