文章目录
进程调度程序可看作是: 可运行态进程之间 分配有限的处理器时间资源 的内核子系统。
在一组处于可运行状态的程序中选择一个来执行,是调度程序的基本工作。
目标是: 在进程响应时间 和 最大系统利用率(吞吐量高) 两个矛盾中寻找平衡。
有如下调度算法:O(1)调度算法 [早期];反转楼梯最后期限调度算法(RSDL),完全公平调度算法(CFS, Completely Fair Scheduler),这俩是一个东西。
策略
进程可以分为I/O消耗性 和 处理器消耗性。
I/O消耗性:经常处于可运行状态,运行一会儿等待IO请求阻塞。如键盘输入,网络IO,用户GUI程序(IO密集型)。
处理器消耗性:一直不停运转不需要太多IO需求。调度策略是尽量降低调度频率、延长运行时间。如大量数学运算程序。
不完全是明确的两类,如字处理器,X Windows服务器。
进程优先级
Linux采用两种不同的优先级范围。普通任务nice值和实时优先级。
nice值:-20~+19,默认为0,值越低优先级越高获得越多处理器时间。
实时优先级:0~99,可配置,值越高优先级越高。
任何实时进程的优先级都高于普通进程,也就是说实时优先级和nice优先级处于互不相交的范畴。
实际上,内核看到的任务优先级和用户看到的并不相同。Linux 内核中使用 0~139 表示任务的优先级,并且,值越小,优先级越高(注意和用户空间的区别)。其中 0~99 保留给实时进程,100~139(映射成 nice 值就是 -20~19)保留给普通进程。
ps -el
查看NI列为进程对应nice值。
查看进程的实时优先级,实时进程在RTPRIO列显示优先级,ps -eo state,uid,pid,ppid,rtprio,time,comm
,– 则不是实时进程。
时间片(timeslice)
时间片是一个数值,表示进程再被抢占前所能持续运行的时间。太长导致系统对交互的相应欠佳,太短导致系统进程切换带来大量处理器消耗。
Linux并没有直接分配时间片到进程,它是将处理器的使用比分给进程,也就是进程分的实际时间片跟系统负载密切相关。很多其他操作系统的默认时间片为10ms。
Linux调度算法
linux调度器是以模块的方式提供,目地是对于不同类型的进程可以有针对性的选择调度算法。该模块化结构被称为调度器类(scheduler classes)。
CFS是针对普通进程的调度类。获得的最小时间片称 最小粒度默认是1ms,不是算数加权是几何加权,分配得到的时间片不再是跟nice的绝对值直接相关,而是相对值处理器使用比。
Linux调度实现
包括了四个组成部分:
- 时间记账(Time Accounting)
- 进程选择(Process Selection)
- 调度器入口(The Scheduler Entry Point)
- 睡眠和唤醒(Sleeping and Waking Up)
1. 时间记账
在include\linux\sched.h中定义,调度器实体struct sched_entity
,成员vruntime
记录程序运行了多长时间还应该运行多长时间ns。update_curr()计算当前进程执行时间,由系统定时器周期调用。
update_curr()
2. 进程选择
CFS调度算法的核心是 选择具有最小vruntime的任务。
CFS采用红黑树来组织可运行的进程队列,节点键值是vruntime。
选择下一个要运行的任务:就是红黑树最左边叶子节点所代表的那个进程。若返回NULL则表示没有可运行的节点,CFS便选择idle任务运行。__pick_next_entity()
加入进程到树:进程变为可运行状态fork()。enqueue_entity()
删除进程从树中:dequeue_entity()
3. 调度器入口
统一的调度器入口schedule(),通常和一个具体的调度器类相关联,在kernel/sched/core.c中定义。找到一个最高优先级的调度类,调度类有自己的运行队列,知道谁是下一个该运行的进程。
pick_next_task()按优先级检查每一个调度类,选择高优先级进程。每个调度类也自己实现了pick_next_task()。
schedule()
pick_next_task()
class->pick_next_task()
pick_next_entity()
__pick_next_entity()
4. 睡眠和唤醒
休眠(被阻塞。INT、UNINT)的进程处于一个特殊的不可执行状态。为等待某一事件进入休眠。
内核干的事:进程把自己标记称休眠,从可执行红黑树中移除,放入等待队列;任何调用schedule()选择和执行一个其他进程。
唤醒: 过程刚好相反,进程被设置为可执行状态,然后从等待队列中移入可执行红黑树中。
等待队列: 由 等待某些事件的进程组成 的简单链表。TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE位于同一等待队列,等待事件不能运行。不同的是TASK_UNINTERRUPTIBLE的进程会忽略信号。
伪唤醒: (被唤醒不是因为等待的事件发生了)。进程状态被设置为TASK_INTERRUPTIBLE,则信号可以唤醒线程,因此进程检查并处理信号。
抢占和上下文切换
上下文切换: 从一个可执行进程切换到另一个可执行进程。由context_switch()函数负责处理,当一个进程被选为下一个运行的对象时,schedule()就会调用该函数。
context_switch()函数中主要调用两个函数,
- switch_mm()切换虚拟内存,
- switch_to()切换处理器状态,包括了栈信息、寄存器信息、与体系结构相关的状态信息。
schedule()
context_switch()
switch_mm()
switch_to()
need_resched标志
内核提供need_resched标志 表明是否需要重新执行一次调度。
每个进程都包含need_resched标志,当某个进程应该被抢占或者优先级高的进程进入可执行状态时,这个标志将被设置。内核会检查该标志确认被设置 并调用schedule()切换到新进程。
位置: need_resched标志在thread_info结构体里,在某个标志变量的某个位里边。
访问和操作: 用于访问和操作need_resched标志的函数有三个:
- set_tsk_need_reshed()
- clear_tsk_need_reshed()
- need_resched() // 检查标志值并返回真假
用户抢占
内核无论是在 中断处理程序 还是 在系统调用后 返回,都会检查need_resched标志,如果need_resched标志被设置会导致schedule()被调用,此时发生用户抢占。
中断处理程序或系统调用返回的路径跟具体的体系结构有关,在entry.S中实现。
用户抢占在以下情况产生:
- 从系统调用返回用户空间时。
- 从中断处理程序返回用户空间时。
内核抢占
其他大部分操作系统,调度程序无法在 内核级的任务正在执行时 重新调度,不具备抢占性。
对于Linux来说,只要重新调度是安全的(没有持有锁,锁是非抢占区域的标志),内核可以在任何时间抢占正在执行的任务。
每个进程的thread_info有preempt_count计数器,使用锁时加1,释放锁时减1。
内核抢占在以下情况产生:
- 中断处理程序正在执行,且返回内核空间之前。
- 内核代码再一次具有可抢占性时。
- 内核任务显示调用schedule()。
- 内核任务被阻塞,同样也会导致调用schedule()。
实时调度策略
SCHED_NORMAL, SCHED_BATCH 和 SCHED_IDLE 进程会映射到 fair_sched_class (由 CFS 实现);SCHED_RR 和 SCHED_FIFO 则映射的 rt_sched_class (实时调度器)。
实时任务的优先级是静态的。
SCHED_FIFO,简单先入先出,无时间片,一直执行直道被阻塞或显示释放处理器,同优先级则轮流执行(也就是当一个任务完成后,会被放到队列尾部等待下次执行),只有更高优先级的FIFO或者RR才能抢占。
SCHED_RR,实时轮流调度算法,带有时间片的SCHED_FIFO。默认的时间片是 100ms。
硬实时任务:会有严格的时间限制,任务必须在时限内完成。比如直升机的飞控系统。然而,Linux 本身并不支持硬实时任务,但是有一些基于它修改的版本,如 RTLinux(它们通常被称为 RTOS)则是支持硬实时调度的。
软实时任务:软实时任务其实也会有时间限制,但不是那么严格。也就是说,任务晚一点运行任务,并不会造成不可挽回的灾难性事故。例如,VOIP 软件会使用软实时保障的协议传来送音视频信号,但是即便因为操作系统负载过高,而产生一点延迟,也不会造成很大影响。
无论如何,软实时任务总会比普通任务的优先级更高。