实现内核线程
线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
进程:是资源分配的基本单位。它拥有独立的内存空间、文件句柄等资源。启动一个程序,系统就会创建一个进程。
线程:是CPU调度的基本单位。一个进程内可以有多个线程,它们共享该进程的内存空间和资源(如代码、数据、堆),但每个线程拥有自己独立的栈和程序计数器。
概念
执行流
过去计算机只有一个处理器于是任务的执行是串行的方式,由任务调度器完成调度
通常指在单核CPU (或核心数少于并发任务数)的环境下,操作系统通过快速切换 多个线程或进程,让它们在微观上"交替执行",而宏观上看起来像是"同时运行"的一种调度现象。严格来说,这属于并发 而非真正的并行。

单任务操作系统
定义:一次只允许一个用户程序在内存中运行,CPU 只能执行一个任务,直到该任务结束,才能执行下一个任务。
特点:
独占性:整个系统资源(CPU、内存、I/O)全部服务于当前正在运行的程序。
简单:无需复杂的进程调度、内存保护、资源竞争处理。
低效:当程序进行 I/O 操作(如读取磁盘、等待用户输入)时,CPU 只能空转等待,无法切换做其他工作。
多任务操作系统
定义:允许多个程序(进程/线程)同时驻留在内存中,CPU 在它们之间快速切换,使得从宏观上看多个任务在"同时"运行。
特点:
并发执行:通过时间片轮转或优先级调度,让 CPU 在任务间快速切换。
资源复用:当某个任务等待 I/O 时,CPU 可以立即切换到其他任务,提高资源利用率。
复杂性:需要进程管理、内存管理(虚拟内存、隔离)、文件系统保护、同步互斥机制等。

程序
定义:一组计算机指令的静态集合,存储在磁盘上(如 .exe、.py 文件)。
特点:被动、无生命周期,只是一个文件。
进程
定义:程序的一次执行实例。当程序被加载到内存并运行时,就成为一个进程。
特点:
资源分配的基本单位(拥有独立的内存空间、文件句柄等)。
进程间相互隔离,一个进程崩溃一般不影响其他进程。
创建和切换开销较大。
运行态进程正在CPU上执行。
单核CPU上,同一时刻只有一个进程处于运行态。
就绪态
进程已获得除CPU外的所有资源,等待分配CPU时间片。
通常排在就绪队列中。
阻塞态(也称等待态)
进程因等待某事件(如I/O完成、信号量、锁释放)而暂时无法继续执行。
即使分配CPU也无法运行,直到事件发生。

线程
定义:进程中的一个执行单元,是CPU调度的基本单位。
特点
一个进程内可包含多个线程,它们共享进程的资源(内存、文件等)。
每个线程有自己的栈和程序计数器,但共享堆和全局变量。
创建和切换开销小,但线程间通信简单(直接读写共享内存),需注意同步问题。
一个线程崩溃通常导致整个进程崩溃。
调度单元
以进程为调度单元
在早期或简单的操作系统中,进程既是资源分配单位,也是CPU调度单位。
特点:
独立资源:每个进程拥有独立的内存空间、文件描述符等。
切换开销大:进程切换需要切换整个地址空间(刷新TLB、页表),保存/恢复大量上下文,开销高。
并发性有限:一个进程内无法同时执行多个任务(除非用多进程,但进程间隔离性强,通信复杂)。
调度决策:调度器直接在进程之间选择。
缺点:
创建/切换成本高,不适合大量并发。
进程间通信(IPC)效率低,需借助管道、共享内存等机制。
单进程内无法利用多核并行(除非通过多进程,但资源冗余大)。
以线程为调度单元
现代操作系统(Linux、Windows、macOS等)将调度单位从进程分离为线程。进程成为资源的容器,线程是CPU调度的实体。
特点:
轻量级调度:线程切换只需保存/恢复寄存器、PC、栈指针等少量上下文,不涉及地址空间切换(同一进程内),开销小。
高并发性:一个进程内可创建成百上千个线程,充分利用CPU时间片,尤其在I/O密集型场景下能掩盖延迟。
并行加速:多线程可真正并行运行在多核CPU上,提升计算密集型任务效率。
资源共享:同一进程的线程共享内存、文件等,通信简单高效(直接读写共享变量),但需要同步机制。
调度器:直接调度线程(或称"轻量级进程"),进程仅作为线程的容器,不直接参与调度。

用户级与内核级线程管理

用户级线程管理
定义
线程的管理(创建、销毁、同步、调度)完全在用户空间由线程库(如 POSIX Pthreads 的用户态实现)完成,内核不知道线程的存在。内核仅将进程作为调度单位。
管理方式
线程库维护一个线程控制块(TCB) 集合,在用户态进行线程切换。
调度器在进程内部按时间片或协作方式调度线程,内核仍只调度进程。
线程切换时,无需陷入内核,只需保存用户态寄存器、栈等,切换开销极小。
特点
优点:
创建、切换、销毁非常快(不涉及系统调用)。
可定制调度算法,适合特定应用。
可在不支持内核线程的操作系统上实现。
缺点:
若一个线程发起阻塞系统调用(如 read),整个进程会被内核阻塞,其他线程无法运行。
无法利用多核并行,因为内核只看到单个进程,无法将不同线程分配到不同 CPU 核心。
线程间协作式调度可能导致饥饿。
内核级线程管理
定义
线程的管理由操作系统内核直接负责。内核维护每个线程的 TCB,并直接调度线程(而不是进程)。线程是内核调度的基本单位。
管理方式
每个线程在内核中有独立的数据结构,创建、销毁、切换都需要通过系统调用陷入内核。
内核根据线程的优先级、时间片等进行抢占式调度。
同一进程的多个线程可被调度到不同 CPU 核心上并行执行。
特点
优点:
一个线程阻塞时,内核可调度同进程的其他线程继续运行,不影响并发。
真正支持多线程并行,充分利用多核 CPU。
内核级的同步原语(如互斥锁、信号量)可靠且统一。
缺点:
线程切换需要陷入内核,开销较大(涉及模式切换和上下文保存)。
创建和销毁线程的系统调用成本较高。
内核需要维护更多数据结构,可能限制线程数量。
PCB
PCB(Process Control Block,进程控制块)是操作系统中用于描述和管理进程的核心数据结构。每个进程在创建时都会生成一个PCB,它相当于进程的"身份证"和"档案",操作系统通过PCB来感知、控制和调度进程

PCB 的作用
唯一标识:PCB 中存储进程的标识符(PID),是操作系统区分不同进程的依据。
状态管理:记录进程当前状态(就绪、运行、阻塞等),便于调度器做出决策。
资源记录:保存进程占用的内存地址、打开的文件描述符、I/O 设备等信息。
上下文存储:当进程被切换时,CPU 寄存器的内容、程序计数器等会保存在 PCB 中,以便下次恢复执行。
调度信息:包含进程优先级、时间片等,供调度算法使用。
PCB 包含的主要信息
|----------|-----------------------------|
| 类别 | 具体内容 |
| 进程标识 | PID(进程ID)、父进程ID、用户ID等 |
| 状态信息 | 当前状态(运行、就绪、阻塞)、是否在内存中、阻塞原因等 |
| 程序计数器 | 下一条要执行的指令地址 |
| CPU 寄存器 | 通用寄存器、堆栈指针、状态字等 |
| 内存管理信息 | 页表指针、段表、内存边界等 |
| I/O 状态信息 | 已分配设备列表、打开文件表 |
| 调度信息 | 优先级、时间片、等待时间等 |
| 记账信息 | CPU 使用时间、实际运行时间等 |
在内核空间实现线程
创建并运行线程
下面我们先构造 PCB 及其相关的基础部分
thread.h
(thread/thread.h)
线程状态枚举 task_status:标识线程/进程的运行、就绪、阻塞等状态。
中断栈 intr_stack:用于保存中断发生时的硬件上下文(寄存器、中断号、错误码等),保证中断返回后能恢复现场。
线程栈 thread_stack:用于线程切换时保存被调度出去的线程的寄存器现场,并存储线程启动时所需执行的函数和参数,配合调度器实现线程的切换与首次运行。
进程控制块(PCB) task_struct:管理每个线程的核心信息,包括内核栈指针、状态、优先级、名称、栈边界魔数(用于检测栈溢出)。
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
//定义一种叫thread_fun的函数类型,该类型返回值是空,参数是一个地址(这个地址用来指向自己的参数)。
//这样定义,这个类型就能够具有很大的通用性,很多函数都是这个类型
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.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;
};
/*********** 线程栈thread_stack ***********
* 线程自己的栈,用于存储线程中待执行的函数
* 此结构在线程自己的内核栈中位置不固定,
* 用在switch_to时保存线程环境。
* 实际位置取决于实际运行情况。
******************************************/
struct thread_stack {
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
//这个位置会放一个名叫eip,返回void的函数指针(*epi的*决定了这是个指针),
//该函数传入的参数是一个thread_func类型的函数指针与函数的参数地址
void (*eip) (thread_func* func, void* func_arg);
//以下三条是模仿call进入thread_start执行的栈内布局构建的,call进入就会压入参数与返回地址,因为我们是ret进入kernel_thread执行的
//要想让kernel_thread正常执行,就必须人为给它造返回地址,参数
void (*unused_retaddr);
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; //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
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
thread.c
(thread/thread.c)
kernel_thread
一个包装函数,作为线程的"启动器",负责调用用户传入的实际执行函数 function,并传递参数 func_arg。
thread_create初始化线程的内核栈布局。
先预留中断栈空间(intr_stack),再预留线程栈空间(thread_stack)。
在栈中预先填入 eip = kernel_thread(线程首次切换后的返回地址),以及用户函数指针 function 和参数 func_arg。
当调度器切换到该线程时,ret 指令会从栈顶弹出 kernel_thread 地址并跳转执行,随后 kernel_thread 再调用真正的用户函数。
init_thread初始化线程控制块(PCB)。
清空 PCB,设置线程名、状态为 TASK_RUNNING、优先级。
设置内核栈指针 self_kstack 为 PCB 地址 + 4KB(即使用一页内存作为该线程的内核栈)。
设置栈魔数 stack_magic,用于检测栈溢出。
thread_start创建并启动一个新线程。
调用 get_kernel_pages(1) 分配一页内存作为线程的 PCB 和内核栈。
调用 init_thread 填充 PCB 基本信息。
调用 thread_create 构造线程栈。
通过内联汇编将当前栈指针切换到新线程的栈顶,并依次弹出 thread_stack 中保存的寄存器(ebp、ebx、edi、esi),最后执行 ret 指令跳转到 kernel_thread 开始运行用户函数。
返回线程的 PCB 指针。
线程PCB所在页的整体布局

线程栈空间

#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_arg) {
function(func_arg);
}
/*用于根据传入的线程的pcb地址、要运行的函数地址、函数的参数地址来初始化线程栈中的运行信息,核心就是填入要运行的函数地址与参数 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
//pthread->self_kstack -= sizeof(struct intr_stack); //-=结果是sizeof(struct intr_stack)的4倍
//self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
pthread->self_kstack = (uint32_t*)((int)(pthread->self_kstack) - sizeof(struct intr_stack));
/* 再留出线程栈空间,可见thread.h中定义 */
//pthread->self_kstack -= sizeof(struct thread_stack);
pthread->self_kstack = (uint32_t*)((int)(pthread->self_kstack) - sizeof(struct thread_stack));
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //我们已经留出了线程栈的空间,现在将栈顶变成一个线程栈结构体
//指针,方便我们提前布置数据达到我们想要的目的
kthread_stack->eip = kernel_thread; //我们将线程的栈顶指向这里,并ret,就能直接跳入线程启动器开始执行。
//为什么这里我不能直接填传入进来的func,这也是函数地址啊,为什么还非要经过一个启动器呢?其实是可以不经过线程启动器的
//因为用不着,所以不用初始化这个返回地址kthread_stack->unused_retaddr
kthread_stack->function = function; //将线程启动器(thread_start)需要运行的函数地址放入线程栈中
kthread_stack->func_arg = func_arg; //将线程启动器(thread_start)需要运行的函数所需要的参数地址放入线程栈中
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}
/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread)); //把pcb初始化为0
strcpy(pthread->name, name); //将传入的线程的名字填入线程的pcb中
pthread->status = TASK_RUNNING; //这个函数是创建线程的一部分,自然线程的状态就是运行态
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址
//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间
pthread->stack_magic = 0xdeadbeef; // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}
/* 创建一优先级为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申请4K空间的起始地址
init_thread(thread, name, prio); //初始化线程的pcb
thread_create(thread, function, func_arg); //初始化线程的线程栈
//我们task_struct->self_kstack指向thread_stack的起始位置,然后pop升栈,
//到了通过线程启动器来的地址,ret进入去运行真正的实际函数
//通过ret指令进入,原因:1、函数地址与参数可以放入栈中统一管理;2、ret指令可以直接从栈顶取地址跳入执行
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");
return thread;
}
main.c
#include "print.h"
#include "init.h"
#include "thread.h"
void k_thread_a(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
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) {
int i = 9999999;
while(i--);
put_str(para);
}
}
运行
makefile
#定义一大堆变量,实质就是将需要多次重复用到的语句定义一个变量方便使用与替换
BUILD_DIR=./build
ENTRY_POINT=0xc0001500
HD60M_PATH=/home/chipfesen/bochs/hd60M.img
#只需要把hd60m.img路径改成自己环境的路径,整个代码直接make all就完全写入了,能够运行成功
AS=nasm
CC=gcc-4.4
LD=ld
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/
ASFLAGS= -f elf
CFLAGS= -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32
#-Wall warning wall的意思,产生尽可能多警告信息,-fno-builtin不要采用内部函数,
#-W 会显示警告,但是只显示编译器认为会出现错误的警告
#-Wstrict-prototypes 要求函数声明必须有参数类型,否则发出警告。-Wmissing-prototypes 必须要有函数声明,否则发出警告
LDFLAGS= -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map -m elf_i386
#-Map,生成map文件,就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件
#里面包含函数大小,入口地址等一些重要信息
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)/bitmap.o \
$(BUILD_DIR)/memory.o $(BUILD_DIR)/thread.o
#顺序最好是调用在前,实现在后
######################编译两个启动文件的代码#####################################
boot:$(BUILD_DIR)/mbr.o $(BUILD_DIR)/loader.o
$(BUILD_DIR)/mbr.o:boot/mbr.s
$(AS) -I boot/include/ -o build/mbr.o boot/mbr.s
$(BUILD_DIR)/loader.o:boot/loader.s
$(AS) -I boot/include/ -o build/loader.o boot/loader.s
######################编译C内核代码###################################################
$(BUILD_DIR)/main.o:kernel/main.c
$(CC) $(CFLAGS) -o $@ $<
# $@表示规则中目标文件名的集合这里就是$(BUILD_DIR)/main.o $<表示规则中依赖文件的第一个,这里就是kernle/main.c
$(BUILD_DIR)/init.o:kernel/init.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/timer.o:device/timer.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/debug.o:kernel/debug.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/string.o:lib/string.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/bitmap.o:kernel/bitmap.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/memory.o:kernel/memory.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/thread.o:thread/thread.c
$(CC) $(CFLAGS) -o $@ $<
###################编译汇编内核代码#####################################################
$(BUILD_DIR)/kernel.o:kernel/kernel.s
$(AS) $(ASFLAGS) -o $@ $<
$(BUILD_DIR)/print.o:lib/kernel/print.s
$(AS) $(ASFLAGS) -o $@ $<
##################链接所有内核目标文件##################################################
$(BUILD_DIR)/kernel.bin:$(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
# $^表示规则中所有依赖文件的集合,如果有重复,会自动去重
.PHONY:mk_dir hd clean build all boot #定义了6个伪目标
mk_dir:
if [ ! -d $(BUILD_DIR) ];then mkdir $(BUILD_DIR);fi
#判断build文件夹是否存在,如果不存在,则创建
hd:
dd if=build/mbr.o of=$(HD60M_PATH) count=1 bs=512 conv=notrunc && \
dd if=build/loader.o of=$(HD60M_PATH) count=4 bs=512 seek=2 conv=notrunc && \
dd if=$(BUILD_DIR)/kernel.bin of=$(HD60M_PATH) bs=512 count=200 seek=9 conv=notrunc
clean:
@cd $(BUILD_DIR) && rm -f ./* && echo "remove ./build all done"
#-f, --force忽略不存在的文件,从不给出提示,执行make clean就会删除build下所有文件
build:$(BUILD_DIR)/kernel.bin
#执行build需要依赖kernel.bin,但是一开始没有,就会递归执行之前写好的语句编译kernel.bin
all:mk_dir boot build hd
#make all 就是依次执行mk_dir build hd
运行后如下图所示

核心数据结构双向链表
上面只是线程的创建与进入,我们要实现依靠线程的pcb之间形成的链表来实现管理与调度,pcb之间形成的链表是为了通过一个pcb顺利找到下一个pcb,因为我们会在task_struct中插入一个双向链表,为了实现这样的数据结构,我们接下来实现会与链表有关的数据结构与函数。
list.h
(lib/kernel/list.h)
偏移量宏 offset:通过将0地址强转为结构体类型,计算成员在结构体中的偏移。
容器宏 member_to_entry:通过成员指针反向获取所在结构体的起始地址,实现从链表节点到包含它的结构体的转换。
链表节点 struct list_elem:包含前驱和后继指针,构成双向链表的节点。
链表头 struct list:包含头尾两个哨兵节点,简化链表操作。
函数声明:提供链表初始化、头插、尾插、插入、删除、弹出、查找、判空、长度、遍历等基础操作,其中遍历可结合回调函数实现灵活筛选。
#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "global.h"
// 偏移量宏
#define offset(struct_type, member_name) (int)(&(((struct_type*)0)->member_name))
#define member_to_entry(struct_type, member_name, member_ptr) \
(struct_type*)((int)member_ptr - offset(struct_type, member_name))
/********** 定义链表结点成员结构 ***********/
struct list_elem {
struct list_elem* prev;
struct list_elem* next;
};
/* 链表结构 */
struct list {
struct list_elem head;
struct list_elem tail;
};
// 回调函数类型
typedef bool (function)(struct list_elem*, int arg);
// 函数声明
void list_init(struct list* list);
void list_push(struct list* plist, struct list_elem* elem);
void list_append(struct list* plist, struct list_elem* elem);
void list_insert(struct list* link,struct list* new_link);
void list_remove(struct list_elem* pelem);
struct list_elem* list_pop(struct list* plist);
bool elem_find(struct list* plist, struct list_elem* obj_elem);
int list_empty(struct list* plist);
uint32_t list_len(struct list* plist);
struct list_elem* list_traversal(struct list* plist, function func, int arg);
#endif
list.c
(lib/kernel/list.c)
链表初始化 list_init:设置头尾哨兵节点,形成空链表。
插入操作 list_insert_before、list_push、list_append:支持在指定节点前插入、头部插入、尾部插入。
删除操作 list_remove:将节点从链表中移除。
弹出操作 list_pop:移除并返回链表头部节点。
查找与遍历 elem_find、list_traversal:按值查找节点,或通过回调函数遍历筛选符合条件的节点。
辅助函数 list_len、list_empty:获取链表长度或判空
#include "list.h"
#include "interrupt.h"
/* 初始化双向链表list */
void list_init (struct list* list) {
list->head.prev = NULL;
list->head.next = &list->tail;
list->tail.prev = &list->head;
list->tail.next = NULL;
}
/* 把链表元素elem插入在元素before之前 */
void list_insert_before(struct list_elem* before, struct list_elem* elem) {
enum intr_status old_status = intr_disable(); //未来这个链表结点插入是用于修改task_struck队列的,这是个公共资源,所以需要不被切换走
/* 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表*/
before->prev->next = elem;
/* 更新elem自己的前驱结点为before的前驱,
* 更新elem自己的后继结点为before, 于是before又回到链表 */
elem->prev = before->prev;
elem->next = before;
/* 更新before的前驱结点为elem */
before->prev = elem;
intr_set_status(old_status); //关中断之前是开着,那么现在就重新打开中断,如果关着,那么就继续关着
}
/* 添加元素到列表队首,类似栈push操作,添加结点到链表队首,类似于push操作, 参数1是链表的管理结点,参数2是一个新结点 */
void list_push(struct list* plist, struct list_elem* elem) {
list_insert_before(plist->head.next, elem); // 在队头插入elem
}
/* 追加元素到链表队尾,类似队列的先进先出操作,添加结点到队尾,实际上就是添加结点到管理结点之前。参数是管理结点与要添加的结点 */
void list_append(struct list* plist, struct list_elem* elem) {
list_insert_before(&plist->tail, elem); // 在队尾的前面插入
}
/* 使元素pelem脱离链表 */
void list_remove(struct list_elem* pelem) {
enum intr_status old_status = intr_disable();
pelem->prev->next = pelem->next;
pelem->next->prev = pelem->prev;
intr_set_status(old_status);
}
/* 将链表第一个元素弹出并返回,类似栈的pop操作,参数是链表的管理结点(入口结点) */
struct list_elem* list_pop(struct list* plist) {
struct list_elem* elem = plist->head.next;
list_remove(elem);
return elem;
}
/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find(struct list* plist, struct list_elem* obj_elem) {
struct list_elem* elem = plist->head.next;
while (elem != &plist->tail) {
if (elem == obj_elem) {
return true;
}
elem = elem->next;
}
return false;
}
/* 把列表plist中的每个元素elem和arg传给回调函数func,
* arg给func用来判断elem是否符合条件.
* 本函数的功能是遍历列表内所有元素,逐个判断是否有符合条件的元素。
* 找到符合条件的元素返回元素指针,否则返回NULL. */
struct list_elem* list_traversal(struct list* plist, function func, int arg) {
struct list_elem* elem = plist->head.next;
/* 如果队列为空,就必然没有符合条件的结点,故直接返回NULL */
if (list_empty(plist)) {
return NULL;
}
while (elem != &plist->tail) {
if (func(elem, arg)) { // func返回ture则认为该元素在回调函数中符合条件,命中,故停止继续遍历
return elem;
} // 若回调函数func返回true,则继续遍历
elem = elem->next;
}
return NULL;
}
/* 返回链表长度,不包含管理结点,参数就是链表的管理结点 */
uint32_t list_len(struct list* plist) {
struct list_elem* elem = plist->head.next;
uint32_t length = 0;
while (elem != &plist->tail) {
length++;
elem = elem->next;
}
return length;
}
/* 判断链表是否为空,空时返回true,否则返回false */
bool list_empty(struct list* plist) { // 判断队列是否为空
return (plist->head.next == &plist->tail ? true : false);
}
多线程调度

thread.h
(thread/thread.h)
线程状态枚举 task_status:标识线程的运行、就绪、阻塞、死亡等状态。
中断栈 intr_stack:用于保存中断发生时的CPU上下文(寄存器、中断号、错误码等),确保中断返回后能正确恢复执行。
线程栈 thread_stack:用于线程切换时保存被换出线程的寄存器现场,并存储线程首次运行时需要执行的函数和参数,配合调度器实现线程切换。
线程控制块(PCB) task_struct:管理每个线程的关键信息,包括内核栈指针、状态、优先级、名称、时间片(ticks)、已执行滴答数(elapsed_ticks)、两个链表结点(用于就绪队列和全局线程队列)、页表地址(进程用)、栈魔数(检测栈溢出)。
函数声明:提供线程创建、初始化、启动、获取当前线程、调度器、初始化线程子系统的接口。
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"
//定义一种叫thread_fun的函数类型,该类型返回值是空,参数是一个地址(这个地址用来指向自己的参数)。
//这样定义,这个类型就能够具有很大的通用性,很多函数都是这个类型
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.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;
};
/*********** 线程栈thread_stack ***********
* 线程自己的栈,用于存储线程中待执行的函数
* 此结构在线程自己的内核栈中位置不固定,
* 用在switch_to时保存线程环境。
* 实际位置取决于实际运行情况。
******************************************/
struct thread_stack {
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
//这个位置会放一个名叫eip,返回void的函数指针(*epi的*决定了这是个指针),
//该函数传入的参数是一个thread_func类型的函数指针与函数的参数地址
void (*eip) (thread_func* func, void* func_arg);
//以下三条是模仿call进入thread_start执行的栈内布局构建的,call进入就会压入参数与返回地址,因为我们是ret进入kernel_thread执行的
//要想让kernel_thread正常执行,就必须人为给它造返回地址,参数
void (*unused_retaddr);
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]; //用于存储自己的线程的名字
uint8_t ticks; //线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; //此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; //general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; //all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t* pgdir; //进程自己页表的虚拟地址
uint32_t stack_magic; //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
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);
struct task_struct* running_thread(void);
void schedule(void);
void thread_init(void);
#endif
thread.c
(thread/thread.c)
获取当前线程:running_thread() 通过当前栈指针地址对齐获取线程PCB。
线程入口:kernel_thread() 作为包装函数,先开中断再执行用户实际函数。
初始化线程栈:thread_create() 在PCB预留空间构造中断栈和线程栈,将 eip 设为 kernel_thread,并存入用户函数及其参数。
初始化PCB:init_thread() 设置线程名、状态、优先级、时间片、栈指针、栈魔数等。
创建线程:thread_start() 分配一页内存作为PCB,调用上述函数完成初始化,并将线程加入就绪队列和全局线程队列。
主线程封装:make_main_thread() 将已运行的 main 函数包装为线程(不另分配内存),加入全局队列。
调度器:schedule() 关中断后,将当前运行线程放回就绪队列(若状态为运行),然后从就绪队列取出下一个线程,调用 switch_to 切换。
初始化:thread_init() 初始化就绪队列和全局队列,并创建主线程。
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#include "debug.h"
#include "interrupt.h"
#include "print.h"
#define PG_SIZE 4096
struct task_struct* main_thread; // 主线程PCB
struct list thread_ready_list; // 就绪队列
struct list thread_all_list; // 所有任务队列
static struct list_elem* thread_tag;// 用于保存队列中的线程结点
extern void switch_to(struct task_struct* cur, struct task_struct* next);
/* 获取当前线程pcb指针 */
struct task_struct* running_thread() {
uint32_t esp;
asm ("mov %%esp, %0" : "=g" (esp));
/* 取esp整数部分即pcb起始地址 */
return (struct task_struct*)(esp & 0xfffff000);
}
/* 由kernel_thread去执行function(func_arg) , 这个函数就是线程中去开启我们要运行的函数*/
static void kernel_thread(thread_func* function, void* func_arg) {
/* 执行function前要开中断,避免后面的时钟中断被屏蔽,而无法调度其它线程 */
intr_enable();
function(func_arg);
}
/*用于根据传入的线程的pcb地址、要运行的函数地址、函数的参数地址来初始化线程栈中的运行信息,核心就是填入要运行的函数地址与参数 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
//pthread->self_kstack -= sizeof(struct intr_stack); //-=结果是sizeof(struct intr_stack)的4倍
//self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
pthread->self_kstack = (uint32_t*)((int)(pthread->self_kstack) - sizeof(struct intr_stack));
/* 再留出线程栈空间,可见thread.h中定义 */
//pthread->self_kstack -= sizeof(struct thread_stack);
pthread->self_kstack = (uint32_t*)((int)(pthread->self_kstack) - sizeof(struct thread_stack));
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //我们已经留出了线程栈的空间,现在将栈顶变成一个线程栈结构体
//指针,方便我们提前布置数据达到我们想要的目的
kthread_stack->eip = kernel_thread; //我们将线程的栈顶指向这里,并ret,就能直接跳入线程启动器开始执行。
//为什么这里我不能直接填传入进来的func,这也是函数地址啊,为什么还非要经过一个启动器呢?其实是可以不经过线程启动器的
//因为用不着,所以不用初始化这个返回地址kthread_stack->unused_retaddr
kthread_stack->function = function; //将线程启动器(thread_start)需要运行的函数地址放入线程栈中
kthread_stack->func_arg = func_arg; //将线程启动器(thread_start)需要运行的函数所需要的参数地址放入线程栈中
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}
/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread)); //把pcb初始化为0
strcpy(pthread->name, name); //将传入的线程的名字填入线程的pcb中
if(pthread == main_thread){
pthread->status = TASK_RUNNING; //由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */
}
else{
pthread->status = TASK_READY;
}
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL; //线程没有自己的地址空间,进程的pcb这一项才有用,指向自己的页表虚拟地址
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址
//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间
pthread->stack_magic = 0xdeadbeef; // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}
/* 创建一优先级为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申请4K空间的起始地址
init_thread(thread, name, prio); //初始化线程的pcb
thread_create(thread, function, func_arg); //初始化线程的线程栈
/* 确保之前不在队列中 */
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
/* 加入就绪线程队列 */
list_append(&thread_ready_list, &thread->general_tag);
/* 确保之前不在队列中 */
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
/* 加入全部线程队列 */
list_append(&thread_all_list, &thread->all_list_tag);
return thread;
}
/* 将kernel中的main函数完善为主线程 */
static void make_main_thread(void) {
/* 因为main线程早已运行,咱们在loader.S中进入内核时的mov esp,0xc009f000,
就是为其预留了tcb,地址为0xc009e000,因此不需要通过get_kernel_page另分配一页*/
main_thread = running_thread();
init_thread(main_thread, "main", 31);
/* main函数是当前线程,当前线程不在thread_ready_list中,
* 所以只将其加在thread_all_list中. */
ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
list_append(&thread_all_list, &main_thread->all_list_tag);
}
/* 实现任务调度 */
void schedule() {
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
}
else {
/* 若此线程需要某事件发生后才能继续上cpu运行,
不需要将其加入队列,因为当前线程不在就绪队列中。*/
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;
switch_to(cur, next);
}
/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
/* 将当前main函数创建为线程 */
make_main_thread();
put_str("thread_init done\n");
}
对应的栈空间
struct thread_stack 首次构造时的内容
关键:self_kstack 指向 ebp 的位置。首次调度时,switch_to 将 esp 设为此地址,依次弹出 ebp、ebx、edi、esi(均为 0),然后 ret 弹出 eip,跳转到 kernel_thread。
此时栈顶指向 unused_retaddr,而 function 和 func_arg 位于 esp+4 和 esp+8,符合 C 调用约定(kernel_thread 会从 [esp+4] 和 [esp+8] 读取参数)。
中断发生时压入的 intr_stack 布局此区域在内核栈中位于 thread_stack 之上,实际中断处理时会使用。
schedule 调用 switch_to 时的栈布局
switch_to 内部保存当前线程现场时的栈布局
此时 switch_to 将当前栈顶 esp(指向 ebp)保存到 cur->self_kstack。
切换到新线程(首次运行)时的栈布局此时 kernel_thread 作为 C 函数被调用,它会从 [esp+4] 取第一个参数(function),从 [esp+8] 取第二个参数(func_arg),从而正确执行用户函数
非首次运行时,被切换出去的线程栈上的内容
当该线程再次获得 CPU 时,switch_to 会将 esp 恢复到此位置,然后依次 pop ebp; pop ebx; pop edi; pop esi,最后 ret 返回到 schedule 中调用 switch_to 的下一条指令(即回到中断处理程序),从而继续执行。
为何next指向下一个线程的pcb?
next 指针指向下一个要运行的线程的 PCB(进程控制块),这是通过就绪队列和侵入式链表机制实现的
就绪队列中的存储方式
就绪队列 thread_ready_list 是一个双向链表(struct list),其每个结点是 struct list_elem 类型。但 list_elem 本身并不包含线程的数据,它只是作为嵌入结点存在于每个线程的 PCB 中。PCB 结构体 struct task_struct 中包含两个 list_elem 成员:
struct task_struct {
// ... 其他成员 ...
struct list_elem general_tag; // 用于就绪队列或等待队列
struct list_elem all_list_tag; // 用于全局线程队列
};
当我们把线程加入就绪队列时,实际上是把该线程的 general_tag 结点的地址插入到链表中。如
list_append(&thread_ready_list, &thread->general_tag);
这样,就绪队列的每个结点都对应某个线程的 general_tag 成员。
调度时如何取出下一个线程的 PCB
在 schedule() 函数中,我们通过 list_pop 从就绪队列头部取出一个结点:
thread_tag = list_pop(&thread_ready_list);
list_pop 返回的是 struct list_elem* 类型的指针,它指向被弹出结点的地址(即某个线程的 general_tag 的地址)。但我们真正需要的是包含这个结点的整个 PCB 的地址,即 struct task_struct* 类型。
为了得到 PCB 地址,我们需要使用宏 elem2entry(或 member_to_entry):
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
elem2entry 宏的原理
elem2entry 的定义如下(来自 list.h):
#define offset(struct_type, member_name) (int)(&(((struct_type*)0)->member_name))
#define member_to_entry(struct_type, member_name, member_ptr) \
(struct_type*)((int)member_ptr - offset(struct_type, member_name))
#define elem2entry(type, member, elem_ptr) member_to_entry(type, member, elem_ptr)
offset:计算结构体成员相对于结构体起始地址的偏移字节数。
例如,offset(struct task_struct, general_tag) 返回 general_tag 在 task_struct 中的偏移量(假设为 off)。
member_to_entry:通过成员指针反推结构体起始地址。
已知成员指针 member_ptr 和偏移量 off,则结构体起始地址 = member_ptr - off,然后强制转换为 type*。
因此,elem2entry(struct task_struct, general_tag, thread_tag) 的作用就是:
next = (struct task_struct*)((int)thread_tag - offset(struct task_struct, general_tag));
这样,next 就正确指向了包含该 general_tag 的线程的 PCB。
总结
next 指向下一个线程的 PCB 的过程可以概括为:
调度器从就绪队列头部弹出一个 list_elem 结点(该结点是线程 PCB 中的 general_tag 成员的地址)。
使用 elem2entry 宏,通过该结点地址减去其在 PCB 中的偏移量,计算出包含该结点的 PCB 的起始地址。
将该地址转换为 struct task_struct* 类型,赋值给 next。
init.c
(/kernel/init.c)
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init();
}
print.s
(/lib/kernelprint.s)
在原先的文件中添加下列代码,不要修改原文件
global set_cursor
set_cursor:
pushad
mov bx, [esp+36]
;;;;;;; 1 先设置高8位 ;;;;;;;;
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
popad
ret
print.h
void set_cursor(uint32_t cursor_pos);
interrupt.h
/kernel/interrupt.h
void register_handler(uint8_t vector_no, intr_handler function);
interrupt.c
/kernel/interrupt.c
general_intr_handler:通用的中断/异常处理函数。
跳过伪中断(0x27、0x2f)。
清空屏幕指定区域,打印异常名称。
若为缺页异常(向量14),从 cr2 寄存器获取并打印出错地址。
最后进入死循环,系统暂停。
register_handler:为指定中断向量安装自定义的处理函数,将函数指针存入中断处理函数表 idt_table 中,供中断发生时调用。
加入下列代码
/* 通用的中断处理函数,用于初始化,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { //伪中断向量,无需处理。详见书p337
return;
}
/* 将光标置为0,从屏幕左上角清出一片打印异常信息的区域,方便阅读 */
set_cursor(0);
int cursor_pos = 0;
while(cursor_pos < 320){
put_char(' ');
cursor_pos++;
}
set_cursor(0); // 重置光标为屏幕左上角
put_str("!!!!!!! excetion message begin !!!!!!!!\n");
set_cursor(88); // 从第2行第8个字符开始打印
put_str(intr_name[vec_nr]);
if (vec_nr == 14) { // 若为Pagefault,将缺失的地址打印出来并悬停
int page_fault_vaddr = 0;
asm ("movl %%cr2, %0" : "=r" (page_fault_vaddr)); // cr2是存放造成page_fault的地址
put_str("\npage fault addr is ");put_int(page_fault_vaddr);
}
put_str("\n!!!!!!! excetion message end !!!!!!!!\n");
// 能进入中断处理程序就表示已经处在关中断情况下,
// 不会出现调度进程的情况。故下面的死循环不会再被中断。
while(1);
}
/* 在中断处理程序数组第vector_no个元素中注册安装中断处理程序function */
void register_handler(uint8_t vector_no, intr_handler function) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
* 见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[vector_no] = function;
}
timer.c
/device/timer.c
ticks:全局滴答计数器,记录系统启动后的时钟中断总次数。
intr_timer_handler:时钟中断处理函数
获取当前正在运行的线程PCB,并检查栈魔数以防溢出。
更新当前线程的累计运行时间 elapsed_ticks 和全局 ticks。
若当前线程剩余时间片 ticks 已用尽,则调用 schedule() 触发线程切换;否则将时间片减1。
timer_init:初始化8253定时器,设置中断周期,并将 intr_timer_handler 注册到中断向量0x20(IRQ0),使时钟中断按固定间隔触发,驱动调度器进行时间片轮转。
加入下列代码
#include "interrupt.h"
#include "thread.h"
#include "debug.h"
uint32_t ticks; // ticks是内核自中断开启以来总共的嘀嗒数
/* 时钟的中断处理函数 */
static void intr_timer_handler(void) {
struct task_struct* cur_thread = running_thread();
ASSERT(cur_thread->stack_magic == 0xdeadbeef); // 检查栈是否溢出
cur_thread->elapsed_ticks++; // 记录此线程占用的cpu时间嘀
ticks++; //从内核第一次处理时间中断后开始至今的滴哒数,内核态和用户态总共的嘀哒数
if (cur_thread->ticks == 0) { // 若进程时间片用完就开始调度新的进程上cpu
schedule();
}
else { // 将当前进程的时间片-1
cur_thread->ticks--;
}
}
/* 初始化PIT8253 */
void timer_init() {
put_str("timer_init start\n");
/* 设置8253的定时周期,也就是发中断的周期 */
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
register_handler(0x20, intr_timer_handler);
put_str("timer_init done\n");
}
switch.s
/thread/switch.S
保存当前线程的CPU寄存器:将 esi、edi、ebx、ebp 压入当前线程的内核栈,然后将当前栈顶指针 esp 保存到当前线程的 task_struct 的 self_kstack 字段(位于PCB开头)。
恢复下一个线程的上下文:从参数 next 的PCB中取出 self_kstack 指针,将其赋给 esp,使CPU切换到下一个线程的内核栈;随后从该栈中弹出先前保存的 ebp、ebx、edi、esi,最后通过 ret 指令返回到下一个线程之前被切换时的执行点。

[bits 32]
section .text
global switch_to
switch_to:
;栈中此处是返回地址
push esi ;这4条就是对应压入线程栈中预留的ABI标准要求保存的,esp会保存在其他地方
push edi
push ebx
push ebp
mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20]
mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段,
; self_kstack在task_struct中的偏移为0,
; 所以直接往thread开头处存4字节便可。
;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ----------------
mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24]
mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,
; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针
pop ebp
pop ebx
pop edi
pop esi
ret ; 返回到上面switch_to下面的那句注释的返回地址,
; 未由中断进入,第一次执行时会返回到kernel_thread
main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 8, k_thread_b, "argB ");
intr_enable(); // 打开中断,使时钟中断起作用
while(1) {
put_str("Main ");
};
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
put_str(para);
}
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
put_str(para);
}
}
运行
makefile
#定义一大堆变量,实质就是将需要多次重复用到的语句定义一个变量方便使用与替换
BUILD_DIR=./build
ENTRY_POINT=0xc0001500
HD60M_PATH=/home/chipfesen/bochs/hd60M.img
#只需要把hd60m.img路径改成自己环境的路径,整个代码直接make all就完全写入了,能够运行成功
AS=nasm
CC=gcc-4.4
LD=ld
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/
ASFLAGS= -f elf
CFLAGS= -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32
#-Wall warning wall的意思,产生尽可能多警告信息,-fno-builtin不要采用内部函数,
#-W 会显示警告,但是只显示编译器认为会出现错误的警告
#-Wstrict-prototypes 要求函数声明必须有参数类型,否则发出警告。-Wmissing-prototypes 必须要有函数声明,否则发出警告
LDFLAGS= -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map -m elf_i386
#-Map,生成map文件,就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件
#里面包含函数大小,入口地址等一些重要信息
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)/bitmap.o \
$(BUILD_DIR)/memory.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o $(BUILD_DIR)/switch.o
#顺序最好是调用在前,实现在后
######################编译两个启动文件的代码#####################################
boot:$(BUILD_DIR)/mbr.o $(BUILD_DIR)/loader.o
$(BUILD_DIR)/mbr.o:boot/mbr.s
$(AS) -I boot/include/ -o build/mbr.o boot/mbr.s
$(BUILD_DIR)/loader.o:boot/loader.s
$(AS) -I boot/include/ -o build/loader.o boot/loader.s
######################编译C内核代码###################################################
$(BUILD_DIR)/main.o:kernel/main.c
$(CC) $(CFLAGS) -o $@ $<
# $@表示规则中目标文件名的集合这里就是$(BUILD_DIR)/main.o $<表示规则中依赖文件的第一个,这里就是kernle/main.c
$(BUILD_DIR)/init.o:kernel/init.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/timer.o:device/timer.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/debug.o:kernel/debug.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/string.o:lib/string.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/bitmap.o:kernel/bitmap.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/memory.o:kernel/memory.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/thread.o:thread/thread.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/list.o:lib/kernel/list.c
$(CC) $(CFLAGS) -o $@ $<
###################编译汇编内核代码#####################################################
$(BUILD_DIR)/kernel.o:kernel/kernel.s
$(AS) $(ASFLAGS) -o $@ $<
$(BUILD_DIR)/print.o:lib/kernel/print.s
$(AS) $(ASFLAGS) -o $@ $<
$(BUILD_DIR)/switch.o:thread/switch.s
$(AS) $(ASFLAGS) -o $@ $<
##################链接所有内核目标文件##################################################
$(BUILD_DIR)/kernel.bin:$(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
# $^表示规则中所有依赖文件的集合,如果有重复,会自动去重
.PHONY:mk_dir hd clean build all boot #定义了6个伪目标
mk_dir:
if [ ! -d $(BUILD_DIR) ];then mkdir $(BUILD_DIR);fi
#判断build文件夹是否存在,如果不存在,则创建
hd:
dd if=build/mbr.o of=$(HD60M_PATH) count=1 bs=512 conv=notrunc && \
dd if=build/loader.o of=$(HD60M_PATH) count=4 bs=512 seek=2 conv=notrunc && \
dd if=$(BUILD_DIR)/kernel.bin of=$(HD60M_PATH) bs=512 count=200 seek=9 conv=notrunc
clean:
@cd $(BUILD_DIR) && rm -f ./* && echo "remove ./build all done"
#-f, --force忽略不存在的文件,从不给出提示,执行make clean就会删除build下所有文件
build:$(BUILD_DIR)/kernel.bin
#执行build需要依赖kernel.bin,但是一开始没有,就会递归执行之前写好的语句编译kernel.bin
all:mk_dir boot build hd
#make all 就是依次执行mk_dir build hd
运行后如下图所示,实现了main线程到a线程到b线程再到main线程循环的轮转

本文代码参考自https://github.com/xukanshan/the_truth_of_operationg_system
但list.h文件中上述代码仓库的作者命名有错,此处我进行了修改





