还是先来问自己几个问题
- 什么是线程?
- 什么是进程?
- 用什么管理线程/进程?
- 如何进程线程调度(切换)?这个问题是我最感兴趣的
简单的问题就简单回答
什么是线程? 线程是一套机制,此机制可以为一般的代码创造它所依赖的上下文环境,从而让代码块具有独立性,能够单独获得处理器资源。
什么是进程? 进程是线程+资源
用什么标识进程? PCB:Process Control Block,程序控制块,用来记录此进程的相关信息,比如进程状态、PID、优先级等
1.线程处理相关数据结构
中断栈:中断发生时用来保存程序的上下文环境,中断退出时恢复程序的上下文环境。(结合kernel.S的中断代码学习)
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;
};
线程栈:有两个用途,1.线程创建时设置eip为kernel_thread。2.在switch_to调用时根据abi的要求保存线程的上下文环境(我觉得也不能叫线程上下文环境,应该是函数调用规范)
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所调用的函数所需的参数
};
task_struct: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;//用于线程在一般的队列中的节点 ready
struct list_elem all_list_tag;//用于线程在thread_all_list中的节点
uint32_t* pgdir;//进程自己页表的虚拟地址
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
2.线程环境初始化流程
在使用线程前需要准备一些必要的环境:创建两个链表保存就绪进程和全部的进程,并将当前程序流初始化为主线程
- 创建thread_ready_list(就绪队列)
- 创建thread_all_list(所有线程队列)
- make_main_thread:将当前运行流当作主线程(预留的esp=0xc009f000所在页为PCB)初始化线程并加入ready队列
3.线程初始化流程
thread_start函数创建线程时做了哪些工作?
- 获得一个内核页作为PCB
- init_thread:初始化线程结构体task_struct(PCB)
- thread_create:初始化线程栈thread_stack
- 将初始化的线程加入到thread_ready_list中,等待被调度上cpu执行
- 将初始化的线程加入到thread_all_list中
4.多线程调度/切换流程
线程调度才是这一章的核心并且和前面的中断联系紧密。多线程调度的基础是时钟中断。只有中断的时候调度程序才能获得对系统的控制权。
- 中断处理函数
- 获取当前线程的PCB
- 判断是否溢出(stack_magic被破坏)
- 线程占用时间(elapsed_ticks)+1
- 判断线程剩余时间片(ticks)是否大于0
- 剩余时间片小于0,进行线程调度
- 调度器schedule
- 获取当前线程的pcb
- 如果线程是因为时间片用完才替换的,需要重新设置ticks,并置status为TASK_READY,加入thread_ready_list队尾
- 从thread_ready_list中弹出下一个需要上cpu的线程next
- switch_to(cur,next)进行线程切换
- 任务切换函数switch_to
- 就干了一件事情,切换esp为next
所以到现在都没看出来什么时候换了线程?其实这里分为了三个部分进行,进入中断,中断处理,退出中断
进入中断(kernel.S):保存了中断发生时的上下文环境
中断处理(timer.c和interrupt.c和thread.c):调用中断处理函数intr_timer_handler(intr_timer_handler->schedule->switch_to),完成了esp的切换
中断退出(kernel.S):此时的esp为next线程的,退出中断会进行上下文环境的恢复,最后iret就能到next中执行,实现进程间的切换。
总结
线程切换和前面的中断密切相关,需要借助中断才能实现线程切换。
需要将kernel.S、interrupt.c、timer.c、thread.c结合起来才比较好理解。
本来想画图的,然后偷懒了。如果有好用的画数据结构和函数调用的工具,可以推荐一下。