六、线程的代码实现:pcb栈、线程栈、PCB初始化、中断处理函数、调度函数->多线程调度
来源:互联网 发布:p城办事处优化 编辑:程序博客网 时间:2024/06/06 00:23
进程与线程的关系和区别
进程、线程的状态
PCB
实现线程的两种方式--内核或用户进程
中断栈
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;};
线程栈
struct thread_stack { uint32_t ebp; uint32_t ebx; uint32_t edi; uint32_t esi;/* 线程第一次执行时,eip指向待调用的函数kernel_thread 其它时候,eip是指向上次被换下时执行到的地址*/ void (*eip) (thread_func* func, void* func_arg);/***** 以下仅供第一次被调度上cpu时使用 ****//* 参数unused_ret只为占位置充数为返回地址 */ void (*unused_retaddr); thread_func* function; // 由Kernel_thread所调用的函数名 void* func_arg; // 由Kernel_thread所调用的函数所需的参数};我们通过 kernel_thread () 函数去调用线程的函数。
/* 由kernel_thread去执行function(func_arg) */static void kernel_thread(thread_func* function, void* func_arg) {/* 执行function前要开中断,避免后面的时钟中断被屏蔽,而无法调度其它线程 */ intr_enable(); function(func_arg); }
PCB栈
/* 进程或线程的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; //该线程总共执行的时间/* general_tag的作用是用于 就绪态线程队列 中的结点 */ struct list_elem general_tag; /* all_list_tag的作用是用于 所有线程队列thread_all_list 中的结点 */ struct list_elem all_list_tag; uint32_t* pgdir; // 进程自己页表的虚拟地址 uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出};
最后PCB这个4K的结构为:
多线程实现
第一步:初始化pcb栈
程序都是从 main 函数开始执行,所以都有一个主线程PCB,PCB都为4K,包括了中断栈、线程栈、普通栈、PCB栈。我们首先要初始化主线程PCB。因为main线程首先运行,咱们在loader.S中进入内核时的mov esp,0xc009f000,就是为其预留了tcb,地址为 0xc009e000,因此不需要通过 get_kernel_page 另分配一页。我们初始化PCB栈即可,不用初始化线程栈,因为我们一开始就运行了 main主线程 。所以我们不需要初始化线程栈。中断栈、线程栈、pcb栈这些只是数据结构,本来里面的元素在PCB 4K 空间中都存在的,只是定义了这些结构后,我们方便赋值了而已,所以main 主线程虽然没有初始化 线程栈,但是循环起来执行时候,还是和其他的普通线程一样,存在线程栈的,只是main 线程没有初始化而已。所以程序开始执行时候先初始化主线程栈,把主线程的状态改为 运行态,并将链表元素加入 所有线程队列中。
对于其他普通线程来说,初始化 pcb栈 时,将 self_kstack 初始化为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函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */ 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 = 0x19930504; // 自定义的魔数}
第二步:初始化线程栈
线程栈中记录的是这个线程的函数和上次执行到的代码地址。
/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) { /* 先预留中断使用栈的空间,此时值为 intr_stack 底部的地址值*/ pthread->self_kstack -= sizeof(struct intr_stack); /* 再留出线程栈空间 ,此时值为thread_stack 底部的地址值 */ pthread->self_kstack -= sizeof(struct thread_stack); struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; kthread_stack->eip = kernel_thread; //eip的值为 调用函数kernel_stack 的地址 kthread_stack->function = function; kthread_stack->func_arg = func_arg; kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;}
eip的值为 kernel_thread()函数的地址值,我们通过 void kernel_thread(thread_func* function, void* func_arg);从而去执行 function(func_arg);这个函数。所以这就是这个栈中占位符的作用,当esp 指向 eip 时候,我们是通过 ret 弹出去。使用 ret 制造了调用的假象,于是开始执行 kernel_thread 函数。函数本质就是一段代码段,从汇编角度来说:
static void kernel_thread(thread_func* function, void* func_arg) { intr_enable(); function(func_arg); }
这是一个被调函数,它的参数都是被主调函数压入栈中的,所以 被调函数的代码都是写好固定的,不管哪个主调函数调用它,都是这样的代码:需要使用参数时,直接去当前栈指针的上面找。
当主调函数调用被调函数时:先压栈,然后 call kernel_thread 开始执行:
kernel_thread:call intr_enablepush [esp+8]call [esp+4]
因为当前 esp 经过 ret 后跳过了 eip,指向 unused addr,所以执行这个函数体时候找参数时候就会跳过这个占位符,因此代码就可以顺利执行,所以我们的占位符只是占了位,并不给它赋值。
第三步:分配页表(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); 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;}
第四步:创建时钟中断处理程序
我们利用时钟中断来计算各个线程的时间,从而实现线程的调度。每发生一次时间中断,我们会在时间中断函数中处理当前线程的时间片,如果当前线程的时间片到了时间,则会调用 调度函数 ,从而更换线程。因此我们需要设置时钟中断函数。之前我们设置的中断函数都是通用的中断函数,我们的中断函数地址数组中的地址都是通用中断处理函数的地址。当我们写好时钟中断处理函数,只要给把函数地址(函数名)储存到对应的中断函数地址数组项中即可。
/* 时钟的中断处理函数 */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--; }}
第五步:创建调度函数
当时间片到了,调度函数将会换下当前的线程,换上下一个 就绪态 的线程。调度函数将备份当前线程的寄存器以变下次恢复时候用。然后从就绪态线程队列中弹出第一个就绪态线程。并将当前线程压入到 就绪态 队列的最后。
schedule()函数功能:将当前进程换下,选择出下一个进程
switch_to(,)函数功能:备份当前指针的寄存器,将上一个指针换上处理器。
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;}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);//将链表元素指针转换为pcb栈指针,原理就是利用链表u元素在pcb栈中的偏移量next->status = TASK_RUNNING;switch_to(cur, next);}
section .textglobal switch_toswitch_to: ;栈中此处是返回地址 push esi push edi push ebx push ebp注意压栈将这些都压入了 cur 的栈中 mov eax, [esp + 20] ;此时的地址是压入参数:cur的值,也代表的是cur线程的pcb栈中的第一个元素self_kstack mov [eax], esp ;把cur线程的指针存入self_kstack字段中,下次该线程换上处理器时候用 ;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ---------------- mov eax, [esp + 24] ; 得到栈中的参数next,也代表的是next线程的pcb栈中的第一个元素self_kstack mov esp, [eax] ; 将next线程的pcb的self_kstack储存的是上次被换下时候的esp值 pop ebp;此时esp 指向了next 的PCB的栈中,所以此时弹出的是 next 的栈 pop ebx pop edi pop esi ret
特别注意:此时 eip 已经指向了next 的PCB中,所以 ret 返回的将是 next 的 PCB 中的值:ret 将 此时 esp 指向的值 加到 eip 中,并且 加4,然后eip 开始执行起来。
可以看到两个链表队列将各个PCB链接起来:我们从就绪态链表队列中弹出线程。
函数调用规则ABI
在我们的压栈时候,可能会看到压入了寄存器。ABI规则:5个寄存器ebp、ebx、edi、esi、esp这5个寄存器是归主调函数所有,其余寄存器是归被调函数所有,也就是说:在调用函数完后,主调函数所拥有的这个5个寄存器的值不能变。所以被调函数要负责保护好这5个寄存器的值。
- 六、线程的代码实现:pcb栈、线程栈、PCB初始化、中断处理函数、调度函数->多线程调度
- linux线程调度函数调用
- Java多线程-线程的调度
- 多线程中的线程调度和常用函数说明
- Java多线程-线程的调度(守护线程)
- java多线程:线程的调度-守护线程
- Java多线程 -- 线程的调度-守护线程
- Java多线程-线程的调度(守护线程)
- 多线程学习(六)线程的调度-让步&合并&守护线程
- Java线程中断处理函数
- 一个调度线程suspend的nanosleep函数介绍
- Java多线程 线程的调度(休眠)
- Java多线程-线程的调度(合并)
- Java多线程-线程的调度(让步)
- Java多线程-线程的调度(优先级)
- java多线程: 线程的调度-休眠
- Java多线程:线程的调度-优先级
- java多线程:线程的调度-让步
- [Unity][Android]安卓打包Build Identifier的问题
- user_tables 的信息依赖于统计信息
- java字符串常用操作(String 类)
- add two numbersⅡ
- es6笔记·····ArrayBuffer
- 六、线程的代码实现:pcb栈、线程栈、PCB初始化、中断处理函数、调度函数->多线程调度
- java.util.Collections.copy()方法注意点
- uva 242 DP 求s张邮票可以连续组合成最大面值多少的邮票
- xz文件解压缩
- 技术动态 | 清华大学开源OpenKE:知识表示学习平台
- expected at least 1 bean which qualifies as autowire candidate.错误
- 关于原码、反码与补码
- rward和redirect的区别
- 2017 ICPC青岛站体验