手写操作系统(二十九)-实现内核线程

代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。

1.线程与进程

线程没有自己独享的地址空间,线程必须借助进程空间中的资源运行。

所以线程被称为轻量级进程

若显示创建了线程,则任务调度器会将它对应的代码块从进程中分离出来单独调度上CPU执行;否则调度器会将整个进程当作一个大的执行流,从头到尾去执行。

操作系统把进程(线程)执行过程中所经历的不同阶段归为几类:

  • 阻塞态:等待外界条件
  • 就绪态:外界条件就绪
  • 运行态:正在运行的进程

状态的转变由调度器负责,状态是描述线程的。

 

2.PCB 程序控制块

PCB,Process Control Block ,操作系统提供的PCB用来解决任务调度相关的问题,用它来记录与此进程相关的信息,比如进程状态、PID、优先级等。

PCB没有具体的格式,实际格式取决于操作系统。

一般PCB 结构如图所示:

每个进程都有自己的PCB,所有的PCB放到一张表格中来维护,就是进程表。

进程表如图所示:

 

3.实现线程的两种方式

实现线程有两种方式:在用户空间实现线程或者在内核空间实现线程

在用户空间实现线程:可移植性强,对处理器来说,会进行进程级别的调度,无法精确到进程中自己实现的具体线程中去
在内核空间实现线程:可以以线程级别来调度执行流,效率更高

线程仅仅是个执行流,在哪里实现取决于线程表在哪里,由谁来调度它上CPU。如果线程在用户空间中实现,则线程表在用户进程中,用户进程就要专门写个线程用作线程调度器;如果线程是在内核空间中实现的,线程表在内核中,该线程就会由OS的调度器统一调度。

如果是程序内实现线程,那处理器调度任务的时候以进程为单位进行,一个进程分配的时间片还是那么多

如果是内核里实现线程,这处理器调度任务的时候以线程为单位进行,一个进程内如果有多个线程,则这个进程占用的时间片会多一些,达到提速的效果

这里我们选择在内核里实现线程

 

4.代码实现

下面咱们先构造 PCB 及其相关的基础部分

thread.h

/thread/thread.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.asm 宏 VECTOR 中 %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 {
    // ABI 规定
    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时使用   ****/
    void (*unused_retaddr);     // unused_ret 只为占位置充数为返回地址, 这里活用ret指令, ret指令是先将栈中地址恢复到 eip, 然后跳转过去, 实际上eip被我们操纵, 所以栈中地址无所谓是啥, eip会被我们修改的
    thread_func* function;      // 由 kernel_thread 所调用的函数名, 线程中执行的函数
    void* func_arg;             // 由 kernel_thread 所调用的函数所需的参数
};


/* 进程或线程的 pcb, 程序控制块 */
struct task_struct {
    uint32_t* self_kstack;      // 各内核线程都用自己的内核栈
    enum task_status status;    // 线程状态
    uint8_t priority;           // 线程优先级
    char name[16];
    uint32_t stack_magic;       // 栈的边界标记, 用于检测栈的溢出
};

struct task_struct* running_thread(void);

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

代码解释:

用typedef定义thread_func它用来指定在线程中运行的函数类型。

/* 自定义通用函数类型, 它将在很多线程函数中做为形参类型 */
typedef void thread_func(void*);

我们在线程中打算运行某段代码(函数)时,需要一个参数来接收该函数的地址,因此这里先定义这个返回值void的函数类型,以后在介绍其他函数实现时大家会多次见到它。

 

接下来用 enum task_status 结构定义了线程和进程的状态,进程与线程的区别是它们是否独自拥有地址空间,也就是是否拥有页表,程序的状态都是通用的,因此enum task_status结构同样也是进程的状态。

/* 进程或线程的状态 */
enum task_status {
    TASK_RUNNING,
    TASK_READY,
    TASK_BLOCKED,
    TASK_WAITING,
    TASK_HANGING,
    TASK_DIED
};

这里先定义了6个状态。

 

接下来用struct intr_stack定义了程序的中断栈,无论是进程,还是线程,此结构用于中断发生时保护程序的上下文环境。

struct intr_stack {
    uint32_t vec_no;        // kernel.asm 宏 VECTOR 中 %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;
};

进入中断后,在kernel.S中的中断入口程序“intr%lentry”所执行的上下文保护的一系列压栈操作都是压入了此结构中。

因此,进程或线程被外部中断或软中断打断时,中断入口程序会按照此结构压入上下文寄存器,所以, kernel.S中intr_exit中的出栈操作便是此结构的逆操作。初始情况下此栈在线程自己的内核栈中位置固定,在PCB所在页的最顶端,每次进入中断时就不一定了,如果进入中断时不涉及到特权级变化,它的位置就会在当前的esp之下,否则处理器会从TSS中获得新的esp的值,然后该栈在新的esp之下。

 

接下来是结构体 struct thread_stack定义了线程栈。

struct thread_stack {
    // ABI 规定
    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时使用   ****/
    void (*unused_retaddr);     // unused_ret 只为占位置充数为返回地址, 这里活用ret指令, ret指令是先将栈中地址恢复到 eip, 然后跳转过去, 实际上eip被我们操纵, 所以栈中地址无所谓是啥, eip会被我们修改的
    thread_func* function;      // 由 kernel_thread 所调用的函数名, 线程中执行的函数
    void* func_arg;             // 由 kernel_thread 所调用的函数所需的参数
};

此栈有 2个作用,主要就是体现在第5个成员eip上。线程是使函数单独上处理器运行的机制,因此线程肯定得知道要运行哪个函数,首次执行某个函数时,这个栈就用来保存待运行的函数,其中eip便是该函数的地址。将来用switch_to函数实现任务切换,当任务切换时,此eip用于保存任务切换后的新任务的返回地址。

总结就是:

  • 首次运行时,eip用来保存待运行的函数的地址
  • 切换任务时,eip用来保存任务切换后的新任务的返回地址

前4个成员是ABI(程序二进制接口)的规定,在函数调用前后这几个寄存器的值不能改变,

// ABI 规定
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;

ABI是Application Binary Interface,即应用程序二进制接口,只要操作系统和应用程序都遵守同一套ABI规则,编译好的应用程序可以无需修改直接在另一套操作系统上运行。

位于Intel386硬件体系上的所有寄存器都具有全局性,因此在函数调用时,这些寄存器对主调函数和被调函数都可见。这5个寄存器ebp、ebx、edi、 esi、和esp归主调函数所用,其余的寄存器归被调函数所用,不管被调函数中是否使用了这5个寄存器,在被调函数执行完后,这5个寄存器的值不该被改变。因此被调函数必须为主调函数保护好这5个寄存器的值,在被调函数运行完之后,这5个寄存器的值必须和运行前一样,它必须在自己的栈中存储这些寄存器的值。

如果要自己手动写汇编函数,并且此函数要供C语言调用的话,也得按照ABI的规则去写汇编才行。这个函数是swich_to,以后实现它。

esp的值会由调用约定来保证,因此我们不打算保护esp的值。在我们的实现中,由被调函数保存除esp外的4个寄存器,这就是线程栈thread_stack前4个成员的作用,我们将来用switch_to函数切换时,先在线程栈thread stack中压入这4个寄存器的值。

 

接下来

/*****   以下仅供第一次被调度上cpu时使用   ****/
void (*unused_retaddr);     // unused_ret 只为占位置充数为返回地址, 这里活用ret指令, ret指令是先将栈中地址恢复到 eip, 然后跳转过去, 实际上eip被我们操纵, 所以栈中地址无所谓是啥, eip会被我们修改的
thread_func* function;      // 由 kernel_thread 所调用的函数名, 线程中执行的函数
void* func_arg;             // 由 kernel_thread 所调用的函数所需的参数

unused_retaddr用来充当返回地址,在返回地址所在的栈帧占个位置,因此unused_retaddr中的值并不重要,仅仅起到占位的作用。

function是由函数kernel_thread所调用的函数名即function是在线程中执行的函数。

func_arg是由kernel_thread所调用的函数所需的参数,即function的参数,因此最终的情形是:在线程中调用的是function(func_arg)

函数在执行前,如果该函数有参数的话,调用者一定会按照调用约定,先把参数压到栈中。在C语言层面,函数的执行都是由调用者发起调用的,这通过call指令完成,此指令会在栈中留下返回地址。因此被调用的函数在执行时,会认为调用者已经把返回地址留在栈中,而且是在栈顶的位置。栈中的情形如图所示:

为了满足C语言的调用形式,使kernel_thread以为自己是通过call指令调用执行的,当前栈顶必须得是返回地址,故参数unused_ret只为占位置充数,由它充当栈顶,其值充当返回地址,所以它的值是多少都没关系,因为将来不需要通过此返回地址“返回”,目的是让kernel_thread去调用func(func_arg),也就是“只管继续向前执行”就好了,此时不需要“回头”。总之我们只要保留这个栈帧位置就够了,为的是让函数kernel_thread以为栈顶是它自己的返回地址,这样便有了一个正确的基准,并能够从栈顶+4和栈顶+8的位置找到参数func和func_arg。否则,若没有占位成员unused_ret的话,处理器依然把栈顶当作返回地址作为基准,以栈顶向上+4和+8的地方找参数func和func_arg,但由于没有返回地址,此时栈顶就是参数func,栈顶+4就是func_arg,栈顶+8的值目前未知,要看实际编译情况,因此处理器便找错了栈帧位置,后果必然出错。

 

结构体struct task_struct是定义的PCB结构。

/* 进程或线程的 pcb, 程序控制块 */
struct task_struct {
    uint32_t* self_kstack;      // 各内核线程都用自己的内核栈
    enum task_status status;    // 线程状态
    uint8_t priority;           // 线程优先级
    char name[16];
    uint32_t stack_magic;       // 栈的边界标记, 用于检测栈的溢出
};

self_kstack是各线程的内核栈顶指针,当线程被创建时,self_kstack被初始化为自己PCB所在页的顶端。之后在运行时,在被换下处理器前,我们会把线程的上下文信息(也就是寄存器映像)保存在0特权级栈中。self_kstack便用来记录0特权级栈在保存线程上下文后的新的栈顶,在下一次此线程又被调度到处理器上时,可以把self_kstack的值加载到esp寄存器,这样便从0特权级栈中获取了线程上下文,从而可以加载到处理器中运行。

status用于记录线程状态,其类型便是前面定义的枚举结构 enum task_status。

priority用于记录线程优先级,进程或线程都要有个优先级,此优先级咱们用来决定进程或线程的时间片,即被调度到处理器上的运行时间。

name[16]用于记录任务(线程或进程)的名字,长度是16,即任务名最长不过16个字符。

stack_magic是栈的边界标记,用于检测栈的溢出。我们PCB和0级栈是在同一个页中,栈位于页的顶端并向下发展,因此担心压栈过程中会把PCB中的信息给覆盖,所以每次在线程或进程调度时要判断是否触及到了进程信息的边界,也就是判断stack_magic的值是否为初始化的内容,stack_magic实际上就是个魔数。最后说明一下,中断栈intr_stack和线程栈thread_stack都位于线程的内核栈中,也就是都位于PCB的高地址处。

 

thread.c

接下来就是/thread/thread.c

完整代码:

#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"

#define PG_SIZE 4096

/* 由 kernel_thread 去执行 function(func_arg) */
static void kernel_thread(thread_func* function, void* func_args) {
    function(func_args);
}


/* 初始化线程栈 thread_stack, 将待执行的函数和参数放到 thread_stack 中相应的位置 */
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 = 0;
    kthread_stack->ebx = 0;
    kthread_stack->esi = 0;
    kthread_stack->edi = 0;
}


/* 初始化线程基本信息, 参数为: 待初始化线程指针(PCB), 线程名称, 线程优先级 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
    memset(pthread, 0, sizeof(*pthread));   // 清零
    strcpy(pthread->name, name);            // 给线程的名字赋值

    pthread->status = TASK_RUNNING;         // 线程的状态
    pthread->priority = prio;

    // self_kstack 是线程自己在内核态下(0特权级)使用的栈顶地址, 大小为一页, 初始化为PCB顶端
    pthread->self_kstack = (uint32_t*) ((uint32_t) pthread + PG_SIZE);
    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);   // 申请一页内核空间存放PCB

    init_thread(thread, name, prio);                    // 初始化线程
    thread_create(thread, function, func_arg);          // 创建线程

    // 将栈顶(esp)置为 eip, 利用 ret 跳转
    asm volatile("movl %0, %%esp; \
                  pop %%ebp; \
                  pop %%ebx; \
                  pop %%edi; \
                  pop %%esi; \
                  ret;" : : "g"(thread->self_kstack) : "memory");

    return thread;
}

 

代码解释:

init_thread (thread, name, prio)函数来初始化刚刚创建的 thread 线程。它接受3个参数, pthread是待初始化线程的指针,name是线程名称,prio是线程的优先级,此函数功能是将 3 个参数写入线程的 PCB,并且完成 PCB 一级的其他初始化。

/* 初始化线程基本信息, 参数为: 待初始化线程指针(PCB), 线程名称, 线程优先级 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
    memset(pthread, 0, sizeof(*pthread));   // 清零
    strcpy(pthread->name, name);            // 给线程的名字赋值

    pthread->status = TASK_RUNNING;         // 线程的状态
    pthread->priority = prio;

    // self_kstack 是线程自己在内核态下(0特权级)使用的栈顶地址, 大小为一页, 初始化为PCB顶端
    pthread->self_kstack = (uint32_t*) ((uint32_t) pthread + PG_SIZE);
    pthread->stack_magic = 0x19870916;      // 自定义魔数, 用于检查"入栈"是否过多(溢出)
}

先调用memset(pthread, 0, sizeof(*pthread))将pthread所在的PCB清0,即清0一页。

再通过strcpy(pthread->name, name)将线程名写入PCB中的name数组中。

接下来为线程的状态pthread->status赋值,目前仅仅为了演示,故直接将status置为TASK_RUNNING,以后再按照正常的逻辑为状态赋值。

接下来再将prio赋值给pthread->priority,目前的优先级没什么用,将来它的作用体现任务(线程和进程的统称)在处理器上执行的时间片长度,即优先级越高,执行的时间片越长。

pthread->self_kstack 是线程自己在 0 特权级下所用的栈,在线程创建之初,它被初始化为线程 PCB 的 最顶端,即(uint32_t)pthread + PG_SIZE.PCB 的上端是 0 特权级栈,将来线程在内核态下的任何栈操作都是用此 PCB 中的栈,如果出现了某些异常导致入栈操作过多,这会破坏PCB低处的线程信息。为此,需要检测这些线程信息是否被破坏了,stack->magic被安排在线程信息的最边缘,作为它与栈的边缘。目前用不到此值,以后在线程调度时会检测它。

pthread->stack_magic自定义魔数就行,我这里用的是0x19870916,这与代码功能无关。

 

thread_create函数创建线程,接受3个参数,pthread 是待创建的线程的指针,function 是在线程中运行的函数,func_arg 是function的参数。函数的功能是初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中 相应的位置。

/* 初始化线程栈 thread_stack, 将待执行的函数和参数放到 thread_stack 中相应的位置 */
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 = 0;
    kthread_stack->ebx = 0;
    kthread_stack->esi = 0;
    kthread_stack->edi = 0;
}

在thread_create中,pthread->self_kstack-=sizeof (struct intr_stack)是为了预留线程所使用的中断栈 struct intr_stack 的空间。

这有两个目的:

  • 将来线程进入中断后,位于kernel.S中的中断代码会通过此栈来保存上下文。
  • 将来实现用户进程时,会将用户进程的初始信息放在中断栈中。

因此必须要事先把struct intr_stack的空间留出来。

pthread->self_kstack在init_thread中已经被指向了PCB的最顶端,所以现在要减去中断栈的大小。此时pthread->self_kstack 指向PCB中的中断栈下面的地址。

在下一行中, struct thread_stack* kthread_stack定义了线程栈指针,这个就是占位成员unused_retaddr所在的栈。

其中的function就是函数thread_start的形参function所指向的函数,其中的func_arg就是thread_start 的形参func_arg的值,这三行就是为能够在kernel_thread中调用function(func_arg)做准备。

eip 指向 kernel_thread,kernel_thread 接受两个参数,function 是 kernel_thread 中调用的函数,func_arg 是 function 的参数,因 此 kernel_thread函数的功能就是调用 function(func_arg)。

/* 由 kernel_thread 去执行 function(func_arg) */
static void kernel_thread(thread_func* function, void* func_args) {
    function(func_args);
}

kernel_thread并不是通过call指令调用的,而是通过ret来执行的,因此无法按照正常的函数调用形式传递kernel_thread所需要的参数,如这样调用是不行的: kernel_thread(function, func_arg),只能将参数放在kernel_thread所用的栈中,即处理器进入kernel_thread函数体时,栈顶为返回地址,栈顶+4为参数function,栈顶+8为参数func_arg。

接下来把 ebp, ebx, esi, edi这 4个寄存器初始化为0,因为线程中的函数尚未执行,在执行过程中寄存器才会有值,此时置为0即可。 kthread_stack->unused_retaddr 是不需要赋值的,就是用来占位的,因此我们代码中并没有对它处理。

 

thread_start函数接受4个参数,name为线程名,prio为线程的优先级,要执行的函数是function,func_arg是函数function的参数。thread_start的功能是创建一优先级为prio 的线程,线程名为name,线程所执行的函数是function(func_arg)。

/* 创建一优先级为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);   // 申请一页内核空间存放PCB

    init_thread(thread, name, prio);                    // 初始化线程
    thread_create(thread, function, func_arg);          // 创建线程

    // 将栈顶(esp)置为 eip, 利用 ret 跳转
    asm volatile("movl %0, %%esp; \
                  pop %%ebp; \
                  pop %%ebx; \
                  pop %%edi; \
                  pop %%esi; \
                  ret;" : : "g"(thread->self_kstack) : "memory");

    return thread;
}

无论是进程或线程的 PCB,这都是给内核调度器使用的结构,属于内核管理的数据,因此将来用户进程的PCB也依然要从内核物理内存池中申请。

在函数体内,先通过get_kernel_pages(1)在内核空间中申请一页内存,即4096字节,将其赋值给新创建的PCB指针thread,即struct task_struct* thread。注意,由于get_kernel_page返回的是页的起始地址,故thread指向的是PCB的最低地址。

接着调用init_thread (thread, name, prio)函数来初始化刚刚创建的 thread 线程。它又调用了thread_create创建了线程。

接着就是汇编了,在输出部分,”g” (thread->self_kstack)使thread_self_kstack的值作为输入,采用通用约束g,即内存或寄存器都可以。

在汇编语句部分,movl %0, %%esp,也就是使thread_self_kstack的值作为栈顶,此时 thread->self_kstack指向线程栈的最低处,这是我们在函数thread_create中设定的。

接下来的这连续4个弹栈操作: pop %%ebp; pop %%ebx; pop %%edi; pop %%esi使之前初始化的0弹入到相应寄存器中。

ret会把找顶的数据作为返回地址送上处理器的EIP 寄存器。

此时栈顶的数据是在thread_create中为kthread_stack->eip所赋的值kernel_thread。 因此,在执行ret后,处理器会去执行kernel_thread函数。 接着在kernel_thread函数中会调用传给函数function(func_arg). 在执行完这句汇编后,线程就会开始执行.

 

修改main函数测试

调用thread_start(“k_thread_a”, 31, k_thread_a, “argA “);创建了新线程。线程名字为k_thread_a,优先级为31,此线程运行的函数是k_thread_a,功能就是打印参数arg。我们传给thread_start的第4个参数是字符串”argA”,因此线程在运行时会在屏幕上循环输出arg_A。

#include "print.h"
#include "init.h"
#include "thread.h"

void k_thread_a(void*);

int main() {
    put_str("I am kernel\n");
    init_all();

    // asm volatile("sti");    // 为演示中断处理, 在此临时开中断

    thread_start("k_thread_a", 31, k_thread_a, "argA ");

    while (1);
    return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    /* 用void*来通用表示参数, 被调用的函数知道自己需要什么类型的参数, 自己转换再用 */
    char* para = arg;
    while (1) {
        put_str(para);
    }
}

 

在makefile文件添加上thread.h和thread.c

LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
       $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o \
       $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o \
       $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o
$(BUILD_DIR)/thread.o:thread/thread.c thread/thread.h lib/stdint.h lib/string.h kernel/global.h kernel/memory.h
    $(CC) $(CFLAGS) $< -o $@
$(BUILD_DIR)/main.o:kernel/main.c lib/kernel/print.h \
    lib/stdint.h kernel/init.h kernel/memory.h \
    thread/thread.h
    $(CC) $(CFLAGS) $< -o $@

 

结果如下:

argA会不断输出

 

5.参考

郑钢著操作系统真象还原

田宇著一个64位操作系统的设计与实现

丁渊著ORANGE’S:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇