代码、内容参考来自于包括《操作系统真象还原》、《一个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:一个操作系统的实现


