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

一、实现内核线程

1. 执行流

  • 调度器:在操作系统中用于把任务轮流调度上CPU运行的一个软件模块。
  • 任务并行:用软件来切换任务,模拟出任务并行的假象。
  • 流:CPU中程序计数器的航向。
  • 执行流的定义:
    • 执行流对应于代码,大到可以是整个程序文件,即进程;小到可以是一个功能独立的代码块,即函数,而线程的本质就是函数。
    • 执行流是独立的,每个执行流都有自己的栈、一套自己的寄存器映像和内存资源。这是Intel处理器在硬件上规定的,是执行流的上下文环境。
    • 任何代码块,无论大小,都可以独立成为执行流。只要在它运行前,给它准备好上下文环境就行。
    • 执行流的本质是进程和线程。
  • 执行流的作用:
    • 在任务调度器眼中,只有执行流才是调度单元。
    • CPU上的每个任务都是调度器给分配的执行流,只要成为执行流就可以独立上CPU运行了。

2. 线程到底是什么

  • 线程的本质是执行流,是一段引导CPU执行的、具有能动性的代码。
  • 例子:thead.test
#include<stdio.h>
#include<pthread.h>

void* thread_func(void* _arg){
	unsigned int* arg = _arg;
	printf(" new thread : my tid is %u\n",*arg);
}

void main(){
	pthread_t new_thread_id;
	pthread_create(&new_thread_id,NULL,thread_func,&new_thread_id);
	printf("main thread : my tid is %u\n",pthread_self());
	usleep(100);
}
  • pthread_create的函数原型:
int pthread_create(pthread_t *__restrict __newthread,_const pthread__attr_t *__restrict __attr,void *(*__start_routine)(void*),void *__restrict __arg) __THROW __nonnull((1,3));
参数名称作用
__newthread用于存储新线程的id,即uid
__attr用于指定线程的类型,默认类型为NULL
__start_routine函数指针,用于指向线程中所调用的函数的地址
__arg给在线程中运行的函数__start_routine的参数,如果有2个及以上的参数,则封装为一个结构体
  • 若pthread_create函数返回值为0,则线程创建成功。
    在这里插入图片描述
  • 线程的实际功能:调用函数。
  • 线程和函数调用的区别:
    1. 执行流上CPU前,要准备好上下文环境。
    2. 函数调用是随着该函数所在的调度单元(执行流)一起上CPU的,是被顺便处理的。
      在这里插入图片描述
  • CPU不是把线程中调用的函数和其他指令混合在一起执行的,而是专门、单独地执行了函数(线程)。

3. 进程与线程的关系

  • 程序与进程
    1. 程序是指静态的、存储在文件系统上、尚未运行的指令代码。
    2. 进程是指正在运行的程序,程序必须在获得运行所需要的各类资源后才能成为进程。资源包括进程所使用的栈和寄存器等。
  • 进程分为单线程进程和多线程进程。如果未显示创建线程,则为单线程进程。
  • 进程和线程
    1. 对于CPU而言,进程是一种控制流集合,集合中至少包含一条执行流,执行流之间是相互独立的,但它们共享进程的所有资源,它们是CPU的执行单元,调度单位,即线程。
    2. 进程有独立的虚拟地址空间,有资源。各进程无法访问对方内部,这是由操作系统的分页机制保证的。
    3. 线程没有独立的地址空间,没有资源,所有线程共用进程的地址空间和资源。
    4. 进程和线程都是执行流,都具备独立寄存器资源和独立栈空间。因此线程可以调用函数。
    5. 在CPU上运行的执行流都是人为划分的逻辑上独立的代码段,本质都是一段代码区域,只不过线程是纯粹的执行部分。它运行所需要的资源都在进程中。进程 = 线程 + 资源。
  • 若显示创建了线程,则任务调度器会将它对应的代码块从进程中分离出来单独调度上CPU执行;否则调度器会将整个进程当作一个大的执行流,从头到尾去执行。
  • 程序员写程序时会将整个任务划分为几个独立的部分,每一部分就用线程完成,各部分是独立的,可以并行完成。(A部分依赖用户输入,B部分不依赖)
  • 书后面提到的进程,都是指单线程进程。

4. 进程、线程的状态

状态名称状态
运行态正在CPU上运行的进程的状态
就绪态外界条件成立,进程可以随时准备运行的状态
阻塞态需要等待外界条件的状态
  • 调度器的调度单位是执行流,所以状态是对于执行流而言的,状态是描述线程的。

5. 进程的身份证——PCB

  • PCB:程序控制块,用来记录与进程有关的信息,如进程状态、PID、优先级等。
  • 进程表:所有PCB放到一张表中维护,调度器可以根据此表选择上CPU运行的进程。PCB故又称为进程表项。
    在这里插入图片描述
PCB结构功能
进程状态保存进程的状态
时间片时间片=0时下CPU
页表代表进程的地址空间
寄存器映像保存进程的现场,进程在CPU上运行时,所有寄存器的值保存在此
栈指针寄存器映像的位置不固定,栈指针记录0级栈栈顶的位置,借此找到进程的“寄存器映像”
  • 寄存器映像的位置不固定,因为寄存器映像存储在内核栈中:
    1. 当进程/线程中断时,CPU自动在TSS中获取内核栈指针,。因此通常情况下,寄存器映像位于PCB顶端。
    2. 当在内核态下工作时,栈指针已经发生了变化时才向栈中保存寄存器映像,例如线程主动让出CPU。此时寄存器映像必然不在PCB顶端了。

6. 实现线程的两种方式——内核或用户进程

  • 线程的分类:
    1. 在0特权级的内核空间中实现线程:线程机制由内核提供,并不是说线程所运行的代码也必须是0特权级的内核级代码。内核毕竟是为用户进程提供服务的。
    2. 在3特权级的内核空间中实现线程:线程机制由用户进程自己提供,相当于用户进程除了负责业务外,还要在进程中实现线程调度器。所以标准库提供了用户级线程库,程序员直接使用标准线程库即可。
  • 用户特权级是3,线程中只能运行自己进程内的代码,只能同级调用,不能调用0特权级的内核代码。
  • 线程仅仅是个执行流,在哪里实现取决于线程表在哪里,由谁来调度它上CPU。如果线程在用户空间中实现,则线程表在用户进程中,用户进程就要专门写个线程用作线程调度器;如果线程是在内核空间中实现的,线程表在内核中,该线程就会由OS的调度器统一调度。
    在这里插入图片描述
  1. 在用户空间中实现线程
    • 线程调度器:开发人员在用户进程中调用线程库来创建线程、结束线程等。线程库中一定存在线程调度器,线程库中的方法都会与此线程调度器有调度关系。当有新线程产生/退出时,线程调度器才会被调用,从而在内部维护的线程表中找出下一个线程上CPU。
    • 优点:
      1. 可移植性强,由于是用户级的实现,所以在不支持线程的OS上也可以写出支持线程的用户程序。
      2. 有用户实现,可以根据实际情况为某线程加权调度。
      3. 将线程的寄存器映像装载到CPU时,可以在用户空间完成,不用陷入到内核态,免去了进入内核时的入栈和出栈操作。(恢复上下文)
    • 缺点:
      1. 若进程中某线程阻塞,OS不知道进程中存在线程,会将该进程挂起。
      2. 线程未在内核空间中实现,调度器的调度单元是整个进程,不是线程。所以时钟中断只能影响进程一级的执行流,但凡进程中的某个线程开始在CPU上执行,只要该线程不主动退出CPU,该进程中的其他线程就没机会运行。
      3. 用户级线程只在内部调度时少了陷入内核的代价,但是整个进程的时间片有限,有限的时间片要分出时间给进程内线程调度器维护线程表、运行调度算法等时间开销,反而会降速。
  2. 在内核空间中实现线程
    • 优点:
      1. 内核级线程相当于让进程多占了CPU资源。
      2. 当一个线程阻塞时,只会阻塞这一个线程。

二、在内核空间实现线程

1. 简单的PCB及线程栈的实现

--------------------------代码----------------------------

  • “/home/lily/OS/boot/thread/thread.h”
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"

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

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

/*中断栈intr_stack
用于中断发生时保护程序上下文环境:
进程/线程被外部中断/软中断打断时,会按照此结构压入上下文
寄存器,intr_exit中的出栈操作是此结构的逆操作
此栈在线程自己的内核栈中位置固定,所在页的最顶端
*/
struct intr_stack{
    uint32_t vec_no;    //kernel.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指向待调用函数kernel_thread,
    其他时候,eip指向switch_to的返回地址*/
    void (*eip) (thread_func* func,void* func_arg);

    /*以下仅供第一次被调度上CPU时使用*/

    /*参数unused_ret只为占位置充数为返回地址*/
    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;       //栈的边界标记,用于检测栈的溢出
};
#endif

2. 线程的实现

在这里插入图片描述

--------------------------代码----------------------------

  • “/home/lily/OS/boot/thread/thread.h”
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"

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

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

/*中断栈intr_stack
用于中断发生时保护程序上下文环境:
进程/线程被外部中断/软中断打断时,会按照此结构压入上下文
寄存器,intr_exit中的出栈操作是此结构的逆操作
此栈在线程自己的内核栈中位置固定,所在页的最顶端
*/
struct intr_stack{
    uint32_t vec_no;    //kernel.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指向待调用函数kernel_thread,
    其他时候,eip指向switch_to的返回地址*/
    void (*eip) (thread_func* func,void* func_arg);

    /*以下仅供第一次被调度上CPU时使用*/

    /*参数unused_ret只为占位置充数为返回地址*/
    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;       //栈的边界标记,用于检测栈的溢出
};

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

/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg);

/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio);

/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg);
#endif
  • “/home/lily/OS/boot/thread/thread.c”
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"

#define PG_SIZE 4096

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

/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg){
    /*先预留中断使用栈的空间*/
    pthread->self_kstack -= sizeof(struct intr_stack);

    /*再预留出线程栈空间*/
    pthread->self_kstack -= sizeof(struct thread_stack);
    struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;    //线程栈指针
    kthread_stack->eip = kernel_thread;
    kthread_stack->function = function;
    kthread_stack->func_arg = func_arg;
    kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0;  //寄存器初始化为0
}

/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio){
    memset(pthread,0,sizeof(*pthread));
    strcpy(pthread->name,name);
    pthread->status = TASK_RUNNING;     //为了演示,直接设置为运行态
    pthread->priority = prio;
    /*self_kstack是线程自己在内核态下使用的栈顶地址*/
    pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
    pthread->stack_magic = 0x19870916;
}

/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg){
    /*pcb都位于内核空间,包括用户进程的pcb也在内核空间*/
    struct task_struct* thread = get_kernel_pages(1);
    init_thread(thread,name,prio);
    thread_create(thread,function,func_arg);
    asm volatile("movl %0,%%esp; \
    pop %%ebp;pop %%ebx;pop %%edi;pop %%esi; \
    ret": : "g"(thread->self_kstack) : "memory");
    //esp = thread->self_kstack为栈顶;4个出栈,将初始化的0弹出到相应寄存器中;执行ret,将栈顶数据作为返回地址送入CPU的eip中;thread->self_kstack为输入
    return thread;
}
  • “/home/lily/OS/boot/kernel/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)
	{
		put_str(para);
	}
	
}
  • “/home/lily/OS/boot/makefile”
BUILD_DIR = ./build
##用来存储生成的所有目标文件
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
##-fno-builtin是告诉编译器不要采用内部函数 -Wstrict-prototypes是要求函数声明中必须有参数类型 
## -Wmissing-prototypes要求函数必须有声明
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
	$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/debug.o \
	$(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序

########## c代码编译 ##########
$(BUILD_DIR)/main.o:kernel/main.c lib/kernel/print.h lib/kernel/stdint.h kernel/init.h kernel/memory.h \
	thread/thread.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/init.o:kernel/init.c kernel/init.h lib/kernel/print.h lib/kernel/stdint.h \
	kernel/interrupt.h device/timer.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/interrupt.o:kernel/interrupt.c kernel/interrupt.h lib/kernel/stdint.h \
	kernel/global.h lib/kernel/io.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/timer.o:device/timer.c device/timer.h lib/kernel/stdint.h lib/kernel/io.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/thread.o:thread/thread.c thread/thread.h lib/kernel/stdint.h lib/string.h kernel/global.h kernel/memory.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/debug.o:kernel/debug.c kernel/debug.h lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/string.o:lib/string.c lib/string.h kernel/debug.h kernel/global.h lib/kernel/stdint.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/memory.o:kernel/memory.c kernel/memory.h lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h \
	lib/kernel/print.h kernel/global.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/bitmap.o:lib/kernel/bitmap.c lib/kernel/bitmap.h lib/string.h kernel/interrupt.h lib/kernel/print.h \
	kernel/debug.h lib/kernel/stdint.h
	$(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 all

mk_dir:
	if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi	
###fi为终止符

hd:	
	dd if=$(BUILD_DIR)/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

clean:	##将build目录下文件清空
	cd $(BUILD_DIR) && rm -f ./*

build:$(BUILD_DIR)/kernel.bin	##编译kernel.bin,只要执行make build就是编译文件

all:mk_dir build hd	
##依次执行伪目标mk_dir build hd,只要执行make all就是完成了编译到写入硬盘的全过程
  • 编译运行
    在这里插入图片描述
    在这里插入图片描述

三、核心数据结构,双向链表

--------------------------代码----------------------------

  • “/home/lily/OS/boot/lib/kernel/list.h”
#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "global.h"

#define offset(struct_type,member) (int)(&((struct_type*)0)->member)
#define elem2entry(struct_type,struct_member_name,elem_ptr)(struct_type*)((int)elem_ptr - offset(struct_type,struct_member_name))

/*定义链接节点成员结构,链表中不需要数据成员,只需要前驱和后继结点指针*/
struct list_elem{
    struct list_elem* prev; //前驱结点
    struct list_elem* next; //后继结点
};

/*链表结构,用来实现队列*/
struct list{
    struct list_elem head;  //队首,是固定不变的,不是第一个元素,第一个元素为head.next
    struct list_elem tail;  //队尾,同样是固定不变的
};

/*自定义函数类型function,用于在list_traversal中做回调函数*/
typedef bool (function)(struct list_elem*,int arg);

void list_init(struct list*);
void list_insert_before(struct list_elem* before,struct list_elem* elem);
void list_push(struct list* plist,struct list_elem* elem);
void list_iterate(struct list* plist);
void list_append(struct list* plist,struct list_elem* elem);
void list_remove(struct list_elem* pelem);
struct list_elem* list_pop(struct list* plist);
bool list_empty(struct list* plist);
uint32_t list_len(struct list* plist);
struct list_elem* list_traversal(struct list* plist,function func,int arg);
bool elem_find(struct list* plist,struct list_elem* obj_elem);
#endif
  • “/home/lily/OS/boot/lib/kernel/list.c”
#include "list.h"
#include "interrupt.h"
#include "stdint.h"
#include "string.h"

/*初始化双向链表list*/
void list_init(struct list* list){
    list->head.prev = NULL;
    list->head.next = &list->tail;  //list->tail.prev表示访问链表尾节点的prev指针。而&list->head表示获取链表头节点的地址。
    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();   //关中断

    before->prev->next = elem;
    elem->prev = before->prev;
    elem->next = before;
    before->prev = elem;

    intr_set_status(old_status);    //恢复中断
}

/*添加元素到队列队首,类似栈push操作*/
void list_push(struct list* plist,struct list_elem* elem){
    list_insert_before(plist->head.next,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){
    if(list_empty(plist))
        return NULL;
    struct list_elem* elem = plist->head.next;
    while (elem != &plist->tail){
        if(func(elem,arg))
            return elem;
        elem = elem->next;
    }
    return NULL;
}

/*返回链表长度*/
uint32_t list_len(struct list* plist){
    uint32_t length = 0;
    struct list_elem* elem = plist->head.next;
    while(elem != &plist->tail){
        length++;
        elem = elem->next;
    }
    return length;
}

/*判断链表是否为空*/
bool list_empty(struct list* plist){
    return (plist->head.next == &plist->tail ? true : false);
}

四、多线程调度

1. 简单优先级调度的基础

在这里插入图片描述

--------------------------代码----------------------------

  • “/home/lily/OS/boot/thread/thread.h”
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"

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

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

/*中断栈intr_stack
用于中断发生时保护程序上下文环境:
进程/线程被外部中断/软中断打断时,会按照此结构压入上下文
寄存器,intr_exit中的出栈操作是此结构的逆操作
此栈在线程自己的内核栈中位置固定,所在页的最顶端
*/
struct intr_stack{
    uint32_t vec_no;    //kernel.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指向待调用函数kernel_thread,
    其他时候,eip指向switch_to的返回地址*/
    void (*eip) (thread_func* func,void* func_arg);

    /*以下仅供第一次被调度上CPU时使用*/

    /*参数unused_ret只为占位置充数为返回地址*/
    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;              //每次在处理器上的嘀咕数
    uint32_t elapsed_ticks;     //此任务自上CPU运行至今用了多少CPU嘀咕数,也就是任务运行了多久
    struct list_elem general_tag;       //用于线程在一般队列中的节点
    struct list_elem all_list_tag;      //用于线程队列thread_all_list中的结点
    uint32_t* pgdir;            //进程自己页表的虚拟地址,如果是线程则为NULL
    uint32_t stack_magic;       //栈的边界标记,用于检测栈的溢出
};

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

/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg);

/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio);

/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg);
#endif
  • “/home/lily/OS/boot/thread/thread.c”
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.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)的值存储到变量esp中。
    return (struct task_struct*)(esp & 0xfffff000);     //取esp整数部分,即pcb起始地址
}

/*由kernel_thread去执行function(func_arg)*/
static void kernel_thread(thread_func* funcion,void* func_arg){
    intr_enable();      //开中断,避免后面的时钟中断被屏蔽,而无法调度其他线程
    funcion(func_arg);
}

/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg){
    /*先预留中断使用栈的空间*/
    pthread->self_kstack -= sizeof(struct intr_stack);

    /*再预留出线程栈空间*/
    pthread->self_kstack -= sizeof(struct thread_stack);
    struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;    //线程栈指针
    kthread_stack->eip = kernel_thread;
    kthread_stack->function = function;
    kthread_stack->func_arg = func_arg;
    kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0;  //寄存器初始化为0
}

/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio){
    memset(pthread,0,sizeof(*pthread));
    strcpy(pthread->name,name);
    if(pthread == main_thread)
        pthread = TASK_RUNNING;
    else
        pthread = TASK_READY;

    /*self_kstack是线程自己在内核态下使用的栈顶地址*/
    pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
    pthread->priority = prio;
    pthread->ticks = prio;
    pthread->elapsed_ticks = 0;
    pthread->pgdir = NULL;
    pthread->stack_magic = 0x19870916;
}

/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg){
    /*pcb都位于内核空间,包括用户进程的pcb也在内核空间*/
    struct task_struct* thread = get_kernel_pages(1);
    init_thread(thread,name,prio);
    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);

    // asm volatile("movl %0,%%esp; \
    // pop %%ebp;pop %%ebx;pop %%edi;pop %%esi; \
    // ret": : "g"(thread->self_kstack) : "memory");
    // //esp = thread->self_kstack为栈顶;4个出栈,将初始化的0弹出到相应寄存器中;执行ret,将栈顶数据作为返回地址送入CPU的eip中;thread->self_kstack为输入
    return thread;
}

/*将kernel中的main函数完善为主线程*/
static void make_main_thread(void){
    /*因为main线程早已在运行,咱们在loader.S中进入内核时的mov esp,0xc009f000,就是为其预留pcb的。
    因为pcb的地址为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);
}

2. 任务调度器和任务切换

  • 调度器的主要任务是读写就绪队列,增删里面的结点。结点是pcb中的general_tag,相当于线程的pcb,从队列中将其取出时一定要还原成pcb才行。
  • 调度方式:RR,轮询调度。
  • 调度器按照先进先出的顺序,把就绪队列中的第一个结点作为下一个要运行的新线程,将该线程的状态设置为TASK_RUNNING,之后通过函数switch_to将新线程的寄存器环境恢复,使新线程开始执行。
  • 完整的调度过程需要三部分的配合:
    1. 时钟中断处理函数。
    2. 调度器schedule
    3. 任务切换函数switch_to
  • 用户态和内核态:
    • 程序所做的完整工作可以分为两部分,一部分是“重要工作”,这由OS代码实现;另一部分是“普通工作”,这由用户代码完成。
    • 完整的程序 = 用户代码 + 内核代码。
    • 任务在执行的过程中会执行用户代码和内核代码,当CPU处于低特权级下执行用户代码时我们称为用户态;当CPU进入高特权级执行内核代码时,我们称为内核态。
    • 当CPU从用户代码所在的低特权级->内核代码所在的高特权级时,这称为陷入内核。
    • 无论是执行用户代码还是内核代码,这些代码都属于一个完整的程序,并不是说当前任务由用户态进入内核态后当前任务就切换成内核了。
    • 任务与任务的区别在于执行流一整套的上下文资源,包括寄存器映像、地址空间、IO位图等,拥有这些资源才称得上是任务。因此,CPU只有被新的上下文资源重新装载后,当前任务才被替换成新的任务,这叫任务切换。
    • 当程序执行代码时,它需要通过系统调用让内核帮忙完成。
  • 保护任务上下文
    • 进入中断时的保护:保护任务的全部寄存器映像。当恢复寄存器后,如果此任务是用户进程,任务就恢复为用户程序继续在用户态下执行;如果该任务是内核线程,任务就恢复为另一段被中断执行的内核代码,在内核态执行。
    • 保护内核环境的上下文:除esp外,只保护esi、edi、ebx和ebp。即恢复为在内核的中断处理程序中继续执行的状态,并不是让任务恢复到中断前,依然还在内核中。这几个寄存器会让CPU把程序执行到内核代码的结束处,在那里可以用第一部分中保护的全部寄存器映像来恢复任务,从而退出中断,使任务彻底恢复为进入中断前的状态。
  • 中断发生时,当前运行的任务(线程/用户进程)被打断,随后会去执行中断处理程序,不管当前任务在中断前的特权级是什么,执行中断处理程序时肯定是0特权级。因此进入中断后所执行的一切内核代码也依然属于当前任务,只是由内核来提供这一部分罢了。
    在这里插入图片描述

--------------------------代码----------------------------

  • “/home/lily/OS/boot/kernel/interrupt.c”
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"

#define IDT_DESC_CNT 0x21           //目前共支持的中断数,33
#define PIC_M_CTRL 0x20             //主片的控制端口是0x20
#define PIC_M_DATA 0x21             //主片的数据端口是0x21
#define PIC_S_CTRL 0xa0             //从片的控制端口是0xa0
#define PIC_S_DATA 0xa1             //从片的数据端口是0xa1
#define EFLAGS_IF 0x00000200	//eflags寄存器中的if=1
#define GET_FLAGS(EFLAG_VAR) asm volatile("pushfl;popl %0":"=g"(EFLAG_VAR))	
//"=g" 指示编译器将结果放在任意通用寄存器中,并将其赋值给 EFLAG_VAR。
//pushfl 指令将标志寄存器 EFLAGS 的值入栈,然后 popl %0 指令将栈顶的值弹出到指定的操作数 %0 中。
//当调用 GET_FLAGS(EFLAG_VAR) 宏时,它将 EFLAGS 寄存器的值存储到 EFLAG_VAR 变量中。

/*中断门描述符结构体*/
struct gate_desc{
    uint16_t func_offset_low_word;  //低32位——0~15位:中断处理程序在目标代码段内的偏移量的第15~0位
    uint16_t selector;              //低32位——16~31位:目标CS段选择子
    uint8_t dcount;                 //高32位——0~7位:此项为双字计数字段,是门描述符中的第4字节,为固定值
    uint8_t attribute;              //高32位——8~15位:P+DPL+S+TYPE
    uint16_t func_offset_high_word; //高32位——16~31位:中断处理程序在目标代码段内的偏移量的第16~31位
};

//静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT];  //IDT是中描述符表,实际上是中断门描述符数组

extern intr_handler intr_entry_table[IDT_DESC_CNT]; //指针格式应该与数组类型一致,这里intr_entry_table中的元素类型就是function,就是.text的地址。所以用intr_handle来引用。声明引用定义在kernel.S中的中断处理函数入口数组
char* intr_name[IDT_DESC_CNT];      //用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT];   //idt_table为函数数组,里面保持了中断处理函数的指针

/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc,uint8_t attr,intr_handler function){ //intr_handler是个空指针类型,仅用来表示地址 
    //中断门描述符的指针、中断描述符内的属性、中断描述符内对应的中断处理函数
    p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000ffff;
    p_gdesc->selector = SELECTOR_K_CODE;
    p_gdesc->dcount = 0;
    p_gdesc->attribute = attr;
    p_gdesc->func_offset_high_word = ((uint32_t)function & 0xffff0000) >> 16;
}

/*初始化中断描述符表*/
static void idt_desc_init(void){
    int i;
    for(i = 0;i < IDT_DESC_CNT;i++){
        make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]);
    }
    put_str("   idt_desc_init done\n");
}

/*初始化可编程中断控制器8259A*/
static void pic_init(void){
    /*初始化主片*/
    outb(PIC_M_CTRL,0x11);          //ICW1:边沿触发,级联8259,需要ICW4
    outb(PIC_M_DATA,0x20);          //ICW2:起始中断向量号为0x20,也就是IR[0-7]为0x20~0x27
    outb(PIC_M_DATA,0x04);          //ICW3:IR2接从片
    outb(PIC_M_DATA,0x01);          //ICW4:8086模式,正常EOI
    /*初始化从片*/
    outb(PIC_S_CTRL,0x11);          //ICW1:边沿触发,级联8259,需要ICW4
    outb(PIC_S_DATA,0x28);          //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28~0x2F
    outb(PIC_S_DATA,0x02);          //ICW3:设置从片连接到主片的IR2引脚
    outb(PIC_S_DATA,0x01);          //ICW4:8086模式,正常EOI
    /*打开主片上IR0,也就是目前只接受时钟产生的中断*/
    outb(PIC_M_DATA,0xfe);
    outb(PIC_S_DATA,0xff);
    put_str("   pic_init done\n");
}

// /*通用的中断处理函数,一般用在异常出现时的处理*/
// static void general_intr_handler(uint8_t vec_nr){
//     if(vec_nr == 0x27 || vec_nr == 0x2f){   //IRQ7和IRQ5会产生伪中断,无需处理;0x2f是从片8259A的最后一个IRQ引脚,保留项
//         return;
//     }
//     //将光标置零,从屏幕左上角清出一片打印异常信息的区域,方便阅读
//     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_vadddr = 0;
//         asm("movl %%cr2,%0" : "=r"(page_fault_vadddr));     //将cr2的值转存到page_fault中。cr2是存放造成page_fault的地址
//         put_str("\npage fault addr is ");
//         put_int(page_fault_vadddr);
//     }
//     put_str("!!!!!!! excetion message end !!!!!!!\n");
//     //能进入中断处理程序就表示已经处在关中断情况下
//     //不会出现调度进程的情况,故下面的死循环不会再被打断
//     while (1);
// }

/* 通用的中断处理请求 */
static void general_intr_handler(uint8_t vec_nr) {
    if (vec_nr == 0x27 || vec_nr == 0x2f) {
        // IRQ7 IRQ15 会产生伪中断, 无需处理
        // 0x2f 是从片 8259A 上的最后一个 IRQ 引脚,保留项
        return;
    }

    // 将光标置为屏幕左上角, 清理一块区域
    set_cursor(0);      // 设置光标位置
    int cursor_pos = 0;
    while(cursor_pos < 320) {
        // 清空四行
        put_char(' ');
        cursor_pos++;
    }
    // 将光标重新置为屏幕左上角
    set_cursor(0);
    put_str("!!!!! exception message begin !!!!!\n");
    set_cursor(88);     // 从第 2 行第 8 个字符开始打印
    put_str(intr_name[vec_nr]);
    if(vec_nr == 14) {
        // 若为 Pagefault, 将缺失的地址打印出来并悬停
        int page_fault_vaddr = 0;
        // cr2 存放造成 page_fault 的地址
        asm("movl %%cr2, %0" : "=r"(page_fault_vaddr));
        put_str("\npage fault addr is ");
        put_int(page_fault_vaddr);
    }

    put_str("\n!!!!! exception message end !!!!!\n");

    // 已经进入中断处理程序就表示已经处在关中断情况下
    // 不会出现线程调度的情况, 故下面的死循环不会再被中断
    // 将程序悬停在此, 便于观察报错信息
    while(1);
}


/*初始化idt_table*/
static void exception_init(void){
    //将idt_table的元素都指向通用的中断处理函数,名称为unknown
    int i;
    for(i = 0;i < IDT_DESC_CNT;i++){
        idt_table[i] = general_intr_handler;    //默认为general_intr_handler,以后会由register_handler来注册具体函数
        intr_name[i] = "unknown";
    }
    intr_name[0] = "#DE Divide Error";
    intr_name[1] = "#DB Debug Exception";
    intr_name[2] = "NMI Interrupt";
    intr_name[3] = "BP Breakpoint Exception";
    intr_name[4] = "#OF Overflow Exception";
    intr_name[5] = "#BR BOUND Range Exceeded Exception";
    intr_name[6] = "#UD Invalid Opcode Exception";
    intr_name[7] = "#NM Device Not Available Exception";
    intr_name[8] = "#DF Double Fault Exception";
    intr_name[9] = "Coprocessor Segment Overrun";
    intr_name[10] = "#TS Invalid TSS Exception";
    intr_name[11] = "#NP Segment Not Present";
    intr_name[12] = "#SS Stack Fault Exception";
    intr_name[13] = "#GP General Protection Exception";
    intr_name[14] = "#PF Page-Fault Exception";
    //intr_name[15]是intel保留项,未使用
    intr_name[16] = "#MF x87 FPU Floating-Point Error";
    intr_name[17] = "#AC Alignment Check Exception";
    intr_name[18] = "#MC Machine-Check Exception";
    intr_name[19] = "#XF SIMD Floating-Point Exception";
}

/*完成所有有关中断的初始化工作*/
void idt_init(){
    put_str("idt_init start\n");
    idt_desc_init();                //初始化中断描述符表
    exception_init();
    pic_init();                     //初始化8259A
    /*加载idt*/
    // uint64_t idt_operand = ((sizeof(idt)-1) | (uint64_t)((uint32_t)idt << 16)); //书上是错的
    uint64_t idt_operand = ((sizeof(idt)-1) | ((uint64_t)(uint32_t)idt << 16)); //低16位是idt的大小,高48位是IDT的基址。因为idt是32位,左移16位后会丢失高16位,所以先转换为64位再左移
    asm volatile("lidt %0" : : "m" (idt_operand));   //加载IDT,IDT的0~15位是表界限,16~47位是表基址
    put_str("idt_init done\n");
}

/*开中断,并返回开中断前的状态*/
enum intr_status intr_enable(){
	enum intr_status old_status;
	if(INTR_ON == intr_get_status()){
		old_status = INTR_ON;
		return old_status;
	}else{
		old_status = INTR_OFF;
		asm volatile("sti");	//开中断
		return old_status;
	}
}

/*关中断,并返回关中断前的状态*/
enum intr_status intr_disable(){
	enum intr_status old_status;
	if(INTR_ON == intr_get_status()){
		old_status = INTR_ON;
		asm volatile("cli" : : : "memory");	//关中断,cli指令将IF位置0
		return old_status;
	}else{
		old_status = INTR_OFF;
		return old_status;
	}
}

/*将中断状态设置为status*/
enum intr_status intr_set_status(enum intr_status status){
	return status & INTR_ON ? intr_enable() : intr_disable();
}

/*获取当前中断状态*/
enum intr_status intr_get_status(){
	uint32_t eflags = 0;
	GET_FLAGS(eflags);
	return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}

/*在中断处理程序数组第vector_no个元素中注册安装中断处理程序function*/
void register_handler(uint8_t vector_no,intr_handler function){
    idt_table[vector_no] = function;    //idt_table数组中的函数是在进入中断后根据中断向量号调用的
}
  • “/home/lily/OS/boot/kernel/interrupt.h”
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;

/*定义中断的两种状态:
INTR_OFF值为0,表示关中断
INTR_ON 值为1,表示开中断*/
enum intr_status{
	INTR_OFF = 0,
	INTR_ON = 1
};
//枚举常量在 C 语言中被赋予默认的整数值,按照声明的顺序从 0 开始递增。因此,在这个例子中,INTR_OFF 的值为 0,INTR_ON 的值为 1。也可以显式地为枚举常量指定特定的值。

// enum intr_status intr_get_status(void);
// enum intr_status intr_set_status(enum intr_status);
// enum intr_status intr_enable(void);
// enum intr_status intr_disable(void);

/*完成所有有关中断的初始化工作*/
void idt_init(void);

/*开中断,并返回开中断前的状态*/
enum intr_status intr_enable(void);

/*关中断,并返回关中断前的状态*/
enum intr_status intr_disable(void);

/*将中断状态设置为status*/
enum intr_status intr_set_status(enum intr_status status);

/*获取当前中断状态*/
enum intr_status intr_get_status(void);

/*在中断处理程序数组第vector_no个元素中注册安装中断处理程序function*/
void register_handler(uint8_t vector_no,intr_handler function);
#endif
  • “/home/lily/OS/boot/lib/kernel/print.S”
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

section .data 
put_int_buffer dq 0             ;定义8字节缓冲区用于数字到字符的转换

[bits 32]
section .text
;------------------------------------------
;put_str通过put_char来打印以0字符结尾的字符串
;------------------------------------------
;输入:栈中参数为打印的字符串
;输出:无

global put_str 
put_str:
;由于函数只用到了ebx和ecx两个寄存器,所以只备份这两个
    push ebx 
    push ecx 
    xor ecx,ecx                 ;准备用ecx存储参数,清空
    mov ebx,[esp+12]            ;从栈中得到待打印的字符串地址(传入的参数)
.goon:
    mov cl,[ebx]                
    cmp cl,0                    ;如果处理到了字符串尾,则跳到结束时返回
    jz .str_over 
    push ecx                    ;为put_char传递参数,把ecx的值入栈
    call put_char               ;call时会把返回地址入栈4
    add esp,4                   ;回收参数的栈空间
    inc ebx                     ;使ebx指向下一个字符
    jmp .goon
.str_over:
    pop ecx 
    pop ebx 
    ret 

;-----------put_char---------------
;功能描述:把栈中的1个字符写入光标所在处
;----------------------------------
global put_char                 ;将put_char导出为全局符号
put_char:
    pushad                      ;备份32位寄存器环境,push all double,将832位寄存器都备份了。它们入栈的顺序为:EAX->ECX->EDX->EBX->ESP->EBP->ESI->EDI
    mov ax,SELECTOR_VIDEO       ;需要保证gs中为正确的视频段选择子,为保险起见,每次打印都为gs赋值
    mov gs,ax                   ;不能直接把立即数送入段寄存器
    ;;;;;获取当前光标的位置;;;;;
    ;先获取高8位
    mov dx,0x03d4               ;索引寄存器,03d4为Address Register,用于索引寄存器。
    mov al,0x0e                 ;用于提供光标位置的高8位
    out dx,al
    mov dx,0x03d5               ;03d5是Data Register;可以写数据和读数据。通过读写数据端口0x3d5来获取/设置光标的位置
    in al,dx                    ;得到光标位置的高8位
    mov ah,al                   ;将得到的光标高8位放入ah中

    ;再获取低8位
    mov dx,0x03d4
    mov al,0x0f                 ;用于提供光标位置的低8位
    out dx,al 
    mov dx,0x03d5 
    in al,dx 

    ;将光标位置存入bx,bx寄存器习惯性作为基址寻址。此时bx是下一个字符的输出位置。
    mov bx,ax 
    ;获取栈中压入字符的ASCII码
    mov ecx,[esp + 36]          ;pushad压入8*32b=32字节,加上主调函数4B的返回地址。故栈顶偏移36字节。
    ;判断字符是什么类型
    cmp cl,0xd                  ;CR是0x0d,回车键
    jz .is_carriage_return 
    cmp cl,0xa                  ;LF是0x0a,换行符
    jz .is_line_feed 

    cmp cl,0x8                  ;BS是0x08,退格键
    jz .is_backspace 
    jmp .put_other 

.is_backspace:                  ;理论上将光标移到该字符前即可,但怕下个字符为回车等,原字符还留着当地,所以用空格/空字符0替代原字符
    dec bx                      ;bx值-1,光标指向前一个字符
    shl bx,1                    ;左移一位等于乘2,表示光标对应显存中的偏移字节
    mov byte [gs:bx],0x20       ;0x20表示空格
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;0x07表示黑屏白字,这是显卡默认的前景色和背景色,不加也行。
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    jmp .set_cursor             ;设置光标位置

.put_other:                     ;处理可见字符
    shl bx,1                    ;光标左移1位等于乘2,表示光标位置
    mov [gs:bx],cl              ;将ASCII字符放入光标位置中
    inc bx                      ;bx+1
    mov byte [gs:bx],0x07       ;字符属性,黑底白字
    shr bx,1                    ;右移一位表示除以2取整,bx由显存的相对地址恢复到光标位置
    inc bx                      ;bx+1,下一个光标值
    cmp bx,2000                 ;看是否需要滚屏
    jl .set_cursor              ;"JL""jump if less"(如果小于则跳转):若光标值<=2000,表示未写到。显存的最后,则去设置新的光标值,若超过屏幕字符数大小(2000),则换行(滚屏)。

.is_line_feed:                  ;是换行符LF(\n)
.is_carriage_return:            ;是回车键CR(\r),\n和\r在Linux中都是\n的意思。
    xor dx,dx                   ;dx是被除数的高16位,清零
    mov ax,bx                   ;ax是被被除数的低16位,bx是光标位置
    mov si,80                   ;si = 80为除数
    div si                      ;80取模,(dx + ax)/si = ax() + dx(余数) 即bx/80=几行(ax) + 第几列(dx)
    ;如果除数是16位,被除数就是32位,位于dx和ax(高16位,低16位)中;结果的商放在ax中,余数放入dx中
    sub bx,dx                   ;bx-dx表示将bx放在行首,实现了回车的功能。

.is_carriage_return_end:        ;回车符处理结束,判断是否需要滚屏
    add bx,80 
    cmp bx,2000 
.is_line_feed_end:              ;若是LF,则光标移+80即可
    jl .set_cursor

.roll_screen:                   ;若超过屏幕大小,开始滚屏:屏幕范围是0~23,滚屏原理是把1~24->0~23,再将24行用空格填充
    cld 
    mov ecx,960                 ;2000-80=1920个字符,共1920*2=3840字节,一次搬运4字节,一共要搬运3840/4=960次        
    mov esi,0xc00b_80a0         ;1行行首,源索引地址寄存器
    mov edi,0xc00b_8000         ;0行行首,目的索引地址寄存器
    rep movsd                   ;repeat move string doubleword,以32b为单位进行移动,直到ecx=0
    ;将最后一行填充为空白
    mov ebx,3840                ;最后一行从3840开始
    mov ecx,80                  ;一行80字符,每次清空1字符(2B),一行要移动80.cls:
    mov word [gs:ebx],0x0720    ;0x0720是黑底白字的空格键,一次清空一个字符(2B)
    add ebx,2                   ;ebx移动到下一个字符处
    loop .cls                   ;循环.cls,直到ecx=0
    mov bx,1920                 ;bx存放下一个字符的光标位置,即3840/2=1920

.set_cursor:                    ;将光标设置为bx值
    ;先设置高8位
    mov dx,0x03d4               ;索引寄存器,通过0x3d4写入待操作寄存器的索引
    mov al,0x0e                 ;用于提供光标的高8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置
    mov al,bh                   ;将bx的光标位置的高8位放入al中,通过al输入到dx = 0x3d5端口
    out dx,al                   ;[0x3d5端口] = bx高8= bh
    ;再设置低8位
    mov dx,0x03d4 
    mov al,0x0f                 ;用于提供光标的低8位
    out dx,al 
    mov dx,0x03d5               ;通过数据端口0x3d5来设置光标位置 
    mov al,bl                   ;将bx的光标位置的低8位放入al中,通过al输入到dx = 0x3d5端口 
    out dx,al                   ;[0x3d5端口] = bx低8= bl
    .put_char_done:
        popad                   ;将之前入栈的832b的寄存器出栈
        ret                     

;------------将小端字节序的数字变成对应的ASCII码后,倒置--------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印16进制数字,并不会打印前缀0x
;------------------------------------------------------------------
global put_int 
put_int:
    pushad                      
    mov ebp,esp 
    mov eax,[ebp+4*9]           ;将参数写入eax中,call返回地址占4B+pushad的84B
    mov edx,eax                 ;eax存储的是参数的备份,edx为每次参与位变换的参数,当转换为16进制数字后,eax将下一个参数给edx
    mov edi,7                   ;指定在put_int_buffer中初始的偏移量,表示指向缓冲区的最后一个字节
    mov ecx,8                   ;32位数字中,每4位表示一个16进制数字。所以32位可以表示816进制数字,位数为8。
    mov ebx,put_int_buffer      ;ebx为缓冲区的基址
;32位数字按照16进制的形式从低到高逐个处理,共处理816进制数字
.16based_4bits:
    ;32位数字按照16进制形式从低到高逐字处理
    and edx,0x0000000F          ;解析16进制数字的每一位,and后edx只有低4位有效(最低位的16进制数字)
    cmp edx,9                   ;数字0~9和a~f需要分别处理成对应的字符
    jg .is_A2F                  ;jg:Jump if Greater 若大于9,则跳转.is_A2F
    add edx,'0'                 ;如果是0~9,则加上'0'的ASCII码
    jmp .store 
.is_A2F:
    sub edx,10                  ;A~F减去10所得的差,10的ASCII码为1
    add edx,'A'                 ;加上10的ASCII码得到字符的ASCII码
;将每个数字转换成对应的字符后,按照类似大端的顺序存储到缓冲区put_int_buffer中。
;高位字符放在低地址,低位字符放在高地址,这样和大端字符序类似。
.store:
    ;此时dl中应该是对应数字的ASCII码
    mov [ebx+edi],dl 
    dec edi 
    shr eax,4                   ;右移4位,去掉最低4位
    mov edx,eax
    loop .16based_4bits
;现在把put_int_buffer中已全是字符,打印之前把高位连续的字符去掉。
;例如:000123 -> 123
.ready_to_print:
    inc edi                     ;此时edi为-1(0xffff_ffff),加1使其为0
.skip_prefix_0:
    cmp edi,8                   ;若以及比较到第9个字符,表示待打印的字符都是0
    je .full0                   ;Jump if Equal 
;找出连续的0字符,edi作为非0的最高位字符的偏移
.go_on_skip:
    mov cl,[put_int_buffer+edi] 
    inc edi 
    cmp cl,'0'                  ;判断下一位字符是否为0 
    je .skip_prefix_0           
    dec edi                     ;若当前字符不为'0',则使edi减1恢复当前字符            
    jmp .put_each_num           ;若下一位不为0,则从这一位开始遍历
.full0:
    mov cl,'0'                  ;当输入字符都是0时,只打印0
.put_each_num:
    push ecx                    ;此时ecx中为可打印字符,作为参数传递入put_char中
    call put_char
    add esp,4                   ;覆盖掉ecx,清理栈参数,相当于pop ecx
    inc edi                     ;使edi指向下个字符
    mov cl,[put_int_buffer+edi] ;将下个字符放入cl中
    cmp edi,8
    jl .put_each_num
    popad 
    ret
    
; 对应函数 void set_cursor(uint32_t cursor_pos);
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


  • “/home/lily/OS/boot/lib/kernel/print.h”
#ifndef __LIB_KERNEL_PRINT_H    //防止头文件被重复包含
#define __LIB_KERNEL_PRINT_H    //以print.h所在路径定义了这个宏,以该宏来判断是否重复包含
#include "stdint.h"

void put_char(uint8_t char_asci);
void put_str(char* message);
void put_int(uint32_t num);     //以16进制打印
void set_cursor(uint32_t cursor_pos);

#endif
  • “/home/lily/OS/boot/device/timer.c”
#include "timer.h"
#include "io.h"
#include "print.h"
#include "thread.h"
#include "debug.h"
#include "interrupt.h"

#define IRQ0_FREQUENCY   100
#define INPUT_FREQUENCY  1193180
#define COUNTER0_VALUE INPUT_FREQUENCY/IRQ0_FREQUENCY
#define COUNTER0_PORT    0x40
#define COUNTER0_NO      0
#define COUNTER_MODE     2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43

uint32_t ticks; //ticks是内核自中断开启以来总共的嘀咕数

/*把操作的计数器counter_no、读写锁属性rw1、计数器模式counter_mode写入模式控制寄存器中并赋予初始值counter_value*/
static void frequency_set(uint8_t coutner_port,uint8_t counter_no,uint8_t rw1,uint8_t counter_mode,uint16_t counter_value){
    /*向控制字寄存器端口0x43写入控制字*/
    outb(PIT_CONTROL_PORT,(uint8_t)(counter_no << 6 | rw1 << 4 | counter_mode << 1));
    /*先写入counter_value的低8位*/
    outb(coutner_port,(uint8_t)counter_value);
    /*再写入counter_value的高8位*/
    outb(coutner_port,(uint8_t)(counter_value >> 8));
}

/*时钟的中断处理函数*/
static void intr_timer_handler(void){
    struct task_struct* cur_thread = running_thread();
    ASSERT(cur_thread->stack_magic == 0x19870916);  //检测是否溢出
    cur_thread->elapsed_ticks++;
    ticks++;
    if(cur_thread->ticks == 0)      //若时间片用完,就开始调度新进程上CPU
        schedule();
    else
        cur_thread->ticks--;        //将当前进程的时间片--
}

/*初始化PIT8253*/
void timer_init(){
    put_str("timer_init start\n");
    /*设置8253的定时周期,也就是发中断的周期*/
    frequency_set(COUNTER0_PORT,COUNTER0_NO,READ_WRITE_LATCH,COUNTER_MODE,COUNTER0_VALUE);
    register_handler(0x20,intr_timer_handler); 
    put_str("timer_init done\n");
}
  • “/home/lily/OS/boot/device/timer.h”
#ifndef __DEVICE_TIMER_H
#define __DEVICE_TIMER_H
#include "stdint.h"
void timer_init(void);
#endif
  • “/home/lily/OS/boot/thread/thread.c”
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#include "interrupt.h"
#include "list.h"
#include "debug.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(void){
    uint32_t esp;
    asm("mov %%esp,%0":"=g"(esp));      //将当前的栈指针(ESP)的值存储到变量esp中。
    return (struct task_struct*)(esp & 0xfffff000);     //取esp整数部分,即pcb起始地址
}

/*由kernel_thread去执行function(func_arg)*/
static void kernel_thread(thread_func* funcion,void* func_arg){
    intr_enable();      //开中断,避免后面的时钟中断被屏蔽,而无法调度其他线程
    funcion(func_arg);
}

/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg){
    /*先预留中断使用栈的空间*/
    pthread->self_kstack -= sizeof(struct intr_stack);

    /*再预留出线程栈空间*/
    pthread->self_kstack -= sizeof(struct thread_stack);
    struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;    //线程栈指针
    kthread_stack->eip = kernel_thread;
    kthread_stack->function = function;
    kthread_stack->func_arg = func_arg;
    kthread_stack->ebp = kthread_stack->ebx = kthread_stack->edi = kthread_stack->esi = 0;  //寄存器初始化为0
}

/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio){
    memset(pthread,0,sizeof(*pthread));
    strcpy(pthread->name,name);
    if(pthread == main_thread)
        pthread->status = TASK_RUNNING;
    else
        pthread->status = TASK_READY;

    /*self_kstack是线程自己在内核态下使用的栈顶地址*/
    pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
    pthread->priority = prio;
    pthread->ticks = prio;
    pthread->elapsed_ticks = 0;
    pthread->pgdir = NULL;
    pthread->stack_magic = 0x19870916;
}

/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg){
    /*pcb都位于内核空间,包括用户进程的pcb也在内核空间*/
    struct task_struct* thread = get_kernel_pages(1);
    init_thread(thread,name,prio);
    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);

    // asm volatile("movl %0,%%esp; \
    // pop %%ebp;pop %%ebx;pop %%edi;pop %%esi; \
    // ret": : "g"(thread->self_kstack) : "memory");
    // //esp = thread->self_kstack为栈顶;4个出栈,将初始化的0弹出到相应寄存器中;执行ret,将栈顶数据作为返回地址送入CPU的eip中;thread->self_kstack为输入
    return thread;
}

/*将kernel中的main函数完善为主线程*/
static void make_main_thread(void){
    /*因为main线程早已在运行,咱们在loader.S中进入内核时的mov esp,0xc009f000,就是为其预留pcb的。
    因为pcb的地址为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;
        cur->status = TASK_READY;
    }else{
        //若此线程需要某事件发生后才能继续上cpu运行,不需要将其放入队列中,因为当前线程不在就绪队列中
    }

    ASSERT(!list_empty(&thread_ready_list));
    thread_tag = NULL;
    thread_tag = list_pop(&thread_ready_list);
    struct task_struct* next = elem2entry(struct task_struct,general_tag,thread_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");
}
  • “/home/lily/OS/boot/thread/thread.h”
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"

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

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

/*中断栈intr_stack
用于中断发生时保护程序上下文环境:
进程/线程被外部中断/软中断打断时,会按照此结构压入上下文
寄存器,intr_exit中的出栈操作是此结构的逆操作
此栈在线程自己的内核栈中位置固定,所在页的最顶端
*/
struct intr_stack{
    uint32_t vec_no;    //kernel.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指向待调用函数kernel_thread,
    其他时候,eip指向switch_to的返回地址*/
    void (*eip) (thread_func* func,void* func_arg);

    /*以下仅供第一次被调度上CPU时使用*/

    /*参数unused_ret只为占位置充数为返回地址*/
    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;              //每次在处理器上的嘀咕数
    uint32_t elapsed_ticks;     //此任务自上CPU运行至今用了多少CPU嘀咕数,也就是任务运行了多久
    struct list_elem general_tag;       //用于线程在一般队列中的节点
    struct list_elem all_list_tag;      //用于线程队列thread_all_list中的结点
    uint32_t* pgdir;            //进程自己页表的虚拟地址,如果是线程则为NULL
    uint32_t stack_magic;       //栈的边界标记,用于检测栈的溢出
};

/*获取当前线程的pcb指针*/
struct task_struct* running_thread(void);
/*初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置*/
void thread_create(struct task_struct* pthread,thread_func function,void* func_arg);

/*线程初始化*/
void init_thread(struct task_struct* pthread,char* name,int prio);

/*创建一优先级为prio,线程名为name,线程所执行函数为function(func_arg)的线程*/
struct task_struct* thread_start(char* name,int prio,thread_func function,void* func_arg);

/*将kernel中的main函数完善为主线程*/
// static void make_main_thread(void);

/*实现任务调度*/
void schedule(void);

/*初始化线程环境*/
void thread_init(void);

#endif
  • “/home/lily/OS/boot/thread/switch.S”
[bits 32]
section .text
global switch_to
switch_to:
    ;栈中此时是返回地址
    push esi
    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
    ;------------- 以上是备份当前线程的环境,下面是恢复下一个线程的环境 -----------------------
    mov eax,[esp + 24]  ;得到栈中参数next,next = [esp + 24]
    mov esp,[eax]       ;pcb的第一个成员是self_kstack成员,它用来记录0级栈,0级栈中保存了进程/线程所有的信息,包括3级指针
    pop ebp
    pop ebx
    pop edi
    pop esi
    ret                 ;返回到上面switch_to下面的那句注释的返回地址;如果未由中断进入,第一次执行时会返回kernel_thread
  • “/home/lily/OS/boot/kernel/init.c”
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"
#include "memory.h"
#include "../thread/thread.h"

/*负责初始化所有模块*/
void init_all(){
    put_str("init_all\n");
    idt_init();     //初始化中断
    mem_init();
    thread_init();
    timer_init();   //初始化PIT   
}
  • “/home/lily/OS/boot/kernel/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);
	}
}
  • “/home/lily/OS/boot/makefile”
BUILD_DIR = ./build
##用来存储生成的所有目标文件
ENTRY_POINT = 0xc0001500
AS = nasm
CC = gcc
LD = ld
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/
ASFLAGS = -f elf
CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes
##-fno-builtin是告诉编译器不要采用内部函数 -Wstrict-prototypes是要求函数声明中必须有参数类型 
## -Wmissing-prototypes要求函数必须有声明
LDFLAGS = -m elf_i386 -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
	$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/debug.o \
	$(BUILD_DIR)/string.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o \
	$(BUILD_DIR)/switch.o $(BUILD_DIR)/list.o
## OBJS用来存储所有目标文件名,不要用%.o,因为不能保证链接顺序

########## c代码编译 ##########
$(BUILD_DIR)/main.o:kernel/main.c lib/kernel/print.h lib/kernel/stdint.h kernel/init.h kernel/memory.h \
	thread/thread.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/init.o:kernel/init.c kernel/init.h lib/kernel/print.h lib/kernel/stdint.h \
	kernel/interrupt.h device/timer.h kernel/memory.h thread/thread.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/interrupt.o:kernel/interrupt.c kernel/interrupt.h lib/kernel/stdint.h \
	kernel/global.h lib/kernel/io.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/timer.o:device/timer.c device/timer.h lib/kernel/stdint.h lib/kernel/io.h lib/kernel/print.h thread/thread.c \
	kernel/debug.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/thread.o:thread/thread.c thread/thread.h lib/kernel/stdint.h lib/string.h kernel/global.h kernel/memory.h \
	kernel/interrupt.h lib/kernel/list.h kernel/debug.h lib/kernel/print.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/debug.o:kernel/debug.c kernel/debug.h lib/kernel/print.h lib/kernel/stdint.h kernel/interrupt.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/string.o:lib/string.c lib/string.h kernel/debug.h kernel/global.h lib/kernel/stdint.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/memory.o:kernel/memory.c kernel/memory.h lib/kernel/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.h \
	lib/kernel/print.h kernel/global.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/bitmap.o:lib/kernel/bitmap.c lib/kernel/bitmap.h lib/string.h kernel/interrupt.h lib/kernel/print.h \
	kernel/debug.h lib/kernel/stdint.h
	$(CC) $(CFLAGS) $< -o $@

$(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \
					 kernel/interrupt.h lib/kernel/stdint.h kernel/debug.h
	$(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 all

mk_dir:
	if [ ! -d $(BUILD_DIR) ]; then mkdir $(BUILD_DIR); fi	
###fi为终止符

hd:	
	dd if=$(BUILD_DIR)/kernel.bin of=/home/lily/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

clean:	##将build目录下文件清空
	cd $(BUILD_DIR) && rm -f ./*

build:$(BUILD_DIR)/kernel.bin	##编译kernel.bin,只要执行make build就是编译文件

all:mk_dir build hd	
##依次执行伪目标mk_dir build hd,只要执行make all就是完成了编译到写入硬盘的全过程

在这里插入图片描述
在这里插入图片描述

  • 21
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值