《操作系统真象还原》 第九章 线程

实现内核线程

线程(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文件中上述代码仓库的作者命名有错,此处我进行了修改

相关推荐
律宏阔5 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔5 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴5 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千5 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei375 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei375 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei375 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW5 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW5 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei375 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化