赞
踩
本篇对应书籍第九章的内容
本篇内容介绍了线程是什么,多线程的原理,多线程用到的核心数据结构,以及通过代码实现了内核多线程的调度
过去,计算机只有一个处理器,一次只能处理一个任务,下一个任务就得等这个任务执行完才能继续执行,为了不让大任务彻底把操作系统给堵塞,于是后来就出现了多任务操作系统,在单核CPU下,多任务是通过“伪并行”的方式实现的:
CPU执行每个任务就执行一会(一定CPU周期),然后切换执行下一个任务执行,以此循环
这个功能由软件实现,实现这个功能的软件叫做任务调度器(任务切换本质上就是改变了CPU中程序计数器的指向)
我们把程序计数器中下一条指令的地址所组成的执行的轨迹称为执行流
任何代码块都可以是执行流(大到整个程序文件即进程、小到一个函数即线程),只要在运行的时候,我们提供好上下文环境即可,上下文环境就是寄存器映像、栈、内存等
在任务调度器眼里,执行流是调度单元,也是处理器的基本执行单位
线程的本质是函数的另一种执行方式,线程是一套机制,能够让所运行的函数能够以调度单元的身份独立上处理器进行执行,函数能够独立执行,可以让多个函数以并行的方式执行给程序提提速
普通的函数执行是加在程序中间进行执行的,线程的函数执行是独立出来单独让CPU处理
程序是静态的未运行的指令代码;
进程是正在运行的程序,程序运行需要各类资源,比如内存,寄存器,对处理器来说,进程是执行流的集合,控制流之间相互独立却共享资源,进程可以根据线程数量分为单线程进程和多线程进程;
线程是后来提出的概念名词,实际上就是独立的执行流,是独立能执行的代码块;
执行流、调度单位、运行实体等概念都是针对线程而言的,一切执行流都是线程
进程相当于是资源容器(拥有地址空间,页表等资源),线程是资源使用者
操作系统把进程(线程)“执行过程”中所经历的不同阶段归为几类:
状态的转变由调度器负责,状态是描述线程的
因为状态是人为定义的,调度器是人为编写的,所以调度方法和状态都可以人为修改
PCB,Process Control Block ,操作系统提供的PCB用来解决任务调度相关的问题,PCB 结构如图所示:

每个进程都有自己的PCB,所有的PCB放到一张表格中来维护,就是进程表:

PCB没有具体的格式,实际格式取决于操作系统。
实现线程有两种方式:在用户空间实现线程或者在内核空间实现线程
如果是程序内实现线程,那处理器调度任务的时候以进程为单位进行,一个进程分配的时间片还是那么多
如果是内核里实现线程,这处理器调度任务的时候以线程为单位进行,一个进程内如果有多个线程,则这个进程占用的时间片会多一些,达到提速的效果
#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.asm 宏 VECTOR 中 %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 { // ABI 规定 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时使用 ****/ void (*unused_retaddr); // unused_ret 只为占位置充数为返回地址, 这里活用ret指令, ret指令是先将栈中地址恢复到 eip, 然后跳转过去, 实际上eip被我们操纵, 所以栈中地址无所谓是啥, eip会被我们修改的 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; // 栈的边界标记, 用于检测栈的溢出 }; struct task_struct* running_thread(void); 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
intr_stack中断栈,用于保存中断发生时的上下文环境,进入中断后,中断入口程序所执行的上下文保护的一系列压栈操作都是压入了此结构
thread_stack线程栈,有两个作用,主要体现在eip上:
其他4个成员是ABI(程序二进制接口)的规定,在函数调用前后这几个寄存器的值不能改变,一般由编译器负责生产,但是自己写汇编代码给C调用的时候,要手动完成这件事
”仅供第一次被调度上使用“这一段注释之后的三个内容:
void (*unused_retaddr);
thread_func* function;
void* func_arg;
因为是用ret进行跳转执行,跳转执行后,使用参数中的函数地址+函数参数进行函数调用,函数调用的时候通常会使用call,这里没用call指令,所以得按照call指令的入栈形式来装填栈才行,第一个是返回地址,接下来是参数,所以这里需要一个占位符
下次再回到这个线程进行执行的时候,就是跳转过来的时候了,跳转的时候会重新指定栈顶位置,所以之后就再也用不上了
注意:结构体中越在后面的参数地址越高
#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_args) { function(func_args); } /* 初始化线程栈 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 = 0; kthread_stack->ebx = 0; kthread_stack->esi = 0; kthread_stack->edi = 0; } /* 初始化线程基本信息, 参数为: 待初始化线程指针(PCB), 线程名称, 线程优先级 */ 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 是线程自己在内核态下(0特权级)使用的栈顶地址, 大小为一页, 初始化为PCB顶端 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); // 申请一页内核空间存放PCB init_thread(thread, name, prio); // 初始化线程 thread_create(thread, function, func_arg); // 创建线程 // 将栈顶(esp)置为 eip, 利用 ret 跳转 asm volatile("movl %0, %%esp; \ pop %%ebp; \ pop %%ebx; \ pop %%edi; \ pop %%esi; \ ret;" : : "g"(thread->self_kstack) : "memory"); return thread; }
无论是进程的还是线程的 PCB 都是给内核调度器使用的,所以要放在内核空间中
最后这段内联汇编就是在巧用ret指令进行跳转,使用ret的时候,此时的栈中指向的是返回地址,然后将该返回地址的值传递给eip,然后进行跳转,这里先通过mov指令将栈改成了该线程的线程栈self_kstack,这个栈在此时的位置上刚好是5个寄存器的值,依次pop4个固定的寄存器,然后下一个值就是eip的值,供ret指令使用,而此时的eip是我们线程函数的地址,就会直接跳转到kernel_thread函数然后执行内容,此时的栈里面3个数字依次是无用的占位符、函数地址、函数参数
我们现在实现的是内核单线程,让内核中的单个线程跑起来,线程就是个函数,让这个函数跑起来就行,但是还是想问,就是线程到底是什么,有了自己的PCB然后按照PCB中的值来进行工作就是线程了吧,此时thread_start函数的工作就是将PCB赋值,然后内联汇编ret执行执行
根据当前的信息,只能认为线程(此处指的是单线程进程)是有PCB的虚拟内存空间中执行的代码,PCB里写了关于当前执行代码的信息,如内核栈、状态、名字。也就是说线程是往内核空间中登记过PCB的程序。
#include "print.h" #include "init.h" #include "debug.h" #include "memory.h" #include "../thread/thread.h" void k_thread_a(void*); int main() { put_str("I am kernel\n"); init_all(); // asm volatile("sti"); // 为演示中断处理, 在此临时开中断 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); } }
thread.h:

thread.c:

编译,运行:
函数成功被调用执行起来,打印一大堆argA。

内核中使用的数据结构是队列,用双向链表来进行维护,链表的关键在于链
#ifndef __LIB_KERNEL_LIST_H #define __LIB_KERNEL_LIST_H #include "global.h" // 获取 member 在 struct_type结构体中的偏移量 #define offset(struct_type, member) (int) (&((struct_type*) 0)->member) // 获取 pcb 首地址, 用 mem 当前地址 - mem偏移量, 然后强制类型转换 #define elem2entry(struct_type, struct_mem_name, elem_ptr) \ (struct_type*) ((int) elem_ptr - offset(struct_type, struct_mem_name)) /********** 定义链表结点成员结构 *********** * 结点中不需要数据成元, 只要求前驱和后继结点指针 */ struct list_elem { struct list_elem* prev; // 前驱节点 struct list_elem* next; // 后继节点 }; /* 链表结构, 用来实现队列 */ struct list { // head 队首, 固定不变, 第 1 个元素为 head.next struct list_elem head; // tail 队尾, 固定不变 struct list_elem tail; }; /* 自定义函数类型function, 用于在 list_traversal(遍历) 中做回调函数 */ typedef bool (function) (struct list_elem*, int); 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
#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(); // 关中断 // 将 before 前驱元素的后继元素更新为 elem, 暂时使 before 脱离链表 before->prev->next = elem; // 更新 elem 自己的前驱结点为 before 的前驱, 更新 elem 自己的后继结点为 before elem->prev = before->prev; elem->next = before; // 更新 before 的前驱结点为 elem 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); // 在队头插入 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; } 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); }
系统中有些东西是公共资源,访问公共资源的代码片段叫临界区,临界区的代码执行必须是原子操作,要么执行完,要么不执行,所以此时需要用到关中断来实现原子操作,之后会使用锁来实现


这里的工作是完成线程的轮询调度
添加/修改如下内容:
// 进程或线程的 PCB
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
enum task_status status; // 线程状态
char name[16];
uint8_t priority; // 线程优先级
uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数
uint32_t elapsed_ticks; // 此任务上 cpu 运行后至今占用了多少嘀嗒数
struct list_elem general_tag; // 用于线程在一般队列中的结点
struct list_elem all_list_tag; // 用于线程在 thread_all_list 中的结点
uint32_t* pgdir; // 进程自己页表的虚拟地址
uint32_t stack_magic; // 栈的边界标记, 用于检测栈的溢出
};
当前即将处于单进程多线程的情况下,目前应该看不到进程是如何独享内存空间了
填改如下内容:基本上只是添加了一些内容
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* runing_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_args) { // 执行 function 前需要开中断, // 避免后面的时钟中断被屏蔽, 而无法调度其它线程 intr_enable(); function(func_args); }
新增了running_thread函数,这个函数的工作是获取当前线程的PCB指针,由于各个线程所用的0级栈都是在PCB当中的,所以取出来栈指针,然后根据分页机制定位到当前页的页首,就是PCB指针了,当时创建PCB的时候,专门申请了一个页,用来存放PCB
/* 初始化线程基本信息, 参数为: 待初始化线程指针(PCB), 线程名称, 线程优先级 */ void init_thread(struct task_struct* pthread, char* name, int prio) { memset(pthread, 0, sizeof(*pthread)); // 清零 strcpy(pthread->name, name); // 给线程的名字赋值 if(pthread == main_thread) { // 把 main 函数也封装成一个线程, main 函数是一直运行的 pthread->status = TASK_RUNNING; } else { pthread->status = TASK_READY; } // self_kstack 是线程自己在内核态下(0特权级)使用的栈顶地址, 大小为一页, 初始化为PCB顶端 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; // 自定义魔数, 用于检查"入栈"是否过多(溢出) }
此处加入了对主线程的判断,如果是主线程就状态设为运行,否则就是就绪
// 线程所执行的函数是 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 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); return thread; }
此函数是创建线程的入口,init_thread初始化线程PCB,thread_create初始化线程PCB中的内核栈,到这里PCB就初始化完毕了,也就是线程就初始化完毕了,接下来将线程分别加入就绪队列和全部线程队列即可
/* 将 kernel 中的 main 函数完善为主线程 */
static void make_main_thread(void) {
// 因为main线程早已运行,咱们在loader.S中进入内核时的 mov esp,0xc009f000
// 就是为其预留了pcb, 地址为 0xc009e000, 因此不需要通过 get_kernel_page 另分配一页
main_thread = runing_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);
}
这里是给main函数一个PCB,成为主线程

任务调度器的工作是读写就绪队列,增删节点,节点就是PCB里面的general_tag,相当于PCB
这里设计的调度原理是:每个线程执行的时间由ticks决定,ticks由prio赋值,每经过一个时钟周期,ticks减1,到0时,切换任务
切换任务实际上就是把当前线程的ticks重置,然后把状态从RUNNING改为READY,然后重新加入就绪队列中,将队列中第一个节点作为下一个要运行的程序设置状态位RUNNING,弹出队列,然后将新线程的寄存器环境恢复,开始执行
这个理论分三部分实现:
时钟中断处理程序
调度器 schedule
任务切换函数 switch_to
1、先创建线程
2、打开中断 每个时钟中断调用中断函数 减去当前时间片
3、时间片为0 简称到期了 到期之后 调用schedule调度器 切换线程
4、schedule 把在最前面的准备队列的任务的pcb获取 把当前的放到最后
5、之后转到switch_to 保存寄存器 上下文环境 切换esp 即切换线程
; 对应函数 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
void set_cursor(uint32_t cursor_pos);
修改 general_intr_handler函数:
/* 通用的中断处理请求 */ 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); }
这里优化了之前的通用中断处理程序,让显示更加清晰,如果发生缺页异常,CPU会把导致此异常的地址存放到控制寄存器CR2中
/* 在中断处理程序数组第 vector_no 个元素中注册安装中断处理程序 function */
void register_handler(uint8_t vector_no, intr_handler function) {
idt_table[vector_no] = function;
}
新增中断处理程序注册函数,通过这个函数直接修改中断向量表中指向的函数,通过数组下标的形式。
/* 实现任务调度 */
void schedule(void);
/* 初始化线程环境 */
void thread_init(void);
增加如下内容:
#include "thread.h" #include "debug.h" #include "interrupt.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 { 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"); }
新增时钟处理程序intr_timer_handler,获取当前线程PCB之后,读取ticks,将总时钟数++,将ticks–,判断是否需要调度,ticks减到0的时候,通过schedule函数进行调度
修改的timer_init函数,将时钟处理程序注册到中断处理程序中
/* 实现任务调度 */ 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 { // 若此线程阻塞, 不需要将其加入队列 } ASSERT(!list_empty(&thread_ready_list)); thread_tag = NULL; // thread_tag清空 // 将 thread_ready_list 队列中的第一个就绪线程弹出, 准备将其调度上cpu thread_tag = list_pop(&thread_ready_list); // 获取 thread_tag 对应的 PCB struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); next->status = TASK_RUNNING; switch_to(cur, next); }
时间片到时间了,进行调度,先获取当前线程信息(PCB),然后判断当前状态,如果是RUNNING则说明只是时间到了而已,那就把该线程重新加入线程就绪队列,然后重置ticks和状态
接下来取出下一个线程,获取其PCB地址,设置状态,通过switch_to函数进行环境准备
这里用的获取PCB的方法是另一种方法:获取当前tag在PCB结构体中的偏移量,用当前地址减去偏移量,然后就是PCB结构首地址了,然后类型强制转换即可
原来用的方法是对tag直接&0xfffff000,这样这个地址就直接是pcb的了

中断分为两种情况,一种是在用户态下被中断的情况,另一种是在内核态下被中断的情况:
第一部分是任务进入中断时的保护,保存当前全部寄存器
第二部分是保护内核环境的上下文,依据ABI,除了esp外,只保护esi/edi/ebx/ebp这4个寄存器,因为内核代码的执行由这4个寄存器决定,这几个寄存器的值可以让处理器把程序执行到内核代码的结束处
在用户态下被中断要保存所有寄存器映像,这点在中断初始化程序中已经完成了,这里需要完成在内核态下的处理:
[bits 32] section .text ; 相当于函数 void switch_to(struct task_struct* cur, struct task_struct* next); global switch_to switch_to: ; 备份当前线程的环境, 栈中还包含返回地址 push esi push edi push ebx push ebp mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp + 20], 4 * 4 + 4(返回地址) = 20 mov [eax], esp ; 保存栈顶指针 esp 到 task_struct 的 self_kstack 字段 ; self_kstack 在 task_struct 中的偏移为 0 ; 所以直接往 thread 开头处存 4 字节即可 ;------------------ 以上是备份当前线程的环, 下面是恢复下一个线程的环境 ---------------- mov eax, [esp + 24] ; 获取栈中参数 next mov esp, [eax] ; pcb 的第一个成员是 self_kstack 成员 ; 它用来记录 0 级栈顶指针, 被换上 cpu 时用来恢复 0 级栈 ; 0 级栈中保存了进程或线程所有信息, 包括 3 级栈指针 pop ebp pop ebx pop edi pop esi ret ; 返回到栈中的返回地址, 即利用 ret 跳转执行流(thread_stack 中的 eip) ; 未由中断进入, 第一次执行时会返回到 kernel_thread
这里分为上下两部分,首先,根据ABI,需要保护esi,edi,ebx,ebp四个寄存器,那我们就先按照要求push,保存到栈中,然后通过我们传递的参数1,将当前栈地址保存到参数1中,然后将参数2作为栈地址恢复到栈指针中,此时栈已经改变了,然后再恢复这四个寄存器实际上恢复的是下一个任务的栈的寄存器了,从而实现了切换寄存器

/* 初始化线程环境 */
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 donw\n");
}
先初始化线程队列,给主函数创建为线程
#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
}
#include "print.h" #include "init.h" #include "debug.h" #include "memory.h" #include "../thread/thread.h" #include "interrupt.h" void k_thread_a(void*); void k_thread_b(void*); int main() { put_str("I am kernel\n"); init_all(); // asm volatile("sti"); // 为演示中断处理, 在此临时开中断 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) { char* para = arg; while(1) { put_str(para); } }
任务调度是时钟中断驱动的,所以需要打开中断,通过之前设置好的时钟中断处理程序来进行调度任务(时间片使用时间到,然后通过schedule函数修改队列,获取下一个线程的信息,通过switch_to进行任务切换)
thread.c:

timer.c:

编译,运行
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/ ASFLAGS = -f elf CFLAGS = -Wall -m32 -fno-stack-protector $(LIB) -c -fno-builtin -W -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)/memory.o $(BUILD_DIR)/string.o \ $(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o \ $(BUILD_DIR)/switch.o ############ C 代码编译 ############## $(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \ lib/stdint.h kernel/init.h lib/string.h $(CC) $(CFLAGS) $< -o $@ $(BUILD_DIR)/init.o: kernel/init.c kernel/init.h lib/kernel/print.h \ lib/stdint.h kernel/interrupt.h device/timer.h $(CC) $(CFLAGS) $< -o $@ $(BUILD_DIR)/interrupt.o: kernel/interrupt.c kernel/interrupt.h \ lib/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/stdint.h \ lib/kernel/io.h lib/kernel/print.h $(CC) $(CFLAGS) $< -o $@ $(BUILD_DIR)/debug.o: kernel/debug.c kernel/debug.h \ lib/kernel/print.h lib/stdint.h kernel/interrupt.h $(CC) $(CFLAGS) $< -o $@ $(BUILD_DIR)/string.o: lib/string.c lib/string.h \ kernel/debug.h kernel/global.h $(CC) $(CFLAGS) $< -o $@ $(BUILD_DIR)/memory.o: kernel/memory.c kernel/memory.h \ lib/stdint.h lib/kernel/bitmap.h kernel/debug.h lib/string.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 $(CC) $(CFLAGS) $< -o $@ $(BUILD_DIR)/thread.o: thread/thread.c thread/thread.h \ lib/stdint.h lib/string.h kernel/global.h kernel/memory.h \ kernel/debug.h kernel/interrupt.h lib/kernel/print.h $(CC) $(CFLAGS) $< -o $@ $(BUILD_DIR)/list.o: lib/kernel/list.c lib/kernel/list.h \ kernel/interrupt.h lib/stdint.h kernel/debug.h $(CC) $(CFLAGS) $< -o $@ ############## 汇编代码编译 ############### $(BUILD_DIR)/kernel.o: kernel/kernel.asm $(AS) $(ASFLAGS) $< -o $@ $(BUILD_DIR)/print.o: lib/kernel/print.asm $(AS) $(ASFLAGS) $< -o $@ $(BUILD_DIR)/switch.o: thread/switch.asm $(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 hd: sudo dd if=$(BUILD_DIR)/kernel.bin \ of=/home/steven/source/os/bochs/hd60M.img \ bs=512 count=200 seek=9 conv=notrunc clean: cd $(BUILD_DIR) && rm -f ./* build: $(BUILD_DIR)/kernel.bin all: mk_dir build hd
运行效果:

可以看到,两个线程和主线程交替执行了,主线程显示Main,子线程分别显示argA和argB
这里出现了异常,其实按理说,如果让主线程也输出字符的话,还是会出现空格,这是异常引起的,问题是临界区代码资源竞争,这将在下一章进行学习,这里就跳过了。
本篇的内容是线程,跟上一篇的内存管理系统一样,先实现了一半,剩下一半后面再完善,还有一些疑问没有解决,比如线程和进程的PCB之间啥关系,应该到进程那一章节就能解决了吧
线程是为了让程序并发提速而设计出来的概念,在本章的理解上,有PCB结构的执行流,就是线程,我们创建一个线程去执行函数,就需要先给它创建PCB,写上线程相关信息,然后存入队列准备执行
线程之间的PCB通过PCB中的两个tag成员来连接,这两个tag是链表节点,通过链表节点地址在PCB结构内,而PCB占据一个自然页,所以可通过链表节点直接获取到PCB的地址
线程的启动巧用ret指令进行跳转执行,因为线程是独立执行的代码块,不需要返回,也不需要返回值,多线程之间的通信会在之后讲到
使用轮询的方法进行调度
系统通过计时器中断实现任务调度,在计时器中断处理程序中获取当前PCB信息,判断其中的时间片是否用完,没用完就减一等下一次中断继续判断,时间片用完后,根据用完时的状态来将该任务放入不同的地方,如果在时间片用完的时候处于RUNNING状态,则将该线程放到就绪队列中,并将状态改外READY然后重置时间片
到这里,一个线程的执行结束了,下一步该进行线程切换了,线程切换需要将原来线程的寄存器都保存下来,然后将要切换的栈的寄存器都还原回去,修改完毕后,CPU 就会开始执行下一个线程了
总之,多线程的实现是通过为每个线程创建PCB结构,来存储PCB信息,通过时钟中断的中断处理程序获取PCB的信息,通过PCB的信息来判断是否切换,进行切换操作则需要保存/还原线程上下文环境。从而实现伪并发
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。