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

配合视频学习效果更佳!
第一节:https://www.bilibili.com/video/BV1ho4y1J7Pi/?vd_source=701807c4f8684b13e922d0a8b116af31
第二节:https://www.bilibili.com/video/BV1MM4y1n7uV/?vd_source=701807c4f8684b13e922d0a8b116af31
第三节:https://www.bilibili.com/video/BV1Pg4y1K7TS/?vd_source=701807c4f8684b13e922d0a8b116af31

程序是指静态的、存储在文件系统上、尚未运行的指令代码,比如说一个编译好的存储在磁盘中的程序。进程是指正在运行中的程序,程序必须在获得运行时所需要的各类资源后才能成为进程,资源包括进程所使用的栈、寄存器、内存、页表等。线程,就是最基本的调度单元,能够以身份独立上处理器运行,也就是一个程序中可以独立的,且正在运行的代码块。线程作为调度单元,一个进程线程越多,那么它上处理器的频率就越高,执行就越快。进程=线程+资源,因为线程运行脱离不了进程,进程里面有线程的运行的资源。线程是调度的单元,进程是分配资源的单元,同一个进程的多个线程享受这个进程的共同资源,如内存,页表等。

把需要等待外界条件的才能运行的进程状态叫做阻塞态,进程可以随时准备运行的状态成为就绪态,把正在处理器上运行的进程的状态成为运行态。

操作系统为每个进程提供了一个PCB,进程控制块,它就是进程的身份证。每个PCB放到一张表格中维护,这就是进程表。调度器可以根据这张表选择上处理器运行的进程。在这里插入图片描述
一个基本的PCB其结构如图所示

本章我们最终要实现的就是一个多线程并行的效果,也就是如果我创建了两个线程A和B,A线程内容就是循环打印A,B线程是循环打印B。那么最终呈现在屏幕上的效果就是,一会打印一堆A,一会打印一堆B,如此循环往复交替执行。

接下来,我们首先实现这一切的基础,创建并运行内核线程。

剖析409,410,414,thread.c代码(核心):

1、代码功能

在内核中实现创建并运行线程

2、实现原理

通过pcb我们可以管理进程、线程的运行。所以,对于进程、线程的运行管理(是否运行、运行多长时间等问题),简化为对于pcb的管理。通过pcb,我们可以找到进程、线程的栈。在栈中,找到实际要运行的线程函数的地址,然后去执行。

3、代码逻辑

A、创建线程信息(包含管理信息与运行信息)

B、执行线程

4、怎么写代码?

A、建立核心数据结构,task_struct,用于记录我们要创建的线程的管理信息;定义核心数据结构thread_stack,用于记录线程的运行信息

B、写出函数init_thread,来初始化线程pcb中的管理信息

C、写出函数thread_create,来初始化线程栈中的函数运行信息

D、写出函数kernel_thread作为线程启动器来启动真正要运行的函数,并将其地址放在thread_stack中,也就是线程的栈中

E、封装BCD成为一个函数thread_create,其内部加入通过pcb中的satck信息找到线程栈,然后ret弹出栈顶地址进入执行的汇编代码

5、 **代码实现 **

myos/thread/thread.h

#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的边界
};

#endif

代码实现如下: myos/thread/thread.c

#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"

#define PG_SIZE 4096

/* 由kernel_thread去执行function(func_arg) , 这个函数就是线程中去开启我们要运行的函数*/
static void kernel_thread(thread_func* function, void* func_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);

   /* 再留出线程栈空间,可见thread.h中定义 */
   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 = 0x19870916;	                                // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖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;
}

6、其他代码详解查看书p410

声明函数(myos/kernel/thread.h

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);

测试代码 myos/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) {
    
    
      int i = 9999999;
      while(i--);
      put_str(para);
   }
}


以上仅仅是线程的创建与进入,我们要实现依靠线程的pcb之间形成的链表来实现管理与调度,pcb之间形成的链表是为了通过一个pcb顺利找到下一个pcb,因为我们会在task_struct中插入一个双向链表,为了实现这样的数据结构,我们接下来实现一大堆与链表有关的数据结构与函数。

创建双链表的管理节点与普通节点数据结构 (myos/lib/kernel/list.h

#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "global.h"

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

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

//定义个叫function的函数类型,返回值是int,参数是链表结点指针与一个整形值
typedef bool (function)(struct list_elem*, int arg);


#endif

写有关双链表的函数

myos/lib/kernel/list.c 代码剖析略

#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);
}

函数声明与通过结构体成员计算整个结构体地址的宏 myos/lib/kernel/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))

void list_init(struct list* list);
void list_insert(struct list* link,struct list* new_link);
void list_push(struct list* list, struct list* new_link);
void list_append(struct list* list,struct list* new_link);
void list_remove(struct list* link);
struct list* list_pop(struct list* list);
int list_find(struct list* list, struct list* link);
int list_empty(struct list* list);
uint32_t list_len(struct list* list);
struct list* list_traversal(struct list* list,function func,int arg);


现在我们来实现多线程的运行

421、422、423、426、427、428、429、432、433、434、435、析代码:thread.h、thread.c、init.c、pirnt.S、interrup.c、timer.c、swith .S、main.c:

1、代码功能

实现多线程的轮询调度运行

2、实现原理

线程的pcb中存储着线程栈的位置,而线程栈中又存储着线程的运行所需要运行的一系列信息,通过这些信息,我们可以进入线程执行。在线程执行过程中,每一次时钟中断都会修改线程pcb中的允许运行的时间值,当一个线程的所允许被执行的时间归零。那么就执行调度,就是从就绪队列中找到下一个线程的pcb,进而找到线程栈中的运行信息,并进入执行。

3、代码逻辑

内核多线程的轮询调度,核心就4个:

  1. 多线程要形成队列,调度是从队列中挑选pcb,通过pcb去运行线程;
  2. main要把自己初始化成主线程与其他线程参与轮询调度;
  3. 用时钟中断来打断线程的运行,然后统计线程运行的时间,由时钟中断来决定是否调度切换;
  4. 有切换机制来完成线程之间的切换;

4、代码干了啥(主要)?

1、thread.h

  • task_struct增加时间片字段,链表字段

2、thread.c

  • 增加主线程的pcb;
  • 增加管理所有线程pcb的队列;
  • 增加管理所有就绪线程的队列;
  • 写出通过当前栈值获得pcb的函数running_thread;
  • 线程启动器函数kernel_thread增加打开中断部分代码;
  • 初始化线程管理信息(pcb)的函数init_thread增加对task_struct增加字段的初始化代码;
  • 创建线程函数thread_start增加加入线程pcb进入所有队列与就绪队列的代码并删除ret进入线程代码(因为我们要用切换函数schedule来选择线程运行,而不是让线程创建函数创建完毕后直接运行);
  • 增加函数make_main_thread用于初始化主线程;
  • 增加切换函数schedule;
  • 增加函数thread_init用于初始化主线程;

3、init.c

  • 将thread_init函数封装进入总初始化函数,并且要调换几个初始化函数的调用顺序

4、print.S

  • 增加用于设定光标位置的函数set_cursor

5、interrupt.c

  • 修改通用中断处理函数general_intr_handler,增加修改光标位置打印错误新的的代码,原因是多线程切换引发的同步问题,有时候会引发光标值超过允许范围,这个范围是显存段段描述符的界限决定的,如果这时候由光标值超限引发的中断,然后调用general_intr_handler去打印信息,将再次导致异常,那么就不会输出错误信息;
  • 增加中断处理函数注册函数register_handler;

6、timer.c

  • 增加全局变量ticks用于记录自时钟中断启动以来发生了多少次时钟中断;
  • 增加时钟中断处理函数intr_timer_handler,其主要功能就是每发生一次时钟中断,就增加一次ticks与当前正在运行的线程的pcb中的时间,如果时间到期,那么就运行切换程序schedule进行切换;
  • 在timer.c中增加注册时钟中断处理函数的代码

7、switch.S

  • 里面写入根据schedule函数传入的当前正在运行的线程pcb与下一个要运行的线程pcb完成切换的代码

5、代码实现如下:

myos/thread/thread.h

#include "list.h" 

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的边界
};


myos/thread/thread.c

#include "debug.h"
#include "interrupt.h"
#include "print.h"

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中存储的是线程的管理信息,此函数用于根据传入的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 = 0x19870916;	                                // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖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");
}

myos/thread/thread.h

struct task_struct* running_thread(void);
void schedule(void);
void thread_init(void);

myos/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();  
}

myos/lib/kernelprint.S(添加新的代码块,而不是修改原有的.set_cursor

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

myos/lib/print.h

void set_cursor(uint32_t cursor_pos);

myos/kernel/interrupt.c

/* 通用的中断处理函数,用于初始化,一般用在异常出现时的处理 */
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; 
}

myos/kernel/interrupt.h

void register_handler(uint8_t vector_no, intr_handler function);

myos/device/timer.c

#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 == 0x19870916);         // 检查栈是否溢出

   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");
}

myos/thread/switch.S

[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

myos/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);
   }
}

由于多线程引起的同步问题,会让光标值位置超过显存段段描述符规定的界限而引发错误,所以我们可以在执行线程时候关闭中断,然后运行完毕之后打开中断,以确保线程的原子执行,来杜绝此类问题

修改后的myos/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();
    int i = 999999;
   thread_start("k_thread_a", 31, k_thread_a, "argA ");
   thread_start("k_thread_b", 31, k_thread_b, "argB ");

   intr_enable();	// 打开中断,使时钟中断起作用
    while(1)
    {
    
    
        while(i--);
        i=999999;
        intr_disable();
        put_str("main ");
        intr_enable();
    }   
   return 0;
}

/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
    
         
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
    int i=9999999;
    char* tmp = arg;
    while(1)
    {
    
    
        while(i--);
        i=999999;
        intr_disable();
        put_str(tmp);  
        intr_enable();      
    }
}

/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
    
         
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
    int i=9999999;
    char* tmp = arg;
    while(1)
    {
    
    
        while(i--);
        i=999999;
        intr_disable();
        put_str(tmp);
        intr_enable();
    }
}

猜你喜欢

转载自blog.csdn.net/kanshanxd/article/details/131181013
今日推荐