【README】
1.本文内容总结自 B站 《操作系统-哈工大李治军老师》,内容非常棒,墙裂推荐;
2.为什么要讲线程呢。实际要讲进程的切换;进程的切换包括切换指令,切换资源;切换指令就是切换线程(简单讲,线程就是程序指令,线程的切换就是程序指令的切换);
- 进程必须在内核中,切换进程实际上是切换内核级线程,而不是用户级线程;
- 但切换用户级线程是切换内核级线程的一部分;
2)为什么没有用户级进程?
- 因为进程要分配资源,如访问内存,所以必须进入内核态才能访问这些资源;
【1】开始核心级线程
1) 多核:多个物理cpu,但共用同一个缓存,同一个MMU内存映射单元;
2)多处理器:多个物理cpu,每个cpu有自己的缓存,自己的MMU内存映射单元;
【补充1】内存映射单元
- 内存逻辑地址 与 内存物理地址 映射表;
【补充2】并发与并行区别
- 并发:交替执行(单核运行多个线程,相互有影响);
- 并行:同时执行(多个线程在多个物理核上运行,可以理解一个核运行一个线程,相互独立不干扰);
3)用户级线程与内核级线程
- 用户级线程(缺点):操作系统看不到,操作系统无法分配硬件,没有发挥出多核价值;
- 多进程:也无法发挥多核价值,因为只有一套MMU,无法做到多个进程同时执行;
- 内核级线程(优点): 操作系统可以看到,为每个线程分配一个cpu,发挥出多核价值,多个内核线程可以同时执行(并行执行);
【1.1】内核级线程原理
1)要有核心级线程,需要既在用户态(用户栈)运行,也在内核态(内核栈)运行;
所以 每个内核级线程需要一套栈(包括用户栈,内核栈),而不一个栈;
即 内核线程切换,需要TCB切换一套栈,切换用户栈和内核栈;
【1.2】用户栈与内核栈
1)用户栈与内核栈组成同一个线程的一套栈;
2)INT 0x80 中断指令:使得 进程(线程)从用户态切换到内核态;
3)进入内核态前,把线程的用户栈信息(元数据)压入到内核栈,即把同一个线程的用户栈与内核栈关联起来,如下表所示:
内核栈(栈元素) | 含义(对应用户栈的寄存器值) |
源SS | 用户态的ss寄存器值; ss 指的是堆栈段寄存器;存放栈的段基址;内存是分段使用的; |
源SP | 用户态的栈指针寄存器值; sp指的是堆栈指针寄存器;存放栈的偏移地址; 说的直白点, 栈基址(栈起始内存地址)=段基址左移n位 加上偏移地址;如n取4 ; |
EFLAGS | 用户态的标志寄存器值; |
源PC | 用户态的pc寄存器值; pc=程序计数器寄存器; |
源CS | 用户态的cs寄存器值;cs=代码段寄存器; |
4)用户栈与内核栈的关联:
5) IRET (从内核态切换到用户态)
- 出栈:把内存中内核栈的5个寄存器值弹出到cpu的寄存器中,回到用户态;
6)用户态切换到内核态的步骤
6.1) sys_read() :启动磁盘读,把自己变成阻塞状态(等待磁盘控制器响应时,当前线程阻塞,cpu需要切换运行其他线程);
6.2)switch_to(cur, next)
切换到下一个内核线程; cur表示当前线程tcb,next表示下一个线程tcb;具体过程:
- 通过线程tcb找到内核栈指针;
- 通过ret切换到某个内核程序;
- 用cs:pc 切换到用户程序;
上图中, 线程S调用函数A, 线程T调用函数C;
- PC=?? 表示 线程T的用户态代码;
- CS=?? 表示 线程T的用户态代码;
- ???? 表示 iret指令,从中断返回;
6.3)内核线程切换 switch_to 实际上是对两套栈的切换;每套栈包含用户栈和内核栈;
【2】内核线程切换switch_to 五段论(*非常重要)
1)五段论步骤
步骤 | 描述 |
1 | 用户态1切换到内核态1; 把用户态1的物理寄存器的值保存到内核栈1;(完成用户栈切换到内核栈) |
2 | 执行读磁盘等中断操作;触发中断; |
3 | 因为中断,调用 switch_to 切换到其他内核线程,如线程2; |
4 | Switch_to函数找到内核线程2的tcb,通过tcb找到内核线程2的内核栈2; 切换到内核栈2(完成内核栈间的切换); |
5 | 通过 iret 把内核栈2中保存的用户态2的寄存器值弹出到物理寄存器; 即 ss:sp, cs:ip 通过内核栈2的用户态寄存器值赋值,从而完成从内核态2切换到用户态2;(完成内核栈切换到用户栈) |
用户线程与内核线程:用户线程与内核线程实际上同一个线程;
- 当操作的内存空间在用户态,则是用户线程;
- 当操作的内存空间在内核态,则是内核线程;
2)附加段:进程切换(S,T);
- 进程切换还需要切换映射表;
3)创建线程代码细节 (tcb 是对线程的结构体抽象)
void threadCreate() { TCB tcb = get_free_page(); // 申请一段内存作为tcb; *krlstatck = …; // 申请一段内存作为内核栈; *userstack 传入; // 用户栈 填写两个stack; // 内核栈与用户栈初始化 tcb.esp = krlstack; // tcb 关联内核栈 tcb.状态=就绪;// 状态为就绪 tcb入队; } |
4)用户级线程与核心级线程对比
用户灵活性, 用户线程大于核心线程;
原因:
- 用户线程的调度,开发人员可以自己编写调度策略进行控制,如调用 yield让出cpu;
- 而内核级线程无法修改调度,即内核线程调度策略是操作系统写死的,无法修改;