背景
继上次讲完了什么是线程"等待"状态(即操作系统线程挂起状态),这里想再详细讲一讲,线程挂起的时候,背后具体会进行哪些操作
这里我们主要分三个部分来进行阐述
一 进程挂起具体执行逻辑
操作系统提供分层的权限机制,把区域分为 用户态 和 内核态
线程挂起的时候,其中核心逻辑为-用户态主动执行操作系统提供的schedule() 方法,从用户态进入内核态
1 用户态的寄存器信息据压入 内核栈
操作系统寄存器:
CPU 是真正干活的,它干活时用很多寄存器来存储它的控制信息,计算信息,以及数据信息 数据信息:通用寄存器 来存储计算过程中暂存数据
控制信息:
CS :代码段寄存器,代码在内存的初始位置
DS: 数据段的寄存器,初始数据等存储位置
SS: 栈寄存器,函数调用栈存储位置
IP:指令指针寄存器
ESP: 栈顶指针寄存器
代码段的偏移量在 IP 寄存器中,数据段的偏移量会放在 通用寄存器中
业务开发的时候,会经常碰到函数调用,这里详细解释一下用户态函数栈
用户态函数栈 SS:
栈帧如图:
组成机构:
1 上一个 栈帧的 ebp 地址
2 局部变量
3 调用方法所用的参数
4 返回地址
这里可以重点了解下:
假如 A 调用了 B,B 通过它的栈帧中的上一个栈帧的EBP地址,找到 A 传进来的参数,且当 B 运行完毕,返回值会保存在 EAX 寄存器中
把上述用户态所有寄存器信息全部压入 内核stack
内核栈 stack 数据结构:
如图:
pt_regs 里有当时用户态运行的所有上下文信息
内核 stack最关键的作用,其实就是用来保存此时这个用户态的上下文。且当执行完操作系统的调用函数时,可以从这里拿取信息恢复用户态上下文,继续运行上次停下的逻辑
2 从队列中选出继任者进程
调度任务策略:
先进先出
轮流调度
完全公平等策略
这里主要介绍,普通进程的安全公平策略
首先每一个任务都有一个 vruntime ,vruntime 代表可分配的运行时间。把任务想关信息封装成一个 scheduler_entity,再放进红黑树进行排序,调度时每次选出最小的 vruntime 任务,放入CPU去执行
CPU 运行时:
线程挂起的时候,从 CPU的相应的队列中,选出下一个任务
每一个 CPU都有一个rq ,rq 里分实时队列和 cfs 队列,实时进程任务信息放在 实时队列中,普通进程任务信息放在 cfs 队列中。
且在 task_struct 数据结构里,有对应的策略 rt,与策略Entity,这样可相互引用
如图:
3 切换 CPU 上下文以及进程空间
读取下一个任务的task_struct,切换成下一个选中任务的上下文以及进程空间
而刚刚运行的任务的相关寄存器信息,CPU自动存储到 TSS中
TSS:
每个 进程都有一个 TSS (Task State Segement,任务状态段),这里面有所有的寄存器
CPU 运行时。
还有一个 特殊的寄存器 TR (Task Register,任务寄存器),指向某个进程的 TSS,更改 TR的值时,将会触发硬件保存CPU所有寄存器的值到当前进程的 TSS中,然后从新进程的 TSS中读出所有的寄存器,加载到CPU对应的寄存器中
4 真正睡着
进入调度队列中,之前的任务没有了 CPU执行,只是变成一个 数据结构
5 等待调度,被唤醒
沉睡的任务等待被调度策略选中,读取 它的TSS 信息,然后获得CPU去运行,且从 schedule() 方法返回
其实也可以理解成,回到了 步骤 2 从队列中选出继任者进程
二 主动引发的挂起
主动调用操作系统提供的挂起的方法 - scheduler()
如:IO 操作的时候,让出 CPU
上一节文章提到的 所有挂起的方法,如 park(),sleep() 等
三 被动引发的挂起
被动挂起场景
1 调度策略
当前 CPU 运行中的进程,运行的时间太长,被 定时的 tick函数检测到需要休息
2 有优先级更高的任务
刚刚被唤醒的进程任务,优先级比当前运行的任务优先级高,则需要被换下来
注意⚠️:被动引发的挂起,并不立即执行,而是先打上 rescheduler 标签,等待时机执行 scheduler()函数
等待时机
1 从内核态返回 用户态时
2 从中断返回时
总结
一个简单的线程休息场景,在操作系统层面居然有这么多复杂的设计,细细咀嚼,别有一番风味,bingo!
附上一张简单的总结图: