《操作系统真象还原》第九章 在内核空间实现线程

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值