我们的基本思路如下:
由于每一个进程都有一个PCB,每一个线程都有一个TCB,所以我们这里首先应该创建的是线程的TCB和进程的PCB。我们这里为了简单,将二者创建的时候使用一个结构体,然后分别实现。
PCB/TCB如下:
struct task_struct
{
uint32_t* self_kstack; //线程自己在内核状态下使用的内核栈
enum task_status status; //线程或进程状态
uint8_t priority; //线程优先级
char name[16]; //线程名字
uint32_t stack_magic; //栈的边界标记,用于检测栈的溢出
}
这里enum task_status是定义进程或线程状态的一个枚举类型,如下:
enum task_status
{
TASK_RUNNING,TASK_READY,TASK_BLOCKED,
TASK_WAITING,TASK_HANGING,TASK_DIED
};
这里简单介绍一下用枚举类型的好处:
enum类型是强类型的,其对一个变量取值范围进行限定,花括号内就是它的取值范围,即我们的status只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或者警告。 其把一些运行期的参数检查放到了编译期,保证了系统的安全性。
在创建了PCB后,由于我们的PCB都是位于内核空间的,则我们需要相应的为其分配一个内存页:
调用函数:
struct task_struct* thread = get_kernel_page(1);
get_kernel_page函数详情可见 c语言实现OS内存分配一文
在有了PCB后,我们首先要来初始化我们的PCB
init_thread(thread,name,prio);
void init_thread(struct task_struct* pthread,char* name,int prio)
{
memset(pthread,0,sizeof(*pthread)); //将Pthread所在的PCB页清零
strcpy(pthread->name,name);
pthread->status = TASK_RUNNING;
pthread->priority = prio;
pthread->self_kstack = (uint32*)((uint32_t)pthread + PG_SIZE);
pthread->stack_magic = 0x123456 //自己定义的魔数
}
初始化我们线程的PCB之后,我们就要来创建线程啦,也就是实现我们的线程栈
我们先来看看线程栈的结构(就两个作用:1⃣️存放调用函数5个寄存器的值2⃣️存放要运行的函数)
//线程自己的栈,用来存放线程中待执行的函数
//该线程栈在线程自己的内核栈中位置不固定
struct thread_stack
{
//在i386体系中所有的寄存器具有全局性,所以在函数调用时,这些寄存器对主被调函数
都可见,其中ebp,ebx,edi,esi,esp这五个寄存器是归主函数所用,其余寄存器归被调函
数使用。所以在被调函数运行完后,这五个寄存器的值必须要和原来一样,所以其在运行的时候,要在自己的栈中存储这些寄存器的值。
//其中esp在函数调用时会自动进行保存,我们就不需要手动保存
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
//线程第一次运行的时候,eip指向待调用的函数,其他时候指向返回地址
void(*eip)(thread_func* func,void* func_arg);
void (*unused_retaddr); //充当返回地址,在返回地址所在的栈桢占个位置
thread_func* function; //线程所调用函数的函数名
void* func_arg; //线程所调用函数的函数参数
}
这里我们无法按照正常的函数调用形式传递线程调用函数所需的参数,只能将其放入栈中,原因见这里
这里书上是这样来实现的,创建线程就是来实现该线程的内核栈,其内核栈存放的是中断栈和线程栈(这里我的理解,及其模糊,希望有了解同学,发现如果我错了,烦请指点出来)
thread_create(thread,function,func_arg);
初始化线程栈thread_stack,将执行的函数和参数放到thread_stack中相应的位置
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg)
{
//由于pthread->self_kstack在初始化线程PCB的时候,就将其放在PCB最顶端位置,
现在最顶端为中断栈,所以要减去中断栈大小
pthread->self_kstack -= sizeof(struct intr_stack);
//预留线程栈的位置
pthread->self_kstack -= sizeof(struct thread_stack);
//创建线程栈指针,将pthread->self_kstack与其指向同一个地址
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;
}
中断栈的结构体
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;
};
创建线程之后,我们就要来启动我们的线程啦!!!
asm volatile ("movl %0 , %%esp; \
pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; \
ret" : : \
"g" (thread->self_kstack) \ //其作为输入,采用通用约束符g,即寄存器和内存都可以
: " memory ");
return thread;
参考书籍:《操作系统真相还原》9.2节