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

实现内核线程

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

相关推荐
bai_lan_ya2 小时前
Linux 输入系统应用编程完全指南
linux·运维·服务器
small_wh1te_coder2 小时前
拷打字节技术总监: 详解c语言嵌入式多线程编程中的头文件 #总结 上下篇合 #
c语言·开发语言·算法·操作系统·嵌入式
UP_Continue3 小时前
Linux--UDP/TCP客户端与服务端模拟实现计算器原理
linux·tcp/ip·udp
FightingHg3 小时前
和claude、openclaw交互的一些杂七杂八记录
linux·运维·服务器
OpenAnolis小助手3 小时前
极速、稳定、丝滑:OpenClaw 接入 Mooncake 后的性能跃迁
操作系统·龙蜥社区·大模型应用·mooncake·sglang·openclaw
深念Y3 小时前
魅蓝Note5 Root + 改内核激活命名空间:让Docker跑在安卓上
android·linux·服务器·docker·容器·root·服务
新兴AI民工3 小时前
【Linux内核二十五】进程管理模块:CFS调度器pick_next_task_fair(一):pick_next_task_fair方法
linux·linux内核
我是一个对称矩阵3 小时前
分区安装Ubuntu系统
linux·运维·ubuntu
小捏哩3 小时前
死锁检测组件的设计
linux·网络·数据结构·c++·后端