1,前言
开始之前我想说明一下,这系列的文章有什么意义?读书本就是一个输入到理解,再输出的过程。我不是简单的罗列书中的知识。写书的人为了兼顾跟多的读者,书中的铺垫信息过多。对我复习其实是干扰。我需要的是核心知识点的学习理解。所以这一系列。我只记录核心的重难点知识。简单概念只列关键词。
废话太多了,本章的难点应该是在线程的实现上。略过前的概念,直接到代码实现部分。
2,简单的PCB及线程栈的实现
1,数据结构
enum task_status;线程状态
struct intr_stack;中断栈
struct thread_stakc;线程栈
struct task_struct*;进程控制块PCB
这里只要记住这些名词就好了,那些什么ABI可以先不看。无非就是一个约定。后面介绍操作函数,再展开。不然脑容量就那么点,记不住这么多的。
2,线程实现函数
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg)
{
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;
}
首先,我们要获取一个物理页存放PCB,所以thread就指向了物理页的低端地址,比如0xc009e000;注意PCB大小至少为1页,4KB。图示如下左图。
成功获取到物理空间后,对该PCB页初始化。故在初始化函数里完成该物理页的清空操作,将部分信息填入pcb控制块中,如进程的状态,优先级,最关键的是将self_kstack指向了PCB的栈顶端,即0xc009f00的位置。图示如上右图。
接下来调用线程创建函数。
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
/* 先预留 中断栈 的空间,*/
pthread->self_kstack -= sizeof(struct intr_stack);
/* 再留出线程栈空间 */
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;
}
首先在PCB中挪了两块空地分配给中断栈,线程栈。此时pcb中的self栈指针位置应该停留在线程栈的最低地址处。骚操作来了,程序里这时候把线程栈的最低地址赋值给了线程栈变量ktread_stack.由于变量是结构体变量,所以后面的内存数据分布是和结构体定义的顺序有关的。图解如下。
线程栈的内存空间分布是依据结构体定义顺序的。ktread_stack是指向了该结构体变量的起始地址,所以可以正确对结构体成员赋值。最下面几个被赋值0,初始化嘛。eip被赋值指向kernel_thread.然后上面空4字节继续func,和arg.
这里是在干什么?有种似曾相识的感觉嘛?这里是在线程栈中构造函数调用时栈机制。
我们知道调用函数时,会从右到左将参数入栈,最后将返回地址也入栈后,进入被调用函数里。
所以这里func和arg就是函数kernel_thread的参数1和参数2.这里被空的unused其实就是调用kernel_thread函数的返回地址。但是这里没有写返回地址。看来不需要返回。
继续分析thread_start函数,只剩下一个内联汇编语句了。
asm volatile ("movl %0,%%esp;\
pop %%ebp;\
pop %%ebx;\
pop %%edi;\
pop %%esi;\
ret"\
::"g"(thread->self_kstack):"memory");
啥意思呢,就是将thread->self_kstack中的指向地址赋值给esp寄存器。上面初始化的时候thread->self_kstack挪了两个结构体空间后就没有再移动过。所以thread->self_kstack还是指向了线程栈的起始位置,即在存放ebp数据的位置(地址)处。所以这里就是将esp寄存器指向了线程栈。然后一次把数据弹回到ebp,ebx等等寄存器。可以看出数据与寄存器是对应关系。
弹到esi寄存后,不弹了。要执行ret指令了。在函数调用中,执行ret指令的本质就是将栈顶数据赋值到eip指令寄存器中。从而实现执行流的跳转。此时的栈顶正好指向了线程栈的eip位置。且在初始化时,我们对eip的赋值语句是:
kthread_stack->eip = kernel_thread;
即kernel_thread本质上是返回函数。
然后根据调用规则,kernel_thread函数两个参数从栈+4和+8的位置获取到。因为从CPU的角度来说,kernel_thread函数被调用执行,那栈顶必须是返回地址,这也就是我们在线程栈中的占位符的含义,栈顶+4才是参数1function,栈顶+8是参数2func_arg。
不常规的地方就是这个返回地址,竟然不写。那咋回去?答:不回去。回去是调度器的事情。
static void kernel_thread(thread_func* function, void* func_arg)
{
function(func_arg);
}
3,使用thread_start函数
上面说的function和func_arg其实就是线程函数及其参数嘛。
//函数原型
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);
//实际调用
thread_start("k_thread_a",31,k_thread_a,"argA ");
//线程函数实现
void k_thread_a(void * arg )
{
char * para=arg;
while (1)
{
console_put_str(para);
}
}
效果就是界面上不断打印 argA