文章目录
进程调度原理
进程调度程序:分配有限处理器时间资源
抢占式多任务 preemptive multitasking
Linux 2.6之前O(1)调度器 使用静态时间片算法和针对每一处理器的运行队列
Linux 2.6 后 为了提高交互程序的调度性能,RSDL反转楼梯最后期限调度算法。 此刻称为 完全公平调度算法CFS
- I/O消耗型: 常常处在提交或等待IO请求。可运行态,运行时间短
- 处理器消耗型:执行代码,不停地在运行,调度策略倾向于降低调度频率而延长运行时间
调度算法要在响应时间短和高吞吐量之间找平衡
进程优先级
Linux倾向于IO消耗型
Linux 优先级范围:
- nice值:[-20,19] 默认为0 越小越优先
ps -ef
- 实时优先级: [0,99] 越高越优先 任何实时进程的优先级都高于普通进程
ps -eo state, pid, uid, ppid, rtprio, time, comm
在RTPRIO列“-”则不是实时进程 (Mac os 下是 pri)
time slice 时间片表示进程在被抢占前所能持续运行的时间
对于IO消耗型进程不需要长时间片,而处理器消耗型希望长。一般处理器默认10ms
- Linux 的CFS调度器并不直接分配时间片,而是分配使用比给进程。使用比进一步受进程nice值影响。nice作为权重调整进程所使用的处理器时间使用比。高nice值,低权重,损失一小部分处理器使用比。
- Linux抢占式系统,进程进入可运行态就被准许投入运行,是否将一个进程立刻投入运行(抢占当前进程)完全由进程优先级和是否有时间片决定。 Linux中抢占时机取决于新的可运行程序消耗了多少处理器使用比,若比当前进程小,则立刻投入运行。
Linux调度算法
Linux调度器以模块方式提供,使得不同类型的进程可以有针对性地选择调度算法,模块化结构称为调度器类。scheduler classes
允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。 基础调度器算法kernel/sched.c
按照优先级顺序遍历调度类,选择最高优先级可执行进程。
- CFS是针对普通进程的调度类Linux称为SCHED_NORMAL。算法定义在
kernel/shed_fair.c
Unix系统中的进程调度
进程优先级和时间片。 进程一旦启动就会有一个默认的时间片。更高优先级运行得更加频繁,赋予更多的时间片。Unix系统中时间片以nice值形式输出到用户空间。现实中会遇到如下问题
-
- nice值映射到时间片,nice单位值对应处理器的绝对时间。进程切换无法最优化
nice | 0 | 5 | |
---|---|---|---|
时间片 | 100ms | 5ms | |
占用处理器时间 | 100/105 | 5/105 | 105ms上下文切换一次 |
5 | 5 | ||
5ms | 5ms | ||
1/2 | 1/2 | 10ms上下文切换两次 |
增加上下文切换次数
高nice值往往是后台进程且计算密集型。普通优先级的进程则更多是前台用户任务。与设计背道而驰
-
- 相对nice值,及时间片映射
nice | 0 | 1 | |
---|---|---|---|
时间片 | 100ms | 95ms | |
占用处理器时间 | 100/195 | 95/195 | 时间片差别不大 |
18 | 19 | ||
10ms | 5ms | ||
10/15 | 5/15 | 前者比后者多两倍处理器时间 |
“把进程nice值减小”取得的效果极大的取决于其nice值的初始值
解决方式:可以把nice几何的增加,抵消比例问题。
-
- 时间片与系统定时器节拍。 整数倍10ms 1ms 的倍数
-
- 基于优先级的调度器为了优化交互任务而唤醒相关进程的问题。 会打破公平原则获得更多的处理器时间
实质性问题:分配的绝对的时间片引发的固定的切换频率
公平调度
-
CFS采用的方法是:完全摒弃时间片而是分配给进程一个处理器使用比重。
允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程。nice值作为获得处理器运行比的权重。 -
CFS为完美多任务中的无限小调度周期的近似值设立了一个目标,这个目标称为目标延迟
越小的调度周期将带来越好的交互性,更接近完美的多任务。同时承担更高的切换代价和更差的系统总吞吐能力。
每个进程时间片底线称为最小粒度 1ms
nice | 0 | 5 |
---|---|---|
时间片权重 | nice(0) | 1/3 nice(0) |
目标延迟 | 20ms | |
占用处理器时间 | 15ms | 5ms |
nice(10) | nice(15) | |
nice(10) | 1/3 nice(10) | |
15ms | 5ms |
Linux调度的实现
- 时间记账
- 进程选择
- 调度器入口
- 睡眠和唤醒
时间记账
多数Unix系统分配一个时间片给每一个进程,每次系统时钟节拍发生时,时间片会被减少一个节拍周期。当进程的时间片减到0,会被另一个未到0的可运行进程抢占。
- 调度器实体结构
<linux/sched.h> struct_sched_entity
调度器实体结构作为一个名为se成员变量嵌在task_struct 内 - 虚拟实时
vruntime虚拟运行时间:花在运行上的时间和,该计算经过所有可运行进程总数加权。vruntime与节拍器不再相关。
完美多任务:优先级相同的所有进程的虚拟时间相同,所有任务都将收到相等的处理器份额。
CFS使用vruntime记录每一个程序到底运行了多长时间以及它还应该再运行多久。 此记账功能存放在kernel/shced_fair.c update_curr()
update_curr()由系统定时器周期性调用,计算了当前进程的执行时间delta_exec。传给__update_curr(),它根据当前可运行进程总数对运行时间进行加权,最终将权重值与当前运行进程的vruntime相加。 vruntime可以准确测量给定进程运行时间,还可以知道下一个被运行的进程
进程选择
- 挑选下一个任务: CFS选择vruntime最小的任务,用红黑树rbtree存储。运行rbtree树种最左叶子节点所代表的那个进程。
kernel/sched_fair.c __pick_next_entity()
此函数不主要负责查找,最左叶子节点已存。 若无可运行进程,CFS选择idle任务运行。 - 向树中加入进程: 发生在进程变为可运行态或fork() 调用第一次创建进程时。 enqueue_entity() 更新运行时间和其他统计数据。它调用__enqueue_entity() 插入数据。其中leftmost是记录最左节点的更新,新节点有一次右移leftmost=0 否则为1 ,更新缓存rb_leftmost指向被插入的进程。
- 向树中删除进程: 发生在不可运行态或终止态 dequeue_entity() 调用__dequeue_entity()实操。若删除最左节点则rb_next()遍历找下一个节点,更新。
调度器入口
shedule() kernel/sched.c
选择哪个进程可运行,何时运行。 调用优先级最高的调度类,该调度类有自己的可运行队列,调度类选择下一个运行进程。schedule() - pick_next_task()(返回下一个可运行进程的指针) - _pick_next_entity() 选择高优先级的调度类 - 高优先级的进程
睡眠和唤醒
休眠:加入等待队列 add_wait_queue() -> 标记TASK_INTERRUPTBLE/ UNINTERRUPTIBLE -> 移除可执行rbtree deactivate_task()-> prepare_to_wait() -> schedule()其他进程
唤醒: 收到信号 -> 设置TASK_RUNNING -> finish_wait() -> 加入可执行rbtree activate_task() -> schedule() -> remove_wait-queue()
- 等待队列: 简单链表,内核中用wake_queue_head_t 代表。 由DECLARE_WAITQUEUE()静态创建、init_wait_queue_head()动态创建
休眠时,while循环等待条件发生,
- 定义宏DEFINE_WAIT()创建一个等待队列的项
- add_wait_queue() 加入队列。事件发生,wake_up()队列
- prepare_to_wait() 变更状态 TASK_UNITERRUPTIBLE/ TASK_INTERRUPTBLE
- TASK_INTERRUPTIBLE 收到信号伪唤醒,检查信号
- 唤醒。再次检查条件为真,是则退出等待循环,否则schedule()
- TASK_RUNNING finish_wait()出列
抢占和上下文切换
上下文切换context_switch() kernel/sched.c
-> schedule()
context_switch()->switch_mm() <asm/mmu_context.h>
将虚拟内存从上一个进程映射到新进程 -> switch_to() <asm/system.h>
从上一个进程的处理器状态切换到新进程处理器状态。保存、恢复栈信息和寄存器信息以及体系结构状态信息,以每个进程为对象进行管理和保存。
进程应被抢占时 scheduler_tick()设置need_resched标志
优先级高的进程可执行时 try_to_wake_up()设置need_reched标志
返回用户空间、中断返回时 内核也会检查need_resched
每当有进程可执行(或者状态改变)时都有函数检查need_resched标志
用户抢占
- 从系统调用返回用户空间时
- 从中断处理程序返回用户空间时
返回路径在entry.S 汇编语言实现。 文件包含内核入口以及内核退出部分代码
内核抢占
不具备抢占的内核各任务以协作方式调度,内核代码一直执行到完成(返回用户空间)或明显的阻塞为止;
抢占式内核只要重新调度是安全的,内核就可以任何时间抢占
只要没有持有锁,内核就可以抢占
在进程的thread_info中引入preempt_count计数器初始为0 持有一个锁+1 释放锁-1
need_resched=1 preempt_count=0 可抢占
need_resched=1 preempt_count>0 不可抢占
当时preempt_count释放锁=0时 检查need_reched
- 中断处理程序正在执行,且返回内核空间之前
- 内核代码再次具备可抢占性时
- 内核显式调用schedule()
- 内核任务阻塞 也会调用schedule()
实时调度策略
kernel/sched_rt.c
- SCHED_FIFO 无时间片,只受优先级控制
- SCHED_RR 有时间片的FIFO 优先级高的时间片耗尽再运行下一个
以上两种的静态优先级,不互相抢占 [0 , MAX_RT_PRIO -1 ] [0,99]
SCHED_NORMAL 普通非实时 nice[MAX_RT_PRIO, MAX_RT_PRIO+40) [100,139]对应[-20 , 19]
与调度有关的系统调用
与调度策略和优先级相关的系统调用
sched_setscheduler()
sched_getscheduler()改写task_struct的policy和rt_priority
sched_setparam() 实时优先级
sched_getparam() sched_param结构体的rt_priority
sched_get_priority_max()
sched_get_priority_min() 返回给定调度策略最大最小优先级 实时调度策略的优先级[1, MAX_USER_RT_PRIO -1]
对于普通进程nice()给静态优先级增加一个给定的量。超级用户可以使其负值。nice()调用set_user_nice()设置进程的task_struct的static_prio和prio
与处理器绑定有关的系统调度
processor affinity 亲和性 使进程尽量在一个处理器上运行,也可强制指定task_struct 的cpus_allowed 位掩码标志。每位对应一个系统可用的处理器。默认所有位都被设置。 sched_setaffinity()设置不同的一个或几个位组合的位掩码。sched_getaffinity()返回cpus_allowed位掩码
- 线程在刚创建时,继承父进程的掩码,使用父进程的处理器。当处理器绑定关系改变时“移植线程”任务推到合法的处理器。
放弃处理器时间
sched_yield() 活动队列 -> 过期队列(一段时间内不会再被运行)
实时进程不会过期,-> 优先级队列最后面
内核yield() 用户空间sched_yield()