| 多任务
|
|
|
|
| 单任务cpu,多任务并发的基本原理
|
| 对时间资源分片,分时复用
|
| 单cpu只有并发,没有并行
|
| 每个task分多少时间,看调度策略
|
| 任务切换,需要保存现场,重新调度可以恢复现场
|
| 任务无感知
|
|
|
| 任务调度基本机制
|
| current是当前任务
|
| ready queue里保存即将运行的task
|
| 然后随时switch
|
|
|
| 任务状态
|
| 当前任务有runing,如果被抢占,进入Ready
|
| 如果等待资源可以用,使用wait进入blocked状态,需要主动唤醒
|
| 子任务结束,不会完全释放,linux需要父任务清理,
|
| 这里设计一个专门的系统任务gc来负责清理
|
|
|
数据结构TaskInner
| idle
|
是标记系统任务,别人都没工作,它会工作
|
| init
|
就是主线程
|
| entry
|
任务逻辑函数入口
| 可以认为这里所有并发的都是线程,没有进程的概念,因为就一个地址空间
|
|
| state
|
就是4个状态
|
| kstack
|
每个线程有自己的stack
| 调度是需要切换的上下文ctx
|
| 包含一系列寄存器状态,通过状态的保存恢复来完成调度
|
|
| task_ext
|
任务扩展属性,用于向宏内核和Hypervisor扩展,这里是空
|
|
|
调度整体框架
| 支持协作式和抢占式调度通用框架
|
在这三里调来调去
| 当前任务,
|
| run_queue,
|
| 并列的wait_queue,
|
|
| 还有定时器,比较关键
|
|
|
|
|
|
|
|
|
|
api
| spawn创建新任务
|
| yield_now主动让出cpu执行权,
|
| 称为协作式
|
|
|
调度框架初始化
| run_queue的初始化,
|
| 初始化idle,main,gc
|
| 初始化定时器
|
|
|
三个系统任务
| main
|
没有多任务就这一个
|
| 其他子线程gc回收
|
|
| idle
|
| 所有任务都阻塞,没有ready
|
| 对特定arch(体系结构),会等待中断irq,或其他非忙等指令
|
|
|
|
| 小结
|
|
任务如何切换
| 对任务上下文context进行切换
|
任务状态的最小寄存器集合
|
| 从两个角度看
|
左边
| 包含ra
|
函数调用返回地址
|
对应于执行流
|
| sp
|
多任务之间关键就是换栈
|
|
| s0-s11
|
体系结构要求必须保存
|
|
|
| context_switch
|
| 进入的时候是current_task,
|
| 返回的时候从另一个任务返回next-task
|
|
|
|
内部实现
| 保存当前任务状态
|
| 所有寄存器
|
| 把下一个任务状态恢复出来
|
|
|
协作式调度
| 如果完全协作式调度
|
| 某个不让出或延迟让出
|
| 其他任务会饥饿
|
|
|
| 协作式算法FIFO
|
|
具体实现
| 实现trait Base Scheduler
|
| 重要的是pick_next_task
|
| 和put_prev_task
|
|
|
同步互斥
| 单cpu,只需要实现上面两层,
|
| 只要关中断,就没有其他抢占
|
|
|
|
| 多cpu需要一个争夺临界区特殊变量,
|
| 一个原子变量
|
|
|
|
互斥锁
| 一块是自旋锁
|
简化版可以用一个原子变量代替
|
| 一块等待队列
|
|
|
|
内存分配算法
| tlsf
|
| 两级bitmap加链表
|
| bitmap第一级,每个bit对应一个内存块
|
| 图右上角,1代表空闲,0占用,这里有两个1,2^15和2^6有空闲块
|
|
| 第二级,继续去找哪块空闲
|
| 有几个bit,对块几等分,这里是4等分
|
| 再指向一个链表头
|
|
|
|
|
| linux用来管理page的一个传统核心算法
|
| 这里用来作为balloc字节分配器使用
|
| order用来表示块大小
|
| 每一级不同大小,上一级是下一级两倍
|
|
|
|
|
| 比较好解决内存碎片化问题
|
|
| Slab
|
| 在linux里跟buddy配合的
|
| 用来做page的上一级,就是缓存的这一级的管理
|
| 也用在字节分配器balloc
|
| 也有一个order list
|
| 指向一个slab管理结构体
|
| 核心结构也是一个空闲链表,包含所有空闲块
|
| 刚开始初始化,从page里申请出来,根据单元大小切分
|
| 不足的时候继续按照buddy申请过程来
|
| 如果链表里块比较多,可以合并再放回去
|
|
|
|
抢占式调度概念
| 必须基于系统定时器来打断当前任务执行
|
| 抢占针对目标就是current task
|
|
| 不是无条件抢占
|
|
| 内部条件
|
比如时间片耗尽
|
| 外部条件
|
通过锁,保证临界区不抢占
|
|
|
|
抢占发生条件
| 内部条件
|
每次中断,对时间片计数递减,耗尽,设置pending标志,表示可以抢占
|
| 外部条件
|
设置临界区的控制标志
| 一个是自旋锁
|
| 自旋锁有两部分能力
|
| 保存中断状态,关中断
|
| 可以关抢占no preempt
|
|
| 单独关闭抢占
|
|
| 这俩可叠加
|
有点像信号量
|
|
|
| 内外条件都具备的时候,可以抢占
|
|
|
抢占与协作式的主要区别
| 需要引入平台时钟中断
|
| 需要外部抢占控制,自旋锁+guard变量
|
|
|
时钟中断与抢占式调度细节
| 注册时钟中断响应函数
|
|
| 中断内部会先检测事件列表
|
check_events
|
| RUN_QUEUE是跟当前task调度相关的
|
| 里面检查task_tick看内部条件是否满足
|
| 满足就可以pending了
|
|
|
|
抢占式调度算法
| rr算法
|
| 简单,定时器递减,到了就pending
|
| 同时如果外部条件允许抢占,处于禁用到启用的边沿上,可以进行抢占
|
| 仍然可以使用yield自己主动让出
|
|
| 实现
|
| 保持一个时间片成员time_slice
|
| 循环列表,使用双端可操作队列VecQueue
|
| 如果时间片没耗尽,放回queue脑袋,下次继续提出这个
|
| 耗尽了,放回队尾,等待调度
|
| task_tick,就是处理递减
|
| <=1的时候可以被抢占了
|
|
|
| CFS最常用的公平调度算法
|
| vruntime最小的任务是优先级最高的任务
|
| delta是可变的,其他固定
|
| delta跟时钟中断相关
|
| 每次触发,只针对当前这个任务操作
|
| 对delta进行递加操作
|
| vruntime向上走,超过其他的之后,它不是最小,会触发其他的抢占
|
|
| 实现
|
| 基于btreemap运行队列
|
可以当成一个有序队列
|
队首最小
|
| 插入进去的时候需要重新计算此时的runtime
|
使用get_vruntime
|
因为当前任务执行后 runtime一定会发生变化
|
| 取下个任务直接从有序队列取第一个
|
|
|
| 存在当前任务放进去后仍然队首,拿出来还是他
|
|
|
| task_tick,第一次运行的时候允许进行一次调度,min_vruntime.is_none
|
|
|
| 再就是计算当前任务的vruntime是不是比min_vruntime大,说明有其他的可以调度,触发重新调度
|
|
|
|
| 具体数据结构
|
| init_vruntime决定一开始是否容易被调度
|
后面影响变小
|
|
| nice跟优先级相关,决定delta的变化速度,nice高,变化慢,优先级高
|
|
|
|
|
|
|
| 总结
|
|
|
Generated
2025-05-05 04:17:35 +0000
25bcfca-dirty
2025-05-04 14:47:56 +0000