# 操作系统真象还原实验记录之实验十四:实现内核线程

操作系统真象还原实验记录之实验十四:实现内核线程

对应书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/

3.实验结果

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值